3

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

 3 years ago
source link: https://bbs.pediy.com/thread-267627.htm
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
[原创]看雪 2021 KCTF 春季赛 第五题 华山论剑-CTF对抗-看雪论坛-安全社区|安全招聘|bbs.pediy.com
[原创]看雪 2021 KCTF 春季赛 第五题 华山论剑
2021-5-16 03:35 1291

用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的代码:

#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)。

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的字母大小写可以替换,造成多解。

[看雪官方培训] Unicorn Trace还原Ollvm算法!《安卓高级研修班》2021年6月班火热招生!!

最后于 2021-5-16 03:48 被mb_mgodlfyn编辑 ,原因:
最新回复 (5) 2 楼 3 楼

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

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

735539_79CN3BZCVS6HYCF.jpg

735539_H47FVVACBXA92DG.jpg

出现疑问可以使用 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 的,下断点必然会抽风。

最后于 5天前 被LeadroyaL编辑 ,原因:
4 楼
原来是这个原因,非常感谢!
These-us 活跃值 5天前
5 楼

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

849527_HJMFP4GNHSFP8XP.jpg

6 楼
楼上,从thumb指令开始(0x5d04),通过一个B跳转跳过1或2个dword(0x5d06和0x5d08)到后面的arm指令(0x5d10),先是 BX PC ,然后是 STR PC, [SP,#var_FC] 把PC放入栈,最后B跳转到一个外部函数(0x7270),外部函数返回到下一条指令。  文中写的很清楚  这些是thumb指令IDA要 alt+g进行指令转换才能识别的
游客
登录 | 注册 方可回帖
返回

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK