原生程序开发
NDK版本
不要下载太新的NDK版本,可能遇到各种库缺少的问题,比如笔者最初使用的21版本的遇到一些问题:
aarch64-linux-android/bin/ld: cannot find crtbegin_dynamic.o: No such file or directory
aarch64-linux-android/bin/ld: cannot find crtend_android.o: No such file or directory
需要将依赖的库拷贝的本地,比如上面这两个:
cp platforms/android-21/arch-arm64/usr/lib/crtbegin_dynamic.o ./
cp platforms/android-21/arch-arm64/usr/lib/crtend_android.o ./
也有可能Mac环境导致,根据作者写书的时间,将ndk版本改为了r10e,很多疑难杂症都解决了,以下是各个NDK版本下载链接:github.com/android/ndk…
常见的编译方式
- 基于ndk-build进行编译
需要提供两个文件,一个是:Android.mk,另一个是:Application.mk
- 基于cmake的方式进行编译
如果使用android studio创建的C++开发项目,采用的就是这种方式编译
-
基于gcc的方式进行编译
-
基于clang/clang++的方式进行编译
cmake的编译方式,直接在android studio就可以完成了,所以着重了解ndk-build和clang/clang++的编译方式
提供环境变量.env脚本,在当前窗口下直接执行:source .env就可以了
export NDK_HOME=/opt/android-ndk-r10e
export NDK=$NDK_HOME/ndk-build
export SYSROOT=$NDK_HOME/platforms/android-21/arch-arm64
export READELF=$NDK_HOME/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-readelf
export OBJDUMP=$NDK_HOME/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-objdump
export LCC=$NDK_HOME/toolchains/llvm-3.6/prebuilt/darwin-x86_64/bin/clang
export CXX=$NDK_HOME/toolchains/llvm-3.6/prebuilt/darwin-x86_64/bin/clang++
export LGCC=$NDK_HOME/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-gcc
export STRIP=$NDK_HOME/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-strip
c程序的编译
app.c
#include <stdio.h>
int add(int a, int b, int c, int d) { return a + b + c + d; }
int main(int argc, char *argv[])
{
printf("add: %d\n", add(1, 2, 3, 4));
return 0;
}
- ndk-build方式
╰─➤ $NDK NDK_PROJECT_PATH=. NDK_APPLICATION_MK=Application.mk
[arm64-v8a] Compile : app <= app.c
[arm64-v8a] Executable : app
[arm64-v8a] Install : app => libs/arm64-v8a/app
╰─➤ adb push app /data/local/tmp
app: 1 file pushed, 0 skipped. 10.5 MB/s (49456 bytes in 0.004s)
手机端:
angler:/data/local/tmp # chmod 777 app
angler:/data/local/tmp # ./app
add: 10
- clang方式
╰─➤ $LCC --sysroot=$SYSROOT -target aarch64-linux-androideabi -gcc-toolchain $NDK_HOME/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64 app.c -o app_clang
╰─➤ adb push app_clang /data/local/tmp
手机端执行:
angler:/data/local/tmp # chmod 777 app_clang angler:/data/local/tmp # ./app_clang
add: 10
编译参数
- PIC
用于生成位置无关的共享库,代码段是只读的
- PIE
用于生成位置无关的可执行程序,可执行程序的代码指令集可以加载到任意位置
- march和mtune
主要用来优化编译出来的程序,通过march指定运行的处理器
原生程序入口函数
入口函数指的是原生程序在加载到内存中后执行的第一个函数,在入口函数处可以做很多事情,比如软件壳初始化与反调试、压缩壳解密等
- 原生程序入口函数
编译指令
╰─➤ $LCC --sysroot=$SYSROOT -target aarch64-linux-androideabi -gcc-toolchain $NDK_HOME/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64 app.c -o app -fPIE -fpic -march=armv8-a -mtune=cortex-a53 -O3
╰─➤ file app
app: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /system/bin/linker64, not stripped
- 通过READELF来查看入口函数
╰─➤ $READELF -h app
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Shared object file)
Machine: AArch64
Version: 0x1
Entry point address: 0x570
Start of program headers: 64 (bytes into file)
Start of section headers: 4144 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 7
Size of section headers: 64 (bytes)
Number of section headers: 23
Section header string table index: 20
Entry point address即为入口点地址:0x570
- 通过OBJDUMP查看程序段信息
╰─➤ $OBJDUMP -t app | head -n 15
app: file format elf64-littleaarch64
SYMBOL TABLE:
00000000000001c8 l d .interp 0000000000000000 .interp
00000000000001e0 l d .hash 0000000000000000 .hash
0000000000000238 l d .dynsym 0000000000000000 .dynsym
00000000000003d0 l d .dynstr 0000000000000000 .dynstr
0000000000000478 l d .rela.dyn 0000000000000000 .rela.dyn
00000000000004d8 l d .rela.plt 0000000000000000 .rela.plt
0000000000000520 l d .plt 0000000000000000 .plt
0000000000000570 l d .text 0000000000000000 .text
000000000000061c l d .rodata 0000000000000000 .rodata
0000000000000628 l d .note.android.ident 0000000000000000 .note.android.ident
0000000000010640 l d .preinit_array 0000000000000000 .preinit_array
发现0x570对应于.text的首地址,查看.text的反汇编代码
╰─➤ $OBJDUMP --section=.text -d app | head -n 10
app: file format elf64-littleaarch64
Disassembly of section .text:
0000000000000570 <_start>:
570: 8b3f63e0 add x0, sp, xzr
574: 14000007 b 590 <do_arm64_start>
只执行0x590处 do_arm64_start()函数,查看该函数对应的内容
╰─➤ $OBJDUMP --section=.text -d app | grep "<do_arm64_start>:" -A 17
0000000000000590 <do_arm64_start>:
590: 90000082 adrp x2, 10000 <abitag+0xf9d8>
594: 90000086 adrp x6, 10000 <abitag+0xf9d8>
598: 90000085 adrp x5, 10000 <abitag+0xf9d8>
59c: 90000084 adrp x4, 10000 <abitag+0xf9d8>
5a0: a9bd7bfd stp x29, x30, [sp,#-48]!
5a4: d2800001 mov x1, #0x0 // #0
5a8: 910003fd mov x29, sp
5ac: f94420c6 ldr x6, [x6,#2112]
5b0: 910063a3 add x3, x29, #0x18
5b4: f94418a5 ldr x5, [x5,#2096]
5b8: f9441484 ldr x4, [x4,#2088]
5bc: f9441c42 ldr x2, [x2,#2104]
5c0: f9000fa6 str x6, [x29,#24]
5c4: f90013a5 str x5, [x29,#32]
5c8: f90017a4 str x4, [x29,#40]
5cc: 97ffffe1 bl 550 <__libc_init@plt>
在最后一行中执行了bl指令,跳转到目的地址0x550,方法名为:__libc_init@plt ,表示位于.plt段,是一个外部函数,__libc_init() 函数是编译器插入的一个运行时的初始化函数,这条指令执行完,程序的运行就结束了,所以,main函数应该在执行该指令之前执行。
对于arm程序来说,它不仅链接了自身的目标.o文件,同时还链接了其他文件,比如:crtbegin_dynamic.o、crtend_android.o等
do_arm64_start()代码位于crtbegin.c里面,可以通过链接进行查看:androidxref.com/8.0.0_r4/xr…
__LIBC_HIDDEN__ void do_arm64_start(void* raw_args) {
structors_array_t array;
array.preinit_array = &__PREINIT_ARRAY__;
array.init_array = &__INIT_ARRAY__;
array.fini_array = &__FINI_ARRAY__;
__libc_init(raw_args, NULL, &main, &array);
}
/*
* Put the value of sp in x0 and call do_arm64_init(). The latter will then
* then be able to access the stack as prepared by the kernel's execve system
* call (via the first argument).
*/
__asm__ (
" .text \n"
" .align 2 \n"
" .global _start \n"
" .hidden _start \n"
" .type _start, %function \n"
"_start: \n"
" add x0, sp, xzr \n"
" b do_arm64_start \n"
" .size _start, .-_start \n"
);
在 __libc_init() 函数中,main函数是第三个参数,第四个参数为一个array数组,按照linker中的加载顺序,preinit_array是最先被执行的,然后是init_array,再然后是main函数,程序执行结束,如果有注册的fini_array,则会再次调用其中的函数指针
- so入口函数
app3.c源码
#include <stdio.h>
__attribute__((constructor)) static void constructor()
{
printf("%s\n", "constructor called");
}
__attribute__((destructor)) static void destructor()
{
printf("%s\n", "destructor called");
}
编译指令
╰─➤ $LCC --sysroot=$SYSROOT -target aarch64-linux-androideabi -gcc-toolchain $NDK_HOME/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64 app3.c -o app3.so -fPIC -fpic -march=armv8-a -mtune=cortex-a53 -O3 -shared
╰─➤ file app3.so
app3.so: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, not stripped
- 通过READELF来查看入口函数
╰─➤ $READELF -h app3.so
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Shared object file)
Machine: AArch64
Version: 0x1
Entry point address: 0x3f0
Start of program headers: 64 (bytes into file)
Start of section headers: 3280 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 4
Size of section headers: 64 (bytes)
Number of section headers: 20
Section header string table index: 17
- 通过OBJDUMP查看程序段信息
╰─➤ $OBJDUMP --section=.text -d app3.so | head -n 10
app3.so: file format elf64-littleaarch64
Disassembly of section .text:
00000000000003f0 <__on_dlclose>:
3f0: 90000080 adrp x0, 10000 <destructor+0xfbc8>
3f4: 91194000 add x0, x0, #0x650
3f8: 17fffff6 b 3d0 <__cxa_finalize@plt>
通过查看入口点和与对应的函数名,发现这是一个被导出的 __on_dlclose 函数,该函数只调用了 __cxa_finalize ,且只会在卸载动态库时执行一些相关的清理工作。
在so动态库中,只有init_array和fini_array,可以通过查看init_array中是否有函数指针来确定它是否包含初始化代码。如果通过dlopen加载动态库,那么到这里工作就完成了,如果通过System.loadLibrary加载动态库,链接器在加载动态库的时候,会查找是否有JNI初始化函数JNI_Onload,如果有就会进行调用。
所以对于so动态库来说,会先执行init_array指定的初始化的函数指针数组,然后才会执行JNI_Onload函数
原生程序的文件类型
ELF格式文件将将原生程序分为三类
- Relocatable File:可重定位文件,即.o文件,编译参数:-c
╰─➤ $LCC --sysroot=$SYSROOT -target aarch64-linux-androideabi -gcc-toolchain $NDK_HOME/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64 app.c -o app.o -O3 -c
╰─➤ file app.o
app.o: ELF 64-bit LSB relocatable, ARM aarch64, version 1 (GNU/Linux), not stripped
- Executeable File:可执行文件,clang在进行编译的时候,编译出来的的都是共享文件,改用GCC进行编译
╰─➤ $LGCC --sysroot=$SYSROOT app.c -o app -O3
╰─➤ file app
app: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /system/bin/linker64, not stripped
- Shared Object File:共享目标文件,clang默认编译出来就是该类型
╰─➤ $LCC --sysroot=$SYSROOT -target aarch64-linux-androideabi -gcc-toolchain $NDK_HOME/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64 app.c -o app -O3
╰─➤ file app
app: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /system/bin/linker64, not stripped
关于app是大端序还是小端序,通过file app来查看,如果是LSB,则为小端序,如果为MSB,则为大端序,android默认使用的小端序
AArch64 ELF文件格式
ELF分为以下三部分
- ELF Header:文件头
通过文件头信息,可以知道ELF文件是32位还是64位,是大端序还是小端序,包含多少个Program Header Table与Section Header Table
查看文件头信息:
╰─➤ $READELF -h app
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Shared object file)
Machine: AArch64
Version: 0x1
Entry point address: 0x570
Start of program headers: 64 (bytes into file)
Start of section headers: 4144 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 7
Size of section headers: 64 (bytes)
Number of section headers: 23
Section header string table index: 20
- Program Header Table:程序头表,包含多个Program Header和Segment(段)数据
主要用于告诉系统如何创建进程
- Section Header Table:节区头表,包含多个Section Header和Section(节区)数据
包含了描述文件节区信息,比如大小、偏移等,但是这些对于文件的执行流程来说并不重要
根据上图可以看到,Section包含了链接时需要的信息,而Segment包含了运行时需要的信息。ELF在链接时,链接器通过Section Header Table寻找节区信息,在运行时,加载器通过Program Header Table寻找Segment并加载。
在Android低版本中,执行不依赖Section Header Table的信息,所以很多软件壳通过修改Section Header Table相关的文件结构信息,达到了反逆向分析的效果;但是在Android7.0以后,动态链接器在加载ELF时加强了对文件格式的验证,包括校验Section Header Table信息,所以这种软件壳的保护此时是无效的。
感觉这篇文章写的不错:bbs.kanxue.com/thread-2556…
ELF Hook
常见的hook方式
- got/plt hook
- 导出表hook
- inline hook
- 异常hook
- 系统调用hook