DNLINYJ 发表于 2023-2-6 00:43

高一人的第一次正经逆向——吾爱2023新年领红包Windows题分析

本帖最后由 DNLINYJ 于 2023-2-7 21:24 编辑

作为一个啥都会点的高一人,逆向一直是拿来玩玩和用在不能说的地方()

今年看到吾爱有逆向题目领红包,就过来试试水(^^ゞ

## 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 : user32.GetDlgItemInt
qword_7FF6B7387C90 : user32.GetDlgItemTextW
qword_7FF6B7387C90 : user32.SendMessageW
qword_7FF6B7387C90 : user32.LoadIconW
qword_7FF6B7387C90 : user32.MessageBoxW
qword_7FF6B7387C90 : user32.EndDialog
qword_7FF6B7387C90 : user32.GetDlgItem
qword_7FF6B7387C90 : user32.SetFocus
qword_7FF6B7387C90 : user32.GetDlgCtrlID
qword_7FF6B7387C90 : user32.SetWindowPos
qword_7FF6B7387C90 : user32.OffsetRect
qword_7FF6B7387C90 : user32.CopyRect
qword_7FF6B7387C90 : user32.GetWindowRect
qword_7FF6B7387C90 : user32.GetDesktopWindow
qword_7FF6B7387C90 : user32.GetParent
qword_7FF6B7387C90 : user32.SendDlgItemMessageW
```

由于sub_7FF6B7371A20中调用的是 user32.GetDlgItemInt (qword_7FF6B7387C90),于是可以确定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};
```

结果,将Key_1输入时依旧报错,看来这并不是判断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),可以确定第二次走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++代码如下
```cpp
void reverse_sub_7ff6b73726e0(unsigned int* arg1, _DWORD* a2, int a3, unsigned int a4)
{
      unsigned int v5 = arg1;
      unsigned int v6 = arg1;

      for (int i = 0; i < 32; i++) {
                a4 += a3;
                v5 += (a2 + (v6 >> 5)) ^ (a4 + v6) ^ (*a2 + 16 * v6);
                v6 += (a2 + (v5 >> 5)) ^ (a4 + v5) ^ (a2 + 16 * v5);
      }
      arg1 = v5;
      arg1 = 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,成功拿下ヾ(^▽^*)))





文章末尾附上注册机代码如下

```cpp
#include <iostream>

// 由ChatGPT生成
wchar_t* reverse_sub_7FF6B73724A0(unsigned int input) {
      wchar_t* result = new wchar_t;
      result = 0;
      for (int i = 7; i >= 0; i--) {
                result = input % 16;
                if (result < 10) {
                        result += 48;
                }
                else {
                        result += 55;
                }
                input /= 16;
      }
      return result;
}

void sub_7ff6b73726e0(unsigned int* arg1, unsigned int* a2, int a3, unsigned int a4) {
      unsigned int v5 = arg1;
      unsigned int v6 = arg1;

      for (int i = 0; i < 32; i++) {
                a4 += a3;
                v5 += (a2 + (v6 >> 5)) ^ (a4 + v6) ^ (*a2 + 16 * v6);
                v6 += (a2 + (v5 >> 5)) ^ (a4 + v5) ^ (a2 + 16 * v5);
      }
      arg1 = v5;
      arg1 = 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;
      while ((k & 0x80000000) == 0)
                k = k + k + 9;
      for (int m = 0; m < 4; ++m)
                v17 = (m + 1) * (k + 1);

      unsigned int* v16 = new unsigned int { 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);
                std::wcout << a1;
      }

      std::cout << "\n";

      return 0;
}
```

## 3. 总结

这篇文章其实还有一些东西没写,像 0x37异或字符串算法,Block数组的还原 都没有详细去讲,因为这两个一个是没多大用处,一个可以直接抄IDA生成的伪C代码,所以也就不浪费空间写这两个了

这次的Windows中级题相对来说难度还行,主要是不熟悉Windows原生的Gui函数拖延了很多时间,在调试和理解代码这方面也花了很多时间,不过确实也学到了很多东西 (^^ゞ

156608225 发表于 2023-2-6 00:54

ChatGPT都用上了可还行。。。

86618513 发表于 2023-2-6 01:39

感谢楼主分享,还没看懂

爱飞的猫 发表于 2023-2-7 01:52

melchior 发表于 2023-2-6 14:34
感谢大佬分享,有个小问题想问下:

windows初级里这一步导出43F000这一步里,上图是怎么操作得来的?


把变量类型改成 DWORD 或 uint32_t 再导出就是对的了

taichao 发表于 2023-2-6 08:41

初级题看看还行,中级的直接懵逼。

darksied 发表于 2023-2-6 08:57

ChatGPT功能这么强了?

w759003376 发表于 2023-2-6 09:28

看起来挺厉害了

galepai 发表于 2023-2-6 09:33

大神啊。
这一看,自己的道路还非常漫长啊。。

天行键丶 发表于 2023-2-6 09:39

darksied 发表于 2023-2-6 08:57
ChatGPT功能这么强了?

很强,可以节约点时间,让gpt帮忙分析下代码逻辑,但真正强的还是楼主。gpt有时候会输出莫名其妙的东西的,得自己会分辨。不过对比市面上其他ai,chatgpt直接乱杀。

bohasun 发表于 2023-2-6 10:01

ChatGPT真的强

wasdzjh 发表于 2023-2-6 10:17

chatgpt这么强,赶紧安装一个
页: [1] 2 3 4 5
查看完整版本: 高一人的第一次正经逆向——吾爱2023新年领红包Windows题分析