首页
社区
课程
招聘
[原创]看雪 2021 KCTF 春季赛 第五题 华山论剑
2021-5-16 03:35 7699

[原创]看雪 2021 KCTF 春季赛 第五题 华山论剑

2021-5-16 03:35
7699

用jadx打开,发现java层只有简单的输入输出,检查逻辑在native层

 

解包apk,ida打开libhello-jni.so看native层,代码很乱,考虑动态调试
先尝试用ida在真机上调试,体验很不好(最主要的问题是,0x5000处的汇编指令ida识别为"BL LR, #0xBA ",但是单步调试时无法进入这条指令内部,不知道原因)

 

考虑到这个so文件接口不复杂,于是写了一个简单的loader在linux上直接加载(按相对偏移mmap所有的load segment;自己定义一些dummy函数填入got表和JNIEnv.functions表),然后gdb本地调试,体验非常好(方便随时重启;配合插件能看多级指针)(另:0x5000处的汇编在gdb里识别为"ldr lr, [sp], #4",和ida里不一样,而且能单步调试)(但是有一个坑,不能下数据断点(rwatch/watch),让后续分析变得麻烦)

 

loader的代码:

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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <assert.h>
 
struct str {
    const char *s;
    int len;
};
 
//struct str global_name = {.s = "ed8b9244350d3644", .len = 16};
//struct str global_serial = {.s = "7C9815255BFE832D3F93140B", .len = 24};
 
struct str global_name = {.s = "KCTF", .len = 4};
struct str global_serial = {.s = "17726331DA0FE737149C8202", .len = 24};
//struct str global_serial = {.s = "17726331da0fe737149c8202", .len = 24};
 
struct JNINativeInterface_ {
    unsigned int f[0x1000/4];
};
 
typedef struct JNIEnv_ {
    struct JNINativeInterface_ *functions;   
} JNIEnv;
 
void JNICALL_FindClass(JNIEnv *env, const char *name) {
    printf("JNICALL_FindClass %s\n", name);
}
 
void JNICALL_NewStringUTF(JNIEnv *env, const char *utf) {
    printf("%s %s\n", __func__, utf);
}
 
int JNICALL_GetMethodID(JNIEnv *env, void *clazz, const char *name, const char *sig) {
    printf("%s %p %s %s\n", __func__, clazz, name, sig);
    return 0x55555501;
}
 
void *JNICALL_CallObjectMethod(JNIEnv *env, void *obj, int methodID) {
    assert(methodID == 0x55555501);
    printf("%s %p %x\n", __func__, obj, methodID);
    return obj;
}
 
int JNICALL_GetArrayLength(JNIEnv *env, struct str *array) {
    printf("%s %p\n", __func__, array);
    return array->len;
}
 
unsigned char *JNICALL_GetByteArrayElements(JNIEnv *env, struct str *array, int isCopy) {
    printf("%s %p %d\n", __func__, array, isCopy);
    return array->s;
}
 
void JNICALL_ReleaseByteArrayElements(JNIEnv *env, void *array, void *elems, int mode) {
    printf("%s %p %p %d\n", __func__, array, elems, mode);
}
 
void *got_malloc(int size) {
    void *r = malloc(size);
    printf("%s %d %p\n", __func__, size, r);
    return r;
}
 
void got_free(void *p) {
    printf("%s\n", __func__);
    free(p);
}
 
void got_memset(char *p, int n, int count) {
    printf("%s %p %d %d\n", __func__, p, n, count);
    memset(p, n, count);
}
 
void bp(void) {
    ;
}
 
void stack_chk_guard(void) {
    printf("%s\n", __func__);
}
 
void imp___gnu_Unwind_Find_exidx(void) {
    printf("%s\n", __func__);
}
 
void cxa_call_unexpected(void) {
    printf("%s\n", __func__);
}
 
int main(void) {
    int fd = open("libhello-jni.so", O_RDONLY);
    unsigned char *fmem = mmap(NULL, 0x7000, PROT_READ, MAP_PRIVATE, fd, 0);
    unsigned char *mem = mmap(0xdead0000, 0x8000, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
    memcpy(mem, fmem, 0x2b13);
    memcpy(mem+0x3e8c, fmem+0x2e8c, 0x1a8);
    memcpy(mem+0x5000, fmem+0x4000, 0x26bc);
    munmap(fmem, 0x7000);
    close(fd);
    *(unsigned int *)(mem+0x3f98) = mem+0x1034+1;    // xxxxxxxxxx2+1
    *(unsigned int *)(mem+0x3f9c) = stack_chk_guard;
    *(unsigned int *)(mem+0x3fb8) = imp___gnu_Unwind_Find_exidx;
    *(unsigned int *)(mem+0x3fc4) = cxa_call_unexpected;
    *(unsigned int *)(mem+0x3fc8) = mem+0x3f98;    // _GLOBAL_OFFSET_TABLE_@got
    *(unsigned int *)(mem+0x3fa0) = mem+0x10e4+1;    // Java_com_example_hellojni_HelloJni_stringFromJNI_ptr+1
    *(unsigned int *)(mem+0x3fa4) = mem+0x4004;    // f_data_key_dllink
    *(unsigned int *)(mem+0x3fa8) = mem+0x401c;    // f_sucess
    *(unsigned int *)(mem+0x3fdc) = got_malloc;    // malloc@got
    *(unsigned int *)(mem+0x3fe0) = got_memset;    // memset@got
    *(unsigned int *)(mem+0x3fe4) = got_free;    // free@got
    for (int i = 0x3f98; i < 0x4000; i+=4) {
        if (*(unsigned int *)(mem+i) == 0) {
            //*(unsigned int *)(0x77770000+i) = i;
        }
    }
 
    struct JNINativeInterface_ jnii = {.f = {0}};
    for(int i = 0; i < 0x100; i++) {
        jnii.f[i] = 0x11110000+i*4;
    }
    jnii.f[0x18/4] = JNICALL_FindClass;
    jnii.f[0x29c/4] = JNICALL_NewStringUTF;
    jnii.f[0x84/4] = JNICALL_GetMethodID;
    jnii.f[0x88/4] = JNICALL_CallObjectMethod;
    jnii.f[0x2ac/4] = JNICALL_GetArrayLength;
    jnii.f[0x2e0/4] = JNICALL_GetByteArrayElements;
    jnii.f[0x300/4] = JNICALL_ReleaseByteArrayElements;
 
    JNIEnv env;
    env.functions = &jnii;
 
    printf("&global_name: %p, &global_serial: %p\n", &global_name, &global_serial);
 
    bp();
 
    ((void (*)(int, int, int, int, int))(mem+0x10e4+1))(&env, 0xaaaa, &global_name, &global_serial, 0xdddd);
    return 0;
}

编译:arm-linux-gnueabi-gcc loader.c -g -mthumb -o a.out
运行:qemu-arm -L /usr/arm-linux-gnueabi/ -g 1234 ./a.out
调试:gdb-multiarch -ex "file ./a.out" -ex "target remote localhost:1234"

 

(在Ubuntu上可以 apt-get install gcc-arm-linux-gnueabi libc6-armel-cross gdb-multiarch 安装依赖)

 

题目是类似vmp的虚拟机。

 

虚拟机指令的结构:以0x5d04处为例:
从thumb指令开始(0x5d04),通过一个B跳转跳过1或2个dword(0x5d06和0x5d08)到后面的arm指令(0x5d10),先是 BX PC ,然后是 STR PC, [SP,#var_FC] 把PC放入栈,最后B跳转到一个外部函数(0x7270),外部函数返回到下一条指令。
外部函数大部分以push所有寄存器开始,以pop所有寄存器结束,通过栈上保存的PC向前找参数(0x5d06和0x5d08),返回到下一条指令(0x5d1c)。

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
LOAD:00005D04 ; ---------------------------------------------------------------------------
LOAD:00005D04                 CODE16
LOAD:00005D04                 B       sub_5D10        ; Branch
LOAD:00005D04 ; ---------------------------------------------------------------------------
LOAD:00005D06                 CODE32
LOAD:00005D06                 DCW 0xBF00
LOAD:00005D08                 DCD 8, 0x30D00
LOAD:00005D10                 CODE16
LOAD:00005D10
LOAD:00005D10 ; =============== S U B R O U T I N E =======================================
LOAD:00005D10
LOAD:00005D10 ; Attributes: thunk
LOAD:00005D10
LOAD:00005D10 sub_5D10                                ; CODE XREF: LOAD:00005D04↑j
LOAD:00005D10                 BX      PC              ; Branch to/from Thumb mode
LOAD:00005D10 ; ---------------------------------------------------------------------------
LOAD:00005D12                 DCB    1
LOAD:00005D13                 DCB    0
LOAD:00005D13 ; End of function sub_5D10
LOAD:00005D13
LOAD:00005D14                 CODE32
LOAD:00005D14
LOAD:00005D14 ; =============== S U B R O U T I N E =======================================
LOAD:00005D14
LOAD:00005D14
LOAD:00005D14 sub_5D14                                ; CODE XREF: sub_5D10↑j
LOAD:00005D14
LOAD:00005D14 var_FC          = -0xFC
LOAD:00005D14
LOAD:00005D14                 STR     PC, [SP,#var_FC] ; Store to Memory
LOAD:00005D18                 B       sub_7270        ; Branch
LOAD:00005D18 ; End of function sub_5D14
 
LOAD:00005D1C ; ---------------------------------------------------------------------------
LOAD:00005D1C                 CODE16
LOAD:00005D1C                 B       sub_5D28        ; Branch
LOAD:00005D1C ; ---------------------------------------------------------------------------
LOAD:00005D1E                 CODE32

几个关键的位置:
所有的内存读取:0x72fc、0x7304、0x730c
所有的内存写入:0x71f4、0x71fc、0x7204
比较(cmp):0x75ac
加法:0x7644

 

在这8个位置下断点,基本上就能看出程序的完整流程,不需要分析虚拟机指令:
分配并初始化若干个缓冲区
对name做一些运算
对serial做hexdecode(逐字符调用程序里的 sub_DDA 函数,这个函数没有混淆)
初始化RC4的sbox(0x6054附近,循环)
计算RC4(0x638c的附近,循环)

 

前期调试分析过程很漫长,但最终找出serial很简单:
先在 0x638c 下断点,运行到这里后在 0x71f4 下断点,可以发现每计算出一个加密值,就会存入另一个缓冲区中,且这个值与serial做hexdecode之后的值是相等的。所以,只需要把name初始化为"KCTF",就可以在这里提取出正确的serial。

 

name:KCTF
serial:17726331DA0FE737149C8202

 

hexdecode没有区分大小写,因此serial的字母大小写可以替换,造成多解。


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2021-5-16 03:48 被mb_mgodlfyn编辑 ,原因:
收藏
点赞3
打赏
分享
最新回复 (6)
雪    币: 269
活跃值: (906)
能力值: ( LV12,RANK:345 )
在线值:
发帖
回帖
粉丝
AJISky 7 2021-5-17 14:46
2
0
点赞哈哈
雪    币: 4752
活跃值: (2923)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
LeadroyaL 1 2021-5-17 16:10
3
2

BL是 IDA 配置的问题,刚开始我也疑惑了很久,arm的标准一直在演进(例如 v5、v7),这个 so 在声明时使用的是 v5(使用 readelf -A),但这个机器码疑似是 v7 才有的,导致 IDA 识别错误(也可能是 IDA 本身默认就是这个错误的配置)。


正确的 IDA 配置方式如图(此时就可以下断点和调试):



出现疑问可以使用 pwntools 来验证猜想


In [3]: disasm(bytes.fromhex('5df804eb'), arch='thumb')

Out[3]: '   0:   f85d eb04       ldr.w   lr, [sp], #4'


In [4]: disasm(bytes.fromhex('5df8'), arch='thumb')

Out[4]: '   0:   Address 0x0000000000000000 is out of bounds.'


断点不成功,因为 BL 是 2byte 的,运行时LDR是 4byte 的,下断点必然会抽风。



最后于 2021-5-17 16:12 被LeadroyaL编辑 ,原因:
雪    币: 19662
活跃值: (10428)
能力值: ( LV15,RANK:1454 )
在线值:
发帖
回帖
粉丝
mb_mgodlfyn 12 2021-5-17 20:18
4
0
原来是这个原因,非常感谢!
雪    币: 335
活跃值: (1257)
能力值: ( LV4,RANK:48 )
在线值:
发帖
回帖
粉丝
These-us 2021-5-17 20:59
5
0

师傅请教一下为什么您的IDA反编译出来的代码存在0x5d04,而我反编译的出现这样的,是怎么得到师傅的那种情况的哇

雪    币: 250
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
wx_Fruits Basket 2021-5-21 17:23
6
0
楼上,从thumb指令开始(0x5d04),通过一个B跳转跳过1或2个dword(0x5d06和0x5d08)到后面的arm指令(0x5d10),先是 BX PC ,然后是 STR PC, [SP,#var_FC] 把PC放入栈,最后B跳转到一个外部函数(0x7270),外部函数返回到下一条指令。  文中写的很清楚  这些是thumb指令IDA要 alt+g进行指令转换才能识别的
雪    币: 250
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
wx_Fruits Basket 2021-5-23 21:34
7
0

大佬,多级指针的那个插件可以分享下么

最后于 2021-5-24 15:29 被wx_Fruits Basket编辑 ,原因:
游客
登录 | 注册 方可回帖
返回