案例APK:https://github.com/lyousan/pic-host/raw/main/apks/ctf1.apk

目标:将核心算法由ARM32还原为C代码并正确运行。

前置要求:假定已经会一点C语言了,至少能看懂一级指针。

0x0 定位关键函数

使用jadx反编译apk,类很少,关键方法很容易就能找到,最终方法的实现是在so层,使用IDA Pro打开对应的so文件,为了简化分析过程,这里选择32位的so进行还原。so层函数采用的静态注册,在导出表中可以直接搜索到目标函数。

经过IDA反编译之后可以明显看出关键函数就是j_TestDec

   TestDec函数如下,这就是我们待会儿要还原成C语言的汇编代码。

0x1 初步分析

这次的主要目标是还原核心算法,整个so层函数的逻辑就大致分析一下,确定一下关键函数的入参即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int __fastcall Java_com_testjava_jack_pingan2_cyberpeace_CheckString(JNIEnv *a1, int a2, int a3) // a3 就是java层传入的字符串
{
int v3; // r8
const char *v4; // r9
size_t v5; // r6
char *v6; // r5

v3 = 0;
v4 = (const char *)((int (__fastcall *)(JNIEnv *, int, _DWORD))(*a1)->GetStringUTFChars)(a1, a3, 0); // 将java的字符串转换到c语言的字符串
v5 = strlen(v4); // 获取字符串的长度
v6 = (char *)malloc(v5 + 1); // 申请一块新的内存空间,大小是字符串长度+1,+1是因为字符串的结尾需要添加一个\0
memset(&v6[v5], 0, v5 != -1); // 初始化这一块内存空间,避免出现脏数据
qmemcpy(v6, v4, v5); // 将输入的字符串填充到这块内存中
j_TestDec((int)v6); // 关键函数
if ( !strcmp(v6, "f72c5a36569418a20907b55be5bf95ad") ) // 比较经过j_TestDec函数处理后的字符串是否与"f72c5a36569418a20907b55be5bf95ad"一致
v3 = 1;
return v3;
}

经过初步分析后,我们可以得知关键函数的入参是一个char*的指针。

0x2 ARM指令前置知识

这里只简单描述一下本次还原中会涉及到的几个ARM指令,遇到不会的指令可以再去问问GPT。

指令 含义
MOV 移动,相当于赋值,比如MOV R0, #1可以理解为R0=1
ADD 加法,比如ADD R0, R1, R2可以理解为R0=R1+R2
BL 带返回的跳转,多用在函数调用上,其中B就是跳转的意思,单个B相当于是单程的船票,BL相当于是往返的船票
CMP 比较,比如CMP R0, #2,其本质是做减法,通常还会紧跟其他指令一起使用,这个指令会改变标志位中的数据,然后其他指令就可以根据标志位来处理不同的情况,根据标志位的不同 可以表示出大于、小于、等逻辑运算符。
BCC 当标志位处于小于的情况时就跳转到指定位置去执行,其中B跳转,CC是小于的意思,通常和CMP连用。结合上一条CMP指令,可以理解为当R0<2的时候就跳转到某个地方去执行。
LDR 从内存中加载数据到寄存器中,比如LDR R0,[R1],此时R1中存放的是一个地址,[R1]就是取R1中存放的地址 在内存中所指向的值,可以理解为R0=*R1
STR 将寄存器中的数据保存到内存中,STR R0,[R1],此时R1中存放的是一个地址,将R0的值写入到[R1]这块内存里,可以理解为*R1=R0

0x3 算法还原

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
                PUSH            {R4,R5,R7,LR}    ;准备所需的栈空间 跟我们这次还原关系不大,暂时不懂也没事
ADD R7, SP, #8 ;准备所需的栈空间 跟我们这次还原关系不大,暂时不懂也没事
MOV R4, R0 ;R0就是传入的参数 ==> char* r4=arg;
BLX strlen ;执行strlen方法,参数在R0中,返回值也在R0中 ==> int len=strlen(arg);
CMP R0, #2 ;将R0与2作比较
BCC loc_108A ;当R0<2时,跳转到loc_108A,需要注意这里的R0已经是strlen的返回值了 ==> len < 2
MOVS R5, #0 ;将0赋值给R5 ==> int r5=0;
loc_1072 ;这里其实是一个循环体,可以看到在这一段的结尾会继续跳转执行
ADDS R1, R4, R5 ;将R4和R5相加,并赋值给R1 ==> char* r1=r4+r5; 这里r4是char*,是我们传入的参数
LDRB R0, [R4,R5] ;将[R4,R5]这块内存空间中的值赋给R0 ==> r0=*(r4+r5);
LDRB R2, [R1,#0x10] ;将[R1,#0x10]这块内存空间中的值赋给R2 ==> char r2=*(r1+0x10);
STRB R2, [R4,R5] ;将R2的值写入到[R4,R5]这块内存中 ==> *(r4+r5)=r2;
ADDS R5, #1 ;R5+=1
STRB R0, [R1,#0x10] ;将R0的值写入到[R1,#0x10]这块内存中 ==> *(r1+0x10)=r0;
MOV R0, R4 ;将R4赋给R0,用于执行strlen
BLX strlen
CMP.W R5, R0,LSR#1 ;比较R5和右移1位后的R0
BCC loc_1072 ;当R5<R0>>1时,跳转到loc_1072继续执行 ==> r5 < len>>1; 到这里就可以将这一段汇编改写成for循环了
loc_108A ;
LDRB R0, [R4] ;将R4这块内存中的值写入到R0中 ==> r0=*r4;
CBZ R0, locret_10B8 ;如果R0等于0的话就跳转到locret_10B8执行 ==> if(r0==0)return;
LDRB R1, [R4,#1] ;将[R4,#1]这块内存空间中的值赋给R1 ==> r1=*(r4+1);
STRB R1, [R4] ;将R1的值写入到[R4]这块内存中 ==> *r4=r1;
STRB R0, [R4,#1] ;将R0的值写入到[R4,#1]这块内存中 ==> *(r4+1)=r0;
MOV R0, R4
BLX strlen
CMP R0, #3
BCC locret_10B8 ;当R0<3时,跳转到locret_10B8执行 ==> if(len < 3)return;
MOVS R5, #0 ;R5+=1
loc_10A0 ;与上面loc_1072类似,也是一个循环
ADDS R0, R4, R5
LDRB R1, [R0,#2]
LDRB R2, [R0,#3]
STRB R2, [R0,#2]
STRB R1, [R0,#3]
MOV R0, R4
BLX strlen
ADDS R1, R5, #4
ADDS R5, #2
CMP R1, R0
BCC loc_10A0
locret_10B8 ;这一段相当于return
POP {R4,R5,R7,PC} ;恢复栈空间

还原成C的参考代码:

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void TestDec(char *str)
{
char *r4 = str;
size_t len = strlen(str);
if (len >= 2)
{
for (int r5 = 0; r5 < strlen(r4) >> 1; r5 += 1)
{
char *r1 = r4 + r5;
char r0 = *(r4 + r5);
char r2 = *(r1 + 0x10);
*(r4 + r5) = r2;
*(r1 + 0x10) = r0;
}
}
char r0 = *r4;
if (r0 == 0)
return;
char r1 = *(r4 + 1);
*r4 = r1;
*(r4 + 1) = r0;
if (strlen(r4) < 3)
return;
int rr0 = 0;
for (int r5 = 0; rr0 < strlen(r4); r5 += 2)
{
char *r0 = r4 + r5;
char r1 = *(r0 + 2);
char r2 = *(r0 + 3);
*(r0 + 2) = r2;
*(r0 + 3) = r1;
rr0 = r5 + 4;
}
}

int main(int argc, char const *argv[])
{
char input[] = "f72c5a36569418a20907b55be5bf95ad";
size_t len = strlen(input);
char *str = malloc((strlen(input) + 1) * sizeof(char));
strcpy(str, input);
TestDec(str);
printf("flag: %s\n", str); // 90705bb55efb59da7fc2a5636549812a
free(str);
return 0;
}

0x4 总结

初学ARM汇编,熟悉一下这些常用的寄存器,可以先从ARM32入手,寄存器少一点。先对这些汇编指令有个大概的印象,囫囵吞枣的看都行,先大概知道这些指令是干什么的,看不懂的时候再去问GPT。建议把汇编和c语言对照着看,这样理解起来更快,然后就可以开始尝试把汇编翻译成伪代码,再逐步完善成C代码。