首页
论坛
课程
招聘
[原创]看雪 2022 KCTF 春季赛 第八题 迷雾散去
2022-5-28 06:09 4236

[原创]看雪 2022 KCTF 春季赛 第八题 迷雾散去

2022-5-28 06:09
4236

(不正常的题,就不能用正常方法做)

 

Android SDK 开一个 armeabi-v7a 的模拟器,本地 x64dbg 调试 qemu-system-armel.exe 进程

 

输入公开的name和serial(C8EB85C90E69EDC8 和 3638386461396366623135623535353361323862656630656561326334303931
),点一下按钮

 

公开的 serial 做 hexdecode 得到 688da9cfb15b5553a28bef0eea2c4091,取后八个字节 ea2c4091,x64dbg 全内存搜索(findallmem 0, "ea2c4091"),找到唯一一个位置,这个位置地址的后三位是 690

 

向上查看内存,定位到地址后三位是 320 的位置

 

生成17个输入:

1
2
3
4
5
print((b'\0'*16).hex().encode().hex())
for i in range(16):
    s = bytearray(b'\0'*16)
    s[i] = 0xff
    print(s.hex().encode().hex())

name固定为KCTF,把17个输入分别作为serial给程序,提取 320 位置上的 int

 

对于第一个输入,从 320 处提取到的值是 0x1287;对于后面16个输入,提取到的值依次为 0x1302, ..., 0x118e

 

则 KCTF name最终的 serial 是:

1
2
3
4
5
serial = []
for c in [0x1302, 0x12ca, 0x1276, 0x13380x1378, 0x1276, 0x122e, 0x133e0x12c2, 0x12f6, 0x1194, 0x11de, 0x136e, 0x1256, 0x135a, 0x118e]:
    serial.append((0x1287-c+0xff) // 2)
 
print(bytes(serial).hex().encode().hex())
1
3432356538383237303738386163323436323438663964343063393831366663

另外此题多解,最终serial中所有奇数位置的值取'4'或'6'不影响结果,总共有256个解。例如:

1
3432354538383237303738384143323436323438463944343043393831364643

 

踩坑过程:

 

ArmVMP,立刻想起了去年的题目以及艰难的做题过程,内心本能的抗拒。

 

jadx逆java层,基本上就是把输入传给so
注意到从 asserts 提取了 Unknown 文件,file 命令发现是 ELF

 

Unknown文件没有加混淆,是一个普通的 ptrace 反调试

 

libcrackme.so
(ps. 高版本IDA对arm和Thumb指令的自动识别效果好了很多;个别识别错误的可以按Alt+G手动修改T寄存器)

 

.init_proc 没加混淆,大概逻辑是做一些自修改

 

从 Jni_OnLoad 开始加了很强的混淆,不过看起来与去年的差不多。

 

例如:

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
.text:00003BDC ; jint JNI_OnLoad(JavaVM *vm, void *reserved)
.text:00003BDC                 EXPORT JNI_OnLoad
.text:00003BDC JNI_OnLoad                              ; DATA XREF: LOAD:00000328↑o
.text:00003BDC                 PUSH            {LR}    ; Push registers
.text:00003BDE                 BL              sub_13990 ; Branch with Link
.text:00003BE2                 ADDS            R2, R0, R4 ; Rd = Op1 + Op2
 
...
 
LOAD:00013990 sub_13990                               ; CODE XREF: .text:00003BDE↑p
LOAD:00013990                 POP.W           {LR}    ; Pop registers
LOAD:00013994
LOAD:00013994 loc_13994                               ; DATA XREF: sub_13990+1C↓r
LOAD:00013994                 BX              PC      ; Branch to/from Thumb mode
LOAD:00013996 ; ---------------------------------------------------------------------------
LOAD:00013996                 MVNS            R0, R6  ; Rd = ~Op2
LOAD:00013998                 CODE32
LOAD:00013998
LOAD:00013998 loc_13998                               ; CODE XREF: sub_13990:loc_13994↑j
LOAD:00013998                 SUB             SP, SP, #0x44 ; 'D' ; Rd = Op1 - Op2
LOAD:0001399C                 PUSH            {R0-PC} ; Push registers
LOAD:000139A0                 MOV             R10, #0x8000 ; Rd = Op2
LOAD:000139A4                 MOV             R5, #0x3C ; '<' ; Rd = Op2
LOAD:000139A8                 MOV             R4, #0xF ; Rd = Op2
LOAD:000139AC                 LDR             R1, loc_13994 ; Load from Memory
LOAD:000139B0                 MOV             R1, R1,ASR#16 ; Rd = Op2
LOAD:000139B4
LOAD:000139B4 loc_139B4                               ; CODE XREF: LOAD:00013A2C↓j
LOAD:000139B4                 AND             R2, R1, R10 ; Rd = Op1 & Op2
LOAD:000139B8                 MOV             R2, R2,ASR R4 ; Rd = Op2
LOAD:000139BC                 EOR             R2, R2, #0 ; Rd = Op1 ^ Op2
LOAD:000139C0                 ADD             PC, PC, R2,LSL#2 ; 139CC
LOAD:000139C0 ; End of function sub_13990             ; 139C8
LOAD:000139C0
LOAD:000139C0 ; ---------------------------------------------------------------------------
LOAD:000139C4                 ALIGN 8
LOAD:000139C8
LOAD:000139C8 ; =============== S U B R O U T I N E =======================================
LOAD:000139C8
LOAD:000139C8
LOAD:000139C8 sub_139C8
LOAD:000139C8                 B               loc_139DC ; 13A20
LOAD:000139C8                                         ; 13a1c
LOAD:000139C8                                         ; 13a18
LOAD:000139CC ; ---------------------------------------------------------------------------
LOAD:000139CC                 LDR             R3, [SP,R4,LSL#2] ; Load from Memory
LOAD:000139D0                 ADD             R6, R5, #0x44 ; 'D' ; Rd = Op1 + Op2
LOAD:000139D4                 STR             R3, [SP,R6] ; Store to Memory
LOAD:000139D8                 SUB             R5, R5, #4 ; Rd = Op1 - Op2
LOAD:000139DC
LOAD:000139DC loc_139DC                               ; CODE XREF: sub_139C8↑j
LOAD:000139DC                 ADD             PC, PC, R4,LSL#2 ; 13A20
LOAD:000139DC ; End of function sub_139C8             ; 13a1c
LOAD:000139DC                                         ; 13a18
LOAD:000139E0 ; ---------------------------------------------------------------------------
LOAD:000139E0                 NOP                     ; No Operation
LOAD:000139E4                 B               loc_13A30 ; Branch
LOAD:000139E8 ; ---------------------------------------------------------------------------
LOAD:000139E8                 NOP                     ; No Operation
LOAD:000139EC                 NOP                     ; No Operation
LOAD:000139F0                 NOP                     ; No Operation
LOAD:000139F4                 NOP                     ; No Operation
LOAD:000139F8                 NOP                     ; No Operation
LOAD:000139FC                 NOP                     ; No Operation
LOAD:00013A00                 NOP                     ; No Operation
LOAD:00013A04                 NOP                     ; No Operation
LOAD:00013A08                 NOP                     ; No Operation
LOAD:00013A0C                 NOP                     ; No Operation
LOAD:00013A10                 NOP                     ; No Operation
LOAD:00013A14                 NOP                     ; No Operation
LOAD:00013A18                 NOP                     ; No Operation
LOAD:00013A1C                 NOP                     ; No Operation
LOAD:00013A20                 B               loc_13A24 ; Branch

0x139A4 是一个mask,0x139A8 给 R4 的初始值在 0x139D8 递减
0x139CC 从内存中取值,这块内存是 0x1399C push进栈的寄存器。
0x13998 开辟栈空间,0x139CC 取出来的值在 0x139D0 和 0x139D4 存入这块空间。

 

所以,这些代码的作用是arm的一条push,其中R4的初始值是要push的寄存器号的mask。

 

模式特征还比较明显,但是不知道有多少种这样的指令,静态匹配化简的工作量大概率会超出4天的承受力。

 

GOT表有导入 fork,pthread_create,结合 Unknown 文件的 ptrace,动态调试的坑也不会小。


 

在三血出来后,感觉题目的可解性似乎比预想的高,开始尝试解题。

 

根据今年第五题的经历以及熟悉的混淆方法,很可能算法还是老的(或者微调)。
拜读了去年秋季赛第八题的writeup。算法大概是对name会做非常复杂的运算,但是可以动态调试直接提取;serial则直接参与最终比较。
这篇文章 得到启示,可以通过改变输入观察内存的变化找突破口。

 

没有root的真机,apk附带的so只有arm架构,这种文件通常的模拟器都不能运行,只能通过Android Studio 的 AVD Manager 开一个 armeabi-v7a 架构的模拟器。

 

在这个过程中突然想到直接从 qemu-system-armel.exe 进程中dump内存(因为虚拟机的物理内存会映射在 qemu 的地址空间中),好处是完全无视反调试,还可以下各种数据断点追踪数据流。缺点是难以跟踪程序的控制流,不过在这种级别的代码混淆下还原控制流是自讨苦吃(实测感觉,qemu JIT 出来的代码都比源程序的代码可读性高)。

 

x64dbg 调试需要忽略所有异常并bypass给qemu,选项->选项,“当以下事件发生时暂停”全部取消勾选,“异常过滤器”添加一个最大的忽略范围并设为不暂停。

 

公开的 serial 可以做两次 hexdecode 得到 16 字节的值,先全内存搜索,通过命令findallmem 0, "<>",搜索结果在“引用”选项卡查看(x64dbg文档没说清楚这一点,绕了些弯)。

 

搜不到结果,尝试分段搜索,试出只有搜最后4个字节才能搜到,发现前12字节被故意覆盖了。(最后4个字节没覆盖,大概是出题人留下的线索?)(同时,这也表明可能不再像去年的题一样能够直接从内存中取出正确的serial)
只会搜到一个有效结果,且虚拟地址的后三位总是690。

 

尝试对serial的16个字节下数据断点,跟踪一会后放弃(基本上每条vmp指令都会push/pop所有通用寄存器,太多了跟踪不过来),但是观察到serial的字节是逐个取的,暗示着每个字节不会影响到其他字节。

 

dump这附近的内存,分别变换name和serial,再次dump内存做比较。
重点关注serial变化引起的内存变化,向上找,注意到了最近的虚拟地址后三位是320的位置的4字节int值,当serial的某个字节加1时这个值也加了1,感觉不是巧合;继续尝试把某个字节+2,+3,发现这个位置也会相应的加+2,+3等,是一个重大的突破口。

 

在693位置(即16字节serial的最后一个)下数据断点,输入一个前15字节错误、只有第16字节正确serial,触发数据断点时把320位置的值手动改为正确的值,然后继续,发现程序提示输入正确。

 

所以320位置的int一定记录了serial所有字节的正确性信息。结合与serial中的字节同加同减的现象,合理猜测这个位置保存的是输入的每个serial字节与正确的字节的差值。随便给几组错误的serial,计算后发现保存的是所有差值的绝对值的求和再加上常量0xbac。

 

由于每个字节的影响是独立的,因此可以把某个字节先设为0再设为0xff,得到这两次320位置的值,根据差值即可得出正确的值。(例如,设某个位置的真实值是x,赋值0时得到的320位置的值是a,赋值0xff时得到的320位置的值是b,其他字节不变,则a-b就是这个字节在两次赋值中与正确值的距离之差,即a-b=(x-0)-(0xff-x),所以x=(a-b+0xff)//2)

 

具体脚本参照本文开头。注意输入给程序的serial是两次hexencode后得到的64字节值。多解的原因在于程序验证serial做hexdecode时不区分大小写。


 

吐槽:

 

说实话,这种题做起来真的毫无体验,既不能 Hack for fun,又学不到新知识,就单纯是体力活硬搞。(感觉更像是帮出题人免费测试壳的强度)
(当然,如果有vmp自动化脱壳机一切都不一样了,不过目前还未有较为成熟的开源工具可以直接使用;另外4天时间从零写一个是个非常困难的任务,特别是还要针对题目做具体修改,以及debug,性价比远不如体力活硬搞)

 

回去看了下去年两季赛的所有有解的vmp or代码混淆题,没有一个人是通过写去混淆脚本解题的,全都是调试硬怼或者找弱点侧面突破

 

其他CTF比赛中很少见到单纯变态级别混淆保护代码逻辑的逆向题(而且赛后出题人一般会公开自己写的混淆器、自动化去混淆脚本或相关的开源工具),这大概也是KCTF的特色之一了,可能对攻击方更友好一些。

 

拿到题前几天直接放了,准备躺平坐等4天零解过完。

 

结果前三血都出来了(今年怎么这么卷,比去年卷多了……),于是不得不……。


【看雪培训】《Adroid高级研修班》2022年夏季班招生中!

最后于 2022-5-28 06:11 被mb_mgodlfyn编辑 ,原因:
收藏
点赞3
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回