菜鸟总结 so 分析,arm 汇编,IDA 静态分析:https://www.52pojie.cn/thread-695063-1-1.html
JNI 静态注册 so 和 IDA 导入的 JNI.h 文件.zip:https://download.csdn.net/download/freeking101/12571373
ARM 静态分析:
代码 和 数据 的 切换:C => 代码D => 数据 A => ascii字符串 U => 解析成未定义的内容p => 识别成一个函数
X => 查看交叉应用
F5 => 查看伪代码
Alt+T => 搜索文本
Alt+B => 搜索十六进制
- https://www.bilibili.com/video/BV1vE411c7Zj?p=49 15' 30''【识别成函数 ,按p】 15' 50''【识别成字符串,按 a】
- https://www.bilibili.com/video/BV1UE411A7rW?p=50
JNI_OnLoad 函数 的 ARM 汇编代码的简单分析
- R0 --- R3:这 4 个寄存器,用来传递函数调用的 第1 到 第4 个参数,超出的参数 通过 堆栈 来传递。
- R0 :R0寄存器 同时用来存放 "函数的返回值",类似 x86 汇编中 ax 寄存器,存放 call 之后的返回值。
- 被调用的函数在返回前无需恢复这些寄存器的内容
关于 ADD PC 寄存器
- https://www.bilibili.com/video/BV1UE411A7rW?p=51
getText 函数分析:
IDA Pro 反编译后,参数类型反编译错误,修改参数类型 和 重命名参数。。。。。
修改变量类型( 快捷键 :按 Y ):
重命名变量名 和 修改变量类型 差不多,这个是右键之后选择 Rename 选项( 快捷键:按 N ),这里 a1 重命名为 env,如图:
同理,参数 int a2 修改为 jobject obj
hide casts ,是代码看起来更清晰
导入 jni.h 头文件。 12'
添加 jni 的结构体。 12' 50''
选择和 jni 相关的结构体进行添加
还原 ARM汇编代码中的方法名:
- https://www.bilibili.com/video/BV1UE411A7rW?p=52
静态函数 getText2 函数的ARM代码分析
ARM 代码
关于 add pc 寄存器: 8' 30''
- https://www.bilibili.com/video/BV1UE411A7rW?p=53
JNI_OnLoad 正向开发:
JNI_OnLoad 的 ARM 代码分析:
C++ 函数定位 13'
C++函数参数对应分析
Cadd 函数、Csub函数 对应 ARM 代码:(思考:怎么通过修改 arm 代码,把 加法 变成 减法???)
菜鸟总结 so 分析,arm 汇编,IDA 静态分析
So 静态 (arm 汇编,IDA 静态分析等)
- R7:栈帧指针(Frame Pointer)。指向前一个保存的栈帧(stack frame)和链接寄存器(link register, lr)在栈上的地址
- R13:又叫SP(stack pointer),是栈顶指针
- R14:又叫LR(link register),存放函数的返回地址。
- R15:又叫PC(program counter),指向当前指令地址
- bl:这条指令对函数进行调用。请记住被调用函数需要的参数已经存储到相关的寄存器中了(r0和r1)。这条指令的执行一般被当做一个分支(branch)。可以理解为执行带链接的分支,也就是说,在跳转到分支之前,会将lr(link register)的值设置为当前函数中将要执行的下一条指令,当从分支(被调函数)中返回时,通过lr中的值可以知道当前函数执行到哪里了。
- blx 中的 x 标示交换 “exchange”,意思是如果有必要,处理器将对指令集模式进行切换。
- 返回值 (存储在 r0 中)
- mov r0, r1 => r0 = r1
- mov r0, #10 => r0 = 10
- ldr r0, [sp] => r0 = *sp
- str r0, [sp] => *sp = r0
- str 把寄存器内容存到栈上去
- ldr 把栈上内容载入一寄存器中
- add r0, r1, r2 => r0 = r1 + r2
- add r0, r1 => r0 = r0 + r1
- push {r0, r1, r2} => 将 r0, r1 和 r2push 到栈中.
- pop {r0, r1, r2} => 将3个值从栈中pop出来,并存放到r0, r1 和 r2中.
- b_label => pc = _label
- bl _label => lr = pc + 4; pc = _label
- self 和 _cmd 占用了 r0 和 r1 寄存器。它存储着当前执行方法的 selector
- 每次调用 Objective-C 方法时,都由 objc_msgSend 方法处理消息的派送。该方法根据传递的消息类型在类的方法列表中查找被调用方法的实现。objc_msgSend方法:id objc_msgSend(id self, SEL _cmd, ...)
- MOV R1, #0 的 机器码计算:这里将 5 位 opcode 分成了两部分 ----- 前 3 位 001 是固定的,后 2 位用于标识 4 中不同的操作: mov, cmp, add, sub。所以 mov 指令的 opcode 二进制表示为 00100;这里 Rd 为 R1,所以 8~10 位为 001;同理, 0~7 为就 0000 0000。所以 MOVS R1, #0 的 2 进制表示为: 0010 0001 0000 0000 = 0x 21 00。
http://blog.csdn.net/zolovegd/article/details/1826192
http://blog.csdn.net/gooogleman/article/details/3758555(opcode学习帖子)
.so 文件(shared object) linux 的动态链接库,
显示调用则是在主程序里使用 dlopen、dlsym、dlerror、dlclose 等系统函数。
1.IDA计算出了成员变量的偏移地址并把 symbol 直接显示出来
IDA:__text:000026C4
mov ebx, ds:(_OBJC_IVAR_$_TestButton_m_model - 26C3h)[esi]
2.函数参数在IDA中被赋予名称,ebp+8为arg_0,ebp+12为arg_1。 arg即为argument的缩写,第n个参数在+号后面的偏移量不是绝对的
。在函数开头和代码中,名称都会直接替换掉实际偏移量。基本上arg_0都是self。
3.常数值型偏移地址被赋予名称,以loc_为前缀。
IDA:jmp short loc_2732
4.局部变量,即 ebp-xxx 会被命名为 var_xxx
搜索特征字符串。具体操作为:①快捷键Ctrl+S,打开搜索类型选择对话框-->双击Strings,跳到字符串段-->菜单项“Search-->Text”;
②快捷键Alt+T,打开文本搜索对话框,在String文本框中输入要搜索的字符串点击OK即可;
(C的函数 ,抹掉了符号表)
So 动态调试 ( .init_array 下断 )
System.loadLibrary()加载so文件流程
- 先读取 so 文件的 .init_array段。
- 再执行 JNI_OnLoad 函数,JNI_ONLoad是.so文件的初始函数
- 然后调用具体的 native 方法
so 被加载之后最开始执行的是 .init_array 段的代码,然后才去执行 jni_onload,那么在 .init_array 处断下来便是很有必要的
- 1. 启动 android_server
- 2. 端口转发:adb forward tcp:23946 tcp:23946
- 3. 调试启动:adb shell am start -D -n com.scottgames.fnaf4/com.putaolab.ptsdk.activity.PTMainActivity
- 4.链接,下断点
Shift+F12 打开 字符串窗口,搜索字符串: dlopen,找到 dlopen 函数的偏移 0xF30
动态调试的 IDA 中, G 跳转到: 400D3000+F30=400D3F30 处,下好断点
搜索字符串: calling
按 F9 运行
然后打开 Eclipse 或者 ddms,执行 jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700
程序就会断在第一个断点处, F9 几次就段在 blx R4 处
F7 跟进就来到 init 段代码处:
1.IDA用32位,
2 ./android_server 要su
3 重启平板
4. <application android:allowBackup="true" android:debuggable="false" android:icon="@drawable/app_icon"
Dump
dvmLoadNativeCode 函数是加载和初始化 so 的函数, dvmDexFileOpenPartial 函数是对缓存 Dex 文件,该函数第一个参数就是解密后 dex 文件头内存地址,而第二个参数是该 dex 大小。
跳到 dvmDexFileOpenPartial 函数或 inflate 函数去下断,
int dvmDexFileOpenPartial(const void* addr, int len, DvmDex** ppDvmDex);
第一个参数就是 dex 内存起始地址,第二个参数就是 dex 大小。所以在这个函数下断点可以直接 dump 出明文 dex
static main(void)
{
auto fp, dex_addr, end_addr;
fp = fopen("D:\\dump.dex", "wb");
end_addr = r0 + r1;
for ( dex_addr = r0; dex_addr < end_addr; dex_addr ++ )
fputc(Byte(dex_addr), fp);
}
遇到反调试
先查 Pid,ps -aux,然后 cat /proc/xxxxx/status 就可以看 tracerPid, 如果不为零及被调试过,当程序打开进程成功后使用 fgets 获得信息,当获得如下信息进我们将其修改为 0,原因:底层会调用 libc 库中的 fopen 函数打开 so 文件句柄,然后通过 fgets 函数读取进程状态值,就可以通过修改读取到的状态值绕过调试进程检测。
修改成 0( Thumb 00 20 , 70 47 即 Mov R0,#0)
1.fopen—/proc/self/cmdline.debug.atrace.app_cmdlines
2.fgets—-包名
3.LoadNativeCode–加载 libexec.so
4.LoadNativeCode–加载 libexecmain.so
5.建立反调试线程(通过检查是否存在调试进程)
6.调用 fopen 一一打开/proc/pid/status
7.调用 fgets —读取调试进程 pid
在 fgets 内部会调用 memchr 函数,和 memcpy 函数, memchr 函数完成以换行为分隔符, memcpy 将此次读取位置拷贝到目的缓冲区
脱壳:https://www.52pojie.cn/forum-5-1.html
nop: 0xc046
INIT_ARRAY,JNI_OnLoad
壳入口 ---> INIT_ARRAY ---> 解密第二层壳(JNI_OnLoad) ---> 解密原始so文件 ---> 解压缩原始so的代码节。
http://www.52pojie.cn/thread-356096-1-1.html
__gnu_armfini_26 此函数是 ELF 的入口函数,此函数就完成了 jni_onload 和 verify 等的解密。