作为一个啥都会点的高一人,逆向一直是拿来玩玩和用在不能说的地方()
今年看到吾爱有逆向题目领红包,就过来试试水(^^ゞ
1. Windows 初级题
首先将程序拉进 ExeinfoPe 查壳, 一看没壳,还是32位,先拉 IDA 里面去静态分析
拉进 IDA 之后直接 F5 看伪C
整体的逻辑也很简单,先检查输入的字符串长度是不是29,符合的话就进入循环与预定的flag比较,如果正确打印Success,错误打印Wrong。
把伪C代码放进 ChatGPT 分析,也得到了相同的结论
双击dword_43F000跳转到 IDA View,先调整一下数组大小,再导出数组
导出数组后用Python打印预定的flag,得到flag为 flag{52PoJie2023HappyNewYear}
2. Windows 中级题
2.1 脱壳
首先运行一下程序,发现程序需要输入UID和Key,并且用了GUI
将程序拉进 ExeinfoPe 查壳,UPX的壳
用upx -d脱壳报错,怀疑对解压缩的结构体动了手脚,直接拉进x64dbg用rsp定律脱壳
拖进x64dbg发现有反调试,直接SharpOD一套带走,成功加载
用RSP定律脱壳,到了OEP直接拿Scylla Dump,运气不错可以自动修IAT
同时发现导入表中有 GetDlgItemTextW,大概率用来获取用户输入
2.2 初步静态分析
将修好IAT的脱壳后程序扔进 IDA 里面分析,寻找 GetDlgItemTextW 的交叉引用,发现有两个函数引用
进入sub_7FF6B7371A20 发现这是一个获取用户输入的函数 至于输入的是UID还是Key暂时还不知道 继续查看交叉引用找到主要逻辑函数sub_7FF6B73711D0
同时在sub_7FF6B7371A20中看到了 qword_7FF6B7387C90 + 偏移
的函数调用方式,怀疑是自己实现的导入表,点进去一看,确实是-O-
修改 qword_7FF6B7387C90 数组长度为20 (因为qword_7FF6B7387C90的第0位和第17-19位为零),导出地址之后一个个输入到x64dbg里面获取具体的API,最后处理结果为
qword_7FF6B7387C90[1] : user32.GetDlgItemInt
qword_7FF6B7387C90[2] : user32.GetDlgItemTextW
qword_7FF6B7387C90[3] : user32.SendMessageW
qword_7FF6B7387C90[4] : user32.LoadIconW
qword_7FF6B7387C90[5] : user32.MessageBoxW
qword_7FF6B7387C90[6] : user32.EndDialog
qword_7FF6B7387C90[7] : user32.GetDlgItem
qword_7FF6B7387C90[8] : user32.SetFocus
qword_7FF6B7387C90[9] : user32.GetDlgCtrlID
qword_7FF6B7387C90[10] : user32.SetWindowPos
qword_7FF6B7387C90[11] : user32.OffsetRect
qword_7FF6B7387C90[12] : user32.CopyRect
qword_7FF6B7387C90[13] : user32.GetWindowRect
qword_7FF6B7387C90[14] : user32.GetDesktopWindow
qword_7FF6B7387C90[15] : user32.GetParent
qword_7FF6B7387C90[16] : user32.SendDlgItemMessageW
由于sub_7FF6B7371A20中调用的是 user32.GetDlgItemInt (qword_7FF6B7387C90[1]),于是可以确定sub_7FF6B7371A20为获取UID的函数 v10为返回的UID
同理可得sub_7FF6B7371FC0为获取Key的函数 v18为返回的Key字符串
之后将UID和Key作为参数调用sub_7FF6B7372110 返回值存储在v12中
2.3 sub_7FF6B7372110算法分析
直接进入sub_7FF6B7372110,一眼发现有一个循环没有结束条件,直接看汇编,还原代码
补充:看其他师傅的文章发现这个循环貌似是没问题的,算我这里分析出了问题`(>﹏<)′
分析代码后,发现主要是将输入的Key作为一个int数组,循环这个数组,将每个元素的前后16位调换+异或特定魔数后,输入sub_7FF6B7371D70进行处理
并将处理后的值于一个同样经过sub_7FF6B7371D70处理过的数组qword_7FF6B73868F0比较,如果数组中的所有值相等,返回0,反之返回v11
ChatGPT的解释也验证了我们的分析 (见下图)
这就是说,只要我们输入的值处理后与qword_7FF6B73868F0的值相等,就可以让sub_7FF6B7372110返回0,用Python写出逆运算算法之后算出Key数组如下
wchar_t Key_1[] = {102, 108, 97, 103, 123, 61135, 61135, 13074, 13075, 4441, 4429, 30503, 30519, 4424, 4422, 13181, 13165, 4422, 4439, 65446, 65441, 4432, 4443, 13164, 13152, 4432, 4430, 30577, 30576, 4400, 4407, 13074, 13075, 0};
结果,将Key1输入时依旧报错,看来这并不是判断Key是否正确的函数 / \
2.4 动态调试 + 更多的静态分析
在 switch ( (unsigned __int16)a3 )
处下断点,对应汇编 cmp eax, 1
处,输入Key,点击确定,发现这一处被触发了3次,同时eax第一次为1,剩下两次为0x300,说明对Key做校验的函数在0x300的分支中
回到 IDA ,0x300的分支先通过比较字符串得到v6的值,再根据v6走不同的分支,具体解释可以看ChatGPT给出的解释
同时根据调试器的结果,可以判断第一次走的是 v6 == 0
的分支,第二次走的是 v6 != 0
的分支,而 v6 != 0
的分支调用了 user32.MessageBoxW (qword_7FF6B7387C90[5]),可以确定第二次走0x300分支为输出结果,那么第一次走0x300分支就是对Key进行校验了
根据调试器中a4输出的结果,发现第一次的a4为 sub_7FF6B7372110 的返回值,第二次的a4是一个flag,用于指定MessageBoxW输出的值
进入 v6 == 0
分支,可以分析出 sub_7FF6B73725E0 和 sub_7FF6B7372840 分别为 GetUID 和 GetKey (具体分析懒得写了,基本上有两种方法判断,一是从控件的资源ID判断,UID为1000,Key的为1001;二是从返回值和具体的前后文判断),初步分析的代码如下
2.4 CheckKey分析
CheckKey的整体逻辑也很简单: 将经过sub_7FF6B73726E0处理过的ProcessedKey数组与v16数组比较,如果全部正确,返回值为4,反正返回值为3,计算魔数那里在还原的时候直接照抄即可
而v16数组的值为 char v16[] = "flag{!!!_HAPPY_NEW_YEAR_2023!!!}"
,将其转换为unsigned int数组结果为 {0x67616c66, 0x2121217b, 0x5041485f, 0x4e5f5950, 0x595f5745, 0x5f524145, 0x33323032, 0x7d212121}
,也就是说,当我们输入的Key经过sub_7FF6B73726E0的运算处理后与v16相等时,这个Key就是正确的,所以我们将v16经过sub_7FF6B73726E0的逆运算后,即可解出这道题的Key
补充:后面看其他师傅的解释才知道sub_7FF6B73726E0是一个tea的解密函数,还是经验不足呀 (⊙ˍ⊙)
逆运算的C++代码如下
void reverse_sub_7ff6b73726e0(unsigned int* arg1, _DWORD* a2, int a3, unsigned int a4)
{
unsigned int v5 = arg1[0];
unsigned int v6 = arg1[1];
for (int i = 0; i < 32; i++) {
a4 += a3;
v5 += (a2[1] + (v6 >> 5)) ^ (a4 + v6) ^ (*a2 + 16 * v6);
v6 += (a2[3] + (v5 >> 5)) ^ (a4 + v5) ^ (a2[2] + 16 * v5);
}
arg1[0] = v5;
arg1[1] = v6;
}
其中这里还有一个问题,就是 reverse_sub_7ff6b73726e0 中 a4 的初始值如何确定,将 sub_7FF6B73726E0 算法还原之后进行动态调试,得到下面的表格,可以很明显的看出 sub_7FF6B73726E0 最后会将 a4 递减至0,所以 reverse_sub_7ff6b73726e0 中 a4 的初始值为0
至于a2和a3的值可直接从CheckKey里的魔数生成部分中抄下来
最后还原出 ProcessedKey 数组的值应为 {0x805b431,0xc46f31a2,0x67d178e8,0xb1d33200,0x17d8e19b,0xc1266b7d,0xc5bbd440,0xfb25dbda}
2.5 得到最终的Key
由于ProcessedKey的值由 ProcessKey 函数得来,我们需要对ProcessKey进行分析,进而得到真正的用户应该输入的Key
根据ChatGPT的解释,ProcessKey是用来将字符串转换成整数的,同时提醒了我们Key的长度应该是8的倍数
然后让ChatGPT写出ProcessKey的逆函数,但是调试之后怪怪的......
随后查看a1的值,发现ProcessedKey的前8个字符已经被还原,并且全部字符为大写,再根据上面对ProcessKey的解释,猜测出用户输入应该是ProcessedKey数组的16进制
撸几行代码测试一下,最终得到正确的Key,成功拿下ヾ(^▽^*)))
文章末尾附上注册机代码如下
#include <iostream>
// 由ChatGPT生成
wchar_t* reverse_sub_7FF6B73724A0(unsigned int input) {
wchar_t* result = new wchar_t[9];
result[8] = 0;
for (int i = 7; i >= 0; i--) {
result[i] = input % 16;
if (result[i] < 10) {
result[i] += 48;
}
else {
result[i] += 55;
}
input /= 16;
}
return result;
}
void sub_7ff6b73726e0(unsigned int* arg1, unsigned int* a2, int a3, unsigned int a4) {
unsigned int v5 = arg1[0];
unsigned int v6 = arg1[1];
for (int i = 0; i < 32; i++) {
a4 += a3;
v5 += (a2[1] + (v6 >> 5)) ^ (a4 + v6) ^ (*a2 + 16 * v6);
v6 += (a2[3] + (v5 >> 5)) ^ (a4 + v5) ^ (a2[2] + 16 * v5);
}
arg1[0] = v5;
arg1[1] = v6;
}
int main()
{
int v11 = 0x11111111;
for (int i = 0; i < 14; ++i)
{
v11 += 0x11111111;
}
int UID;
std::cout << "UID: ";
std::cin >> UID;
std::cout << "\n";
int k = v11 + UID;
int v17[4];
while ((k & 0x80000000) == 0)
k = k + k + 9;
for (int m = 0; m < 4; ++m)
v17[m] = (m + 1) * (k + 1);
unsigned int* v16 = new unsigned int[8] { 0x67616c66, 0x2121217b, 0x5041485f, 0x4e5f5950, 0x595f5745, 0x5f524145, 0x33323032, 0x7d212121 };
for (int n = 0; n < 4; n++)
sub_7ff6b73726e0((v16 + (2 * n)), (unsigned int*)v17, (unsigned int)k, 0);
wchar_t* a1;
std::cout << "Key: ";
for (int n = 0; n < 8; n++)
{
a1 = reverse_sub_7FF6B73724A0(v16[n]);
std::wcout << a1;
}
std::cout << "\n";
return 0;
}
3. 总结
这篇文章其实还有一些东西没写,像 0x37异或字符串算法,Block数组的还原 都没有详细去讲,因为这两个一个是没多大用处,一个可以直接抄IDA生成的伪C代码,所以也就不浪费空间写这两个了
这次的Windows中级题相对来说难度还行,主要是不熟悉Windows原生的Gui函数拖延了很多时间,在调试和理解代码这方面也花了很多时间,不过确实也学到了很多东西 (^^ゞ