Featured image of post 安卓逆向入门六:so(ELF)文件分析与ARM基础知识

安卓逆向入门六:so(ELF)文件分析与ARM基础知识

关键词:ELF文件入门、so文件分析 、ARM基础知识

0x00 ELF文件入门

ELFExecutable and Linkable Format)是一种可执行可链接的文件格式,是linux底下二进制文件,可以理解为windows下的PE文件,在Android中可以比作SO,方便函数的移植,在常用于保护Android软件,增加逆向难度。其核心价值在于支持程序的执行(如二进制程序运行、动态库加载)和链接(编译过程中的模块组合),是跨平台二进制兼容的基础。

ELF文件的主要组成部分包括:

  • ELF Header:文件头,描述文件的基本信息

  • Program Header Table:程序头表,描述进程映像的布局

  • Section Header Table:节区头表,描述文件的各个节区

  • 程序头表与分段头表引用的数据,比如 .text .data。

ELF Header(文件头)

  • ELF Header 描述了 ELF 文件的概要信息,利用这个数据结构可以索引到 ELF 文件的全部信息,是ELF 文件的 “总目录”,存储文件的基础元信息,可通过这些信息索引到其他所有结构。32 位 ELF 的文件头结构定义如下:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#define EI_NIDENT   16

typedef struct {
    unsigned char   e_ident[EI_NIDENT];
    ELF32_Half      e_type;
    ELF32_Half      e_machine;
    ELF32_Word      e_version;
    ELF32_Addr      e_entry;
    ELF32_Off       e_phoff;
    ELF32_Off       e_shoff;
    ELF32_Word      e_flags;
    ELF32_Half      e_ehsize;
    ELF32_Half      e_phentsize;
    ELF32_Half      e_phnum;
    ELF32_Half      e_shentsize;
    ELF32_Half      e_shnum;
    ELF32_Half      e_shstrndx;
} Elf32_Ehdr;

ELF 头中的 e_shoff 项给出了从文件开头到节头表位置的字节偏移e_shnum 告诉了我们节头表包含的项数e_shentsize 给出了每一项的字节大小

Program Header Table(程序头表)

Program Header Table 是一个结构体数组,每一个元素的类型是 Elf32_Phdr,描述了一个段或者其它系统在准备程序执行时所需要的信息。

其中,ELF 头中的 e_phentsizee_phnum 指定了该数组每个元素的大小以及元素个数。一个目标文件的段包含一个或者多个节。程序的头部只有对于可执行文件和共享目标文件有意义。

可以说,Program Header Table 就是专门为 ELF 文件运行时中的段所准备的。

Elf32_Phdr的定义如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
typedef struct {
    ELF32_Word  p_type;
    ELF32_Off   p_offset;
    ELF32_Addr  p_vaddr;
    ELF32_Addr  p_paddr;
    ELF32_Word  p_filesz;
    ELF32_Word  p_memsz;
    ELF32_Word  p_flags;
    ELF32_Word  p_align;
} Elf32_Phdr;
  • 字段说明
字段 说明
p_type 该字段为段的类型,或者表明了该结构的相关信息。
p_offset 该字段给出了从文件开始到该段开头的第一个字节的偏移。
p_vaddr 该字段给出了该段第一个字节在内存中的虚拟地址。
p_paddr 该字段仅用于物理地址寻址相关的系统中, 由于 “System V” 忽略了应用程序的物理寻址,可执行文件和共享目标文件的该项内容并未被限定。
p_filesz 该字段给出了文件镜像中该段的大小,可能为 0。
p_memsz 该字段给出了内存镜像中该段的大小,可能为 0。
p_flags 该字段给出了与段相关的标记。
p_align 可加载的程序的段的 p_vaddr 以及 p_offset 的大小必须是 page 的整数倍。该成员给出了段在文件以及内存中的对齐方式。如果该值为 0 或 1 的话,表示不需要对齐。除此之外,p_align 应该是 2 的整数指数次方,并且 p_vaddrp_offset 在模 p_align 的意义下,应该相等。

核心意义:操作系统加载 ELF 文件时,根据程序头表将 “段” 映射到内存,例如:

  • p_type=PT_LOAD的段会被加载到p_vaddr指定的内存地址。
  • p_flags=0x5(R+X)通常对应代码段(可执行且只读),p_flags=0x6(R+W)对应数据段。

Section Header Table(节区头表)

该结构用于定位 ELF 文件中的每个节区的具体位置。描述 ELF 文件在编译链接时的结构(即 “节区” 信息),用于调试、静态分析等场景。每个条目对应一个Elf32_Shdr结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
typedef struct {
    ELF32_Word      sh_name;
    ELF32_Word      sh_type;
    ELF32_Word      sh_flags;
    ELF32_Addr      sh_addr;
    ELF32_Off       sh_offset;
    ELF32_Word      sh_size;
    ELF32_Word      sh_link;
    ELF32_Word      sh_info;
    ELF32_Word      sh_addralign;
    ELF32_Word      sh_entsize;
} Elf32_Shdr;
成员 说明 取值 说明
sh_name sh_name节名称,是节区头字符串表节区中(Section Header String Table Section)的索引,因此该字段实际是一个数值。在字符串表中的具体内容是以 NULL 结尾的字符串。 0 无名称
sh_type sh_type根据节的内容和语义进行分类,具体的类型下面会介绍。 SHT_NULL 限制
sh_flags sh_flags每一比特代表不同的标志,描述节是否可写,可执行,需要分配内存等属性。 0 无标志
sh_addr sh_addr如果节区将出现在进程的内存映像中,此成员给出节区的第一个字节应该在进程镜像中的位置。否则,此字段为 0。 0 无地址
sh_offset sh_offset给出节区的第一个字节与文件开始处之间的偏移。SHT_NOBITS 类型的节区不占用文件的空间,因此其 sh_offset 成员给出的是概念性的偏移。 0 无文件偏移
sh_size sh_size此成员给出节区的字节大小。除非节区的类型是 SHT_NOBITS ,否则该节占用文件中的 sh_size 字节。类型为 SHT_NOBITS 的节区长度可能非零,不过却不占用文件中的空间。 0 无大小
sh_link sh_link此成员给出节区头部表索引链接,其具体的解释依赖于节区类型。 SHN_UNDEF 无链接信息
sh_info sh_info此成员给出附加信息,其解释依赖于节区类型。 0 无辅助信息
sh_addralign sh_addralign某些节区的地址需要对齐。例如,如果一个节区有一个 doubleword 类型的变量,那么系统必须保证整个节区按双字对齐。也就是说,sh_addr%sh_addralign=0。目前它仅允许为 0,以及 2 的正整数幂数。 0 和 1 表示没有对齐约束。 0 无对齐要求

常见节区(Sections)

节区是 ELF 文件的实际数据载体,常见类型及作用如下:

节区名 描述
.text 代码段,存放程序的指令
.data 数据段,存放已初始化的全局变量和静态变量
.rodata 只读数据段,存放只读数据
.bss 未初始化数据段,存放未初始化的全局变量和静态变量
.symtab 符号表,存放符号信息
.strtab 字符串表,存放字符串数据
.dynsym 动态符号表,存放动态链接需要的符号信息
.dynamic 动态链接信息,存放动态链接器需要的信息

段与节区的关系

  • 段(Segment):面向运行时,由一个或多个节区组成(如.text.rodata可合并为一个 “代码段”)。
  • 节区(Section):面向编译时,按功能划分的最小数据单元。

例如:一个PT_LOAD段可能包含.text(代码)和.rodata(只读数据)两个节区,共同映射到内存的只读可执行区域。

0x01 ARM基础知识

  • x86、arm、RISC-V” 都是 指令集架构 ( CPU架构 )。ARM 是主流的 RISC(精简指令集)架构,广泛用于嵌入式设备(如 Android 手机)。

  • 指令集特点

  • 固定指令长度(32 位 ARM 指令,Thumb 模式为 16 位)。

  • 大部分指令可条件执行(如ADDNE R0, R1, R2:仅当标志位不为 0 时执行加法)。

  • 寄存器操作优先(算术 / 逻辑运算通常基于寄存器,内存访问需专用指令)。

  • 核心寄存器

​ ARM32 架构有 16 个通用寄存器(R0-R15),其中:

  • R0-R3:函数调用参数 / 返回值寄存器。
  • R4-R11:通用寄存器(需手动保存)。
  • R12(IP):临时寄存器。
  • R13(SP):栈指针寄存器(指向栈顶)。
  • R14(LR):链接寄存器(存储函数返回地址)。
  • R15(PC):程序计数器(存储下一条执行的指令地址)。

常见寻址方式

寻址方式 描述
立即数寻址 直接使用立即数值作为操作数,例如:MOV R0, #5
寄存器直接寻址 使用寄存器中的值作为操作数,例如:MOV R0, R1
寄存器间接寻址 使用寄存器中的值作为内存地址,访问该地址中的数据,例如:LDR R0, [R1]
寄存器相对寻址 使用寄存器中的值加上一个立即偏移量作为内存地址,例如:LDR R0, [R1, #4]
寄存器变址寻址 使用两个寄存器中的值相加作为内存地址,例如:LDR R0, [R1, R2]
带有变址寄存器的寄存器相对寻址 使用寄存器中的值加上另一个寄存器的值乘以一个比例因子作为内存地址,例如:LDR R0, [R1, R2, LSL #2]
堆栈寻址 使用堆栈指针寄存器(如SP)进行操作,例如:PUSH {R0, R1}POP {R0, R1}

压栈和出栈指令

指令类型 指令示例 描述
压栈 PUSH {R0, R1} 将寄存器R0和R1的内容压入堆栈中
压栈 PUSH {R0-R5} 将寄存器R0到R5的内容压入堆栈中
压栈 STMDB SP!, {R0-R5} 将寄存器R0到R5的内容压入堆栈中(与PUSH等效)
出栈 POP {R0, R1} 从堆栈中弹出数据,恢复到寄存器R0和R1中
出栈 POP {R0-R5} 从堆栈中弹出数据,恢复到寄存器R0到R5中

跳转指令

指令类型 指令示例 描述
无条件跳转 B label 无条件跳转到标签label指向的位置
子程序调用 BL label 调用子程序,将当前指令的下一条指令地址存入链接寄存器(LR),然后跳转到标签label指向的位置
子程序返回 BX LR 返回子程序调用前的位置,跳转到链接寄存器(LR)中存储的地址
寄存器跳转 BX Rn 跳转到寄存器Rn中存储的地址

算术运算指令

汇编中也可以进行算术运算, 比如加减乘除,常用的运算指令用法如表 所示:

指令 计算公式 备注
ADD Rd, Rn, Rm Rd = Rn + Rm 加法运算,指令为 ADD
ADD Rd, Rn, #immed Rd = Rn + #immed 加法运算,指令为 ADD
ADC Rd, Rn, Rm Rd = Rn + Rm + 进位 带进位的加法运算,指令为 ADC
ADC Rd, Rn, #immed Rd = Rn + #immed + 进位 带进位的加法运算,指令为 ADC
SUB Rd, Rn, Rm Rd = Rn - Rm 减法
SUB Rd, #immed Rd = Rd - #immed 减法
SUB Rd, Rn, #immed Rd = Rn - #immed 减法
SBC Rd, Rn, #immed Rd = Rn - #immed - 借位 带借位的减法
SBC Rd, Rn ,Rm Rd = Rn - Rm - 借位 带借位的减法
MUL Rd, Rn, Rm Rd = Rn * Rm 乘法 (32 位)
UDIV Rd, Rn, Rm Rd = Rn / Rm 无符号除法
SDIV Rd, Rn, Rm Rd = Rn / Rm 有符号除法

逻辑运算

汇编语言的时候也可以使用逻辑运算指令,常用的运算指令用法如表 所示: img

0x02 Android so文件分析

1.SO 文件概述

  • SO 文件是 Unix/Linux 系统中的动态库文件,被称为共享目标文件(Shared Object File),后缀名为 .so,它是 ELF 的一种,另外属于 ELF 类型的还有可重定位文件(Relocatable File)以及核心转储文件(Core Dump File)。

  • SO 文件通常用于提高开发效率、方便快速移植代码,以及保护 Android 应用的核心逻辑,增加逆向工程的难度。不同的 CPU 架构(如 ARM、x86 等)需要不同版本的 SO 文件来适配其指令集和运行环境。

ELF 文件类型 典型后缀 核心用途 逆向 / 开发场景举例 与 ARM 的关联
可执行文件 无(如/bin/ls 直接运行的程序 分析 Linux/ARM 嵌入式设备的二进制逻辑 ARM32/64 位设备的可执行文件需对应架构
共享目标文件(SO) .so 供其他程序动态调用的库(代码复用 + 保护) 逆向 Android 的libnative.so(核心逻辑载体) Android SO 分armeabi-v7a(ARM32)、arm64-v8a(ARM64)
目标文件 .o 编译后的中间文件(未链接) 分析编译后的单个模块代码(如 NDK 编译的xxx.o ARM 编译器(如arm-linux-androideabi-gcc)生成对应.o
核心转储文件 .core 程序崩溃时的内存快照 分析 SO 崩溃原因(如空指针访问导致的 core dump) ARM 设备崩溃时生成的 core 需用 ARM 架构的 gdb 分析
  • Android 是基于 Linux 内核开发的操作系统,所以 Android 平台上的可执行文件格式和 Unix/Linux 是一致的。

  • so文件大体上可分为四部分,一般来说从上往下是ELF头部->Pargarm头部->节区(Section)->节区头,其中,除了ELF头部在文件位置固定不变外,其余三部分的位置都不固定。

  • 整体结构图参考非虫大佬的图:

img

2.SO 文件的加载方法

Android 通过 Java 层的System类加载 SO,核心方法有两种:

方法 签名 特点 适用场景
loadLibrary System.loadLibrary(String name) 自动拼接文件名(name → libname.so),从默认路径(如/data/app/.../lib)加载。 应用内置 SO 的常规加载。
load System.load(String path) 需传入 SO 的绝对路径(如/sdcard/libtest.so)。 插件化加载、动态下载的 SO。

3.SO 文件的加载流程

loadLibrary 加载流程

  • 调用 System.loadLibrary(String libName) 时,实际上会调用 Runtime.getRuntime().loadLibrary(String libName)
  • Runtime.loadLibrary(String libName) 会进一步调用 Runtime.loadLibrary(String libName, ClassLoader loader),其中 loader 是当前线程的类加载器。
  • 通过类加载器找到对应的 SO 文件路径,然后调用 doLoad(String name, ClassLoader loader) 方法进行加载。

load 加载流程

  • 调用 System.load(String pathName) 时,实际上会调用 Runtime.getRuntime().load(String pathName)
  • Runtime.load(String pathName) 会调用 Runtime.load(String pathName, ClassLoader loader),同样会调用 doLoad(String name, ClassLoader loader) 方法进行加载。

核心加载流程:doLoad

  • 路径处理doLoad 方法会根据传入的路径和类加载器,确定 SO 文件的实际路径。
  • 动态链接库加载:调用 dlopen 函数加载动态链接库。
  • JNI_OnLoad 调用:如果 SO 文件中定义了 JNI_OnLoad 函数,则在加载完成后调用该函数进行初始化。

参考文件:

Android so(ELF)文件解析

《安卓逆向这档事》十、不是我说,有了IDA还要什么女朋友?

android 加载so过程分析

[ctfwiki]so 介绍

前途似海,来日方长。

<