ez_maze

这回v&N也是爆零了,等我有实力第一个投的绝对就是v&n,所以也是来wp大学习这是mfc逆向,mfc是微软提供的一种c++的类库,当然这道题还有upx壳

4

5

红了一片,upx0,upx1,upx2,upx!,upx的各种标志全没有了

一边来说运行两次然后找push就行了但这道题upx代码与源代码在

6

但是这道题并非常见的在两次运行,跳过前面的系统代码,并没见到pushad(通用寄存器),pushfd(标志寄存器),push,还有lea但是三次运行找到了jmp 112419_ezre.7FF6518D102A这一看就是主程序入口,

进去一看这题将显示的push rax等给整没了

1
LEA 目标寄存器, [内存寻址表达式]   //通过内存地址(相对地址,偏移量)计算出有效地址

不过还后留下了lea与push rdx,下访问断点,运行程序,直接到一堆pop这太对了

7

首先得用cff-exploer改配置

10

在头文件的characteristics选上executable

再在可选头文件的dllcharacteristics去除dll可移动

11

之后一直步过,找到大跳,进入后用scylla,脱壳结束(基本上是在千位上差,16进制的),因为版本的问题,所以xspy一开始演了我一把

12

找基址和偏移

13

这就是最后的迷宫函数

一开始一串赋值为0,不难看出起始点为(0,0)

srand(100u);这还有一个种子生成,参数为100

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
do
{
*((_DWORD *)v7 + 152) = 1;
*(_DWORD *)((char *)a1 + n1600 + 612) = 1;
*(_DWORD *)((char *)a1 + n1600 + 616) = 1;
*(_DWORD *)((char *)a1 + n1600 + 620) = 1;
*(_DWORD *)((char *)a1 + n1600 + 624) = 1;
*(_DWORD *)((char *)a1 + n1600 + 628) = 1;
*(_DWORD *)((char *)a1 + n1600 + 632) = 1;
*(_DWORD *)((char *)a1 + n1600 + 636) = 1;
*(_DWORD *)((char *)a1 + n1600 + 640) = 1;
*(_DWORD *)((char *)a1 + n1600 + 644) = 1;
*(_DWORD *)((char *)a1 + n1600 + 648) = 1;
*(_DWORD *)((char *)a1 + n1600 + 652) = 1;
*(_DWORD *)((char *)a1 + n1600 + 656) = 1;
*(_DWORD *)((char *)a1 + n1600 + 660) = 1;
*(_DWORD *)((char *)a1 + n1600 + 664) = 1;
*(_DWORD *)((char *)a1 + n1600 + 668) = 1;
*(_DWORD *)((char *)a1 + n1600 + 672) = 1;
*(_DWORD *)((char *)a1 + n1600 + 676) = 1;
*(_DWORD *)((char *)a1 + n1600 + 680) = 1;
*(_DWORD *)((char *)a1 + n1600 + 684) = 1;
n1600 += 80;
v7 = (CWnd *)((char *)v7 + 80);
}
while ( n1600 < 1600 );

看这个一看就是迷宫生成,先全赋值为1,之后部分赋值零,在循环中的列中刚好20,循环中的行也是0~19

也就是长20宽20

1
*((_DWORD *)a1 + 152) = 0;

将(0,0)位置置为0

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
#include<stdio.h>    //顺序右下左上
int a[10][10];
void f(int x, int y, int k)
{
a[x][y] = k;
if(y+1<=4&&a[x][y+1] == 0)
{
f(x, y+1, k+1);
}
if(x+1<=4&&a[x+1][y] == 0)
{
f(x+1, y,k+1);
}
if(y-1>0&&a[x][y-1]==0)
{
f(x, y-1, k+1);
}
if(x-1 >=1&&a[x-1][y]==0)
{
f(x-1, y, k+1);
}
}
int main(void)
{
f(1, 1, 1);
for(int i = 1; i<=4; i++)
{
for(int j = 1; j<=4;j++)
{
printf("%d ", a[i][j]);
}
printf("\n");
}
}

深度优先搜索,如果是自定义的迷宫,先要创建char a[10][10](int的全局变量会直接赋值为0,之后创建path函数[430][2]主要是存x与y)

迷宫生成两个算法:1.将墙当为线,将路当为网格2.把路和墙均当为网格(其实就是第一种的线变粗成网格)
所谓完美迷宫,就是没有回路(循环路),没有不可达区域的迷宫,并且迷宫中任意两个网格间都有唯一的路径。

直接dump首先在开始初始化的打断点去找地图位置(注意一个占四字节)

最后在最后要返回是打断点,在g跳到对应区域得到第图,然后深度搜索

得到

14

方法二

VNCTF2026 WP by YHalo - YHalo’s Blog

感谢大佬的方法,直接一个x64dbg解决,感觉我对x64dbg了解的还是太少了

8

先在内存布局中找到主模块,找到ezre(这一看就是主程序,直接在text下断点),之后运行

之后多点几次运行直到程序出来这时就可以去找text节区(存代码)

补冲data(存放已初始化的全局变量和局部静态变量),rdata(全局常量和字符串常量),arch(系统内部使用)

bss段(存放未始化的全局变量和局部静态变量)

找到后搜索当前模块的字符串

9

看到wsad这一看就是迷宫题,迷宫要找到起始点和迷宫的具体内容

00007FF7BCBE1953 | | xor ebx,ebx 关键操作将ebx寄存器归零

汇编dec为-1,inc为加1

根据汇编知道rsi(64位寄存器),rdi,r12d,r14,r15d

接下来

00007FF7BCBE1A7E | | cmp rsi,13 |
00007FF7BCBE1A82 | | ja 112419_ezre.7FF7BCBE1B01 |
00007FF7BCBE1A84 | | cmp rdi,13 (16进制数) |

也就是坐标的范围不可能超过19,所以坐标为0~19 //这里要注意x64dbg的数字均用16进制

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
00007FF7BCBE1A36 |  | call qword ptr ds:[<&Ordinal#4913>]    |
00007FF7BCBE1A3C | | cmp ax,61 | 61:'a'
00007FF7BCBE1A40 | | je 112419_ezre.7FF7BCBE1A76 |
00007FF7BCBE1A42 | | cmp ax,64 | 64:'d'
00007FF7BCBE1A46 | | je 112419_ezre.7FF7BCBE1A6C |
00007FF7BCBE1A48 | | cmp ax,73 | 73:'s'
00007FF7BCBE1A4C | | je 112419_ezre.7FF7BCBE1A60 |
00007FF7BCBE1A4E | | cmp ax,77 | 77:'w'
00007FF7BCBE1A52 | | jne 112419_ezre.7FF7BCBE1A7E | //这里是jne
00007FF7BCBE1A54 | | inc r15d | //‘w’
00007FF7BCBE1A57 | | inc rdi |
00007FF7BCBE1A5A | | add r14,14 |
00007FF7BCBE1A5E | | jmp 112419_ezre.7FF7BCBE1A7E |
00007FF7BCBE1A60 | | dec r15d | //'s'
00007FF7BCBE1A63 | | dec rdi |
00007FF7BCBE1A66 | | sub r14,14 |
00007FF7BCBE1A6A | | jmp 112419_ezre.7FF7BCBE1A7E |
00007FF7BCBE1A6C | | dec ebx | //'d'
00007FF7BCBE1A6E | | dec rbp |
00007FF7BCBE1A71 | | dec rsi |
00007FF7BCBE1A74 | | jmp 112419_ezre.7FF7BCBE1A7E |
00007FF7BCBE1A76 | | inc ebx | //‘a’
00007FF7BCBE1A78 | | inc rbp |
00007FF7BCBE1A7B | | inc rsi |
00007FF7BCBE1A7E | | cmp rsi,13 |

接下来就是w,a,s,d的对坐标的影响根据以上可知r14是纵坐标,一回w/s变0x14可知横长为20,一看rbp就是横坐标

接着看汇编

1
2
3
4
00007FF7BCBE1A8A |  | lea rax,qword ptr ds:[r14+rbp]         |
00007FF7BCBE1A8E | | mov rcx,qword ptr ss:[rsp+20] | rcx:KiUserCallbackDispatcher
00007FF7BCBE1A93 | | cmp dword ptr ds:[rcx+rax*4+260],1 | rcx+rax*4+260:RtlCallEnclaveReturn+65
00007FF7BCBE1A9B | | je 112419_ezre.7FF7BCBE1B01 | //跳动error

所以1就是墙,这是撞墙逻辑,我们要去找地图,由于dword所以4字节一组,所以将rax也就是将地图展平后的位置

那rcx是什么前面是mov rcx, [rsp+20],最前面是mov [rsp+20], rcx这是this(c++中的隐式指针,成员被调用时,编译器会隐式的传送该对象的地址作为this指针),第一个参数是对象的地址(this指针,用于区分·不同的变量),其实就是rcx的值,rcx就是基址(参考的起点),在没对rcx进行别的操作前一开始存的就是对象的地址

接下来看终点

1
2
3
4
5
6
7
8
9
00007FF6953D1AAA |  | cmp ebx,13                             |
00007FF6953D1AAD | | jne 112419_ezre.7FF6953D1B01 |
00007FF6953D1AAF | | cmp r15d,ebx |
00007FF6953D1AB2 | | jne 112419_ezre.7FF6953D1B01 |
00007FF6953D1AB4 | | lea rcx,qword ptr ss:[rsp+40] |
00007FF6953D1AB9 | | call qword ptr ds:[<&Ordinal#296>] |
00007FF6953D1ABF | | nop |
00007FF6953D1AC0 | | mov r8,qword ptr ds:[r13] |
00007FF6953D1AC4 | | lea rdx,qword ptr ds:[7FF6953D4DF0] | 00007FF6953D4DF0:L"correct! your flag is VNCTF{%s} "

ebx也就是横坐标为19, cmp r15d,ebx 也就是横纵坐标与横坐标相同,也就是终点为(19,19)

maze从rcx+0x260开始

00007FF9E5485E20

mfc逆向学习

mfc程序是由mfc类库编写的c++程序,图形界面通常是检验,用ida

由于是消息映射所以点进去也是看不懂,用xspy看最下面的消息

一般看oncommand

1
2
3
4
5
6
7
8
9
10
virtual BOOL OnCommand(WPARAM wParam, LPARAM lParam);


//定义
BOOL CDlgTest::OnCommand(WPARAM wParam, LPARAM lParam)
{

return CDialog::OnCommand(wParam, lParam);
}

主要接受菜单项,控件与快捷键,而按钮则是控件,所以主要找确认就是找Oncommand,找偏移地址,

然后通过detect it easy找到文件基址,而ida中的地址是sub_xxxx,xxxx即为实际地址

mfc使用消息映射基机制,即一个消息与消息处理函数(对应的消息映射表,消息处理函数的声明和实现)

窗口消息分为三个部分:1.无符号整数,即消息指2.消息附带的WPARAM类型的参数(传递标识、标志位、小数据)3.消息附带的LPARAM类型的参数(传递复杂数据、结构指针和坐标数据)

通过导入结构体将消息映射表(类)

1
2
3
4
DECLARE_MESSAGE_MAP	声明将在类中使用消息映射来将消息映射到函数(必须在类声明中使用)。
BEGIN_MESSAGE_MAP 开始消息映射的定义(必须在类实现中使用)。
BEGIN_TEMPLATE_MESSAGE_MAP 开始在包含单个模板参数的类类型上定义消息映射。
END_MESSAGE_MAP 结束消息映射的定义(必须在类实现中使用)。

声明与实现类似函数的声明与定义

可以通过导入结构体将

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct AFX_MSGMAP_ENTRY
{
UINT nMessage;
UINT nCode;
UINT nID; // ID号
UINT nLastID;
UINT_PTR nSig;
void (*pfn)(void); // 该ID号的消息处理函数
};

struct AFX_MSGMAP
{
const AFX_MSGMAP *(__stdcall *pfnGetBaseMap)();
const AFX_MSGMAP_ENTRY *lpEntries; // 消息处理函数映射表
};

在view->open sibview -> local type(本地类型)存的就是各种函数、类和空间的内部定义的类型别名或类型定义

(map为映射),add type的将两个结构体导入导入,找到消息映射表的偏移(xspy)找到偏移,在detedct找到机制,然后找到消息映射表,按g跳到对应地址,按alt+q导入struct AFX_MSGMAP,之后对应xspy的表的各个指令,有几个就是几个struct AFX_MSGMAP_ENTRY