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

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

2021-5-16 03:35
8528

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

编译: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)。

几个关键的位置:
所有的内存读取: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的字母大小写可以替换,造成多解。

 
 
 
#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;
}
#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) {

[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

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

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编辑 ,原因:
2021-5-17 16:10
2
雪    币: 23575
活跃值: (12110)
能力值: ( LV15,RANK:1744 )
在线值:
发帖
回帖
粉丝
4
原来是这个原因,非常感谢!
2021-5-17 20:18
0
雪    币: 337
活跃值: (1257)
能力值: ( LV4,RANK:48 )
在线值:
发帖
回帖
粉丝
5

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

2021-5-17 20:59
0
雪    币: 250
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
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进行指令转换才能识别的
2021-5-21 17:23
0
雪    币: 250
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
7

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

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