用 IDA Pro 挖穿固件:从二进制到漏洞的实战之路
你有没有试过打开一个路由器的固件,发现里面全是sub_804123a这种函数名?没有源码、没有文档、甚至连架构都搞不清——这几乎是每个做嵌入式安全的人必经的“地狱开局”。但正是在这种混沌中,IDA Pro成了我们最锋利的探针。
今天不讲虚的,咱们就从一次真实的固件逆向出发,聊聊怎么用 IDA Pro 把一段黑盒二进制“扒”出漏洞来。不是教科书式的流程复述,而是像老手带徒弟一样,一步步告诉你:看到什么、想到什么、下一步该做什么。
为什么是 IDA Pro?
先说句实话:现在开源工具越来越多,Ghidra 免费、radare2 脚本强、angr 能自动跑路径……那为啥很多专业团队还是首选 IDA?
因为它够“聪明”,也够“听话”。
- 智能分析能力强:它不仅能反汇编,还能猜函数边界、识别库函数、建调用图。
- 交互体验极佳:你可以随时重命名变量、加注释、定义结构体,整个过程就像在读一份不断完善的代码笔记。
- 生态成熟:Hex-Rays 反编译器 + FLIRT 签名库 + IDAPython 脚本支持,让它既能手动精修,也能批量处理。
特别是面对那些没有符号表、压缩混淆过的固件时,IDA 的综合能力几乎是不可替代的。
固件分析第一步:别急着打开 IDA
很多人一拿到.bin文件就想直接拖进 IDA,结果加载完一片红——指令解析失败,控制流乱成麻。问题出在哪?你还不知道它是谁。
真正的起点,其实是这三个问题:
- 这是什么格式?
- 运行在什么 CPU 上?
- 代码从哪开始执行?
第一步:拆包提取关键文件
大多数固件镜像是个“大杂烩”:Bootloader、内核、根文件系统全打包在一起。我们要做的第一件事,就是把它拆开。
常用命令:
binwalk -e firmware.binbinwalk会扫描文件中的特征签名,识别出 squashfs、gzip、JFFS2 等常见文件系统,并自动提取出来。解压后进入_firmware.extracted目录,找到/bin、/sbin下的可执行程序,比如httpd、busybox或某个叫web_server的神秘二进制。
这时候可以用file命令看看它的身份:
file web_server # 输出示例:ELF 32-bit LSB executable, MIPS, version 1 (SYSV), statically linked看到了吗?MIPS 架构、小端序、静态链接。这些信息决定了你在 IDA 里该怎么设置。
⚠️ 小贴士:如果
file识别不出来,可能是裸二进制(raw binary)。这时需要结合设备型号查芯片手册,或者用strings找点线索,比如"U-Boot"、"Linux version"这类字符串往往能暴露平台信息。
加载进 IDA:选对参数比什么都重要
打开 IDA Pro,选择“New”,然后导入你的目标文件。
关键来了:Processor type怎么选?
根据前面的信息,我们选MIPS->Little-endian。如果是 ARM926EJ-S 就选 ARM,STM32 就选 Cortex-M 系列。错一步,后面全废。
接下来是加载地址(Loading segment)。
- 如果是 ELF 文件,IDA 通常能自动解析 Program Header,给出正确的基址(如
0x400000)。 - 如果是 raw bin,就得自己推断了。
怎么推?
方法一:看是否有标准入口点模式。例如 MIPS 平台常把代码映射到0x80000000或0x400000开始的位置。
方法二:用readelf -l查看 ELF 头部的 LOAD 段起始地址。
方法三:不确定时可以先按默认加载,再通过交叉引用和字符串定位主逻辑,后期修正基址。
建议:加载后立刻按Shift+F12打开字符串窗口,搜一下"password"、"login"、"admin"这类关键词。如果能看到大量可读字符串,说明架构和地址大概率是对的。
让 IDA “看懂”代码:自动化分析之后的手工补完
IDA 加载完成后会自动运行一轮分析(Auto-analysis),完成以下工作:
- 反汇编所有可识别的代码段
- 标记函数入口
- 提取字符串及其引用位置
- 恢复部分标准库函数名称(靠 FLIRT)
但别指望它全搞定。尤其是厂商删了符号表的固件,满屏都是sub_函数,怎么办?
用 FLIRT 快速识别标准库函数
IDA 自带的FLIRT(Fast Library Identification and Recognition Technology)技术,能通过函数指令特征匹配已知库函数。比如你看到一段代码调用了strcpy,虽然没符号,但指令模式和标准 libc 中的一模一样,IDA 就能自动标出来。
操作很简单:确保你在加载时勾选了“Use FLIRT signatures”。
效果立竿见影——原本叫sub_40c120的函数,突然变成了strcpy。
✅ 实战价值:一旦识别出
strcpy、sprintf、memcpy等高危函数,你就知道哪里可能有缓冲区溢出了。
用 IDAPython 自动扫描危险调用
光靠肉眼找太慢。我们可以写个脚本,让 IDA 主动帮我们挖雷。
# ida_find_dangerous.py import idautils import idaapi import idc DANGEROUS = [ "strcpy", "strcat", "sprintf", "vsprintf", "gets", "scanf", "realpath", "system", "memcpy", "memmove" ] def scan_risky_calls(): print("[*] 正在扫描高风险函数调用...") found = 0 for name in DANGEROUS: ea = idaapi.get_name_ea(0, name) if ea == idc.BADADDR: continue # 未找到该函数 for ref in idautils.CodeRefsTo(ea, 0): func = idaapi.get_func(ref) caller_name = idc.get_func_name(func.start_ea) if func else "unknown" print(f"[!] {name} 被 {caller_name} 在 0x{ref:X} 调用") found += 1 # 可选:自动添加注释 idc.set_cmt(ref, f"DANGEROUS CALL to {name}", 0) print(f"[*] 完成,共发现 {found} 处高风险调用") scan_risky_calls()把这个脚本保存为.py文件,在 IDA 的 Script command 窗口运行,几秒钟就能列出所有潜在风险点。
💡 经验提示:重点关注那些处理用户输入的地方,比如 CGI 接口、配置解析函数、网络数据包处理等模块。如果它们调用了
strcpy,基本就可以标记为“重点怀疑对象”。
实战案例:一个路由器登录接口的栈溢出
假设我们在字符串窗口看到这么一行:
Checking user '%s', password '%s'顺藤摸瓜,双击跳转到引用处,发现它在一个名为handle_login_request的函数里被调用(可能是 IDA 已经帮你重命名了,也可能你还得自己猜)。
深入进去,看到类似这样的逻辑:
lw $t9, (strcpy - 0x8000)(gp) addiu $a0, $s1, 0x10 # dst = g_user_buffer addiu $a1, $s0, 0 # src = user_input jalr $t9 # strcpy(g_user_buffer, user_input)注意这里的g_user_buffer是全局变量,偏移是s1+0x10。右键点击s1,查看其赋值来源,发现它指向.bss段的一个固定地址,比如0x412000。
接着去 Data -> Jump to Offset 输入0x412000,查看这块内存的定义。IDA 可能显示为:
bss_412000: .space 32也就是说,目标缓冲区只有32 字节,而输入来自 HTTP 请求参数,完全可控且无长度检查。
💥 漏洞确认:典型的栈(或堆)溢出条件已满足。
下一步怎么做?验证是否可利用。
动态调试:让漏洞“活”起来
静态分析只能告诉你“可能有问题”,动态调试才能证明“真的能打”。
我们可以借助 QEMU 用户态模拟 + GDB + IDA 远程调试组合拳。
步骤如下:
使用
qemu-mipsel-static启动目标程序:bash qemu-mipsel-static -g 1234 ./web_server-g 1234表示开启 GDB Stub,监听 1234 端口。在 IDA 中选择 Debugger → Attach → Remote GDB debugger
Host: localhost, Port: 1234成功连接后,就可以下断点、单步执行、查看寄存器和内存变化。
回到刚才的strcpy调用地,设个断点,构造一个超长用户名发送过去:
import requests url = "http://192.168.1.1/login.cgi" data = {"username": "A" * 100, "password": "test"} requests.post(url, data=data)断点命中后观察栈布局。你会发现$sp指向的栈帧上布满了'A',继续往下走,程序崩溃,EIP 被覆盖成0x41414141—— 控制流劫持成功!
此时打开 IDA 的“Structures”视图,结合反编译窗口(Hex-Rays),开始寻找 ROP gadget,构建 exploit 链……
但这已经超出本文范围了。重点是:你已经用 IDA 把漏洞从静态字节变成了可触发的现实威胁。
遇到难题怎么办?三个常见坑与应对策略
坑1:全是sub_xxxxxx,根本看不懂谁是谁
解决办法:
- 用调用关系推理功能:频繁调用
socket/bind/listen/recv的函数很可能是网络服务主线程; - 看参数传递习惯:MIPS 中前四个参数放
$a0-$a3,ARM 放r0-r3,观察这些寄存器的使用模式可以帮助判断函数用途; - 比对相似固件:用 BinDiff 插件对比两个版本的固件,把已有分析成果迁移到新版本中。
坑2:程序被压缩或加密,IDA 解不出来
典型表现:一大段.data区域全是乱码,函数稀少,字符串极少。
应对方式:
- 用
binwalk -A firmware.bin检测是否存在 LZMA、gzip 等压缩节; - 使用
dd+gunzip单独解压后再分析; - 若为运行时解密,可在真实设备上抓内存 dump,定位解密后的代码段重新载入。
坑3:不是 Linux,是裸机或 RTOS
有些工业设备跑的是 FreeRTOS、uC/OS,甚至自研调度器,系统调用方式完全不同。
对策:
- 明确 ABI 规则(参数传递、栈平衡、中断处理);
- 手动定义系统调用表(Syscall Table);
- 从复位向量(Reset Vector)开始追踪启动流程,定位 main 或 task_create 类函数。
高阶玩法:把经验变成生产力
当你做过十几个项目后,就会意识到:重复劳动是最浪费时间的。
所以聪明人都会做这几件事:
1. 创建专属 FLIRT 签名
针对某厂商私有库函数,提取其特征生成.sig文件,以后只要遇到同系列设备,IDA 就能自动识别内部函数。
工具链:sigmake+ids文件。
2. 写通用分析脚本
除了检测危险函数,还可以写脚本:
- 自动提取所有 CGI 接口函数
- 构建函数调用图(Call Graph)
- 标记所有外部输入源(如
recv,fopen,getenv)
IDAPython 结合networkx甚至能生成可视化图表。
3. 和其他工具联动形成闭环
- 用 Ghidra 做初步探索(免费 + 自动化好)
- 用 radare2 写自动化流水线(CI/CD 集成)
- 用 angr 符号执行辅助路径探索(自动找可达的危险函数)
IDA 不一定每步都上,但在深度分析阶段,它依然是无可争议的王者。
最后几句掏心窝的话
掌握 IDA Pro,本质上不是学会点菜单、写脚本,而是培养一种逆向思维:
- 看到一段汇编,你能想象它对应的 C 代码长什么样;
- 看到一个指针操作,你能预判它的内存布局;
- 看到一个函数调用,你能推测它上游的数据来自哪里。
这才是真正的“漏洞猎人”素质。
当然也要提醒一句:只对你拥有或获得授权的设备进行分析。技术再酷,也不能越界。
如果你正在入门嵌入式安全,不妨现在就下载一个公开固件,试着用 IDA 打开它。哪怕只是找到一个硬编码密码,也是迈出的第一步。
毕竟,每一个漏洞的背后,都藏着一段等待被读懂的机器语言。而 IDA Pro,就是我们与二进制对话的语言翻译器。
想试试吗?评论区留下你第一次在 IDA 里发现漏洞的经历吧。