Codegate 2014: "Clone technique" 250 pts

This was a challenging reverse engineering task that consisted of a Windows executable.

Upon running it nothing seems to happen except for the machine slightly slowing down. Let's check the decompiled code after we made some annotations to it:

int __stdcall WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
  return main_spawner();
}
 
unsigned int __cdecl main_spawner()
{
  unsigned int result; // eax@2
  DWORD v1; // [sp+8h] [bp-62B8h]@1
  struct _STARTUPINFOW StartupInfo; // [sp+Ch] [bp-62B4h]@1
  DWORD ExitCode; // [sp+50h] [bp-6270h]@6
  WCHAR Filename; // [sp+54h] [bp-626Ch]@3
  WCHAR CommandLine; // [sp+6054h] [bp-26Ch]@6
  unsigned int v6; // [sp+62ACh] [bp-14h]@1
  struct _PROCESS_INFORMATION ProcessInformation; // [sp+62B0h] [bp-10h]@1
 
  v1 = 29 * mem_loc1 + 7 * pow(mem_loc1, 2);
  v6 = pow(v1 ^ mem_loc2, v1 % 2 + 5);
  ProcessInformation.hProcess = 0;
  ProcessInformation.hThread = 0;
  ProcessInformation.dwProcessId = 0;
  ProcessInformation.dwThreadId = 0;
  memset(&StartupInfo.lpReserved, 0, 0x40u);
  StartupInfo.cb = 68;
  if ( mem_loc1 <= 0xD0000000 )
  {
    mem_loc1 = v1;
    mem_loc2 = v6;
    GetModuleFileNameW(0, &Filename, 0x12Cu);
    do
    {
      if ( process_index > 400 )
        return 0;
      ++process_index;
      wsprintfW(&CommandLine, L"\"%s\" %u %u %u", &Filename, v1, v6, process_index);
      CreateProcessW(0, &CommandLine, 0, 0, 0, 0, 0, 0, &StartupInfo, &ProcessInformation);
      WaitForSingleObject(ProcessInformation.hProcess, 0xFFFFFFFFu);
      GetExitCodeProcess(ProcessInformation.hProcess, &ExitCode);
      v1 = ExitCode;
      v6 = pow(ExitCode ^ v6, ExitCode % 0x1E);
    }
    while ( ExitCode );
    result = 0;
  }
  else
  {
    result = 13 * v1 / 0x1B ^ 0x1F2A990D;
  }
  return result;
}

So it seems that the first process spawns 400 extra processes each with 3 arguments: 2 parameters and the process index. But nothing interesting seems to happen anywhere. Let's check the xrefs to process_index. The only procedure linking to it is sub_401160 but it can't be decompiled because of a small trick:

.text:00401169                 add     esp, 80h
.text:0040116F                 sub     esp, 80h

We change it to NOPs and then try again.

BOOL __cdecl sub_401160()
{
  const WCHAR *v0; // eax@1
  unsigned int Buffer; // [sp+Ch] [bp-18h]@2
  LPVOID lpBaseAddress; // [sp+10h] [bp-14h]@4
  SIZE_T NumberOfBytesWritten; // [sp+14h] [bp-10h]@4
  LPWSTR *v5; // [sp+18h] [bp-Ch]@1
  int pNumArgs; // [sp+1Ch] [bp-8h]@1
  unsigned int v7; // [sp+20h] [bp-4h]@2
 
  v0 = GetCommandLineW();
  v5 = CommandLineToArgvW(v0, &pNumArgs);
  if ( pNumArgs == 4 )
  {
    Buffer = _wtoi(v5[1]);
    v7 = _wtoi(v5[2]);
    process_index = _wtoi(v5[3]);
  }
  else
  {
    Buffer = 0xA8276BFAu;
    v7 = 0x92F837EDu;
    process_index = 1;
  }
  memset(sub_401070((const char *)&unk_407030, Buffer, v7), 0, 0x1Cu);
  Buffer ^= 0xB72AF098u;
  v7 ^= v7 * Buffer;
  WriteProcessMemory((HANDLE)0xFFFFFFFF, mem_loc1, &Buffer, 4u, &NumberOfBytesWritten);
  return WriteProcessMemory((HANDLE)0xFFFFFFFF, mem_loc2, &v7, 4u, &NumberOfBytesWritten);
}

Notice how the program seems to be calling a procedure using the parameters and then sets that memory to zero using memset. Let's rename and examine it:

void *__cdecl mystery_func(const char *a1, int a2, int a3)
{
  unsigned int len; // kr04_4@1
  int v4; // edi@1
  int i; // ecx@1
  int j; // [sp+8h] [bp-8h]@4
  char *allocd_buffer; // [sp+Ch] [bp-4h]@1
 
  len = strlen(a1) + 1;
  allocd_buffer = (char *)malloc(len);
  memset(allocd_buffer, 0, 4 * (len >> 2));
  v4 = (int)&allocd_buffer[4 * (len >> 2)];
  for ( i = len & 3; i; --i )
    *(_BYTE *)v4++ = 0;
  for ( j = 0; j < (signed int)(len - 1); j += 2 )
  {
    allocd_buffer[j] = a2 ^ a1[j];
    a2 = rot_left_by_5(a2) ^ 0x2F;
    if ( !a1[j + 1] )
      break;
    allocd_buffer[j + 1] = a3 ^ a1[j + 1];
    a3 = (unsigned __int8)a2 ^ rot_left_by_11(a3);
  }
  return allocd_buffer;
}

It seems to be “attempting crypto”. Well how could we check all 400 values? We first need to get the parameters for each process. We do this using Process Monitor, logging API calls and filtering only for the command line. The initial exported CSV looks like:

"6:02:03.8944287 PM","clone_technique.exe","38704","Process Create","C:\Temp\clone_technique.exe","SUCCESS","PID: 38712, Command line: ""C:\Temp\clone_technique.exe"" 3026539702 3580248161 2"
"6:02:04.0120585 PM","clone_technique.exe","38712","Process Create","C:\Temp\clone_technique.exe","SUCCESS","PID: 38720, Command line: ""C:\Temp\clone_technique.exe"" 466510610 2867152813 3"
"6:02:04.1136464 PM","clone_technique.exe","38720","Process Create","C:\Temp\clone_technique.exe","SUCCESS","PID: 38728, Command line: ""C:\Temp\clone_technique.exe"" 2580226910 609694577 4"
"6:02:04.2091374 PM","clone_technique.exe","38728","Process Create","C:\Temp\clone_technique.exe","SUCCESS","PID: 38736, Command line: ""C:\Temp\clone_technique.exe"" 3729770 4114200461 5"

Filtering leads to this:

 Command line: ""C:\Temp\clone_technique.exe"" 
 Command line: ""C:\Temp\clone_technique.exe"" 3026539702 3580248161 2
 Command line: ""C:\Temp\clone_technique.exe"" 466510610 2867152813 3
 Command line: ""C:\Temp\clone_technique.exe"" 2580226910 609694577 4
 Command line: ""C:\Temp\clone_technique.exe"" 3729770 4114200461 5
 .......................

 Command line: ""C:\Temp\clone_technique.exe"" 2941701190 2496288805 397
 Command line: ""C:\Temp\clone_technique.exe"" 1288739010 2151119537 398
 Command line: ""C:\Temp\clone_technique.exe"" 2134084791 912261120 399
 Command line: ""C:\Temp\clone_technique.exe"" 684983738 221932960 400
 Command line: ""C:\Temp\clone_technique.exe"" 994078838 1090513760 401

Now we could manually start each process using those commands and investigate the decrypted buffer. But there would be 2 problems: each launched process would spawn 400 other processes and secondly, it would be too tedious and would waste lots of precious time.
To solve the first problem we manually patch the spawning and waiting to NOPs. Here's the diff (including the obfuscation mentioned above removed)

clone_technique.exe
00001169: 81 90
0000116A: C4 90
0000116B: 80 90
0000116C: 00 90
0000116D: 00 90
0000116E: 00 90
0000116F: 81 90
00001170: EC 90
00001171: 80 90
00001172: 00 90
00001173: 00 90
00001174: 00 90
0000141A: 51 90
0000141B: 6A 90
0000141C: 00 90
0000141D: FF 90
0000141E: 15 90
0000141F: 10 90
00001420: 60 90
00001421: 40 90
00001422: 00 90
00001423: 6A 90
00001424: FF 90
00001425: 8B 90
00001426: 55 90
00001427: F0 90
00001428: 52 90
00001429: FF 90
0000142A: 15 90
0000142B: 0C 90
0000142C: 60 90
0000142D: 40 90
0000142E: 00 90

To solve the second problem we would need to somehow print out the resulting buffer before it's memset to zero. We can use a debugger such as winedbg (given that we will need to script it). Let's break right before the memset is started at 0x401203:

.text:004011F5 E8 76 FE FF FF                                      call    mystery_func
.text:004011FA 83 C4 0C                                            add     esp, 0Ch
.text:004011FD 8B F8                                               mov     edi, eax
.text:004011FF 8B C3                                               mov     eax, ebx
.text:00401201 8B CE                                               mov     ecx, esi
.text:00401203 F3 AB                                               rep stosd
$ winedbg clone_patched.exe 781976132 187909120 326
WineDbg starting on pid 0029
0x7b86225f: movl	%edi,0x4(%esp)
Wine-dbg>b *0x401203
Breakpoint 1 at 0x00401203
Wine-dbg>cont
Stopped on breakpoint 1 at 0x00401203
Wine-dbg>x/s $edi
KÄ4╩H·╛)uÇ╟╛╒± σΘ¥[aoâ≥┼ß
Wine-dbg>

We seem to be on to something. Let's script everything and run it:

commands
b *0x401203
finish
x/s $edi
script.sh
#!/bin/bash
 
IFS=$'\n'
for i in `cat all_cmdlines.csv | cut -d' ' -f5-`; do
	winedbg --file commands clone_patched.exe $i  | grep -v Break | grep -v Stop | grep -v movl | grep -v Wine | strings
done
$ ./script.sh 
VjGi2
96)fN
HisJ&n
Vt!eS
ant<\
\7[I
h#,9
gl;0
E<}'0T
 jfh
`5y^
hY-HK
[xB"
dIp,`A
%O[Z
>"P$
b1FA
_Ff%
8"lB
Y.ZA
h'aO
no33[
RkNo
ntNTf>io
Qnl3PW
!nz3
>;w*
A+q>
b`|wD
2c44
Xs[l!
w8&lf
br4a
tZFHg1W
nlJ\
 Dy}
5w_V
_gi6
mg`@
W X~:
Oq;1
ncsO
YK)'L
$DI$
C*Z!<
w1AAq8uz
ohLm
Ze\A
XX]9
Qfz6p
J#g(q
;C6T
;d9 
R,`#
'}\y&
WtID
*H8p)
_:yK
	@8}U
%7"Z
j3?^
!E+\
q\	-
KrNBF
 c&lD
OEmG
n|HP
@ZM7
L.xk
->hl
J!)H
And Now His Watch is Ended
.XZ{d
jCmm"
sj?3
?$/}R
&l*n-F
Ux73
"ut<
Hmzv
13m=u
rAO[/
g]&Rb4
:r[,
-SeG
KnC~
Lz`)`
-"BM
7G~\
1?T$
#@~}
 DIi7L
x0q4x
.{l 
7u3T
:9%W
kQ]_
%l^N
'!!s
c5f$
4qD02
2]t%
WahO
+1{F
3F?e
hD[*/

There it is, in plain sight: “And Now His Watch is Ended”.