9 个版本 (3 个稳定版)

1.1.1 2024年7月29日
1.1.0 2024年2月29日
1.0.0 2021年8月21日
0.4.2 2021年6月6日
0.3.0 2019年10月18日

#18 in 命令行工具

Download history 1/week @ 2024-06-09 270/week @ 2024-07-28 9/week @ 2024-08-04 4/week @ 2024-08-11

每月 283 次下载

MIT 许可协议

160KB
3.5K SLoC

at51

Crates.io

用于逆向工程 8051 固件的一组应用。目前有四个应用

  • stat,提供关于给定文件操作码分布与正常 8051 代码相似性的分块统计信息
  • base,确定 8051 固件文件的加载地址
  • libfind,读取库文件并扫描固件文件中的这些文件的例程
  • kinit,读取由 C51 编译器生成的特定初始化数据结构

每个子命令的输出也可以通过 JSON 在其他程序中使用。

安装

可下载版本应在 GitHub 仓库的发布页上。

为了手动编译,只需要 cargo,它可以通过 rustup 安装。使用 cargo 可以使用以下命令安装:cargo install at51

或者,要从仓库源安装,执行以下操作

git clone 'https://github.com/8051Enthusiast/at51.git'
cargo install --path at51

stat

此子程序有助于确定文件中哪些区域可能是 8051。如果您想确定文件的一般架构,可能一个有用的工具是 cpu_rec

此子命令对固件进行一些统计。它将文件作为连续指令流来遍历,并对这些指令进行一些测试。图像被分成大小相等的块,并返回每个块(默认大小为 512)的测试值。这意味着它通常更适合更大的图像(在此上下文中,类似于 >4kB),其中您想了解哪些区域可能是 8051 代码,哪些是数据。

默认情况下,它计算对齐跳转测试,这给出了跳转目标不在指令开始处的相对跳转指令的百分比。其值在0到1之间,0表示更好,通常表现良好,但在由0和类似重复指令组成的流中有很多NaN,因为那些块中没有跳转。如果位置完全是8051代码,它应该有一个值为0(尽管有人可能通过不规则的跳转进行一些破解),但它可以包含小的跳转表,因此有时并不完全为零,但仍然应该相当低(小于0.1)。可以通过使用-n标志来显示使用的跳转数量,从而知道这个值有多确定。此外,还有两个其他标志-A-O,其中第一个标志将绝对跳转也包含在计算中(如果文件已经对齐并且跳转不足,则很有用),第二个标志将跳转到固件图像外的跳转视为缺失(如果知道没有代码在固件之外,且固件文件没有覆盖整个地址空间,则与-A一起使用很有用)。

它还可以对操作码分布进行块状Kullback-Leibler距离,这意味着每个块有一个从0到1的值,0表示最像8051。包含了一个从我做的语料库中得出的默认分布(由于版权问题,我可能无法发布),但你可以用-选项设置自己的语料库。使用这个度量标准,小于0.06通常意味着它是8051代码,0.06-0.12表示它可能既包含8051代码又包含一些数据(如跳转表)或不寻常(可能是一小部分指令重复很多次)。请注意,随机数据仅在约0.25左右,因此Kullback-Leibler可能不太可靠。

另一种方法是对方码分布进行卡方测试,它可以有大于1的值,并且其值没有限制。但缺点是,很难确定通常哪些范围是8051代码,因为这会随着块大小而变化。它用于比较不同块的8051程度,并且通常比Kullback-Leibler距离在该情况下更可靠。另外,请注意,我在统计学方面没有经验,所以我可能做错了。

可以在配置中设置当没有提供选项时使用的标准度量标准,该度量标准名为stat_mode,可以是AlignedJumpSquareChiKullbackLeibler

我通常不需要第二个或第三个选项(Kullback-Leibler或卡方),它们主要是因为我没有在早期实现第一个测试。

可以将输出作为gnuplot的输入,例如

at51 stat path/to/firmware | gnuplot -p -e "plot '-' with lines"

base

这个应用程序试图确定固件映像的加载地址(在最佳情况下,它只包含将设备上的实际固件)。它加载给定文件的第一个64k,并从00x10000的每个偏移量,确定有多少ljmp/lcall跳转直接跟在ret指令之后,因为新函数通常从这里开始。偏移量也可以在16位空间内循环解释(使用-),这意味着在偏移量0xffe0处,前0x20字节加载在0xffe0-0xffff处,其余的然后加载在地址空间的开头。输出的可能性是跳转和调用指向紧跟在ret之后的指令的数量,例如在这个例子中

Index by likeliness:
	1: 0x3fe0 with 218
	2: 0xc352 with 89
	3: 0xd096 with 87

在这里,最可能的加载位置是0x3fe0,因为它有218条ljmp/lcall指令,而第二和第三情况中只有89条或87条指令。在给出的示例中,这个特定的0x3fe0地址的加载位置是由于一个0x20字节的头部,而代码本身从0x4000开始。

通常,acall/ajmp会被忽略,因为这会引入大量非代码数据噪声(8051指令集的1/16是acall/ajmp),但可以通过-a标志启用。但请确保噪声/非8051部分的文件(如entrpoy和stat应用程序可检测的)被清零。

也可以使用多个固件映像,其中一个知道它们加载在相同的位置(对于存在不同修订的小映像很有用),在这种情况下,将计算每个偏移量上匹配指令的算术平均值。

libfind

此应用程序加载用户提供的某些库,并试图在固件中找到标准库函数。目前,支持C51(据我所知,大多数固件的编译器)的OMF-51库和sdcc的sdld库。

一般来说,库文件包含一些库函数的字节,然后是一些“修复”位置,这些位置在链接时会被更改,并且通常是跳转的目标。它们通常分为不同的段,每个段可以为自己定义公共符号,每个修复位置可以通过id或公共符号引用其他段。

对于每个段,通过将非修复位置的字节与固件中每个可能的位置进行比较,找到它的出现。然后通过跟随修复(可以通过读取固件中修复位置处的值来完成)来尝试验证它实际上是不是该段,并确定引用的段是否位于固件引用的目标处。

然后输出每个匹配段的公共符号,以及其位置和有时是描述。如果某些引用的段不存在,则将其放在方括号中以表示。另一方面,如果段被引用但实际上不存在,则将其放在括号中输出(这主要用于查找main,因为它不能包含在库中,但被引用)。如果有多个匹配的段,但其中一个匹配更好(没有任何>方括号>括号),则只输出匹配最佳的段。

为了说明这一点,考虑以下三个段

segment 0: 01 23 45 XX XX 67
           public symbol: "sym1"
           fixup XX XX: 16-bit absolute code reference to segment 1
segment 1: 89 AB CD EF
segment 2: 01 23 45 00 08
           public symbol: "sym2"

然后是代码

      0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F
0000: 02 25 54 01 23 45 00 12 67 52 36 14 46 39 45 23
0010: 00 00 89 AB CD EF 33 01 23 45 00 08 67 25 34 12

程序会搜索段,并会在0x03和0x17位置找到段0,在0x12位置找到段1,在0x17位置找到段2。然后它会验证所有段出现的修复

  • 位置0x03的段0在修复位置有00 12,解释为绝对16位地址指向0x0012,即段1的位置。因此它是有效的。
  • 位置0x17的段0指向0x0008,然而没有出现段,所以它被放在括号中。
  • 段1是有效的,但没有公共符号,因此没有输出。这通常是模块中的辅助段,输出它们不会真正提供任何见解。
  • 段2是有效的,并且有sym2作为公共符号。它覆盖了相同位置段0的出现,因为它没有有效的引用。

输出将是

Address | Name                 | Description
0x0003    sym1
0x0017    sym2

对于C51,相关的库文件格式为C51*.LIB(不是C[XHD]51*.LIB),目前只需在互联网上搜索即可找到(可能出现的名称之一是C51L.LIB),当然,您也可以尝试下载C51的试用版以获取库文件。

在搜索C51编译的固件中的函数时,经常会看到一个 [?C_START] 和一个 (MAIN)。这是因为编译器在main函数之前插入了一个名为 ?C_START 的函数,该函数从数据结构中加载变量,该数据结构可以通过 at51 kinit 读取。 ?C_START 用方括号括起来,因为它引用了 MAIN,这当然不是一个库函数,这也是为什么 (MAIN) 用括号括起来的原因。

对于sdcc,如果您的系统中安装了Linux sdcc,相关的库通常位于 /usr/share/sdcc/lib/{small,small-stack-auto,medium,large,huge}/。请注意,sdcc库可能存在噪声,因为库文件中的修复位置没有指定目标是在代码、imem等地址空间。

建议在使用前将文件与加载地址对齐,因为否则绝对位置可能无法验证。长度小于4字节的段不会输出,因为它们提供了很多噪声,实际上并没有添加任何信息。

如果没有给出其他参数,可以在配置中使用字段 "libraries" 指定要使用的库列表,该字段包含一个库路径列表。

示例(在某些随机的WiFi固件上)

使用 at51 libfind some_random_firmware /path/to/lib/dir/

Address | Name                 | Description
0x4220    ?C?CLDOPTR             char (8-bit) load from general pointer with offset
0x424d    ?C?CSTPTR              char (8-bit) store to general pointer
0x425f    ?C?CSTOPTR             char (8-bit) store to general pointer with offset
0x4281    ?C?IILDX              
0x4297    ?C?ILDPTR              int (16-bit) load from general pointer
0x42c2    ?C?ILDOPTR             int (16-bit) load from general pointer with offset
0x42fa    ?C?ISTPTR              int (16-bit) store to general pointer
0x4319    ?C?ISTOPTR             int (16-bit) store to general pointer with offset
0x4346    ?C?LOR                 long (32-bit) bitwise or
0x4353    ?C?LLDXDATA            long (32-bit) load from xdata
0x435f    ?C?OFFXADD            
0x436b    ?C?PLDXDATA            general pointer load from xdata
0x4374    ?C?PLDIXDATA           general pointer post-increment load from xdata
0x438b    ?C?PSTXDATA            general pointer store to xdata
0x4394    ?C?CCASE              
0x43ba    ?C?ICASE              
0x46f5    [?C_START]            
0x50e1    (MAIN)                

对于一些通用的符号名称,有可用的描述。

kinit

此应用程序非常特定于C51生成的代码,它解码用于启动时初始化内存值的特定数据结构。该结构由 ?C_START 程序读取,因此通常可以通过运行libfind并查看 ?C_START 开始后的两个字节来找到该结构的位置(因为它以 mov dptr, #structure_address 开始)。当 (?C_START) 在括号内时,这可能不是这种情况,因为 ?C_START 在keil库的位置0处被引用,这个位置恰好是大多数8051固件开始的指令,即使没有 ?C_START 函数。

示例

使用 at51 kinit -o offset some_random_firmware

bit 29.6 = 0
idata[0x5a] = 0x00
xdata[0x681] = 0x00
xdata[0x67c] = 0x00
xdata[0x692] = 0x00
xdata[0x6aa] = 0x01
xdata[0x46f] = 0x00
bit 27.2 = 0
bit 27.0 = 0
bit 26.3 = 0
bit 26.1 = 0
xdata[0x47d] = 0x00
xdata[0x40c] = 0x00
bit 25.3 = 0
xdata[0x46d] = 0x00
idata[0x5c] = 0x00
xdata[0x403..0x40a] = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
xdata[0x467] = 0x00

配置

可以在以下位置创建一个(基本的)JSON格式的配置文件:$CONFIG_PATH/at51/config.json,其中 $CONFIG_PATH 取决于操作系统。以下路径通常使用

  • ~/.config(Linux)
  • ~/Library/Preferences(macOS)
  • ~/AppData/Roaming(Windows)

示例配置

{
	"libraries": [
    "/usr/share/sdcc/lib/small",
    "/usr/share/sdcc/lib/medium",
    "/usr/share/sdcc/lib/large",
    "/usr/share/sdcc/lib/huge",
    "/opt/C51/LIB"
  ],
	"stat_mode": "AlignedJump"
}

依赖项

~6-16MB
~172K SLoC