《ANDROID 逆向專題》在android中運行 elf程序

我們已經知道,android的底層是linux。本質上,其實android就是一個經過特殊裁剪處理的linux系統,裁剪是爲了讓我們有限的移動端硬件能夠更好地運行這個linux。

相對linux,很多人更加熟悉window。這裏我們來做個對比。

                          windows              linux

可執行文件         exe                      elf

靜態鏈接庫         .lib                       .a

動態鏈接庫          .dll                     .so

這裏說的不太準確,比如windows 下可執行程序不僅僅只有.exe,還有.com、.sys等(這裏重點不是這個,槓精請忽略)。雖說在linux中,不像windows那樣通過文件後綴來判斷文件名,但是依舊是可以使用文件後綴的,而且使用後綴也能幫助我們快速分辨文件類型。

ps:這裏插入一個小拓展。原生程序的類型,其實可以大致分爲三類。

1、可重定位文件(relocatable file)。通常是.o結尾的目標文件

2、原生可執行文件(executable file)

3、共享的目標文件(shared object file)

 

這裏我們注意到,在apk中,經常會出現有so文件,它之所以會執行,也就是因爲linux的內核中,so是作爲動態鏈接庫或者可執行程序的。也就是說,so還是elf 可執行文件,但是這種情況下,so的後綴一般是不顯示的。

這裏我們腦洞打開

android 的原生開發,我的理解本質上就是在android 中跑 C/C++ 代碼。

因爲android 是linux 系統。所以我想的是,換句話說,就是在linux 下跑C/C++ 代碼。

如此,我們是否可以把一個linux文件放到我們的android 系統上去運行呢??

瞎搞開始了,我打開我的ubuntu,然後用gedit寫了一個hello world

然後,找到壓箱底很久的 《C程序設計》,終於按照上面的方法編譯得到了可執行文件。而且成功用運行了。

對,就是成功打印出來了 "hello world"

我們使用hex 文件查看器來看看我們生成的hello 文件。可以清楚發現,文件頭是有.ELF標記的,也就是hello 確實就是linux下的可執行文件

驗證我們想法的時候到了,我們將這個hello 文件上傳到android 手機上,讓android 來運行它。

然後好像運行不來額,報了一個錯誤說這個是64-bit 的ELF文件。

其實這裏就很接近真相了,我們的elf 是二進制文件直接用來運行的。但是與運行直接相關的是cpu處理器。處理器架構的不同,對應的執行程序肯定是不通用的。

目前市面上的CPU分類主要有兩大陣營。

1、intel、AMD爲首的複雜指令集CPU。架構是X86

2、IBM、ARM爲首的精簡指令集CPU。IBM公司的CPU是PowerPC架構,ARM公司是ARM架構。

這裏我的pc 是x86架構,android 手機是arm,所以肯定是不能運行的。

那麼有沒方法可以跨平臺編譯?

肯定是有的,在windows 下進行android開發不就是這麼回事麼?

我們知道,android開發需要下載兩個組件。一個叫sdk,一個叫ndk。其中ndk就是做native 層開發的,現在就派上用場了。

我們接下來就是要用ndk來編譯我們的hello.c 代碼,然後來生成適用於android 的可執行程序elf文件。

1、先創建一個叫jni 的文件夾(名稱一定是jni 不能改),然後將我們的.c 文件放進去,同時創建Android.mk、Application.mk。如下所示:

2、在Application.mk 中輸入如下:

這裏也就是說編譯生成所有平臺的可執行文件,當然也可以不生成所有的平臺。

比如你這樣:

APP_ABI:= x86 armeabi-v7a

那最後就只生成 x86  和 armeabi-v7a 下 使用的文件

3、在Android.mk 輸入如下:

這裏設置的是編譯 hello.c 這個文件 並最後輸出 libhello-jni 文件

這裏輸出的是可執行程序 BUILD_EXECUTABLE ,所以最終我們生成的elf 是不帶so後綴的,可以直接運行。

另外還有

BUILD_SHARED_LIBRARY   編譯動態鏈接庫

BUILD_STATIC_LIBRARY   編譯靜態鏈接庫

但是在一般的android開發中,我們生成的elf文件並不是傳統原生可執行文件,而是可執行文件的一種拓展,叫共享目標庫文件。因此這裏的編譯類型需要稍微修改一下。

nclude $(BUILD_SHARED_LIBRARY)

這個要區別開。

LOCAL_CFLAGS += -pie -fPIE

LOCAL_LDFLAGS += -pie -fPIE

是因爲,在Android 4.4之後添加了新的保護機制,可執行文件必須是採用PIE編譯的。

 

這三個文件準備好之後,我們就可以開始編譯了。使用ndk-build。這裏我將ndk 加到了環境變量中,所以可以直接調用,不然需要進到ndk目錄下找到ndk-build.cmd 所在的目錄下。

命令執行之後,會看到生成了兩個目錄 obj 和 libs

我們打開libs ,已經得到了各個平臺的編譯結果。這些平臺就是由Application.mk 配置的

這裏面的 libhello-jni 就是我們的生成可執行的elf 文件。是沒有so後綴的額。

我們找到自己android設備支持的cpu架構,從裏面找到對應的 libhello-jni ,最後將這個 elf 放到 android 中去執行,如下:

順利執行,打印出 hello world。

上面手動讓android運行了elf程序,其實在apk中,調用so原理上也是差不多的。

 

介紹了使用Android.mk 來生成我們的elf 文件之後這裏再介紹另一種比較傳統的做法,那就是使用makefile來生成我們的elf。相比android.mk,makefile 使用比較麻煩。需要制定生成的目標文件,依賴的文件等等。但是這樣的好處就是我們能更加直觀體會到elf的生成過程,同時也可以生成出c源碼對應的彙編代碼.s文件。可以方便我們進行對比理解和學習arm彙編指令。

其實,android.mk 就是android編譯環境下的一種特殊的“makefile”文件, 它是經過了android編譯系統處理的。所謂android編譯系統,就是android頂層目錄下的build目錄裏面的一系列編譯控制文件,其實就是一系列makefile文件和 *.mk 文件,這些文件纔是編譯android系統完整的makefile文件.每個模塊裏的android.mk只不過是被包含進android編譯系統的一小部分而已。經過android編譯系統的一大堆處理,android.mk的格式就變得非常簡單,且與普通的makefile文件書寫格式不一樣了。

所以我們可以理解爲使用makefile其實就是繞過了android.mk 文件在android編譯系統中的層層解析,直接就可以調用gcc進行編譯了。

接下來簡單介紹下這個過程。

需要準備兩個文件:

一個是c源文件,然後寫上最簡單的C代碼,比如:

接下來就是編寫makefile 文件:

#ndk根目錄
NDK_ROOT=E:/android/android-ndk-r14b-windows-x86_64/android-ndk-r14b
#編譯器根目錄
TOOLCHAINS_ROOT=$(NDK_ROOT)/toolchains/arm-linux-androideabi-4.9/prebuilt/windows-x86_64
#編譯器目錄
TOOLCHAINS_PREFIX=$(TOOLCHAINS_ROOT)/bin/arm-linux-androideabi
#頭文件搜索路徑
TOOLCHAINS_INCLUDE=$(TOOLCHAINS_ROOT)/lib/gcc/arm-linux-androideabi/4.9.x/include-fixed
#SDK根目錄
PLATFROM_ROOT=$(NDK_ROOT)/platforms/android-21/arch-arm
#sdk頭文件搜索路徑
PLATFROM_INCLUDE=$(PLATFROM_ROOT)/usr/include
#sdk庫文件搜索路徑
PLATFROM_LIB=$(PLATFROM_ROOT)/usr/lib

#文件名稱
MODALE_NAME=demo
MODALE_TYPE=c
PATH_ANDROID=/data/local/tmp

#刪除
RM=rm

#編譯選項
FLAGS=-I$(TOOLCHAINS_INCLUDE) \
      -I$(PLATFROM_INCLUDE)   \
      -L$(PLATFROM_LIB) \
      -nostdlib \
      -lgcc \
      -Bdynamic \
      -lc \
	  -pie -fPIE
	  
	  
#所有obj文件
OBJS=$(MODALE_NAME).o \
     $(PLATFROM_LIB)/crtbegin_dynamic.o \
     $(PLATFROM_LIB)/crtend_android.o 
#編譯器鏈接
all:
	$(TOOLCHAINS_PREFIX)-gcc $(FLAGS) -c $(MODALE_NAME).$(MODALE_TYPE) -o $(MODALE_NAME).o
	$(TOOLCHAINS_PREFIX)-gcc $(FLAGS) -s $(MODALE_NAME).$(MODALE_TYPE) -o $(MODALE_NAME).s
	$(TOOLCHAINS_PREFIX)-gcc $(FLAGS) $(OBJS) -o $(MODALE_NAME)
old:
	$(TOOLCHAINS_PREFIX)-gcc $(FLAGS) -E $(MODALE_NAME).$(MODALE_TYPE) -o $(MODALE_NAME).i
	$(TOOLCHAINS_PREFIX)-gcc $(FLAGS) -S $(MODALE_NAME).i -marm -o $(MODALE_NAME).s
	$(TOOLCHAINS_PREFIX)-gcc $(FLAGS) -c $(MODALE_NAME).s -o $(MODALE_NAME).o
	$(TOOLCHAINS_PREFIX)-gcc $(FLAGS) $(OBJS) -o $(MODALE_NAME)
#刪除所有.o文件
clean:
	$(RM) *.o
	$(RM) *.i
	$(RM) $(MODALE_NAME)
	
#安裝程序到手機
install:
	adb push $(MODALE_NAME) /data/local/tmp
	adb shell chmod 755 /data/local/tmp/$(MODALE_NAME)
	adb shell /data/local/tmp/$(MODALE_NAME) 
#運行程序
run:
	adb shell /data/local/tmp/$(MODALE_NAME)

這裏要注意的是我上面設置的 ndk 路徑和版本問題。在實際使用中要改成自己的路徑,對應的版本也要改。

還有一個就是 makefile 文件中的MODALE_NAME ,MODALE_TYPE。這裏我的源程序就是demo.c ,實際使用的時候要改成自己的。

然後還有一個就是找到ndk 中的make.exe .這個就是我們待會要使用的的make 工具。我的在

E:\android\android-ndk-r14b-windows-x86_64\android-ndk-r14b\prebuilt\windows-x86_64\bin

這個目錄下,有興趣的可以對應找找自己的,並將這個路徑添加到 path 環境變量中,方便我們使用。

最後我們進入到我們的demo.c  和 makefile 文件目錄下,使用make 工具進行編譯 ,如:

過程很順利,然後查看目錄。發現已經生成了很多文件:

demo 就是我們的elf 文件,可以直接放到手機上去運行的。比如,我們使用剛剛寫的makefile 中的install 命令

將demo文件上傳到手機 ,並給與權限和運行。打印出結果。

再打開我們的demo.s 文件,這裏有我們關注的 c 的arm彙編代碼。接下來我們可以通過比較來學習c 語句和對應的arm 彙編的翻譯過程。最後給出上面demo.c 對應的arm 彙編代碼。有興趣的話可以看看。

	.arch armv5te
	.fpu softvfp
	.eabi_attribute 20, 1
	.eabi_attribute 21, 1
	.eabi_attribute 23, 3
	.eabi_attribute 24, 1
	.eabi_attribute 25, 1
	.eabi_attribute 26, 2
	.eabi_attribute 30, 6
	.eabi_attribute 34, 0
	.eabi_attribute 18, 4
	.file	"demo.c"
	.text
	.align	2
	.global	hunter_add
	.type	hunter_add, %function
hunter_add:
	@ args = 0, pretend = 0, frame = 16
	@ frame_needed = 1, uses_anonymous_args = 0
	@ link register save eliminated.
	str	fp, [sp, #-4]!
	add	fp, sp, #0
	sub	sp, sp, #20
	str	r0, [fp, #-8]
	str	r1, [fp, #-12]
	str	r2, [fp, #-16]
	str	r3, [fp, #-20]
	ldr	r2, [fp, #-8]
	ldr	r3, [fp, #-12]
	add	r2, r2, r3
	ldr	r3, [fp, #-16]
	add	r2, r2, r3
	ldr	r3, [fp, #-20]
	add	r3, r2, r3
	mov	r0, r3
	sub	sp, fp, #0
	@ sp needed
	ldr	fp, [sp], #4
	bx	lr
	.size	hunter_add, .-hunter_add
	.section	.rodata
	.align	2
.LC0:
	.ascii	"add: %d\012\000"
	.text
	.align	2
	.global	main
	.type	main, %function
main:
	@ args = 0, pretend = 0, frame = 8
	@ frame_needed = 1, uses_anonymous_args = 0
	stmfd	sp!, {fp, lr}
	add	fp, sp, #4
	sub	sp, sp, #8
	str	r0, [fp, #-8]
	str	r1, [fp, #-12]
	mov	r0, #1
	mov	r1, #2
	mov	r2, #3
	mov	r3, #5
	bl	hunter_add(PLT)
	mov	r2, r0
	ldr	r3, .L5
.LPIC0:
	add	r3, pc, r3
	mov	r0, r3
	mov	r1, r2
	bl	printf(PLT)
	mov	r3, #0
	mov	r0, r3
	sub	sp, fp, #4
	@ sp needed
	ldmfd	sp!, {fp, pc}
.L6:
	.align	2
.L5:
	.word	.LC0-(.LPIC0+8)
	.size	main, .-main
	.ident	"GCC: (GNU) 4.9.x 20150123 (prerelease)"
	.section	.note.GNU-stack,"",%progbits

對於arm 反彙編的學習,首先是要有C/C++的功底。看懂是最基本的要求,最好是能寫C/C++代碼.然後,我們可以嘗試自己編寫一些小型的C/C++,再對應編譯成elf和.s 文件。通過IDA 的調試來學習這個arm指令與C/C++ 的轉化。我的話是先編譯一份elf,然後通過ida查看arm指令,在還原爲C代碼,最後跟原來寫的代碼比較或者運行一遍逆向結果來判斷是否成功逆向。如此反覆,同時也能促進arm 彙編指令的學習。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章