外挂流程
这个外挂是一个典型的andlua程序,核心特征就是主要逻辑都在lua里面,外挂启动就会执行lua,由于lua被andlua官方加密了,所以外挂需要在内存中经过lua虚拟机加载函数时解密再运行,先判断了是否占用27042 和23946端口,以及/data/local/tmp下有没有re.frida.sever文件,进行反调试,然后申请root权限,创建一个随机文件名的空文件,同时将assert文件夹下的aes文件,进行rc4解密,解密后的文件字节流,写入前面创建的空文件中,并用root权限启动了这个随机文件名的文件进程,这个进程根据偏移修改了游戏数据,实现了飞天和人物移动加速的两个外挂功能。
外挂流程图
具体的分析思路(提一句,我是调试分析完再写的wp,里面的函数经过分析后确定功能之后被我重命名了。)
首先先查看外挂的结构
发现是一个andlua程序,同时lua被加密了,用010打开是这样的,所有的lua都是类型这种结构
assert文件夹下面有个aes文件不太一样,里面和lua的格式不太一样,怀疑是外挂会解密后,新进一个进程,但是ps -A 并没有看到叫aes的进程
andlua的核心在lua里面,所以需要先把lua解密出来,这里我绕了一些弯路,这里是还没看出来是andlua官方加密,自己去逆了很久的lua的虚拟机
在这里跟了很久,发现了解密的地方,同时为了避免麻烦,我hook j_lua_load这个函数,这里就出现了问题了,dump出来的是luas,用unluac反编译后,符号名出现丢失,代码逻辑是正常,但是没有符号可以说就是绝路,dump下来,是这样的
继续逆虚拟机,找了很久,似乎没法通过frida和ida dump的方式,将符号问题搞定,这里我搜了一些关于andlua的解密方法,在b站上找到了https://www.bilibili.com/video/BV1W5411o7H3?spm_id_from=333.999.0.0,这个andlua的官方加解密,然后发现视频中lua 加密的跟这题lua 加密后的很像,又搜了一下andlua官方加密里面是有base64的,之前逆它解密方法的时候,也看到了,所以就开始用视频的方式dump出luac,用视频给的工具包,下载到本地。
在/storage/emulated/0/ExaGear按照视频,将本地的 ExaGear文件夹复制过去
,然后将外挂中需要解密的lua,都放入src文件夹中
最后退出,运行ExaGear,点击luatools,自动解密开始
在dst文件夹下,生成解密好的luac文件
在用unlua53这个app,将这些luac反编译回lua,可以看到反编译后效果非常好,符号名也都有,很清晰
这里main.lua文件是主要逻辑,可以看到进程在启动的时候,会检测23946和27042的端口,也就是ida和frida的常用默认端口,还检测了/data/local/tmp下有无re.frida.server相关的文件
同时又创建了一个随机进程名的空白文件,并用shell命令运行了这个文件
难怪之前的找aes什么的进程,是找不到的,名字都是随机的,同时解密assert/aes文件,看了下解密方法是rc4,密钥也是明文zyp
rc4没魔改,我直接在本地解密aes文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | DEFAULT_KEY = ""
def rc4(data, key = DEFAULT_KEY, skip = 1024 ):
x = 0
box = range ( 256 )
x = 0
for i in range ( 256 ):
x = (x + box[i] + ord (key[i % len (key)])) % 256
tmp = box[i]
tmp2 = box[x]
box[i] = box[x]
box[x] = tmp
x = 0
y = 0
out = []
if skip > 0 :
for i in range (skip):
x = (x + 1 ) % 256
y = (y + box[x]) % 256
box[x], box[y] = box[y], box[x]
for char in data:
x = (x + 1 ) % 256
y = (y + box[x]) % 256
box[x], box[y] = box[y], box[x]
k = box[(box[x] + box[y]) % 256 ]
out.append( chr ( ord (char) ^ k))
return out
if __name__ = = '__main__' :
import sys
tt = open ( "aes" , "rb" )
ww = tt.read()
decrypt_files = rc4(ww, "zyp" , 0 )
decttt = open ( "dectt" , "wb" )
decttt.write("".join(decrypt_files))
|
因为外挂运行了这个文件,所以这个文件肯定是很重要的,解密后的文件发现是一个elf文件,用ida打开,有很明显的加壳,就两个区段,是很不正常,搜了下字符串,原来是upx的壳子
从github上下了一份upx的代码,看看能不能upx -d直接脱了
很舒服,是可以直接拖了的,哈哈,然后就可以用ida愉快的分析了,就是ida打开后也没符号,而且进程运行之后就死掉了,应该是出题人防止被附加吧,为了方便调试,我将调用kill和exit的地方都nop掉了
但是还是闪退,这和upx的脱壳有关系了,每次upx -d的程序基本都运行的有点问题,可能是目前解压缩壳的问题,不过对我分析影响不是非常大,由于没符号,先从入口开始调试libc_init, 找到了这里,提一句,我是调试分析完再写的wp,里面的函数经过分析后确定功能之后被我重命名了。
继续跟到这里
这里基本确实是主函数,这里两个函数,猜测分别是两个功能的实现,后面的分析也验证了我的想法,一个一个的去跟,里面有比较严重的混淆,f5不了,慢慢审汇编,多用f4、f7、f8、f9,先跟mmain函数
前面一大团字符串解密,直接跳到后面去看
到这里的流程静态很难看,本地测试了下外挂直接运行,也是有效果的,也就是独立于游戏的,直接远程启动外挂进程就好了
跟进去
发现这里是获取fps游戏的pid,查看下参数,通过这个pidof来获取的
到这里拿到pid
继续f7
每次调试这里都会跑飞,我只能在下一句汇编上下断才行,继续下一个函数
查看参数为libUE4.so,应该是找到so基地址的
跟进去看看,跟平常获取libue4.so基地址差不多
查看参数
这里是打开/proc/pid/maps,然后将每一行都和libUE4.so字符串做对比
如果查询到了,再将地址拿出并转换
退出来,进入下一个函数
这里发现是将libue4的基地址加上了0x4924570
[libue4.so_base+0x4924570]
跟进去,最终跟到一个syscall
syscall的调用号是0x178,也就是读取,将libue4的基地址+0x4924570所对应的四个字节取出来了,继续下一个函数
将取出来的数字+0x20,又经过相同的函数,和上面一样的分析,读取了4个字节到本地了
yk1=[libue4.so_base+0x4924570]
ykquestion=[yk1]+0x20
继续分析下一个函数
同样将上面取出的字节加上了0x70,再次进行读取
yk2=[ykquestion]+0x70
yk3=[yk2]
继续跟进下一个函数
发现这里是用libue4.so的基地址+0x4877034,然后依旧是同样的函数利用syscall,读取了4个字节
jf=libue4.so_base+0x4877034
jf1=[jf]
继续分析下一个函数,这里是一个远程读写的api,读取的jf1地址的0x78个字节到本地
读取的数据
发现是三个地址,这三个地址和后面的分析有关,继续看
读取了yk3地址的4个字节到本地
yk4=[yk3]
继续yk4+0x10的地址,取出4个字节
yk5=yk4+0x10
yk6=[yk5]
继续跟
发现这里是一个大循环
应该是遍历actor数组的,同时发现后面有拿到每个actor的name,并且对比
总结下这里在干什么,并举例
1 2 3 4 5 6 7 8 9 10 11 | yk6 = [yk5]
yk7 = ak6 - 0x4000
((yk6)>> 14 )<< 2 )的值去选择之前弄的三个地址其中之一
yk8 = 0xbaf2f000 (三地址中之一) + (yk7清零最高 4 位二进制)<< 2 )
yk9 = [yk8]
[yk9] = 类似这种editorcube8
|
通过strstr来对比取出的字符串是否为FirstPersonC
经过我的调试,发现这里actor数组都是固定的,index为0x23时,就找到了,调试了很多次都是一样的,也为我后续写外挂提供了很大便利
在index为0x23时,这时加的数值为0x90,字符串对比成功
pk4=[yk3+0x90]
字符串对比成功后,进入下一个逻辑
找到人物的偏移
总结下,然后这里运行完,发现可以加速了,但是无法飞天
1 2 3 4 5 6 7 8 9 10 11 12 13 | yk1 = [libue4.so_base + 0x4924570 ]
ykquestion = [yk1] + 0x20
yk2 = [ykquestion] + 0x70
yk3 = [yk2]
pk4 = [yk3 + 0x90 ]
lk1 = pk4 + 0x2f0
lk2 = [lk1] + 0x164
往 lk2这里地址写入了 0x460ca000
|
这里我开始用ue4dumper,将符号dump下来查看究竟是修改了什么
经过这个文件的对比的查找,上面的那份偏移可以这么改
1 2 3 4 5 6 7 8 9 10 11 | gworld = [libue4.so_base + 0x4924570 ]
ULevel = [yk1] + 0x20
yk2 = [Ulevel] + 0x70
Actors * = [yk2]
Character.Pawn.Actor = [Actors * + 0x90 ]
CharacterMovementComponent * = Character.Pawn.Actor + 0x2f0
MaxWalkSpeed = [CharacterMovementComponent * ] + 0x164
|
往 人物的MaxWalkSpeed这里地址写入了0x460ca000,增加了最大移动速度,可以越跑越快
继续飞天功能的分析,上面的函数无法在找出写数据的操作了,说明在别的函数里面,发现是在这个函数里面
跟进去,因为分析过了一遍,我根据功能重命名了一下
跟进
先fopen了 /proc/filesystem , 本地测试了是这个东西,
然后它在找这个
在我上图倒数第二行,找到之后设置了为1 ,又fopen了这个文件
很多挂载的东西,但是还是想找这个
最终拼接拿到了这个
往这个文件里写入了0x30 ,应该是开启selinux的意思,这个函数分析完毕,继续下一个
这里和移动加速的地方差不多,就是获取fps游戏进程的pid
跟进去看看
但是这里是附加的意思,让游戏成为自己的子进程
这里阻塞游戏,ok,ok这个函数分析结束,下一个
跟进去,发现拿出ro.build.version ,查看安卓的版本,为了后面的查找libc的路径有关系,不同安卓版本会不一样,分析下一个函数
外挂先获取自己的libc的mprotect的指针,然后进去拿到fps的libc基地址
通过寄存器知道了这个函数拿fps进程的libc和外挂进程的目的,是拿到外挂的libc的mprotect的偏移,加上fps的libc基地址拿到fps的libc中mprotect的函数地址,方便后面的ptrace调用
分析下一个函数
跟进去,这里libue4.so基地址+0x1e00000+0x1d6000作为参数传入
伪代码也很清晰,可以看到这里是ptrace利用getregs在保存寄存器,也就是保护上下文
继续跟下去,这里有四个ptrace
伪代码挺清晰的
这里是修改了libue4.so基地址+0x1e00000+0x1d6000所在页的读写权限,利用mprotect函数。
继续下一个函数分析,这里libue4.so基地址+0x1e00000+0x1d6000+0x428为参数
这里是关键函数,跟进去
这里ptrace参数为5,是写入数据的意思,在libue4.so基地址+0x1e00000+0x1d6000+0x428这个地址上写入了0xe3a00801这四个字节
再次运行游戏,按音量键,发现飞天了,说明这里就是飞天的关键
继续分析下一个函数
跟进去
查看伪代码,原来是退出ptrace,全部函数分析完毕!
总结一下飞天的实现,通过ptrace让游戏成为外挂进程的子进程,然后通过先拿出外挂进程libc中mprotect函数地址,算出mprotect的偏移,再拿到游戏的libc基地址,算出游戏进程的mprotect,再利用ptrace操纵寄存器的效果,调用mprotect函数,修改想修改内存的读写权限,然后再通过ptrace 写入数据,最后退出ptrace,进程运行结束
1 | [libue4.so基地址 + 0x1e00000 + 0x1d6000 + 0x428 ] = 0xe3a00801
|
这里通过反编译libUE4.so,找到了这里
RT _ZNK27UCharacterMovementComponent11GetGravityZEv
.text:01FD640C _ZNK27UCharacterMovementComponent11GetGravityZEv
很明显是这里修改了跳跃高度,每跳一次,高度直接太高,相当于飞天了。
外挂实现
这里我是用frida脚本实现了外挂功能,首先游戏并没有任何保护,所以可以选择远程读写或者注入,但是有些奇怪的这个外挂的两个功能,飞天是mprotect 页然后修改数据,但是人物移动速度是直接修改的,远程读写需要考虑权限,注入需要考虑修改的时机,基于两者我都想要,所以还是选择了frida来实现,通过之前得到的偏移,frida本身也提供内存权限的修改, 游戏启动后,attach方式启动,可以实现两个外挂的功能,飞天利用了人物actor在数组中下标一直不变,所以我这里不需要去遍历数组再通过字符串对比的方式找到人物,根据我之前记录人物的下标,就可以直接拿到人物actor了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | function cheat()
{
var libc_addr = Module.findBaseAddress( "libUE4.so" );
/ / console.log( "libUE4 addr is :0x%x" , libc_addr);
/ / fly sky
var tt = ptr(libc_addr).add( 0x1e00000 );
var tt1 = tt.add( 0x1d6000 );
var tt2 = tt1.add( 0x428 );
Memory.protect(tt2, 4096 , 'rwx' );
var tt4 = 0xe3a00801 ;
tt2.writeU32(tt4);
/ / speed up
var yk1 = ptr(libc_addr).add( 0x4924570 );
var jf = ptr(libc_addr).add( 0x4877034 );
var mid1 = yk1.readU32();
var ykquest = mid1 + 0x20 ;
var yk2 = ptr(ykquest).readU32() + 0x70
/ / console.log( "yk2" ,yk2.toString( 16 ));
var yk3 = ptr(yk2).readU32()
var jf1 = jf.readU32()
console.log( "jf1" ,jf1.toString( 16 ));
var fun1 = ptr(jf1).readU32();
var fun2 = ptr(jf1).add( 4 ).readU32();
var fun3 = ptr(jf1).add( 8 ).readU32();
console.log( "fun2" ,fun1.toString( 16 ));
console.log( "fun2" ,fun2.toString( 16 ));
console.log( "fun3" ,fun3.toString( 16 ));
var actor = ptr(yk3).add( 0x90 ).readU32()
var actor1 = ptr(actor).add( 0x2f0 );
var actor2 = actor1.readU32();
var actor3 = ptr(actor2).add( 0x164 );
var speed = actor3.readU32();
console.log( "actor3" ,speed.toString( 16 ));
Memory.protect(actor3, 4096 , 'rwx' );
ptr(actor3).writeU32( 0x460ca000 );
}
setImmediate(cheat);
|
总结下不足
- 混淆没去,第一次参加没啥经验,实际上重建控制流是可行的,f5是对mov pc这个东西反编译不了,所以patch成b xx比较好
- 第二个就是没分析的很细,尤其是偏移,做决赛的题目的时候,注重关注了这点
- 有啥不懂的,欢迎交流233
阿里云助力开发者!2核2G 3M带宽不限流量!6.18限时价,开
发者可享99元/年,续费同价!
最后于 2022-4-28 11:49
被YenKoc编辑
,原因: