说明
本文档是进行完相关分析之后的总结回忆, 所以可能有的地方的花指令是被去除了再截图的, 函数名和数据结构被重命名了. 这决赛的混淆太恶心了,我无法做到将代码还原, 所以完全手撕汇编做的.
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,
9f93c add x1, x1, x8 ;
9f940 ldr x8, [sp,
9f944 add x0, sp,
9f948 mov x2,
9f94c add x3, x3,
9f950 ldr w4, [x8, x9, lsl
9f954 ldr w8, [sp,
9f958 adrp x9,
9f95c ldr x9, [x9,
9f960 str w8, [sp,
9f964 mov w8,
9f968 movk w8,
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,
9f978 ldr w9, [sp,
9f97c ldr w10, [sp,
9f980 mov w11,
9f984 mov w4,
9f988 mov w3,
9f98c mov w2,
9f990 mov w0,
9f994 mov w17,
9f998 mov w16,
9f99c mov w15,
9f9a0 mov x1, x27 ; x1 = 0x7ce2ef6b83 - - > 0x7ce2ef7850 ( 00000000 )
;这里能看到生成了一串 0 , 这个 0 就是上面的br跳转得到的
9f9a4 mov w27,
......
|
而上面的那个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 | / / 这里使用了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,
99ff4 add x5, sp,
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,
9a010 add x0, sp,
9a014 add x9, x8, x25 ; x9 = 0xd55c7b4c8b012ca8 - - > 0x7ce3499bec (�)
9a018 add x8, sp,
9a01c blr x9 ; x0 = 0x7cda7e56f8 - - > 0x7cda7e5750 x1 = 0x984a295f - - > 0xb400007cd01e8000 x2 = 0x2 - - > 0x7ce349bb84 x9 = 0x7ce3499bec - - > 0x7d4c11878c x10 = 0x38 - - > 0xffffffff973833f8
9a020 ldr x8, [x23,
|
其中能看到PK
, vm_main
, builtin
, PK是压缩包的magic标识, 有可能这里的br是执行了解压缩操作, 回溯查看这个PK的来源能够发现是sub_B59DC函数获取该地址的.
其中libsec2023.so + 0x10A698处便是压缩包的数据.
使用010editor将其手动提取出来, 打开后可以发现里面存在两个文件a64.dat
和a64.sig
. 此处尚且知其具体含义, 继续阅读后面的汇编代码, 查看是如何使用该数据的. 果不其然在0x96190附近看到了这样一段汇编记录
1 2 3 4 5 6 7 | 96178 ldp x0, x1, [sp,
9617c ldr x2, [sp,
96180 add x3, sp,
96184 add x8, x8, x26 ; x8 = 0x7ca616a9ec - - > 0x7cddd933dc (���O��{���)
96188 mov w28,
9618c mov w26,
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,
95fac add x8, x8, x9 ; x8 = 0x7ca616b3dc - - > 0x7cddd93dcc
95fb0 blr x8 ; x0 = 0x7cd4f5c5d8 - - > 0x2a3 获取dat文件长度
......
9621c add x0, sp,
96220 add x8, x8, x28 ; x8 = 0x7ca616b3d4 - - > 0x7cddd93dc4
96224 blr x8 ; x0 = 0x7cd4f5c5d8 - - > 0xb400007cb95ea200 这里调用了一个函数获取文件内容的地址, 跟踪这个地址 0xb400007cb95ea200
......
96248 ldp x5, x1, [sp,
9624c ldr x4, [sp,
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,
96264 mov w28,
96268 mov x27, x26 ; x27 = 0xb400007cb95ea200 - - > 0x7cd4f5c750 (builtin)
9626c mov w26,
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,
97da0 add x8, x8, x27 ; x8 = 0x7c7940ee0c - - > 0x7cecf5b65c (h)
97da4 mov w3,
97da8 blr x8 ; x8 = 0x7cecf5b65c - - > 0x1 sub_5A65C
|
可以看到97da8中调用了sub_5A65C函数
这个函数是我分析完对结构体命名后的结果, 其主要是初始化了某个结构体, 将文件地址填入前8字节. 猜测是某种数据流的实现, 用于读取文件的信息. 在初始化完之后, 之后的分析应该是围绕这个结构体进行的.
1 2 3 4 5 6 | ;继续往下
97dac ldr x8, [x21,
97db0 add x0, sp,
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,
97f74 ldr x8, [x8,
97f78 mov w9,
97f7c movk w9,
97f80 add x0, sp,
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,
97f90 str w0, [sp,
......
9779c add x0, sp,
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,
977b0 ldr x8, [sp,
977b4 ldr w9, [sp,
977b8 ldr w10, [sp,
|
从汇编中分析可以看出在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,
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,
979e8 str x0, [sp,
......
9745c ldr w8, [sp,
97460 ldr x9, [sp,
97464 mov w27,
97468 ldrb w10, [x9, x8] ; x10 = 0xcb036d0 - - > 0x4
9746c eor x10, x10, x27 ; x10 = 0x4 - - > 0x27
97470 adrp x27,
97474 add x27, x27,
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世界