2022春秋杯-春季赛-RetroRegisterWp

本文最后更新于:1 年前

作业文件见: https://github.com/m1n9yu3/2022chunqiubei_RetroRegister

题目逻辑分析

一个重启验证。

程序流程为,

  1. 用户输入用户名和密码
  2. 进入 CheckInput 中检查输入
  3. 判断输入是否符合要求, 写 reg.dat 文件

重启程序时

  1. 读取 reg.dat 文件 ReadRegData

  2. 验证 reg.dat 文件是否符合逻辑

    1. CheckRegData 验证 2 3
    2. sub_401220 验证 0
    3. sub_401250 验证 1
    4. CheckInput 验证 4, 重启验证时,不验证 4

题目给出一组正确的用户名密码

1
2
用户名 3272C2168ECE2A8 
注册码 95BVX-M44DC-RP43X-4NM7T-DLWTW

用户输入检查

还原 CheckInput

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
// 对输入密码格式进行的一个验证
int CheckInput(char* szUserName, char* szPassWord)
{
// 根据观察 加密数据 9 * 4 大小, 0-4 存储密码加密后的数据, 5-8 存储用户名加密后的数据
DWORD OutBuffer[8] = { 0 };
unsigned char aStringTable[0x21] = { "23456789ABCDEFGHJKLMNPQRSTUVWXYZ" };

// 经过一个 SM3 加密后,拿到一组数据,进行xor 后得到后续运算的依据
//SM3EncryPt(szUserName, strlen(szUserName), OutBuffer);

aEncryMsgArry[5] = OutBuffer[0] ^ OutBuffer[1];
aEncryMsgArry[6] = OutBuffer[2] ^ OutBuffer[3];
aEncryMsgArry[7] = OutBuffer[4] ^ OutBuffer[5];
aEncryMsgArry[8] = OutBuffer[6] ^ OutBuffer[7];

if (strlen(szPassWord) != 0x1d)
{
return 0;
}

aEncryMsgArry[0] = 0;

DWORD* pEncryPtAr = aEncryMsgArry;
int nCount = 0;

// 循环 0x1d 次
for (int j = 0, *pEncryPtAr = 0; ; j++)
{

int cTmp = szPassWord[j];
int nIndex = 0;
// 判断字符串是否出现在给定的字符表中
while (nIndex < 0x20)
{
if (cTmp == aStringTable[nIndex + 0])
{
*pEncryPtAr = (*pEncryPtAr << 5) + nIndex;
}
nIndex += 1;
}
// 如果没有,则直接返回0
if (*pEncryPtAr == 0)
{
return 0;
}
nCount += 1;
// 如果 运算次数大于等于 5
if (nCount >= 5)
{
// 首先判断当前是否超出索引
if (j > 0x1d)
{
break;
}
// 判断当前字符是否为 - (规定: 密码中 5的倍数位必为 - )
if (szPassWord[j] != '-')
{
return 0;
}
pEncryPtAr = pEncryPtAr + 1;
// 如果 当前位置 大于等于于 szEncryPtArry[5] 则退出循环。 应该是只需要加密 0-4 的位置的数据。
if ((DWORD*)pEncryPtAr < &(aEncryMsgArry[5]))
{
nCount = 0;
*pEncryPtAr = 0;
}
else
{
break;
}
}

}

int nTmp = 0;

// 验证 0-4 位置的数据是否合理。
if (
(((((((((((aEncryMsgArry[0] ^ aEncryMsgArry[4]) >> 5) ^ aEncryMsgArry[0]) >> 5) ^ aEncryMsgArry[0]) >> 5) ^ aEncryMsgArry[0]) >> 5) ^ aEncryMsgArry[0]) & 0x1f) == 0)
&&
((((((((((aEncryMsgArry[1] >> 5) ^ aEncryMsgArry[1]) ^ aEncryMsgArry[4]) >> 5) ^ aEncryMsgArry[1]) >> 5) ^ aEncryMsgArry[1]) >> 5 ^ aEncryMsgArry[1]) & 0x1f) == 0)
&&
(((((((((((aEncryMsgArry[2] >> 5) ^ aEncryMsgArry[2]) >> 5) ^ aEncryMsgArry[2]) ^ aEncryMsgArry[4]) >> 5) ^ aEncryMsgArry[2]) >> 5) ^ aEncryMsgArry[2]) & 0x1f) == 0)
&&
((((((((((aEncryMsgArry[3] >> 5) ^ aEncryMsgArry[3]) >> 10) ^ (aEncryMsgArry[3] >> 5)) ^ aEncryMsgArry[3]) ^ aEncryMsgArry[4]) >> 5) ^ aEncryMsgArry[3]) & 0x1f) == 0)
&&
(((((((aEncryMsgArry[3] >> 5) ^ aEncryMsgArry[3]) ^ ((aEncryMsgArry[2] >> 5) ^ aEncryMsgArry[2]) ^ ((aEncryMsgArry[1] >> 5) ^ (aEncryMsgArry[1])) ^ \
((aEncryMsgArry[0] >> 5) ^ aEncryMsgArry[0])) >> 10 ^ ((aEncryMsgArry[3] >> 5) ^ aEncryMsgArry[3]) ^ ((aEncryMsgArry[2] >> 5) ^ aEncryMsgArry[2]) ^ \
((aEncryMsgArry[1] >> 5) ^ aEncryMsgArry[1]) ^ ((aEncryMsgArry[0] >> 5) ^ aEncryMsgArry[0]) >> 5) ^ \
aEncryMsgArry[3] ^ aEncryMsgArry[2] ^ aEncryMsgArry[1]) & 0x1f)\
== \
(aEncryMsgArry[4] & 0x1f))
)
{
return 1;
}
return 0;

}

aEncryMsgArry 存储结构

0-4 存储着密码

5-8 存储着 SM3encrypt(用户名)

读取密码,判断是否在给定的字符串表中,不在表中,直接返回失败

在表中, 写入索引在当前指向的 aEncryMsgArry中,左移 5 位。

如果每次读取了5位密码后,比较下一个密码是否是 - ,不是则直接返回失败。

aEncryMsgArry 的位置 + 1, 指向下一个表,然后置零下一个表。

然后判断当前密码长度是否小于 0x1d,小于则继续读取密码。否则进入到验证密码输入环节。

验证密码输入算法分析:

1
(((((((((((aEncryMsgArry[0] ^ aEncryMsgArry[4]) >> 5) ^ aEncryMsgArry[0]) >> 5) ^ aEncryMsgArry[0]) >> 5) ^ aEncryMsgArry[0]) >> 5) ^ aEncryMsgArry[0]) & 0x1f) == 0)

有效数据位为 20-32 位,

与 aEncryMsgArry[4] 的 20-24 位 xor

与 aEncryMsgArry[0] 的 15-19 位 xor

与 aEncryMsgArry[0] 的 10-14 位 xor

与 aEncryMsgArry[0] 的 05-09 位 xor

与 aEncryMsgArry[0] 的 00-04 位 xor

还原的时候就应该, 从前向后 xor , 最后 & 0x1f 就能还原 aEncryMsgArry[4] 20-24 位的值。

从下依次为 比较aEncryMsgArry[4]的 15-19 , 10-14,5-9, 求解。

aEncryMsgArry[4] 的 0-4 有些特殊,因为求值公式已经给出,最后就是在和 原来的 0-4 位进行比较,直接CV还原即可。

写入文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 写入注册码信息
int WriteRegDat()
{
// 打开文件
HANDLE hFile = CreateFileA("reg.dat", 0x40000000, 0, 0, 2, 0x80, 0);
if (hFile == INVALID_HANDLE_VALUE)
{
return 0;
}

DWORD NumberOfBytesWritten = 0;
DWORD Buffer[13];

// 按照既定格式对用户名密码进行存储
//*(__int64*)&Buffer = *(__int64*)aUserName;
//*(((__int64*)&Buffer) + 1) = *(((__int64*)&aUserName) + 1);

memcpy_s(&Buffer[0], 16, aUserName, 16);
memcpy_s(&Buffer[4], 16, &aEncryMsgArry[5], 16);
memcpy_s(&Buffer[8], 16, &aEncryMsgArry[0], 16);
Buffer[12] = aEncryMsgArry[4];

// 写文件
if (WriteFile(hFile, Buffer, 0x34, &NumberOfBytesWritten, 0) != 0)
{
if (NumberOfBytesWritten == 0x34)
{
return 1;
}
}
return 0;


}

存储格式为

aUserName :16byte

&aEncryMsgArry[5]:16byte

&aEncryMsgArry[0]:16byte

&aEncryMsgArry[4]:4byte

重启验证逻辑逆向

首先进行了一个读取文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 读取注册码信息
int ReadRegData()
{
// 对着 PE 文件头部一顿操作
HMODULE GetModuleHandleA(NULL);
// Debug出来为 .text节表 放入 Sm3 加密后拿到的值, 直接扣出来
DWORD PEHEADENCRYPT[8] = { 0x26e9458a, 0x3c13520b, 0xdef20ace, 0xa703f8c1, 0x43dc0b29, 0x7e4a7fa7, 0x725366c3, 0x80680cfc };


DWORD NumberOfBytesWritten = 0;
DWORD Buffer[13];
memset(Buffer, 0, 0x34);

HANDLE hFile = CreateFileA("reg.dat", 0x80000000, 1, 0, 3, 0x80, 0);
if (hFile == INVALID_HANDLE_VALUE)
{
return 0;
}

if (ReadFile(hFile, Buffer, 0x34, &NumberOfBytesWritten, 0) = 0)
{
CloseHandle(hFile);
return 0;
}

// 0-3 填充到 aUserName 中 16位
for (int i = 0; i < 4; i++)
{
*((DWORD*)aUserName + i) = Buffer[i];
}
// 4-7 与 12 进行 xor 后 填充到 aEncryMsgArry[5] 之后的位置 16 位
for (int i = 0; i < 4; i++)
{
aEncryMsgArry[i + 5] = Buffer[i + 4] ^ PEHEADENCRYPT[i];
}

// 8-11 填充到 szEncryPtArry[0] 之后的 16位
for (int i = 0; i < 4; i++)
{
aEncryMsgArry[i] = Buffer[i + 8];
}
// aEncryMsgArry[4] = Buffer[12] 4位
aEncryMsgArry[4] = Buffer[12];
return 1;
}

此处读取结构为:

&Buffer[0]: aUserName : 16 byte

&Buffer[4] ^ PEHEADENCRYPT[i]: &aEncryMsgArry[5] : 16byte

&Buffer[8]: &aEncryMsgArry[0] : 16byte

Buffer[12]: aEncryMsgArry[0] : 4byte

&Buffer[4] ^ PEHEADENCRYPT[i] 意味着我想要拿到真正的 aEncryMsgArry5还要与 sm3(.text节) 加密后 xor

众所周知.text 存储着 代码,意味着,如果直接下普通断点调试,就可能会导致 sm3(.text节表)取得的值异常。

这里有两种方案:

  1. 下硬件断点
  2. dump内存,取出 .text 节, 因为取的是内存中的节数据,并非文件中的节数据,所以只能 dump 内存

然后再验证读取的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
int CheckRegData()
{

size_t nCount = 32;

DWORD n0 = 0;
DWORD n1 = ((aEncryMsgArry[5] << 7) | (aEncryMsgArry[7] & 0x0FE03FFFF)) << 0x12;
DWORD n2 = ((aEncryMsgArry[6] << 7) | (aEncryMsgArry[8] & 0x0FE03FFFF)) << 0x12;
DWORD n3 = aEncryMsgArry[8] & 0x1FFFFFF;
DWORD n4 = aEncryMsgArry[7] & 0x1FFFFFF;

//DWORD n5 = aEncryMsgArry[3];
//DWORD n6 = aEncryMsgArry[2];

DWORD n5 = 0x1127;
DWORD n6 = 0x1852;


while (nCount-- != 0)
{
n6 += ((n0 - 0x0C39582) + n5) ^ ((n5 << 5) + n2) ^ ((n5 << 4) + n1);
n6 = n6 & 0x1FFFFFF;
n5 += ((n0 - 0x0C39582) + n6) ^ ((n6 << 5) + n3) ^ ((n6 << 4) + n4);
n5 = n5 & 0x1FFFFFF;
n0 = n0 + 0x13C6A7E;
}

if (n6 == 0x1852 && n5 == 0x1127)
{
// Sucess
}
else
{
// Fail
}
}

该逻辑可逆

此处验证了 加密数据中的 2、3 , 5、6、7、8 其中 5,6,7,8 可以由用户自定义构造,后可逆推 2,3 的内容。而5,6,7,8刚好是 SM3(用户名)。

1
2
3
4
5
int sub_401220()
{
IsSucess &= aEncryMsgArry[0] == ((aEncryMsgArry_5 ^ 0xFF14F72A) & 0x1FFFFFF);
return 0;
}

此处 0, 必须等于 f(5) , 则1 可以通过 5 逆推。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
int sub_401250()
{
unsigned int v0; // edx
int v1; // ebx
int v2; // edi
int v3; // esi
int v5; // [esp+Ch] [ebp-14h]
int v6; // [esp+1Ch] [ebp-4h]

v0 = aEncryMsgArry_1 ^ aEncryMsgArry_6 & 0x1FFFFFF;
v6 = (v0 >> 15) & 0x1F;
v1 = (v0 >> 10) & 0x1F;
v5 = v0 & 0x1F;
v2 = (v0 >> 5) & 0x1F;
v3 = (v0 >> 20) & 0x1F;
if ( 9 * v6 + 11 * (v1 + v3) + 19 * (v5 + v2) == 918
&& 27 * ((v0 & 0x1F) + v6) + 6 * v2 + 4 * v1 + 23 * v3 == 1402
&& 11 * v2 + 16 * (v0 & 0x1F) + 3 * (v1 + 4 * v6) + 10 * v3 == 782
&& 9 * (v5 + v2 + 2 * v5) + 8 * (v3 + v1 + 2 * v3) + 31 * v6 == 1566
&& v6 + v5 + 15 * v1 + 28 * v2 + 24 * v3 == 680 )
{
IsSucess &= 1u;
return 0;
}
else
{
IsSucess = 0;
return 0;
}
}

此处 v0 经过 z3 计算,可以得到唯一确定的值.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 求 z3 解  aEncryMsgArry[1]
from z3 import *

n0,n1,n2,n3,n4 = z3.BitVecs("n0 n1 n2 n3 n4", 8)
v0 = z3.BitVec("v0", 32)

mySolver = z3.Solver()

mySolver.add(v0 == (ZeroExt(24, n4) << 20) | (ZeroExt(24, n3) << 15) |(ZeroExt(24, n2) << 10) |(ZeroExt(24, n1) << 5) |(ZeroExt(24, n0) << 0) )
mySolver.add(9 * n3 + 11 * (n2 + n4) + 19 * (n0 + n1) == 918)
mySolver.add(27 * ((v0 & 0x1F) + ZeroExt(24,n3)) + 6 * ZeroExt(24,n1) + 4 * ZeroExt(24,n2) + 23 * ZeroExt(24,n4) == 1402)
mySolver.add(11 * ZeroExt(24,n1) + 16 * (v0 & 0x1F) + 3 * (ZeroExt(24,n2) + 4 * ZeroExt(24,n3)) + 10 * ZeroExt(24,n4) == 782)
mySolver.add(9 * (n0 + n1 + 2 * n0) + 8 * (n4 + n2 + 2 * n4) + 31 * n3 == 1566)
mySolver.add(n3 + n0 + 15 * n2 + 28 * n1 + 24 * n4 == 680)

mySolver.check()
res = mySolver.model()
print(res)
# [n2 = 8, n4 = 10, n0 = 17, n1 = 10, n3 = 23, v0 = 11247953]
# hex(11247953) = 0xaba151

最终的 0 可以通过 5 来推算出来, 1 通过 6 推算出来。

4 为 CheckInput 时进行的验证中的确定值,可通过 0,1,2,3 逆推。

那么所有的逻辑就摸清楚了。

注册机编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#include<Windows.h>
#include<iostream>

#include"GmSSL-develop/include/gmssl/sm3.h"

int SM3Encrypt(uint8_t* buf, size_t len, uint8_t* dgst)
{
SM3_CTX sm3_ctx;
//uint8_t dgst[32];

sm3_init(&sm3_ctx);
sm3_update(&sm3_ctx, buf, len);
sm3_finish(&sm3_ctx, dgst);
return 0;
}
unsigned char aStringTable[0x21] = { "23456789ABCDEFGHJKLMNPQRSTUVWXYZ" };

void Solver()
{
// 输入的用户名
uint8_t szUserName[] = "TEST";
// 加密后的数据内容
DWORD aEncryMsgArry[9] = { 0 };
DWORD OutBuffer[32] = { 0 };

SM3Encrypt(szUserName, strlen((const char*)szUserName), (uint8_t*)OutBuffer);

aEncryMsgArry[5] = OutBuffer[0] ^ OutBuffer[1];
aEncryMsgArry[6] = OutBuffer[2] ^ OutBuffer[3];
aEncryMsgArry[7] = OutBuffer[4] ^ OutBuffer[5];
aEncryMsgArry[8] = OutBuffer[6] ^ OutBuffer[7];

// 动态取得
DWORD PEHEADENCRYPT[8] = { 0x26e9458a, 0x3c13520b, 0xdef20ace, 0xa703f8c1, 0x43dc0b29, 0x7e4a7fa7, 0x725366c3, 0x80680cfc };
//SM3Encrypt(hexData, 0x2113, (uint8_t*)PEHEADENCRYPT);
for (int i = 0; i < 4; i++)
{
aEncryMsgArry[i + 5] = aEncryMsgArry[i + 5] ^ PEHEADENCRYPT[i];
}

DWORD n1 = ((aEncryMsgArry[5] >> 7) | (aEncryMsgArry[7] & 0x0FE03FFFF)) >> 0x12;
DWORD n2 = ((aEncryMsgArry[6] >> 7) | (aEncryMsgArry[8] & 0x0FE03FFFF)) >> 0x12;
DWORD n3 = aEncryMsgArry[8] & 0x1FFFFFF;
DWORD n4 = aEncryMsgArry[7] & 0x1FFFFFF;

DWORD n5 = 0x1127;
DWORD n6 = 0x1852;

// 经过32轮反转,可拿到 2,3 的值
int nCount = 32;
int nTest = 0;
DWORD n0 = 0x13C6A7E * 32;
do
{
n0 = n0 - 0x13C6A7E;
n5 -= ((n0 - 0x0C39582) + n6) ^ ((n6 >> 5) + n3) ^ ((n6 << 4) + n4);
n5 = n5 & 0x1FFFFFF;

n6 -= ((n0 - 0x0C39582) + n5) ^ ((n5 >> 5) + n2) ^ ((n5 << 4) + n1);
n6 = n6 & 0x1FFFFFF;
nCount--;
} while (nCount);

// 0 1 可逆
aEncryMsgArry[0] = ((aEncryMsgArry[5] ^ 0xFF14F72A) & 0x1FFFFFF);
aEncryMsgArry[1] = 0xaba151 ^ aEncryMsgArry[6];
aEncryMsgArry[2] = n6;
aEncryMsgArry[3] = n5;

// 4 经过分析 CheckInput 之后可逆
aEncryMsgArry[4] = \
((((((((((aEncryMsgArry[0] >> 5) ^ aEncryMsgArry[0]) >> 5) ^ aEncryMsgArry[0]) >> 5) ^ aEncryMsgArry[0]) >> 5) ^ aEncryMsgArry[0]) & 0x1f) << 20) |
((((((((((aEncryMsgArry[1] >> 5) ^ aEncryMsgArry[1]) >> 5) ^ aEncryMsgArry[1]) >> 5) ^ aEncryMsgArry[1]) >> 5) ^ aEncryMsgArry[1]) & 0x1f) << 15) |
((((((((((aEncryMsgArry[2] >> 5) ^ aEncryMsgArry[2]) >> 5) ^ aEncryMsgArry[2]) >> 5) ^ aEncryMsgArry[2]) >> 5) ^ aEncryMsgArry[2]) & 0x1f) << 10) |
(((((((((aEncryMsgArry[3] >> 5) ^ aEncryMsgArry[3]) >> 10) ^ (aEncryMsgArry[3] >> 5)) ^ aEncryMsgArry[3]) >> 5) ^ aEncryMsgArry[3]) & 0x1f) << 5) |
((aEncryMsgArry[0] ^ (aEncryMsgArry[1] ^ aEncryMsgArry[2] ^ aEncryMsgArry[3] ^ ((aEncryMsgArry[0] ^ (aEncryMsgArry[0] >> 5) ^ aEncryMsgArry[1] ^ (aEncryMsgArry[1] >> 5) ^ aEncryMsgArry[2] ^ (aEncryMsgArry[2] >> 5) ^ aEncryMsgArry[3] ^ (aEncryMsgArry[3] >> 5) ^ ((aEncryMsgArry[0] ^ (aEncryMsgArry[0] >> 5) ^ aEncryMsgArry[1] ^ (aEncryMsgArry[1] >> 5) ^ aEncryMsgArry[2] ^ (aEncryMsgArry[2] >> 5) ^ aEncryMsgArry[3] ^ (aEncryMsgArry[3] >> 5)) >> 10)) >> 5))) & 0x1f) ;


// 打印密码,注意还原需要从前向后顺序打印
for (int i = 0; i < 5; i++)
{
DWORD nTmp = aEncryMsgArry[i];

for (int j = 4; j >= 0; j--)
{
printf("%c", aStringTable[(nTmp >> (5 * j)) & 0x1f]);
}
printf("-");
}
}

给定一组正确的注册码

1
2
test
HSRNJ-35YYZ-DECEK-QQARR-6XJA3

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!