IDA F5堆栈不平衡的处理
1.引出问题
F5时出现如图错误,一般是程序代码有一些干扰代码,让IDA的反汇编分析出现错误。比如用push + n条指令 + retn来实际跳转,而IDA会以为retn是函数要结束,结果它分析后发现调用栈不平衡,因此就提示sp analysis failed.
我们选择抖音老版本的libcms.so作为分析案例,假设Java代码如下
static {
System.loadLibrary("cms");
}
public native int getUserInfo();
我们想要分析getUserInfo这个函数,函数被声明为native,这代表了它是一个native方法,真正的代码逻辑由C/C++等native语言实现,它来自名为cms的库,由静态代码块中的System.loadLibrary加载进内存。在加载和寻找动态库时,Java会判断系统并扩展库名,在Windows平台下cms会被扩展成libcms.dll,Android作为基于linux的系统,则会依照linux被扩展成libcms.so,也就是我们常说的xxx SO库,解压APK取出libcms.so库,我们使用ida打开,默认加载,注意观察IDA左下角,地址值不变化时即加载完毕。
如何找到getUserInfo函数的实现代码?按照惯例,查看Exports栏——函数导出表,意为此SO文件提供给外部调用的函数列表。在最一般的情况下,我们会在导出表中找到它。
我们可以看出,名称似乎是有规律的,即Java + 包名 + 类名 + 方法名,这就涉及到”绑定方式”的话题了。将Java 方法和真正实现其逻辑的Native函数实现对应也被称为绑定或注册,这种通过规则命名进行绑定的方式称为静态注册/绑定,我们可以在导出函数表中搜索“Java“,找到可能采取静态注册方式的函数,但我们的cms库似乎并不是采用静态注册。
Java_com_ss_sys_ces_a_DebugPrint这个函数似乎是静态注册,但很可惜的是,找不到我们要的getUserInfo,它应该叫“Java_xxx_xxx_getUserInfo”,如果按照静态注册的规则命名法。那么可以推断,它使用了动态注册。
动态注册不采用“Java + 包名 + 类名 + 方法名“的规则命名来对应,而是由程序员提供一个函数映射表,主动告知函数的对应关系。这种方式效率更高,而且C++层的函数名不用长且丑。
我们不妨打开Android Studio,动手写一个动态注册。
2.动手实现动态注册
我们需要3.2以上版本的Android Studio,打开Android Studio新建Project,拖到最下,选择Native C++,后续一路默认,这样可以生成一个支持native的Android demo。
Ctrl+鼠标左键 停留在stringFromJNI函数声明上,进入函数真正定义的地方。
函数名为Java_com_example_demo1(包名)_MainActivity(类名)_stringFromJNI(方法名),这是静态注册。我们将它修改为动态注册,只需要两步。
- 自己编写JNI_OnLoad覆盖系统默认的注册函数
在加载so库的时候,系统首先会寻找JNI_OnLoad(JavaVM *vm, void *reserved)方法,用于注册JNI函数,因此我们便可以重写该方法来覆盖android默认的JNI_OnLoad来实现动态注册。 - 调用JNIEnv->RegisterNatives()来注册JNI函数。
第一步复制粘贴即可
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = nullptr;
jint result = -1;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK)
return JNI_ERR;
//注册方法
********
result = JNI_VERSION_1_6;
return result;
}
第二步是重点
JNIEnv是一个很重要的概念,//TODO 附带资料
调用这个函数即可完成动态注册,在JNI.h中我们可以看到这个函数的定义
jint RegisterNatives(jclass clazz, const JNINativeMethod* methods,jint nMethods)
//可以看到由三个参数
//Jclass clazz:native方法所属的类,通过findclass(Java类名) 得到.
//const JNINativeMethod* methods: 方法数组,需要了解一下JNINativeMethod结构体
//jint nMethods:方法数组的长度,即我们动态注册了jint个函数。
如下是JNINativeMethod结构体的定义
typedef struct {
const char* name; // native 的方法名,比如StringFromJNI
const char* signature; // 方法签名,例如 ()Ljava/lang/String;
void* fnPtr; // 函数指针
} JNINativeMethod;
Name+signature描述了Java层的信息,别忘了RegisterNatives 函数的参数一是方法所属的Java类,再加上此处结构体所提供的方法名和方法签名,加起来最终得到了完整的Java层信息,结构体中的fnPtr则是native中的函数名,Java世界和Native世界完成对应关系。
我们先把“Java_com_example_demo1_MainActivity_stringFromJNI “函数名修改成stringFromJNI,这样的函数名清爽多了。
接下来就是编写代码了
JNINativeMethod methods[] = {
{"stringFromJNI","()Ljava/lang/String;",(void*)stringFromJNI},
};
env->RegisterNatives(env->FindClass("com/example/demo1/MainActivity"),methods,1);
这两句增加到我们JNI_OnLoad中留白的地方,就大功告成了。还有一个困惑我们需要解释一下,方法签名的表述似乎和Java语法不太一样,事实上,这和Smali的语法一致。
我们可以参考一下Android源码中对JNINativeMethod方法列表的处理
似乎写法不太一样,这是因为经过了封装,可以看出,方法数组的长度(jint nMethods)使用NELEM(methods)取得,可以猜测这类似于length(methods)。
查看源码,NELEM 是一个宏定义,返回length。
我们对代码稍作修改,向Android源码靠近。
大功告成,运行试试。
知道动态注册如何实现后,自然就知道逆向时怎么做。
首先找JNI_OnLoad,找到其中RegisterNative函数,它的倒数第二个参数是一个大数组。里面包含了诸多对应表。
三、回到libcms.so
首先找JNI_Onload
我们遇到了错误,无法F5反编译成C代码,报错提示是栈不平衡。处理的方法非常多,我们尝试朴素的几种。
3.1、栈不平衡就改回去
先打开IDA的通用设置窗口,显示栈高度
我修改了三处,增加栈高度的显示,自动注释汇编指令的意义,以及显示操作码的字节(有时候可以帮助我们判断thumb,arm指令)
接下来按G跳转地址,到栈出问题的地方
往上找,在第一次出现负数的上一行按alt+k, alt+K是栈指针调节的快捷键,我们使用它将上一行的栈高度调整成一样的负数。
栈高度被调整和扩散成了正数,我们再回到JNI_OnLoad,试一下F5
继续重复刚才的工作,G跳转,往上找遇到的第一个正直栈高度,alt+k修改
再次尝试F5
修改完再F5
反编译成功,但反编译的结果似乎很糟糕,参数个数多达几十个,代码也奇怪极了。
这是因为我们强行平衡了栈,而IDA为了完成配平堆栈的任务, 会给一些函数增删错误的参数数量, 整体代码识别混乱极了,我们可以在函数上按Y,把函数的参数个数修改成正确的个数。
除此之外,我们可以在汇编代码上,alt+p,对函数的方方面面进行调整。
我们不对JNI_OnLoad做过多处理,因为我们只是想看其中的RegisterNative函数。
———————认真看JNI_OnLoad反编译后的代码1分钟———————————
反汇编代码非常杂乱,我们想识别出函数中的JNI函数,有些困难,或许这不是一个好主意,这种方法得到的垃圾反汇编代码还不如看汇编。
3.2、根据特征找RegisterNative函数
所有的JNI函数的指针在一个大表格中,RegisterNative函数位于860(0x35C)这个位置,正常反汇编后形如(*(v1 + 860))(),然后我们会转换v1为env结构体指针,即可得到正确结果。但我们现在得不到正确的反汇编代码,所以老老实实看汇编的特征。
在IDA中搜索立即数,ALT+I热键,如图操作。
Ctrl+F 搜索0x35C(这种信息一般十六进制显示)
看到有两处使用了这个函数
先看第一处调用
阅读此处汇编代码,这里涉及到地址重定位和调用约定的问题,off_8A5C4即为我们需要的methods列表。
Ps.//TODO 补充汇编的资料
大功告成
3.3 、通过字符串搜索调用
我们在动态注册函数时,需要提供对应关系
typedef struct {
const char* name; // native 的方法名,比如StringFromJNI
const char* signature; // 方法签名,例如 ()Ljava/lang/String;
void* fnPtr; // 函数指针
} JNINativeMethod;
因此可以搜索字符串“Java“等,查看其引用。Shift+F12打开字符串窗口,CTRL+F搜索。
任意选择一个方法签名进入
字符串值为“(Ljavaxxxxx“,IDA将值赋给了某个变量,变量名为aLjavaxxx,在变量名上按x查看其引用,进入。
OK搞定
3.4、使用Hook得到函数地址
使用Hook工具 Hook 得到真实地址,Hook工具的玩儿法太多了。
// TODO
// TODO