0x00
如果不熟悉ARM汇编的同学,请先阅读这两篇文章,常用ARM汇编指令,ARM子函数定义中的参数放入寄存器的规则。
0x01
这一节我们通过逆向Android SO文件,来理解C++基本数据类型,如int、float、bool、char、指针、引用、常量的ARM汇编形式。
还有理解C++函数调用,用ARM汇编是怎么实现的?参数如何传递,返回值怎么传?函数执行完毕后怎么返回执行?
0x02
我们知道Android SO是使用ndk来开发的。大概的步骤如下:
1、首先根据类文件在jni文件夹下生成.h文件,在源码根目录执行指令为javah -classpath bin/classes -d jni com.example.ndkreverse.Lesson。
com.example.ndkreverse.Lesson这个类的源码。
package com.example.ndkreverse;
public class Lesson {
static {
System.loadLibrary("lesson");
}
public native void main();
}
有一个native方法,那么生成的com_example_ndkreverse_Lesson.h就声明了这个JNI方法,如下:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_ndkreverse_Lesson1 */
#ifndef _Included_com_example_ndkreverse_Lesson1
#define _Included_com_example_ndkreverse_Lesson1
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_example_ndkreverse_Lesson1
* Method: main
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_example_ndkreverse_Lesson_main
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
com_example_ndkreverse_Lesson.h,并实现里面声明的函数。
#include "com_example_ndkreverse_Lesson.h"
#include <android/log.h>
#define LOG_TAG "lesson"
#define ALOGD(...) ((void)__android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__))
//define定义的常量
#define NUMBER_ONE 7
int func2(char* c, int d) {
int buf[100];
char *p;
char &r = c[2];
p = c;
buf[99] = d;
return (*(p + 1)) + r * buf[99];
}
float func1(int a, int c, int i, int j, int k, int q) {
int buf;
float f;
bool b;
const int nVar = NUMBER_ONE;
buf = func2("hello", 10);
b = true;
f = 1.2;
return buf * a * c * i * j * k * q * f * b * nVar;
}
JNIEXPORT void JNICALL Java_com_example_ndkreverse_Lesson_main
(JNIEnv *env, jobject jobject) {
float f;
f = func1(1,2,3,4,5,6);
ALOGD("f=%f\n", f);
}
3、最后写Android.mk,用于生成SO文件。
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := lesson
LOCAL_SRC_FILES := Lesson.cpp
LOCAL_LDLIBS := -llog -ldl
LOCAL_CPPFLAGS += -Wno-write-strings
include $(BUILD_SHARED_LIBRARY)
然后使用ndk-build,生成了liblesson.so文件。
0x03
下面用ida来静态和动态分析liblesson.so,动态分析SO请参考ida动态调试so,在init_array和JNI_ONLOAD处下断点。主要是去理解上面0x01我们提出的两个问题。
我们先来Java_com_example_ndkreverse_Lesson_main这个函数的汇编代码。
.text:00001280 Java_com_example_ndkreverse_Lesson_main
.text:00001280
.text:00001280 var_10 = -0x10
.text:00001280 var_C = -0xC
.text:00001280
.text:00001280 MOVS R3, #5
.text:00001282 PUSH {R0-R2,LR}
.text:00001284 STR R3, [SP,#0x10+var_10] ; int
.text:00001286 MOVS R3, #6
.text:00001288 MOVS R2, #3 ; int
.text:0000128A STR R3, [SP,#0x10+var_C] ; int
.text:0000128C MOVS R1, #2 ; int
.text:0000128E MOVS R3, #4 ; int
.text:00001290 MOVS R0, #1 ; int
.text:00001292 BL _Z5func1iiiiii ; func1(int,int,int,int,int,int)
.text:00001296 BL j_j___extendsfdf2
.text:0000129A LDR R2, =(aFF - 0x12AA)
.text:0000129C STR R0, [SP,#0x10+var_10]
.text:0000129E STR R1, [SP,#0x10+var_C]
.text:000012A0 LDR R1, =(aLesson - 0x12A8)
.text:000012A2 MOVS R0, #3
.text:000012A4 ADD R1, PC ; "lesson"
.text:000012A6 ADD R2, PC ; "f=%f\n"
.text:000012A8 BL j_j___android_log_print
.text:000012AC POP {R0-R2,PC}
.text:000012AC ; End of function Java_com_example_ndkreverse_Lesson_main
在这个函数中我们向fun1传递了6个参数,在
ARM子函数定义中的参数放入寄存器的规则,我们知道函数的形参不超过4个,如果形参个数少于或等于4,则形参由R0,R1,R2,R3四个寄存器进行传递;若形参个数大于4,大于4的部分必须通过堆栈进行传递。我们可以看到前四个形参是用R0,R1,R2,R3来传递的,后两个参数是用堆栈来传递的,然后调用_Z5func1iiiiii,也就是func1函数,我们先看fun1函数的汇编实现。
.text:00001238 ; _DWORD __fastcall func1(int, int, int, int, int, int)
.text:00001238 EXPORT _Z5func1iiiiii
.text:00001238 _Z5func1iiiiii ; CODE XREF: Java_com_example_ndkreverse_Lesson_main+12p
.text:00001238
.text:00001238 arg_0 = 0
.text:00001238 arg_4 = 4
.text:00001238
.text:00001238 PUSH {R3-R7,LR}
.text:0000123A MOVS R7, R0
.text:0000123C LDR R0, =(aHello - 0x1244)
.text:0000123E MOVS R6, R1
.text:00001240 ADD R0, PC ; "hello"
.text:00001242 MOVS R1, #0xA ; int
.text:00001244 MOVS R5, R2
.text:00001246 MOVS R4, R3
.text:00001248 BL _Z5func2Pci ; func2(char *,int)
.text:0000124C MULS R0, R7
.text:0000124E MULS R0, R6
.text:00001250 MULS R0, R5
.text:00001252 MULS R0, R4
.text:00001254 LDR R3, [SP,#0x18+arg_0]
.text:00001256 MULS R3, R0
.text:00001258 MOVS R0, R3
.text:0000125A LDR R3, [SP,#0x18+arg_4]
.text:0000125C MULS R3, R0
.text:0000125E MOVS R0, R3
.text:00001260 BL j_j___floatsisf
.text:00001264 LDR R1, =0x3F99999A
.text:00001266 BL j_j___mulsf3
.text:0000126A LDR R1, =0x40E00000
.text:0000126C BL j_j___mulsf3
.text:00001270 POP {R3-R7,PC}
.text:00001270 ; End of function func1(int,int,int,int,int,int)
在fun1中首先是入栈操作,在THUMB指令中通常使用R4~R7来保存局部变量,所以子程序进入时必须保存这些寄存器的值,在返回前必须恢复这些寄存器的值。
然后分别把R0~R3四个参数分别赋值给R4~R7四个局部变量,接着要调用函数fun2,传递的参数分别用R0,R1传递,R1被直接赋值10,我们重点看下R0的赋值。主要是这两行代码:
.text:0000123C LDR R0, =(aHello - 0x1244)
.text:00001240 ADD R0, PC ; "hello"
=(aHello - 0x1244)是被IDA优化过后的伪指令,也就是
aHello - 0x1244被赋值给R0,我使用objdump来查看liblesson.so,这行代码其实是
ldr
r0,
[pc, #52],也就是取pc+52地址的内容赋值给R0。我们看下这个地址里面存的内容究竟是什么呢?
.text:00001274 off_1274 DCD aHello - 0x1244
其中aHello字符串存储在程序.rodata中:
.rodata:00002D44 AREA .rodata, DATA, READONLY, ALIGN=0
.rodata:00002D44 ; ORG 0x2D44
.rodata:00002D44 aHello DCB "hello",0
那么最后赋值给R0其实是ADD R0, PC这行代码PC和"hello"字符串的间隔。
既然R0和R1参数都准备好了,接着会调用fun2,汇编代码如下:
.text:0000122C ; _DWORD __fastcall func2(char *, int)
.text:0000122C EXPORT _Z5func2Pci
.text:0000122C _Z5func2Pci ; CODE XREF: func1(int,int,int,int,int,int)+10p
.text:0000122C LDRB R3, [R0,#2]
.text:0000122E LDRB R2, [R0,#1]
.text:00001230 MULS R1, R3
.text:00001232 ADDS R0, R2, R1
.text:00001234 BX LR
.text:00001234 ; End of function func2(char *,int)
我们看到汇编代码,并不是我们想象中的先对buf,指针p,引用r赋值,然后再去取值,编译器已经优化了很多,不过可以看出指针和引用本质是一样的,都是取地址+偏移中的内容。
从fun2中的返回值被赋值给了R0,回到fun1中,R0继续参与运算,不断与R4~R7相乘。我们还记得还有两个参数是用堆栈传递的,那么怎么取得用堆栈传递的参数呢?我们先来看一张图:
目前堆栈如图所示,取参数使用的指令是LDR R3, [SP,#0x18+arg_0],LDR R3, [SP,#0x18+arg_4]。之后是浮点数乘法运算的一些函数,有些float型的二进制,比如LDR R1, =0x3F99999A,这里的0x3F99999A可以通过在线任意进制转换计算,来转换为10进制的float型。
执行完这个函数,由于float型较大,所以通过R0,R1来传递返回值。POP {R3-R7,PC}把堆栈回退到参数5的位置,由于LR存储是Java_com_example_ndkreverse_Lesson_main函数中下一条指令的位置,所以返回到Java_com_example_ndkreverse_Lesson_main继续执行。
在Java_com_example_ndkreverse_Lesson_main函数中调用了j_j___android_log_print,函数传递参数的要点我们都在前面讲过了,不过这个第四个参数就是float这个值由于比较大,不能单独放在R3中传递,所以最后放在了堆栈中传递。
POP {R0-R2,PC}清空了上图中的堆栈,回到调用Java_com_example_ndkreverse_Lesson_main它的地方继续执行。
0x04
我们在后续独自分析SO文件时,经常会看到
.text:00001280 var_10 = -0x10
.text:00001280 var_C = -0xC
.text:00001238 arg_0 = 0
.text:00001238 arg_4 = 4
这里我们可以看到var其实不是C++语言的变量,可能是参数。arg_0是从堆栈中取出的参数。
我们再看func1汇编后在IDA环境中按F5得到的C++代码。
int __fastcall func1(int a1, int a2, int a3, int a4, int a5, int a6)
{
int v6; // r7@1
int v7; // r6@1
int v8; // r5@1
int v9; // r4@1
int v10; // r0@1
int v11; // r0@1
int v12; // r0@1
v6 = a1;
v7 = a2;
v8 = a3;
v9 = a4;
v10 = func2("hello", 10);
v11 = j_j___floatsisf(a6 * a5 * v10 * v6 * v7 * v8 * v9);
v12 = j_j___mulsf3(v11, 1067030938);
return j_j___mulsf3(v12, 1088421888);
}
我们看到这里的局部变量也和C++语言时定义的局部变量不一样了,这里的局部变量是R4~R7和每次调用函数产生的中间变量。编译优化后并没有对于C++语言的局部变量赋值。
我们在C++语言中定义的const 常量,只是在ide会检查的一个限制,在汇编层次没有特殊处理。并且#define NUMBER_ONE 7 在使用时直接用上了这个值,这个值并没有存储在哪个地方。
我们通过分析fun2函数,其实引用和指针在汇编层次上实现是一致的。还有字符串存在.rodata区域内。寻找到字符串的偏移我们在上面也讲到了。