PwnThyBytes TTM Writeup (TimeTravelDebugging)

Two files are given, TTM.py and TTM.run, along with a hint to Time Travel Debugging (TTD). The contents of ttm.py is:

from Crypto.Cipher import AES
from Crypto.Util import Counter
from os import urandom

with open("secret_key", "rb") as key_file_fd:
    KEY = key_file_fd.read(16)

with open("flag") as flag_fd:
    FLAG = flag_fd.readline().strip()

print("key =", bytes.hex(KEY), "length =", len(KEY))
# key = REDACTED length = 16
nonce = urandom(8)
print("nonce =", bytes.hex(nonce))
# nonce = f0d27667d7000df9
ctr = Counter.new(128, initial_value=int.from_bytes(nonce, "big"))
aes_cipher = AES.new(KEY, AES.MODE_CTR, counter=ctr)
flag = FLAG.encode("utf-8")
enc_flag = aes_cipher.encrypt(flag)
print("encrypted flag =", bytes.hex(enc_flag))
# encrypted flag = 63242f9d6c4c748231602c990c0b6a51dbc0ca0ea0f26a780fe613

My first thought was that this python script had been run, and the .run file was a TTD dump of the run. After much searching around for something analogues to TTD in python, I came across Microsoft's TTD in WinDbg that I initially dismissed. So the .run file is a WinDbg TTD dump, opening it with WinDbg Preview it also becomes clear that the program being debugged isn't python but an exe called secret_key_gen.exe:

0:000> lm
start             end                 module name
00007ff6`64f00000 00007ff6`64f25000   secret_key_gen   (deferred)             
00007ffa`f12f0000 00007ffa`f14cc000   TTDRecordCPU   (deferred)             
00007ffa`fd110000 00007ffa`fd20f000   ucrtbase   (deferred)             
00007ffa`fd210000 00007ffa`fd237000   bcrypt     (deferred)             
00007ffa`fd240000 00007ffa`fd4ff000   KERNELBASE   (deferred)             
00007ffa`fd560000 00007ffa`fd5fd000   msvcp_win   (deferred)             
00007ffa`fd600000 00007ffa`fd622000   win32u     (deferred)             
00007ffa`fd6e0000 00007ffa`fd7ea000   gdi32full   (deferred)             
00007ffa`fd890000 00007ffa`fd94d000   KERNEL32   (deferred)             
00007ffa`fdb00000 00007ffa`fdb2a000   GDI32      (deferred)             
00007ffa`fdc10000 00007ffa`fdc40000   IMM32      (deferred)             
00007ffa`fe6e0000 00007ffa`fe880000   USER32     (deferred)             
00007ffa`fe9b0000 00007ffa`ff0c1000   SHELL32    (deferred)             
00007ffa`ff750000 00007ffa`ff944000   ntdll      (pdb symbols)          

I haven't used WinDbg before, people joke about the learning curve on radare2, this is worse.

TTD

Time Travel Debugging is what we would all like all debugging to be, you can read Microsofts documentation on it here. In essence it's a recording of execution, that you can jump around in using the debugger. However as far as i can tell, it's not the actual program, nor did i find a way to view any console associated with the execution.

Finding the interesting part

So just running it in WinDbg yields nothing of any interest, just the loaded modules and the last instruction to terminate. There also didn't seem to be any symbolic information in the .run file, as i was unable to explore any functions in the secret_key_gen module.
After a bunch of not understanding WinDbg, and running in vague circles i decided just to trace the program. My thought was that any interesting code must be calling the WinAPI with SOMETHING i could use. So i jumped to right before the bcrypt module got loaded, as i assumed this was also where the interesting parts are.

Usting the wt command to print functioncalls along the way, and surely enough i found a lot of calls to our secret_key_gen module. I started digging around and found values returned in RAX that looked a lot like ascii. Translating them i got
"--== TTM (beta) ==--"

Reversing

As you might note, that string is not printed by our python script above!
We're dealing with a completely different program as i suspected.
A little further down the trace, more ascii values are returned by something. The translated string here is "Secret key successfully saved to secret_key.". Now here is a reference to our script! This is the file that the key is loaded from. Somewhere in between these two series of function calls, there was two interesting kernel32 calls:
* CreateFileW
* WriteFile
If we issue a breakpoint on kernel32!CreateFileW and inspect the call parameters as according to the x64 calling convention, we can see what file is being created or opened. The first argument should be stored in the RCX register second in RDX and so on. Here is how the CreateFile prototype looks:

HANDLE CreateFileA(
  LPCSTR                lpFileName,
  DWORD                 dwDesiredAccess,
  DWORD                 dwShareMode,
  LPSECURITY_ATTRIBUTES lpSecurityAttributes,
  DWORD                 dwCreationDisposition,
  DWORD                 dwFlagsAndAttributes,
  HANDLE                hTemplateFile
);

So the register RCX should be a pointer to the filename, let's see:

0:000> !tt 0:0
Setting position to the beginning of the trace
[...]
0:000> bp kernel32!CreateFileW
breakpoint 0 redefined
0:000> g
[...]
Breakpoint 0 hit
Time Travel Position: CA1:52
KERNEL32!CreateFileW:
00007ffa`fd8b4b60 ff25bac50500    jmp     qword ptr [KERNEL32!_imp_CreateFileW (00007ffa`fd911120)] ds:00007ffa`fd911120={KERNELBASE!CreateFileW (00007ffa`fd26f290)}
0:000> r rcx
rcx=000001ecd6cf4648
0:000> db 000001ecd6cf4648
000001ec`d6cf4648  73 00 65 00 63 00 72 00-65 00 74 00 5f 00 6b 00  s.e.c.r.e.t._.k.
000001ec`d6cf4658  65 00 79 00 00 00 00 00-00 00 00 00 00 00 00 00  e.y.............
000001ec`d6cf4668  73 c9 d5 d5 a8 e6 00 00-c0 4a cf d6 ec 01 00 00  

So the filename being passed here is "secret_key" and we can assume that a call to WriteFile follows with the contents of the secret key. Looking at the WriteFile prototype:

 BOOL WriteFile(
  HANDLE       hFile,
  LPCVOID      lpBuffer,
  DWORD        nNumberOfBytesToWrite,
  LPDWORD      lpNumberOfBytesWritten,
  LPOVERLAPPED lpOverlapped
);

This time, the lpBuffer is the bytes to be written to the file, and is stored in RDX. Setting a breakpoint and dumping the buffer yields the following:

0:000> bp kernel32!WriteFile
0:000> g
Breakpoint 1 hit
Time Travel Position: CB0:97
KERNEL32!WriteFile:
00007ffa`fd8b4fd0 ff255ac10500    jmp     qword ptr [KERNEL32!_imp_WriteFile (00007ffa`fd911130)] ds:00007ffa`fd911130={KERNELBASE!WriteFile (00007ffa`fd26fa50)}
0:000> r rdx
rdx=0000002154aff7a00:000> db 0000002154aff7a0
00000021`54aff7a0  ec 73 92 b7 b6 8f 59 99-82 02 3c 4d a7 8f ce 7c  .s....Y...<M...|
[..]

So there we have the secret_key!

In essence the exe that is being run generates some secret key, and stores it in a file. We intercepted the api call, and dumped the paremeters to the call using WinDbg, thereby getting what would become the contents of the file "secret_key".

Using is with the AES decrypt function as well as the nonce from ttm.py, we get the flag: ptbctf{t1m3_1s_4n_1llus10n}

This was a rather hard challenge for me, since my work with WinDbg is minimal, but a fun introduction to the subject of both WinDbg and TTD for me.

Show Comments