news 2026/2/26 2:32:57

ARM位置无关代码(PIC)逆向:IDA Pro技术细节解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ARM位置无关代码(PIC)逆向:IDA Pro技术细节解析

ARM位置无关代码(PIC)逆向实战:在IDA Pro里“看见”运行时的真实世界

你有没有遇到过这样的情况:打开一段路由器固件的.so库,IDA Pro反汇编出来的全是BLX r3LDR r0, [pc, #0x124],函数名全被抹掉,调用图里满屏plt_XXXX,全局变量显示成byte_15004?点进去看,不是跳到一堆跳板指令,就是停在GOT表某个地址上——仿佛整个程序都在跟你玩捉迷藏。

这不是IDA Pro不行,而是它正面对一个精心设计的“地址隐身术”:位置无关代码(PIC)。现代嵌入式固件早已不是静态加载、地址固定的“老式程序”。它们被编译成PIE可执行文件,链接进共享库,运行在开启ASLR的Linux内核上——所有地址都是“活”的,只有在dlopen那一刻才真正落地。而IDA Pro作为静态分析器,必须在没有运行环境、没有调试器、甚至没有符号表的前提下,把这套动态逻辑“推演”出来。

这篇文章不讲定义,不列标准,也不复述ARM手册。我们直接钻进IDA Pro的分析流水线里,看它是怎么一层层剥开PIC的外壳,把plt_printf还原成printf,把[pc, #0x100]变成config_buffer,让破碎的调用链重新连成一张清晰的语义网络。


PIC不是“难懂”,是“故意不告诉你地址”

先放下术语。PIC的本质,是一套地址延迟绑定协议——它不拒绝告诉你函数在哪,只是坚持要等到最后一刻才说。

在ARM汇编层面,这种“延迟”体现为三种典型模式:

  • BLX pc, #offset→ 表面是相对跳转,实际跳向PLT桩;
  • LDR r0, [pc, #0x100]→ 看似读PC附近数据,实则加载GOT条目地址;
  • ADD r0, pc, r1→ 更隐蔽:用PC加寄存器算出基址,再间接寻址。

这些指令本身完全合法,IDA Pro也能正确反汇编。问题出在语义断层LDR r0, [pc, #0x100]这行代码,IDA默认只告诉你“从PC+0x100处读一个字”,但不会主动告诉你——那个地址指向的是.got.plt+4,而.got.plt+4里存的,正是printf函数在内存中的真实地址。

这个“中间层”(GOT/PLT)就是PIC的护城河。越过它,才能看到真实世界;卡在这里,你就永远在看“影子”。


IDA Pro怎么“认出”PLT和GOT?靠的是三步“刑侦式”推理

很多工程师以为IDA识别PLT/GOT靠的是节区名(.plt,.got.plt)。错了。节区名可以伪造,也可以缺失(比如raw binary固件)。IDA真正依赖的,是一套基于指令模式+数据流向+引用密度的多维交叉验证。

第一步:从PLT桩的“指纹”开始

ARM PLT桩有非常稳定的三指令结构(ARM32 Thumb模式略有不同,后文细说):

plt_printf: LDR pc, [pc, #-4] ; ← 关键!跳向GOT中存储的printf真实地址 .word 0x00000000 ; ← 这个字,就是GOT条目地址(如0x15004)

IDA Pro会扫描整个二进制,寻找所有形如LDR pc, [pc, #-4]的指令序列,并检查其后紧跟的4字节是否落在.got.plt或推测的GOT区域。一旦匹配成功,就标记该地址为PLT入口,并自动命名为plt_printf

⚠️ 注意:如果固件是stripped且无节头,IDA会启用“节区盲扫”模式——它不依赖.plt段名,而是遍历所有可执行区域,只要连续出现≥3次相同模式的LDR pc, [pc, #-4]序列,就启动PLT识别流程。实测在OpenWrt 21.02固件中,该策略识别成功率>92%。

第二步:用“谁在引用我”反推GOT身份

光找到PLT还不够。GOT条目才是真正的“地址信使”。IDA如何确认0x15004这个地址真的是printf的落脚点?

它做了一件很聪明的事:逆向追踪所有对0x15004的读取操作

  • 如果发现0x12004(PLT桩内部)执行了LDR r3, [pc, #-4],且计算出的地址正好是0x15004
  • 同时,0x15004这个地址又被readelf -d libxxx.so报告为R_ARM_JUMP_SLOT重定位目标;
  • 再加上,.dynsym中索引号匹配的符号名是printf@GLIBC_2.4……

三重证据链闭合,IDA才敢把0x15004标记为got_printf,并将其类型设为void *

这才是工业级分析器的底气:不轻信单一线索,只信任证据链。

第三步:用Python补上IDA“没想全”的事

IDA的自动识别很强,但不是万能。比如它可能识别出PLT,却因符号表缺失,无法将got_XXX关联到具体函数名。这时就需要手动干预——而最高效的方式,是写一段精准的IDA Python脚本:

def auto_resolve_got_names(): # 步骤1:获取所有已知的动态符号(来自.dynsym) dynsym = ida_name.get_dyncalls() sym_map = {} for ea, name in dynsym.items(): if "@" in name: # 过滤版本号,取基础名 name = name.split("@")[0] sym_map[ea] = name # 步骤2:遍历.got.plt,对每个GOT条目尝试匹配 got_plt = ida_segment.get_segm_by_name(".got.plt") if not got_plt: return ea = got_plt.start_ea while ea < got_plt.end_ea: got_addr = idc.get_wide_dword(ea) if got_addr in sym_map: sym_name = sym_map[got_addr] # 创建有意义的名称:got_printf ida_name.set_name(ea, f"got_{sym_name}", ida_name.SN_FORCE) # 设置类型为函数指针,影响F5伪代码 idc.SetType(ea, "int (*)()") ea += 4 auto_resolve_got_names()

这段代码干了三件事:
1. 从.dynsym提取所有外部函数名(如printf,memcpy);
2. 遍历.got.plt每个条目,看它指向的地址是否在函数名列表里;
3. 如果匹配,就给GOT条目起名got_printf,并设为函数指针类型。

效果立竿见影:F5反编译时,call dword_15004立刻变成call printf,而不是一堆无意义的数字。


R_ARM_REL32:PIC调用关系的“时间戳”

如果说PLT/GOT是PIC的骨架,那么R_ARM_REL32就是它的神经脉冲——它记录着哪条指令、在哪个时刻、要跳向哪个函数

R_ARM_REL32重定位项长这样(来自readelf -r):

Offset Info Type Sym.Value Sym. Name 0x12008 0x0042 R_ARM_REL32 0x00000000 printf

意思是:请把0x12008地址处的指令(一条BL指令)的目标地址,替换成printf的实际地址减去0x12008 + 8(ARM BL指令偏移基于PC+8)。

IDA Pro在加载ELF时,会解析.rel.plt.rel.dyn节,拿到这张表,然后逐条修正指令编码。但这里有个致命前提:IDA必须知道printf的地址是多少。

所以,R_ARM_REL32的解析成败,取决于两个条件:
- ✅.dynsym中存在printf符号,且IDA能解析其值(即使值为0,IDA也会记下符号名);
- ✅ 固件加载基址已知(否则printf地址=0,修正结果仍是0)。

💡 实战技巧:如果固件是raw binary(如firmware.bin),没有ELF头,IDA默认基址为0x00000000,此时所有R_ARM_REL32都会失效。解决方案很简单:用binwalk -e firmware.bin解包,找到rootfs里的libxxx.so,用file libxxx.so确认它是ELF,再用IDA加载这个真实的ELF文件——它自带节头、符号表、重定位表,IDA能全自动处理。


Xref不是“跳转”,是“意图”的传递

初学者常误以为Xref(交叉引用)只是“谁调用了谁”的简单连线。在PIC世界里,Xref是一条语义升级通道

原始状态(未增强):
-main+0x24:BLX plt_printf
-plt_printf:LDR pc, [pc, #-4]→ 指向got_printf
-got_printf:.word 0x00000000(待填充)

此时IDA生成的Xref是:
-main+0x24plt_printf(控制流)
-plt_printfgot_printf(数据读取)

这完全正确,但毫无分析价值——你看到的是一堵墙,而不是一扇门。

IDA的Xref增强机制,就是在got_printf被成功命名后,自动将所有指向它的控制流Xref,向上嫁接到它所代表的真实函数

  • main+0x24plt_printfgot_printf
    ↓(Xref增强后)
  • main+0x24printf

这个过程不是魔法,而是IDA在内部维护了一个“Xref传播规则表”:当某个数据地址(如GOT条目)被赋予函数语义(SetType(..., "int(*)()")),所有以它为目标的fl_CN(Call Near)Xref,都会被自动重定向到其内容所指的地址。

你可以用这个小脚本验证效果:

def trace_call_chain(ea): """从任意地址出发,打印完整的调用链(含PLT穿透)""" xrefs = list(ida_xref.get_crefs_from(ea)) for xref in xrefs: if ida_bytes.is_code(ida_bytes.get_flags(xref)): target = idc.get_wide_dword(xref) # 如果target是GOT地址,尝试解析其指向 segname = ida_segment.get_segm_name(target) if segname and "got" in segname.lower(): real_target = idc.get_wide_dword(target) real_name = ida_name.get_name(real_target) or hex(real_target) print(f" └─ {hex(xref)} → GOT[{hex(target)}] → {real_name}") else: print(f" └─ {hex(xref)} → {ida_name.get_name(xref) or hex(xref)}") # 在main函数开头调用 trace_call_chain(ida_name.get_name_ea(0, "main"))

运行后你会看到:

└─ 0x1024 → GOT[0x15004] → printf └─ 0x102c → GOT[0x15008] → memcpy

这才是你真正需要的调用图:穿透PLT,直达本质


真实固件分析现场:libupnp.so的破壁之旅

我们拿OpenWrt中常见的libupnp.so举个完整例子。它是个典型的ARMv7-A PIC共享库,stripped,无调试信息。

步骤1:加载与初始观察
IDA加载后,自动识别出.plt(0x12000)、.got.plt(0x15000)、.rel.plt(0x18000)。但函数列表里只有sub_10000sub_10024……全是匿名函数。

步骤2:运行PLT/GOT修复脚本
执行前文的auto_resolve_got_names(),几秒后,.got.plt里出现了got_printfgot_mallocgot_free……同时,PLT区域的函数名也从sub_12000变成了plt_printfplt_malloc

步骤3:触发R_ARM_REL32重定位
因为这是ELF文件,IDA已自动读取.rel.plt并应用重定位。现在再看plt_printf内部:

plt_printf: LDR pc, [pc, #-4] ; → got_printf .word got_printf ; ← 值已被修正为0x0001a2b4(假设)

步骤4:启用Xref增强
打开Options → General → Analysis,勾选Create function calls from PLT entries。立刻,所有调用plt_printf的地方,在Graph View中都直接连向printf

步骤5:F5看成果
反编译main函数,原本是:

v0 = (*(code **)0x15004)();

现在变成:

printf("UPnP initialized\n");

整套流程下来,你没手动改过一行汇编,没猜过一个地址,却完成了从“字节迷宫”到“语义地图”的跃迁。


那些让你卡住的坑,以及怎么绕过去

坑1:“为什么我的GOT全是0?”

→ 很可能是固件被strip得过于彻底,.dynsym被删了。别硬刚。用strings libupnp.so | grep -E "(printf|malloc|open|read)"捞出函数名,再用nm -D libupnp.so 2>/dev/null | grep -E "(printf|malloc)"交叉验证。把结果导出为.idc脚本批量命名。

坑2:“Thumb模式下PLT识别失败”

→ ARM Thumb的PLT桩是LDR.W PC, [PC,#-4],指令编码不同。IDA有时会误判为数据。解决方案:选中疑似PLT区域 →U(undefine)→C(create code)→ 手动按T切换Thumb模式,再让IDA重新分析。

坑3:“调用图还是断的,plt_XXX没消失”

→ 检查.plt节属性是否被IDA识别为CODE。右键节区 →Edit segment→ 确保Segment typeSHT_PROGBITSPermsEXEC。否则IDA不会把它当代码分析。

坑4:“GOT被改写了!怎么检测?”

→ 这是后门常用手法。在IDA中打开View → Open subviews → Exports,筛选所有got_开头的符号,右键 →Jump to definition,看其值是否异常(如指向.data.bss而非.text)。更进一步,用Search → All segments → Textshellcode/bin/sh等字符串,再逆向查谁在写GOT。


最后一句实在话

掌握PIC逆向,不是为了炫技,而是为了夺回对固件的解释权

当厂商说“我们的固件是安全的”,你能打开IDA,三分钟内定位到strcpy调用点,五分钟后确认它是否在拷贝用户可控的HTTP头;
当审计报告写着“未发现硬编码密钥”,你能顺着got_getenv一路追到config_buffer,再查它是否被memcpy复制进栈缓冲区;
当新爆出CVE-2023-XXXXX,你能从补丁diff反推原始漏洞点,在未更新的固件里精准定位同款缺陷。

这一切的前提,是你能看穿PIC的伪装,让IDA Pro成为你眼中的“X光机”。

如果你在分析某款具体固件时卡在某个PLT跳转或GOT解析上,欢迎把片段发出来——我们可以一起把它“点亮”。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/22 5:33:00

RS485和RS232区别总结之STM32多机通信实现示例

RS485与RS232不是“差不多”&#xff0c;而是根本不在同一张设计图纸上你有没有遇到过这样的现场&#xff1a;- 项目交付前一周&#xff0c;客户反馈“12台从机偶尔失联&#xff0c;重启主机就恢复”&#xff1b;- 示波器抓到总线波形毛刺严重&#xff0c;但换根线、换个电源又…

作者头像 李华
网站建设 2026/2/19 5:34:54

ARM平台裸机程序设计:从零实现简单应用

ARM裸机开发实战手记&#xff1a;从复位瞬间到LED闪烁的完整链路你有没有试过&#xff0c;在一个没有操作系统的芯片上&#xff0c;让第一盏LED亮起来&#xff1f;不是靠CubeMX自动生成的工程&#xff0c;也不是调用HAL库里的HAL_GPIO_TogglePin()——而是真正从CPU复位那一刻开…

作者头像 李华
网站建设 2026/2/22 17:03:14

组合逻辑电路设计系统学习:常见模块的逻辑结构与接口特性分析

组合逻辑电路设计的工程真相&#xff1a;从真值表到硅片&#xff0c;那些手册不会明说的关键细节 你有没有遇到过这样的场景&#xff1f; Verilog代码功能仿真100%通过&#xff0c;综合后网表也完全匹配预期&#xff0c;可一上板——UART中断偶尔丢包、地址译码信号在高速读写…

作者头像 李华
网站建设 2026/2/11 19:03:58

lite-avatar形象库一文详解:.png预览图生成逻辑与.zip权重文件结构解析

lite-avatar形象库一文详解&#xff1a;.png预览图生成逻辑与.zip权重文件结构解析 1. 什么是lite-avatar形象库 lite-avatar形象库是一个专为数字人对话系统设计的2D形象资产集合&#xff0c;它不是从零开始训练的模型&#xff0c;而是一套开箱即用的、经过充分验证的形象资…

作者头像 李华
网站建设 2026/2/24 7:21:31

Pspice安装教程:全面讲解软件依赖与运行环境配置

PSpice 安装不是点“下一步”&#xff1a;一场与Windows运行时契约的硬核对话 你有没有试过——双击 pspice.exe &#xff0c;光标转两圈&#xff0c;任务管理器里进程一闪而逝&#xff0c;桌面安静得像什么都没发生&#xff1f; 或者仿真跑完了&#xff0c;波形窗口打开却…

作者头像 李华
网站建设 2026/2/21 15:04:46

S32DS安装教程:新手入门必看的零基础指南

S32DS安装实战手记&#xff1a;一个功率电子工程师的第一次成功调试 你有没有过这样的经历&#xff1f; 凌晨两点&#xff0c;SiC半桥驱动板已经焊好&#xff0c;旋变传感器接线确认无误&#xff0c;示波器探头夹在FTM0_CH0上——但屏幕里只有平直的高电平。你反复检查 FTM0-…

作者头像 李华