文章总结: 本文系统梳理so逆向基础,涵盖arm32汇编核心概念(寄存器、指令、寻址)、ELF文件结构、GOT/PLT机制、init_array、objdump工具及Frida反调试技术,并附汇编与C互逆练习。强调汇编是逆向不可绕开的基础,因反编译工具在加壳、混淆等场景失效。内容实用,适合入门者建立完整知识框架。 综合评分: 88 文章分类: 二进制安全,逆向分析,移动安全,安全工具
so 逆向基础全图:ARM 汇编、ELF、GOT/PLT、init_array、Frida 反调试
原创
r0zerox r0zerox
逆向狂人
2026年8月14日 18:28 广东
在小说阅读器读本章
去阅读
在公众号小说中沉浸阅读
so 逆向基础全图:ARM 汇编、ELF、GOT/PLT、init_array、Frida 反调试
声明:本文所有内容仅供技术学习交流,不涉及任何具体应用破解,严禁用于商业用途和非法用途。
so 逆向要碰的知识点很散,汇编、文件格式、链接、反调试,各有各的坑。这篇把最基础的几块串成一张全图,代码尽量给全,看完你对 so 逆向的地基就有完整概念了。
目录:零 为什么学汇编、一 ARM 汇编、二 ELF 文件结构、三 GOT/PLT、四 init_array、五 objdump、六 Frida 反调试、七 汇编↔C 互逆练习 20 题。
零、为什么必须学汇编
动手之前,先讲清一个前提,为什么 so 逆向绕不开汇编。
很多人的想法是,IDA 按一下 F5 一键反编译成 C 伪代码,Ghidra 免费也能用,为什么还要手啃汇编。
因为 F5 不是万能的。反编译本质是「翻译」,把机器码翻译成人话,翻译就有信息损失。三个场景它会失灵:
- 加壳,真正的代码加密,静态拖进 IDA 是密文,F5 直接报错
- 混淆(Ollvm),控制流被拆成几百个基本块用 switch 串起来,F5 出来就是 goto 地狱
- 无符号 / 内联汇编,so 被 strip 过,函数名全 sub_xxx,手写内联汇编经常识别错
伪代码是转述,汇编才是原话。不管 App 怎么加壳、混淆、加密,最后它得在 CPU 上跑,而 CPU 执行的最小单位,就是一条条汇编指令。
所以汇编是逆向里,唯一不会骗你的东西。这就是你必须学它的根本原因。
一、ARM32 汇编基础
1.1 寄存器:16 个,分工明确
ARM32 处理器有 16 个通用寄存器 r0-r15,分工固定:
| 寄存器 | 别名 | 用途 | | — | — | — | | r0 – r3 | – | 前 4 个参数,r0 兼返回值 | | r4 – r11 | – | 局部变量、中间结果(callee-saved) | | r12 | ip | 过程内调用临时寄存器(linker 草稿纸) | | r13 | sp | 栈指针 | | r14 | lr | 链接寄存器,存返回地址 | | r15 | pc | 程序计数器 |
关键调用约定(AAPCS):参数超过 4 个,第 5 个开始压栈;返回值放 r0;调用用 bl,返回地址自动存 lr,函数结束 bx lr 返回。
LR(链接寄存器)
LR 的本质是存返回地址。bl 调用时,CPU 自动把下一条指令地址存入 lr:
0x1000: mov r0, #1 ; 准备参数
0x1004: bl 0x2000 ; 调用函数,lr 自动 = 0x1008(下一句)
0x1008: mov r1, r0 ; 函数返回后继续
函数怎么返回,分两种情况:
; 叶子函数,不调别的函数,直接返回
bx lr
; 非叶子函数,内部还会调别的函数
push {r4, lr} ; 把 lr 压栈保存
bl 0x3000 ; 这里 lr 被改了
pop {r4, lr} ; 从栈弹回原来的 lr
逆向要点:IDA 里看到函数开头 push {lr},就说明这个函数内部还会调用其他函数。
PC(程序计数器)与三级流水线
PC 指向 CPU 正在取指的指令。ARM 经典三级流水线(取指→译码→执行),所以读 pc 的值 = 当前指令地址 + 8。
; 假设当前在 0x1000 执行
mov r0, pc ; r0 = 0x1008,不是 0x1000
PC 相对寻址常用于加载大立即数(常量池):
ldr r0, [pc, #0x40] ; 读 pc+0x40 处的值
IDA 里经常看到这种用法,加载一个塞不进指令的 32 位立即数,放旁边,PC 相对寻址。
IP(R12)
R12 是链接器的草稿纸,函数调用中间过渡用的临时寄存器,用完就扔,不需要恢复。静态分析基本可以忽略它,只有研究 GOT/PLT 劫持时才关注。看到 R12 频繁出现,大概率是 PLT 跳板代码。
1.2 CPSR 与条件标志
CPSR 是状态寄存器,核心是 N/Z/C/V 四个条件标志位,占据 bit[31:28]。
凡是算术/数据传送指令,末尾带 S 就会自动更新 NZCV:
movs r0, #0x80000000 ; 带 S:刷新 NZCV,结果最高位是 1,N=1
mov r0, #0x80000000 ; 不带 S:r0 被赋值,但 NZCV 维持原样
四个标志位含义:
| 标志 | 含义 | | — | — | | N | 结果为负 | | Z | 结果为零 | | C | 无符号加法进位 | | V | 有符号加法溢出 |
1.3 数据处理指令
MOV / MVN
mov r0, #5 ; r0 = 5
mov r1, r0 ; r1 = r0
mvn r2, #0 ; r2 = ~0 = 0xFFFFFFFF,先取反再送目标
# 是立即数标记。ARM 立即数有限制,必须是「8 位值循环右移偶数位」能表示的数,塞不下的(如 0x12345678)要用 ldr = 伪指令或 movw/movt。
ADD / SUB / RSB
add r0, r1, r2 ; r0 = r1 + r2
add r0, r1, #0x40 ; r0 = r1 + 0x40
add r0, r1, r2, lsl #1 ; r0 = r1 + (r2 << 1)
adds r0, r1, r2 ; 加法 + 更新 NZCV
sub r0, r1, r2 ; r0 = r1 - r2
sub r0, r1, #5 ; r0 = r1 - 5
sub r0, r1, r2, asr #2 ; r0 = r1 - (r2 >> 2 算术右移)
rsb r0, r1, r2 ; r0 = r2 - r1(反着减)
rsb r0, r1, #0 ; r0 = 0 - r1,等价取负数
重点:ARM 写法是「目标,源1,源2」,第一个是结果,新手最容易搞反。
MUL
mul r0, r1, r2 ; r0 = r1 * r2
硬性限制:目标和第一个源不能是同一个寄存器,mul r0, r0, r1 非法。
位运算 AND / ORR / EOR / BIC
and r0, r1, #0x0F ; 保留 r1 低 4 位,其余清零
orr r0, r1, #(1<<5) ; r1 第 5 位置 1
eor r0, r1, #0xFF ; r1 低 8 位取反
bic r0, r1, #0x0F ; r1 低 4 位清零(ARM 特有)
bic 是位清除,把操作数里为 1 的位在源里清零。清除个别位用 bic 更直观,截取部分位只能用 and。
1.4 访存指令 LDR / STR
ARM 是 load/store 架构,不能直接操作内存,必须先读进寄存器(ldr),算完再写回(str)。
后缀区分数据宽度:
| 后缀 | 含义 | 长度 | 符号 | | — | — | — | — | | (无) | 字 Word | 32bit | – | | B | 字节 | 8bit | 无符号 | | SB | 有符号字节 | 8bit | 符号扩展 | | H | 半字 | 16bit | 无符号 | | SH | 有符号半字 | 16bit | 符号扩展 |
ldr r0, [r1] ; r0 = *(uint32_t*)r1
str r0, [r1] ; *(uint32_t*)r1 = r0
ldr r0, [r1, #0x40] ; r0 = *(uint32_t*)(r1+0x40)
ldrb r0, [r1] ; 读 1 字节
strb r0, [r1] ; 写 1 字节
ldrsb r0, [r1] ; 8bit 有符号,符号扩展;0xF0 -> 0xFFFFFFF0
ldrh r0, [r1] ; 读半字
strh r0, [r1] ; 写半字
ldrsh r0, [r1] ; 16bit 有符号,符号扩展
注意 str 没有 SB/SH,存储不需要符号扩展,只有 str/strb/strh。
LDM / STM(批量读写)
LDM 是内存→一组寄存器(读),STM 是一组寄存器→内存(写)。后缀决定地址变化方向:
| 后缀 | 含义 | | — | — | | IA | 先访问,地址自增(后加),最常用 | | IB | 地址先自增,再访问 | | DA | 先访问,地址自减 | | DB | 地址先自减,再访问 |
ldmia r0!, {r4-r6} ; 连续读三个字到 r4/r5/r6
stmdb sp!, {r4, lr} ; 等价 push,先减栈再存
1.5 寻址方式(6 种)
| 寻址类型 | 指令 | 公式 |
| — | — | — |
| 基址 | ldr r0,[r1] | EA = r1 |
| 基址+立即偏移 | ldr r0,[r1,#4] | EA = r1+4 |
| 基址+寄存器偏移 | ldr r0,[r1,r2] | EA = r1+r2 |
| 基址+寄存器移位 | ldr r0,[r1,r2,lsl #2] | EA = r1+r2*4 |
| 前变址 | ldr r0,[r1,#4]! | r1+=4,再取 |
| 后变址 | ldr r0,[r1],#4 | 先取,再 r1+=4 |
逆向要点:看到 [r1, r2, lsl #2] 基本就是访问 int 数组,r1 首地址,r2 下标,lsl #2 是乘 4。
1.6 分支与条件码
b label ; 直接跳转
bl func ; 调用函数,返回地址存 lr
bx r3 ; 跳转到寄存器地址
blx func ; 调用 + 切换 ARM/Thumb 状态
条件码(加在指令后缀):
| 条件码 | 含义 | 标志位 | | — | — | — | | EQ | 相等 | Z=1 | | NE | 不等 | Z=0 | | GT | 有符号大于 | Z=0, N=V | | LT | 有符号小于 | N≠V | | GE | 有符号大于等于 | N=V | | LE | 有符号小于等于 | Z=1 或 N≠V | | HI | 无符号大于 | C=1, Z=0 | | LS | 无符号小于等于 | C=0 或 Z=1 |
条件执行示例,求最大值:
cmp r0, r1 ; 计算 r0-r1,只更新标志位
movlt r0, r1 ; 若 r0 < r1,才执行 mov
整个 if 用条件后缀完成,连跳转都省了。这是 ARM 跟 x86 最大的区别。
1.7 ARM 模式 vs Thumb 模式
| 特性 | ARM 模式 | Thumb 模式 | | — | — | — | | 指令宽度 | 32 位 | 16 位(Thumb-2 混合 32 位) | | PC 最低位 | 0 | 1 | | 场景 | 性能敏感 | 省 ROM,嵌入式 |
IDA 里根据 blx/bx 跳转地址的最低位判断:最低位 0 是 ARM,1 是 Thumb。不能根据 b/bl 判断,因为实际跳转时非偶数地址会被 CPU 舍弃。
二、ELF 文件结构
2.1 readelf 四连
readelf 专门解析 ELF(可执行文件、.so、.a),逆向 so 的第一步。核心命令:
readelf -h libtest.so # 文件头
readelf -s libtest.so # 符号表
readelf -d libtest.so # 动态段
readelf -l libtest.so # 段视图(Program Headers)
readelf -S libtest.so # 节视图(Section Headers)
readelf -r libtest.so # 重定位表
2.2 ELF Header(readelf -h)
readelf -h libtest.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
Type: DYN (Shared object file)
Machine: Advanced Micro Devices X86-64
Entry point address: 0x0
Magic 前 4 字节解读:
| 字节 | 值 | 含义 | | — | — | — | | 7f 45 4c 46 | ELF | ASCII 标识,内核判断格式 | | 02 | ELF64 | 64 位(01 是 32 位) | | 01 | little endian | 小端(02 是大端) | | 01 | – | ELF 版本 |
关键字段:Class 是位数,Type 的 DYN 表示共享库 .so(EXEC 是可执行),Machine 是指令集架构。
2.3 符号表(readelf -s)
符号表有两个,这是 ELF 逆向最重要的一点:
Symbol table '.dynsym' contains 6 entries:
Num: Value Size Type Bind Vis Ndx Name
5: 00000000000010f9 20 FUNC GLOBAL DEFAULT 9 add
Symbol table '.symtab' contains 25 entries:
| 符号表 | 给谁用 | strip 后 | | — | — | — | | .dynsym | 运行时 ld.so 动态链接 | 保留 | | .symtab | 链接时 ld | 删除 |
所以 strip 过的 so,.symtab 全没了,函数名变 sub_xxx;但 .dynsym 还在。逆向第一步看 .dynsym,摸清导出和导入。
符号表列含义:
| 列 | 含义 | | — | — | | Num | 序号 | | Value | 地址 | | Size | 大小 | | Type | 类型 | | Bind | 局部还是全局 | | Name | 符号名 |
关键字段 Ndx:Ndx=UND 表示外部导入(定义在别的库),Ndx=数字 表示定义在第几号 section。WEAK UND 是可选外部符号,缺失不报错。
2.4 动态段(readelf -d)
Dynamic section at offset 0x2e78 contains 17 entries:
Tag Type Name/Value
0x000000000000000c (INIT) 0x1000
0x0000000000000019 (INIT_ARRAY) 0x3e68
0x000000000000001b (INIT_ARRAYSZ) 8 (bytes)
0x000000000000001a (FINI_ARRAY) 0x3e70
0x0000000000000004 (STRTAB) 0x318
0x0000000000000005 (SYMTAB) 0x288
0x0000000000000006 (SYMENT) 24 (bytes)
0x0000000000000003 (PLTGOT) 0x3fe8
0x0000000000000007 (RELA) 0x378
动态段是 ld.so 的操作手册,关键字段:
| Tag | 含义 | | — | — | | INIT | _init 地址,加载时最先执行 | | INIT_ARRAY | 构造函数指针数组 | | FINI / FINI_ARRAY | 析构函数 | | SYMTAB / STRTAB | 动态符号表 / 字符串表地址 | | SYMENT | 符号表每个条目字节数 | | PLTGOT | GOT 表地址(即 GLOBAL_OFFSET_TABLE) | | RELA | 重定位表 |
一个验证:SYMTAB(0x288) + 6 条 × SYMENT(24) = 0x318 = STRTAB,正好对上。PLTGOT 的地址跟 readelf -s 里的 GLOBAL_OFFSET_TABLE 一致。
2.5 段视图(readelf -l)
4 个 LOAD 段 = 4 次 mmap,不同权限:
| LOAD 段 | 权限 | 内容 | | — | — | — | | LOAD 1 | R+E | 代码段(.text) | | LOAD 2 | R | 只读数据(.rodata) | | LOAD 3 | RW | 可读写数据(.data + .bss) |
两个重要段:
- GNU_RELRO:加载后把 .dynamic、.got 锁成只读,防 GOT 覆写
- GNU_STACK:RW=NX,栈不可执行
执行顺序:_init → init_array → 业务代码 → fini_array → _fini。
三、GOT/PLT 延迟绑定与 Hook
3.1 为什么要绕 GOT/PLT
so 调外部函数(如 strcmp),编译时不知道 libc.so 加载到哪个基址,没法写死地址。于是走「延迟绑定」:
调用方 → PLT(跳板) → GOT(地址表) → 真实函数
GOT 是 Global Offset Table,存函数真实地址。PLT 是 Procedure Linkage Table,跳板代码。
3.2 GOT 前 3 项,ABI 固定
| GOT 项 | 内容 | | — | — | | GOT[0] | .dynamic 段地址 | | GOT[1] | link_map 指针 | | GOT[2] | resolver 地址(_dl_runtime_resolve) |
从 GOT[3] 开始,每个条目对应一个外部导入函数。
3.3 首次调用 vs 后续调用
首次调用,GOT[3] 还不是真实地址,流程是:
call func@plt
→ jmp [GOT[n]] ; GOT[n] 指向 PLT 下一句
→ push 重定位索引
→ jmp PLT0 ; 公共解析桩
→ resolver 查真实地址
→ 写回 GOT[n]
→ 跳真实函数
后续调用,GOT[n] 已是真实地址,直接查表直跳。
3.4 GOT Hook 原理
既然调用都查 GOT,那把 GOT[n] 改成自己的函数地址,下次调用就被劫持:
got_strcmp = (void**)find_got_entry("strcmp");
*got_strcmp = my_strcmp; // 改成自己的函数
改完,so 里所有通过 PLT 调 strcmp 的地方,都会跳进 my_strcmp。
关键:GOT Hook 改的是「调用方」自己的 GOT 表,不是被调用方。
3.5 局限
GOT Hook 只对「过 PLT/GOT 的跨模块调用」有效。so 内部对某函数的直接相对调用(不过 PLT/GOT),Hook 不到。所以很多 Hook 要上 inline hook,直接改函数开头指令。
3.6 dlsym 反制
风控 SDK 用 dlsym 运行时查函数真实地址,然后直接 call,绕过 PLT/GOT:
real_strcmp = dlsym(RTLD_NEXT, "strcmp");
real_strcmp(a, b); // 直接调真实函数,GOT Hook 不到
IDA 里看到频繁 dlsym 拿地址再 call,大概率在防 Hook。
四、init_array 构造函数
4.1 是什么
init_array 是 ELF 里存构造函数指针的数组。构造函数不是 C++ 专属,C 也能用,本质是「加载时自动执行的初始化函数」:
__attribute__((constructor))
void first() {
// 这里会在 so 加载时自动执行
if (1) {
exit(0);
}
}
4.2 执行顺序
加载 so → _init → init_array(依次执行) → 业务代码
IDA 里 init_array 段看到的是函数指针数组:
.init_array:
dq offset frame_dummy ; 编译器添加的
dq offset first ; 你自己写的 constructor
4.3 风控的反调试藏在这
风控 SDK 把反调试、环境检测、完整性校验塞进 init_array,让 so 一加载就先跑检测。所以逆向带风控的 so,IDA 打开 .init_array 段,经常能看到 5-10 条函数指针,里面往往藏着反调试、Root 检测、模拟器检测。
4.4 逆向怎么看
- IDA 找到 .init_array 段
- 里面是一堆函数地址,逐个双击进去
- 重点找带 ptrace、TracerPid、/proc/self、syscall 这些特征的,大概率是反调试
五、objdump 反汇编
| 参数 | 作用 | | — | — | | -d | 反汇编代码段 .text(最常用) | | -C | 自动 demangle C++ 符号 | | -D | 反汇编所有段(大量垃圾) | | -S | 反汇编 + 混合源码(需 -g) | | -s | 显示所有段十六进制 | | -t | 打印符号表 | | -T | 动态符号表 .dynsym(逆向最重要) | | -R | 显示重定位表(GOT、外部导入) | | -h | 查看段头信息 | | -j .text | 只解析指定段 | | -m arm | 指定架构 | | -M force-thumb | 强制按 Thumb 解码(ARM32 逆向神器) |
指定 Thumb 解码(llvm-objdump):
llvm-objdump -d -C --triple=armv7a-linux-androideabi16-thumb libanti_frida.so | grep -A 80 "detect"
一个实际函数还原
下面这段是笔记里一个真实函数(结构体 + printf)的反汇编,逐行读一遍,你就知道「还原」是怎么回事:
000013b0 <r0zerox>:
13b0: e92d4800 push {r11, lr} ; 保存帧指针、返回地址;内部调 bl 会覆盖 lr
13b4: e1a0b00d mov r11, sp ; r11 作为帧指针 fp
13b8: e24dd008 sub sp, sp, #8 ; 栈上开 8 字节,放结构体 struct addr a
13bc: e59f0024 ldr r0, [pc, #0x24] ; PC 相对寻址,读字符串偏移
13c0: e08f0000 add r0, pc, r0 ; r0 = pc + 偏移,得到字符串地址
13c4: e58d0000 str r0, [sp] ; *(uint32_t*)sp = r0,即 a.sheng = "s"
13c8: e59d1000 ldr r1, [sp] ; 取出 a.sheng 作为 printf 第 2 参数
13cc: e59f0010 ldr r0, [pc, #0x10] ; 读格式化字符串偏移
13d0: e08f0000 add r0, pc, r0 ; 得到 "sheng=%s"
13d4: eb000015 bl 0x1430 ; 调用 printf,lr = 0x13d8
13d8: e3000000 movw r0, #0x0 ; 返回值 r0 = 0
13dc: e1a0d00b mov sp, r11 ; 恢复 sp
13e0: e8bd8800 pop {r11, pc} ; 恢复 r11,lr 送入 pc,返回
对应的 C 代码:
struct addr {
const char* sheng;
const char* shi;
};
int r0zerox() {
struct addr a;
a.sheng = "s";
printf("sheng=%s", a.sheng);
return 0;
}
几个关键点:sub sp, sp, #8 是开局部变量栈空间,str r0, [sp] 是给结构体字段赋值,pop {r11, pc} 里 pc 直接接住保存的 lr 实现返回。
六、Frida 检测与反调试
6.1 Frida 暴露的痕迹(8 项)
Frida 注入后会在目标进程留一堆痕迹,App 靠这些发现你:
| # | 检测项 | 方法 | 风险 | | — | — | — | — | | 1 | 进程名 | ps 匹配 frida / hluda | 低 | | 2 | 磁盘文件 | /data/local/tmp/ 下 frida-server | 低 | | 3 | TCP 端口 | /proc/net/tcp 查 hex 端口 69A6~69A9 | 中 | | 4 | D-Bus 协议 | 连 127.0.0.1:27042 发 AUTH | 中高 | | 5 | maps 库名 | /proc/pid/maps 匹配 frida / gadget | 高 | | 6 | rwxp 段 | maps 匹配 rwxp | 高 | | 7 | 线程名 | /proc/pid/task/*/status 匹配 gum-js-loop | 中 | | 8 | 命名管道 | fd 解析匹配 linjector | 中 |
6.2 检测五层模型
App 的反 Frida 检测,从浅到深五层:
- 环境痕迹,查文件、进程、端口、maps
- 运行时特征,查线程名、命名管道、D-Bus
- 代码完整性,内存-磁盘校验、检查函数前导码有没有被改写
- 行为异常,时间差、syscall 监控
- 自保护,syscall 直调绕过 libc hook、O-LLVM 混淆
大部分 App 只做到前两层,「查端口 + 查进程 + 查 maps」就够发现 Frida 了。
6.3 静态特征(apk-scan 思路)
Java 层 smali 关键字:
| 特征 | 关键字 | | — | — | | 端口扫描 | 27042, 27043 | | 进程检测 | frida-server, hluda | | maps 检测 | /proc/self/maps | | TracerPid | /proc/self/status | | 线程检测 | gum-js-loop, gmain |
Native 层 .so 字符串:
| 特征 | 关键字 | | — | — | | Frida 字符串 | frida, FRIDA | | 端口硬编码 | 27042 | | 线程名 | gum-js-loop, gmain | | 管道名 | linjector | | syscall 直调 | syscall(, svc 0 | | 反调试 | PTRACE_TRACEME, ptrace |
6.4 动态监控(frida-spy 思路)
5 个 Hook 点:
| # | Hook 目标 | 监控内容 | | — | — | — | | 1 | libc.so:open/openat | 参数含 /proc/self/ 或 frida | | 2 | libc.so:ptrace | request == PTRACE_TRACEME(0) | | 3 | libc.so:connect | 目标端口 27042-27045 | | 4 | libc.so:syscall | nr 为 openat(56)/read(63)/ptrace(117) | | 5 | 任意 so:dlsym | symbol 含 frida/agent/gum |
6.5 绕过手段速查
| 检测方式 | 绕过 | | — | — | | 扫端口 27042 | 改 frida-server 监听端口 | | 扫进程名 | 重命名二进制 | | 查 maps 库名/线程名/管道 | sed 替换 server 二进制里的字符串 | | 内存-磁盘校验 | hook 校验函数,patch 校验和 | | syscall 直调检测 | frida-gadget script 模式先于目标执行 | | ptrace 反调试 | 用 frida-gadget(非注入模式,不调 ptrace) |
核心一句话,Frida 的痕迹基本都在 frida-server 二进制里,改名字、改端口、替换字符串,能抹掉一大半。
6.6 ARM64 syscall 编号(OnePlus 8T 环境)
| syscall | NR | 用途 | | — | — | — | | read | 63 | 文件读取 | | write | 64 | 文件写入 | | openat | 56 | 文件打开 | | close | 57 | 文件关闭 | | ptrace | 117 | 进程追踪 | | nanosleep | 101 | 定时休眠 |
ARM64 的 syscall 指令是 svc #0,风控常用它直调 syscall 绕过 libc hook。
七、汇编 ↔ C 互逆练习(20 题全)
7.1 汇编 → C(题 1-10)
题 1 加法(★)
add:
add r0, r0, r1
bx lr
int add(int a, int b) { return a + b; }
考点:叶子函数直接 bx lr 返回,r0 是返回值。
题 2 乘加混合(★)
calc:
add r0, r0, r0, lsl #2 ; a + (a<<2)
sub r0, r0, r1 ; - b
bx lr
int calc(int a, int b) { return a * 5 - b; } // a + (a<<2) = 5a
考点:带移位操作数的加法,a<<2 即 a*4。
题 3 条件执行求最大(★★)
max:
cmp r0, r1
movlt r0, r1
bx lr
int max(int a, int b) { return a > b ? a : b; }
考点:cmp 更新标志位,movlt 是「小于时执行」,if 用条件后缀完成。
题 4 循环求和(★★)
sum_n:
mov r2, #0 ; s = 0
mov r3, #1 ; i = 1
loop:
cmp r3, r0 ; i 和 n 比
bgt done
add r2, r2, r3 ; s += i
add r3, r3, #1
b loop
done:
mov r0, r2
bx lr
int sum_n(int n) {
int s = 0;
for (int i = 1; i <= n; i++) s += i;
return s;
}
考点:循环三件套,初始化、比较跳转、自增。bgt done 是 i>n 跳出,循环条件 i<=n。
题 5 strlen(★★)
mystrlen:
mov r1, #0
loop:
ldrb r2, [r0, r1] ; s[n]
cmp r2, #0
beq done
add r1, r1, #1
b loop
done:
mov r0, r1
bx lr
int mystrlen(char *s) {
int n = 0;
while (s[n] != 0) n++;
return n;
}
考点:ldrb 读 1 字节,字符串以 \0 结尾,cmp r2,#0 / beq 判断结尾。
题 6 数组求和(★★)
sum_arr:
mov r2, #0
mov r3, #0
loop:
cmp r3, r1
bge done
ldr r12, [r0, r3, lsl #2]
add r2, r2, r12
add r3, r3, #1
b loop
done:
mov r0, r2
bx lr
int sum_arr(int *arr, int len) {
int sum = 0;
for (int i = 0; i < len; i++) sum += arr[i];
return sum;
}
考点:ldr r12,[r0,r3,lsl #2] 是数组寻址,r0 首地址,r3 下标,lsl #2 是乘 4。
题 7 位提取(★★)
get_bit:
mov r2, #1
lsl r2, r2, r1 ; 1 << n
and r0, r0, r2 ; x & (1<<n)
lsr r0, r0, r1 ; >> n
bx lr
int get_bit(int x, int n) { return (x >> n) & 1; }
考点:lsl/and/lsr 组合提取某一位。
题 8 结构体求和(★★)
@ struct Point { int x; int y; int z; };
sum_xyz:
ldr r1, [r0, #0]
ldr r2, [r0, #4]
ldr r3, [r0, #8]
add r0, r1, r2
add r0, r0, r3
bx lr
struct Point { int x; int y; int z; };
int sum_xyz(struct Point *p) { return p->x + p->y + p->z; }
考点:结构体成员按声明顺序连续存放,偏移 0/4/8。
题 9 非叶子函数(★★★)
g:
add r0, r0, #1
bx lr
f:
push {r4, lr} ; 保存 lr,要调 g
bl g ; r0 = g(x)
mov r0, r0, lsl #1 ; r0 *= 2
pop {r4, pc}
int g(int x) { return x + 1; }
int f(int x) { return g(x) * 2; }
考点:f 内部调 g(bl 覆盖 lr),必须 push lr 保存返回地址,pop pc 恢复返回。
题 10 奇偶累加(★★★)
sum_even:
mov r2, #0
mov r3, #0
loop:
cmp r3, r1
bge done
ldr r12, [r0, r3, lsl #2]
tst r12, #1
addeq r2, r2, r12
add r3, r3, #1
b loop
done:
mov r0, r2
bx lr
int sum_even(int *arr, int len) {
int s = 0;
for (int i = 0; i < len; i++)
if ((arr[i] & 1) == 0) s += arr[i];
return s;
}
考点:tst r12,#1 按位与不写回,只更新标志位判断奇偶,addeq 是偶数才累加。
7.2 C → 汇编(题 11-20)
题 11 加法(★)
int add(int a, int b) { return a + b; }
add:
add r0, r0, r1
bx lr
题 12 两数差绝对值(★★)
int abs_diff(int a, int b) { return a > b ? a - b : b - a; }
abs_diff:
cmp r0, r1
subgt r0, r0, r1
suble r0, r1, r0
bx lr
考点:subgt / suble 条件执行实现 if-else。
题 13 循环求和(★★)
int sum_to_n(int n) {
int s = 0;
for (int i = 1; i <= n; i++) s += i;
return s;
}
sum_to_n:
mov r2, #0
mov r3, #1
loop:
cmp r3, r0
bgt done
add r2, r2, r3
add r3, r3, #1
b loop
done:
mov r0, r2
bx lr
题 14 统计 1 的个数(★★★)
int count_ones(int x) {
int c = 0;
while (x) {
c += x & 1;
x >>= 1;
}
return c;
}
count_ones:
mov r1, #0
loop:
cmp r0, #0
beq done
and r2, r0, #1
add r1, r1, r2
lsr r0, r0, #1
b loop
done:
mov r0, r1
bx lr
考点:and 取最低位,lsr 逻辑右移对应无符号 >>,负数要用 asr。
题 15 数组找最大(★★★)
int max_arr(int *a, int n) {
int m = a[0];
for (int i = 1; i < n; i++)
if (a[i] > m) m = a[i];
return m;
}
max_arr:
ldr r2, [r0, #0] ; m = a[0]
mov r3, #1
loop:
cmp r3, r1
bge done
ldr r12, [r0, r3, lsl #2]
cmp r12, r2
movgt r2, r12 ; a[i] > m 时更新
add r3, r3, #1
b loop
done:
mov r0, r2
bx lr
题 16 高低 16 位交换(★★)
unsigned int swap16(unsigned int x) {
return ((x & 0xFFFF) << 16) | ((x >> 16) & 0xFFFF);
}
swap16:
mov r1, r0, lsl #16 ; 低 16 位移到高
mov r0, r0, lsr #16 ; 高 16 位移到低
orr r0, r0, r1
bx lr
考点:mov 带移位,lsl #16 天然丢弃高位低位补 0,等效 (x&0xFFFF)<<16。
题 17 结构体面积(★★)
struct Rect { int w; int h; };
int area(struct Rect *r) { return r->w * r->h; }
area:
ldr r1, [r0, #0] ; r->w
ldr r2, [r0, #4] ; r->h
mul r0, r1, r2
bx lr
题 18 递归阶乘(★★★)
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
factorial:
push {r4, lr}
cmp r0, #1
ble base
mov r4, r0
sub r0, r0, #1
bl factorial
mul r0, r4, r0
pop {r4, pc}
base:
mov r0, #1
pop {r4, pc}
考点:递归是非叶子函数,必须 push lr;n 在 bl 后还要用,存进 callee-saved 的 r4。
题 19 switch 跳转表(★★★)
int menu(int op) {
switch (op) {
case 0: return 10;
case 1: return 20;
case 2: return 30;
default: return -1;
}
}
menu:
cmp r0, #2
bhi default_case
ldr r1, =jump_table
ldr r1, [r1, r0, lsl #2]
bx r1
case0:
mov r0, #10
bx lr
case1:
mov r0, #20
bx lr
case2:
mov r0, #30
bx lr
default_case:
mov r0, #-1
bx lr
jump_table:
.word case0
.word case1
.word case2
考点:switch 常被编译成跳转表,ldr [r1,r0,lsl #2] 按下标取地址,bx 间接跳转,cmp/bhi 是越界检查。
题 20 点积(★★★)
int dot(int *a, int *b, int n) {
int s = 0;
for (int i = 0; i < n; i++) s += a[i] * b[i];
return s;
}
dot:
push {r4, r5}
mov r3, #0
mov r12, #0
loop:
cmp r12, r2
bge done
ldr r4, [r0, r12, lsl #2]
ldr r5, [r1, r12, lsl #2]
mul r4, r4, r5
add r3, r3, r4
add r12, r12, #1
b loop
done:
mov r0, r3
pop {r4, r5}
bx lr
考点:用了 r4/r5(callee-saved)要 push/pop 保护,mul+add 两行可合并成 MLA。
小结
一张图串起来:
ARM 汇编(怎么读代码)
→ ELF(文件长什么样)
→ GOT/PLT(函数怎么调、怎么 Hook)
→ init_array(加载时先跑什么)
→ Frida 反调试(怎么发现你、你怎么藏)
这五块加 objdump、20 道互逆题,就是 so 逆向的完整地基。搞懂它们,再去做实战逆向、写 Frida Hook、对抗风控检测,就不会抓瞎了。
群名:Opcode.Fight.Club / 作者:r0zerox
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:逆向狂人 r0zerox r0zerox《so 逆向基础全图:ARM 汇编、ELF、GOT/PLT、init_array、Frida 反调试》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论