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 命令行工具
每月 283 次下载
160KB
3.5K SLoC
at51
用于逆向工程 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
,可以是AlignedJump
、SquareChi
或KullbackLeibler
。
我通常不需要第二个或第三个选项(Kullback-Leibler或卡方),它们主要是因为我没有在早期实现第一个测试。
可以将输出作为gnuplot的输入,例如
at51 stat path/to/firmware | gnuplot -p -e "plot '-' with lines"
base
这个应用程序试图确定固件映像的加载地址(在最佳情况下,它只包含将设备上的实际固件)。它加载给定文件的第一个64k,并从0
到0x10000
的每个偏移量,确定有多少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