吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1093|回复: 13
收起左侧

[CTF] 【2025春节】解题领红包之五 {Windows 高级题} 简单版本 第一视角 writeup

[复制链接]
御史神风 发表于 2025-2-13 00:16
本帖最后由 御史神风 于 2025-2-13 19:19 编辑

这次做太晚了,差了6分钟,没做完
uid = 2353116 flag{9c62eaeafae1184d3cf7d582c3a276b9367241e026697027}

解题脚本

[C] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
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 <stdio.h>
#include <sysinfoapi.h>
#include <stdint.h>
int h2i(char c){
    int x = c - '0';
    if (x <= 9)
        return x;
    return c - 'a' + 10;
}
const int R = 12;
void op25_dec (uint32_t* v, uint32_t* k) {
    uint32_t v0=v[0], v1=v[1], sum=0, i;           /* set up */
    uint32_t delta=0xb979379e;                     /* a key schedule constant */
    uint32_t k0=k[0], k1=k[1], k2=k[2], k3=k[3];   /* cache key */
    for (i=0; i < R; i++) {                       /* basic cycle start */
        sum += delta;
        v1 += ((v0<<4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3);
        v0 += ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1);
    }                                              /* end cycle */
    v[0]=v0; v[1]=v1;
}
void op25_enc (uint32_t* v, uint32_t* k) {
    uint32_t v0=v[0], v1=v[1], sum=0xb979379e*R, i;  /* set up */
    uint32_t delta=0xb979379e;                     /* a key schedule constant */
    uint32_t k0=k[0], k1=k[1], k2=k[2], k3=k[3];   /* cache key */
    for (i=0; i < R; i++) {                         /* basic cycle start */
        v0 -= ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1);
        v1 -= ((v0<<4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3);
        sum -= delta;
    }                                              /* end cycle */
    v[0]=v0; v[1]=v1;
}
void enc(char *password, int uid) {
    uint64_t buf[2] = {0,};
    uint8_t *p = (uint8_t *)buf;
    uint64_t a, b, c;
    // 0x15
    for (int i = 0; i < 32; i += 2) {
        p[i >> 1] = (h2i(password[i]) << 4) | h2i(password[i+1]);
    }
    printf("buf = ");
    for (int i = 0; i < 16; i++) {
        printf("%x ", p[i]);
    }
    printf("\n");
    // 0x25
    uint32_t key0[4] = {0xD7851B65, 0x473457C1, 0x1231F787, 0x9ACD6D9A};
    op25_enc((uint32_t *)buf, key0);
    uint32_t key1[4] = {0xB728E994, 0x1746382E, 0xC52D865C, 0x10778A6E};
    op25_enc((uint32_t *)(&buf[1]), key1);
    printf("buf = %llX %llX\n", buf[0], buf[1]);
    // 0x55
    // 0x35
    FILETIME time;
    GetSystemTimePreciseAsFileTime(&time);
    a = 1800 * ((*(uint64_t *)&time / 10000000 - 11644473600LL) / 1800);
    printf("a = %lld\n", a);
    // check
    printf("%llX == %llX, %llX == %llX\n", a, buf[0], uid, buf[1]);
}
void dec(int uid) {
    uint64_t buf[3] = {0, };
    uint8_t *p = (uint8_t *)buf;
    FILETIME time;
    GetSystemTimePreciseAsFileTime(&time);
    uint64_t a = 1800 * ((*(uint64_t *)&time / 10000000 - 11644473600LL) / 1800);
    buf[0] = a;
    buf[1] = uid;
    buf[2] = 0x0404040400000000 | 0xA0C4D3EC; // 0xA0C4D3EC通过调试得到
    uint32_t key0[4] = {0xD7851B65, 0x473457C1, 0x1231F787, 0x9ACD6D9A};
    op25_dec((uint32_t *)buf, key0);
    uint32_t key1[4] = {0xB728E994, 0x1746382E, 0xC52D865C, 0x10778A6E};
    op25_dec((uint32_t *)(&buf[1]), key1);
    uint32_t key2[4] = {0x7459F437, 0x90D1E5D, 0x779375B2, 0xEFCB8541};
    op25_dec((uint32_t *)(&buf[2]), key2);
    printf("buf = ");
    for (int i = 0; i < 24; i++) {
        printf("%02x", p[i]);
    }
    printf("\n");
}
int main() {
    char password[] = "bae8bc715b8329433ebf884fbf3bbd9d";
    // 2353116
    // flag{9c62eaeafae1184d3cf7d582c3a276b9367241e026697027}
    
    enc(password, 2353116);
    dec(2353116);
    return 0;
}


1 初探

做的简单版本

一开始没运行之间拉IDA,没注意是个对话框程序,以为是混淆,在TLS回调和start上浪费了点时间,回调中有个CriticalSection,不是确定是编译器生成还是有什么用,后面没关注了。

看start的时候,感觉不对劲,很像编译器生成的代码,才想起来运行了一下是GUI,然后开始关注API。

2 MessageBox入手

因为有弹窗,找了一下__imp_MessageBoxW的交叉引用(找MessageBoxW的交叉引用找不到,没有用这个thunk),有5处调用,字符串都上了混淆。


用unicorn去call这几个使用了MessageBox的函数(用unicorn这个模拟执行框架加载PE和调用函数的框架,有空整理一下再发出来)。
hook MessageBox读传参拿到明文。
(以下均为[推测函数名]_[RVA])
EnvError_3300 是环境异常。
About_4640 是关于。
Err_4E40 根据参数不同有四种文本,分别是未知错误、密码错误、环境异常、环境异常。
Succ_5480 是成功提示,会输出密码,密码在参数中。
EnvError_5E40 是环境异常。


其中 EnvError_5E40 的前面还调用了 DialogBoxParamW API


3 WinMain_5E40 分析


DialogBoxParamW 是用于创建对话框的,猜测5E40这个函数是 WinMain。


开头memset了栈上0xB8的数据,这是第一个结构体命名为S1吧,根据对后续函数的分析,最终还原出是这样的:
[C] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
struct S1
{
  int dd0;
  char db4_10h;
  char db5;
  __int16 dw6;
  S3 *ps3;
  void *funcs[16];
  _LIST_ENTRY *Flink;
  HWND hWnd;
  HWND DlgItem2_uid;
  HWND DlgItem3_pw;
  DLGPROC DialogProc;
};



接着调用了f_3E80,这是个变参函数会将后续函数指针填充到s1.funcs[]中,根据下标对这些函数重新命名。
[C++] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
f_3E80(
&s1,
F0_Err_4E40,
F1_5180,
F2_About_4640,
F3_Succ_5480,
F4_Close_5160,
F5_4740,
F6_55C0,
0LL);



接着调用CreateThreads_4430,创建了4个线程,用一个结构S3存储了线程相关的信息,详细后面分析。


然后就是调用 DialogBoxParamW,根据参数找到对话框消息处理函数 DialogProc_5800。


4 DialogProc_5800 对话框消息处理分析


4.1 对话框消息处理函数


该函数是对话框消息的处理函数,初始化会执行一次,然后每次点击GUI也都会执行这个函数,具体上下文细节就不是很清楚了,不过不影响做题。
具体定义如下:
__int64 __fastcall DialogProc_5800(HWND hWnd, __int64 Msg, __int16 wParam, S1 *ps1_1)
其中第4个参数的类型由 WinMain_5E40 调用 DialogBoxParamW 时的参数推断出。


稍微解释一下函数参数。
hWnd是对话框的句柄,至少在初始化时是可以简单理解为整个GUI的句柄。
Msg是消息,是个宏。WM_INITDIALOG是初始化。WM_CLOSE是关闭。WM_COMMAND可以当成是点击事件。
Msg = WM_COMMAND时,wParam 是点击的控件Id。


使用 Resource Hacker 可以查看控件信息。
UID(锁编号)输入框是2。
密码输入框是3。
关于按钮是5。
验证按钮是6。


所以找到Msg = WM_COMMAND,wParam = 6的位置,就是验证代码开始的地方。


4.2 验证按钮代码


这里先调用一些api获取了uid、密码,这些api就不赘述了。


然后检查密码格式,长度>=6,开头"flag{",结尾"}\x00"。


然后malloc了一个大小0x18的数据结构,称为S2吧,定义如下:
[C] 纯文本查看 复制代码
1
2
3
4
5
6
7
8
9
struct S2
{
char op;
char isFreeS2;
__int16 dw2;
int dd4;
_QWORD uid;
char *pw;
};

用该结构存储了uid和password,然后调用 CS_Queue_Push_4330。


5 多线程消息队列


这部分反复看了好几次才看出眉头。


5.1 单向链表实现的队列


这里涉及一个单向链表实现的队列。
void __fastcall Queue_Push_3520(List *pList, ListData *pData) 是队尾插入。
ListData *__fastcall Queue_Pop_34D0(List *pList) 是队首弹出。
void __fastcall Queue_Init_3450(List *a1) 是初始化。
分析这三个函数得到该单向列表的数据结构定义如下:
[C] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
struct List
{
  ListENTRY *head;
  ListENTRY *tail;
  _QWORD count;
};
 
 
struct ListENTRY
{
  ListData *pData;
  ListENTRY *next;
};
 
 
struct ListData
{
  S1 *ps1;
  S2 *ps2;
  _QWORD a4;
  S3 *ps3;
};



5.2 CS_Queue_Push_4330 多线程消息队列插入


CS_Queue_Push_4330 这个函数先是进入临界区,然后往队列插入消息,接着唤醒一个等待线程。


该等待线程就是 CreateThreads_4430 创建的4个线程之一。


5.3 CreateThreads_4430 多线程启动


这里创建了4个线程,用结构S3保存线程信息:
[C] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
struct S3
{
  struct _RTL_CRITICAL_SECTION CS;
  struct _RTL_CONDITION_VARIABLE CV;
  List s2List;
  void (__fastcall *Dispatcher_4DC0)(_QWORD, _QWORD, char *);
  bool isPop;
  char field_51;
  __int16 field_52;
  int field_54;
  _QWORD Threads[4];
};



每个子线程启动例程都是 ThreadRoutine_4510,子线程启动例程的参数的结构:
[C] 纯文本查看 复制代码
1
2
3
4
5
struct S5
{
  S3 *ps3;
  _QWORD tn;
};



5.4 ThreadRoutine_4510 子线程启动例程

子线程会等待 CS_Queue_Push_4330 的唤醒,然后从消息队列中取出消息(struct ListData)。
接着调用 Dispatcher_4DC0 处理消息。

5.5 Dispatcher_4DC0

该函数修复数据类型后如下:
[C++] 纯文本查看 复制代码
1
2
3
4
5
6
void __fastcall Dispatcher_4DC0(S1 *ps1, S2 *ps2, char *a4)
{
  ((void (__fastcall *)(S1 *, S2 *, char *))ps1->funcs[ps2->op & 0xF])(ps1, ps2, a4);
  if ( ps2->isFreeS2 )
    free(ps2);
}

根据S2.op的低4位选择要执行S1.funcs[]中的函数。
前面WinMain_5E40分析过,S1.funcs[]中一共初始化了7个函数。

5.6 多线程消息队列总结

这题使用单向链表实现了一个队列数据结构。
有一个全局的异步响应的消息队列在多个线程间共享。
CS_Queue_Push_4330 函数用于往队列插入消息。
4个启动例程为 ThreadRoutine_4510 的子线程负责从队列中取出消息。
这些子线程调用 Dispatcher_4DC0 分发消息到7个不同的处理函数中。


6 加密与解密


6.1 F1_5180 按钮处理函数


回顾 DialogProc_5800 中验证按钮的代码,op = 0x101,所以会在子线程中异步执行 F1_5180。


该函数先调用了 f_51E0,在这里面插入了4次消息事件,op分别为0x15、0x25、0x55、0x35,也就是会在子线程中执行 F5_4740 4次,这四次插入消息后都会执行 wait_2A10,是GUI和子线程之间是同步操作。


这四次对F5的调用,S2的结构体稍微有些不同。
[C] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
struct S2B
{
  char op;
  char isFreeS2;
  __int16 dw2;
  int dd4;
  S6 *ps6;
  __int64 a;
  __int64 b;
  __int64 c;
};



然后调用 f_50F0,该函数根据 f_51E0 的返回值,选择插入op=0x00或者op=0x30的消息,分别对应 F0_Err_4E40 密码错误和 F3_Succ_5480 密码正确。


6.2 加密过程


在 f_51E0 中完成了加密和校验。具体是由通过消息队列同步调用 F5_4740 4次完成加密,然后在 f_51E0 最后校验。
最后会比较前16字节是否等于时间戳和uid。


op = 0x15
相当于python的int(password, 16).to_bytes(16, 'big')。
将输入转成二进制数据。

op = 0x25
是tea的解密过程,delta=0xb979379e,轮数12,先算v0,再算v1。通过调试提取了三组key出来,详见脚本。
这段加密完后有一段代码,大概意思是取出加密后的最后一个字节x,然后判断最后x字节是否都相等,相等就返回长度-x。
在 f_51E0 会校验 长度-x = 20。
由于二进制输入前16字节等于时间戳和uid,所以猜测长度是24,x是就是4。
也就是最后8字节的高4字节为0x04040404。

op = 0x55
这段不知道具体在干什么。通过静态分析和调试,大概是根据op=0x25结果的前16字节生成一个4字节数据,然后和op=0x25的后8字节的低4字节进行比较。


op = 0x35
取时间,并按30min的粒度取整。


总的来说,就是先将hex格式的输入转成24字节的二进制数据。
然后校验:
tea(data)[0:8] == time
tea(data)[8:16] == uid
tea(data)[20:24] == 0x04040404
tea(data)[16:20] == op55(tea(data)[0:16])

可以先解密前16字节,然后通过调试得到op55(tea(data)[0:16]),拼出完整的flag。

免费评分

参与人数 3吾爱币 +3 热心值 +3 收起 理由
笙若 + 1 + 1 用心讨论,共获提升!
Lyscross + 1 + 1 学习
l223860 + 1 + 1 我还不会但是学习一下

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

爱飞的猫 发表于 2025-2-13 23:03

main 的那个变量的结构体是这样的(编译器可能会插字节使其对齐):

struct wnd_main_t
{
    uint32_t event_count; // 没用到
    uint8_t checksum_flag; // zero if good
    bool verify_in_progress; // 已经验证中就不能按了

    struct thread_pool_t *thread_pool;
    event_handler_t event_handlers[0x10];

    void *hInstance;
    void *hWnd;
    void *hEditLockId;
    void *hEditLockPass;

    wnd_proc_t wnd_proc;
};

op55 是一个轻度魔改过的老牌哈希算法,给喜欢完整分析算法的埋了个坑。

爱飞的猫 发表于 2025-2-13 09:35
其实没想着弄 VM,整了个异步的事件循环

主线程(负责 UI)+ 4 工作线程(处理事件)
crsky 发表于 2025-2-13 19:02
本帖最后由 crsky 于 2025-2-14 14:30 编辑

这算法只能是你那个时间戳计算出来才对,朝后改半小时,或者其它时间算出来都不对.
image.png
image.png

Hmily 发表于 2025-2-13 01:08
可惜了,早点参与就好了,不过现在还可以提交答案进行验证。
nanaqilin 发表于 2025-2-13 09:27
没关系的,你已经很优秀了,一开始带壳压根分析不了,也不会脱
等不带壳了,然后开始分析,分析了一天,这堆线程给我整的,我连入口都没找到
_莫逸 发表于 2025-2-13 11:42
给你个赞吧
 楼主| 御史神风 发表于 2025-2-13 17:55
爱飞的猫 发表于 2025-2-13 09:35
其实没想着弄 VM,整了个异步的事件循环

主线程(负责 UI)+ 4 工作线程(处理事件)

加密那也带点分发处理,感觉题目想做控制流的混淆

点评

[md]是的,想着让流程混乱一点,没那么容易看出来。 每个异步事件只干一件事,有点类似 JS 里的异步 `const result = await do_work(...params)` 那样。[/md]  详情 回复 发表于 2025-2-13 18:06
爱飞的猫 发表于 2025-2-13 18:06
御史神风 发表于 2025-2-13 17:55
加密那也带点分发处理,感觉题目想做控制流的混淆

是的,想着让流程混乱一点,没那么容易看出来。

每个异步事件只干一件事,有点类似 JS 里的异步 const result = await do_work(...params) 那样。

 楼主| 御史神风 发表于 2025-2-13 19:18
crsky 发表于 2025-2-13 19:02
这算法只能是你那个时间戳计算出来才对,朝后改半小时,或者其它时间算出来都不对,并且和uid没什么关系,B ...

更正了,脚本中有一个常量是通过调试得到的,我静态看应该是和flag的前16字节有关系,所以随时间会变。BMK分析了这个值是怎么计算的,但他脚本里好像是写死的。
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2025-3-20 00:55

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表