首页
社区
课程
招聘
[原创]2023腾讯游戏安全竞赛初赛题解(安卓)
2023-4-22 01:03 37258

[原创]2023腾讯游戏安全竞赛初赛题解(安卓)

2023-4-22 01:03
37258

前言

虽然这次很不走运没拿名次,但是既然都花了这么长时间,还是得好好复现总结总结所学,尤其是才接触安卓两三个月,基础有点不牢固,于是准备重做一遍并记录下过程,分享出来与各逆向大佬共勉

预备工作

启发式扫描

拿到题目首先用 WinRAR 打开,看lib

1
2
3
4
5
lib\arm64-v8a\libil2cpp.so
lib\arm64-v8a\libil2cpp.so.merged.result
lib\arm64-v8a\libmain.so
lib\arm64-v8a\libsec2023.so
lib\arm64-v8a\libunity.so

说明这个是一个 unity il2cpp 框架下的 题目,那么立马就想到 il2cppdumpper

 

提取文件夹下的 assets\bin\Data\Managed\Metadata\global-metadata.dat

 

不出所料失败了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Initializing metadata...
Metadata Version: 29
Initializing il2cpp file...
Applying relocations...
WARNING: find .init_proc
ERROR: This file may be protected.
Il2Cpp Version: 29
Detected this may be a dump file.
Input il2cpp dump address or input 0 to force continue:
0
Searching...
CodeRegistration : 0
MetadataRegistration : 0
ERROR: No symbol is detected
ERROR: Can't use auto mode to process file, try manual mode.
Input CodeRegistration:

尝试使用Zygisk-Il2CppDumper,也失败了,那么到此只能使用终极 dump 方案

 

adb install apk 之后用 gg 在内存中 dump 解密之后的 libil2cpp.so

 

dump 过程如下,选择起始

 

 

以及结尾,然后保存就可以了

 

 

这时候用 il2cppdumper 选择 dump 下来的二进制文件,填上 so 在内存中的基址,直接 dump 下来了,甚至不需要手动找CodeRegistration 和 MetadataRegistration, global-metadata.dat 文件也没有被修改,如果被修改了也可以从内存中 dump 下来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Initializing metadata...
Metadata Version: 29
Initializing il2cpp file...
Applying relocations...
Il2Cpp Version: 29
Detected this may be a dump file.
Input il2cpp dump address or input 0 to force continue:
7ad74c3000
Searching...
CodeRegistration : 7ad85254d0
MetadataRegistration : 7ad856ff38
Dumping...
Done!
Generate struct...
Done!
Generate dummy dll...
Done!
Press any key to exit...

到此拿到了

1
2
3
4
5
DummyDll
dump.cs
il2cpp.h
script.json
stringliteral.json

但是还没结束,dump的文件没有导入导出函数的符号,而 il2cpp 符号的脚本又是针对于 dump 文件的,所以可以顺手修复一下 dump 的文件头之类的,具体如下操作

 

首先可以看见 SECTION_HEADER 处一片空白,暂且不管

 

 

将 program_table 的每一个的 p_offset 改成 p_vaddr,以及 p_filesz 改成 p_memsz

 

 

值得注意的是第十一个表,这个表的结尾地址,正是之前 SECTION_HEADER 的开头处,也就是说 SECTION_HEADER 本来应该是移动到 0x13BC000+63352 = 0x13CB778,虽然这个地方是空的,但是暂且不管,把这个地址填到 SECTION_HEADER 处再说

 

 

修改完按 program_table 按一下 F5 就会重新分析了,也可以看到一片空白的 section_header

 

 

直接十六进制复制粘贴,即 Ctrl+shift+C Ctrl+shift+V,再 F5 刷新

 

可以看见字符还是对应不上,这其实是由 elf_header 中的 e_shtrndx 指定的,e_shtrndx 为26,意思是第 26 个指定了字符串的位置

 

 

索引一下就可以发现问题所在,是因为这个地方被重定位了

 

 

而在第 26 个表中 s_addr 为 0,说明这个 Section 是不会存在于运行的文件内的,然而往上翻发现了有趣的事情

 

 

没错,第 25 个表的 s_offset 与第 26 个的一样,但是他有 s_addr,所以可以确定第 26 个表的 s_addr 与第 25 个的一样,顺手把第 25个 s_offset 覆盖成 s_addr

 

不幸的是又出现了空白,不过没关系,直接二进制复制粘贴

 

 

除此之外,把其他的所有 section 中的 s_offset 覆盖成 s_addr,最后再按一次 F5 重载模板,就可以看见心心念念的符号了

 

 

最后记得保存,不然白忙活一场

 

将 dump 文件载入 ida 之后,最好是 Rebase 一下,因为是运行态文件,可能有些内存值已经被重定位,因此 Rebase 之后可能得到更多符号, Rebase 的值就是 dump 文件的载入地址,文件名上就有,比如我的是 0x7ad74c3000

 

 

等待 ida 初轮分析完之后,加载两次 il2cppdumper 的 ida_with_struct_py3.py 脚本,选择 script.json 和 il2cpp.h, 第二次的 json 选择 stringliteral.json,一阵分析之后,基于 il2cpp 能拿到的符号就全拿到了

Anti-antidebug

经过尝试,发现直接挂 frida 服务上游戏,会直接弹窗,但是后开 frida 服务不会弹

 

后来发现 hook libsec2023.so 会弹窗,但是有一定延时才会弹出来,猜测调用 sleep, 尝试 hook 一下 sleep 发现真的不弹了,

 

包括 ida 附加的时候也不会闪退了

 

hook 的 frida 脚本如下,,既然是复现那就我自己来看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//frida -U -f com.com.sec2023.rocketmouse.mouse -l ./desktop/antidebug.js
function AntiDebug()
{
    var sleep_addr = Module.findExportByName(null, "sleep");
    var sleep = new NativeFunction(sleep_addr, "void",["int"]);
    Interceptor.attach(sleep,
    {
        onEnter: function (args)
        {
            args[0] = ptr(1000000);
            console.log("hooked sleep");
            console.log('sleep called from:\n' + Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n') + '\n');
        },
        onLeave: function (returnValue)
        {
        }
    })
}AntiDebug();

用 frida 上号直接可以看见打印 log

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
hooked sleep
hooked sleep
hooked sleep
sleep called from:
0x7b3c2bc908 libsec2023.so!0x36908
0x7b3c2bc7f8 libsec2023.so!0x367f8
0x7b3c2be708 libsec2023.so!0x38708
0x7b3c2be5a0 libsec2023.so!0x385a0
0x7b3c2bc278 libsec2023.so!0x36278
0x7b3c297f44 libsec2023.so!0x11f44
0x7bd4bd350c libc.so!_ZL15__pthread_startPv+0x44
0x7bd4b73b10 libc.so!__start_thread+0x44
 
sleep called from:
0x7b3c2bc794 libsec2023.so!0x36794
0x7b3c297f44 libsec2023.so!0x11f44
0x7bd4bd350c libc.so!_ZL15__pthread_startPv+0x44
0x7bd4b73b10 libc.so!__start_thread+0x44
 
sleep called from:
0x7b3c2a80a8 libsec2023.so!0x220a8
0x7b3c297f44 libsec2023.so!0x11f44
0x7bd4bd350c libc.so!_ZL15__pthread_startPv+0x44
0x7bd4b73b10 libc.so!__start_thread+0x44
 
hooked sleep
sleep called from:
0x7bd3e16b24 libEGL.so!0x13b24
0x7bd4bd350c libc.so!_ZL15__pthread_startPv+0x44
0x7bd4b73b10 libc.so!__start_thread+0x44

最后一个是弹出了一个什么权限我拒绝之后 hook 到的,应该是没用的,那么主要就是分析前面的 libsec2023.so 的函数

 

0x11f44 是函数 0x11f24 的一部分,这个函数看上去像是某种初始化,说明重点不在这,重点在下一步 call 的函数

 

 

0x220a8 是函数 0x22080 的一部分,这个 sleep 在函数的头部,但是这整个函数充斥着混淆,太难了不看

 

 

0x36794 是函数 0x36784 的一部分

 

 

 

 

看上去好像没什么异常的地方

 

0x36908 是 0x36818 的一部分,而这个函数...发现了很熟悉的操作

 

 

以我上一次做这个的经验,这里是一个字符串解密,解密函数 0x36f6c,修复去混淆之后重建函数,逻辑如下

 

 

解密之后

 

 

0x36818 剩下的逻辑也没啥好看的,其实是看不懂,用插件确实能找到 CRC 常数,不过却没有引用,算了算了,能跑就不管了,反反调试就到这吧

 

另外提一嘴,关于 ida 调试的事情

 

首先一定要新开 ida,不要用分析了 so 的 ida 附加,否则退出调试的时候会卡死,之前的分析就炸了,推荐是动静结合来分析

 

还有一件很重要的事情是关闭一些异常处理

 

Debugger Options...->Edit exceptions->

 

SIGPWR SIGXCPU 右键 edit,去掉挂起程序的勾,并勾上通过引用,report 选 log 或者 Silent 都行

 

 

这一点可能是因为 unity 的问题或者是 Android 的问题,调着调着就会弹这俩个异常,如果挂起程序,程序就会崩溃,程序崩溃又会导致 ida 崩溃,而设置完之后就不会出现这个情况了

 

还有一件事,程序暂停调试的时候不要点击屏幕,否则会出现和前面异常挂起一样的问题

开始分析

getflag

按照说明,游戏内获得 1000 金币的时候,会出现 flag

 

那么肯定需要有个地方来判断金币数量,金币一般是用 coin 表示,在 dump.cs 中可以找到

 

 

同时发现 CollectCoin 函数

1
2
// RVA: 0x4652E4 Offset: 0x4652E4 VA: 0x7AD79282E4
private void CollectCoin(Collider2D coinCollider) { }

在有符号的 dump 中可以直接跳转到这个函数,也很容易发现相关逻辑

 

 

用 frida 改一下字节,比较1000改成比较0,这样随便捡一个金币就可以显示 flag 了

1
2
3
4
5
6
7
8
function main()
{
    var g_libil2cpp_addr = Module.findBaseAddress("libil2cpp.so");
    var coin_cmp = g_libil2cpp_addr.add(0x4653cc);
    Memory.protect(coin_cmp, 4, "rwx");
    Memory.writeInt(coin_cmp, 0x7100001f);//cmp w0,0
    Memory.protect(coin_cmp, 4, "rx");
}main();

效果如下

 

注册机

这游戏自带外挂,但是必须输入 token 相对应的密码才能够使用,所以所谓注册机就是关于这个密码的逆向

 

注册机界面如下

 

 

本来没有什么下手点,但是从 dump.cs coin 往下随便翻翻,看到了 SmallKeyboard 相关的函数

 

甚至专门混淆了函数名,简直是此地无银三百两

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
public enum SmallKeyboard.KeyboardType // TypeDefIndex: 3310
{
    // Fields
    public int value__; // 0x0
    public const SmallKeyboard.KeyboardType Number = 0;
    public const SmallKeyboard.KeyboardType Character = 1;
    public const SmallKeyboard.KeyboardType EnterKey = 2;
    public const SmallKeyboard.KeyboardType BackSpace = 3;
}
 
public class SmallKeyboard : MonoBehaviour // TypeDefIndex: 3313
{
    // Fields
    public static int KeyboardNum; // 0x0
    public string strInput; // 0x18
    public GameObject inputObj; // 0x20
    public List<SmallKeyboard.iII1i> list; // 0x28
    public GameObject IDObj; // 0x30
    public string oO0o0o0; // 0x38
    public string iIIIi; // 0x40
 
    // Methods
 
    // RVA: 0x465880 Offset: 0x465880 VA: 0x7AD7928880
    private void iI1Ii(SmallKeyboard.iII1i _info) { }
 
    // RVA: 0x465FDC Offset: 0x465FDC VA: 0x7AD7928FDC
    private void iI1Ii(GameObject go) { }
 
    // RVA: 0x465AB0 Offset: 0x465AB0 VA: 0x7AD7928AB0
    private void iI1Ii(ulong i1I) { }
 
    // RVA: 0x465E90 Offset: 0x465E90 VA: 0x7AD7928E90
    private void oO0oOo0() { }
 
    // RVA: 0x466184 Offset: 0x466184 VA: 0x7AD7929184
    private string oO0oOoO() { }
 
    // RVA: 0x46618C Offset: 0x46618C VA: 0x7AD792918C
    private void Start() { }
 
    // RVA: 0x466300 Offset: 0x466300 VA: 0x7AD7929300
    public void .ctor() { }
}

直接看第一个函数就一步到位了

 

 

先看看后面那个oO0oOo0

 

 

很清晰就是生成随机 TOKEN 的逻辑,按照逻辑这个函数应该点进来会执行,enter 以后又会执行以刷新,看看引用果然如此

 

 

那么 SmallKeyboard__iI1Ii_527602715312 函数很显然就是 check 函数了

1
2
// RVA: 0x465AB0 Offset: 0x465AB0 VA: 0x7AD7928AB0 = 527602715312
private void iI1Ii(ulong i1I) { }

这个函数只有一个 int64的参数

 

往里走这个函数被 init_proc 搞得有点坏了

 

 

把 init_proc 函数 u 掉,再重新p就好了

 

 

而这个函数的庐山真面目其实是

 

 

然而 g_sec2023_p_array 是导入的

 

 

导入库中,只有 libsec2023.so 是非系统库

 

那么分析转到 libsec2023.so 中

 

 

可以看到 g_sec2023_p_array[9] 实际上就是 libsec2023.so + 0x31164

 

 

这个函数逻辑又是调用 g_sec2023_o_array,参数是类实例和 sub_3B8CC(input)

 

索引 g_sec2023_o_array 发现没有写入,那就说明是在其他 so 里写的,看一下 libil2cpp 里的调用果然找到了

 

 

最终这个函数调用到了 sub_7AD887BD64 (libil2cpp.so + 0x13b8d64)

 

可以猜测下大体逻辑,实际的话还是一边调试一边走比较好

 

 

索引一下最后比较成功他写的东西

 

 

 

虽然看的不是很懂,但是应该是开挂之类的,前面那个是无敌我还是看的明白的

 

所以到此,大体逻辑就已经看明白了,那么第一个用到 input 的函数是 sec2023 中的 sub_3b8cc,这里面有一点小混淆,掌握了规律还是很好去掉的

 

先拿 sub_3B8CC 里的函数 sub_3B9D4 举例

 

 

第一个X10,计算过程如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if(0<2)
    b10 = off_72C40[0]
else
    b10 = off_72C40[5]
b10 += 0x740078FC;
 
.data:0000000000072C40 08 41 03 8C FF FF FF FF       off_72C40 DCQ 0xFFFFFFFF8C034108
.data:0000000000072C48 30 42 03 8C FF FF FF FF       DCQ 0xFFFFFFFF8C034230
.data:0000000000072C50 38 41 03 8C FF FF FF FF       DCQ 0xFFFFFFFF8C034138
.data:0000000000072C58 78 41 03 8C FF FF FF FF       DCQ 0xFFFFFFFF8C034178
.data:0000000000072C60 E4 41 03 8C FF FF FF FF       DCQ 0xFFFFFFFF8C0341E4
.data:0000000000072C68 54 42 03 8C FF FF FF FF       DCQ 0xFFFFFFFF8C034254
 
0:0xFFFFFFFF8C034108 + 0x740078FC = 0x3BA04
8:0x3BB2C
10:0x3BA34
18:0x3BA74
20:0x3BAE0
28:0x3BB50

0x3BA04 可以当作是小于的时候的执行,那么不小于的时候就要跳转到 0x3BB50

 

也就是 B.GE loc_3BB50

 

后面的一般都是这样的套路,手动计算跳转,然后满足原逻辑即可.当然 patch 完需要先 u c 下面的函数,然后按 u p 重建最上面的函数,才能恢复整个函数

 

 

直到 patch 到 ret,就说明这个函数已经去混淆完了,F5 可以看到清晰的逻辑

 

 

逆算法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void Decode1(unsigned int* flag)
{
    for(int j = 0; j < 2; ++j)
    {
        unsigned int v7 = flag[j];
        //v7 += 0x18100800;
        ((unsigned char *)&v7)[0] += 0x00;
        ((unsigned char *)&v7)[1] += 0x08;
        ((unsigned char *)&v7)[2] += 0x10;
        ((unsigned char *)&v7)[3] += 0x18;
 
        ((unsigned char *)&v7)[3] ^= 0x86;
        ((unsigned char *)&v7)[2] += 94;
        ((unsigned char *)&v7)[1] ^= 0xD3;
        ((unsigned char *)&v7)[0] += 28;
 
        for(int i = 3; i >= 0; --i)
        {
            ((unsigned char*)flag)[j * 4 + i] = (unsigned char)(v7 >> (i * 8)) ^ i;
        }
    }
}

我的 patch 点如下

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
sub_3B9D4
.text:000000000003BA00 8A 0A 00 54                   B.GE            loc_3BB50
.text:000000000003BA30 23 02 00 54                   B.CC            loc_3BA74
.text:000000000003BA70 2A FE FF 54                   B.GE            loc_3BA34
.text:000000000003BADC 14 00 00 94                   B.CC            loc_3BB2C
.text:000000000003BB28 CA FD FF 54                   B.GE            loc_3BAE0
.text:000000000003BB4C C3 F5 FF 54                   B.CC            loc_3BA04
 
sub_3A054
.text:000000000003A08C 21 03 00 54                   B.NE            loc_3A0F0
.text:000000000003A0C8 41 01 00 54                   B.NE            loc_3A0F0
 
sub_3B4B8
.text:000000000003B508 1F 20 03 D5                   NOP
.text:000000000003B54C 0D FE FF 54                   B.LE            loc_3B50C
 
sub_3B570
.text:000000000003B5C0 1F 20 03 D5                   NOP
.text:000000000003B604 0D FE FF 54                   B.LE            loc_3B5C4
 
sub_3A924
.text:000000000003AA70 40 05 00 54                   B.EQ            loc_3AB18
.text:000000000003AAA0 C0 03 00 54                   B.EQ            loc_3AB18
.text:000000000003AAD4 A0 01 00 54                   B.EQ            loc_3AB08
.text:000000000003AB04 60 03 00 54                   B.EQ            loc_3AB70
 
sub_3B8CC
.text:000000000003B950 61 00 00 54                   B.NE            loc_3B95C
.text:000000000003B990 61 00 00 54                   B.EQ            loc_3B99C

patch 之后就看的很清楚了

 

 

0x3A054 像是个什么初始化的函数,不像在运算

 

0x3A924 用了 v9,也就是经过 0x3b9d4 加密后的翻转过的 input

 

这个函数中间解密了两次字符串,很明显是一个 java 层的函数,至于为什么这里有 JNIEnv 的函数自然是我盲猜之后转换的

 

 

不过把 apk 放到 jadx 里之后并没有找到这个函数,猜测是动态加载的

 

果然在内存中找到了,同样用 gg 把它 dump 下来

1
7bd11f6000-7bd11f8000 r--p 00000000 fd:0a 297064                         /data/data/com.com.sec2023.rocketmouse.mouse/files/encrypt.dex (deleted)

用 jadx 可以反编译这个 dex,果然找到了 encrypt 函数

 

 

混淆了控制流,不过整体不是很大,还是可以手动还原的,具体还原的时候可以通过和 Smali 代码一起看,因为反编译的字符串有些问题

 

具体还原的时候用在线 java 弄个 String 就可以取 hashCode 了

 

 

这一部分的正向逻辑如下..算了懒得再逆一遍了,直接放逆算法

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
void Decode2(int* flag)
{
    char temp[] = {50, -51, -1, -104, 25, -78, 0x7C, -102};
    for(int j = 0; j < 2; ++j)
    {
        unsigned int v7 = flag[j];
        for(int i = 0; i < 4; ++i)
        {
            ((char *)&v7)[i] -= i;
            ((char *)&v7)[i] ^= temp[i % 8];
        }
 
        char v77[4] = {};
        v77[0] = ((char *)&v7)[3];
        v77[1] = ((char *)&v7)[2];
        v77[2] = ((char *)&v7)[1];
        v77[3] = ((char *)&v7)[0];
        v7 = *(int*)v77;
        //printf("0x%x\n", v7);
 
        v7 = (unsigned int)(v7 << 7) | (v7 >> 25);
 
        v77[0] = ((char *)&v7)[3];
        v77[1] = ((char *)&v7)[2];
        v77[2] = ((char *)&v7)[1];
        v77[3] = ((char *)&v7)[0];
        v7 = *(int*)v77;
        //printf("0x%x\n", v7);
        flag[j] = v7;
    }
}

到此,算法继续回到 libil2cpp 中

 

先把后面的 Tea 解决了,key 可以通过 hook InitializeArray 或者直接调试都可以拿到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int* DecodeTea(int RandNum)
{
    unsigned int v20 = 0xBEEFBEEF - 0x21524111 * 64;
    unsigned int v21 = 0x9D9D7DDE - 0x21524111 * 64;
    int v22 = 64;
    char key_char[] = {0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76};
 
    int* key = (int*)key_char;
    unsigned int v16 = RandNum;
    unsigned int v17 = 0;
    do
    {
        v21 += 0x21524111;
        v17 -= (v21 + key[(v21 >> 13) & 3]) ^ (((v16 << 8) ^ (v16 >> 7)) - v16);
        v20 += 0x21524111;
        v16 -= (v20 - key[v20 & 3]) ^ (((v17 << 7) ^ (v17 >> 8)) + v17);
        --v22;
    }
    while ( v22 );
    unsigned int* temp = (int*)malloc(8);
    temp[0] = v16;
    temp[1] = v17;
    return temp;
}

看看最后剩下的

1
2
3
OO0OoOOo_Oo0___ctor(v19, (System_UInt16_array *)v15, 0, v13, v22);
v23->fields.oOOO0Oo0 = v23->fields.oOOO0O0O;
OO0OoOOo_Oo0__oOOoO0o0(v23, v24);

可以先稍微改一下 struct,在 Structures 里面就可以找到

 

 

例如这个结构体直接先改成 a1 到 a22,方便观察

 

ctor 一般是初始化的函数,那么下面那个函数应该就是计算函数

 

一眼望过去,可以感觉到非常抽象,不过简单梳理一下,把一些什么判断指针为不为空,什么长度有没有太长的删掉,就好看很多了

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
void __fastcall OO0OoOOo_Oo0__oOOoO0o0(OO0OoOOo_Oo0_o *this, const MethodInfo *method)
{
  OO0OoOOo_Oo0_o *v2; // x19
  __int64 v3; // x1
  struct System_UInt16_array *aa1; // x8
  __int64 aa5; // x9
  OO0OoOOo_oO0OoOOo_c *v6; // x0
  int32_t v7; // w20
  struct System_UInt16_array *v8; // x9
  int32_t v9; // w8
  System_Collections_Generic_Dictionary_TKey__TValue__o *aa8; // x0
  Il2CppObject *Item; // x0
 
  v2 = this;
  aa1 = v2->fields.aa1;
  aa5 = v2->fields.aa5;
  while ( 1 )
  {
    v6 = OO0OoOOo_oO0OoOOo_TypeInfo;
    v7 = aa1->m_Items[aa5 + 2];//v7 又来源于aa1
    if ( v6->static_fields->a18 == v7 )
      break;
    v8 = v2->fields.aa1;
    v9 = v2->fields.aa5;
    aa8 = (System_Collections_Generic_Dictionary_TKey__TValue__o *)v2->fields.aa8;
    v2->fields.aa5 = v9 + 1;
    Item = System_Collections_Generic_Dictionary_int__object___get_Item(
             aa8,
             v7,
             (const MethodInfo_7C771C *)Method_System_Collections_Generic_Dictionary_int__Action__get_Item__);
      //Item 的来源和 v7 和 this.fields.aa8 有关
    this = (OO0OoOOo_Oo0_o *)((__int64 (__fastcall *)(Il2CppClass *, void *))Item[1].monitor)(
                               Item[4].klass,
                               Item[2].monitor);//调用 Item 的函数
    aa1 = v2->fields.aa1;
    aa5 = v2->fields.aa5;
  }
}

有点 vm 的意思,一个 while 循环,然后不断从表里拿东西,那么 aa5 就是 eip,aa1 就是 cmd

 

cmd 在初始化中来源于第二个参数

 

 

第二个参数来源于一个 199 的 array,验证了猜想,可以通过动调或者 hook 函数的方式 dump 下来

 

 

同时 eip 又是等于 aa7, aa7 是初始化的第 3 个参数,为0

 

 

vm 题型里有了指令,还需要解决 handler

 

ctor 中还有一个函数,似乎设置了一些函数

 

 

设置值来源于 OO0OoOOo_oO0OoOOo_TypeInfo.static_fields,通过索引可以看见,是设置为了 1~22

 

 

那就可以合理推测这里是设置了 1 ~ 22 对应的函数了

 

逐个分析,然后通过指令列表回推算法即可,偷懒直接把上次做的粘贴过来

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
dispatcher
{
    a5++;
    get_Item(key: cmd->m_Items[a5 - 1]).method();
}
 
1//add
{
    v6 = buffer->m_Items[a4];
    v8 = buffer->m_Items[a4 - 1];
    buffer->m_Items[--a4] = v8 + v6;
}
 
2//sub
{
    v6 = buffer->m_Items[a4];
    v8 = buffer->m_Items[a4 - 1];
    buffer->m_Items[--a4] = v8 - v6;
}
 
4//<
{
    v6 = buffer->m_Items[a4];
    v8 = buffer->m_Items[a4 - 1];
    buffer->m_Items[--a4] = v8 < v6;
}
 
5//==
{
    v6 = buffer->m_Items[a4];
    v8 = buffer->m_Items[a4 - 1];
    buffer->m_Items[--a4] = v8 == v6;
}
 
7//je
{
    if ( buffer->m_Items[a4--] == 1 )
        v7 = cmd->m_Items[v5];
    else
        v7 = a5 + 1;
    a5 = v7;
}
 
8//je
{
    if ( buffer->m_Items[a4--] )
        v7 = a5 + 1;
    else
        v7 = cmd->m_Items[v5];
    a5 = v7;
}
 
9//push from cmd[]
{
   buffer->m_Items[++a4] = cmd->m_Items[a5++];
}
 
a//[]
{
    buffer->m_Items[a4] = input->m_Items[buffer->m_Items[a4]];
}
 
b//push from input[cmd[]]
{
    buffer->m_Items[++a4] = input->m_Items[cmd->m_Items[a5++]];
}
 
d//pop
{
    input->m_Items[cmd->m_Items[a5++]] = buffer->m_Items[a4--];
}
 
13//>>
{
    v6 = buffer->m_Items[a4];
    v8 = buffer->m_Items[a4 - 1];
    buffer->m_Items[--a4] = v8 >> v6;
}
 
14//<<
{
    v6 = buffer->m_Items[a4];
    v8 = buffer->m_Items[a4 - 1];
    buffer->m_Items[--a4] = v8 >> v6;
}
 
15//and
{
    v6 = buffer->m_Items[a4];
    v8 = buffer->m_Items[a4 - 1];
    buffer->m_Items[--a4] = v6 & v8;
}
 
16//xor
{
    v6 = buffer->m_Items[a4];
    v8 = buffer->m_Items[a4 - 1];
    buffer->m_Items[--a4] = v6 ^ v8;
}

具体推导算法过程我也偷个懒不重复推导了,到此所有算法都出来了,直接贴计算脚本

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
#include<stdio.h>
#include <stdlib.h>
 
void vmDecode(int* flag)
{
    for(int i = 0; i <= 3; ++i)
        ((char*)flag)[4 + i] -= i * 8;
 
    ((char*)flag)[7] ^= 0x98;
    ((char*)flag)[6] -= 0x37;
    ((char*)flag)[5] ^= 0xb6;
    ((char*)flag)[4] += 0x2f;
 
    for(int i = 0; i <= 3; ++i)
        ((char*)flag)[i] ^= i * 8;
 
    ((char*)flag)[3] ^= 0x36;
    ((char*)flag)[2] -= 0xa8;
    ((char*)flag)[1] ^= 0xc2;
    ((char*)flag)[0] += 0x1b;
}
 
void Decode2(int* flag)
{
    char temp[] = {50, -51, -1, -104, 25, -78, 0x7C, -102};
    for(int j = 0; j < 2; ++j)
    {
        unsigned int v7 = flag[j];
        for(int i = 0; i < 4; ++i)
        {
            ((char *)&v7)[i] -= i;
            ((char *)&v7)[i] ^= temp[i % 8];
        }
 
        char v77[4] = {};
        v77[0] = ((char *)&v7)[3];
        v77[1] = ((char *)&v7)[2];
        v77[2] = ((char *)&v7)[1];
        v77[3] = ((char *)&v7)[0];
        v7 = *(int*)v77;
        //printf("0x%x\n", v7);
 
        v7 = (unsigned int)(v7 << 7) | (v7 >> 25);
 
        v77[0] = ((char *)&v7)[3];
        v77[1] = ((char *)&v7)[2];
        v77[2] = ((char *)&v7)[1];
        v77[3] = ((char *)&v7)[0];
        v7 = *(int*)v77;
        //printf("0x%x\n", v7);
        flag[j] = v7;
    }
}
 
void Decode1(unsigned int* flag)
{
    for(int j = 0; j < 2; ++j)
    {
        unsigned int v7 = flag[j];
        //v7 += 0x18100800;
        ((unsigned char *)&v7)[0] += 0x00;
        ((unsigned char *)&v7)[1] += 0x08;
        ((unsigned char *)&v7)[2] += 0x10;
        ((unsigned char *)&v7)[3] += 0x18;
 
        ((unsigned char *)&v7)[3] ^= 0x86;
        ((unsigned char *)&v7)[2] += 94;
        ((unsigned char *)&v7)[1] ^= 0xD3;
        ((unsigned char *)&v7)[0] += 28;
 
        for(int i = 3; i >= 0; --i)
        {
            ((unsigned char*)flag)[j * 4 + i] = (unsigned char)(v7 >> (i * 8)) ^ i;
        }
    }
}
 
void ObfuDecode(unsigned int* flag)//sub_3B8CC
{
    vmDecode(flag);
 
    Decode2(flag);//sub_3A054
 
    char v77[4] = {};
    v77[0] = ((char *)flag)[3];
    v77[1] = ((char *)flag)[2];
    v77[2] = ((char *)flag)[1];
    v77[3] = ((char *)flag)[0];
    flag[0] = *(int*)v77;
 
 
    v77[0] = ((char *)flag)[7];
    v77[1] = ((char *)flag)[6];
    v77[2] = ((char *)flag)[5];
    v77[3] = ((char *)flag)[4];
    flag[1] = *(int*)v77;
 
 
    Decode1(flag);//sub_3B9D4
 
}
 
int* DecodeTea(int RandNum)
{
    unsigned int v20 = 0xBEEFBEEF - 0x21524111 * 64;
    unsigned int v21 = 0x9D9D7DDE - 0x21524111 * 64;
    int v22 = 64;
    char key_char[] = {0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76};
 
    int* key = (int*)key_char;
    unsigned int v16 = RandNum;
    unsigned int v17 = 0;
    do
    {
        v21 += 0x21524111;
        v17 -= (v21 + key[(v21 >> 13) & 3]) ^ (((v16 << 8) ^ (v16 >> 7)) - v16);
        v20 += 0x21524111;
        v16 -= (v20 - key[v20 & 3]) ^ (((v17 << 7) ^ (v17 >> 8)) + v17);
        --v22;
    }
    while ( v22 );
    unsigned int* temp = (int*)malloc(8);
    temp[0] = v16;
    temp[1] = v17;
    return temp;
}
 
 
int main()
{
    printf("pls input rand:");
    int rand;
    scanf("%d", &rand);
    int* temp = DecodeTea(rand);
 
    //unsigned int* temp = malloc(10);
    //temp[0] = 0x4d09f96f;
    //temp[1] = 0x2d55b70a;
 
    ObfuDecode(temp);
 
    printf("%llu\n", (((unsigned long long)temp[0]<<32) | (temp[1] & 0xffffffff)));
    free(temp);
}

后记

决赛的放下一篇明天再写


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

最后于 2023-4-22 20:49 被|_|sher编辑 ,原因:
收藏
点赞11
打赏
分享
最新回复 (7)
雪    币: 2160
活跃值: (3518)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
R0g 2 2023-4-22 01:18
2
0
前排支持
雪    币: 62
活跃值: (556)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
万里星河 2023-4-22 18:34
3
0
牛逼
雪    币: 342
活跃值: (345)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
r0k 2023-4-26 09:55
4
0
NB 学到了
雪    币: 375
活跃值: (427)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
yinX 2 2023-5-24 17:52
5
0
楼主你好,请教一下,有一个地方我看了很久实在是没有看懂。
“直接十六进制复制粘贴,即 Ctrl+shift+C Ctrl+shift+V,再 F5 刷新”

这里的源数据是从哪里来的呢?要从哪里复制数据到section_header去呢?期待答复,谢谢!
雪    币: 375
活跃值: (427)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
yinX 2 2023-5-24 19:06
6
0
哦,我知道了,从原来的libil2cpp.so里复制section_header到dump出来的bin里。
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
NOOB@ToT 2023-8-31 15:03
7
0
师傅我完全不懂so结构,但是通过so_fixer项目是否可行
雪    币: 5879
活跃值: (4457)
能力值: ( LV10,RANK:160 )
在线值:
发帖
回帖
粉丝
淡然他徒弟 1 2023-9-7 12:27
8
0
mark
游客
登录 | 注册 方可回帖
返回