duksctf

Bunch of sec. enthusiasts who sometimes play CTF

Y-Not-CTF - BlackBox

The binary ask for a password and use RunPE technic to create a new process. Two technic can be used, first is to dump the PE when the program copy it, second is to break on SetThreadContext and put a breakpoint on the new entrypoint.

Description

An idea is like a virus. Resilient. Highly contagious. And even the smallest seed of an idea can grow. It can grow to define or destroy you. (quote Inception movie)

Details

Points: 474

Category: reverse

Validations: 4

Solution

We were given a file called blackbox.exe.

After launching the file a password is asked.

C:\Users\test\Desktop>blackbox_9d4eede1603846f7cf5acd389a4f9a2bc92fd6b3.exe
Enter password: aaa
Bad password!

Opening the file in ida pro shown an interesting technic used by malware to thwart antivirus. the main code looks like this:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v3; // ecx
  DWORD v4; // eax
  char v6; // [esp+0h] [ebp-1Ch]

  dword_436CA0 = (int (__stdcall *)(_DWORD, _DWORD))WaitForSingleObject;
  sub_401020("Enter password: ", v6);
  sub_401050("%21s", &v6);
  v4 = sub_401090(v3, &v6);
  dword_436CA0(v4, -1);
  return 0;
}

The password seems to be 21 char long and the verification seems to be in sub_401090:

DWORD __fastcall sub_401090(int a1, CHAR *a2)
{
  CHAR *v2; // edi
  signed int v3; // eax
  int v4; // esi
  DWORD result; // eax
  CONTEXT *v6; // eax
  const CONTEXT *v7; // edi
  int v8; // ebx
  signed int v9; // edi
  CONTEXT *v10; // [esp+8h] [ebp-464h]
  char *v11; // [esp+Ch] [ebp-460h]
  char Buffer; // [esp+10h] [ebp-45Ch]
  struct _PROCESS_INFORMATION ProcessInformation; // [esp+14h] [ebp-458h]
  struct _STARTUPINFOA StartupInfo; // [esp+24h] [ebp-448h]
  CHAR Filename; // [esp+68h] [ebp-404h]

  v2 = a2;
  v3 = 0;
  do
    dword_41E8B0[v3++] ^= 0xFEu;
  while ( v3 < 96768 );
  v4 = dword_41E8EC;
  result = GetModuleFileNameA(0, &Filename, 0x400u);
  if ( *(_DWORD *)&dword_41E8B0[v4] == 'EP' )
  {
    ProcessInformation = 0i64;
    memset(&StartupInfo, 0, 0x44u);
    result = CreateProcessA(&Filename, v2, 0, 0, 0, 4u, 0, 0, &StartupInfo, &ProcessInformation);
    if ( result )
    {
      v6 = (CONTEXT *)VirtualAlloc(0, 4u, 0x1000u, 4u);
      v7 = v6;
      v10 = v6;
      v6->ContextFlags = 65543;
      result = GetThreadContext(ProcessInformation.hThread, v6);
      if ( result )
      {
        ReadProcessMemory(ProcessInformation.hProcess, (LPCVOID)(v7->Ebx + 8), &Buffer, 4u, 0);
        v11 = (char *)VirtualAllocEx(
                        ProcessInformation.hProcess,
                        *(LPVOID *)((char *)&dword_41E8E4 + v4),
                        *(int *)((char *)&dword_41E900 + v4),
                        0x3000u,
                        0x40u);
        WriteProcessMemory(ProcessInformation.hProcess, v11, dword_41E8B0, *(int *)((char *)&dword_41E904 + v4), 0);
        if ( *(unsigned __int16 *)((char *)&word_41E8B6 + v4) > 0u )
        {
          v8 = 0;
          v9 = 0;
          do
          {
            WriteProcessMemory(
              ProcessInformation.hProcess,
              &v11[*(_DWORD *)((char *)&unk_41E9A8 + dword_41E8EC + v8 + 12)],
              &dword_41E8B0[*(_DWORD *)((char *)&unk_41E9A8 + dword_41E8EC + v8 + 20)],
              *(_DWORD *)((char *)&unk_41E9A8 + dword_41E8EC + v8 + 16),
              0);
            v8 += 40;
            ++v9;
          }
          while ( v9 < *(unsigned __int16 *)((char *)&word_41E8B6 + v4) );
          v7 = v10;
        }
        WriteProcessMemory(ProcessInformation.hProcess, (LPVOID)(v7->Ebx + 8), (char *)&dword_41E8E4 + v4, 4u, 0);
        v7->Eax = (DWORD)&v11[*(int *)((char *)&dword_41E8D8 + v4)];
        SetThreadContext(ProcessInformation.hThread, v7);
        ResumeThread(ProcessInformation.hThread);
        result = (DWORD)ProcessInformation.hThread;
      }
    }
  }
  return result;
}

This is a classic implementation of the technique called RunPE.

The binary will create a new process with the same content using CreateProcess API with the flag CREATE_SUSPENDED, then it will remove everything inside it and replace it with it’s own code.

In the challenge binary, a simple xor is used to hide the real program.

It’s not hard to unpack such malware, we can simply run the decryption code and dump it, or we can put a break point on the newest process and dump the binary using x64dbg Scylla plugin.

First technique, dump the decrypted code

in our case, the malware is using simple xor technique to derypt his payload:

do
    dword_41E8B0[v3++] ^= 0xFEu;
  while ( v3 < 96768 );
  v4 = dword_41E8EC;

Putting a breakpoint after the while condition will be enough to dump the decrypted code at address 0x41E8B0.

When the malware is using obfuscation and is trying to hide the final payload, it’s better to break on ZwWriteVirtualMemory or WriteProcessMemory:

Here is the calling method:

BOOL WINAPI WriteProcessMemory(
  _In_  HANDLE  hProcess,
  _In_  LPVOID  lpBaseAddress,
  _In_  LPCVOID lpBuffer,
  _In_  SIZE_T  nSize,
  _Out_ SIZE_T  *lpNumberOfBytesWritten
);

The lpBuffer will contains the new binary.

More funny method, Break on SetThreadContext

Another method is to break on the API SetThreadContext which is responsible to setup the new context for the created thread. At the end of the code we see this snippet:

v7->Eax = (DWORD)&v11[*(int *)((char *)&dword_41E8D8 + v4)];
SetThreadContext(ProcessInformation.hThread, v7);

Here, v7 is a structure of type CONTEXT. This structure is undocumented by microsoft and it contains all the context for the newer thread. The ULONG Eip is the new entrypoint in the newer process.

Putting a breakpoint on it and resuming the first binary should break on the entrypoint of the new process.

From here, just dump the binary using Scylla.

x64dbg snippet:

01091256 | 89 87 B0 00 00 00        | mov dword ptr ds:[edi+B0],eax           | [edi+B0]:EntryPoint
0109125C | FF B5 AC FB FF FF        | push dword ptr ss:[ebp-454]             |
01091262 | FF 15 24 70 0A 01        | call dword ptr ds:[<&SetThreadContext>] |

Here eax contains the entrypoint: 0x004012B8

Attaching the debugger to the second process spawned and settings a break point on this address should break when resuming:

BAMM, now using Scylla to dump:

Reversing the dumped binary, showed another simple xor operation:

signed int __cdecl sub_401032(int a1, _DWORD *a2)
{
  signed int result; // eax
  int v3; // ecx

  result = 1;
  if ( a1 == 1 )
  {
    v3 = 0;
    while ( ((unsigned __int8)byte_416494[v3] ^ *(_BYTE *)(*a2 + v3)) == byte_4164AC[v3] )
    {
      if ( ++v3 >= 21 )
      {
        sub_401006("Congratz!");
        goto LABEL_7;
      }
    }
    sub_401006("Bad password!");
LABEL_7:
    result = 0;
  }
  return result;
}

Using ida pro Export Data and a little python script showed us the flag: YNOT17{RUN_P3_1S_FUN}

from itertools import izip

a = "732298B81C9D7F70E9BE3F307EA220BEE093BB1C7A"
b = "2A6CD7EC2DAA0422BCF060604DFD11EDBFD5EE5207"

flag = ''.join(chr(ord(c)^ord(k)) for c,k in izip(a.decode("hex"), b.decode("hex")))

print flag

I would like to thanks YNOTCTF Organizer st4ck for his Windows task, there is not so much in CTF these days.

Challenges resources are available in the resources folder

Written on November 17, 2017