-
-
[原创]lcweik的第三题解题思路
-
发表于: 2015-10-18 23:21 4189
-
第三题writeup
拿到题目,我先反编译java代码,看到,关键函数是check函数。
check函数是一个native函数,所以要把libcrackme.so放到ida去分析。
这个so在ida里看到的东西有很多乱字符串,也看不到函数,
所以先把elf头里的节偏移与节大小抹为0,再用ida重新打开so文件,就能看到函数了。
但里,这里没有找到JNI_onLoad函数,使用readelf -s libcrackme.so,
却提示readelf: Error: no .dynamic section in the dynamic segment
为了得到JNI_onload的地址,我写了一个libd.so文件,这里调用dlopen打开libcrackme.so。
dlopen返回的是soinfo结构体,用下面的代码从内存中寻找so里的符号。
soinfo *si = dlopen("/data/local/libcrackme.so");
Elf32_Sym *symtab = si->symtab;
unsigned n;
for (n=0; n< 300; ++n){
Elf32_Sym *s =symtab + n;
printf("n=%d %d %x %x %x %s %p\n", n, si->chain[n], s->st_name, s->st_value, s->st_size, get_string(s->st_name, si), si->base + s->st_value);
char *sym_name = get_string(s->st_name, si);
if (!strcmp(sym_name, "JNI_OnLoad")) {
printf("%s %d %p\n", sym_name, n, s->st_value);
}
}
最终得到的JNI_OnLoad的地址是0x1c5f5。check函数的静态符号不存在,check应该是动态注册上去的。
在ida里反汇编地址0x1c5f5的代码。
发现这里的代码被混淆过了,使用了很多跳转,很难分析,然后,再试一下别的方法去定位check的地址。
在dalivk虚拟机里,对于非内部native方法,Method结构里的insns成员变量保存了它的native地址。
对于Method结构体的获取方式,可以在dvmFindDirectMethodHierByDescriptor下断,其返回值就是Method地址。
JNI_OnLoad函数调用完后,就可以从Method结构体里取insns了。
check函数的native地址是0x19C39
在ida里逆向check函数,发现,也是混淆过的,难以分析。
于是使用ida的trace功能去跟踪代码,但跟踪了一个小时后还是没有结果,
从trace的记录里看,发现check函数会去调用libdvm.so的某些方法将jstring转换为字符串数组。
获取到字符串数组后,返回到0x2076D,在这里,从[r0]可以看到用户输入的坐标。
这个时候得使用内存断点了,我使用gdb+armv8模拟器进行内存断点跟踪。
(armv7好像不支持硬件断点,所以不能设置内存断点,好像ida不支持armv8的设备,所以得用gdb,
gdb里内存断点触发时,需要先将内存断点disable,才能执行si,然后再enable,并使用c继续执行)。
假定0x2076D里得到的用户输入坐标的内存地址为input_addr1,
在gdb里使用awatch *input_addr1就能设置内存读写断点,并告诉我,断点号为2。
继续执行,随后的代码调用一次strlen获取字符串长度,然后在0xF527地址处,读取input_addr1的数据。
开始猜测这里可能是算法或者比较逻辑了,所以详细地调试了这段代码0xF527,得到了两点信息:
1、0xF527地址的代码只是将input_addr1拷贝到新的内存区域input_addr2,input_addr2的尾号总是0x618。
2、代码混淆的规律:
a.代码被分成块与片,一个块可能是一个函数,一个片包含0-n行arm汇编。
块的开始位置是一个地址表,每四个字节保存着一个偏移地址,这个偏移地址+地址表的起始地址+8=片的起始地址。
片的后面保存着一个地址表中的索引,这个索引对应的地址就是当前片执行完后,将执行的下一片的地址。
b.在多个片拼成的代码里面又存在一种根据状态字改变流程的混淆方式:
将状态字的相对偏移保存在代码片中,通过一个函数sub_2A5A4,从该偏移取到状态字,再把状态字压栈,
随后的代码里,分别将状态字出栈,赋值给到r1和r4。
比较r1和r4,当r1==r4时,进入正确的执行顺序。
c.在单个片末尾,存在跳转改变代码执行流程的方式:
这时,片的后面,不再保存地址表的偏移,而是保存新的地址的相对偏移,
通过一个函数sub_2A594获取到该偏移,并跳转。
将1中的内存区域input_addr2也下内存断点,并继续跟踪,发现input_addr1会被抹空,数据只保存在input_addr2,
随后,会将input_addr2的字符串末尾设置\0,然后会调用两次strlen,调用一次strcpy,得到数据input_addr3。
对input_addr3下断点,又跟踪到0x17CB0,这里做了很复杂的运算,会根据用户的输入数据生成很多数据。
至此,个人认为还是要对代码进行反混淆处理才能识别它的算法。
最大的一块代码是地址表地址为0xF734的代码,以下代码描述了如何静态找到混淆规律中的a的正确顺序
unsigned char b1[1024*1024];
unsigned char b2[1024*1024];
int s1;
int s2;
typedef signed u4;
u4 getTarget(unsigned char* code, u4 pc) {
if ((code[1] & 0xf8) == 0xf0 && (code[3] & 0xf8) == 0xf8) {
u4 j1 = (code[1] & 0x7) << 8;
u4 j2 = (code[0]) + j1;
u4 j3 = j2 << 12;
u4 j4 = (code[3] & 0x7) << 8;
u4 j5 = (code[2]) + j4;
u4 j6 = j5 << 1;
//printf("%p %p %p %p %p\n", j3, j5, j6, code[3], code[2]);
u4 result = j3 + j6 + pc + 4;
if (j1 & 0x400)
result = result - 0x800000;
return result;
}
return 0;
}
u4 getPosBL(int pos) {
return getTarget(b1 + pos, pos);
}
u4 get04b501(int pos) {
//printf("cc %p\n", b1[pos - 3]);
if (b1[pos - 4] == 0x03 && b1[pos - 3] == 0xb5 && b1[pos - 2] == 0x01)
return pos - 4;
if (b1[pos - 6] == 0x03 && b1[pos - 5] == 0xb5 && b1[pos - 4] == 0x01)
return pos - 6;
return 0;
}
u4 getCodeOffsetFromTable(u4 table, int index) {
return *(u4*)&b1[table + index*4 + 8] + table + 4;
}
int main(){}
...
FILE *f1 = fopen("libcrackme.so", "rb");
s1 = fread(b1, 1, 1024*1024, f1);
...
for (i=0; i<s1; i+=2) {
u4 r = getPosBL(i); //获取BL跳转的目标地址,若该位置不是BL语句,返回0
if (r == 0xF734) { //表地址
u4 p = get04b501(i); //获取push r0 r1 lr; ldr r0, xx的地址
int appendix_index = 4; //表索引的偏移位置
if (i%4) {
appendix_index = 6; //位置需要对齐4字节
}
u4 *pos_index = (u4*)&b1[i+appendix_index]; //获取索引
u4 offset_code = getCodeOffsetFromTable(0xF734, *pos_index); //获取偏移
printf("change %p to B %p\n", p, offset_code);
...
}
}
}
...
}
至此,比赛时间结束。
拿到题目,我先反编译java代码,看到,关键函数是check函数。
check函数是一个native函数,所以要把libcrackme.so放到ida去分析。
这个so在ida里看到的东西有很多乱字符串,也看不到函数,
所以先把elf头里的节偏移与节大小抹为0,再用ida重新打开so文件,就能看到函数了。
但里,这里没有找到JNI_onLoad函数,使用readelf -s libcrackme.so,
却提示readelf: Error: no .dynamic section in the dynamic segment
为了得到JNI_onload的地址,我写了一个libd.so文件,这里调用dlopen打开libcrackme.so。
dlopen返回的是soinfo结构体,用下面的代码从内存中寻找so里的符号。
soinfo *si = dlopen("/data/local/libcrackme.so");
Elf32_Sym *symtab = si->symtab;
unsigned n;
for (n=0; n< 300; ++n){
Elf32_Sym *s =symtab + n;
printf("n=%d %d %x %x %x %s %p\n", n, si->chain[n], s->st_name, s->st_value, s->st_size, get_string(s->st_name, si), si->base + s->st_value);
char *sym_name = get_string(s->st_name, si);
if (!strcmp(sym_name, "JNI_OnLoad")) {
printf("%s %d %p\n", sym_name, n, s->st_value);
}
}
最终得到的JNI_OnLoad的地址是0x1c5f5。check函数的静态符号不存在,check应该是动态注册上去的。
在ida里反汇编地址0x1c5f5的代码。
发现这里的代码被混淆过了,使用了很多跳转,很难分析,然后,再试一下别的方法去定位check的地址。
在dalivk虚拟机里,对于非内部native方法,Method结构里的insns成员变量保存了它的native地址。
对于Method结构体的获取方式,可以在dvmFindDirectMethodHierByDescriptor下断,其返回值就是Method地址。
JNI_OnLoad函数调用完后,就可以从Method结构体里取insns了。
check函数的native地址是0x19C39
在ida里逆向check函数,发现,也是混淆过的,难以分析。
于是使用ida的trace功能去跟踪代码,但跟踪了一个小时后还是没有结果,
从trace的记录里看,发现check函数会去调用libdvm.so的某些方法将jstring转换为字符串数组。
获取到字符串数组后,返回到0x2076D,在这里,从[r0]可以看到用户输入的坐标。
这个时候得使用内存断点了,我使用gdb+armv8模拟器进行内存断点跟踪。
(armv7好像不支持硬件断点,所以不能设置内存断点,好像ida不支持armv8的设备,所以得用gdb,
gdb里内存断点触发时,需要先将内存断点disable,才能执行si,然后再enable,并使用c继续执行)。
假定0x2076D里得到的用户输入坐标的内存地址为input_addr1,
在gdb里使用awatch *input_addr1就能设置内存读写断点,并告诉我,断点号为2。
继续执行,随后的代码调用一次strlen获取字符串长度,然后在0xF527地址处,读取input_addr1的数据。
开始猜测这里可能是算法或者比较逻辑了,所以详细地调试了这段代码0xF527,得到了两点信息:
1、0xF527地址的代码只是将input_addr1拷贝到新的内存区域input_addr2,input_addr2的尾号总是0x618。
2、代码混淆的规律:
a.代码被分成块与片,一个块可能是一个函数,一个片包含0-n行arm汇编。
块的开始位置是一个地址表,每四个字节保存着一个偏移地址,这个偏移地址+地址表的起始地址+8=片的起始地址。
片的后面保存着一个地址表中的索引,这个索引对应的地址就是当前片执行完后,将执行的下一片的地址。
b.在多个片拼成的代码里面又存在一种根据状态字改变流程的混淆方式:
将状态字的相对偏移保存在代码片中,通过一个函数sub_2A5A4,从该偏移取到状态字,再把状态字压栈,
随后的代码里,分别将状态字出栈,赋值给到r1和r4。
比较r1和r4,当r1==r4时,进入正确的执行顺序。
c.在单个片末尾,存在跳转改变代码执行流程的方式:
这时,片的后面,不再保存地址表的偏移,而是保存新的地址的相对偏移,
通过一个函数sub_2A594获取到该偏移,并跳转。
将1中的内存区域input_addr2也下内存断点,并继续跟踪,发现input_addr1会被抹空,数据只保存在input_addr2,
随后,会将input_addr2的字符串末尾设置\0,然后会调用两次strlen,调用一次strcpy,得到数据input_addr3。
对input_addr3下断点,又跟踪到0x17CB0,这里做了很复杂的运算,会根据用户的输入数据生成很多数据。
至此,个人认为还是要对代码进行反混淆处理才能识别它的算法。
最大的一块代码是地址表地址为0xF734的代码,以下代码描述了如何静态找到混淆规律中的a的正确顺序
unsigned char b1[1024*1024];
unsigned char b2[1024*1024];
int s1;
int s2;
typedef signed u4;
u4 getTarget(unsigned char* code, u4 pc) {
if ((code[1] & 0xf8) == 0xf0 && (code[3] & 0xf8) == 0xf8) {
u4 j1 = (code[1] & 0x7) << 8;
u4 j2 = (code[0]) + j1;
u4 j3 = j2 << 12;
u4 j4 = (code[3] & 0x7) << 8;
u4 j5 = (code[2]) + j4;
u4 j6 = j5 << 1;
//printf("%p %p %p %p %p\n", j3, j5, j6, code[3], code[2]);
u4 result = j3 + j6 + pc + 4;
if (j1 & 0x400)
result = result - 0x800000;
return result;
}
return 0;
}
u4 getPosBL(int pos) {
return getTarget(b1 + pos, pos);
}
u4 get04b501(int pos) {
//printf("cc %p\n", b1[pos - 3]);
if (b1[pos - 4] == 0x03 && b1[pos - 3] == 0xb5 && b1[pos - 2] == 0x01)
return pos - 4;
if (b1[pos - 6] == 0x03 && b1[pos - 5] == 0xb5 && b1[pos - 4] == 0x01)
return pos - 6;
return 0;
}
u4 getCodeOffsetFromTable(u4 table, int index) {
return *(u4*)&b1[table + index*4 + 8] + table + 4;
}
int main(){}
...
FILE *f1 = fopen("libcrackme.so", "rb");
s1 = fread(b1, 1, 1024*1024, f1);
...
for (i=0; i<s1; i+=2) {
u4 r = getPosBL(i); //获取BL跳转的目标地址,若该位置不是BL语句,返回0
if (r == 0xF734) { //表地址
u4 p = get04b501(i); //获取push r0 r1 lr; ldr r0, xx的地址
int appendix_index = 4; //表索引的偏移位置
if (i%4) {
appendix_index = 6; //位置需要对齐4字节
}
u4 *pos_index = (u4*)&b1[i+appendix_index]; //获取索引
u4 offset_code = getCodeOffsetFromTable(0xF734, *pos_index); //获取偏移
printf("change %p to B %p\n", p, offset_code);
...
}
}
}
...
}
至此,比赛时间结束。
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
赞赏
他的文章
- deodex android o的vdex文件 21440
- [原创][转帖]分享一个最好用的root方法,大家一起来捣鼓一下 5393
- 逆向修改手机内核,绕过反调试 57062
- 没有标题的水贴 5633
看原图
赞赏
雪币:
留言: