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区域内。寻找到字符串的偏移我们在上面也讲到了。

0 0
原创粉丝点击