[MiniL CTF 2022] Reverse部分赛题复现

再不学re👶要被开了

为什么是部分复现呢?因为有个wasm题没有环境没法复现捏

twin

👶最开始以为这是个签到题。。

for (int i = 0; i < 40; ++i) flag[i] = v5[i] ^ i ^ 0x7f, putchar(flag[i]);
// You_are_too_young_this_is_a_fake_flag!!!

打断点调试发现main之前已经有东西在运行了,调了一会发现有个函数叫TlsCallback_0

搜了下发现是TLS回调函数

TLS回调函数是指,每当创建/终止进程的线程时会自动调用执行的函数,创建进程的主线程时也会自动调用回调函数,且其调用执行先于EP代码

可以理解为创建线程和终止线程的时候会各执行一次TLS回调函数

发现没法反编译(sp-analysis failed,栈帧错误),点进去看汇编

.text:00401990 TlsCallback_0   proc near               ; DATA XREF: .data:TlsCallbacks↓o
.text:00401990
.text:00401990 var_12C         = dword ptr -12Ch
.text:00401990
.text:00401990                 push    ebp
.text:00401991                 mov     ebp, esp
.text:00401993                 sub     esp, 11Ch
.text:00401999                 push    ebx
.text:0040199A                 push    esi
.text:0040199B                 push    edi
.text:0040199C                 call    $+5
.text:004019A1                 add     [esp+12Ch+var_12C], 1Eh
.text:004019A5                 retn
.text:004019A5 TlsCallback_0   endp ; sp-analysis failed
.text:004019A5
.text:004019A5 ; ---------------------------------------------------------------------------
.text:004019A6 aWelcomeTo2022M db 'Welcome_to_2022_miniLCTF',0
.text:004019BF ; ---------------------------------------------------------------------------

分析一下汇编,发现0x401999C处有个call $+5,$+5就是40199C + 5 = 4019A1,就是下一句话的地址。call将下一句的地址压栈,然后add给esp指针加了个0x1E(var_12C = -12Ch),retn的地址就是0xA1 + 0x1E = 0xBF,所以中间这一串屁用没有,那直接把call到字符串这里全nop了。

发现还是不能反编译(继续sp-analysis failed),最后发现是函数的结束地址被最开始0x40199C给干扰了(指看了wp才知道),最后正确的retn地址是0x401D5B,alt + p把tls函数的end address改成401D5C就行。401A03处也有这个花指令。

void __cdecl TlsCallback_0(int a1, int a2)
{
  char *v2; // eax
  char Buffer[80]; // [esp+14h] [ebp-11Ch] BYREF
  struct _STARTUPINFOA StartupInfo; // [esp+64h] [ebp-CCh] BYREF
  struct _PROCESS_INFORMATION ProcessInformation; // [esp+A8h] [ebp-88h] BYREF
  char v7[22]; // [esp+BCh] [ebp-74h] BYREF
  char v8[4]; // [esp+D2h] [ebp-5Eh] BYREF
  char v9[39]; // [esp+D8h] [ebp-58h] BYREF
  char v10[4]; // [esp+FFh] [ebp-31h] BYREF
  char v11[6]; // [esp+104h] [ebp-2Ch] BYREF
  char v12[4]; // [esp+10Ah] [ebp-26h] BYREF
  CHAR Name[8]; // [esp+110h] [ebp-20h] BYREF
  CHAR ApplicationName[8]; // [esp+118h] [ebp-18h] BYREF
  char v15[5]; // [esp+120h] [ebp-10h] BYREF
  char v16[10]; // [esp+125h] [ebp-Bh] BYREF
  uint8_t BeingDebugged; // [esp+12Fh] [ebp-1h]

  if ( a2 == 1 )
  {
    memset(Buffer, 0, sizeof(Buffer));
    printf(Buffer);
    BeingDebugged = 0;
    BeingDebugged = NtCurrentPeb()->BeingDebugged;
    if ( !BeingDebugged )
      *(&TlsCallbacks + 1) = (int (__cdecl *)(int, int))sub_401D60;
    strcpy(Name, "93>8");
    Xor_0x7Fu(Name);
    hObject = CreateFileMappingA(0, 0, 4u, 0, 0x1000u, Name);
    *(_DWORD *)dword_404448 = MapViewOfFile(hObject, 0xF001Fu, 0, 0, 0x1000u);
    v7[0] = 47;
    v7[1] = 19;
    v7[2] = 26;
    v7[3] = 30;
    v7[4] = 12;
    v7[5] = 26;
    v7[6] = 95;
    v7[7] = 22;
    v7[8] = 17;
    v7[9] = 15;
    v7[10] = 10;
    v7[11] = 11;
    v7[12] = 95;
    v7[13] = 6;
    v7[14] = 16;
    v7[15] = 10;
    v7[16] = 13;
    v7[17] = 95;
    v7[18] = 25;
    v7[19] = 19;
    v7[20] = 30;
    v7[21] = 24;
    strcpy(v8, "E_");
    v2 = (char *)Xor_0x7Fu(v7);
    printf(v2);
    v16[3] = 90;
    v16[4] = 12;
    v16[5] = 0;
    Xor_0x7Fu(&v16[3]);
    input(&v16[3], *(_DWORD *)dword_404448, 41);
  }
  if ( !a2 )
  {
    qmemcpy(ApplicationName, "QP\v", 3);
    ApplicationName[3] = 18;
    ApplicationName[4] = 15;
    ApplicationName[5] = 0;
    Xor_0x7Fu(ApplicationName);
    sub_401410();
    memset(&StartupInfo, 0, sizeof(StartupInfo));
    StartupInfo.cb = 68;
    CreateProcessA(ApplicationName, 0, 0, 0, 0, 3u, 0, 0, &StartupInfo, &ProcessInformation);
    v11[0] = 28;
    v11[1] = 16;
    v11[2] = 13;
    v11[3] = 13;
    v11[4] = 26;
    v11[5] = 28;
    strcpy(v12, "\vu");
    qmemcpy(v15, "\b\r", 2);
    v15[2] = 16;
    v15[3] = 17;
    v15[4] = 24;
    strcpy(v16, "u");
    v9[0] = 47;
    v9[1] = 19;
    v9[2] = 26;
    v9[3] = 30;
    v9[4] = 12;
    v9[5] = 26;
    v9[6] = 95;
    v9[7] = 28;
    v9[8] = 19;
    v9[9] = 16;
    v9[10] = 12;
    v9[11] = 26;
    v9[12] = 95;
    v9[13] = 11;
    v9[14] = 23;
    v9[15] = 26;
    v9[16] = 95;
    v9[17] = 27;
    v9[18] = 26;
    v9[19] = 29;
    v9[20] = 10;
    v9[21] = 24;
    v9[22] = 24;
    v9[23] = 26;
    v9[24] = 13;
    v9[25] = 95;
    v9[26] = 30;
    v9[27] = 17;
    v9[28] = 27;
    v9[29] = 95;
    v9[30] = 11;
    v9[31] = 13;
    v9[32] = 6;
    v9[33] = 95;
    v9[34] = 30;
    v9[35] = 24;
    v9[36] = 30;
    v9[37] = 22;
    v9[38] = 17;
    strcpy(v10, "u");
    sub_401510(ApplicationName, &ProcessInformation);
    if ( dword_404440 == 1 )
    {
      XXTea((_DWORD *)(*(_DWORD *)dword_404448 + 20), 5, (int)&unk_40405C);
      if ( !memcmp((const void *)(*(_DWORD *)dword_404448 + 20), &unk_40402C, 0x14u) )
      {
        Xor_0x7Fu(v11);
        printf(v11);
LABEL_13:
        CloseHandle(hObject);
        return;
      }
    }
    else if ( dword_404440 == -2 )
    {
      Xor_0x7Fu(v9);
      printf(v9);
      goto LABEL_13;
    }
    Xor_0x7Fu(v15);
    printf(v15);
    goto LABEL_13;
  }
}

a2 = 1

最开始的BeingDebugged = NtCurrentPeb()->BeingDebugged;是反调试,动调的时候jz jnz反着patch就能过掉

v7这串玩意在异或0x7F后是Please input your flag:,v16异或后是%s,可以发现后面两个函数分别是输出和输入,输入存储在dword_404448内

在反调试的下面有一句*(&TlsCallbacks + 1) = (int (__cdecl *)(int, int))sub_401D60;

这里判断若没有在动调就在 TLS函数后增加一个函数sub_401D60,这个函数就在TLS下面。

现在切过去看发现这里也爆红了,但花指令和TLS的花是一样的

void __cdecl __noreturn sub_401D60(int a1, int a2)
{
  CHAR ModuleName[16]; // [esp+4h] [ebp-1Ch] BYREF
  CHAR ProcName[12]; // [esp+14h] [ebp-Ch] BYREF

  if ( a2 == 1 )
  {
    qmemcpy(ProcName, "(\r", 2);
    ProcName[2] = 22;
    ProcName[3] = 11;
    ProcName[4] = 26;
    ProcName[5] = 57;
    ProcName[6] = 22;
    ProcName[7] = 19;
    ProcName[8] = 26;
    ProcName[9] = 0;
    ModuleName[0] = 20;
    ModuleName[1] = 26;
    ModuleName[2] = 13;
    ModuleName[3] = 17;
    ModuleName[4] = 26;
    ModuleName[5] = 19;
    ModuleName[6] = 76;
    ModuleName[7] = 77;
    ModuleName[8] = 81;
    ModuleName[9] = 27;
    ModuleName[10] = 19;
    ModuleName[11] = 19;
    ModuleName[12] = 0;
    Xor_0x7Fu(ProcName);
    Xor_0x7Fu(ModuleName);
    hModule = GetModuleHandleA(ModuleName);
    dword_4043DC = (int)GetProcAddress(hModule, ProcName);
    sub_4016C0(dword_4043DC, sub_401650, hModule);
  }
  ExitProcess(0xFFFFFFFF);
}
  • Xor_0x7Fu(ProcName);:WriteFile
  • Xor_0x7Fu(ModuleName);:kernel32.dll
  • hModule = GetModuleHandleA(ModuleName);:获得kernel32.dll的句柄

这里是导入了WriteFile和kernel32.dll(后续实现了注入)。GetProcAddress取得WriterFile的函数指针存储在dword_4043DC中

int __cdecl sub_4016C0(int a1, int a2, HMODULE a3)
{
  DWORD flOldProtect; // [esp+Ch] [ebp-10h] BYREF
  int v5; // [esp+10h] [ebp-Ch]
  HMODULE ModuleHandleA; // [esp+14h] [ebp-8h]
  LPVOID lpAddress; // [esp+18h] [ebp-4h]

  ModuleHandleA = GetModuleHandleA(0);
  v5 = (int)ModuleHandleA + *(_DWORD *)((char *)ModuleHandleA + *((_DWORD *)ModuleHandleA + 15) + 128);
  flOldProtect = 0;
  do
  {
    if ( !*(_DWORD *)(v5 + 16) || dword_4043D0 )
      break;
    if ( a3 == GetModuleHandleA((LPCSTR)ModuleHandleA + *(_DWORD *)(v5 + 12)) )
    {
      for ( lpAddress = (char *)ModuleHandleA + *(_DWORD *)(v5 + 16); lpAddress; lpAddress = (char *)lpAddress + 4 )
      {
        if ( *(_DWORD *)lpAddress == a1 )
        {
          VirtualProtect(lpAddress, 4u, 4u, &flOldProtect);
          *(_DWORD *)lpAddress = a2;
          VirtualProtect(lpAddress, 4u, flOldProtect, 0);
          dword_4043D0 = 1;
          break;
        }
      }
    }
    v5 += 20;
  }
  while ( !dword_4043D0 );
  return dword_4043D0;
}

该函数中遍历了kernel32.dll的导入表, 并在lpAddress为WriteFile的时候调用VituralProtect获取权限,从而修改其为a2(即sub_401650)。换言之,该函数将原本的WriteFile给hook成了sub_401650。

好几把高端,以前没见过这种玩意。。

int __stdcall sub_401650(int a1, int a2, int a3, int a4, int a5)
{
  *(_BYTE *)(a2 + 1822) = 6;
  *(_BYTE *)(a2 + 1713) = 6;
  dword_4043DC(a1, a2, a3, a4, a5);
  sub_4017C0(dword_4043DC, sub_401650, hModule);
  return 0;
}

一开始看的时候不知道上面两个6有啥用,唯一能看懂的是sub_4017C0和sub_40166C0类似,只是这次是将hook给取消了,WriteFile调用的还是自己。但一开始也能猜出来这次hook就是为了执行前面两个置6的操作。

(后面会发现是把xxtea的z的位移改成了6)

a2 != 1

sub_401D60末尾有ExitProcess,这之后a2 = 0,进入下半段函数。

v11是correct,v15是wrong,v9是Please close the debugger and try again。

其中有一个加密函数内是xxtea加密,对输入的后20位进行判断。在动调拿出enflag和key后可以解出是3e90c91c02e9b40b78b},显然是后半段flag

这里面有个重要函数sub_401410

BOOL sub_401410()
{
  CHAR Type[8]; // [esp+0h] [ebp-2Ch] BYREF
  CHAR FileName[8]; // [esp+8h] [ebp-24h] BYREF
  BOOL v3; // [esp+10h] [ebp-1Ch]
  DWORD NumberOfBytesWritten; // [esp+14h] [ebp-18h] BYREF
  HGLOBAL hResData; // [esp+18h] [ebp-14h]
  LPCVOID lpBuffer; // [esp+1Ch] [ebp-10h]
  DWORD nNumberOfBytesToWrite; // [esp+20h] [ebp-Ch]
  HRSRC hResInfo; // [esp+24h] [ebp-8h]
  HANDLE hFile; // [esp+28h] [ebp-4h]

  qmemcpy(FileName, "QP\v", 3);
  FileName[3] = 18;
  FileName[4] = 15;
  FileName[5] = 0;
  strcpy(Type, ":':-:,");
  Xor_0x7Fu(FileName);
  Xor_0x7Fu(Type);
  hResInfo = FindResourceA(0, (LPCSTR)0x65, Type);
  nNumberOfBytesToWrite = SizeofResource(0, hResInfo);
  hResData = LoadResource(0, hResInfo);
  lpBuffer = LockResource(hResData);
  sub_401E40(lpBuffer, nNumberOfBytesToWrite);
  hFile = CreateFileA(FileName, 0xC0000000, 0, 0, 2u, 0x80u, 0);
  NumberOfBytesWritten = 0;
  v3 = WriteFile(hFile, lpBuffer, nNumberOfBytesToWrite, &NumberOfBytesWritten, 0);
  FlushFileBuffers(hFile);
  return CloseHandle(hFile);
}

子文件tmp

sub_401410中创建了一个叫做tmp的文件,断点打在return处动调再运行几步可以拿到完整的tmp文件。然后分析tmp文件。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  void *v3; // ecx

  sub_401400(v3);
  if ( sub_4010E0() )
  {
    delta ^= 0x90909090;
    key[1] = 144;
  }
  delta = sub_401210(delta);
  sub_401390(&unk_4043A8);
  xxtea(&unk_4043A8, 5, key);
  if ( !memcmp(&unk_4043A8, &unk_404018, 0x14u) )
    return 1;
  else
    return -1;
}

最开始的sub_401400中有sub_4010F0,是个检测动调的函数,如果检测到一些动调进程的名字的话就会退出程序。然后有对delta的异或操作和一步添加VEH(但好像并没有用到这个VEH)。

sub_4010E0是IsDebuggerPresent反调试

BOOL __cdecl sub_401390(void *a1)
{
  HANDLE hFileMappingObject; // [esp+8h] [ebp-8h]
  LPCVOID lpBaseAddress; // [esp+Ch] [ebp-4h]

  hFileMappingObject = CreateFileMappingA(0, 0, 4u, 0, 0x1000u, "FLAG");
  lpBaseAddress = MapViewOfFile(hFileMappingObject, 0xF001Fu, 0, 0, 0x1000u);
  qmemcpy(a1, lpBaseAddress, 0x28u);
  UnmapViewOfFile(lpBaseAddress);
  return CloseHandle(hFileMappingObject);
}

后面才发现sub_401390和之前主体文件a2=1部分中的这一句对应

strcpy(Name, "93>8");
Xor_0x7Fu(Name);
hObject = CreateFileMappingA(0, 0, 4u, 0, 0x1000u, Name);
*(_DWORD *)dword_404448 = MapViewOfFile(hObject, 0xF001Fu, 0, 0, 0x1000u);

这个NAME字符串异或出来就是"FLAG",hObject这一句创建了名为FLAG的文件映像,dword_404448这一句将内存映射的文件存了下来,这样可以让子进程访问dword_404448指向的内存,实现修改等操作。

那么sub_401390里面获取了dword_404448的共享内存,然后把lpBaseAddress指向的数据复制0x28u位(40位,按位数来说应该是输入)给a1(unk_4043A8)。

随后,对unk_4043A8进行了xxtea加密。

注意,此处的xxtea加密是有变动的:其右移位数从原来的5位变成了6位(前面sub_401650写进去的两个6就在这里)。

动调发现sub_401210炸了,看汇编看到了和以前一样的花指令,去掉后发现还是爆红了

int __cdecl sub_401210(int a1)
{
  MEMORY[0] = 0;
  return (a1 ^ 0x7B) + 12345;
}

MEMORY[0] = 0是往地址0内写值了,引发了内存访问异常。在汇编里面你可以看见这几句话

.text:00401230                 xor     ebx, ebx
.text:00401232                 mov     [ebx], ebx
.text:00401234                 mov     eax, [ebp+var_4]

xor先把ebx置0,然后mov [ebx], ebx就向地址0里面写值了, 就异常了。

尝试动调的时候把eip改改跳到401234,发现还是死了。。

分析一下发现[ebp+var_4]的值是delta(在401220处mov [ebp+var_4], eax),显然这个地址没卵用,还是会爆异常(这就是为什么上面说好像并没有用到VEH,他完全没处理异常。。)

滚回原来的程序,从创建tmp那里往下看,发现sub_401510里面又有花,去掉

BOOL __cdecl sub_401510(int a1, int a2)
{
  CONTEXT Context; // [esp+8h] [ebp-33Ch] BYREF
  int v4[23]; // [esp+2D4h] [ebp-70h] BYREF
  HANDLE hThread; // [esp+330h] [ebp-14h]
  int v6; // [esp+334h] [ebp-10h]
  int v7; // [esp+338h] [ebp-Ch]
  int v8; // [esp+33Ch] [ebp-8h]
  int v9; // [esp+340h] [ebp-4h]

  v4[22] = *(_DWORD *)a2;
  hThread = *(HANDLE *)(a2 + 4);
  v6 = *(_DWORD *)(a2 + 8);
  v7 = *(_DWORD *)(a2 + 12);
  v9 = 1;
  while ( v9 )
  {
    WaitForDebugEvent(&DebugEvent, 0xFFFFFFFF);
    if ( DebugEvent.dwDebugEventCode == 1 )
    {
      qmemcpy(v4, &DebugEvent.u, 0x54u);
      v8 = v4[0];
      if ( v4[0] == -1073741819 )
      {
        memset(&Context, 0, sizeof(Context));
        Context.ContextFlags = 65543;
        GetThreadContext(hThread, &Context);
        Context.Eip += 5;
        Context.Eax ^= 0x1B207u;
        SetThreadContext(hThread, &Context);
      }
    }
    if ( DebugEvent.dwDebugEventCode == 5 )
    {
      dword_404440 = DebugEvent.u.Exception.ExceptionRecord.ExceptionCode;
      v9 = 0;
    }
    ContinueDebugEvent(DebugEvent.dwProcessId, DebugEvent.dwThreadId, 0x10002u);
  }
  Sleep(0x64u);
  return DeleteFileA((LPCSTR)a1);
}

WaitForDebugEvent表明父进程是通过调试的方法打开的子进程,并且通过百度发现

联合体u的值

1u.Exception

2 u.Create Thread

3 u.CreateProcessInfo

4 u.ExitThread

5 u.ExitProcess

6 u.LoadDll

7 u.UnloadDll

8 u.DebugString

9 u.RipInfo

union的值是通过DebugEvent.dwDebugEventCode决定的,也就是说,=1的时候说明触发了异常,此时父进程会处理子进程的异常,修改子进程代码;=5说明程序正常退出。

if ( v4[0] == -1073741819 )下面对eip和eax进行了修改,经过搜索发现-1073741819(就是0xC0000005)代表的错误类型是发生访问冲突,也就是内存访问异常。

联系一下tmp文件里面爆红的MEMORY[0]=0,说明这里就是检测到子进程发生了异常后,程序会转到父进程进行异常处理,并且如果是内存访问异常的话就修改eip和eax。

联系到tmp文件内对应的eax的值是处理后的delta,说明tmp里面的xxtea的delta还得再异或0x1B207u才是正确的值。这下就可以用正确的魔改版xxtea解出前一半的flag了:miniLctf{cbda59ff59e

最后得到flag:

miniLctf{cbda59ff59e3e90c91c02e9b40b78b}

这个题学到的新东西真的太多了,👶打算下一个自学坑就开seh veh这些异常处理了

NotRC4

lemon

posted @ 2022-05-18 17:49  iPlayForSG  阅读(188)  评论(0编辑  收藏  举报