最近,我一直在进行一个小的逆向工程项目,希望能使我的一些电子测试设备更加便捷易用。
在进行逆向工程时,我通常遵循的一个策略是,首先做出关于某个系统如何工作的(有根据的)猜测,然后寻找方法验证这些猜测是否正确。在这个项目中,Cynthion对这一过程非常有帮助,因为我可以利用它模拟目标系统的部分功能,从而快速、轻松地测试关于协议的理论。
这篇文章总结了我到目前为止所取得的一些进展,希望能提供一些有用的技巧,供你们在自己的项目中参考!
背景
在我的工作和兴趣项目中,我经常使用矢量网络分析仪(VNA)来测量射频(RF)组件。理想情况下,在每次开机或更改测量参数时,VNA 应通过连接并依次测量四个标准来进行校准,每个测量端口都需要进行一次。这种过程如果需要频繁重新校准,可能会有些繁琐,因此一种替代方案是使用电子校准模块(ECal),它只需要每个端口连接一次,内部开关就可以自动选择不同的标准。我的 VNA(Agilent E8803A)也可以使用 ECal 模块,但这些模块既稀缺又昂贵,因此我希望能够弄清楚 VNA 如何与它们通信,从而自己实现一个开源的 ECal。
逆向工程选项
当然,最简单的方式是连接一个 ECal 模块并捕获 USB 流量(使用 Packetry!),但实际上 ECal 模块太难找到。
接下来的一种选择是拆解并分析 VNA 上运行的软件,我花了一些时间做了这项工作,但进展较慢,因为我对 Windows 上的 API 以及它如何使用这些 API 进行 USB 通信并不太熟悉。不过,通过查看软件,还是有一些可以快速学到的东西。该软件分为多个 DLL 文件,因此很容易查看每个文件的导入和导出,从中大致了解我们可能从 USB ECal 中期望的功能:
winedump -j export ecalusb.dll
Contents of ecalusb.dll: 33280 bytes
[...]
Entry Pt Ordn Name
00001F50 1 ReadModule
00002160 2 SetState
00001F90 3 ReadModule1K
000016D0 4 Reset
00001FD0 5 ReadModuleData
00002010 6 WriteModuleData
00002210 7 EraseSector
Done dumping ecalusb.dll
因此,ecalusb.dll 确实包含了读取和写入模块数据的功能,这与它需要存储用于描述不同标准特性的 S 参数数据并用于校准的需求相符。它还包括 SetState
函数,这可能用于设置开关的位置,以便在每个端口选择不同的标准。
设备仿真
我决定选择仿真 ECal 设备的路径,看看 VNA 尝试请求哪些信息,并从中进行迭代。使用 Cynthion 和 Facedancer 库,你可以通过编写 Python 脚本轻松地仿真一个 USB 设备,并快速修改脚本来尝试不同的操作。
我从 Facedancer 项目的 template.py
示例开始。通常,USB 主机通过查看厂商 ID 和产品 ID,以及/或制造商/产品/序列号字符串来识别特定设备。在搜索论坛和其他测试设备小组后,我找到了目标设备的预期值,并用这些值修改了模板:
#!/usr/bin/env python3
from facedancer import main
from facedancer import *
class ECalDevice(USBDevice):
vendor_id : int = 0x0957
product_id : int = 0x0001
manufacturer_string : str = "Agilent Technologies"
product_string : str = "USB ECal Module"
serial_number_string : str = "S/N 12346"
device_speed : DeviceSpeed = DeviceSpeed.FULL
class ECalConfiguration(USBConfiguration):
class ECalInterface(USBInterface):
class ECalInEndpoint(USBEndpoint):
number : int = 1
direction : USBDirection = USBDirection.IN
transfer_type : USBTransferType = USBTransferType.BULK
max_packet_size : int = 64
def handle_data_requested(self):
self.send(b"Hello!")
class ECalOutEndpoint(USBEndpoint):
number : int = 1
direction : USBDirection = USBDirection.OUT
def handle_data_received(self, data):
print(f"Received data: {data}")
main(ECalDevice)
运行这个脚本并点击 VNA 上的“检测连接的 ECals”后,得到了以下输出:
$ python ecal-emulate.py --suggest
INFO | __init__ | Starting emulation, press 'Control-C' to disconnect and exit.
INFO | moondancer | Using the Moondancer backend.
INFO | moondancer | Connected FULL speed device '__main__.ECalDevice' to target host.
INFO | device | Host issued a bus reset; resetting our connection.
INFO | moondancer | Target host configuration complete.
WARNING | device | Stalling unhandled OUT VENDOR request 0x04 to DEVICE [value=0x0000, index=0x0000, length=0].
这表明脚本收到了来自 VNA 的厂商特定请求,但由于没有处理该请求的代码,因此返回了 STALL 响应(这是 USB 设备响应未处理请求时的术语)。由于我使用了 --suggest
参数,停止脚本后,它给出了一个可以添加到代码中的处理建议!
^CINFO | moondancer | Disconnecting from target host.
Automatic Suggestions
These suggestions are based on simple observed behavior;
not all of these suggestions may be useful / desirable.
Request handler code:
@vendor_request_handler(number=4, direction=USBDirection.OUT)
@to_device
def handle_control_request_4(self, request):
# Most recent request data: bytearray(b'').
# Replace me with your handler.
request.stall()
该代码建议定义了一个用于处理厂商特定请求(编号为 4)的方法。当 VNA 发出该请求时,仿真设备将接收并返回一个 STALL 响应,表示该请求尚未处理。你可以在此基础上编写实际的处理逻辑来应对该请求。
我将建议的请求处理程序添加到脚本中,但将响应从 stall
更改为 ack
,并打印出了请求内容:
--- a/ecal-emulate.py
+++ b/ecal-emulate.py
@@ -34,4 +34,11 @@ class ECalDevice(USBDevice):
def handle_data_received(self, data):
print(f"Received data: {data}")
+ @vendor_request_handler(number=4, direction=USBDirection.OUT)
+ @to_device
+ def handle_control_request_4(self, request):
+ print(request)
+ request.ack()
+
+
main(ECalDevice)
运行脚本后,我收到了一个新消息,提示还需要处理厂商请求编号 2。于是,我按照相同的过程处理了这个请求。处理完后,我收到了更多输出——现在它发送了许多编号为 #2 的厂商请求,每次值从 0x400
(1024)开始递减,每次递减 6:
这表明 VNA 正在连续发送不同的厂商请求,每个请求带有递减的数值。这个行为可能与设备状态的变化或校准过程中的步骤有关。通过仿真和处理这些请求,你可以更好地了解 VNA 和 ECal 设备之间的通信协议。
OUT VENDOR request 0x04 to DEVICE [value=0x0000, index=0x0000, length=0]
OUT VENDOR request 0x02 to DEVICE [value=0x0400, index=0x0000, length=0]
OUT VENDOR request 0x02 to DEVICE [value=0x03fa, index=0x0000, length=0]
...
OUT VENDOR request 0x02 to DEVICE [value=0x0010, index=0x0000, length=0]
OUT VENDOR request 0x02 to DEVICE [value=0x000a, index=0x0000, length=0]
OUT VENDOR request 0x02 to DEVICE [value=0x0004, index=0x0000, length=0]
这看起来非常有前景,因为根据之前提到的一些 DLL 导出,我预计它会读取出 1 kB 的内存。然而,在这时我有些困惑,因为虽然它似乎在访问内存,但我没有看到设备实际返回数据的方式。由于 USB 控制传输的工作方式,OUT 请求本身是无法返回任何数据的——它只能返回 ACK 或 NAK 响应。
我想看看是否有其他的事情发生在总线上,而我可能漏掉了。幸运的是,我手头有几个 Cynthion,于是我将第二个 Cynthion 连接到链路中进行数据包捕获,希望能够进一步了解情况:
哎呀!当然,我从模板示例中获得的代码也设置了一个 BULK IN 端点来返回“Hello!”每次处理完地址厂商请求后,VNA 会请求数据通过该 BULK 端点,并接收这 6 个字节,然后调整地址并重复。如果我当时意识到这一点,我本可以直接在处理程序中添加打印语句来观察这一过程,而不需要设置数据包捕获。
现在,我已经大致了解了数据是如何被读取出来的,可以继续正确实现这一功能,但我需要一些适当的数据来返回。幸运的是,我找到了一些另一位用户在线分享的 8506x 系列 ECal 模块的内存转储文件。他们分享的是 ASCII 十六进制转储,因此我使用 xxd
将其转换为二进制格式:
$ head HP85062-60006.txt
=~=~=~=~=~=~=~=~=~=~=~= PuTTY log 2021.09.29 21:47:04 =~=~=~=~=~=~=~=~=~=~=~=
dump 0 040000
000000: 48 50 38 35 30 36 30 43 20 45 43 41 4C 00 E8 D1 | HP85060C ECAL... |
000010: 31 83 C4 02 64 00 4E 6F 76 20 32 38 20 31 39 39 | 1...d.Nov 28 199 |
000020: 34 00 FF FF FF FF FF FF FF FF FF FF FF FF FF FF | 4............... |
000030: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF | ................ |
000040: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF | ................ |
000050: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF | ................ |
000060: FF FF FF FF 30 30 33 36 37 00 31 C0 50 E8 88 08 | ....00367.1.P... |
$ xxd -r HP85062-60006.txt HP85062-60006.bin
$ hexdump -C HP85062-60006.bin | head
00000000 48 50 38 35 30 36 30 43 20 45 43 41 4c 00 e8 d1 |HP85060C ECAL...|
00000010 31 83 c4 02 64 00 4e 6f 76 20 32 38 20 31 39 39 |1...d.Nov 28 199|
00000020 34 00 ff ff ff ff ff ff ff ff ff ff ff ff ff ff |4...............|
00000030 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
*
00000060 ff ff ff ff 30 30 33 36 37 00 31 c0 50 e8 88 08 |....00367.1.P...|
00000070 33 35 46 33 35 46 20 4d 57 31 00 c4 04 56 e8 fb |35F35F MW1...V..|
00000080 08 83 c4 02 38 20 41 75 67 20 32 30 30 31 20 00 |....8 Aug 2001 .|
00000090 41 47 49 4c 45 4e 54 2f 4d 54 41 00 b8 f8 6a eb |AGILENT/MTA...j.|
000000a0 93 83 3e 94 06 00 74 1e a1 82 06 83 c0 41 a2 98 |..>...t......A..|
然后,我修改了我的 Python 脚本,加载该文件并从 BULK 端点返回数据,之后再次尝试从 VNA 检测设备:
--- a/ecal-emulate.py
+++ b/ecal-emulate.py
@@ -14,6 +14,9 @@ class ECalDevice(USBDevice):
serial_number_string : str = "S/N 12346"
device_speed : DeviceSpeed = DeviceSpeed.FULL
+ address = 0
+ data = open('EEPROM/HP85062-60006.bin', 'rb').read()
+
class ECalConfiguration(USBConfiguration):
class ECalInterface(USBInterface):
@@ -25,7 +28,10 @@ class ECalDevice(USBDevice):
max_packet_size : int = 64
def handle_data_requested(self):
- self.send(b"Hello!")
+ # Respond with 32 bytes of EEPROM data
+ dev = self.get_device()
+ addr = dev.address
+ self.send(dev.data[addr:addr+32])
class ECalOutEndpoint(USBEndpoint):
number : int = 1
成功了!…有点儿。VNA 确实检测到了一些东西,这是一个很大的进展,但输出还是一团糟。在盯着它看了一会儿之后,我意识到我的愚蠢错误——我忘记在接收厂商请求 2 时更新地址了:
--- a/ecal-emulate.py
+++ b/ecal-emulate.py
@@ -44,6 +44,7 @@ class ECalDevice(USBDevice):
@to_device
def handle_control_request_2(self, request):
print(request)
+ self.address = request.value
request.ack()
我加上了这一行,重新运行了一切,结果是……什么都没检测到!但是,实际上这个错误使得事情变得更好。
常见的文件格式通常会有一个头部,其中包含某种“魔法值”(magic value),解析器会在进行任何操作之前检查这个值。在没有修正地址的情况下,脚本会返回前 32 个字节的数据,因此这可能足以通过头部检查并显示已检测到设备。然而,这也意味着在我实现了地址之后,脚本在 VNA 期望的时候并没有返回头部。
我回过头来看了一下厂商请求 #2 的模式:
OUT VENDOR request 0x02 to DEVICE [value=0x0400, index=0x0000, length=0]
OUT VENDOR request 0x02 to DEVICE [value=0x03e0, index=0x0000, length=0]
...
OUT VENDOR request 0x02 to DEVICE [value=0x0040, index=0x0000, length=0]
OUT VENDOR request 0x02 to DEVICE [value=0x0020, index=0x0000, length=0]
^CINFO | moondancer | Disconnecting from target host.
有两件事让我特别注意:
-
VNA 从
value=0x400
开始请求,然后递减——这有点奇怪,我原本以为它应该从地址 0 开始递增。 -
VNA 从未发送
value=0x0
的请求,因此在启用地址时,脚本根本没有返回头部数据!
这让我想到,也许地址存在某种问题,应该反向处理(即请求 value=0x400
应该返回地址 0x0
)。于是我做了这个更改,并重新运行了测试:
--- a/ecal-emulate.py
+++ b/ecal-emulate.py
@@ -44,7 +44,7 @@ class ECalDevice(USBDevice):
@to_device
def handle_control_request_2(self, request):
print(request)
- self.address = request.value
+ self.address = 0x400 - request.value
request.ack()
Bingo! 设备成功检测到,并且所有信息现在看起来都正确了。
虽然还有一些细节需要搞清楚,但我决定在这里结束,因为我认为这已经很好地展示了整个过程。希望它能为你们提供一些灵感,如果你们在自己的项目中使用这些技术和工具,记得告诉我们!
对于感兴趣的人,完整代码和任何后续研究可以在这里找到:https://github.com/miek/ecal-reversing
原文始发于微信公众号(网络安全知识):使用 Cynthion 对 VNA ECal 接口进行逆向工程
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论