Android SO逆向-基本数据类型及函数的工作原理
来源:互联网 发布:东方网络彭朋 编辑:程序博客网 时间:2024/05/22 14:50
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 __cplusplusextern "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
2、随后写一个Lesson.cpp文件包含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 7int 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.cppLOCAL_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,这行代码其实是ldrr0, [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区域内。寻找到字符串的偏移我们在上面也讲到了。
- Android SO逆向-基本数据类型及函数的工作原理
- Android SO逆向-对象的拷贝构造函数
- Android SO逆向-对象的继承和虚函数
- AsyncTask的基本使用及工作原理
- Windows基本的数据类型 和 Windows 工作原理
- IRP原理及派遣函数基本工作流程
- Android SO逆向-对象的构造函数与析构函数
- Android 逆向apk的.so动态库
- Javascript -- 函数及基本数据类型
- Android SO逆向-流程控制语句及表达式运算
- Android SO逆向-C++虚函数表解析
- 51单片机存储器的基本结构及工作原理
- struts1的工作原理及基本配置详解
- 计算机的基本组成及工作原理(1)
- Android ScrollView的基本使用及原理。
- C++/CLI 托管C++的基本数据类型及函数【3】
- Android库so文件及skia函数的调用
- Android库so文件及skia函数的调用
- 开发中的BUG
- iOS学习笔记29-系统服务(二)通讯录
- Druid 配置
- Centos6.5 mysql安装
- ubuntu终端的颜色设置
- Android SO逆向-基本数据类型及函数的工作原理
- semantic web
- knowledge base, knowledge graph
- LeetCode 294. Flip Game II(反转游戏II)
- bzoj 2560: 串珠子 子集dp
- reasoning
- 自定义圆头像
- mysql简单查询操作
- 全网优秀IT博客导航