-
-
[原创]看雪 2022 KCTF 春季赛 第八题 迷雾散去
-
发表于: 2022-5-28 06:09 10833
-
(不正常的题,就不能用正常方法做)
Android SDK 开一个 armeabi-v7a 的模拟器,本地 x64dbg 调试 qemu-system-armel.exe 进程
输入公开的name和serial(C8EB85C90E69EDC8 和 3638386461396366623135623535353361323862656630656561326334303931
),点一下按钮
公开的 serial 做 hexdecode 得到 688da9cfb15b5553a28bef0eea2c4091,取后八个字节 ea2c4091,x64dbg 全内存搜索(findallmem 0, "ea2c4091"),找到唯一一个位置,这个位置地址的后三位是 690
向上查看内存,定位到地址后三位是 320 的位置
生成17个输入:
name固定为KCTF,把17个输入分别作为serial给程序,提取 320 位置上的 int
对于第一个输入,从 320 处提取到的值是 0x1287;对于后面16个输入,提取到的值依次为 0x1302, ..., 0x118e
则 KCTF name最终的 serial 是:
另外此题多解,最终serial中所有奇数位置的值取'4'或'6'不影响结果,总共有256个解。例如:
踩坑过程:
ArmVMP,立刻想起了去年的题目以及艰难的做题过程,内心本能的抗拒。
jadx逆java层,基本上就是把输入传给so
注意到从 asserts 提取了 Unknown 文件,file 命令发现是 ELF
Unknown文件没有加混淆,是一个普通的 ptrace 反调试
libcrackme.so
(ps. 高版本IDA对arm和Thumb指令的自动识别效果好了很多;个别识别错误的可以按Alt+G手动修改T寄存器)
.init_proc 没加混淆,大概逻辑是做一些自修改
从 Jni_OnLoad 开始加了很强的混淆,不过看起来与去年的差不多。
例如:
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天零解过完。
结果前三血都出来了(今年怎么这么卷,比去年卷多了……),于是不得不……。
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
())
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
())
serial
=
[]
for
c
in
[
0x1302
,
0x12ca
,
0x1276
,
0x1338
,
0x1378
,
0x1276
,
0x122e
,
0x133e
,
0x12c2
,
0x12f6
,
0x1194
,
0x11de
,
0x136e
,
0x1256
,
0x135a
,
0x118e
]:
serial.append((
0x1287
-
c
+
0xff
)
/
/
2
)
print
(bytes(serial).
hex
().encode().
hex
())
serial
=
[]
for
c
in
[
0x1302
,
0x12ca
,
0x1276
,
0x1338
,
0x1378
,
0x1276
,
0x122e
,
0x133e
,
0x12c2
,
0x12f6
,
0x1194
,
0x11de
,
0x136e
,
0x1256
,
0x135a
,
0x118e
]:
serial.append((
0x1287
-
c
+
0xff
)
/
/
2
)
print
(bytes(serial).
hex
().encode().
hex
())
3432356538383237303738386163323436323438663964343063393831366663
3432356538383237303738386163323436323438663964343063393831366663
3432354538383237303738384143323436323438463944343043393831364643
3432354538383237303738384143323436323438463944343043393831364643
.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
.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