作业文件见: https://github.com/m1n9yu3/2022chunqiubei_RetroRegister
题目逻辑分析
一个重启验证。
程序流程为,
- 用户输入用户名和密码
- 进入 CheckInput 中检查输入
- 判断输入是否符合要求, 写 reg.dat 文件
重启程序时
读取 reg.dat 文件 ReadRegData
验证 reg.dat 文件是否符合逻辑
- CheckRegData 验证 2 3
- sub_401220 验证 0
- sub_401250 验证 1
- 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) { DWORD OutBuffer[8] = { 0 }; unsigned char aStringTable[0x21] = { "23456789ABCDEFGHJKLMNPQRSTUVWXYZ" };
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;
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; } if (*pEncryPtAr == 0) { return 0; } nCount += 1; if (nCount >= 5) { if (j > 0x1d) { break; } if (szPassWord[j] != '-') { return 0; } pEncryPtAr = pEncryPtAr + 1; if ((DWORD*)pEncryPtAr < &(aEncryMsgArry[5])) { nCount = 0; *pEncryPtAr = 0; } else { break; } }
}
int nTmp = 0;
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];
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() { HMODULE GetModuleHandleA(NULL); 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; }
for (int i = 0; i < 4; i++) { *((DWORD*)aUserName + i) = Buffer[i]; } for (int i = 0; i < 4; i++) { aEncryMsgArry[i + 5] = Buffer[i + 4] ^ PEHEADENCRYPT[i]; }
for (int i = 0; i < 4; i++) { aEncryMsgArry[i] = Buffer[i + 8]; } 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节表)取得的值异常。
这里有两种方案:
- 下硬件断点
- 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 = 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) { } else { } }
|
该逻辑可逆
此处验证了 加密数据中的 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; int v1; int v2; int v3; int v5; int v6;
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
| 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)
|
最终的 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;
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 }; 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;
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);
aEncryMsgArry[0] = ((aEncryMsgArry[5] ^ 0xFF14F72A) & 0x1FFFFFF); aEncryMsgArry[1] = 0xaba151 ^ aEncryMsgArry[6]; aEncryMsgArry[2] = n6; aEncryMsgArry[3] = n5;
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
|