前言
决赛在 il2cpp 上的防护以及 anti-debug 上并没有增加难度,而是把重心放在了注册机算法上,所以决赛篇就不重复写 dump 和 anti-debug 部分了
分析
getflag
在有 dump 的情况下拿 flag 就是洒洒水了,分析我都懒得贴了,上个代码意思一下
1
2
3
4
5
6
7
8
|
function CheatFlag()
{
var g_libil2cpp_addr
=
Module.findBaseAddress(
"libil2cpp.so"
);
var coin_cmp
=
g_libil2cpp_addr.add(
0x4652ac
);
Memory.protect(coin_cmp,
4
,
"rwx"
);
Memory.writeInt(coin_cmp,
0x7100001f
);
Memory.protect(coin_cmp,
4
,
"rx"
);
}CheatFlag();
|
注册机
il2cpp 中分析过程还是一样的,很容易可以找到验证点,稍微分析就可以知道这里的验证方法是低位与rand比较,高位为0
sec 的 so 里也同样有个加密函数
这次调试出现了一个奇怪的问题(上次打的时候没这个问题,不知道是怎么触发的),跳转 libsec 会到一个只读的段,里面代码完全相同,所以断点下载这里面根本不会断下来
把这个段删除就好了
分析加密函数 0x94368,像这种函数还是好计算的,不过为了方便也可以直接调试
可以看到 0x9E93C 函数的参数是输入& 0xFFFF00FFFFFF 的值,也就是拿了输入的这些位
同时下面那个函数 ida 也标出来了是 strcmp,观察参数可以理解 v8 是 0x9E93C 的输出
而函数内第一个用到输入的函数是 0x9f4c4,这个函数很简单就是赋值,那么接下来就是需要针对 v10,也就是需要观察下一个函数 0x9f0a8
这个函数里面有一些混淆但不多,还是调试起来看好一点
先看传进来的参数
第一个参数是输入&0xFFFF00FFFFFF,第二个 0x11 也就是 17,在 0x9E93C 中可以看到赋值 17,第三个参数是一串不知道干什么用的二进制,第四个是 0
第一个函数调用完也就是用了 a4 的那个函数,把 1 写到了 a4
第二个函数的参数 v15, ida 分析它是一个 char 数组,原本里面的东西应该是没有清空,执行完第二个函数之后就清空了并且 memcpy 了输入,第三个函数和第二个函数是一样的,所以结果就是把 17 存储到了 v14 的空间处
可以简单的把这俩个函数都归为 copy 的功能
至于后面的,就慢慢 F8,看它执行哪个分支
第一句是比较 (0x11 & 1) == 0,很显然结果是假
而后要执行的是这一块,把它暂且叫分支 1
这里的参数 a4 前面被赋值为1,v15 是 input 的复制,v13 前面还没有用到过
执行完以后 a4 和 v15 没有改变,而 v13 变成了 0x2b67,也就是 v15 的当前值
下一句用到了更改后的 v13,以及不知道用来做什么的二进制入参和没变过的 a4
执行完以后 v13 和 a3 都没有变化, a4 则变成了 0x2b67
似乎这就是复制来复制去,暂时猜不出有效信息
然后程序继续执行,跳到了这个分支,把它暂且叫分支 2
第一个函数执行完之后 v13 变成了8,v14 没有变化还是 0x11
第二个函数跟最开始的 copy 是一个函数,也就是把 v13 的 8 赋值给了 v14
第三个函数啥事没干,v14 进去什么样出来就什么样,不过出来的时候 x0 = 0,应该是用于判断走向的
后面又走到这了,把它暂且叫分支 3
实际上这个分支的函数就是第一次分支执行的,不过参数不一样,区别就在于 a4 换成了 v15, v15 目前还是 input 的复制
执行完以后 v13 从 8 变成了 0x75bc371,并且 v15 也被这个值覆盖了,看起来 v13 是一个 temp 值,只用于储存中间值
这之后又到 (8 & 1) == 0,这回的结果是真,会执行不同的分支
果然这一次直接执行到了分支 2,之前为假是执行到分支 1 的
这个函数之前把 0x11 变成了 0x8,这回变成了 0x4,可以猜测是除 2,判断结果还是 0
不出意外又执行到了分支 3,这一次 0x75bc371 变成了 0x362594b58b57e1, 好像执行一次,长度就会涨一倍,符合这种运算的应该是平方
1
2
3
4
|
>>>
hex
(
0x75bc371
*
0x75bc371
)
'0x362594b58b57e1'
>>>
hex
(
0x2b67
*
0x2b67
)
'0x75bc371'
|
那这个意思应该明白了
先判断是不是单数次方,是单数就执行分支 1,分支 1 是用最开始赋值的 1 来乘
然后把次方数除 2,如果不为零就执行分支 3 继续乘,乘法的两边是自己,也就是平方
那么 off_12EC00 是乘,off_12EC10 是除,off_12EC18 是判断
还剩下一个用到了入参的 off_12EC08,猜测是取余,只是因为输入的数太小而没有显示出作用
本来这次运算的结果应该是左边 hex 中的内容,执行后右边变小了
按照它的读法,a3 是0x28a831a5bf4b902e95318e50c2075259f91094d08d84409e1b76eadfa0865d1278acc90fa7c6cf6acb375,尝试一下
1
2
|
hex
(
0xfff8081bc7dca7d4e8c54ebc420e2cb73b9dbc3fdc7d4455b2f5b4ef598916bd20fc37f15870001bc8140007f8000001
%
0x28a831a5bf4b902e95318e50c2075259f91094d08d84409e1b76eadfa0865d1278acc90fa7c6cf6acb375
)
'0x228c023ea3485a0244b4820c12cc034e874a8afe26cfc87b997c23889da89ed6d7b038236b04f34dc353f'
|
发现结果是对应的
那么最开始所比较的应该是次方后的结果的字符串
果不其然
所以只要知道哪个数的17次方取余这个大数等于另一个大数就可以了
这个问题实际上是 rsa 问题,可以用 yafu 把模数分解乘两个大质数相乘,当时我吃了不会用 yafu 的亏
yafu.ini 配置如下,特别注意 dir 相关配置,我这配置有问题执行不下去,后面快结束了我才回头整的 yafu
1
2
3
4
5
6
7
8
9
10
11
|
B1pm1
=
100000
B1pp1
=
20000
B1ecm
=
11000
rhomax
=
1000
threads
=
16
pretest_ratio
=
0.25
%
ggnfs_dir
=
.\ggnfs
-
bin
\Win32\
ggnfs_dir
=
.\ggnfs
-
bin
\
%
ecm_path
=
.\gmp
-
ecm\
bin
\x64\Release\ecm.exe
%
ecm_path
=
.\ecm\current\ecm
tune_info
=
Intel(R) Xeon(R) CPU E5
-
4650
0
@
2.70GHz
,LINUX64,
1.73786e
-
05
,
0.200412
,
0.400046
,
0.0987873
,
98.8355
,
2699.98
|
最后运算结果是
1
2
3
4
5
6
7
8
9
10
|
NFS elapsed time
=
925.8149
seconds.
Total factoring time
=
925.8163
seconds
*
*
*
factors found
*
*
*
P51
=
555183147936225271626794036740589959032732535469347
P51
=
640704384372038524783151782406101498608483916642951
ans
=
1
|
顺手上传到 factordb 上了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
import
gmpy2
q
=
640704384372038524783151782406101498608483916642951
p
=
555183147936225271626794036740589959032732535469347
n
=
0x28a831a5bf4b902e95318e50c2075259f91094d08d84409e1b76eadfa0865d1278acc90fa7c6cf6acb375
c
=
0x25f6b048b4f32e3ce9175bb64930f65101a706ae74988a4ec87b4d5ec7feb9223ab782bcf1ec9d7fee750
e
=
17
phi
=
(p
-
1
)
*
(q
-
1
)
d
=
gmpy2.invert(e, phi)
m
=
pow
(c, d, n)
print
(
hex
(m))
|
最终结果是 0x7be300df8b2c
就是说,0x7be300df8b2c 是 input % 0xFFFF00FFFFFF 的结果,还剩下 3 个 byte 在余下的运算之中
回到最初的解密 0x94368, strcmp 下面的是混淆看不懂结构了
我的做法是再每一处跳转做记号,并对被跳转地也做记号,这样就可以清晰看到路径
然后发现对照图来还比较方便
可以看到是先比较 strcmp 的结果,再下一次运行就往 3 走了
4 这里我做了标记,参数是排列好的输入,并且执行完之后改变了输入
5 就是正常的程序自检然后退出
那么重点就在 4 调用的函数 0x99ef4 之中
0x99ef4 虽然后面做了混淆,不过经过调试可以知道到 0x98e50 这个函数就计算完了,后面没有再改变
不过这个函数里面混淆的很厉害,什么也看不见,经过调试,可以发现有一处用到了输入,并且更改了输入
而这个函数内部不用说也是混淆的厉害,而且通过多次这个函数会发现这个函数不是一个固定的函数,就是说这里跳转的函数是从函数列表中拿出来的,与此同时 X8 似乎是个记录 index 的寄存器,观察汇编可以看出 X0 也是从内存列表中与 X8 相关取出的,于是乎可以拿到两个列表
另注意,ARM64架构处理器采用48位物理寻址机制,就是说开头 1 字节不要
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
|
0
:
0x7042D23DB4
,
0x7042D2327C
,
0x7042D10004
,
0x7042D22148
,
0x7042D11A88
,
0x7042D10004
,
0x7042D13AD0
,
0x7042D22148
,
0x7042D13AD0
,
0x7042D17158
,
0x7042D22148
,
0x7042D17E00
,
0x7042D12CC0
,
0x7042CFB1BC
,
0x7042CFB1BC
,
0x7042CFB1BC
10
:
0x7042CFB1BC
,
0x7042CFB1BC
,
0x7042D27BF4
,
0x7042D11A88
,
0x7042D22148
,
0x7042D10004
,
0x7042D10004
,
0x7042D24E18
,
0x7042D25C50
,
0x7042D2A564
,
0x7042D21BE8
,
0x7042D22148
,
0x7042D21BE8
,
0x7042D2075C
,
0x7042D19B64
,
0x7042D16A78
20
:
0x7042D11B2C
,
0x7042D2A564
,
0x7042D13420
,
0x7042D10AC4
,
0x7042D29054
,
0x7042D25980
,
0x7042D27BF4
,
0x7042D27BF4
,
0x7042D12AE8
,
0x7042D22148
,
0x7042D19B64
,
0x7042D19B64
,
0x7042D2A564
,
0x7042D13420
,
0x7042D16A78
,
0x7042D25980
30
:
0x7042D10004
,
0x7042D29054
,
0x7042D12AE8
,
0x7042D18F4C
,
0x7042D10004
,
0x7042D24E18
,
0x7042D18F4C
,
0x7042D16518
,
0x7042D24E18
,
0x7042D17E00
,
0x7042D21BE8
,
0x7042D10004
,
0x7042D24E18
,
0x7042D2A564
,
0x7042D2A564
,
0x7042D2A564
40
:
0x7042D13420
,
0x7042D16518
,
0x7042D24E18
,
0x7042D2A564
,
0x7042D21BE8
,
0x7042D19B64
,
0x7042D29054
,
0x7042D2A564
,
0x7042D13420
,
0x7042D16A78
,
0x7042D25980
,
0x7042D11B2C
,
0x7042D20458
,
0x7042D27BF4
,
0x7042D10AC4
,
0x7042D27BF4
50
:
0x7042D12AE8
,
0x7042D10004
,
0x7042D29054
,
0x7042D12AE8
,
0x7042D10004
,
0x7042D22468
,
0x7042CFB1BC
0
:
0xF01E0FF3
,
0x95417BFD
,
0xD30043FD
,
0x92003F3
,
0x5000000
,
0xB200E000
,
0x64000000
,
0x761303E1
,
0xF8000000
,
0xBD417BFD
,
0x673303E0
,
0xC04207F3
,
0x14123456
,
0
,
0x6D5F6D76
,
0x206E6961
10
:
0x41007825
,
0
,
0xC38043FF
,
0x91000009
,
0x541F03E8
,
0xCB856129
,
0xFD0033EA
,
0x75C03BFF
,
0xFD401BFF
,
0x18137C0C
,
0x1E1C03EB
,
0x6F00158C
,
0x7A9F03ED
,
0x31CB7D8E
,
0x424D6D2F
,
0x750B01CE
20
:
0x3F001DD0
,
0x6F9E7610
,
0x63861DD0
,
0x601001EE
,
0x8F0001BF
,
0xFDAD694E
,
0xE88005AD
,
0x1D00216B
,
0xAEFFFEBC
,
0xD53F03EB
,
0x77EB6D4C
,
0x400B6D2D
,
0x75857D8E
,
0x3D1D1D8E
,
0x2E2D01CC
,
0xC8AB694C
30
:
0xFA80056B
,
0xBF000D7F
,
0x1FFFFF11
,
0xDD003BEB
,
0x1E01D56B
,
0xBDC03BEB
,
0x558037EB
,
0x719F196B
,
0x418037EB
,
0xB9400FEC
,
0x7A9D03EB
,
0x3F03058D
,
0xFDC033ED
,
0x60907D8D
,
0x18105D8C
,
0x1E1F798C
40
:
0x57871DAC
,
0x759F018C
,
0x55803BEC
,
0x71901D80
,
0x340003EC
,
0x16EC654D
,
0xBF00019F
,
0x6F877DAE
,
0x639F1DAE
,
0x610C01CD
,
0x2EC6D4D
,
0x3D001DAD
,
0x48EB35AD
,
0x9D00058C
,
0x572001A0
,
0x5E80216B
50
:
0x3FFFFEAC
,
0x61800508
,
0x5804011F
,
0xBCFFF8C1
,
0xF58043FF
,
0xEAC73BD4
,
0xD06BA216
|
X1 中储存着输入
多执行几次可以发现这应该是一个整体,以 8 字节为一个整体
最后一个字节表示 index
然后就可以通过看这块内存猜运算了,不过这也局限于比较简单的,如果比较复杂还是猜不出来,这个时候可以用 ida trace
比方说第 1e 个指令赋值 0x6B(上次知道的),想知道 0x6B 是怎么来的就得看 trace 了
可以通过下条件断点精准降落
断下来之后先修改 trace 选项
取消跳过 debugger 段
再点选项上面的 Instruction tracing,然后对下一句语句按 F4,就可以了记录下中间所有的指令以及寄存器的变化,点开 trace window 右键 Export trace to text file,然后就可以面向 trace 逆向了
很容易可以找到 0x6b 是来源于 0x6ECDD62D00 + 0x15A
1
2
3
4
5
6
|
000013D3
libsec2023.so:
0000007042D18DDC
ADD X2, X8, X0 X2
=
000000000000015A
000013D3
libsec2023.so:
0000007042D0D010
LDR X16, [X1,
000013D3
libsec2023.so:
0000007042D0D030
ADD X12, X16, X2 X12
=
B400006ECDD62E5A
000013D3
libsec2023.so:
0000007042D0D074
STR
X12, [SP,
000013D3
libsec2023.so:
0000007042D0D2C4
LDR X0, [SP,
000013D3
libsec2023.so:
0000007042D0D2C8
LDRB W0, [X0] X0
=
000000000000006B
|
0x6ECDD62D00 是这一轮的 X0,再往上其实可以找到是来源于 input 那块空间的后面一个指针,不过知道是来源于这里的就行了
剩余的字节都以此类推,还包括一些跳转
最大的跳转是一个 256 轮次的,可以还原之后,运算一轮对比结果,如果运算十轮都没错,那基本上就是没错了
我最终扣出来的正向代码如下
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
|
int
main()
{
int
input
=
0xffffff
;
/
/
这是输入的剩下的那几位
for
(
int
j
=
0
; j <
256
;
+
+
j)
/
/
这是
0x53
-
>
0x19
之间的
256
次循环
{
int
magic[
3
]
=
{
0x6b
,
0xA2
,
0x16
};
int
t
=
((
input
>>
16
) >>
3
);
int
temp[
3
]
=
{};
for
(
int
i
=
0
; i <
3
;
+
+
i)
/
/
这是
0x28
-
>
0x1d
之间的
3
次循环
{
long
long
s
=
(
2
-
i)
*
8
;
long
long
tt
=
((t | (
input
<<
5
)) >> s) ^ s;
/
/
printf(
"tt = 0x%llx\n"
, tt);
long
long
ttt
=
(tt &
0xff
) <<
2
;
long
long
tttt
=
(((((tt >>
6
) | (tt<<
0x1A
)) &
0x3FC000003
) | ttt) &
3
) | ttt;
/
/
printf(
"tttt = 0x%llx\n"
, tttt);
tttt
+
=
magic[i];
temp[i]
=
tttt &
0xff
;
/
/
printf(
"tttt = 0x%llx\n"
, tttt);
}
int
temp2[
3
]
=
{};
for
(
int
i
=
2
;i >
=
0
;
-
-
i)
/
/
这是
0x32
-
>
0x2a
之间的
3
次循环
{
long
long
t
=
temp[i];
long
long
tt
=
((t >>
5
) | (t <<
3
)) ^ magic[i];
/
/
printf(
"tt = 0x%llx\n"
, tt);
temp2[i]
=
tt &
0xff
;
}
temp2[
0
]
+
=
0x75
;
temp2[
1
] ^
=
0xfe
;
temp2[
2
]
+
=
0xc1
;
for
(
int
i
=
0
; i <
3
;
+
+
i)
/
/
本来这个循环只是
0x50
到
0x45
的两次循环,修改了一下逻辑把前面一部分加进去了
{
if
(i
=
=
0
)
{
long
long
t
=
temp2[i] &
0xff
;
/
/
printf(
"t = 0x%x\n"
, t);
long
long
ttt
=
0xff00
| t;
long
long
tttt
=
(((t >>
0x1f
) | (t <<
1
)) &
0xfffffffe
);
long
long
ttttt
=
((((ttt >>
7
) | (ttt <<
0x19
) &
0x1FE000001
) | tttt) &
1
);
long
long
tt
=
(tttt | ttttt) ^ (
2
-
i);
/
/
printf(
"tt = 0x%x\n"
, tt);
((char
*
)&
input
)[
2
-
i]
=
tt &
0xff
;
}
else
{
long
long
t
=
temp2[i] &
0xff
;
/
/
printf(
"t = 0x%x\n"
, t);
long
long
tt
=
((((t <<
1
) | (t >>
0x1f
)) &
0x1fe
) | ((t >>
7
| t <<
0x19
) &
0x1FFFFFF
))^ (
2
-
i);
/
/
printf(
"tt = 0x%x\n"
, tt);
((char
*
)&
input
)[
2
-
i]
=
tt &
0xff
;
}
}
}
printf(
"result = 0x%08x\n"
,
input
);
}
|
不是很懂有些部分怎么逆,范围也不大,就直接爆破了,最终 solve 代码如下
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
|
int
vm(
int
inp)
{
int
input
=
inp;
/
/
这是输入的剩下的那几位
for
(
int
j
=
0
; j <
256
;
+
+
j)
/
/
这是
0x53
-
>
0x19
之间的
256
次循环
{
int
magic[
3
]
=
{
0x6b
,
0xA2
,
0x16
};
int
t
=
((
input
>>
16
) >>
3
);
int
temp[
3
]
=
{};
for
(
int
i
=
0
; i <
3
;
+
+
i)
/
/
这是
0x28
-
>
0x1d
之间的
3
次循环
{
long
long
s
=
(
2
-
i)
*
8
;
long
long
tt
=
((t | (
input
<<
5
)) >> s) ^ s;
/
/
printf(
"tt = 0x%llx\n"
, tt);
long
long
ttt
=
(tt &
0xff
) <<
2
;
long
long
tttt
=
(((((tt >>
6
) | (tt<<
0x1A
)) &
0x3FC000003
) | ttt) &
3
) | ttt;
/
/
printf(
"tttt = 0x%llx\n"
, tttt);
tttt
+
=
magic[i];
temp[i]
=
tttt &
0xff
;
/
/
printf(
"tttt = 0x%llx\n"
, tttt);
}
int
temp2[
3
]
=
{};
for
(
int
i
=
2
;i >
=
0
;
-
-
i)
/
/
这是
0x32
-
>
0x2a
之间的
3
次循环
{
long
long
t
=
temp[i];
long
long
tt
=
((t >>
5
) | (t <<
3
)) ^ magic[i];
/
/
printf(
"tt = 0x%llx\n"
, tt);
temp2[i]
=
tt &
0xff
;
}
temp2[
0
]
+
=
0x75
;
temp2[
1
] ^
=
0xfe
;
temp2[
2
]
+
=
0xc1
;
for
(
int
i
=
0
; i <
3
;
+
+
i)
/
/
本来这个循环只是
0x50
到
0x45
的两次循环,修改了一下逻辑把前面一部分加进去了
{
if
(i
=
=
0
)
{
long
long
t
=
temp2[i] &
0xff
;
/
/
printf(
"t = 0x%x\n"
, t);
long
long
ttt
=
0xff00
| t;
long
long
tttt
=
(((t >>
0x1f
) | (t <<
1
)) &
0xfffffffe
);
long
long
ttttt
=
((((ttt >>
7
) | (ttt <<
0x19
) &
0x1FE000001
) | tttt) &
1
);
long
long
tt
=
(tttt | ttttt) ^ (
2
-
i);
/
/
printf(
"tt = 0x%x\n"
, tt);
((char
*
)&
input
)[
2
-
i]
=
tt &
0xff
;
}
else
{
long
long
t
=
temp2[i] &
0xff
;
/
/
printf(
"t = 0x%x\n"
, t);
long
long
tt
=
((((t <<
1
) | (t >>
0x1f
)) &
0x1fe
) | ((t >>
7
| t <<
0x19
) &
0x1FFFFFF
))^ (
2
-
i);
/
/
printf(
"tt = 0x%x\n"
, tt);
((char
*
)&
input
)[
2
-
i]
=
tt &
0xff
;
}
}
}
return
input
;
}
int
main()
{
int
tofind
=
0
;
printf(
"pls input token:"
);
scanf(
"%d"
, &tofind);
int
i;
for
(i
=
0
; i <
0xffffff
;
+
+
i)
{
if
( vm(i)
=
=
tofind )
{
break
;
}
}
unsigned
long
long
aa
=
(unsigned
long
long
)(((unsigned
long
long
)(i &
0xffff00
)) <<
40
) | (unsigned
long
long
)((unsigned
long
long
)(i &
0xff
) <<
24
);
aa |
=
0x7be300df8b2c
;
printf(
"%llu"
, aa);
}
|
后记
vm 逆到最后一天晚上 9 点,那时候还没有解 RSA ,准备出去吃个饭,回来就直接交了,后来看到论坛一个 512 位的 RSA yafu 爆破,觉得还是有希望能出的,11点半解出来两个大质数,当时最终代码不是这样写的,打了一个 0xffffff 的表,直接找表,导致代码庞大,测试打包改wp也要时间,最后上传时间太长,逾期了 3 分钟
当然,最主要还是 rsa 问题以前上手解的少了,工具不熟悉,流程不熟悉,不然也不会最后 10 分钟才完整搞完,既然事实已成,后悔也是无用,今年也要毕业了,明年就没得打了,爷青结