Skip to content
KitploitKITPLOIT
工具博客
Log in
提交
工具博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

订阅源联系隐私© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
Philips-PM-5139-5138A-5136-Firmware-Project — 飞利浦 PM5139 / PM5138A / PM5136 函数发生器的固件逆向工程:将 8051 仿真器用作测量仪器、35 个章节的硬件文档,以及修正后的固件 V2.0 | Kitploit
工具/GitHubGitHub/doctormord/philips-pm-5139-5138a-5136-firmware-project
嵌入式系统安全静态分析动态分析 (沙盒)逆向工程硬件安全二进制分析论文与研究学习与教育固件分析

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享
GitHubdoctormord/philips-pm-5139-5138a-5136-firmware-project

Philips-PM-5139-5138A-5136-Firmware-Project

飞利浦 PM5139 / PM5138A / PM5136 函数发生器的固件逆向工程:将 8051 仿真器用作测量仪器、35 个章节的硬件文档,以及修正后的固件 V2.0

查看仓库
47421天前尚未审核

Philips PM5139 — 固件逆向工程

一台约 1994 年的 20 MHz 函数发生器,在软件层面被拆解:两份 EPROM 转储、一个被当作测量仪器使用的 8051 模拟器,以及 35 节文档,其中每一条论断都有清单地址、模拟器测量结果或原理图作为依据。

最终成果是一个 固件 V2.0,修复了 Philips 出厂时的一个缺陷, 六个我们自己的任意波形,以及一个逐条指令运行原始 ROM 的浏览器模拟器。

V1.3 ROM 中的所有波形表

程序 EPROM 中的每一个波形表,直接从二进制中绘制出来。右下角那个 开启了本项目最有趣的部分。


目录

  • 这是什么
  • 结果一览
  • 仪器
  • 方法:模拟器即测量仪器
  • 走到这里的路
  • 精彩之处
  • 固件 V2.0 — 新在哪里
  • 彩蛋
  • 然后发现它竟然是复音的
  • 六个我们自己的任意波形
  • 浏览器模拟器
  • 仓库结构
  • 使用工具
  • 复现一切
  • 刷写回去
  • 这有多可靠?
  • 仍未解决
  • 来源

这是什么

Philips PM5139 是三仪器系列(PM5136 / PM5138A / PM5139)中的 20 MHz 顶级型号。 内部有一块 PCB80C652 —— 带硬件 I²C 的 8051 内核 —— 一片 27512 程序 EPROM, 以及挂在串行总线上的六个模拟组件。

没有 PM5139 的维修手册。自 2010 年以来人们一直在论坛上寻找它。 存在的是其 10 MHz 姊妹型号 PM5138A 的手册,内部几乎完全相同。

所以本项目从另一端开始:转储 EPROM,弄清代码在做什么, 直到对仪器理解得足够透彻,能够修改它。

有两个固件版本可用,V1.3 和 V1.5,都是 64 KiB 的 M27512 转储。


结果一览

反汇编两个版本均完整,约 23 000 行,带交叉引用
注释清单147 个命名例程,145 条头部注释,3 826 行注释
文档35 节,4 600 行,每条论断均有出处
信号路径频率、幅度、偏移、AM、FM、突发、对称性、扫描 —— 全部计算并与原始代码核对
硬件全部 10 个选通、C 总线、与每个参与者的 I²C、端口、键盘、旋钮、显示位图
状态位128 个中有 75 个具有已记录的效果
版本差异V1.3 与 V1.5 结构上 91.4 % 相同;每处改动均已命名
模拟器一个 Python 版,一个 JavaScript 版(约 8 M 指令/秒),外加一个单文件浏览器模拟器
我们自己的固件V2.0 —— 修复了一个出厂缺陷,处理了校验和,在模拟器和真实硬件上验证通过

仪器

位置类型功能
D301PCB80C652带硬件 I²C 的 8051 内核,12 MHz
D30627512程序 EPROM —— V1.3 占用 0000h–AC70h
D310X28C64MOVX 总线上的任意 EEPROM
D305PCF8570I²C 上 256 字节电池后备 NVRAM(A0h)
D304-APCF8576I²C 上的 LCD 驱动器(70h),20 字节缓冲
D302-ASAA3007键盘编码器,单线上脉冲宽度编码
D30774HCT4514选通解码器 —— 选通编号即地址位 A8…A11

模拟侧是一条串行 C 总线:8051 的 UART 运行在移位寄存器模式, TXD 是时钟,RXD 是数据,一个选通决定十个移位寄存器中哪一个锁存字节。 MOV DPH,#8nh 后跟 MOVX @DPTR,A 触发选通 n。 这一行就是整个模拟部分的关键。


方法:模拟器即测量仪器

这是值得你为自己的项目借鉴的部分。

用肉眼阅读 44 KB 的 8051 二进制文件大概只能让你走完三分之一。 其余的一切都来自运行原始代码并观察会掉出什么:```python

What formula turns the entered amplitude into the byte on the bus?

Don't read the routine. Call it.

c = CPU(rom) for w in test_values: set_amplitude(c, w) c.call(0x0AAC) # the original routine, untouched print(w, c.ram[0x1C]) # the byte that goes out on STR9

改变输入,读取输出,对照假设进行验证。这对频率、幅度、偏移、AM 深度、FM 偏差、突发计数、对称性以及两种扫描特性都有效。文档中的每个公式都附带了其验证所用的采样点。

三项改进使其真正富有成效:

**观察总线,而非显示器。** 第 15 节测量了状态位对显示缓冲区的影响,128 位中有 74 位似乎不起作用。但其中许多并不驱动显示器,它们驱动的是*模拟组件*——而这些只能作为 C 总线上的电报可见。记录 `MOV SBUF,…` 和终止的 `MOVX @DPTR` 将已记录位的数量从 54 提升到了 75。

**按键,而非直接写 RAM。** 手动设置 RAM 字节会产生仪器从未进入的状态。这让我们付出了两个错误发现和一次撞入命令表的代价。通过模拟的 SAA3007 注入真实键码,可以得到固件实际到达的状态——正是对所有 256 个键码的暴力扫描揭示了哪个键触发哪个处理程序。

**首先怀疑你自己的模拟器。** 我们核心中的三个 bug 产生了“无法解释的”固件行为:`ACALL` 被执行为 `AJMP`,缺少辅助进位标志(因此 `DA A` 行为异常,固件看起来像是在用二进制计数),以及键盘中断被加倍。那个时期的所有发现之后都重新进行了测量。

---

## 走到这里的路

**先静态。** 一个带有完整操作码表的反汇编器,然后使用跳转表启发式进行递归下降。这产生了 30 508 字节的代码,并留下 13 637 字节无法解释。

**然后动态。** 一次跟踪运行——冷启动、全部 23 个前面板按键、旋钮的两个方向、每种操作模式、8600 万周期——标记每个实际执行的地址。与静态分析对照,它恰好发现了下降分析遗漏的**一个**区域,而 10 686 个无法解释的字节中有 5 个已知表块。

**然后是原理图。** 服务手册的 OCR 对原理图毫无用处,但 400 dpi 的页面图像非常出色。切成重叠的图块后,它们可读至引脚编号。六张图纸就是这样读出的——而在五条平行走线相距 90 像素的地方,目测被一个从位图中提取线段的脚本(`lines.py`)所取代。

**然后是两个被拔出的芯片。** 一个标有“SINUS 1.1”的 27C64 和一个 X28C64 被读出。两者都被放入原理图中,其内容被解码。

**然后是版本差异。** 对两个 ROM 进行标记化(使用相对跳转距离而非绝对目标),并对它们运行 `SequenceMatcher`,得到一个能在代码移动后仍然存活的地址映射——这就是 V1.3 符号被带到 V1.5 上的方式。

---

## 精彩之处

### Philips 交付了一个有噪声的波形

三个内置任意曲线位于 `A047h`、`A447h` 和 `A847h`。第三个的形状与 ROM 中已经以计算形式存在的表相同——但有 **563 次方向变化,而后者只有 13 次**,标准差为 4.1 LSB。

它是从模拟源采样而来,而非计算得出。偏差的均值为零,1024 个点中只有两个偏差超过 10——这不是不同的波形,而是*同一个*波形上带有噪声。

### 那个表是一个 30 dB 电平阶梯

干净版本在早期草稿中被描述为“一个带有十个 AM 深度的正弦波”,这是对图表的目测解读,而不是代码所说的内容。经过计算,1024 个点分成十个正弦弧,其跨度是```
255  171  120  80  56  38  26  17  12  8

一个公比为 0.681 = 10^(−1/6) 的等比数列,即每步 3.33 dB,总共 30.1 dB。减半模型最多偏差 56,3 dB 模型偏差 10。这是一个对数电平阶梯——一种幅度或衰减测试模式。

STR9 以两个 1 字节电报发送 16 位

幅度控制器在同一个选通信号上有两个移位寄存器,但固件每次电报只发送一个字节。原理图解释了这一点:两个 4094 通过 **QS'(引脚 10)**级联,引脚 9 未使用——而且电报成对出现,相隔约 42 000 个周期,成对之间有数百万个周期的静默。先发送的字节会被推入第二个寄存器。

同样的级联模式出现在每一个拥有多个移位寄存器的组件上——包括一个案例,其中链路通过一条名为 E 的线跨越了组件边界。

衰减器不是计算出来的

STR9 电报中的五个位直接驱动继电器:S1 切换直流发生器量程,S2…S5 控制衰减器继电器。20 dB (for 40dB)、20 dB、50/600 ohms——这些都印在原理图上。没有需要计算的阈值。

一个藏在眼皮底下的处理程序

位于 0301h 的跳转表通过 JMP @A+DPTR 读取。第 15 项落在 0301h + 30 = 031Fh——而那里,不是通常的 AJMP,而是处理程序本身,内联放置,省去了一次跳转。ROM 中没有任何跳转指令指向它,因此静态分析漏掉了它。它是 DIAL LOCK 处理程序,只有动态跟踪才发现了它。

六个任意槽位,而非二十四个

数据手册承诺 24 个波形存储器。EEPROM 中的目录显示为六个。算术给出了结论:``` 1024 points × 10 bit, packed 4 values per 5 bytes -> 1280 bytes per curve 6 × 1280 = 7 680 bytes, 0100h…1EFFh (X28C64, 8 KB) <- what was fitted 24 × 1280 = 30 720 bytes, 0100h…78FFh (X28C256, 32 KB) <- what the schematic says

固件实测读取范围是 `0100h–1EFFh`——六条曲线,精确到字节。该仪器是用小芯片构建的。

### 与不存在的设备通信的死代码

`9AFFh` 处的 186 字节与地址 `5Ah` 进行 I²C 通信——该地址在其他任何地方都未出现。在**两个**固件版本中,都没有跳转指向它。它与 `5Eh` 处的接口卡位于同一设备类型块中,只是 bank 位不同,并且它以两条十字节电报发送接收缓冲区和算术寄存器。这看起来像是针对从未出货的设备的工厂诊断程序。

### 你无法从任意 EEPROM 执行代码

一个显而易见的想法——将代码放入任意波形槽并跳转到它——从一开始就行不通。8051 是哈佛架构:指令通过 `/PSEN` 从程序 EPROM 取指,数据通过 `/RD` 从任意 EEPROM 读取。这不是被阻止了;只是线路根本不存在。

### 最后是频率编码

显示数字行位于发送到 PCF8576 的图像中的 `3Eh–43h` 处,所有位置共享同一种段编码,而字节 `43h` 在第 7 和第 8 个十进制位之间从 kHz 切换到 MHz。由此:```
f = M · 10^(D−8) kHz

在真实仪器上测得的三段频率步进序列被此完全复现——包括那段因尾数 2500 意味着 25 MHz 超出限制而提前停止的序列。


固件 V2.0 — 新特性

任意曲线 3:V1.5 与 V2.0 对比

左侧为出厂曲线,右侧为修正后的曲线。左下角是与计算表的偏差——那条 ±5 LSB 的带是采样模拟源留下的痕迹。

mkv20.py 从 V1.5(或 V1.3)构建 V2.0。它通过签名而非硬编码来查找每个地址,因此同一脚本可同时适用于两个源版本:

  1. 任意曲线 3 被替换为干净的计算表。两者均为 1024 字节的 8 位数据,因此替换在大小上是中性的,且不触及任何代码。1024 字节中有 863 字节发生变化。
  2. 任意曲线 2 被替换为对数啁啾(1 → 40 个周期)。它与曲线 1 恰好相差两个字节——多了一个针状脉冲——因此是冗余的。
  3. 版本标识位于 *IDN? 中:PHILIPS,PM5139,0,V2.0/0000。
  4. 显示屏上的版本:复位序列写入两个显示单元,这些字节现在以测量段编码拼出 2.0。
  5. 校验和重新计算并存储在固件期望的位置。

其余一切均未改动。还发现了另外三处异常,但有意保留——对不存在的 SFR 的写入(无害,两个版本中均有)、已失效的 5Ah 块,以及三个被测试但从未置位的状态位。修补它们不会改变任何行为,只会增加风险。

M27512_PM5139_V20.bin 正是此内容,别无其他。下面的旋律是一个独立的、可选的构建步骤。

已验证: 在模拟器中冷启动产生与 V1.5 相同的显示缓冲区和相同的标志,校验和验证通过,且构建是字节可复现的。它已被烧录并在真实的 PM5139 上运行——显示屏显示 2.0,且全部六个任意槽位均正常工作。


彩蛋

由于 V1.5 中校验和之后有 19 509 个未使用字节,且频率路径将音符频率作为三个 BCD 字节接收,该仪器可以通过自身输出播放音乐。

编码方式令人愉快地直接——第 3 位十进制,然后是以 0.01 Hz 为单位的 BCD 频率,因此 82.41 Hz 为 30 82 41。每个音符四个字节:三个用于音高,一个用于时长。

有趣的部分是触发机制。诊断菜单(开机时按住 LOCAL)有一个包含八个条目的跳转表,但菜单循环仅从 1 计数到 7 的 0Bh——因此第八个条目不可达。它也是冗余的:它跳转到菜单起始处,而该处本来就可以从另外两个地方到达。

因此整个钩子只有两个字节:``` 5B94h table entry 8: LJMP 5B45h -> LJMP 5B62h count limit: 08h -> 09h

没有丢失自检,没有表格被移动,也没有出现失效的菜单项。按住 LOCAL,打开开关,让菜单计数到 8,按一个键。

时序来自 MCS-51 数据手册。两个模拟器现在都在指令旁计数机器周期(`mcyc`,来自 `mcs51.CYCLES`),步进等待循环测得每单位 **1009 µs** —— 在 140 BPM 下每十六分音符 106.95 ms,偏离目标 0.2 %。这个数字以前是手工计算的 1006 µs,漏掉了两条指令。

`mkdoom.py` 也可以转换 MIDI 文件。必须选择一个声部(最高音、最低音或某一个通道),并且短于约 25 ms 的片段会被合并 —— 低于这个长度,低音无法完成一个完整振荡,你只会听到一声咔嗒。

---

## 然后它竟然是复音的

上面的旋律是一个声部。它不必如此,原因在于我们之前读过却忽略的服务手册中的一句话:

> 在信号生成期间,不同的信号幅度采样从 RAM 中读出。如果基本信号波形被改变 [...] 相应的幅度采样由 **CPU 加载到 RAM 中**。

PM5139 是一个 **1024 点波表 DDS**。TWS 在任何朴素意义上都不是三角波发生器 —— 它是一个相位累加器,为单元 4 上的快速 RAM 产生 0…1023 的读取地址,而该 RAM 由 CPU 通过 C 总线填充。正弦、方波、锯齿波和任意波形都是同一机制:一张表。

而这张表恰好保存**输出信号的一个周期**。因此,由*谐波之和*构建的表在其 1024 个点上仍然是周期性的,并且会作为和弦播放。不是琶音,不是调制技巧 —— 而是多个音符同时以完整的 20 Vpp 发声,在它们发声时 CPU 完全不做任何事。由于分音必须是表频率的整数倍,音程以纯律呈现,而对于持续和弦来说,这本来就是更好的调音。
下载工具