sec2023安卓赛题
Android逆向解题-sec2023安卓赛题
项目
APK分析
入口点
1 | "com.unity3d.player.UnityPlayerActivity" |
包名
1 | "com.com.sec2023.rocketmouse.mouse" |
是一个游戏,发现是Unity制作的。
看一下lib,发现libil2cpp.so
,libmain.so
等so。
Native分析
libmain.so
Unity的加载il2cpp的逻辑。不重要。
1 | jint JNI_OnLoad(JavaVM *vm, void *reserved) |
libil2cpp.so
Unity C#编译成C++Native后生成的so文件。需要用il2CppDumper
等其他方式恢复。
libsec2023.so
需要重点分析。
libunity.so
绝对是Unity引擎文件。不管了。
Root下启动APP⭐
经过多次尝试,发现使用Magisk Delta
最好,开启MaigskHide
后可以隐藏root,规避APP的安全检查,成功启动程序。否则每次都会出现hack detected
,然后自动退出。
由于开启MagiskHide后无法使用Frida,因此最后具体使用的方法请参考Frida过调试
部分。
解密libil2cpp.so
直接打开libil2cpp.so
,发现有严重混淆,判断是加密了。
因此尝试通过内存Dump或者其他方式来解密。
尝试il2CppDumper静态(失败)
Release Il2CppDumper v6.7.40 · Perfare/Il2CppDumper (github.com)
然后提示程序加密了:
1 | Initializing metadata... |
继续查一下,这个Input CodeResigstration
是什么玩意:
[Tutorial] Il2CppDumper tutorial finding CodeRegistration and MetadataRegistration (unknowncheats.me)
这一块先等一等,太复杂了。
检查global-metadata.dat
,没有加密。
反编译libil2cpp.so
,发现加密了。
GG+il2CppDumper
用GG dump内存中的libil2cpp.so
首先安装GameGuardian
GameGuardian - Official Downloads - GameGuardian
然后dump内存中的libil2cpp.so
部分。具体而言的操作方法:
右上角打开设置,选择导出内存
选择内存范围起始和结尾:
开始从红色的Xa:.....libil2cpp.so
结尾到绿色的Cd:.....libil2cpp.so
(最后一个,后面就是其他东西了)
这样把包含libil2cpp.so
的内存块给dump下来。
然后使用最新的il2cppdumper
(版本v6.7.40)(2023-10-11)
必须版本要高,否则可能没法读取dump文件,或者还必须要你的
global-metadata.dat
文件也是从内存里dump出来的,因为要你输入.dat
文件在内存的基址。高版本的工具就不需要了。
Release Il2CppDumper v6.7.40 · Perfare/Il2CppDumper (github.com)
然后选择dump下来的内存bin文件,以及解包得到的global-metadata.dat
文件(这个因为没有加密过,所以直接用)
检测到是内存dump后,输入dump下来文件的起始基址,然后就会自动恢复了。
成功拿到
1 | DummyDll |
修补libil2cpp.so
dump下来的.so
和普通的so是有一定区别的,所以要修补。将dump下来的与静态解包后得到的.so的结构体进行比较(使用010Editor的ELF.bt辅助解析文件结构),发现:
- dump中丢失了
dynamic_symbol_table
- dump中的
section_header_table
被空字节填充了 - dump中的
program_header_table
和静态解包的so是一样的,但是由于dump后的文件结构实际上有所变化,和静态的不一样,所以需要对dump的进行patch。
具体的东西放在blog”Linux-ELF文件结构”里面。
IDA中获取符号表
ida_with_struct.py
For IDA, read il2cpp.h file and apply structure information in IDA
将修补好的dump文件用IDA打开,然后选择il2cppdumper
里面的ida_with_struct_py3.py
,然后将上次提取出来的script.json
和il2cpp.h
选中。
经过漫长的等待,第一次脚本跑完,不少Unity函数和结构体都被注释好了。
在运行一次脚本,选中scriptliteral.json
和il2cpp.h
。
Frida dump libil2cpp.so
Frida过调试⭐
直接打开都被check了。这是真不知道为什么,难道是查到我的root环境了嘛
绕过frida-server
与端口检查,但还是被check。
用下面的脚本Hook AlertDialog:
1 | Java.perform( |
看到:
1 | Sec2023MsgBox.show is called: str=hack detected |
歪方法
- 首先在Magisk Delta下,MagiskHide开启,隐藏root权限,然后打开APP
- 然后关闭MagiskHide(因为Magisk Delta的MagiskHide是基于ptrace的,和frida冲突),再用adb shell启动frida-server,以防万一换一个端口
- 再去frida去Hook即可
1 | frida -H 127.0.0.1:11451 -F -l .\hook_sec2023.js |
持续性绕过(不懂原理)
根据另一个师傅的描述,通过Hook神秘Sleep
函数似乎就能过反调试?搞不明白
后来发现 hook libsec2023.so 会弹窗,但是有一定延时才会弹出来,猜测调用 sleep, 尝试 hook 一下 sleep 发现真的不弹了。
[原创]2023腾讯游戏安全竞赛初赛题解(安卓)-Android安全-看雪-安全社区|安全招聘|kanxue.com
1 | function AntiDebug() |
原来是通过延长Sleep的时间来预留足够长的时间进行调试。
不过可以比较确定的是,反调试+反Frida逻辑都是在libsec2023.so
里面实现的。
我编写一个脚本:
1 | let sec2023 = Module.findBaseAddress("libsec2023.so"); |
很快啊,就触发了反调试逻辑。
1 | hooked sleep |
反调试检测点
现在分析可以推测出的反调试检查点:
- 检查su
- 检查frida固定端口
- 疑似为启动时一次性检查,没有持续性检查
- 一次成功后便永久记录(后面无论几次重启APP,只要第一次成功过,后面都能持久保留;失败亦是如此;除非重启手机)
- 只要用
Module.findBaseAddress
函数寻找此lib,就会触发反调试检查
Dump
[原创]细品sec2023安卓赛题-Android安全-看雪-安全社区|安全招聘|kanxue.com
这位师傅过反调试似乎比我要轻松很多,单纯绕了个端口就搞定了,可能是su等已经被完美隐藏了吧。
于是乎就有下面的dump脚本:
1 | function dump_so(so_name) { |
得到:
1 | [name]: libil2cpp.so |
修复libil2cpp.so
和上面用GG dump的一样。此言不表。
反-反调试
顺着刚刚通过Hook Sleep成功绕过反调试,并获得Stacktrace;来溯源分析一波。
不过由于刚刚一开始是先用歪方法绕过反调试的,然后再Attach上来的,因此后续的Sleep函数调用可能不是直接与反调试逻辑相关的。
我们需要重新启动程序,以Spawn方式来启动:
1 | frida -H 127.0.0.1:11451 -f com.com.sec2023.rocketmouse.mouse -l .\hook_sec2023.js |
得到:
1 | hooked sleep |
sub_11F24
Stacktrace中的0x11f44在这个函数里面:
1 | __int64 __fastcall sub_11F24(_BYTE *a1) |
这里面全是间接调用,不是重点
sub_22080
0x220a8在这个函数里面。混淆严重。注意到以下函数:
1 | LABEL_77: |
sub_21408
1 | __int64 __fastcall sub_21408(JNIEnv *a1, unsigned int a2, __int64 a3) |
sub_21500
太长了,直接看结尾:
1 | LABEL_73: |
sub_21B10
似乎是一个变参调用Java方法的玩意
1 | __int64 sub_21B10(JNIEnv *a1, __int64 a2, __int64 a3, ...) |
总结
总体来说这个函数的执行逻辑:
- FindClass
- GetVersion
- GetStaticMethodID
- NewStringUTF
- CallStaticVoidMethodV
- DeleteLocalRef
- DefineClass
种种迹象表明这个函数似乎主要是用于执行了一个Java方法。不过由于其他的混淆显然都是用于加密字符串等其他数据的,因此具体干了啥还是需要动调Hook。
sub_36340
0x36340
不知道干啥的
1 | __int64 *__fastcall sub_36340(__int64 *a1, __int64 a2) |
sub_363E0
0x3643c
1 | void __fastcall sub_363E0(__int64 a1) |
继续尝试——主动触发反调试逻辑
将上面Hook Sleep脚本设计的时间变短一点(10s),看看会有哪些其他更多的地方会触发Sleep。
1 | hooked sleep |
还是没有弹出hook detected!
这种期望错误的框。看来得重启手机后才能触发了。
通过尝试Module.findBaseAddress
函数寻找此lib,就会触发反调试检查,最后我们得到弹出hack detected!
框函数:
1 | sleep called from: |
sub_36818(hack detected)
1 | void sub_36818() |
仔细一看,似乎和sub_36340很相似
sub_36F6C(字符串解密函数1)
传入了两个数字进去
1 | v6 = 0x35AF37FC0E8D1668LL; |
根据某师傅的描述,这是一个很熟悉的字符串解密函数。
但是但从伪代码啥也看不出来,因为有间接跳转:
1 | void __fastcall sub_36F6C(__int64 a1) |
下面具体看汇编:
1 | .text:0000000000036F6C ; __unwind { |
模拟执行
使用IDA7.5的uEmu插件,从0x36818
函数头开始模拟执行,然后进入该函数观察具体逻辑:(因为此函数传入的形参似乎会影响X11寄存器的值)
经过调试,得到:
1 | .text:0000000000036FC0 LDR X11, [SP] |
这里已经很清晰了,就是一个
1 | a[i] ^= (0x77 * i) |
这样的逻辑。而被解密的值是外面传来的第二个参数:
1 | v6 = 0x35AF37FC0E8D1668LL; |
解密脚本
1 | #0x35AF37FC0E8D1668LL |
得到
1 | hack detectedù |
反正可以实锤是报错弹窗了。
Patch
1 | 0x36FBC: BR X11 |
修改成
1 | NOP |
1 | .text:0000000000036FF4 CSET W11, HI |
修改成:
1 | .text:0000000000036FF4 NOP ; Keypatch modified this from: |
这样IDA也能正常反编译了:
1 | void __fastcall sub_36F6C(__int64 a1, _BYTE *a2) |
sub_36340(获得1000分,您将赢得比赛。)
根据上面的分析,可以发现这里执行的应该都是字符串解密后的。
1 | __int64 *__fastcall sub_36340(__int64 *a1, __int64 a2) |
因此去找调用这个函数的函数,发现了经典的间接跳转:
模拟执行sub_361D0
1 | void __fastcall sub_361D0(__int64 a1) |
太经典啦。还是用uEmu去模拟执行函数头,慢慢看间接跳转是怎么走的。
最后比较理想的是,所有间接跳转都如愿正常跳转了。
最后跳到block 0x362BC处,就是要调用sub_36340
的位置。
不过在它前头有一个BL sub_365A8
:
1 | .text:00000000000362BC MOV W8, #1 |
sub_365A8(字符串解密函数2)
和上面的Patch同理:
1 | .text:00000000000365F8 NOP ; Keypatch modified this from: |
得到
1 | void __fastcall sub_365A8(__int64 a1, __int64 a2) |
因此现在需要知道具体解密的值。通过模拟执行,得到X1指向的栈中的数据:
1 | FFFFFFFFFFFFFF70: E8 7E 57 35 7E 27 91 A0 B0 40 85 D8 C6 DF 9C 9C .~W5~'...@...... |
解密脚本
1 | a = "E8 7E 57 35 7E 27 91 A0 B0 40 85 D8 C6 DF 9C 9C E6 72 48 35 70 36 48 25 22 95 DE C7 A6 9F B4 F8 B5 6B 03 50 42 B0" |
得到:
1 | 获得1000分,您将赢得比赛。 |
动调
首先一定要新开 ida,不要用分析了 so 的 ida 附加,否则退出调试的时候会卡死,之前的分析就炸了,推荐是动静结合来分析
还有一件很重要的事情是关闭一些异常处理
Debugger Options…->Edit exceptions->
SIGPWR SIGXCPU 右键 edit,去掉挂起程序的勾,并勾上通过引用,report 选 log 或者 Silent 都行
这一点可能是因为 unity 的问题或者是 Android 的问题,调着调着就会弹这俩个异常,如果挂起程序,程序就会崩溃,程序崩溃又会导致 ida 崩溃,而设置完之后就不会出现这个情况了
还有一件事,程序暂停调试的时候不要点击屏幕,否则会出现和前面异常挂起一样的问题
分析游戏逻辑
在dump.cs
中找到表示金币的Coins
1 | private TssSdtInt Coins; // 0x50 |
以及收集硬币的函数:
1 | // RVA: 0x4652E4 Offset: 0x4652E4 VA: 0x763C08E2E4 |
在恢复符号表的libil2cpp.so
里面也可以找到具体实现逻辑:
1 | void __fastcall MouseController__CollectCoin( |
其中有:
1 | if ( TssSdtInt__op_Implicit(this->fields.Coins, v13) >= 1000 ) |
Frida破解
1 | function fuck_il2cpp() { |
可能一开始直接注入会报错,提示找不到模块;不过后面等一会儿再注入也行。
之后主动去收集金币,就会触发这个函数CollectCoin
,继而触发我们的魔改逻辑,得到flag。
1 | sec2023_baacf2 |
注册机
在一开始的界面,有一个Mod Menu
,里面有一个数字键盘,和一个显示的Token。
在dump.cs
中寻找键盘相关逻辑,发现SmallKeyboard
类,同时此类还有严重混淆。
SmallKeyboard__iI1Ii
在IDA里面搜索相关类逻辑:
1 | void __fastcall SmallKeyboard__iI1Ii(SmallKeyboard_o *this, SmallKeyboard_iII1i_o *info, const MethodInfo *method) |
发现:
1 | if ( KeyType == 2 ) |
这里的iI1Ii
个人猜测可能是生成的Token,或者是用户的输入。
SmallKeyboard__oO0oOo0
1 | void __fastcall SmallKeyboard__oO0oOo0(SmallKeyboard_o *this, const MethodInfo *method) |
这个很确定就是生成随机Token的逻辑了。
1 | // RVA: 0x465880 Offset: 0x465880 VA: 0x763C08E880 |
因此可以确定iI1Ii
是check函数。
观察第3个方法,传入了一个数字,显然就是用户输入的Token。
SmallKeyboard__iI1Ii_4610736
1 | void __fastcall SmallKeyboard__iI1Ii_4610736(SmallKeyboard_o *this, uint64_t i1I, const MethodInfo *method) |
然后
1 | // attributes: thunk |
汇编里面一看:
1 | LOAD:00000000013B8DC8 08 00 00 D0 ADRP X8, #off_13BAFF0@PAGE ; Address of Page |
这整个代码区域都被.init_proc
函数给包裹了,导致伪代码错误。因为我们不需要.init_proc
,因此直接u掉,单独对这里按p构建函数即可。(为了不影响后续可能的分析流程,剩余的数据要重新c回来,变成汇编或者p成函数)
最后得到:
1 | __int64 sub_13B8DC8() |
而这个off指向了
1 | g_sec2023_p_array |
这是一个从libsec2023.so
导入的列表。
回到libsec2023.so
,看导出符号:
1 | .data:0000000000071240 g_sec2023_p_array DCQ sub_35404 ; DATA XREF: LOAD:0000000000000FF8↑o |
第九个元素:
1 | sub_31164 |
sub_31164
1 | __int64 __fastcall sub_31164(__int64 a1, __int64 a2) |
调用g_sec2023_o_array
里的另一个函数,其传参是sub_3B8CC(a2)
这个array在libsec2023
里面没有被定义,猜测应该是从其他so里写入的。
在libil2cpp.so
里面看到里导出。然后用交叉引用发现了调用了的函数。
这里一开始没有发现交叉引用,事实证明那一串代码被我在u掉
.init_proc
的时候给搞成数据了。所以需要立刻重新patch成代码。
sub_763CFE1DD8(libil2cpp.so基址为0x763bc29000)
1 | void sub_763CFE1DD8() |
第一个off指向了一个函数:sub_763CFE1D64
sub_763CFE1D64⭐
1 | a2_low = System_Convert__ToUInt32_8761148((unsigned int)a2, 0LL);// a2的低4字节 |
最后TEA成功后:
1 | if ( !num2_hi && (_DWORD)result == num2_lo ) |
对SmallKeyboard_TypeInfo
交叉引用一下,看看哪里用了这个玩意:
在MouseController__Start
:
1 | if ( SmallKeyboard_TypeInfo->static_fields->KeyboardNum == -1 ) |
还有4个引用点,具体算法没细看,应该是开挂无敌逻辑。
修复sub_3B8CC
复盘sub_31164
,了解到上面这一串位于libil2cpp.so
的注册机开挂逻辑,其输入是先经过了sub_3B8CC
变换过的。
1 | __int64 __fastcall sub_31164(__int64 a1, __int64 a2) |
因此现在开始patch:
1 | void __fastcall sub_3B8CC(__int64 a1) |
先从其中的子函数开始patch:
修复sub_3B9D4
1 | void sub_3B9D4() |
主要搞不懂的是CSEL指令:
1 | CSEL X10, XZR, X9, CC ; Conditional Select |
大概意思是:
ARM Cortex-A Series Programmer’s Guide for ARMv8-A
Code | Encoding | Meaning (when set by CMP) | Meaning (when set by FCMP) | Condition flags |
---|---|---|---|---|
EQ | 0b0000 |
Equal to. | Equal to. | Z =1 |
NE | 0b0001 |
Not equal to. | Unordered, or not equal to. | Z = 0 |
CS | 0b0010 |
Carry set (identical to HS). | Greater than, equal to, or unordered (identical to HS). | C = 1 |
HS | 0b0010 |
Greater than, equal to (unsigned) (identical to CS). | Greater than, equal to, or unordered (identical to CS). | C = 1 |
CC | 0b0011 |
Carry clear (identical to LO). | Less than (identical to LO). | C = 0 |
1 | if 0 < 2: |
整个代码则为:
1 | if 0 < 2: |
由于0x3BA04就是紧接着的一个基本块,且0x3BA04可以当作是小于的时候的执行,那么不小于的时候就要跳转到0x3BB50
也就是B.GE loc_3BB50
。
反正patch完后得到:
1 | void __fastcall sub_3B9D4(_DWORD *a1) |
修复点
这是一位师傅总结的:这里我就偷个懒了。
1 | sub_3B9D4 |
修复后
1 | unsigned __int64 __fastcall sub_3B8CC(__int64 a1) |
sub_3A924
1 | __int64 __fastcall sub_3A924(__int64 a1, __int64 a2, unsigned int a3, __int64 a4, __int64 a5) |
这里面似乎又用了JNI接口去调用java方法。由于并没有在jadx里面找到这个encrypt方法,因此猜测又是动态加载的。因此再一次用GG。
encrypt.dex(用GG修改器Dump)
- 首先
frida-ps -U
找到进程PID(进程名是mouse) - 然后
adb shell
,su
后,cat /proc/<pid>/maps | grep "encrypt"
- 然后再去GG里面复制内存,直接写上具体数字,用不着对着手机里的maps列表嗯看了。
1 | 130|crosshatch:/ # cat /proc/23943/maps | grep "encrypt" |
deleted?,但我还是想把他dump出来。实在不行我就只能看.vdex
了,不过还没仔细研究。
dump下来后,Jadx报错,一检查说是checksum有问题。因此直接关闭这个JADX中的checksum设置。
混淆极其严重,里面有一设计了一大堆恶心人的UTF8特殊字符作为字符串。
因此在JADX设置里面开启UTF转义。
1 | package sec2023; |
稍微花点时间把平坦化给去掉的话,这里还是挺好恢复逻辑的。
逆算法
1 | void Decode2(int* flag) |
TEA逆向(没有具体调试&Hook)
这是sub_763CFE1D64里的TEA。里面的初始化队列可以通过 hook InitializeArray 或者直接调试都可以拿到:
1 | int* DecodeTea(int RandNum) |
OO0OoOOo_Oo0__oOOoO0o0
为了方便识别,把OO0OoOOo_Oo0_o
结构体的名字改成好识别的a1
等。
然后把一些没用的if条件给删掉,得到:
1 | void __fastcall OO0OoOOo_Oo0__oOOoO0o0(OO0OoOOo_Oo0_o *this, const MethodInfo *method) |
这很像一个虚拟机代码。其中a5
就是PC寄存器,a1
是汇编字节码。
获得VM汇编码(没具体调试&Hook)
在OO0OoOOo_Oo0__oOOoO0o0中:
1 | v3 = this; |
而this就是此函数的第一个形参:
1 | void __fastcall OO0OoOOo_Oo0__oOOoO0o0(OO0OoOOo_Oo0_o *this, const MethodInfo *method) |
因此回去看初始化函数OO0OoOOo_Oo0___ctor:
1 | // local variable allocation has failed, the output may be wrong! |
继续往上看它的调用:
1 | OO0OoOOo_Oo0___ctor(v26, (System_UInt16_array *)v22, 0, num, v29); |
v22在这里被初始化:
1 | v22 = (System_Array_o *)sub_763BFB2B90(ushort___TypeInfo, 199LL); |
这是一个199大小的Array,因此这里可以通过Hook或者动调来直接得到初始的VM字节码。
获取VM Handler
对VM代码里面经常使用的OO0OoOOo_oO0OoOOo_TypeInfo
进行交叉引用,发现另一个函数对它的引用:
1 | void __fastcall OO0OoOOo_oO0OoOOo___cctor(const MethodInfo *method) |
1 | .rodata:000000763C877470 xmmword_763C877470 DCB 1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0 |
可以发现1-22数字(4字节DWORD)
以及函数OO0OoOOo_Oo0__O000O000000o:(下面取部分代码片段)
1 | OO0OO0 = OO0OoOOo_oO0OoOOo_TypeInfo->static_fields->OO0OO0; |
这里显然就是将其设置成了VM方法Method_OO0OoOOo_Oo0_OOOOOO0__
,这个的注释上会写上具体函数的偏移的(只要正确运行了il2cppdumper的那个脚本),即可找到真正的Handler函数。
而实际上每个VM Handler也可以直接在函数列表里面找到,只要搜索OO0OoOOo.Oo0
即可,会出现一大堆OO0OoOOo.Oo0$$
开头的函数,就是VM Handler。
VM Handler逆向
1 | dispatcher |
注册机代码
1 |
|
参考
[原创]细品sec2023安卓赛题-Android安全-看雪-安全社区|安全招聘|kanxue.com
Perfare/Il2CppDumper: Unity il2cpp reverse engineer (github.com)
[Tutorial] Il2CppDumper tutorial finding CodeRegistration and MetadataRegistration (unknowncheats.me)
[求助]如何修改frida-server 服务端的默认监听端口?-求助问答-看雪-安全社区|安全招聘|kanxue.com
Java Native Interface Specification: 4 - JNI Functions (oracle.com)
[原创]2023腾讯游戏安全竞赛初赛题解(安卓)-Android安全-看雪-安全社区|安全招聘|kanxue.com(已经阅读完)
- 本文作者: Taardis
- 本文链接: https://taardisaa.github.io/2023/09/15/Android逆向解题-sec2023安卓赛题/
- 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!