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相乘。我们还记得还有两个参数是用堆栈传递的,那么怎么取得用堆栈传递的参数呢?我们先来看一张图:

      

Android 怎么实现printf android_log_print_R3



     目前堆栈如图所示,取参数使用的指令是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区域内。寻找到字符串的偏移我们在上面也讲到了。