高一人的第一次正经逆向——吾爱2023新年领红包Windows题分析
2023-2-18 10:12:59 Author: 吾爱破解论坛(查看原文) 阅读量:13 收藏

作者坛账号:DNLINYJ

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

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

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函数拖延了很多时间,在调试和理解代码这方面也花了很多时间,不过确实也学到了很多东西 (^^ゞ

活动已结束,题目打包放到爱盘供大家下载学习(web也一起打包,可以重新下载):
https://down.52pojie.cn/Challenge/Happy_New_Year_2023_Challenge.rar

-官方论坛

www.52pojie.cn

--推荐给朋友

公众微信号:吾爱破解论坛

或搜微信号:pojie_52


文章来源: http://mp.weixin.qq.com/s?__biz=MjM5Mjc3MDM2Mw==&mid=2651138978&idx=2&sn=2edbfeb818125145cbe8b6bce187e3cb&chksm=bd50bbf68a2732e01698076e40c61fc4f591ce6d62f437124e86fdac773fda943c7d7bee6690#rd
如有侵权请联系:admin#unsafe.sh