/*
* 作者:wuaiwu
* 原文链接:http://www.sanwho.com/552.html
* 转载请注明出处
*/
一、Ndk下内联汇编
跟vc下一样,ndk编译环境下也能使用内联汇编,如下:
include <stdio.h>
int my_thumb(int dummy)
{
__asm__(
“mov r0,#1 \t\n”
“mov r1,#2 \t\n”
“add r0,r0,r1 \t\n”
“bx lr”
);
}
int main()
{
int n = my_thumb(12);
printf(“result :%08x”,n);
return 0;
}
再看看使用标签的例子:
#include <stdio.h>
int my_thumb(int dummy)
{
__asm(
“mov r0,#0x1\t\n”
“ldr r0,__start\t\n”
“adr r0,__start\t\n”
“ldr r0,=__start\t\n”
“__start:\t\n”
“nop\t\n”
);
}
int main()
{
int n = my_thumb(12);
printf(“result :%08x”,n);
return 0;
}
二、编译
创建android.mk文件,然后使用ndk-build进行编译
1)编译一个可执行程序,Android.mk文件的内容如下:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE:= helloa
LOCAL_SRC_FILES := testHello.c
include $(BUILD_EXECUTABLE)
2)编译一个so,android.mk文件内容如下:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := testJniSo
LOCAL_SRC_FILES := testJniSo.c
include $(BUILD_SHARED_LIBRARY)
3)必须有一个jni目录,目录结构:
4)这样之后,就可以在cmd窗口中,切换到代码目录,使用ndk-build命令进行编译。
三、调试可执行程序
1)Push 编译好的elf到样机中
2)使用gdbserver 启动该文件
“/data/local/gdbserver :12345 /data/local/helloa”
3) adb forward tcp:12345 tcp:12345
4) 启动gdb
5) Target remote 127.0.0.1:12345
6) Ni 是单步,set disassemble-next on 下一句指令显示反汇编,使用set arm force-mode arm或者set arm force-mode thumb让gdb切换thumb和arm代码显示。
7) Display /i $pc 显示当前的代码
8) Continue 就是windbg的f5,od的f9
9) Info breakpoints 显示断点,而 delete删除断点 disable禁用断点
10) Disas 0xAAAA,+20(20字节的数据) 显示反汇编
11) 调试比较
四、调试so
以爱加密为例,进行调试,范例是看雪求助帖给出的,链接如下:
http://bbs.pediy.com/showthread.php?t=184375
调试方法
1)So中实现的方法,程序跑起来再附加的话,该方法可能已经执行完。看雪上给出方法步骤较多,应用和底层都调试,有点复杂如下:
我试出来的方法,对so文件进行修改,让其在入口处直接调用它本身。需要注意的是,必须在so的入口函数处修改,如果在内部调用函数修改,app会自动退出。
2)gdb的调试,使用ndk下自带的gdbserver和gdb进行调试
3)将修改的so导入到手机该应用的lib目录下,覆盖之前的so文件。然后运行app,出现只有边界的黑框。Gdbserver附加,使用set 命令,将bl 调用自身的命令改回去,则可以进行正常调试了。
4)模块基地址的获取 cat /proc/pid/maps 找到
5)Dump 内存的命令式 dump binary memory c:\xxx startAd endAd
6) 小细节要注意的是:.init_proc的地址是0x31cb9,ida直接点过去,是一堆数据,摁c无法转成代码
因为arm指令和thumb指令是2字节或者4字节对齐的,所以,改地址加1应该是真正的地址。将该地址修改为调用自身,然后gdb附加,成功断下来,然后输入“i r $lr”查看返回值,如下
此处的在linker中的代码如下:
7)至于为什么系统的linker会调用.init_proc,不得而知。该so的oep为空,没有section表,但是有dynamic section表。自己写一个so,并没有函数“init_proc”的导出,需要再研究下elf文件格式。
自解码
本身的so是经过加密的,解密的代码是在.init_proc+1的位置,字节码的代码较长,部分代码如下:
1)windows下逃避断点的方法,就是利用“code shadow”的方式,在申请内存,然后将代码拷贝到指定的申请的空间上执行,这样,直接断原代码是断不到的。但是,直接搜索二进制就可以找到这样的镜像代码。
如下,之前应该运行的代码:
拷贝到其它地方执行的代码:
2)计算指定函数的汇编指令长度:代码要进行拷贝执行,必须计算拷贝代码的长度。爱加密计算拷贝代码的长度方式是通过call来计算的。函数实现的排布如下:
__asm call targetFunc
xxxxxx
xxxxxx
xxxxxx
xxxxxx
xxxxxx
xxxxxx
Void targetFunc
而call的指令是:
00 f0是指令opcode,c0与跳转的距离有关,计算公式是(targetAddr – curAddr)/2 – 2 =opdataLen。那么知道c0就知道中间xxx代码的长度了。
3)爱加密处理的数据有三块,第一块未知,第二块是压缩过的指令数据,第三块是需要拷贝到堆上的代码,这三块数据总长度为0x29e3c,使用mmap2进行申请映射内存,如下:
在此之后,数据内存排布:
AAAAAAAAA
BBBBBBBBBB
CCCCCCCCCC
爱加密还原代码前,处理了三块数据,如上。其中,“AAAAAAAAAA”数据是一组未使用过的数据,具体功能未知,长度为0x6b0;“BBBBBBBBBB”代表的是lzma压缩过的数据;“CCCCCCCCCC”代表的是拷贝函数的数据(一共拷贝三个函数的代码)。
4)修改lr,然后刷新cache,返回到mmap2得到的堆上的函数进行执行
OAD:510A4010 01 1C MOVS R1, R0 ; r0 0x4fb83e3c
LOAD:510A4012 10 1C MOVS R0, R2 ; r2 0x4fb83bd8
LOAD:510A4014 BE 46 MOV LR, R7 ; r7 0x4fb83d91
LOAD:510A4014 ; 这里对lr寄存器进行修改
LOAD:510A4016 C3 E7 B libexec___ARM_NR_cacheflush
5)再次调用mmap2,抹掉最初的“AA”、“BB”和“CC”三块数据,这样,之前执行的代码和数据将都被清零。这三块数据就被搬运到第一次调用mmap2得到的地址上,数据在内存重新做了排布。
6)解压数据:将数据解压到第二次调用mmap2清零的地址上,偏移为0x6b0。数据的解压使用的是lzma算法,解压的代码:
上图中,r4指向的lzma的解压函数,r0是待解压的数据,r1是待解压的长度,r2是解压到的地址,r3上是保存解压到的地址空间的长度的地址。Lzma的具体实现:
7)修正指定数据:解压完的数据并不是最终的代码,还需要进行修正。修正代码如下:
修正的方法是:解压后的数据从末端开始往前,每四字节为单位,如果这四字的最高个字节是低四位是0xb,则进行移位修复。(这种方法很像pe壳中解密代码后,对0xe8、0xe9、0xff25等的修复)修复代码如下:
解密前的导出函数代码如下:
解码后的代码分析
解密后的代码上边已经给出,r0传递的就是jnienv虚表指针,gdb中显示相关api在libicuil8n.so模块中,如下:
1)动态注册接口:解密后的代码主要是动态注册用于java调用的接口,如下:
gdb调试的问题:
1)Gdb在遇到bx pc;nop指令,从thumb指令切换到arm指令时,会出错,调试无法继续进行。解决方法:
2)gdb遇到如下的拷贝指令也会退出调试,解决方法是跳过这段代码的位置下断点。
LOAD:510A3F88 loc_510A3F88 ; CODE XREF: libexec_memcopy+1Ej
LOAD:510A3F88 DC 13 B1 E8 LDMIA R1!, {R2-R4,R6-R9,R12}
LOAD:510A3F8C 01 50 55 E2 SUBS R5, R5, #1 ; 这里r5不断的减少
LOAD:510A3F90 DC 13 A0 E8 STMIA R0!, {R2-R4,R6-R9,R12}
LOAD:510A3F94 FB FF FF 1A BNE loc_510A3F88
LOAD:510A3F98 D4 03 BD E8 LDMFD SP!, {R2,R4,R6-R9}