我一直在尝试对使用 8 位 uC 的旧设备进行逆向工程。我可以访问 uC 发送到计算机的几条消息以及它们各自的校验和的结果。我还找到了 uC 的代码,但它是一个不完整的版本,因此它没有提供比这更多的信息:
是CRC8
也许有一个未知的初始值或最终值。
这里是一些消息,结构是:STX+ DATA+ EOT+checksum+EXT
编辑:这是 CRC 算法的常见实现
byte CRC8(byte param_1,byte *checksum)
{
byte bVar1;
char cVar2= '\b';
byte bStack_3 = param_1;
do {
bVar1 = (bStack_3 ^ *checksum) << 7;
if ((char)bVar1 < '\0') {
*checksum = *checksum ^ 0x18;
}
*checksum = *checksum >> 1 | bVar1;
bStack_3 = bStack_3 >> 1 | bStack_3 << 7;
cVar2 = cVar2 + -1;
} while (cVar2 != '\0');
return param_1;
}
在前一个问题(现已删除)中,您提供了二进制文件的链接,我用它来设置 Ghidra 项目。所以我有两种方法来找到 CRC 算法。
将二进制文件加载到 Ghidra 后,我发现几个中断条目被设置为跳转。这很常见。其中是串行接口的中断服务程序,从0x0107开始。更深入地观察,我发现他们使用循环缓冲区进行接收和传输。
访问变量的使用位置,我发现基本发送例程位于0x00DE。沿着参考链,我终于找到了一个有趣的函数,我称之为
sendAndUpdateCrc()
。这是清理后的版本:
void sendAndUpdateCrc(char c)
{
fputc(c);
updateCrc(c);
}
这个核心函数在整个程序中都会被调用,我稍后再讲。最重要的部分是我称之为
updateCrc()
的函数。
这是 CRC 更新功能的清理版本。它使用一个全局变量来累加,该变量位于内部RAM的0x7A处。
void updateCrc(char c) {
char cVar2 = 8 /* '\b' */;
byte bStack_3 = c;
do {
byte bVar1 = (bStack_3 ^ crc) << 7;
if ((char)bVar1 < 0 /*'\0' */) { /* checks the sign bit that was LSBit */
crc = crc ^ 0x18;
}
crc = crc >> 1 | bVar1;
bStack_3 = bStack_3 >> 1 | bStack_3 << 7;
cVar2 = cVar2 + -1;
} while (cVar2 != 0 /* '\0' */);
}
有了这个函数,第二种方法就容易多了,见下文。
访问
sendAndUpdateCrc()
的调用位置时,我遇到了这些专门的功能。这里列出了它们的地址和 crc
的初始值。
地址 | 初始CRC | 我的名字 | 意义 |
---|---|---|---|
0x46cc | 0x24 | 发送FfDollarIAndMore() | 发送 等等,可能是一些初始消息 |
0x48a7 | 0x00 | sendQuot12310AndMore() | 发送 还有更多,可能有很多变量 |
0x4d57 | 0x30^ID | 发送Xy() | 发送 和两个个位数值 |
0x4daa | 0x40^ID | sendHash_____x() | 根据变量发送 和另外六个字符 |
0x4e11 | 0x40^ID | 发送X() | 发送 和一位数字 |
0x5184 | 0x40^ID | 发送不同大小的数据() | 发送 和(对我来说)未知数量的值 |
匹配消息格式,所有这些函数都会更新 CRC,包括前导 STX (0x02) 和分隔 ETX (0x04)。然而,他们不断更新 CRC,这毫无意义。
如您所见,设备 ID 的最后两位以 BCD(二进制编码的十进制)形式存储在 ROM 的 0x0033 处。这是上表中的“ID”。
您观察到的消息的前两个字符为
AB
,分别从内部 RAM 中的地址 0x77 和 0x78 获取。它们的值是在 0x4293 处的函数中计算的,但我没有费力地浏览它的代码来尝试理解它。它似乎在应用程序启动期间被调用。也许这两个字符是PC设定的。
在重新设计过程中,我发现了两种不同风格的代码。一种是相当理想的,而另一种则非常差,因此很慢。然而,没有人会编写这样的代码,所以我假设一个非常简单的编译器作为生产者。好的代码很可能是由人类用汇编语言编写的。
好的代码示例:
send:
PUSH BANK0_R0
LAB_CODE_00e0: ; wait for space in the buffer
CLR EA
MOV R0,txdBufferSize
CJNE R0,#0x2,LAB_CODE_00eb
SETB EA
SJMP LAB_CODE_00e0
LAB_CODE_00eb:
MOV R0,txdBufferWritePointer ; store character in buffer
MOV @R0,A
INC R0
CJNE R0,#txdBufferEnd,LAB_CODE_00f4
MOV R0,#txdBufferBegin
LAB_CODE_00f4:
MOV txdBufferWritePointer,R0 ; and advance buffer pointer
INC txdBufferSize ; increment used size
MOV R0,txdBufferEmpty ; check empty flag
CJNE R0,#0x1,LAB_CODE_0102
MOV txdBufferEmpty,#0x0
SETB TI ; start transmission after an idle phase
LAB_CODE_0102:
SETB EA
POP BANK0_R0
RET
糟糕代码的示例(请记住,8051 使用向高地址增长的堆栈):
sendAndUpdateCrc:
PUSH A ; save c on the stack
MOV A,SP
ADD A,#0x0
MOV R0,A
MOV A,@R0 ; load c from the stack top
LCALL fputc ; call fputc()
MOV A,SP
ADD A,#0x0
MOV R0,A
MOV A,@R0 ; load c from the stack top
LCALL updateCrc ; call updateCrc()
DEC SP ; rewind stack
RET
作为最后一个“好东西”,我发现了这个错误的代码:
putchar:
CJNE A,#0xa,LAB_CODE_00da ; 0xA = \n
LCALL send
LAB_CODE_00da:
LCALL send
RET
该函数显然应该将单个输入字符
'\n'
作为公共序列 CR-LF ('\r'
, '\n'
) 进行传输。但它不是重复 CR,而是重复 LF。 Outch。
由于我们只有 8 位作为校验和,因此暴力方法很容易实现。为此,我使用了增强的 CRC 函数,并查找产生观察到的 CRC 所需的初始值。以下是我写的小程序:
#include <stdio.h>
#include <stdint.h>
#include <string.h>
static uint8_t crc;
static void updateCrc(char c)
{
for (int b = 0; b < 8; ++b) {
if (((c ^ crc) & 1) != 0) {
crc ^= 0x18;
crc >>= 1;
crc |= 0x80;
} else {
crc >>= 1;
}
c >>= 1;
}
}
int main(void) {
#define NUMBER_OF_MESSAGES 5
static const char * const MESSAGES[NUMBER_OF_MESSAGES] = {
"ABB", "AB11", "AB10", "ABp", "AB0FFFF",
};
static const struct {
uint8_t device_id;
uint8_t expected_crc[NUMBER_OF_MESSAGES];
} SETS[] = {
{ 0x00, { 0x10, 0xBD, 0x79, 0xAC, 0xF0, } },
{ 0x01, { 0xDD, 0x8A, 0x4E, 0x61, 0x54, } },
{ 0x02, { 0x93, 0xD3, 0x17, 0x2F, 0xA1, } },
{ 0x56, { 0x19, 0x21, 0xE5, 0xA5, 0x63, } },
{ 0x99, { 0x54, 0x9A, 0x5E, 0xE8, 0x1E, } },
};
for (size_t s_index = 0; s_index < sizeof SETS / sizeof SETS[0]; ++s_index) {
printf("\tID=%02X", SETS[s_index].device_id);
}
puts("");
for (size_t m_index = 0; m_index < NUMBER_OF_MESSAGES; ++m_index) {
printf("%s", MESSAGES[m_index]);
for (size_t s_index = 0; s_index < sizeof SETS / sizeof SETS[0]; ++s_index) {
static const int NONE = -1;
int initial = NONE;
for (int candidate = 0x00; candidate <= 0xFF; ++candidate) {
crc = (uint8_t)candidate;
const char STX = 0x02;
updateCrc(STX);
for (size_t c_index = 0; c_index < strlen(MESSAGES[m_index]); ++c_index) {
updateCrc(MESSAGES[m_index][c_index]);
}
const char ETX = 0x04;
updateCrc(ETX);
if (crc == SETS[s_index].expected_crc[m_index]) {
if (initial == -1) {
initial = candidate;
} else {
printf("!"); /* report double */
}
}
}
if (initial == NONE) {
printf("\t--"); /* report none */
} else {
printf("\t%02X", initial);
}
}
puts("");
}
}
以表格形式查看的结果给了我们提示并验证了重新设计的结果:
ID=00 ID=01 ID=02 ID=56 ID=99
ABB 40 41 42 16 D9
AB11 30 31 32 66 A9
AB10 30 31 32 66 A9
ABp 40 41 42 16 D9
AB0FFFF 30 31 32 66 A9
图案跃入眼帘。初始值分别由 0x40 和 0x30 与设备 ID 的最后两位数字异或为 BCD 值。
在第二个小程序中,我检查了该理论:
#include <ctype.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
static uint8_t crc;
static void updateCrc(char c)
{
static const uint8_t POLYNOM = 0x18;
for (int b = 8; b != 0; --b) {
if (((c ^ crc) & 1) != 0) {
crc ^= POLYNOM;
crc >>= 1;
crc |= 1 << 7;
} else {
crc >>= 1;
}
c >>= 1;
}
}
static void sendAndUpdateCrc(char c)
{
if (isalnum(c)) {
putchar(c);
} else {
putchar('.');
}
updateCrc(c);
}
int main(void) {
#define NUMBER_OF_MESSAGES 5
static const char * const MESSAGES[NUMBER_OF_MESSAGES] = {
"ABB", "AB11", "AB10", "ABp", "AB0FFFF",
};
static const struct {
uint8_t device_id;
uint8_t expected_crc[NUMBER_OF_MESSAGES];
} SETS[] = {
{ 0x00, { 0x10, 0xBD, 0x79, 0xAC, 0xF0, } },
{ 0x01, { 0xDD, 0x8A, 0x4E, 0x61, 0x54, } },
{ 0x02, { 0x93, 0xD3, 0x17, 0x2F, 0xA1, } },
{ 0x56, { 0x19, 0x21, 0xE5, 0xA5, 0x63, } },
{ 0x99, { 0x54, 0x9A, 0x5E, 0xE8, 0x1E, } },
};
for (size_t m_index = 0; m_index < NUMBER_OF_MESSAGES; ++m_index) {
for (size_t s_index = 0; s_index < sizeof SETS / sizeof SETS[0]; ++s_index) {
printf("ID=%02X ", SETS[s_index].device_id);
printf("message=");
switch (strlen(MESSAGES[m_index])) {
case 3:
crc = 0x40;
break;
case 4:
crc = 0x30;
break;
default:
crc = 0x30;
break;
}
crc ^= SETS[s_index].device_id;
const char STX = 0x02;
sendAndUpdateCrc(STX);
for (size_t c_index = 0; c_index < strlen(MESSAGES[m_index]); ++c_index) {
sendAndUpdateCrc(MESSAGES[m_index][c_index]);
}
const char ETX = 0x04;
sendAndUpdateCrc(ETX);
char crc_hex[3];
sprintf(crc_hex, "%02X", crc);
sendAndUpdateCrc(crc_hex[0]);
sendAndUpdateCrc(crc_hex[1]);
const char EOT = 0x03;
sendAndUpdateCrc(EOT);
printf(" expected CRC=%02X\n", SETS[s_index].expected_crc[m_index]);
}
}
}
这是它的输出,它验证了理论:
ID=00 message=.ABB.10. expected CRC=10
ID=01 message=.ABB.DD. expected CRC=DD
ID=02 message=.ABB.93. expected CRC=93
ID=56 message=.ABB.19. expected CRC=19
ID=99 message=.ABB.54. expected CRC=54
ID=00 message=.AB11.BD. expected CRC=BD
ID=01 message=.AB11.8A. expected CRC=8A
ID=02 message=.AB11.D3. expected CRC=D3
ID=56 message=.AB11.21. expected CRC=21
ID=99 message=.AB11.9A. expected CRC=9A
ID=00 message=.AB10.79. expected CRC=79
ID=01 message=.AB10.4E. expected CRC=4E
ID=02 message=.AB10.17. expected CRC=17
ID=56 message=.AB10.E5. expected CRC=E5
ID=99 message=.AB10.5E. expected CRC=5E
ID=00 message=.ABp.AC. expected CRC=AC
ID=01 message=.ABp.61. expected CRC=61
ID=02 message=.ABp.2F. expected CRC=2F
ID=56 message=.ABp.A5. expected CRC=A5
ID=99 message=.ABp.E8. expected CRC=E8
ID=00 message=.AB0FFFF.F0. expected CRC=F0
ID=01 message=.AB0FFFF.54. expected CRC=54
ID=02 message=.AB0FFFF.A1. expected CRC=A1
ID=56 message=.AB0FFFF.63. expected CRC=63
ID=99 message=.AB0FFFF.1E. expected CRC=1E
注意:由于列表中 ID 56 和 99 的交换消息“ABp”,您让这件事变得有点困难。不错的陷阱。