chapter8 Android原生程序开发与逆向分析

444 阅读6分钟

原生程序开发

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

  1. 通过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
  1. 通过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
  1. 通过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
  1. 通过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 TableSection 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(节区)数据
包含了描述文件节区信息,比如大小、偏移等,但是这些对于文件的执行流程来说并不重要

image.png

根据上图可以看到,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

参考资料

www.cnblogs.com/yanqi0124/p…

blog.csdn.net/DaoDivDiFan…