Basic protection
使用模拟器打开游戏,发现进入不了猜测是检测,使用syscall hook 获取全部系统调用,发现使用 prctl和seccomp 后程序卡死 。
[Handler_Syscall__NR_seccomp] __NR_seccomp(0x1, 0x1, 0xbc9bac30) libUE4.so+0x46D293B
Hook调用seccomp将prog数据获取下来,分析发现进行系统调用号拦截0x129 __NR_recvmsg杀死线程,将prog->len 设置为0游戏正常运行。
1 2 3 4 | sock_filter[ 0 ] { code: 0x20 , jt: 0x0 , jf: 0x0 , k: 0x0 } LD W ASB
sock_filter[ 1 ] { code: 0x15 , jt: 0x0 , jf: 0x1 , k: 0x129 } JEQ( 0x129 )
sock_filter[ 2 ] { code: 0x6 , jt: 0x0 , jf: 0x0 , k: 0x0 } RET_KILL_THREAD
sock_filter[ 3 ] { code: 0x6 , jt: 0x0 , jf: 0x0 , k: 0x7fff0000 } RET_ALLOW
|
26秒后游戏调用access进行环境检测,环境异常游戏闪退。
这里捕获到access两个返回地址:
1.libUE4.so+0x46D1FE5
2.libUE4.so+0x46D2093
可能是不同的检测。
分析libUE4.so+0x46D1FE5,我给检测函数命名为access_check_risk_46d1f8c,调用来源于libUE4.so+0x46d1be4。
使用Syscall(0x21) access检测是否存在异常文件。
分析函数sub_46d1be4
一、get_main_base_46d1d38
还原代码
1 2 3 4 5 6 7 8 | auto f = fopen( "/proc/self/maps" );
auto base;
while (buf = fgets(f)) {
if (strstr(buf, "libUE4.so" ))
base = strtoull(buf);
}
close(f)
return base;
|
二、init_crc_buf_46d1e74。
分析地址0x046d1ef4向内存中libUE4.so + 4FA9950地址写入0, 0x77073096,根据这个判断是crc_buf。
三、get_buf_crc_46d1f28(key, (_FINI_1 + libUE4.so), 0x36cc964);
这段代码从_FINI_1到_FINI_1+0x36cc964正好是对应着.text段开始和结束,返回的crc值存入data_4fa9d50 = crc,结果判断 if (crc == 0xd18b51ab) 。
四、access_check_risk_46d1f8c,上面分析过
五、anti_frida_46d2038,反汇编和内存都看得见
1 | access( "/data/local/tmp/frida-server" ) access( "/data/local/tmp/re.frida.server" )
|
六、anti_frida_046d20e4
1 2 3 4 5 6 7 8 9 | auto f = fopen( "/proc/self/maps" , "r" );
while (buf = fgets(f)) {
str = get_str_frida(); / / 46D21BB "frida"
if (strstr(buf, str )) retrun 1 ;
str = get_str_gadget(); / / "gadget"
if (strstr(buf, str )) retrun 1 ;
}
fclose(f);
return 0 ;
|
七、anti_frida_46D2261
这里可以看得出来是去遍历/proc/self/task/全部线程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | while (ptr = readdir( dir )) {
/ / 过滤目录
if (strcmp(ptr - >d_name, "." ) = = 0 ||strcmp(ptr - >d_name, ".." ) = = 0 ) continue ;
/ / 获取文件内容 / proc / self / task / {taskId} / status
sprinf(path, "/%s/%s/status" , "proc/self/task" , ptr - >d_name);
auto f = fopen(path);
buf = fgets(f);
/ / 判断是否包含{ "gmain" , "gum-js-loop" , "frida" , "ggdbus" }
for (auto str : { "gmain" , "gum-js-loop" , "frida" , "ggdbus" }) {
if (strstr(buf, str .c_str())) return 1 ;
}
}
return 0 ;
|
八、kill_46D26D5
以上检测函数如果有返回1 则会调用memclr让程序闪退。
绕过方案
1. 直接将start1 和 start2 写入 return 0
1 2 3 4 5 | / / start1_anti_hack 过检测 1
DobbyHook((void * )(UE4 + 0x46d1be4 + 1 ), (void * )sub_46d1be4, (void * * )&sub_46d1be4_orig);
/ / start2_anti_hack 过检测 2
DobbyHook((void * )(UE4 + 0x46d1afc + 1 ), (void * )sub_46d1afc,
(void * * )&sub_46d1afc_orig);
|
2.绕过CRC
将正常游戏.text拷贝到自己申请的内存中,创建一个maps文件,内容写入刚刚创建的地址十六进制加上libUE4.so让检测识别我们拷贝正常游戏.text的地址为crc目标地址,在通过hook系统fopen函数对打开/proc/self/maps文件重定向到自己的maps。
3.环境检测
游戏检测调用的并svc而是libc中的syscall函数,因此可以在这个地方进行syscal拦截,判断__number == 0x21 这直接将access的path路径改成空这样就绕过检测了。
4.frida检测
hook fopen函数判断path是否包含/proc/self/task,将path重定向
hook syscall函数判断__number == 0x21,将path重定向
5.seccomp
可以直接修改过滤器的字节码将00000006 00000000 (RET_KILL_THREAD) 修改为 00000006 7FFF0000 (RET_ALLOW),或者使用hook syscall 拦截__number == 0x17F。
6.无敌字符串大法
可以发现很多字符串会被解密到.bss段这也就意味着没有crc,观察被加密的字符串结尾都是以00 01结尾,没错01就代表这个字符串被解密了,这里就可以修改字符串实现绕过检测,当然在真正的tersafe中字符串会被二次检测。
Section 1
分析游戏登录
1 2 3 4 5 6 | sub_19f4304(utf16 input ){
Buf = malloc( 0x100 );
sub_223c524(to_utf8( input ), Buf );
/ / 检测返回Buf是否等于;
return Buf = = { 0x3D , 0xF2 , 0x2C , 0xF8 , 0x8F , 0xFB , 0x47 , 0x5B , 0x49 , 0x04 , 0x78 , 0xD9 , 0x4E , 0x31 , 0xEF , 0x3E , 0xA1 , 0xA7 , 0xAA , 0x7B , 0xCF , 0x72 , 0xA8 , 0xBC , 0x53 , 0x2B , 0x67 , 0x00 , 0xB2 , 0xB0 , 0x32 , 0xFA }; / / 这个后面回用到
}
|
虚拟机入口分析
初始化寄存器0~17赋值=0
1 2 3 4 5 | R0 = 0x4FA9D54 ; / / key字符串地址tencentgamesecfs
R1 = 0x10 ; / / key字符串长度
R2 = Input ; / / 输入字符串
R3 = output; / / 输出数据
( * SP) = outputSize; / / 0x20
|
初始化虚拟机字节码
vm分析与还原
ctx + 3C 获取pc寄存器地址,取指令后bswap,opcode >> 27执行对应handler。(因为取指令的肯定是PC寄存器啊)
虚拟机介绍
其实一开始我挺蒙的,直到我看到这个get_bit函数,一切都合情合理了在去年比赛的时候用到的是Arm64虚拟机,Arm64解析里面使用各种asm_ubfx无符号位域提取,解析指令需要的位域,今年则是Arm虚拟机。
具体查看:[原创]2023腾讯游戏安全大赛-安卓赛道决赛"TVM"分析与还原
逆向还原Handler
比如我现在要还原handler:6,opcode:30020000
[SP, 4] = get(opcode, 0x16, 0x1B); // = 00000000
[SP, 8] = get(opcode, 0x11, 0x16); // = 00000001
R0 = get(opcode, 0x0C, 0x11); // = 00000000
将输入寄存器1值和输入寄存器2值进行ROR然后写入输出寄存器,这就是指令解析过程。
Handler6实现为 ROR,当然这里只有一种情况,有些指令内部又有多种情况。
接着解析剩余的Handler也是成功扣了一天一夜。
vm_to_asm
将虚拟机的字节码Dump下来这里我用的Dowrd Hex,我称它为vm_shellcode
开始调用解析函数,从vm_shellcode的0x7FC地方开始解析(当然你想解析哪里都行)
解析出来Arm32汇编,在通过 ARM to HEX网站把汇编转换成十六进制字节码。
然后将ARM to HEX生成的字节码替换vm_shellcode
将文件写出vm_to_asm.img
asm_to_c
vm_to_asm.img拖入ida pro
算法还原
FindCrypt3插件查找到AES_Rijndael_rcon算法。
将函数对应名称补齐,发现没有uint8_t* iv且多次使用inv,函数为aes ECB Decrypt算法。
完整还原后发现有几处变化,aesKey.dK 变成了aesKey.eK、invShiftRows 和 invSubBytes 顺序颠倒、invMixColumns 被替换成了 mixColumns与addRoundKey顺序颠倒。
验证
将解密算法用游戏同样的方式修改,在游戏内输入32个1,验证一下解密数据相同。
将加密算法用游戏同样的方式修改。
将login验证结果(不懂请返回分析游戏登录部分)进行加密,游戏内输入加密结果登录成功。
登录密码:dde8cdf098e8434b93f04f86085a88f9
Section 0
一、方框透视
世界坐标转换_读写路径不同
1.使用ViewMatrix
2.使用CameraCacheEntry
3.使用 ProjectWorldLocationToScreen
获取世界坐标_读写路径不同
1.Transform
2.RelativeLocation
3.GetCenterOfMass
4.GetActorBounds
使用不同排列组合绘制出来
二、自动瞄准:按下屏幕时自瞄,当ControlRotation触发写入时停止自瞄
调试移动屏幕时触发写入ControlRotation
调试移动屏幕时触发写入ControlRotation
DobbyInstrument((void*)(UE4 + 0x3622218), ControlRotationSet);
实现自瞄的逻辑
1 2 3 4 5 6 7 8 9 | / / 方案一 修改 ControlRotation
ControlRotation - >x = rotation.x;
ControlRotation - >y = rotation.y;
/ / 方案二 Call SetControlRotation
((call)(UE4 + 0x3622054 ))(PlayerController, &rotation);
/ / 方案三 输入 ControlRotation 增量
/ / ((call)(UE4 + 0x39C0D24 ))(PlayerController, 10.f );
|
End
最后附上易语言vm_to_asm.e
编程也就图一乐收收心找个电子厂上班了
最后于 15小时前
被a'ゞCicada编辑
,原因: