so逆向基础全图:ARM汇编、ELF、GOT/PLT、init_array、Frida反调试

admin 2026-09-06 04:45:12 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文系统梳理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 不是万能的。反编译本质是「翻译」,把机器码翻译成人话,翻译就有信息损失。三个场景它会失灵:

  1. 加壳,真正的代码加密,静态拖进 IDA 是密文,F5 直接报错
  2. 混淆(Ollvm),控制流被拆成几百个基本块用 switch 串起来,F5 出来就是 goto 地狱
  3. 无符号 / 内联汇编,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 &nbsp;; r0 = r1 + (r2 << 1)
adds r0, r1, r2 &nbsp; &nbsp; &nbsp; &nbsp; ; 加法 + 更新 NZCV

sub r0, r1, r2 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; r0 = r1 - r2
sub r0, r1, #5 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; r0 = r1 - 5
sub r0, r1, r2, asr #2 &nbsp;; r0 = r1 - (r2 >> 2 算术右移)

rsb r0, r1, r2 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; r0 = r2 - r1(反着减)
rsb r0, r1, #0 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; r0 = 0 - r1,等价取负数

重点:ARM 写法是「目标,源1,源2」,第一个是结果,新手最容易搞反。

MUL

mul r0, r1, r2 &nbsp; &nbsp;; r0 = r1 * r2

硬性限制:目标和第一个源不能是同一个寄存器,mul r0, r0, r1 非法。

位运算 AND / ORR / EOR / BIC

and r0, r1, #0x0F &nbsp; &nbsp; &nbsp;; 保留 r1 低 4 位,其余清零
orr r0, r1, #(1<<5) &nbsp; &nbsp;; r1 第 5 位置 1
eor r0, r1, #0xFF &nbsp; &nbsp; &nbsp;; r1 低 8 位取反
bic r0, r1, #0x0F &nbsp; &nbsp; &nbsp;; 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] &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; r0 = *(uint32_t*)r1
str r0, [r1] &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; *(uint32_t*)r1 = r0
ldr r0, [r1, #0x40] &nbsp; ; r0 = *(uint32_t*)(r1+0x40)

ldrb r0, [r1] &nbsp; &nbsp; &nbsp; &nbsp; ; 读 1 字节
strb r0, [r1] &nbsp; &nbsp; &nbsp; &nbsp; ; 写 1 字节

ldrsb r0, [r1] &nbsp; &nbsp; &nbsp; &nbsp;; 8bit 有符号,符号扩展;0xF0 -> 0xFFFFFFF0

ldrh r0, [r1] &nbsp; &nbsp; &nbsp; &nbsp; ; 读半字
strh r0, [r1] &nbsp; &nbsp; &nbsp; &nbsp; ; 写半字

ldrsh r0, [r1] &nbsp; &nbsp; &nbsp; &nbsp;; 16bit 有符号,符号扩展

注意 str 没有 SB/SH,存储不需要符号扩展,只有 str/strb/strh。

LDM / STM(批量读写)

LDM 是内存→一组寄存器(读),STM 是一组寄存器→内存(写)。后缀决定地址变化方向:

| 后缀 | 含义 | | — | — | | IA | 先访问,地址自增(后加),最常用 | | IB | 地址先自增,再访问 | | DA | 先访问,地址自减 | | DB | 地址先自减,再访问 |

ldmia r0!, {r4-r6} &nbsp; ; 连续读三个字到 r4/r5/r6
stmdb sp!, {r4, lr} &nbsp;; 等价 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 &nbsp; label &nbsp; &nbsp; ; 直接跳转
bl &nbsp;func &nbsp; &nbsp; &nbsp;; 调用函数,返回地址存 lr
bx &nbsp;r3 &nbsp; &nbsp; &nbsp; &nbsp;; 跳转到寄存器地址
blx func &nbsp; &nbsp; &nbsp;; 调用 + 切换 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 &nbsp; &nbsp; ; 计算 r0-r1,只更新标志位
movlt r0, r1 &nbsp; ; 若 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 &nbsp;&nbsp;# 文件头
readelf -s libtest.so &nbsp;&nbsp;# 符号表
readelf -d libtest.so &nbsp;&nbsp;# 动态段
readelf -l libtest.so &nbsp;&nbsp;# 段视图(Program Headers)
readelf -S libtest.so &nbsp;&nbsp;# 节视图(Section Headers)
readelf -r libtest.so &nbsp;&nbsp;# 重定位表

2.2 ELF Header(readelf -h)

readelf -h libtest.so
ELF Header:
&nbsp; Magic: &nbsp; 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
&nbsp; Class: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ELF64
&nbsp; Data: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;2's complement, little endian
&nbsp; Type: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;DYN (Shared object file)
&nbsp; Machine: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Advanced Micro Devices X86-64
&nbsp; Entry point address: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 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:
&nbsp; &nbsp;Num: &nbsp; &nbsp;Value &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;Size Type &nbsp; &nbsp;Bind &nbsp; Vis &nbsp; &nbsp; &nbsp;Ndx Name
&nbsp; &nbsp; &nbsp;5: 00000000000010f9 &nbsp; &nbsp;20 FUNC &nbsp; &nbsp;GLOBAL DEFAULT &nbsp; &nbsp;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:
&nbsp; Tag &nbsp; &nbsp; &nbsp; &nbsp;Type &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Name/Value
&nbsp; 0x000000000000000c (INIT) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 0x1000
&nbsp; 0x0000000000000019 (INIT_ARRAY) &nbsp; &nbsp; &nbsp; &nbsp; 0x3e68
&nbsp; 0x000000000000001b (INIT_ARRAYSZ) &nbsp; &nbsp; &nbsp; 8 (bytes)
&nbsp; 0x000000000000001a (FINI_ARRAY) &nbsp; &nbsp; &nbsp; &nbsp; 0x3e70
&nbsp; 0x0000000000000004 (STRTAB) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 0x318
&nbsp; 0x0000000000000005 (SYMTAB) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 0x288
&nbsp; 0x0000000000000006 (SYMENT) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 24 (bytes)
&nbsp; 0x0000000000000003 (PLTGOT) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 0x3fe8
&nbsp; 0x0000000000000007 (RELA) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 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
&nbsp; → jmp [GOT[n]] &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; GOT[n] 指向 PLT 下一句
&nbsp; → push 重定位索引
&nbsp; → jmp PLT0 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; 公共解析桩
&nbsp; → resolver 查真实地址
&nbsp; → 写回 GOT[n]
&nbsp; → 跳真实函数

后续调用,GOT[n] 已是真实地址,直接查表直跳。

3.4 GOT Hook 原理

既然调用都查 GOT,那把 GOT[n] 改成自己的函数地址,下次调用就被劫持:

got_strcmp = (void**)find_got_entry("strcmp");
*got_strcmp = my_strcmp; &nbsp;&nbsp;// 改成自己的函数

改完,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,&nbsp;"strcmp");
real_strcmp(a, b); &nbsp;&nbsp;// 直接调真实函数,GOT Hook 不到

IDA 里看到频繁 dlsym 拿地址再 call,大概率在防 Hook。

四、init_array 构造函数

4.1 是什么

init_array 是 ELF 里存构造函数指针的数组。构造函数不是 C++ 专属,C 也能用,本质是「加载时自动执行的初始化函数」:

__attribute__((constructor))
void&nbsp;first()&nbsp;{
&nbsp; &nbsp;&nbsp;// 这里会在 so 加载时自动执行
&nbsp; &nbsp;&nbsp;if&nbsp;(1) {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;exit(0);
&nbsp; &nbsp; }
}

4.2 执行顺序

加载 so → _init → init_array(依次执行) → 业务代码

IDA 里 init_array 段看到的是函数指针数组:

.init_array:
&nbsp; &nbsp; dq offset frame_dummy &nbsp; ; 编译器添加的
&nbsp; &nbsp; dq offset first &nbsp; &nbsp; &nbsp; &nbsp; ; 你自己写的 constructor

4.3 风控的反调试藏在这

风控 SDK 把反调试、环境检测、完整性校验塞进 init_array,让 so 一加载就先跑检测。所以逆向带风控的 so,IDA 打开 .init_array 段,经常能看到 5-10 条函数指针,里面往往藏着反调试、Root 检测、模拟器检测。

4.4 逆向怎么看

  1. IDA 找到 .init_array 段
  2. 里面是一堆函数地址,逐个双击进去
  3. 重点找带 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&nbsp;"detect"

一个实际函数还原

下面这段是笔记里一个真实函数(结构体 + printf)的反汇编,逐行读一遍,你就知道「还原」是怎么回事:

000013b0 <r0zerox>:
&nbsp; &nbsp; 13b0: e92d4800 &nbsp; &nbsp; &nbsp;push &nbsp; &nbsp;{r11, lr} &nbsp; &nbsp; &nbsp; &nbsp;; 保存帧指针、返回地址;内部调 bl 会覆盖 lr
&nbsp; &nbsp; 13b4: e1a0b00d &nbsp; &nbsp; &nbsp;mov &nbsp; &nbsp; r11, sp &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; r11 作为帧指针 fp
&nbsp; &nbsp; 13b8: e24dd008 &nbsp; &nbsp; &nbsp;sub &nbsp; &nbsp; sp, sp, #8 &nbsp; &nbsp; &nbsp; ; 栈上开 8 字节,放结构体 struct addr a
&nbsp; &nbsp; 13bc: e59f0024 &nbsp; &nbsp; &nbsp;ldr &nbsp; &nbsp; r0, [pc, #0x24] &nbsp;; PC 相对寻址,读字符串偏移
&nbsp; &nbsp; 13c0: e08f0000 &nbsp; &nbsp; &nbsp;add &nbsp; &nbsp; r0, pc, r0 &nbsp; &nbsp; &nbsp; ; r0 = pc + 偏移,得到字符串地址
&nbsp; &nbsp; 13c4: e58d0000 &nbsp; &nbsp; &nbsp;str &nbsp; &nbsp; r0, [sp] &nbsp; &nbsp; &nbsp; &nbsp; ; *(uint32_t*)sp = r0,即 a.sheng = "s"
&nbsp; &nbsp; 13c8: e59d1000 &nbsp; &nbsp; &nbsp;ldr &nbsp; &nbsp; r1, [sp] &nbsp; &nbsp; &nbsp; &nbsp; ; 取出 a.sheng 作为 printf 第 2 参数
&nbsp; &nbsp; 13cc: e59f0010 &nbsp; &nbsp; &nbsp;ldr &nbsp; &nbsp; r0, [pc, #0x10] &nbsp;; 读格式化字符串偏移
&nbsp; &nbsp; 13d0: e08f0000 &nbsp; &nbsp; &nbsp;add &nbsp; &nbsp; r0, pc, r0 &nbsp; &nbsp; &nbsp; ; 得到 "sheng=%s"
&nbsp; &nbsp; 13d4: eb000015 &nbsp; &nbsp; &nbsp;bl &nbsp; &nbsp; &nbsp;0x1430 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; 调用 printf,lr = 0x13d8
&nbsp; &nbsp; 13d8: e3000000 &nbsp; &nbsp; &nbsp;movw &nbsp; &nbsp;r0, #0x0 &nbsp; &nbsp; &nbsp; &nbsp; ; 返回值 r0 = 0
&nbsp; &nbsp; 13dc: e1a0d00b &nbsp; &nbsp; &nbsp;mov &nbsp; &nbsp; sp, r11 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; 恢复 sp
&nbsp; &nbsp; 13e0: e8bd8800 &nbsp; &nbsp; &nbsp;pop &nbsp; &nbsp; {r11, pc} &nbsp; &nbsp; &nbsp; &nbsp;; 恢复 r11,lr 送入 pc,返回

对应的 C 代码:

struct&nbsp;addr&nbsp;{
&nbsp; &nbsp;&nbsp;const&nbsp;char* sheng;
&nbsp; &nbsp;&nbsp;const&nbsp;char* shi;
};

int&nbsp;r0zerox()&nbsp;{
&nbsp; &nbsp;&nbsp;struct&nbsp;addr&nbsp;a;
&nbsp; &nbsp; a.sheng =&nbsp;"s";
&nbsp; &nbsp;&nbsp;printf("sheng=%s", a.sheng);
&nbsp; &nbsp;&nbsp;return&nbsp;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 检测,从浅到深五层:

  1. 环境痕迹,查文件、进程、端口、maps
  2. 运行时特征,查线程名、命名管道、D-Bus
  3. 代码完整性,内存-磁盘校验、检查函数前导码有没有被改写
  4. 行为异常,时间差、syscall 监控
  5. 自保护,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:
&nbsp; &nbsp; add r0, r0, r1
&nbsp; &nbsp; bx &nbsp;lr
int&nbsp;add(int&nbsp;a,&nbsp;int&nbsp;b)&nbsp;{&nbsp;return&nbsp;a + b; }

考点:叶子函数直接 bx lr 返回,r0 是返回值。

题 2 乘加混合(★)

calc:
&nbsp; &nbsp; add r0, r0, r0, lsl #2 &nbsp; ; a + (a<<2)
&nbsp; &nbsp; sub r0, r0, r1 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; - b
&nbsp; &nbsp; bx &nbsp;lr
int&nbsp;calc(int&nbsp;a,&nbsp;int&nbsp;b)&nbsp;{&nbsp;return&nbsp;a *&nbsp;5&nbsp;- b; } &nbsp;&nbsp;// a + (a<<2) = 5a

考点:带移位操作数的加法,a<<2 即 a*4。

题 3 条件执行求最大(★★)

max:
&nbsp; &nbsp; cmp r0, r1
&nbsp; &nbsp; movlt r0, r1
&nbsp; &nbsp; bx &nbsp;lr
int&nbsp;max(int&nbsp;a,&nbsp;int&nbsp;b)&nbsp;{&nbsp;return&nbsp;a > b ? a : b; }

考点:cmp 更新标志位,movlt 是「小于时执行」,if 用条件后缀完成。

题 4 循环求和(★★)

sum_n:
&nbsp; &nbsp; mov r2, #0 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; s = 0
&nbsp; &nbsp; mov r3, #1 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; i = 1
loop:
&nbsp; &nbsp; cmp r3, r0 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; i 和 n 比
&nbsp; &nbsp; bgt done
&nbsp; &nbsp; add r2, r2, r3 &nbsp; &nbsp; &nbsp;; s += i
&nbsp; &nbsp; add r3, r3, #1
&nbsp; &nbsp; b &nbsp; loop
done:
&nbsp; &nbsp; mov r0, r2
&nbsp; &nbsp; bx &nbsp;lr
int&nbsp;sum_n(int&nbsp;n)&nbsp;{
&nbsp; &nbsp;&nbsp;int&nbsp;s =&nbsp;0;
&nbsp; &nbsp;&nbsp;for&nbsp;(int&nbsp;i =&nbsp;1; i <= n; i++) s += i;
&nbsp; &nbsp;&nbsp;return&nbsp;s;
}

考点:循环三件套,初始化、比较跳转、自增。bgt done 是 i>n 跳出,循环条件 i<=n。

题 5 strlen(★★)

mystrlen:
&nbsp; &nbsp; mov r1, #0
loop:
&nbsp; &nbsp; ldrb r2, [r0, r1] &nbsp; ; s[n]
&nbsp; &nbsp; cmp r2, #0
&nbsp; &nbsp; beq done
&nbsp; &nbsp; add r1, r1, #1
&nbsp; &nbsp; b &nbsp; loop
done:
&nbsp; &nbsp; mov r0, r1
&nbsp; &nbsp; bx &nbsp;lr
int&nbsp;mystrlen(char&nbsp;*s)&nbsp;{
&nbsp; &nbsp;&nbsp;int&nbsp;n =&nbsp;0;
&nbsp; &nbsp;&nbsp;while&nbsp;(s[n] !=&nbsp;0) n++;
&nbsp; &nbsp;&nbsp;return&nbsp;n;
}

考点:ldrb 读 1 字节,字符串以 \0 结尾,cmp r2,#0 / beq 判断结尾。

题 6 数组求和(★★)

sum_arr:
&nbsp; &nbsp; mov r2, #0
&nbsp; &nbsp; mov r3, #0
loop:
&nbsp; &nbsp; cmp r3, r1
&nbsp; &nbsp; bge done
&nbsp; &nbsp; ldr r12, [r0, r3, lsl #2]
&nbsp; &nbsp; add r2, r2, r12
&nbsp; &nbsp; add r3, r3, #1
&nbsp; &nbsp; b &nbsp; loop
done:
&nbsp; &nbsp; mov r0, r2
&nbsp; &nbsp; bx &nbsp;lr
int&nbsp;sum_arr(int&nbsp;*arr,&nbsp;int&nbsp;len)&nbsp;{
&nbsp; &nbsp;&nbsp;int&nbsp;sum =&nbsp;0;
&nbsp; &nbsp;&nbsp;for&nbsp;(int&nbsp;i =&nbsp;0; i < len; i++) sum += arr[i];
&nbsp; &nbsp;&nbsp;return&nbsp;sum;
}

考点:ldr r12,[r0,r3,lsl #2] 是数组寻址,r0 首地址,r3 下标,lsl #2 是乘 4。

题 7 位提取(★★)

get_bit:
&nbsp; &nbsp; mov r2, #1
&nbsp; &nbsp; lsl r2, r2, r1 &nbsp; &nbsp; &nbsp;; 1 << n
&nbsp; &nbsp; and r0, r0, r2 &nbsp; &nbsp; &nbsp;; x & (1<<n)
&nbsp; &nbsp; lsr r0, r0, r1 &nbsp; &nbsp; &nbsp;; >> n
&nbsp; &nbsp; bx &nbsp;lr
int&nbsp;get_bit(int&nbsp;x,&nbsp;int&nbsp;n)&nbsp;{&nbsp;return&nbsp;(x >> n) &&nbsp;1; }

考点:lsl/and/lsr 组合提取某一位。

题 8 结构体求和(★★)

@ struct Point { int x; int y; int z; };
sum_xyz:
&nbsp; &nbsp; ldr r1, [r0, #0]
&nbsp; &nbsp; ldr r2, [r0, #4]
&nbsp; &nbsp; ldr r3, [r0, #8]
&nbsp; &nbsp; add r0, r1, r2
&nbsp; &nbsp; add r0, r0, r3
&nbsp; &nbsp; bx &nbsp;lr
struct&nbsp;Point&nbsp;{&nbsp;int&nbsp;x;&nbsp;int&nbsp;y;&nbsp;int&nbsp;z; };
int&nbsp;sum_xyz(struct&nbsp;Point *p)&nbsp;{&nbsp;return&nbsp;p->x + p->y + p->z; }

考点:结构体成员按声明顺序连续存放,偏移 0/4/8。

题 9 非叶子函数(★★★)

g:
&nbsp; &nbsp; add r0, r0, #1
&nbsp; &nbsp; bx &nbsp;lr

f:
&nbsp; &nbsp; push {r4, lr} &nbsp; &nbsp; &nbsp; ; 保存 lr,要调 g
&nbsp; &nbsp; bl &nbsp;g &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; r0 = g(x)
&nbsp; &nbsp; mov r0, r0, lsl #1 &nbsp;; r0 *= 2
&nbsp; &nbsp; pop {r4, pc}
int&nbsp;g(int&nbsp;x)&nbsp;{&nbsp;return&nbsp;x +&nbsp;1; }
int&nbsp;f(int&nbsp;x)&nbsp;{&nbsp;return&nbsp;g(x) *&nbsp;2; }

考点:f 内部调 g(bl 覆盖 lr),必须 push lr 保存返回地址,pop pc 恢复返回。

题 10 奇偶累加(★★★)

sum_even:
&nbsp; &nbsp; mov r2, #0
&nbsp; &nbsp; mov r3, #0
loop:
&nbsp; &nbsp; cmp r3, r1
&nbsp; &nbsp; bge done
&nbsp; &nbsp; ldr r12, [r0, r3, lsl #2]
&nbsp; &nbsp; tst r12, #1
&nbsp; &nbsp; addeq r2, r2, r12
&nbsp; &nbsp; add r3, r3, #1
&nbsp; &nbsp; b &nbsp; loop
done:
&nbsp; &nbsp; mov r0, r2
&nbsp; &nbsp; bx &nbsp;lr
int&nbsp;sum_even(int&nbsp;*arr,&nbsp;int&nbsp;len)&nbsp;{
&nbsp; &nbsp;&nbsp;int&nbsp;s =&nbsp;0;
&nbsp; &nbsp;&nbsp;for&nbsp;(int&nbsp;i =&nbsp;0; i < len; i++)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;((arr[i] &&nbsp;1) ==&nbsp;0) s += arr[i];
&nbsp; &nbsp;&nbsp;return&nbsp;s;
}

考点:tst r12,#1 按位与不写回,只更新标志位判断奇偶,addeq 是偶数才累加。

7.2 C → 汇编(题 11-20)

题 11 加法(★)

int&nbsp;add(int&nbsp;a,&nbsp;int&nbsp;b)&nbsp;{&nbsp;return&nbsp;a + b; }
add:
&nbsp; &nbsp; add r0, r0, r1
&nbsp; &nbsp; bx &nbsp;lr

题 12 两数差绝对值(★★)

int&nbsp;abs_diff(int&nbsp;a,&nbsp;int&nbsp;b)&nbsp;{&nbsp;return&nbsp;a > b ? a - b : b - a; }
abs_diff:
&nbsp; &nbsp; cmp r0, r1
&nbsp; &nbsp; subgt r0, r0, r1
&nbsp; &nbsp; suble r0, r1, r0
&nbsp; &nbsp; bx &nbsp;lr

考点:subgt / suble 条件执行实现 if-else。

题 13 循环求和(★★)

int&nbsp;sum_to_n(int&nbsp;n)&nbsp;{
&nbsp; &nbsp;&nbsp;int&nbsp;s =&nbsp;0;
&nbsp; &nbsp;&nbsp;for&nbsp;(int&nbsp;i =&nbsp;1; i <= n; i++) s += i;
&nbsp; &nbsp;&nbsp;return&nbsp;s;
}
sum_to_n:
&nbsp; &nbsp; mov r2, #0
&nbsp; &nbsp; mov r3, #1
loop:
&nbsp; &nbsp; cmp r3, r0
&nbsp; &nbsp; bgt done
&nbsp; &nbsp; add r2, r2, r3
&nbsp; &nbsp; add r3, r3, #1
&nbsp; &nbsp; b &nbsp; loop
done:
&nbsp; &nbsp; mov r0, r2
&nbsp; &nbsp; bx &nbsp;lr

题 14 统计 1 的个数(★★★)

int&nbsp;count_ones(int&nbsp;x)&nbsp;{
&nbsp; &nbsp;&nbsp;int&nbsp;c =&nbsp;0;
&nbsp; &nbsp;&nbsp;while&nbsp;(x) {
&nbsp; &nbsp; &nbsp; &nbsp; c += x &&nbsp;1;
&nbsp; &nbsp; &nbsp; &nbsp; x >>=&nbsp;1;
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;return&nbsp;c;
}
count_ones:
&nbsp; &nbsp; mov r1, #0
loop:
&nbsp; &nbsp; cmp r0, #0
&nbsp; &nbsp; beq done
&nbsp; &nbsp; and r2, r0, #1
&nbsp; &nbsp; add r1, r1, r2
&nbsp; &nbsp; lsr r0, r0, #1
&nbsp; &nbsp; b &nbsp; loop
done:
&nbsp; &nbsp; mov r0, r1
&nbsp; &nbsp; bx &nbsp;lr

考点:and 取最低位,lsr 逻辑右移对应无符号 >>,负数要用 asr。

题 15 数组找最大(★★★)

int&nbsp;max_arr(int&nbsp;*a,&nbsp;int&nbsp;n)&nbsp;{
&nbsp; &nbsp;&nbsp;int&nbsp;m = a[0];
&nbsp; &nbsp;&nbsp;for&nbsp;(int&nbsp;i =&nbsp;1; i < n; i++)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(a[i] > m) m = a[i];
&nbsp; &nbsp;&nbsp;return&nbsp;m;
}
max_arr:
&nbsp; &nbsp; ldr r2, [r0, #0] &nbsp; &nbsp; &nbsp; &nbsp;; m = a[0]
&nbsp; &nbsp; mov r3, #1
loop:
&nbsp; &nbsp; cmp r3, r1
&nbsp; &nbsp; bge done
&nbsp; &nbsp; ldr r12, [r0, r3, lsl #2]
&nbsp; &nbsp; cmp r12, r2
&nbsp; &nbsp; movgt r2, r12 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; a[i] > m 时更新
&nbsp; &nbsp; add r3, r3, #1
&nbsp; &nbsp; b &nbsp; loop
done:
&nbsp; &nbsp; mov r0, r2
&nbsp; &nbsp; bx &nbsp;lr

题 16 高低 16 位交换(★★)

unsigned&nbsp;int&nbsp;swap16(unsigned&nbsp;int&nbsp;x)&nbsp;{
&nbsp; &nbsp;&nbsp;return&nbsp;((x &&nbsp;0xFFFF) <<&nbsp;16) | ((x >>&nbsp;16) &&nbsp;0xFFFF);
}
swap16:
&nbsp; &nbsp; mov r1, r0, lsl #16 &nbsp; &nbsp; ; 低 16 位移到高
&nbsp; &nbsp; mov r0, r0, lsr #16 &nbsp; &nbsp; ; 高 16 位移到低
&nbsp; &nbsp; orr r0, r0, r1
&nbsp; &nbsp; bx &nbsp;lr

考点:mov 带移位,lsl #16 天然丢弃高位低位补 0,等效 (x&0xFFFF)<<16。

题 17 结构体面积(★★)

struct&nbsp;Rect&nbsp;{&nbsp;int&nbsp;w;&nbsp;int&nbsp;h; };
int&nbsp;area(struct&nbsp;Rect *r)&nbsp;{&nbsp;return&nbsp;r->w * r->h; }
area:
&nbsp; &nbsp; ldr r1, [r0, #0] &nbsp; &nbsp; &nbsp; &nbsp;; r->w
&nbsp; &nbsp; ldr r2, [r0, #4] &nbsp; &nbsp; &nbsp; &nbsp;; r->h
&nbsp; &nbsp; mul r0, r1, r2
&nbsp; &nbsp; bx &nbsp;lr

题 18 递归阶乘(★★★)

int&nbsp;factorial(int&nbsp;n)&nbsp;{
&nbsp; &nbsp;&nbsp;if&nbsp;(n <=&nbsp;1)&nbsp;return&nbsp;1;
&nbsp; &nbsp;&nbsp;return&nbsp;n * factorial(n -&nbsp;1);
}
factorial:
&nbsp; &nbsp; push {r4, lr}
&nbsp; &nbsp; cmp r0, #1
&nbsp; &nbsp; ble base
&nbsp; &nbsp; mov r4, r0
&nbsp; &nbsp; sub r0, r0, #1
&nbsp; &nbsp; bl &nbsp;factorial
&nbsp; &nbsp; mul r0, r4, r0
&nbsp; &nbsp; pop {r4, pc}
base:
&nbsp; &nbsp; mov r0, #1
&nbsp; &nbsp; pop {r4, pc}

考点:递归是非叶子函数,必须 push lr;n 在 bl 后还要用,存进 callee-saved 的 r4。

题 19 switch 跳转表(★★★)

int&nbsp;menu(int&nbsp;op)&nbsp;{
&nbsp; &nbsp;&nbsp;switch&nbsp;(op) {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;case&nbsp;0:&nbsp;return&nbsp;10;
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;case&nbsp;1:&nbsp;return&nbsp;20;
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;case&nbsp;2:&nbsp;return&nbsp;30;
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;default:&nbsp;return&nbsp;-1;
&nbsp; &nbsp; }
}
menu:
&nbsp; &nbsp; cmp r0, #2
&nbsp; &nbsp; bhi default_case
&nbsp; &nbsp; ldr r1, =jump_table
&nbsp; &nbsp; ldr r1, [r1, r0, lsl #2]
&nbsp; &nbsp; bx &nbsp;r1

case0:
&nbsp; &nbsp; mov r0, #10
&nbsp; &nbsp; bx &nbsp;lr
case1:
&nbsp; &nbsp; mov r0, #20
&nbsp; &nbsp; bx &nbsp;lr
case2:
&nbsp; &nbsp; mov r0, #30
&nbsp; &nbsp; bx &nbsp;lr
default_case:
&nbsp; &nbsp; mov r0, #-1
&nbsp; &nbsp; bx &nbsp;lr

jump_table:
&nbsp; &nbsp; .word case0
&nbsp; &nbsp; .word case1
&nbsp; &nbsp; .word case2

考点:switch 常被编译成跳转表,ldr [r1,r0,lsl #2] 按下标取地址,bx 间接跳转,cmp/bhi 是越界检查。

题 20 点积(★★★)

int&nbsp;dot(int&nbsp;*a,&nbsp;int&nbsp;*b,&nbsp;int&nbsp;n)&nbsp;{
&nbsp; &nbsp;&nbsp;int&nbsp;s =&nbsp;0;
&nbsp; &nbsp;&nbsp;for&nbsp;(int&nbsp;i =&nbsp;0; i < n; i++) s += a[i] * b[i];
&nbsp; &nbsp;&nbsp;return&nbsp;s;
}
dot:
&nbsp; &nbsp; push {r4, r5}
&nbsp; &nbsp; mov r3, #0
&nbsp; &nbsp; mov r12, #0
loop:
&nbsp; &nbsp; cmp r12, r2
&nbsp; &nbsp; bge done
&nbsp; &nbsp; ldr r4, [r0, r12, lsl #2]
&nbsp; &nbsp; ldr r5, [r1, r12, lsl #2]
&nbsp; &nbsp; mul r4, r4, r5
&nbsp; &nbsp; add r3, r3, r4
&nbsp; &nbsp; add r12, r12, #1
&nbsp; &nbsp; b &nbsp; loop
done:
&nbsp; &nbsp; mov r0, r3
&nbsp; &nbsp; pop {r4, r5}
&nbsp; &nbsp; bx &nbsp;lr

考点:用了 r4/r5(callee-saved)要 push/pop 保护,mul+add 两行可合并成 MLA。

小结

一张图串起来:

ARM 汇编(怎么读代码)
&nbsp; → ELF(文件长什么样)
&nbsp; &nbsp; → GOT/PLT(函数怎么调、怎么 Hook)
&nbsp; &nbsp; &nbsp; → init_array(加载时先跑什么)
&nbsp; &nbsp; &nbsp; &nbsp; → Frida 反调试(怎么发现你、你怎么藏)

这五块加 objdump、20 道互逆题,就是 so 逆向的完整地基。搞懂它们,再去做实战逆向、写 Frida Hook、对抗风控检测,就不会抓瞎了。

群名:Opcode.Fight.Club / 作者:r0zerox


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:逆向狂人 r0zerox r0zerox《so 逆向基础全图:ARM 汇编、ELF、GOT/PLT、init_array、Frida 反调试》

评论:0   参与:  0