固件逆向:从一坨二进制,到一个可以被解释的系统

一份面向实际分析的固件逆向指南。

重点不在"认识多少工具",而在于如何建立从实物设备、存储介质、固件镜像、文件系统、可执行程序、数据流到真实行为的完整证据链。

目录


很多人第一次接触固件逆向,会自然地把它理解成:

拿到一个 .bin,用 Binwalk 解包,再把可执行文件扔进 Ghidra。

这当然是常见流程,但它只描述了动作,没有描述真正的问题。

固件逆向真正要做的事情是:

把一个不透明的二进制对象,逐步还原成一个能够被解释、验证和推理的系统。

你最终想回答的,通常不是"这个文件里有什么",而是这些问题:

  • 设备启动时到底做了什么?
  • 哪段代码决定某个功能是否开启?
  • 某个标识、配置、密钥或令牌从哪里来?
  • 数据经过了哪些函数、算法和协议?
  • 一个安全假设到底成立在数学层、实现层,还是只存在于文档里?
  • 某个行为是固件自身完成的,还是由安全芯片、外部 MCU、云端或工厂参数参与完成的?

因此,固件逆向最重要的能力不是"会用工具",而是知道下一步应该证明什么

本文不围绕某个品牌或设备,也不把固件逆向包装成神秘的"黑客术"。它讨论的是一套通用方法:如何从真实设备或未知二进制出发,把软件从硬件里拿出来,恢复结构,定位关键逻辑,并用静态与动态证据完成闭环。

本文默认分析对象是你自己拥有、公开提供或已获得授权研究的固件。


一、先定一个锚点:你究竟在逆向什么

固件不是一个统一格式。

"固件"只是一个非常宽泛的名字。它可能是一份升级包,也可能是一整块 Flash 的原始镜像;可能装着完整 Linux 系统,也可能只是几十 KB 的裸机代码。

你拿到的两个文件虽然都叫 firmware.bin,内部结构可能完全不同。

一个典型 Linux 嵌入式固件可能是:

text 复制代码
firmware.bin
├── vendor header
├── bootloader
├── kernel
├── device tree
├── root filesystem
├── configuration
└── checksum / signature

一个 MCU 固件则可能只是:

text 复制代码
firmware.bin
├── vector table
├── executable code
├── read-only data
├── initial values
└── padding

还有一种更常见的现实情况:

text 复制代码
upgrade.pkg
└── encrypted container
    └── compressed image
        └── signed partition table
            ├── bootloader
            ├── kernel
            ├── rootfs
            └── application

所以第一条原则是:

不要一上来就"解包"。先确认你手里究竟是什么。

这一步看似基础,却决定后面几乎所有判断。


二、固件逆向的核心不是解包,而是建立证据链

我更愿意把完整逆向过程写成下面这样:

text 复制代码
实物设备 / 官方升级包 / 已有镜像
   ↓
识别硬件、芯片、接口与存储介质
   ↓
选择最小侵入的提取路径
   ↓
获得原始字节并校验完整性
   ↓
识别容器、分区与文件系统
   ↓
恢复 CPU 架构与运行环境
   ↓
定位关键程序、配置和数据
   ↓
建立调用关系与数据流
   ↓
提出假设
   ↓
动态验证
   ↓
得到能够复现的结论

真正成熟的分析,不是:

"我在这个文件里看到一个字符串,所以它一定是密码。"

而是:

"这个字符串被函数 A 引用;函数 A 在启动阶段由 B 调用;参数来自配置分区;最终进入认证函数 C。通过运行时跟踪,可以确认它确实参与了认证计算。"

两者区别非常大。

前者是线索。

后者才是证据。

因此整篇文章只有三个不可放弃的锚点:

  1. 先确认对象结构,再深入代码。
  2. 任何结论都尽量形成"来源 → 处理 → 使用"的数据链。
  3. 静态分析提出假设,动态分析负责验证;不要把猜测当结论。

后面所有工具,都只服务于这三个位置。


三、从硬件到固件:先把软件从设备里拿出来

前面的流程默认了一个很容易被忽略的前提:

你手里已经有固件文件。

可现实里经常不是这样。

你面对的可能只是一台真实设备、一块 PCB、一颗看不懂型号的芯片,甚至只是一个能正常工作的黑盒。设备可以启动、联网、执行功能,但你并没有:

text 复制代码
firmware.bin
update.img
rootfs.squashfs

这时固件逆向的第一步,不是 Binwalk,也不是 Ghidra。

而是:

先确定软件住在哪里,再想办法把它完整、可靠地拿出来。

真正完整的链路应该是:

text 复制代码
实物设备
   ↓
识别主控、存储和调试接口
   ↓
判断软件存放在哪里
   ↓
选择最小侵入的提取方式
   ↓
获得原始镜像
   ↓
确认镜像是否完整、是否加密、是否包含多个分区
   ↓
进入软件逆向

这一步既是硬件分析,也是固件分析的入口。


3.1 先看板子:不要急着焊线

拿到设备以后,第一件事不是立即接编程器。

先观察。

你需要建立一张最初的硬件地图:

text 复制代码
PCB
├── 主 SoC / CPU / MCU
├── Flash / EEPROM / eMMC / NAND
├── RAM
├── 电源管理
├── 无线芯片
├── 安全芯片
├── 其他 MCU / DSP
├── UART 测试点
├── JTAG / SWD 测试点
└── 其他未知接口

重点看:

  • 芯片丝印;
  • 封装类型;
  • PCB 上的测试焊盘;
  • 排针;
  • 未焊接接口;
  • 丝印文字,例如 TXRXGNDJTAGSWD
  • 存储芯片附近的走线关系。

很多时候,一张清晰的 PCB 照片加芯片型号搜索,就已经能回答几个关键问题:

text 复制代码
CPU 是什么架构?
系统可能跑 Linux 还是裸机?
外部 Flash 容量多大?
是否有独立安全芯片?
是否存在第二颗 MCU?

这些信息会直接改变后续路径。

例如:

text 复制代码
Linux SoC + SPI NOR + eMMC

和:

text 复制代码
Cortex-M MCU + 内部 Flash

不是同一种分析对象。

前者可能有:

text 复制代码
Bootloader + Kernel + RootFS + App

后者可能只是:

text 复制代码
Vector Table + Code + Data

所以不要把"拆机"理解成物理动作。

它真正的意义是:

恢复系统边界。


3.2 固件通常住在哪里

设备中的软件可能存在多个位置。

最常见的是:

外部 SPI NOR Flash

常见于:

  • 路由器;
  • IoT;
  • 各类嵌入式 Linux 设备;
  • 启动固件;
  • 小型系统。

特点通常是:

text 复制代码
容量较小
8 引脚封装常见
读取相对直接
可能包含 Bootloader / Kernel / RootFS

但不要看到 8 引脚芯片就直接认定是 Flash。

先确认型号和数据手册。


NAND Flash

常见于容量较大的嵌入式设备。

相比 NOR,它更复杂,因为原始数据可能涉及:

text 复制代码
Page
Block
OOB
ECC
Bad Block

这意味着:

读出整个芯片的原始字节,不等于已经得到可直接解包的固件镜像。

你可能还需要处理:

  • 页结构;
  • 坏块;
  • ECC;
  • OOB;
  • UBI / UBIFS。

eMMC

很多 Linux 设备和消费电子会使用 eMMC。

它通常不只是一个"固件文件",而更像一块完整磁盘:

text 复制代码
eMMC
├── boot0
├── boot1
├── RPMB
└── user area
    ├── partition 1
    ├── partition 2
    ├── rootfs
    ├── data
    └── ...

因此分析目标可能不是一个 .bin,而是一整张块设备镜像。


MCU 内部 Flash

有些微控制器根本没有外部存储芯片。

程序直接烧录在芯片内部。

这时提取路径可能依赖:

  • SWD;
  • JTAG;
  • Bootloader;
  • 厂商下载协议;
  • 调试器。

但如果启用了读保护,能否读取取决于具体芯片、安全配置和保护等级。


EEPROM / NVRAM / OTP

这里不一定存放完整程序,但经常保存:

  • 设备序列号;
  • 校准数据;
  • 工厂参数;
  • 密钥材料;
  • 配置;
  • 身份信息。

所以一个很重要的判断是:

"主固件在哪里"和"秘密在哪里"可能是两个不同问题。

你可能已经完整拿到了主 Flash,却仍然找不到某个关键值,因为它根本存放在另一颗芯片里。


安全芯片或安全区域

有些系统会把秘密放在:

  • Secure Element;
  • TPM;
  • TrustZone 环境;
  • SoC eFuse;
  • OTP;
  • Secure Enclave;
  • 受保护的 Key Storage。

这时主固件可能只包含:

text 复制代码
调用接口
算法流程
句柄
密钥编号

却没有真正密钥。

如果逆向到最后发现:

text 复制代码
主 CPU 只向安全组件提交请求
安全组件内部完成敏感运算

那说明你的分析已经找到了真正的安全边界。

这不是"没找到"。

而是:

答案不在主固件里。


3.3 提取固件,不是只有"拆芯片"这一条路

一个常见误区是:

从硬件拿固件,就要拆机、夹芯片、上编程器。

实际上成熟的顺序应该反过来:

优先选择信息损失最少、侵入程度最低、可重复性最高的方法。

通常可以按下面的顺序尝试。


路径一:官方升级包

这是最便宜的入口。

来源可能包括:

  • 厂商官网;
  • 设备 Web 管理页面;
  • App 下载缓存;
  • OTA 更新;
  • 本地升级文件。

优点:

text 复制代码
容易获取
不会损坏设备
便于重复分析
通常已经包含主要软件

缺点:

text 复制代码
可能只是增量包
可能经过加密
可能有签名
可能缺少工厂分区
可能不含 Bootloader

所以官方升级包适合回答:

厂商准备更新什么?

却不一定能回答:

设备上完整存在什么?


路径二:从正在运行的系统里导出

如果设备已经提供:

  • Shell;
  • SSH;
  • Telnet;
  • 串口控制台;
  • 调试终端;
  • 已授权维护接口;

那么可以直接从运行系统观察:

text 复制代码
/proc/mtd
/proc/partitions
/sys/
lsblk
mount
df

这些信息可能告诉你:

text 复制代码
分区叫什么
大小是多少
挂载在哪里
底层设备是什么

在有合法权限的研究环境中,可以从块设备或 MTD 设备读取镜像。

这条路径的优势很大:

你不是猜分区结构,而是让正在运行的系统直接告诉你。

但也有局限:

  • 某些区域无法从用户态访问;
  • 安全分区可能受保护;
  • 运行时读取可能得到逻辑视图,而不是原始物理布局;
  • 某些内容会在启动时解密,磁盘上和内存中的形态不同。

路径三:UART 串口

UART 往往是硬件逆向最值得先检查的接口之一。

它可能提供:

text 复制代码
Bootloader 日志
Kernel 日志
启动参数
分区信息
芯片型号
错误信息
Shell
调试菜单
工厂命令

即使没有 Shell,启动日志也可能直接暴露:

text 复制代码
CPU
内存大小
Flash 类型
MTD 分区
Kernel command line
RootFS 类型
启动失败位置

这会极大缩小分析范围。

但 UART 不是"看到三个焊盘就直接接"。

至少要先确认:

  • GND;
  • TX;
  • RX;
  • 电平;
  • 波特率。

最重要的是电平。

不要默认所有设备都是 3.3V。

可能存在:

text 复制代码
1.8V
3.3V
5V
其他电平

错误电平不仅会通信失败,还可能损坏设备。

因此合理顺序是:

text 复制代码
识别 GND
   ↓
测量空闲电压
   ↓
判断可能电平
   ↓
只监听 TX
   ↓
确认波特率和日志
   ↓
必要时再连接 RX

先听,再说。

这是一个很好的硬件分析原则。


路径四:JTAG / SWD

JTAG 和 SWD 可能提供更深层的调试能力:

text 复制代码
暂停 CPU
读取寄存器
查看内存
设置断点
读取 Flash
单步执行

它们尤其常见于:

  • MCU;
  • 嵌入式开发板;
  • 调试阶段设备;
  • 工厂测试环境。

但能否真正使用,取决于:

text 复制代码
接口是否暴露
引脚是否确认
调试是否禁用
芯片是否启用读保护
安全启动状态
调试认证机制

所以不要把 JTAG 当成:

插上就能把固件导出来。

它只是一个潜在入口。


路径五:直接读取外部存储芯片

如果设备使用:

  • SPI NOR;
  • EEPROM;
  • NAND;
  • eMMC;

可以考虑直接读取存储。

这里又分两种方式。

在板读取(in-circuit)

芯片不拆下,直接在 PCB 上连接。

优点:

text 复制代码
风险较低
操作方便
不用热风拆焊

问题是:

text 复制代码
主控可能同时驱动总线
其他器件会影响信号
供电路径复杂
读取结果可能不稳定

所以读完以后不要只看"工具提示成功"。

至少应:

text 复制代码
读取多次
   ↓
分别计算哈希
   ↓
比较结果

例如:

text 复制代码
dump1.bin
dump2.bin
dump3.bin

如果三个 SHA-256 不一致,就还不能把任何一个当成可靠原始镜像。


离板读取(off-board)

把芯片拆下后读取。

优点:

text 复制代码
电气环境更可控
不受板上其他器件干扰

缺点:

text 复制代码
操作风险更高
可能损伤焊盘或芯片
复原成本更高

因此它不应该是第一选择。

合理原则是:

能不拆就不拆;能通过软件拿到,就不要先动热风枪。


路径六:Chip-off 是最后手段,不是仪式感

真正困难的设备可能最终需要:

text 复制代码
拆芯片
直接读取
重建数据结构

但"拆下来读"解决的只是物理访问问题。

后面仍然可能面对:

  • 加密;
  • ECC;
  • OOB;
  • 坏块;
  • 分区表;
  • Secure Boot;
  • 设备唯一密钥;
  • 多芯片协作。

因此:

拿到原始字节,不等于拿到明文固件。

这是硬件到软件之间最重要的一道认知边界。


3.4 一台设备里可能有不止一份固件

很多真实设备不是:

text 复制代码
一颗 CPU
+
一份 firmware.bin

而更像:

text 复制代码
主 SoC
├── Linux 固件
├── Bootloader
└── RootFS

控制 MCU
└── MCU Firmware

无线芯片
└── Radio Firmware

安全芯片
└── Internal Applet / Secure Storage

甚至还有:

  • 显示控制器;
  • 电机控制器;
  • FPGA;
  • DSP;
  • 基带;
  • 独立网络模块。

因此,当你研究某个功能时,不应该只问:

主固件里有没有这个函数?

而应该问:

到底是哪颗处理器真正执行了这个功能?

假设流程是:

text 复制代码
主 CPU
   ↓
发送命令
   ↓
串口 / SPI / I²C
   ↓
副 MCU
   ↓
执行真正动作

你在主固件里可能只能看到:

c 复制代码
send_command(0x37, data);

真正算法在另一颗芯片里。

这也是为什么硬件地图很重要。

没有系统边界,就容易在错误的二进制里找答案。


3.5 拿到文件以后,先判断你拿到的是哪一层

假设你成功读出:

text 复制代码
dump.bin

先不要庆祝。

它可能是:

text 复制代码
完整芯片镜像
单一分区
升级容器
压缩包
加密区
逻辑文件系统
原始 NAND 页数据
某个 MCU 的代码区

你需要继续回答:

text 复制代码
起始地址是什么?
总长度是否合理?
是否完整?
是否重复读取一致?
有没有 ECC / OOB?
有没有多个分区?
是否存在全 0xFF 或全 0x00 填充区?
有没有可识别文件头?
是否可能经过压缩或加密?

这一步决定了你下一步应该:

text 复制代码
直接解包
手工切片
处理 NAND
恢复分区表
寻找解密流程
还是重新提取

所以真正的链条不是:

text 复制代码
硬件
 ↓
固件
 ↓
Ghidra

而是:

text 复制代码
硬件
 ↓
识别系统
 ↓
选择入口
 ↓
读取原始数据
 ↓
验证读取可靠性
 ↓
恢复数据结构
 ↓
确认目标二进制
 ↓
逆向

3.6 一个实用的提取决策树

面对未知设备,可以按这个顺序思考:

text 复制代码
能否获得官方升级包?
├── 能 → 先下载、校验、分析
└── 不能
     ↓
设备是否提供合法维护接口或 Shell?
├── 是 → 先获取分区和运行环境信息
└── 否
     ↓
PCB 上是否存在 UART?
├── 是 → 先监听启动日志
└── 否
     ↓
是否存在 JTAG / SWD 或已知调试接口?
├── 是 → 判断保护状态和可读范围
└── 否
     ↓
是否存在外部 Flash / eMMC / NAND?
├── 是 → 优先考虑非侵入或在板读取
└── 否
     ↓
判断是否为 MCU 内部 Flash、受保护存储或其他封装

这个顺序背后的原则很简单:

先拿最便宜的证据,再逐步增加侵入性。

不要一开始就做成本最高、风险最大的事情。


3.7 提取阶段真正要保存什么

从硬件拿固件,不应该只留下一个 dump.bin

建议至少记录:

text 复制代码
设备型号
硬件版本
PCB 版本
芯片型号
芯片丝印照片
接口位置
电压
连接方式
读取工具
读取参数
原始 dump
多次 dump 的哈希
提取日期
是否修改过设备

如果是多芯片设备,可以这样组织:

text 复制代码
case/
├── photos/
├── board-map/
├── datasheets/
├── dumps/
│   ├── spi-nor/
│   ├── emmc/
│   └── mcu/
├── hashes/
├── extracted/
├── notes/
└── analysis/

原因很现实。

几个月以后,你很可能已经忘了:

这个 final_dump_v2_ok.bin 到底是从哪颗芯片、用什么参数、在什么状态下读出来的?

而逆向最怕的,就是证据来源断掉。


四、拿到固件后,不要急着打开 Ghidra

4.1 保存原始样本

原始样本永远不要直接修改。

建议至少保留:

text 复制代码
case/
├── original/
│   └── firmware.bin
├── working/
├── extracted/
├── notes/
└── hashes.txt

计算哈希:

bash 复制代码
sha256sum firmware.bin

作用并不复杂:确保后来分析的仍是同一个样本。

当你开始切片、解包、修补文件头、修改校验和之后,如果没有原始镜像和哈希,很容易失去基准。


4.2 先看最便宜的信息

第一轮分析追求的不是深入,而是快速建立轮廓。

bash 复制代码
file firmware.bin
bash 复制代码
xxd -l 256 firmware.bin
bash 复制代码
strings -a -n 6 firmware.bin | head

你可能立即看到:

  • 文件系统名字;
  • Linux 版本;
  • U-Boot 标识;
  • 编译器信息;
  • 芯片型号;
  • URL;
  • 错误日志;
  • 配置路径;
  • 函数名;
  • 压缩格式;
  • 厂商自定义头部。

这一步的价值不在于"找到秘密",而在于建立最初的地图。

例如:

text 复制代码
Squashfs filesystem
Linux version 5.x
BusyBox
/etc/init.d/
ARM

这已经足够形成一个初步判断:

text 复制代码
这是一个 Linux 类嵌入式固件
    ↓
存在 SquashFS 根文件系统
    ↓
大概率能恢复 shell 脚本、配置和 ELF 程序
    ↓
后续重点应放在启动链、服务、配置和关键二进制

这比盲目地把整个固件扔进反编译器有效得多。


五、识别固件结构:先问"边界在哪里"

固件解析的第一个真正难点,是找到边界。

你需要回答:

  • 哪一段是头部?
  • 哪一段是压缩数据?
  • 哪一段是内核?
  • 哪一段是文件系统?
  • 是否存在多个分区?
  • 有没有加密区?
  • 有没有签名或校验字段?

常用工具之一是 Binwalk。它的核心作用不是"神奇解包一切",而是根据文件特征识别嵌入的数据、压缩流、文件系统以及其他结构。

基础扫描:

bash 复制代码
binwalk firmware.bin

自动提取:

bash 复制代码
binwalk -e firmware.bin

递归提取:

bash 复制代码
binwalk -Me firmware.bin

但要注意:

Binwalk 的输出是候选解释,不是真理。

一个字节序列偶然匹配 magic signature,并不代表那里一定存在真正的文件。

因此看到:

text 复制代码
123456  Squashfs filesystem

你还应该继续验证:

  • 偏移是否合理?
  • 长度是否越界?
  • 是否能成功挂载或提取?
  • 文件系统内部是否具有一致结构?
  • 前后数据是否符合预期?

必要时手工切片:

bash 复制代码
dd if=firmware.bin of=part.bin bs=1 skip=123456

逆向中很重要的一种习惯是:

工具负责提出候选解释,人负责判断解释是否成立。


六、熵:判断"看不懂"到底是哪一种看不懂

某一段数据高度随机,并不自动等于"被加密"。

它可能是:

  • 加密数据;
  • 高压缩率数据;
  • 已压缩文件系统;
  • 伪随机填充;
  • 厂商自定义编码;
  • 真正的随机数据。

这时可以看熵(Entropy)

直观理解:

text 复制代码
规律很多 → 熵较低
随机性强 → 熵较高

Binwalk 可以辅助观察熵变化。

真正有价值的是边界:

text 复制代码
低熵区
   ↓
突然进入高熵区
   ↓
长时间保持
   ↓
再次回到低熵区

这通常说明固件内部存在不同性质的数据区域。

但熵只能告诉你:

"这一段看起来很随机。"

它不能直接告诉你:

"这一段是 AES-256-CBC 加密。"

因此一个更合理的判断链是:

text 复制代码
高熵
 ↓
排除已知压缩格式
 ↓
检查文件头和容器结构
 ↓
寻找解密函数、密钥来源或升级流程
 ↓
动态验证

不要看到高熵就写"固件被加密"。

这是固件分析里非常典型的过度结论。


七、解包之后,真正的分析才开始

很多教程把"成功解出 SquashFS"当作重大突破。

实际上,对于 Linux 类固件,这往往只是拿到了目录。

比如:

text 复制代码
squashfs-root/
├── bin/
├── etc/
├── lib/
├── sbin/
├── usr/
├── var/
└── www/

接下来不要漫无目的地翻文件。

应该先建立系统地图。


7.1 先找启动链

重点看:

text 复制代码
/etc/inittab
/etc/init.d/
/etc/rc*
/etc/systemd/
/etc/config/

你真正想知道的是:

text 复制代码
设备上电
   ↓
谁成为第一个用户态进程
   ↓
启动哪些脚本
   ↓
脚本启动哪些服务
   ↓
服务读取哪些配置
   ↓
关键功能在哪个程序里

举例:

text 复制代码
/etc/init.d/S50service
    ↓
/usr/bin/device_service
    ↓
读取 /etc/device.conf
    ↓
访问 /dev/xxx
    ↓
监听 TCP 端口

到这里,你已经从"一堆文件"得到了一条行为链。


7.2 文件系统里最值得先看的位置

第一遍通常优先检查:

text 复制代码
/etc/
/etc/init.d/
/etc/config/
/usr/bin/
/usr/sbin/
/bin/
/sbin/
/lib/
/www/
/var/

重点搜索:

text 复制代码
password
passwd
secret
token
apikey
private
certificate
key
debug
factory
serial
license
encrypt
decrypt
auth

例如:

bash 复制代码
grep -RniE 'password|secret|token|apikey|private|encrypt|decrypt' squashfs-root/

但仍然要记住:

搜到敏感字符串,不等于发现漏洞。

你必须判断它是什么:

  • 示例配置?
  • 默认值?
  • 哈希?
  • 公钥?
  • 证书?
  • 测试数据?
  • 真正的共享秘密?
  • 是否在运行时被使用?

一个公开证书和一个私钥,安全意义完全不同。

一个默认密码和一个从未被启用的测试字符串,也完全不同。


八、先识别架构,否则反汇编全是噪声

拿到一个可执行文件后,不要急着导入 Ghidra。

先确认:

  • 文件格式;
  • 32 位还是 64 位;
  • CPU 架构;
  • 大端还是小端;
  • 是否动态链接;
  • 是否剥离符号;
  • 依赖哪些库。

例如:

bash 复制代码
file ./usr/bin/device_service
bash 复制代码
readelf -h ./usr/bin/device_service
bash 复制代码
readelf -d ./usr/bin/device_service

常见架构包括:

  • ARM / AArch64
  • MIPS / MIPS64
  • x86 / x86_64
  • PowerPC
  • RISC-V
  • Xtensa

嵌入式领域还有大量 MCU 架构和 DSP 架构。

对于 ELF 文件,通常比较容易识别。

真正麻烦的是裸二进制:

text 复制代码
firmware.bin

它没有:

  • ELF header;
  • section table;
  • symbol table;
  • 明确加载地址。

这时候需要从其他证据推断:

  • 芯片型号;
  • 向量表;
  • 指令特征;
  • 字节序;
  • 地址常量;
  • 固件升级文档;
  • PCB 上的 SoC 丝印;
  • SDK 或开源代码。

一个错误的架构选择,会让反汇编器生成大量"看起来像代码"的垃圾。

所以遇到完全不可读的反汇编时,先别怀疑自己。

先怀疑:

架构、字节序、加载地址是不是错了?


九、Ghidra 不是答案生成器,它是推理环境

Ghidra 能做反汇编、反编译、交叉引用、控制流分析和脚本自动化。

但反编译出来的 C 伪代码,不是原始源码。

比如:

c 复制代码
undefined4 FUN_001024ac(int param_1, char *param_2)

这里的信息很少。

逆向的工作,是逐步把它改造成:

c 复制代码
int verify_license(DeviceContext *ctx, const char *license)

这不是"改个名字好看"。

这是在建立认知模型。

一个有效的 Ghidra 分析过程通常是:

text 复制代码
发现字符串
   ↓
查看 XREF
   ↓
进入引用函数
   ↓
理解参数
   ↓
重命名函数
   ↓
重命名变量
   ↓
定义结构体
   ↓
追踪上游调用者
   ↓
追踪下游数据流

例如你发现字符串:

text 复制代码
authentication failed

先看谁引用它。

然后看到:

c 复制代码
if (result != 0) {
    log("authentication failed");
}

继续追踪 result

c 复制代码
result = FUN_00108210(ctx, input);

进入函数,发现:

c 复制代码
hash(input, temp);
return compare(temp, stored_value);

再继续追踪 stored_value 从哪里来。

最后可能得到:

text 复制代码
配置分区
   ↓
读取设备参数
   ↓
解码
   ↓
存入内存结构体
   ↓
认证函数读取
   ↓
参与比较

这才是完整的逆向。


十、不要从入口函数往下硬啃,要从"锚点"反向追踪

大型程序可能有几千甚至几万个函数。

main() 开始逐行看,往往效率极低。

更好的方式是寻找锚点。

常见锚点包括:

  • 错误字符串;
  • 日志文本;
  • 文件路径;
  • URL;
  • 命令名称;
  • 协议字段;
  • JSON 键名;
  • 算法常量;
  • 设备节点;
  • 系统调用;
  • 导入函数;
  • 已知库函数。

比如你关心配置加载。

搜索:

text 复制代码
/etc/device.conf

然后:

text 复制代码
字符串
 ↓
XREF
 ↓
open()
 ↓
read()
 ↓
parse_config()
 ↓
数据写入结构体
 ↓
其他函数读取结构体

你关心网络认证:

text 复制代码
"Authorization"
"token"
"login"
"/api/auth"

然后从这些位置逆推。

你关心密码实现:

text 复制代码
AES
SHA
HMAC
EVP_
mbedtls_
wolfSSL

或者识别算法特征常量和 S-Box。

真正高效的逆向往往不是顺着程序读,而是:

先找到和问题最相关的可见痕迹,再沿交叉引用向上下游扩展。


十一、秘密分析:不要问"有没有密钥",要问"秘密从哪里来"

这是固件安全分析最容易产生误解的地方。

很多人认为:

text 复制代码
找到密钥 = 破解

有时候成立,有时候完全不成立。

先区分几种情况。


11.1 固件中存在硬编码秘密

例如:

c 复制代码
static const uint8_t key[] = {
    0x91, 0x27, 0x3a, ...
};

如果这段数据确实参与敏感运算,那么需要继续确认:

  • 是否所有设备共用?
  • 是否只用于测试?
  • 是否用于解密本地数据?
  • 是否用于网络认证?
  • 是否有设备级派生过程?
  • 是否可以远程利用?
  • 是否只是公开材料,例如公钥?

真正的问题不是"字符串像不像密钥",而是:

它具有什么安全权力。


11.2 固件里只有算法,没有秘密

例如:

text 复制代码
ciphertext
   ↓
AES-256
   ↓
key 从安全芯片读取

你可以完全恢复算法,却仍然拿不到密钥。

这不叫失败。

因为你已经明确了真正边界:

text 复制代码
固件不是秘密来源
安全边界在安全芯片

接下来的研究对象已经发生变化。


11.3 密钥由设备信息派生

例如概念上:

text 复制代码
device_id
   +
factory_secret
   +
KDF
   ↓
device_key

这时需要继续追踪:

  • device_id 从哪里来?
  • factory_secret 在哪里?
  • KDF 是什么?
  • 每台设备是否不同?
  • 是否存在固定盐或固定种子?

注意:知道派生算法,不一定意味着能恢复密钥。

关键仍然是:

输入中是否包含真正未知且无法读取的秘密。


11.4 密钥在运行时出现

有些秘密不静态存在于固件镜像中,但运行时必须进入内存。

例如:

text 复制代码
安全存储
   ↓
启动时解封
   ↓
进入 RAM
   ↓
用于解密
   ↓
清理

静态搜索找不到,不代表不存在。

这也是动态分析、调试和内存观察的价值所在。


十二、区分编码、混淆、压缩、加密和哈希

很多误判来自把这五件事混在一起。

编码

目的是表示数据。

例如:

text 复制代码
Base64
Hex
TLV
ASN.1

通常可逆,不需要秘密。


混淆

目的是增加理解成本。

例如:

text 复制代码
XOR
字符串拆分
控制流打乱
变量重命名

不一定提供真正密码学安全。


压缩

目的是减少体积。

例如:

text 复制代码
gzip
xz
LZMA
zstd

高压缩数据通常也具有较高熵。


加密

目的是在没有密钥时隐藏内容。

例如:

text 复制代码
AES
ChaCha20

核心是密钥。


哈希

目的是把任意输入映射到固定长度摘要,通常设计为单向。

例如:

text 复制代码
SHA-256
SHA-3

不能因为看到 32 字节数据,就断定它是"256 位密钥"。

它也可能是:

  • SHA-256 摘要;
  • 随机 ID;
  • ECC 参数;
  • 校验值;
  • 数据结构的一部分。

逆向里最危险的错误之一,是根据长度直接给数据贴标签。


十三、动态分析:让程序自己暴露答案

静态分析回答的是:

代码可能做什么。

动态分析回答的是:

这一次运行时,它实际做了什么。

两者缺一不可。

常见动态观察对象:

  • 函数调用;
  • 文件访问;
  • 网络连接;
  • 系统调用;
  • 参数;
  • 返回值;
  • 内存;
  • 设备节点;
  • 日志;
  • IPC;
  • 环境变量。

Linux 用户态程序在兼容条件下,可以借助 QEMU 用户模式运行其他 CPU 架构的程序。

概念上:

text 复制代码
x86_64 主机
   ↓
qemu-arm / qemu-mips
   ↓
ARM 或 MIPS 用户态程序

完整系统仿真则尝试恢复:

text 复制代码
CPU
+
内存
+
内核
+
根文件系统
+
部分硬件设备

但这里必须保持现实感:

不是所有固件都能直接 QEMU 一键启动。

常见障碍:

  • 自定义内核;
  • 私有驱动;
  • 未支持的 SoC;
  • 特殊外设;
  • watchdog;
  • 安全芯片;
  • 硬件寄存器;
  • 厂商私有设备节点。

因此动态分析通常有三个层次。


13.1 单程序运行

只运行一个目标二进制。

适合:

  • 解析器;
  • 网络服务;
  • 算法模块;
  • 数据处理程序。

13.2 部分环境模拟

提供:

  • rootfs;
  • 动态库;
  • 配置;
  • 必要设备节点的替代实现。

目标不是恢复整个设备,而是让关键服务运行起来。


13.3 完整系统仿真

模拟内核和系统环境。

适合更复杂的行为验证,但成本也最高。

一个重要原则是:

不要为了"完整仿真"而完整仿真。只模拟足以回答当前问题的部分。

如果你只想知道一个配置解析函数如何工作,让目标进程跑起来就够了。

没必要先重建整个硬件世界。


十四、当软件分析撞上硬件边界:别在固件里找不存在的答案

前面已经讲过如何通过 UART、JTAG、SWD 和存储芯片把固件拿出来。

这里讨论的是另一个问题:

当你已经拿到主固件,为什么有些答案还是找不到?

原因往往不是逆向能力不够,而是问题跨出了主 CPU 的软件边界。

现实设备可能是:

text 复制代码
主 SoC
   ↓
驱动 / IPC / 总线
   ↓
副 MCU
   ↓
安全芯片
   ↓
传感器 / 执行器

你在 Linux 主固件里看到的,也许只是:

c 复制代码
send_command(0x42, payload);

真正的判断、算法或秘密可能在另一颗芯片里。

因此,当某条数据流突然"消失"时,不要无限追问不存在的函数。

先问:

text 复制代码
它是否通过 /dev 节点离开用户态?
是否进入 ioctl?
是否通过 I²C / SPI / UART 发给其他芯片?
是否调用 TEE / Secure Element 接口?
是否读取 eFuse / OTP / 独立 EEPROM?

一个典型边界可能是:

text 复制代码
Application
   ↓
Library
   ↓
ioctl()
   ↓
Kernel Driver
   ↓
SPI
   ↓
Secure MCU

如果你只分析 Application,自然找不到最终实现。


UART 在这里的价值:观察另一端说了什么

UART 不只用于提取启动日志。

它也可能是两个组件之间的运行时通信链路。

你需要关注:

text 复制代码
谁发起通信?
消息有没有固定帧头?
长度字段在哪里?
有没有校验?
有没有请求/响应关系?
某个用户动作对应哪条消息?

这里的目标已经不是"拿固件"。

而是:

恢复组件之间的协议。


JTAG / SWD 在这里的价值:观察活着的状态

如果目标 MCU 可调试,JTAG 或 SWD 的价值可能是:

text 复制代码
暂停在关键函数
观察寄存器
观察 RAM
确认某个分支
确认某个值何时出现

也就是说,它们从"提取入口"变成了"动态证据来源"。


安全芯片:有时真正的秘密从未进入主固件

某些系统的设计就是:

text 复制代码
主 CPU 提交数据
   ↓
安全芯片内部计算
   ↓
只返回结果

主 CPU 可能永远拿不到真正密钥。

这时即使你完整逆向:

text 复制代码
Bootloader
Kernel
RootFS
Application

也不会找到目标秘密。

真正的结论应该是:

主固件只持有调用逻辑,不持有密钥本体;安全边界位于独立安全组件。

这是一个有效结论,而不是分析失败。


什么时候应该停止在主固件里继续找

可以用三个问题判断:

  1. 继续分析更深的主固件代码,会不会改变可见的数据流?
  2. 关键数据是否已经明确跨过了硬件接口?
  3. 真正决定结果的组件是否已经变成另一颗芯片?

如果答案明确,那么应该停止。

继续在错误边界里"深挖",只会增加噪声。

这也是系统逆向很重要的一条原则:

先确认计算发生在哪里,再研究计算如何发生。

十五、协议分析:程序不是孤岛

一个设备的关键行为可能跨越多个组件:

text 复制代码
Host software
   ↓
Network / USB / Serial
   ↓
Main processor
   ↓
Secondary MCU
   ↓
Secure element

如果只逆向主处理器固件,可能永远找不到答案。

所以需要问:

  • 数据从哪里进入?
  • 谁处理第一层协议?
  • 谁做验证?
  • 谁保存秘密?
  • 谁最终执行动作?

例如:

text 复制代码
输入数据
   ↓
网络服务 A
   ↓
IPC
   ↓
守护进程 B
   ↓
驱动
   ↓
MCU

你在 A 中看到的可能只是转发逻辑。

真正算法在 B。

真正秘密又在 MCU。

因此固件逆向最终经常变成:

系统逆向。

固件只是入口。


十六、一个更有效的分析框架:四张图

面对复杂固件,我建议至少维护四张图。


16.1 结构图

回答:

固件由什么组成?

text 复制代码
Firmware
├── Header
├── Bootloader
├── Kernel
├── RootFS
├── App
└── Data

16.2 启动图

回答:

系统如何从上电走到业务功能?

text 复制代码
Boot ROM
 ↓
Bootloader
 ↓
Kernel
 ↓
init
 ↓
startup scripts
 ↓
services

16.3 数据流图

回答:

某个关键数据从哪里来,经过什么处理,去了哪里?

text 复制代码
Input
 ↓
Parser
 ↓
Decoder
 ↓
Validator
 ↓
Storage / Action

16.4 信任边界图

回答:

系统究竟相信谁?

text 复制代码
External input
   ↓
[validation boundary]
   ↓
Main firmware
   ↓
[privileged boundary]
   ↓
Secure component

很多安全问题,本质上都发生在边界处:

  • 不该信的数据被信了;
  • 不该共享的秘密被共享;
  • 不该由普通组件承担的安全职责,被普通组件承担;
  • 文档声称存在边界,实际代码没有。

十七、常见突破口:别只盯着"漏洞"

固件分析并不等于 CVE 搜索。

真正有价值的突破口通常包括下面几类。


17.1 默认配置

例如:

  • 默认凭据;
  • 调试模式;
  • 测试接口;
  • 宽松权限;
  • 不安全服务。

17.2 硬编码秘密

重点不是"看见 key 字样",而是确认:

text 复制代码
秘密
 ↓
由谁读取
 ↓
参与什么运算
 ↓
影响什么安全边界

17.3 不安全更新链

要问:

  • 是否验证完整性?
  • 是否验证签名?
  • 谁持有验证材料?
  • 是否允许降级?
  • 是否绑定设备型号?
  • bootloader 是否参与验证?

这里需要区分:

text 复制代码
checksum ≠ cryptographic signature

CRC 能发现偶然损坏,不等于能抵抗恶意修改。


17.4 调试和工厂逻辑

生产固件中可能残留:

  • factory mode;
  • engineering mode;
  • hidden menu;
  • debug commands;
  • test accounts;
  • diagnostic services。

但同样需要验证它们是否真正可达。

一个函数存在,不代表正常运行路径会调用它。


17.5 第三方组件

固件中经常包含:

  • BusyBox;
  • OpenSSL;
  • mbedTLS;
  • Web server;
  • 数据库;
  • 压缩库;
  • 多媒体库。

识别版本可以帮助判断已知风险,但不要简单地做:

text 复制代码
版本号匹配 CVE
        =
设备一定可利用

真正需要确认:

  • 代码是否真的存在?
  • vulnerable function 是否编译进去?
  • 运行时是否可达?
  • 配置是否满足触发条件?
  • 厂商是否 backport 了补丁?

版本匹配只是线索。


十八、工具链:工具应该按问题分组,而不是按名气排序

固件结构识别

常用:

text 复制代码
file
xxd
hexdump
strings
binwalk
7z
dd

目标:

确认文件结构、边界、压缩格式和嵌入对象。


文件系统处理

常见对象:

text 复制代码
SquashFS
UBIFS
JFFS2
CramFS
ext*
FAT

工具依文件系统而定,例如:

text 复制代码
unsquashfs
ubireader
jefferson
mount

目标:

恢复尽可能接近设备运行时的目录结构。


二进制基础分析

text 复制代码
file
readelf
objdump
nm
strings
ldd(仅在适用环境)

目标:

识别架构、ABI、依赖、符号和程序基本属性。


逆向工程

常用:

text 复制代码
Ghidra
IDA
Binary Ninja
radare2 / Cutter

目标:

恢复函数、控制流、调用关系、数据引用和程序语义。

对于多数个人研究者,Ghidra 已经足够作为主力工具。


动态运行与仿真

text 复制代码
QEMU
gdb / gdb-multiarch
strace
ltrace
chroot
容器化辅助环境

目标:

验证程序实际行为,而不是继续猜。


自动化分析

常见项目包括:

text 复制代码
EMBA
FACT

它们可以帮助自动完成部分:

  • 固件提取;
  • 静态扫描;
  • 组件识别;
  • 漏洞线索整理;
  • SBOM;
  • 报告生成。

但自动化系统最适合做:

扩大搜索面。

它不能替代:

确认某个具体结论是否成立。


十九、一次完整分析应该长什么样

假设你拿到一个未知固件:

text 复制代码
update.bin

正确流程不应该是:

text 复制代码
看到设备
   ↓
随便找一个 .bin
   ↓
binwalk -Me
   ↓
grep password
   ↓
打开 Ghidra

而更接近下面这样。


第零步:确认软件从哪里来

先回答:

text 复制代码
我面对的是实物设备、官方升级包,还是已有镜像?
主程序在哪颗芯片?
是否还有副 MCU、安全芯片或独立存储?
最小侵入的提取路径是什么?

能从官方升级包开始,就不要先拆芯片。

能从运行系统获得分区信息,就不要先猜。

如果必须从硬件读取,则保留:

text 复制代码
芯片型号
连接方式
电压
读取参数
多次 dump
哈希

第一步:确认样本

记录:

text 复制代码
文件名
来源
大小
SHA-256
获取日期
对应设备/版本

第二步:建立外层结构

回答:

text 复制代码
是否存在厂商头?
是否压缩?
是否加密?
是否签名?
有几个分区?
每段偏移是多少?

形成:

text 复制代码
0x000000  Header
0x001000  Bootloader
0x080000  Kernel
0x400000  RootFS
0xF00000  Data

这一步的产物不是截图。

是地图。


第三步:恢复文件系统

识别:

text 复制代码
init system
startup scripts
services
configuration
web interface
executables
libraries
certificates

形成第一版组件清单。


第四步:定义问题

不要说:

我要全面逆向整个固件。

这通常意味着没有目标。

应该说:

我要确认设备身份参数从哪里读取。

或者:

我要确定升级包是否经过签名验证。

或者:

我要知道认证结果由哪个模块决定。

目标越明确,逆向越快。


第五步:从可见锚点进入

例如:

text 复制代码
字符串
文件路径
日志
API 路由
配置字段
设备节点
导入函数
算法特征

建立 XREF。


第六步:恢复数据流

例如:

text 复制代码
/etc/device.conf
   ↓
load_config()
   ↓
decode_value()
   ↓
DeviceContext.secret
   ↓
verify_request()

到这里,结论已经越来越接近系统真实逻辑。


第七步:动态验证

确认:

  • 文件是否真的被读取;
  • 函数是否真的被执行;
  • 参数是否符合静态判断;
  • 关键数据是否真的参与运算;
  • 不同输入是否产生预期差异。

第八步:写结论

一个好的逆向结论应该能够回答:

text 复制代码
发现了什么?
证据是什么?
影响范围是什么?
哪些部分已经确认?
哪些仍然只是推测?
如何复现?

例如:

程序 service_a 在启动时读取 /etc/device.conf 中的字段 device_token。该值经函数 decode_token() 处理后写入全局上下文,并在 verify_request() 中参与 HMAC 比较。静态交叉引用和运行时文件访问记录均支持这一判断。

这比:

"固件里发现了 token,可能存在硬编码密钥。"

强得多。


二十、最容易浪费时间的五种方式

1. 从 main 开始逐行看

大型二进制里极低效。

先找与问题有关的字符串、路径、API、函数和数据。


2. 迷信自动解包

自动提取失败,不代表固件无法分析。

可能只是:

  • 私有文件头;
  • 错误长度;
  • 轻量混淆;
  • 多层容器;
  • 自定义校验。

3. 看到高熵就认定加密

高熵也可能是压缩。

必须继续验证。


4. 看到敏感字符串就认定漏洞

字符串只是线索。

必须证明它被谁使用、如何使用、有什么权限。


5. 追求"完全理解整个固件"

多数时候没必要。

真正有效的方法是:

text 复制代码
明确问题
   ↓
找到锚点
   ↓
恢复相关数据流
   ↓
验证
   ↓
停止

当更深层分析不会改变当前结论和行动空间时,就应该停止。

逆向不是考古竞赛。

目标不是把每个函数都改名。


二十一、学习路线:不要按工具学,要按能力学

第一阶段:看懂系统

先掌握:

  • Linux 基础;
  • 文件系统;
  • 进程;
  • 权限;
  • 启动流程;
  • 网络基础;
  • Shell;
  • C 语言。

你要能回答:

text 复制代码
一个 Linux 程序如何启动?
如何加载动态库?
如何读取配置?
如何访问设备?
如何监听网络?

第二阶段:看懂二进制

掌握:

  • CPU 基础;
  • 寄存器;
  • 调用约定;
  • 栈;
  • 堆;
  • ELF;
  • 汇编;
  • 反编译。

不需要一开始就成为汇编专家。

先做到:

能把反编译代码和底层指令对应起来,知道反编译器什么时候可能骗你。


第三阶段:掌握固件结构

学习:

  • Bootloader;
  • Kernel;
  • Device Tree;
  • RootFS;
  • Flash;
  • 分区;
  • 固件升级。

你会逐渐发现:

"固件逆向"其实不是单纯的软件逆向,而是计算机系统知识的交叉应用。


第四阶段:动态验证

学习:

  • QEMU;
  • GDB;
  • strace;
  • 调试;
  • 仿真;
  • 环境修复。

这一步会让你从:

我觉得代码是这样运行。

进入:

我已经看到它确实这样运行。


第五阶段:建立自己的证据习惯

最终真正拉开差距的,不是工具。

而是记录。

建议每个项目至少维护:

text 复制代码
目标
已知事实
当前假设
支持证据
反对证据
待验证项
最终结论

不要只记命令。

要记:

为什么执行这条命令,以及结果改变了什么判断。


二十二、最后:固件逆向真正训练的是什么

表面看,固件逆向是在做:

text 复制代码
解包
反汇编
反编译
调试

但真正训练的是另一种能力:

面对一个完全不透明的系统,不靠文档承诺,而靠证据逐步恢复它真实的结构和行为。

一个成熟的逆向者看到未知固件,不会立即问:

用什么工具打开?

而会先问:

text 复制代码
我现在已经知道什么?
什么仍然未知?
哪个未知量最关键?
我能从哪里得到最便宜的证据?
什么结果可以证伪当前假设?

这也是为什么固件逆向既不是纯粹的"破解",也不是单纯的工具操作。

它更像一种技术侦查:

text 复制代码
从痕迹建立结构
从结构寻找路径
从路径提出假设
从行为验证结论

最后得到的不是"我看懂了一些代码"。

而是:

我知道这个系统为什么会这样工作,也知道我的结论依赖哪些证据。

这才是固件逆向真正值得学习的地方。


附录 A:一份实战检查表

硬件与提取

  • 记录设备型号、硬件版本和 PCB 版本
  • 拍摄主板与芯片丝印
  • 识别主 SoC / MCU
  • 识别外部 Flash / NAND / eMMC / EEPROM
  • 检查是否存在副 MCU 或安全芯片
  • 检查 UART / JTAG / SWD 测试点
  • 优先寻找官方升级包
  • 判断是否能从运行系统获得分区信息
  • 选择最小侵入的提取方式
  • 多次读取并比较哈希
  • 区分完整芯片镜像、单分区、升级包和逻辑文件系统
  • 记录提取工具、电压、连接方式和参数

样本

  • 保存原始文件
  • 计算 SHA-256
  • 记录来源、版本、设备信息
  • 建立 working copy

结构

  • file
  • 查看文件头
  • strings
  • Binwalk 扫描
  • 检查熵
  • 确认分区边界
  • 验证提取结果

文件系统

  • 确认文件系统类型
  • 恢复目录
  • 检查启动脚本
  • 检查配置
  • 检查网络服务
  • 检查 Web 资源
  • 检查证书与密钥材料
  • 建立可执行文件清单

二进制

  • 确认 CPU 架构
  • 确认字节序
  • 确认 ELF / raw binary
  • 查看动态依赖
  • 查看字符串
  • 选择关键程序
  • 建立 XREF
  • 恢复函数和变量语义
  • 绘制调用链
  • 绘制数据流

动态分析

  • 明确要验证的假设
  • 判断单程序运行是否足够
  • 判断是否需要部分环境
  • 判断是否需要完整系统仿真
  • 记录文件访问
  • 记录网络行为
  • 记录系统调用
  • 观察关键参数
  • 对比静态结论

输出

  • 区分事实与推测
  • 每个重要结论有证据
  • 说明影响范围
  • 说明未解决问题
  • 给出复现路径
  • 保留分析日志和哈希

附录 B:参考工具与资料

以下项目适合继续深入,具体命令与版本应以各项目当前官方文档为准。


这篇文章只给出了一条主线:从二进制恢复系统,从系统追踪数据,从数据验证行为。

工具会变,文件格式会变,芯片会变,但这条主线通常不会变。

相关推荐
2501_941982053 小时前
企业微信二次开发:如何用 Python 搭建通用的 Webhook 实时消息接收
开发语言·python·企业微信
在世修行3 小时前
第24篇:PyInstaller打包实战 — 从Python脚本到Windows EXE
人工智能·python·pyinstaller
华研前沿标杆游学3 小时前
2026年制造企业标杆参访实操指南:3步选对适配参访路线
python
Wang ruoxi3 小时前
Pygame 小游戏——一笔画挑战
python·pygame
萧青山4 小时前
AI阅读增强套件:用苏格拉底诘问+对抗性阅读+知识图谱构建深度阅读技能套件(Python实现)
人工智能·python·知识图谱·ai阅读增强
Java面试题总结4 小时前
Python,单引号和双引号有何区别
开发语言·python
lhxcc_fly4 小时前
LangGraph基础知识点
python·langchain·llm·langgraph
Lvan的前端笔记4 小时前
python:Mac 系统 uv 完整安装+入门实战
python·macos·uv
会飞锦鲤4 小时前
基于YOLOv10的瓜果成熟度智能检测系统
人工智能·python·深度学习·yolo·flask