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


红了一片,upx0,upx1,upx2,upx!,upx的各种标志全没有了
一边来说运行两次然后找push就行了但这道题upx代码与源代码在

但是这道题并非常见的在两次运行,跳过前面的系统代码,并没见到pushad(通用寄存器),pushfd(标志寄存器),push,还有lea但是三次运行找到了jmp 112419_ezre.7FF6518D102A这一看就是主程序入口,
进去一看这题将显示的push rax等给整没了
1
| LEA 目标寄存器, [内存寻址表达式] //通过内存地址(相对地址,偏移量)计算出有效地址
|
不过还后留下了lea与push rdx,下访问断点,运行程序,直接到一堆pop这太对了

首先得用cff-exploer改配置

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

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

找基址和偏移

这就是最后的迷宫函数
一开始一串赋值为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跳到对应区域得到第图,然后深度搜索
得到

方法二
VNCTF2026 WP by YHalo - YHalo’s Blog
感谢大佬的方法,直接一个x64dbg解决,感觉我对x64dbg了解的还是太少了

先在内存布局中找到主模块,找到ezre(这一看就是主程序,直接在text下断点),之后运行
之后多点几次运行直到程序出来这时就可以去找text节区(存代码)
补冲data(存放已初始化的全局变量和局部静态变量),rdata(全局常量和字符串常量),arch(系统内部使用)
bss段(存放未始化的全局变量和局部静态变量)
找到后搜索当前模块的字符串

看到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
login
在这道题是流量分析与安卓的结合体
wireshark打开流量包可以看见getkey与login请求
用手机打开程序发现是个登录界面
用apk查看源代码
找到post函数查找引用,并关注函数的参数Context context, String path, String body, String headerName, String headerValue
login体是encrypted,header是sign
主要函数是
1 2
| final String encrypted = NativeBridge.encrypt(payload, this); final String sign = NativeBridge.sign(payload, encrypted, this);
|
关注payload
1
| String payload = username + ":" + password
|
用户名与“:”+密码拼接,并且这是Native层函数
直接看native层代码
用ida反编译so文件
找到Java_com_britney_login_util_NativeBridge_setKey这个native方法,出现导入头文件,将参数导入JNIEnv(java native interference)
对于key一直引用找到JNI_Onload最后找出RegisterNatives 函数这个是native层库函数为动态注册方法
1 2 3
| jint RegisterNatives(JNIEnv *env, jclass clazz, const JNINativeMethod *methods, jint nMethods);
|
看结构体
const JNINativeMethod *methods
- 类型:JNINativeMethod结构体数组
- 作用:定义Java方法与C/C++函数的映射关系
- 结构体定义:
c
1 2 3 4 5
| typedef struct { const char *name; // Java层方法名 const char *signature; // 方法签名(参数和返回值类型) void *fnPtr; // Native函数指针 } JNINativeMethod;
|
参数第一个一般JNIEnv_ env第二个参数类型jclass
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
| __int64 __fastcall encrpty(JNIEnv_ *JNIEnv__1, __int64 a2, __int64 a3, __int64 a4) { unsigned int v5; // [xsp+68h] [xbp-3B8h] const char *StringUTFChars_w; // [xsp+78h] [xbp-3A8h] _OWORD android_id[22]; // [xsp+A0h] [xbp-380h] BYREF _BYTE v11[256]; // [xsp+200h] [xbp-220h] BYREF JNIEnv_ JNIEnv__; // [xsp+300h] [xbp-120h] BYREF _QWORD v13[2]; // [xsp+400h] [xbp-20h] BYREF int v14; // [xsp+410h] [xbp-10h] __int64 v15; // [xsp+418h] [xbp-8h]
v15 = *(_QWORD *)(_ReadStatusReg(TPIDR_EL0) + 40); StringUTFChars_w = (const char *)GetStringUTFChars_w(JNIEnv__1, a3, 0); v14 = 0; v13[1] = 0; v13[0] = 0; getandroidid(JNIEnv__1, a4, (__int64)v13); snprintf_0(JNIEnv__, 256, 256, (__int64)"%s:%s", StringUTFChars_w, (const char *)v13); ReleaseStringUTFChars_w(JNIEnv__1, a3, (__int64)StringUTFChars_w); v5 = sub_262B0(JNIEnv__, 256); memset(v11, 0, sizeof(v11)); sub_274D0(&key, 16, JNIEnv__); memset(android_id, 0, 350); sub_27728(v11, v5); return NewStringUTF_w(JNIEnv__1, (__int64)android_id); }
|
重命名后JNIEnv__1为env,而a2是jclass,参数a3是username与密码拼接之后有snprintf_0(JNIEnv__, 256, 256, (__int64)”%s:%s”, StringUTFChars_w, (const char *)v13);相当于又拼上个”:androidid”
接着看
1 2 3 4 5 6 7 8
| for ( len = 0; ; ++len ) { v3 = 0; if ( len < n256 ) v3 = *((_BYTE *)&JNIEnv_->functions + len) != 0; if ( !v3 ) break; }
|
当非零时才执行,所以大概率为len
这个最后
if ( len == n256 || !len )
return 0;
v5 = (16 - (len & 0xF)) & 0xF;
if ( len + v5 + 1 > n256 )
return 0;
for ( i = 0; i < v5; ++i )
*((_BYTE *)&JNIEnv_->functions + len + i) = ‘\x01’;
*((BYTE *)&JNIEnv->functions + len + v5) = 0;
return len + v5;
很明显是个如果长度不到16字节的整数自动补齐
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| __memcpy_chk(v14, p_key, (unsigned int)n16, 16); sub_26A34(v14, 16, v15); for ( i = 0; i < i_1; i += 16 ) { sub_26904(); sub_26D10(v13, v15); for ( n9 = 1; n9 <= 9; ++n9 ) { v8 += 16; sub_26EC8(); sub_26E1C(v13); sub_270B8(v13); sub_26D10(v13, v8); } sub_26EC8(); sub_26E1C(v13); sub_26D10(v13, v8 + 16); sub_2699C(v13, v9); v9 += 16; JNIEnv__1 = (const struct JNINativeInterface_ *)((char *)JNIEnv__1 + 16); v8 = v15; }
|
这一看AES加密
sub_26EC8();
sub_26E1C(v13);
sub_270B8(v13);
sub_26D10(v13, v8);就是字节替换,行位移,列混合,轮密钥加
AES魔改在调换字节替换与行位移调换位置,字节替换的s盒魔改,在轮密钥加多异或一个
之后去看sign函数,对于snprintf函数拼接的后几个参数
1 2 3
| if ( pthread_create(newthread_, 0, (void *(*)(void *))sub_27FAC, &arg_) || pthread_create(newthread_, 0, (void *(*)(void *))sub_28060, &arg__0) || (result = pthread_create(newthread_, 0, (void *(*)(void *))sub_28114, &arg__1), (_DWORD)result) )
|
这是个创建线程的函数sub_27FAC(对ida23946端口的检验),sub_28060(对frida27042的检测),反调试
sub_28114这个是字符串检验,经典的反调试
这三个正常情况下为0
根据wireshark分析getkey得到的key为MnpiiylSrRk_mZ-H
之后要找到加密后的结果魔改的AES+魔改的base64
Y7nFpNWxMh0rzWixEN1+1dzQPzjE/PxfCVWEvGww3eK+fIstVlwllNUaHFujEveg
解密base64得到0x6b,0xb8,0xa4,0xdd,0x1,0x6b,0x3c,0xdb,0x93,0xb4,0x53,0xab,0x8d,0xb,0xfe,0xbc,0x9b,0x58,0x4a,0xd7,0x63,0xfd,0x2a,0xcb,0x78,0x41,0x63,0x5e,0x5a,0xaa,0xd0,0xaa,0x7e,0x2e,0x75,0x15,0x12,0xa,0xa0,0x81,0,0xdc,0x9a,0x45,0x9d,0x8d,0x72,0x8c,0,0xfe,0xee,0xab,0xab,0xab,0xab,0xab,0xab,0xab,0xab,0xab,0xab,0xab,0xab,0xab,这就是密文
首先进行逆AES魔改解密得到
VNCTF2026:Vv&nN_W3lC0me!!:b2e90a5f379ea4db
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| function hookSprintf(){ const m = Process.findModuleByName("liblogin.so") if(!m) { console.warn("Could not find 'liblogin.so'"); return ; } var addr = m.base.add(0x26178); Interceptor.attach(addr,{ //args是NativePointer的集合吗 onEnter: function(args) { var format = args[3].readCString(); if(format ==null) { return; } if(format.indexOF("VNCTF")!=-1) { arg[5].writeUtf8string("b2e90a5f379ea4dc"); arg[7] = ptr(0); arg[8] = ptr(0); arg[9] = ptr(0) } else{ args[5].writeUtf8String("b2e90a5f379ea4dc"); } } } ) } function hookdlopen() { const m = Process.findModuleByName("libdl.so"); if(!m) { console.warn("Could not find 'libdl.so'"); } Interceptor.attach(addr,{ onEnter(args) { this.path = arg[0].readCString(); }, onLeave(arfs) { if(this.path.indexOF("liblogin.so")!==-1) { hookSprintf();//加载完后hook } } }) } setImmediate(hookdlopen);//在执行完所有代码在执行其参数
|
进行frida执行得到结果,setImmediat是将**hookdlopen 函数推入一个专门的“微任务/定时回调队列”中执行**
(事件)