首页
社区
课程
招聘
[原创]2023腾讯游戏安全大赛-安卓赛道决赛wp
2023-4-17 20:12 42457

[原创]2023腾讯游戏安全大赛-安卓赛道决赛wp

2023-4-17 20:12
42457

说明

​ 本文档是进行完相关分析之后的总结回忆, 所以可能有的地方的花指令是被去除了再截图的, 函数名和数据结构被重命名了. 这决赛的混淆太恶心了,我无法做到将代码还原, 所以完全手撕汇编做的.

flag获取分析

​ 这一部分和初赛是一样的, il2cpp那块并没有过多的加密, 还是和初赛一样从内存中将libil2cpp.so dump下来之后使用il2cppdumper得到对应的cs文件, 定位到MouseController__CollectCoin方法中, 找到偏移0x4652AC这里, 使用gg修改器将CMP W0, #0x3E8这条指令修改为CMP W0, #0之后随便吃一个金币得到flag.

 

反调试分析

​ 有了初赛的经验, 所以这次也是将frida_server的二进制文件patch掉re.frida.server字符串, 并且使用./fs -l 0.0.0.0:2394命令切换监听端口, 为了保证万无一失, 将工作目录从tmp文件夹移到别的地方取, 这样一来就能够成功过掉这一大类的检测.

 

​ 但是也是一样的, 除了文件检测和端口检测, libsec2023.so中还有针对代码段的crc检测, 使用findcrypt插件从so文件的特征中也能够发现. 意外的这里还看到了aes特征, 后面的算法也有可能使用到了aes算法, 只是说可能.

 

 

​ 也是同初赛一样, 对于crc检测一定是不间断的读取代码段数据, 所以只需要对其代码段下一个硬件读写断点就能够定位到检测的关键点. 我依旧是使用rwProcMem项目中的内核硬件断点程序进行操作, 在libsec2023.so的内存区域中随意选择一个地址, 如下图所示成功定位到读取的关键点.

 

 

​ 此时libsec2023.so的内存基址是0x7cecf00000. 可以定位到读取内存段的代码在libsec2023.so + 0x7d934处, 这里因为混淆方式的改变, 所以f5也不能和初赛一样将其还原出来, 因此只能查看汇编代码.

 

 

​ 这里因为被混淆了, 并且这里只是做了读取内存的操作, 所以找不到合适的patch点, 此时查看lr寄存器, 可以得到调用读取函数的返回的上一层地址是libsec2023.so + 0x7B300.

 

 

​ 这里能够看到0x7B2FC的blr x8就是调用点, 读取完代码段的校验和之后返回值会存储在x0寄存器中, 因此可以按照此为线索往下面拉, 看到0x7B320处有一个cmp w0, w8. 之后一个b跳转到0x7AF00处

 

 

​ 在0x7AF04这里会依据前面的cmp语句的结果对w8进行赋值, 而w8的值就会影响到之后的分支, 所以这就能够找到patch的关键点了, 用硬件断点工具下执行断点查看寄存器的数据可以发现正常流程中w0和w8的值应当是相等的, 所以要越过crc检测则应该将cmp w0, w8语句改成mov w0, w8, 同时将csel w8, w9, w8, eq改成mov w8, w9. 实践中发现后面那句不改也可以, eq默认成立.

 

​ 所以在frida的脚本中初始化中加入以下语句即可彻底过掉检测.

1
2
3
4
5
6
function PatchCode(addr, value){
    Memory.protect(addr, 4, 'rwx');
    addr.writeInt(value);
}
 
PatchCode(libsec2023.base.add(0x7B320), 0x2a0803e0);//过crc32

​ 至此反调试就分析完毕, frida能够正常使用了.

算法分析

​ 相比于初赛, 决赛的混淆程度更加严重了, ida的伪代码功能彻底报废, 程序中函数的跳转和程序内分支语句全部变成了寄存器间接跳转, 对于函数跳转尚可使用unidbg这种模拟执行的方式还原, 但是条件分支具有动态性, 所以很难还原回去. 所以我选择了frida-trace日志静态分析 + frida hook动态的方式进行分析.

算法执行流程

​ 同样先大概看了一下流程, 同初赛一样, 刚开始会从libil2cpp.so中接收输入数字, 然后传入后续的算法执行, 然后生成随机的token.

 

 

​ 通过frida hook可以得知这里的跳转会跳转到libsec2023.so + 0x70E74.

 

 

​ 上图的sub_94368便是加密算法的主体部分, v5跳转则会跳回libil2cpp.so + 0x465994, 参数v3是返回的int64值.

 

 

​ 返回的值如果满足低32位等于token, 高32为0, 则开启mod. 不同于初赛, 这里最后并没有其他校验, 所以算法的全部实现都在libsec2023.so中.

离散对数

​ 首先假设输入的数字是666666, 对应的十六进制表达是0xa2c2a. 先进入sub_94368函数, 从这里开始的所有代码都被严重混淆了, 截图为我部分修复之后的伪代码. 其中要满足strcmp结果一致之后才会进入离散对数算法之后的后续逻辑

 

 

​ 在这个函数中主要是初始化了两个大数数组, 之后进入sub_9e93c中进行某种运算, 其中第一个参数是输入的数字和0xffff00ffffff进行按位与运算的结果. 对于输入数字数字666666, 其计算的结果是1db29b949927d377f0270e1161964bb0b9fc004f7125b6956d057e09a7c2fadf77df04bbcae93aa0000字符串, 之后将s2的字符串与前面初始化的某个数组使用strcmp函数进行比较, 经过hook获取发现该数组是一个恒定不变的字符串25f6b048b4f32e3ce9175bb64930f65101a706ae74988a4ec87b4d5ec7feb9223ab782bcf1ec9d7fee750. 刚开始看到这个字符串想到了哈希, 但是具体是什么还需要进入sub_9e93c函数查看.

 

 

​ sub_9e93c函数首先会进行一系列初始化, 然后进入sub_9F0A8函数和sub_9f704中, 使用frida hook也能够发现输入的数字是在最后执行完sub_9f704出现了字符串. 理所应当先进入sub_9f704看看这个字符串是如何产生的, 查看frida-trace的log能够发现其在某处开始会每8个一组产生0, 一直产出了168个0之后才生成想看到的字符串.

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
......
9f934        mov x27, x1 ;      x27 = 0x81d8ae82 --> 0x7ce2ef7850
9f938        adrp x3, #0x7ced00b000 ;      x3 = 0x3762f4b5 --> 0x7ced00b000  
9f93c        add x1, x1, x8 ;
9f940        ldr x8, [sp, #8] ;      x8 = 0x0 --> 0x7ce2ef7470
9f944        add x0, sp, #0x20 ;      x0 = 0x767ac01f --> 0x7ce2ef73e0  
9f948        mov x2, #-1 ;      x2 = 0x471527a8 --> 0xffffffffffffffff
9f94c        add x3, x3, #0x64b ;      x3 = 0x7ced00b000 --> 0x7ced00b64b   (%.08x)
9f950        ldr w4, [x8, x9, lsl #2] ;      x4 = 0x22cd9db --> 0x0   (null)
9f954        ldr w8, [sp, #0x24] ;      x8 = 0x7ce2ef7470 --> 0xeecf7326
9f958        adrp x9, #0x7ced030000 ;      x9 = 0x1f --> 0x7ced030000   (U!3})
9f95c        ldr x9, [x9, #0xa68] ;      x9 = 0x7ced030000 --> 0x7cb537824c
9f960        str w8, [sp, #0x20] ;      str = 0xeecf7326
9f964        mov w8, #0x89f0 ;      x8 = 0xeecf7326 --> 0x89f0
9f968        movk w8, #0x37c2, lsl #16 ;      x8 = 0x89f0 --> 0x37c289f0
9f96c        add x8, x9, x8 ;      x8 = 0x37c289f0 --> 0x7cecfa0c3c  
9f970        blr x8 ;      x0 = 0x7ce2ef73e0 --> 0x8     x1 = 0x7ce2ef7850 --> 0x7ce2ef6b83     x2 = 0xffffffffffffffff --> 0x9     x3 = 0x7ced00b64b --> 0x7ce2ef7260     x4 = 0x0 --> 0x7ce2ef6b84     x5 = 0x50 --> 0x7ce2ef7858     x6 = 0x110 --> 0x30     x7 = 0x108 --> 0x30     x8 = 0x7cecfa0c3c --> 0xd55c7b4c8b012ca8     x9 = 0x7cb537824c --> 0xd55c7b4c8b012ca8     x10 = 0xcd53af93 --> 0x0     x11 = 0x7cecfa092c --> 0x2     x12 = 0x48 --> 0x0     x13 = 0x400 --> 0x7ffffff7     x14 = 0xc8 --> 0xeecf7326     x15 = 0xf0fc4d01 --> 0x0     x16 = 0xd1993aaf --> 0x7dd3e069a0     x17 = 0xb952cbee --> 0x7dd3d4bb94
9f974        ldr w8, [sp, #0x28] ;      x8 = 0xd55c7b4c8b012ca8 --> 0x0   (null)
9f978        ldr w9, [sp, #0x2c] ;      x9 = 0xd55c7b4c8b012ca8 --> 0x1f
9f97c        ldr w10, [sp, #0x24] ;      x10 = 0x0 --> 0xeecf7326
9f980        mov w11, #0x8ff8 ;      x11 = 0x2 --> 0x8ff8
9f984        mov w4, #0xd9db ;      x4 = 0x7ce2ef6b84 --> 0xd9db
9f988        mov w3, #0xf4b5 ;      x3 = 0x7ce2ef7260 --> 0xf4b5
9f98c        mov w2, #0x27a8 ;      x2 = 0x9 --> 0x27a8
9f990        mov w0, #0xc01f ;      x0 = 0x8 --> 0xc01f
9f994        mov w17, #0xcbee ;      x17 = 0x7dd3d4bb94 --> 0xcbee
9f998        mov w16, #0x3aaf ;      x16 = 0x7dd3e069a0 --> 0x3aaf
9f99c        mov w15, #0x4d01 ;      x15 = 0x0 --> 0x4d01
9f9a0        mov x1, x27 ;      x1 = 0x7ce2ef6b83 --> 0x7ce2ef7850   (00000000)
;这里能看到生成了一串0, 这个0就是上面的br跳转得到的
9f9a4        mov w27, #0xae82 ;      x27 = 0x7ce2ef7850 --> 0xae82
......

​ 而上面的那个br跳转则是进入了sub_9FC3C这个函数

 

 

​ 进去查看发现这个函数实现的功能是将某个数字按照十六进制生成字符串, 不足8位补齐, 所以前面的00000000就是这么产生的, 经过hook发现最终生成的一长串数字也是在这里一组一组生成最终拼接而成. 经过详细的比对, 最终确定sub_9f704函数只是一个类似于显示层的函数, 计算出的数字是在sub_9F0A8中生成出来的.

 

​ sub_9F0A8函数存在有控制流混淆, 使用OBPO插件去除混淆后能够得到这样伪代码, 其中注释和函数名称是我经过大量hook与对应trace log比对后得到其对应功能后命名的.

 

 

​ 这个函数最终实现的功能是对于输入数字x, 输出结果y = pow(x, 17) mod key, 其中key的值是已知的, 也就是前面初始化的某个数组之一, 经过hook后得到key为0x028a831a5bf4b902e95318e50c2075259f91094d08d84409e1b76eadfa0865d1278acc90fa7c6cf6acb375. 这个算法是离散对数问题, y为已知的25f6b048b4f32e3ce9175bb64930f65101a706ae74988a4ec87b4d5ec7feb9223ab782bcf1ec9d7fee750, 其逆算法也就是求得输入数字x. 其中x的范围已知是和0xffff00ffffff按位与之后的结果, 那么最简单的逆算法就是爆破枚举.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <gmpxx.h>
 
//这里使用了gmp库来计算大整数, 会暴力输出所有满足的解
//这里得到的解是原始输入int64 & 0xffff00ffffff之后应当满足的部分
void GetAns() {
    mpz_class z("0x28a831a5bf4b902e95318e50c2075259f91094d08d84409e1b76eadfa0865d1278acc90fa7c6cf6acb375", 0);
    mpz_class y("0x25f6b048b4f32e3ce9175bb64930f65101a706ae74988a4ec87b4d5ec7feb9223ab782bcf1ec9d7fee750", 0);
    mpz_class j, x;
    mpz_ui_pow_ui(j.get_mpz_t(), 2, 32); // j = 2^32
    for (int i = 0x0; i <= 0xffff; i++) {
        x = i;
        x *= j; // x = i * 2^32
        for (int k = 0; k <= 0xffffff; k++) {
            x += k;
            mpz_class result;
            mpz_powm_ui(result.get_mpz_t(), x.get_mpz_t(), 17, z.get_mpz_t());
            if (result == y) {
                std::cout << x << std::endl;
            }
        }
    }
    return 0;
}

a64

​ 在满足前面的strcmp条件之后, 程序执行流会进入到sub_99EF4中.

 

 

​ 在上图中能够看到修复后伪代码, 主要是会初始化一些参数. 使用frida-trace对其进行trace能够看到一些奇怪的字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
99ff0           add x0, sp, #0x18 ;      x0 = 0x409 --> 0x7cda7e56f8
99ff4           add x5, sp, #0x10 ;      x5 = 0x50 --> 0x7cda7e56f0
99ff8           add x8, x8, x25 ;        x8 = 0x7d3ae73f70 --> 0x7ce3496970 
99ffc           mov x1, x20 ;    x1 = 0x7cda7e5740 --> 0x7cda7e5750   (builtin)
9a000           mov x2, x21 ;    x2 = 0x471527a8 --> 0x7cda7e5740   (vm_main.img)
9a004           mov x3, x22 ;    x3 = 0x3762f4b5 --> 0x7ce350b698   (PK)
9a008           blr x8 ;         x0 = 0x7cda7e56f8 --> 0x0   (null)      x1 = 0x7cda7e5750 --> 0x984a295f        x2 = 0x7cda7e5740 --> 0x2      x3 = 0x7ce350b698 --> 0xb       x4 = 0x409 --> 0xa97ab639       x5 = 0x7cda7e56f0 --> 0x8095fb42        x6 = 0x110 --> 0xbcf7b136      x7 = 0x108 --> 0x4      x8 = 0x7ce3496970 --> 0xd55c7b4c8b012ca8        x10 = 0xeba2023a --> 0x38
         x11 = 0x7 --> 0x68      x12 = 0x8 --> 0x7ce352dc90      x13 = 0x7ce349552c --> 0x73b4c850       x14 = 0x90409722 --> 0x9       x15 = 0x7a5cace1 --> 0xa        x16 = 0x7ce349af04 --> 0xc      x17 = 0x7ce349542c --> 0x8
9a00c           ldr x8, [x23, #0x40] ;   x8 = 0xd55c7b4c8b012ca8 --> 0x7d3ae771ec
9a010           add x0, sp, #0x18 ;      x0 = 0x0 --> 0x7cda7e56f8
9a014           add x9, x8, x25 ;        x9 = 0xd55c7b4c8b012ca8 --> 0x7ce3499bec   (�)
9a018           add x8, sp, #0x70 ;      x8 = 0x7d3ae771ec --> 0x7cda7e5750   (builtin)
9a01c           blr x9 ;         x0 = 0x7cda7e56f8 --> 0x7cda7e5750      x1 = 0x984a295f --> 0xb400007cd01e8000  x2 = 0x2 --> 0x7ce349bb84      x9 = 0x7ce3499bec --> 0x7d4c11878c      x10 = 0x38 --> 0xffffffff973833f8
9a020           ldr x8, [x23, #0x48] ;   x8 = 0x7cda7e5750 --> 0x7d3ae78a74

​ 其中能看到PK, vm_main, builtin, PK是压缩包的magic标识, 有可能这里的br是执行了解压缩操作, 回溯查看这个PK的来源能够发现是sub_B59DC函数获取该地址的.

 

 

​ 其中libsec2023.so + 0x10A698处便是压缩包的数据.

 

 

​ 使用010editor将其手动提取出来, 打开后可以发现里面存在两个文件a64.data64.sig. 此处尚且知其具体含义, 继续阅读后面的汇编代码, 查看是如何使用该数据的. 果不其然在0x96190附近看到了这样一段汇编记录

1
2
3
4
5
6
7
96178           ldp x0, x1, [sp, #0x28] ;        x0 = 0x7cd4f5c5d8 --> 0x7cdde4b698   (PK)    x1 = 0xc8 --> 0x409
9617c           ldr x2, [sp, #0x20] ;    x2 = 0xd0 --> 0x7cdde4b527   (a64.dat)
96180           add x3, sp, #0x58 ;      x3 = 0x7cdde4b527 --> 0x7cd4f5c5d8
96184           add x8, x8, x26 ;        x8 = 0x7ca616a9ec --> 0x7cddd933dc   (���O��{���)
96188           mov w28, #0x178 ;        x28 = 0x7cdde6e9e0 --> 0x178
9618c           mov w26, #0xf8 ;         x26 = 0x37c289f0 --> 0xf8
96190           blr x8 ;         x0 = 0x7cdde4b698 --> 0x0   (null)      x1 = 0x409 --> 0xb400007d323694c0       x2 = 0x7cdde4b527 --> 0x0      x3 = 0x7cd4f5c5d8 --> 0x1       x4 = 0x7cdde4b698 --> 0xb400007be73f08a3        x5 = 0x409 --> 0xb400007cb95ea4a3      x6 = 0x7cd4f5c6f0 --> 0x838b832b83258397        x7 = 0x108 --> 0x833d83178326832b       x8 = 0x7cddd933dc --> 0xd55c7b4c8b012ca8       x9 = 0x7cdddd7140 --> 0xd55c7b4c8b012ca8        x10 = 0x58 --> 0x1      x11 = 0xeecf7326 --> 0x8f0ea10  x12 = 0xbf849a51 --> 0xb400007cb95ea4a0        x13 = 0xb4efdc76 --> 0x9fbfe4a5         x14 = 0x72e53092 --> 0x20       x15 = 0x2dc8ee0b --> 0x1ff     x16 = 0x189b0123 --> 0x7dd3e069c0       x17 = 0x8265feb --> 0x7dd3dfa9fc

​ 其中x0是压缩包的二进制数据地址, x1为该压缩包二进制数据的长度, x2是a64.dat字符串, x3是某个数据. 使用ida进入该函数sub_523DC可以发现并没有被混淆. 那么大概率不是什么重要的函数, 根据参数猜测应该是从安装包中提取a64.dat这个文件. 使用frida hook查看参数和内存变化情况.

 

 

​ 从上面的图中可以看到, 0x7cd3ee05d8这个地址的内存数据在经过sub_523DC之后被修改了, 其中前8个字节像一个指针, 后两个字节像是文件大小.

 

 

​ 将该内存区域打印后和文件做对比发现确实是该文件, 0x2a3也是a64.dat的文件大小. 得知sub_523DC提取了a64.dat文件之后, 继续往后面看汇编代码, 看看是如何使用该文件的.

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
......
95fa8           add x0, sp, #0x58 ;      x0 = 0xc0 --> 0x7cd4f5c5d8
95fac           add x8, x8, x9 ;         x8 = 0x7ca616b3dc --> 0x7cddd93dcc
95fb0           blr x8 ;         x0 = 0x7cd4f5c5d8 --> 0x2a3 获取dat文件长度
......
9621c           add x0, sp, #0x58 ;      x0 = 0xc0 --> 0x7cd4f5c5d8
96220           add x8, x8, x28 ;        x8 = 0x7ca616b3d4 --> 0x7cddd93dc4
96224           blr x8 ;         x0 = 0x7cd4f5c5d8 --> 0xb400007cb95ea200   这里调用了一个函数获取文件内容的地址, 跟踪这个地址0xb400007cb95ea200
......
96248           ldp x5, x1, [sp, #0x10] ;        x1 = 0xc8 --> 0x7cd4f5c740   (vm_main.img)      x5 = 0xb400007cb95ea4a3 --> 0x7cd4f5c6f8
9624c           ldr x4, [sp, #0x38] ;    x4 = 0xb400007be73f08a3 --> 0x7cd4f5c6f0
96250           mov w3, w0 ;     x3 = 0x1 --> 0x2a3
96254           add x9, x9, x28 ;        x9 = 0x7ca61ae928 --> 0x7cdddd7318  
96258           mov x0, x8 ;     x0 = 0x2a3 --> 0x7cd4f5c750   (builtin)
9625c           mov x2, x27 ;    x2 = 0xd0 --> 0xb400007cb95ea200   ( ")
96260           mov w23, #0x28 ;         x23 = 0x7cdde6e9f0 --> 0x28
96264           mov w28, #0x178 ;        x28 = 0x37c289f0 --> 0x178
96268           mov x27, x26 ;   x27 = 0xb400007cb95ea200 --> 0x7cd4f5c750   (builtin)
9626c           mov w26, #0xf8 ;         x26 = 0x7cd4f5c750 --> 0xf8
96270           blr x9 ;         x0 = 0x7cd4f5c750 --> 0x0   (null)      x1 = 0x7cd4f5c740 --> 0x91844490        x2 = 0xb400007cb95ea200 --> 0x8a5b6c8a         x3 = 0x2a3 --> 0x8614eee7       x4 = 0x7cd4f5c6f0 --> 0x7a5cace1        x5 = 0x7cd4f5c6f8 --> 0x6b8d117a       x6 = 0x838b832b83258397 --> 0x5c68331f  x7 = 0x833d83178326832b --> 0x3a59490c  x8 = 0x7cd4f5c750 --> 0xd55c7b4c8b012ca8       x9 = 0x7cdddd7318 --> 0xd55c7b4c8b012ca8        x10 = 0x30 --> 0xcb036d0        x11 = 0x8f0ea10 --> 0x10e05d66         x12 = 0xbf849a51 --> 0xf244e8a7         x13 = 0xb4efdc76 --> 0xeb3e178a         x14 = 0x72e53092 --> 0xd45ef95c 这里调用了sub_96318, 跟进去查看
......
;进入这个函数还是主要跟踪文件地址, 由于trace的log基址不同, 所以这里文件的地址和上面不一样
;一路跟进到96da8
97d9c        add x0, sp, #0xb8 ;      x0 = 0xa475f42f --> 0x7cdb8682c8
97da0        add x8, x8, x27 ;      x8 = 0x7c7940ee0c --> 0x7cecf5b65c   (h)
97da4        mov w3, #1 ;      x3 = 0x8614eee7 --> 0x1
97da8        blr x8 ;      x8 = 0x7cecf5b65c --> 0x1 sub_5A65C

​ 可以看到97da8中调用了sub_5A65C函数

 

 

​ 这个函数是我分析完对结构体命名后的结果, 其主要是初始化了某个结构体, 将文件地址填入前8字节. 猜测是某种数据流的实现, 用于读取文件的信息. 在初始化完之后, 之后的分析应该是围绕这个结构体进行的.

1
2
3
4
5
6
;继续往下
97dac        ldr x8, [x21, #8] ;      x8 = 0x1 --> 0x7c7940ef58
97db0        add x0, sp, #0xb8 ;
97db4        add x8, x8, x27 ;      x8 = 0x7c7940ef58 --> 0x7cecf5b7a8   ( @�    @�? �I)
97db8        blr x8 ;      x0 = 0x7cdb8682c8 --> 0x20220118     x8 = 0x7cecf5b7a8 --> 0x1182220     x9 = 0x7cecf98d70 --> 0x118     x10 = 0xcb036d0 --> 0x20220118     x11 = 0x10e05d66 --> 0x1     x12 = 0xf244e8a7 --> 0x2022     x13 = 0xeb3e178a --> 0x1801     x14 = 0xd45ef95c --> 0x3 sub_5A7A8
;

​ 可以看到这里调用了sub_5A7A8函数, 使用到了该结构体, 进ida查看

 

 

​ 其主要的功能是按照大端读取四个字节的int数字并返回.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
;继续跟进
97f70        adrp x8, #0x7ced02e000 ;      x8 = 0xeb3e178a --> 0x7ced02e000   (��^�|)
97f74        ldr x8, [x8, #0xa20] ;      x8 = 0x7ced02e000 --> 0x7c7940ef58
97f78        mov w9, #0xc850 ;      x9 = 0x7cecf98f70 --> 0xc850
97f7c        movk w9, #0x73b4, lsl #16 ;      x9 = 0xc850 --> 0x73b4c850
97f80        add x0, sp, #0xb8 ;      x0 = 0xa475f42f --> 0x7cdb8682c8
97f84        add x8, x8, x9 ;      x8 = 0x7c7940ef58 --> 0x7cecf5b7a8   ( @�    @�? �I)
97f88        blr x8 ;      x0 = 0x7cdb8682c8 --> 0x1     x8 = 0x7cecf5b7a8 --> 0x10000     x9 = 0x73b4c850 --> 0x1     x10 = 0xcb036d0 --> 0x1     x11 = 0x10e05d66 --> 0x1     x12 = 0xf244e8a7 --> 0x0     x13 = 0xeb3e178a --> 0x100     x14 = 0xd45ef95c --> 0x7 sub_5A7A8
97f8c        str wzr, [sp, #0x44] ;      str = 0
97f90        str w0, [sp, #0x104] ;      str = 0x1 这里是调用了上面的sub_5A7A8函数读取第二个int, 将其保存到栈中, 后面会继续用到, 所以标注起来
......
9779c        add x0, sp, #0xb8 ;      x0 = 0xa475f42f --> 0x7cdb8682c8
977a0        add x8, x8, x9 ;      x8 = 0x7c7940f288 --> 0x7cecf5bad8   (�_���W��O��{���)
977a4        mov w1, wzr ;      x1 = 0x91844490 --> 0x0   (null)
977a8        blr x8 ;      x0 = 0x7cdb8682c8 --> 0xb400007d3ec291b0   (vm_main.img)     x1 = 0x0 --> 0x50     x2 = 0x8a5b6c8a --> 0x168     x3 = 0x8614eee7 --> 0x140     x4 = 0x7a5cace1 --> 0x178     x5 = 0x6b8d117a --> 0xb     x6 = 0x5c68331f --> 0x68     x7 = 0x3a59490c --> 0x5c9a8776     x8 = 0x7cecf5bad8 --> 0xd55c7b4c8b012ca8     x9 = 0x73b4c850 --> 0xd55c7b4c8b012ca8     x10 = 0xcb036d0 --> 0xf658b097     x11 = 0x10e05d66 --> 0x128     x12 = 0xf244e8a7 --> 0x78     x13 = 0xeb3e178a --> 0xe0     x14 = 0xd45ef95c --> 0xc246eb53     x15 = 0xbfb89f2d --> 0x7ced03f8ee     x16 = 0xa5356f4c --> 0xa0     x17 = 0xa4ff4077 --> 0xa6667466
977ac        str x0, [sp, #0x108] ;      str = 0xb400007d3ec291b0   (vm_main.img)
977b0        ldr x8, [sp, #0x108] ;      x8 = 0xd55c7b4c8b012ca8 --> 0xb400007d3ec291b0   (vm_main.img)
977b4        ldr w9, [sp, #0xf4] ;      x9 = 0xd55c7b4c8b012ca8 --> 0xeecf7326
977b8        ldr w10, [sp, #0xf4] ;      x10 = 0xf658b097 --> 0xeecf7326

​ 从汇编中分析可以看出在977a8之后出现了字符串, 因此对其内部进行分析, 发现会调用sub_5AAD8函数, 而这个函数确实是读取某一块内存并且对字符串进行解密. 然后使用frida hook发现这里会产出这三个字符串: vm_main.img, __ff_11, __ff_12.

1
2
3
4
5
;继续跟进
978c4        mov x1, x0 ;      x1 = 0x91844490 --> 0xb400007bef75a300
978c8        add x0, sp, #0xb8 ;      x0 = 0xb400007bef75a300 --> 0x7cdb8682c8
978cc        add x8, x8, x27 ;      x8 = 0x7c7940f228 --> 0x7cecf5ba78   (�O���{��C)
978d0        blr x8 ;      x0 = 0x7cdb8682c8 --> 0x1     x1 = 0xb400007bef75a300 --> 0x83578390830c8340     x2 = 0x284 --> 0x83598390833d83f4     x3 = 0x8614eee7 --> 0xb400007bef75a500     x4 = 0x7a5cace1 --> 0xb400007bf780a79f     x5 = 0x6b8d117a --> 0xb400007bef75a584     x6 = 0x5c68331f --> 0x83258397836e83df     x7 = 0x3a59490c --> 0x8326832b838b832b     x8 = 0x7cecf5ba78 --> 0x29f     x9 = 0x7cecf98898 --> 0xd4fcb35983838361     x10 = 0xcb036d0 --> 0xd4fcea78d4fca104     x11 = 0x10e05d66 --> 0xd4fcea7883838383     x12 = 0xf244e8a7 --> 0x830c834083598390     x13 = 0xeb3e178a --> 0x833d83f483578390     x16 = 0xa5356f4c --> 0x7ced026c80     x17 = 0xa4ff4077 --> 0x7dd3d8ec40

​ 在上面这段汇编中0xb400007bef75a300是之前定义的结构体指针, x2参数很明显是某个比文件小的长度. 进入sub_5AA78看看.

 

 

​ 发现该函数的主要功能是对文件的某一段内容复制到另一块内存区域当中, 对应到上面的调用, 那含义很明显就是从a64.dat文件的0x1b起始, 长度为0x284的部分. 继续跟进看看其是如何使用这个区域的.

1
2
3
4
5
6
7
8
9
10
11
12
;将前面那块地址存到[sp, #0x128]栈上
979e8        str x0, [sp, #0x128] ;      str = 0xb400007bef75a300
......
9745c        ldr w8, [sp, #0x144] ;      x8 = 0x91844490 --> 0x0   (null)
97460        ldr x9, [sp, #0x128] ;      x9 = 0x7cecf9845c --> 0xb400007bef75a300  
97464        mov w27, #0x23 ;      x27 = 0x2d0 --> 0x23
97468        ldrb w10, [x9, x8] ;      x10 = 0xcb036d0 --> 0x4
9746c        eor x10, x10, x27 ;      x10 = 0x4 --> 0x27
97470        adrp x27, #0x7ced00b000 ;      x27 = 0x23 --> 0x7ced00b000
97474        add x27, x27, #0x536 ;      x27 = 0x7ced00b000 --> 0x7ced00b536  
97478        ldrb w10, [x27, x10] ;      x10 = 0x27 --> 0x23
9747c        strb w10, [x9, x8] ;

​ 可以发现前面内存复制之后, 会马上使用到这块区域, 这是一个很明显的加解密操作, 其实现的操作是取第一个字节0x4和0x23异或得到结果0x27, 然后从原来的字节被替换为MEM[0x10A536 + 0x27]的值. 那么这个0x10A536偏移的区域很有可能是映射码表之类的东西.
​ 继续查看后面的汇编可以发现果然在后面的x8会逐渐递增, 并重复这段操作, 也就是循环对每个字节进行这样的操作. 那么可以看看这个码表解密之后是什么样的. 这里使用hook会造成程序卡死, 那么使用以下的python脚本静态解密一下看看是什么东西.

1
2
3
4
5
6
7
8
9
10
import io
 
a64Data = io.open("a64.dat", 'rb').read()
Mem_10A536Data = io.open("Mem_10A536.bin", 'rb').read()
 
data = bytearray(0x284)
 
for i in range(0x284):
    data[i] = Mem_10A536Data[a64Data[i+0x1b] ^ 0x23]
io.open("out.bin", "wb").write(data)

 

​ 可以看到这块区域被解密出来了, 其中也能够看到vm_main字符串, 所以应该是没有错误的.

 

​ 时间和精力原因后面的没有分析完, 前面findcrypt插件发现了aes的特征, 所以也有可能后面用到了aes算法, 看字符串带vm字样也不排除使用了vm技术. 看汇编是真的累

总结

​ 到此为止成功获取了flag, 过掉了所有反调试使得frida能够正常工作, 之后静态分析加动态调试还原了离散对数算法, 并且还给出了其逆算法.看汇编还是太累了, 说实在的这么高强度的混淆和前面的vm混打, 这次比赛难度真不小.


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

收藏
点赞8
打赏
分享
最新回复 (32)
雪    币: 1726
活跃值: (8681)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
你瞒我瞒 2023-4-18 09:15
2
0
TQL
雪    币: 17792
活跃值: (60018)
能力值: (RANK:125 )
在线值:
发帖
回帖
粉丝
Editor 2023-4-18 11:10
3
0

感谢分享,题目防止时间长了链接失效,本地存档一份。



https://gslab.qq.com/html/competition/2023/race-final.htm

上传的附件:
雪    币: 219
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
Skyart 2023-4-18 14:39
4
0
mark
雪    币: 7
活跃值: (50)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
zxcvb_217307 2023-4-18 16:46
5
0
dumpde 的so发出来看下,楼主
雪    币: 1954
活跃值: (3653)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
method 2023-4-24 16:00
6
0
老哥 你使用的rwProcMem 具体是那个项目 gui是自己写的吗
雪    币: 6451
活跃值: (3679)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
juice4fun 1 2023-4-24 16:44
7
0
method 老哥 你使用的rwProcMem 具体是那个项目 gui是自己写的吗
原项目里就有, 但是字符串打印有一点问题, 要自己修一下
雪    币: 1119
活跃值: (2014)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
DiamondH 2023-4-24 17:17
8
0
请教一下间接跳转是如何处理的呢
雪    币: 6451
活跃值: (3679)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
juice4fun 1 2023-4-24 17:22
9
0
DiamondH 请教一下间接跳转是如何处理的呢
对于函数跳转可以直接patch, 因为加载到寄存器的偏移是确定的, 但是条件分支就不好搞了, 我查的资料说这种很难还原
雪    币: 1119
活跃值: (2014)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
DiamondH 2023-4-26 17:19
10
0
juice4fun 对于函数跳转可以直接patch, 因为加载到寄存器的偏移是确定的, 但是条件分支就不好搞了, 我查的资料说这种很难还原
看到您帖子里可以查看伪代码是手动修复的嘛?
雪    币: 948
活跃值: (1557)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
fallw1nd 1 2023-4-27 08:58
11
0
DiamondH 看到您帖子里可以查看伪代码是手动修复的嘛?
它的混淆是这样的,call的混淆是可以做到写脚本一键去除的,只需要模拟执行计算出地址然后patch回去就好;但是它另一个混淆是控制流平坦化,并且switch-case的跳转和到预处理块的跳转是使用间接跳转实现的。除此之外,相比普通的控制流平坦化,它是通过在预处理块把当前case值减去一个固定的值来得到下一个case值的,所以从每个基本块模拟执行得到后继也不可行,总之就是写脚本去除非常麻烦。。
雪    币: 948
活跃值: (1557)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
fallw1nd 1 2023-4-27 09:10
12
0
fallw1nd 它的混淆是这样的,call的混淆是可以做到写脚本一键去除的,只需要模拟执行计算出地址然后patch回去就好;但是它另一个混淆是控制流平坦化,并且switch-case的跳转和到预处理块的跳转是使用间接 ...
而对于这个控制流平坦化中的条件跳转,由于CMP和条件选择指令都集中在同一个代码块中,写脚本的时候得考虑几个问题:第一个就是你得判断哪个BR对应哪个条件选择指令,第二个就是得把CMP指令也一起patch到对应的BR指令之前,第三个就是不够地方patch,可以考虑把其他指令往前挪或者往后挪,但是这样就又得考虑所有被影响的跳转指令的修复了,而且如果存在需要重定位的指令,也得处理。
雪    币: 6451
活跃值: (3679)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
juice4fun 1 2023-4-27 09:44
13
0
DiamondH 看到您帖子里可以查看伪代码是手动修复的嘛?
楼上大佬说的对, 也就只有函数跳转能够修复了其实, 对于逻辑复杂的函数看伪代码意义不是很大
雪    币: 62
活跃值: (556)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
万里星河 2023-4-27 21:34
14
0
可怜本人没文化 一句卧槽走天下 大佬太强了
雪    币: 62
活跃值: (556)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
万里星河 2023-4-27 22:08
15
0
找到了这个项目 但是readMe并没有说具体怎么用 很难受
雪    币: 62
活跃值: (556)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
万里星河 2023-4-27 22:09
16
0
juice4fun 原项目里就有, 但是字符串打印有一点问题, 要自己修一下
找到了这个项目 但是readMe并没有说具体怎么用 很难受
雪    币: 6451
活跃值: (3679)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
juice4fun 1 2023-4-27 22:58
17
0
万里星河 找到了这个项目 但是readMe并没有说具体怎么用 很难受
三步,编译加载内核驱动模块,编译运行server端,编译运行client端连接上就能用了
雪    币: 62
活跃值: (556)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
万里星河 2023-4-28 12:02
18
0
juice4fun 三步,编译加载内核驱动模块,编译运行server端,编译运行client端连接上就能用了
好的 感谢大佬指点
雪    币: 62
活跃值: (556)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
万里星河 2023-4-29 14:01
19
0
juice4fun 三步,编译加载内核驱动模块,编译运行server端,编译运行client端连接上就能用了
我尝试编译了下 用的ubuntu 感觉需要交叉编译啥的 不知道怎么配环境 感觉不是make一下就能搞定的 大佬能否指点下呀 大概怎么个编译思路?
雪    币: 6451
活跃值: (3679)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
juice4fun 1 2023-4-29 14:47
20
0
万里星河 我尝试编译了下 用的ubuntu 感觉需要交叉编译啥的 不知道怎么配环境 感觉不是make一下就能搞定的 大佬能否指点下呀 大概怎么个编译思路?
要刷内核, 不是那么简单就能搞定的, 不知道怎么弄的话买台pixel吧, 那个起码有官方教程
雪    币: 62
活跃值: (556)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
万里星河 2023-4-29 15:05
21
0
juice4fun 要刷内核, 不是那么简单就能搞定的, 不知道怎么弄的话买台pixel吧, 那个起码有官方教程
这个倒不难 可以慢慢搞 对了请问那个gui是怎么来的 我看源码包里好像没有
雪    币: 6451
活跃值: (3679)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
juice4fun 1 2023-4-29 16:00
22
0
源码里有, client, 你仔细看看
雪    币: 62
活跃值: (556)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
万里星河 2023-5-3 12:48
23
0
juice4fun 源码里有, client, 你仔细看看
好的 内核源码没有适配pixel1的内核 花了些时间适配
[45651.873560] c0   6364 Hello, rwProcMem37
sailfish:/ # lsmod
Module                  Size  Used by
rwProcMem              13055  0
sailfish:/ # ls /dev | grep Mem
rwProcMem37
sailfish:/ #
雪    币: 62
活跃值: (556)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
万里星河 2023-5-3 12:53
24
0
请问gui的客户端能不能分享下呀 那个好像是MFC的 我没装VS 编译有点儿麻烦
服务端还好 ndk-build一下应该就可以了
雪    币: 3
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
Mark. 2023-5-3 14:12
25
0
请问想达到你这样的程度要学什么语言,想入门但是没方向,求教
游客
登录 | 注册 方可回帖
返回