This is my writeup for Time Machine reverse challenge from FwordCTF 2021, I enjoyed the CTF so much there were 4 reverse challenges and I solved 2 of them, Unfortunately I did not have much time to look at the other two challenges but I’m sure they’re awesome too and will tackle them later!
(NOTE: I got the flag 8 minutes after the CTF 😅)
Time Machine is my first nanomites challenge to solve ever that is why I want to share this experience for people who are new to this exciting concept and here is the official writeup with the challenge source code by my friend and the challenge author joezid
Let’s go back to the past!
Time Machine
I checked the binary Time_Machine.exe using PEscope and It’s a 64-bit binary with interesting libraries as libgcc_s_seh-1.dll
and libstdc++-6.dll
, I also notices interesting APIs like: CreateProcessA
, VirtualAlloc
, ReadProcessMemory
and WriteProcessMemory
:
By running the binary and monitoring the behavior through process hacker while the binary is waiting for input I noticed a child process which is expected from the APIs above:
Then to understand the behavior even more I used tiny tracer to monitor the APIs calls and I noticed this pattern being repeated after CreateProcessA
:
I moved to IDA and there was a small main function that checks the value of the environment variable joezidsecret
if null it continues with re_debugger
function otherwise it will call re_debuggee
function with the argv
as argument:
Debuggee
I went for reversing the re_debuggee
function and it prints Enter The Flag:
then takes user input then copies a region in memory to an intermediate memory, allocates a new region with VirtualAlloc
, and copies the intermediate memory to the newly allocated region which is a shellcode and finally calling that shellcode with the user input as argument:
I was curious to see the shellcode so I sat a breakpoint on the conditional branch in main and flipped the zero flag bit to enter the re_debugee
function:
And another breakpoint on the call to the shellcode:
Stepping in to the shellcode revealed a chunk of assembly instructions:
This chunk does the following:
- loads the first byte from the user input into
r12b
register - XORing a weird value with 0x1337
- rotating right the result by 0xD bits
- setting
r13
register to 0 - executing
ud2
which is the defined “undefined” opcode that crashes the program!
Since the argument of the size of the previous memcpy
and VirtualAlloc
is 0x3855 that means the shellcode size is 14421
bytes, so I converted the following chunk to code:
It’s pretty much the same previous chunk the only difference is the weird value that MOVes to r11
register!
I played some time with the values and converted 2 more chunks, and I noticed a difference on the 4-th chunk that r13
register is set to 1
not 0
, that’s interesting:
I wrote an IDApython script to extract the shellcode assembly instructions:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import idc
from idaapi import *
SHELLCODE_SIZE = 0x3855
MOV_R12B = 0x8A44
UD2_OPCODE_LENGTH = 2
START = 0x170000
ea = START
with open("instructions.txt", "w") as f:
while True:
# mov r12b, <letter>
if Word(ea) == MOV_R12B:
MakeCode(ea)
MakeFunction(ea)
# getting the assembly instructions
inst = idc.generate_disasm_line(ea, 0)
f.write(inst + "\n")
# advance ea pointer to next chunk
if GetMnem(ea) == "ud2":
ea += UD2_OPCODE_LENGTH
else:
ea = NextHead(ea)
# if last ud2 opcode
if ea >= START + (SHELLCODE_SIZE - UD2_OPCODE_LENGTH):
break
print("=== DONE GETTING INSTRUCTIONS ===")
Then I noticed the mov r13, 1
instruction is present only once in the 10 chunks for each letter which makes it an indicator of a true value for the weird value MOVed to r11
as it’s also ends with 337
as in 83BDBECD337
I changed EIP to the address of the chunk that returns 1 for the first letter and after going through the XOR
and ROR
the result in r11
was 41DEDF66
which is not meaningful so I assumed it’s a hash:
I then knew it’s time to go to the re_debugger
function, and this is the thing about nanomites, the parent acts like a debugger and runs the wanted chunk that exists in the child that acts as the debuggee, and the child at the end of the chunk causes a signal or crash (ud2
here) to return the execution to the parent which as a debugger have access to the registers of the debuggee, that prevents attaching a debugger to the child since it’s already being debugged (at least a debugger which uses the same APIs as parent)
Debugger
At first the re_debugger
function it sets the environment variable joezidsecret=1
then creates the child process using CreateProcessA
API:
Then it enters a loop that includes some switch cases for the debug event code to handle the exception thrown by the debuggee and track the state:
By inspecting the functions called in the cases or an easier approach viewing the strings, the string Correct Flag :)
is interesting:
The XREF to this string is in the function re_check_flag
and it seems very interesting:
Combining this with my understanding of the debuggee makes it clear, it’s basically:
- reading the
r12
register (the i-th letter in user input) - hashing the letter
- comparing the result with
r11
(result fromXOR
andROR
in the debuggee) - if the hashes equal and the
r13
register is set to1
, then a counter increments - if the counter equals 39, it prints out
Correct Flag :)
So the solution approach is now obvious:
- mapping printable letters to its corresponding hashes
- getting the values MOVed to
r11
from the instructions wherer13=1
and - XORing and rotating these values to get the hashes of the flag letters
- mapping each flag letter to the plain printable letter
Since I already have the shellcode instructions, I’m missing the hashes for printable characters, one way to get them is to get the hashing algorithm code, but that seems easy so I wanted to challenge myself in IDApython, I set a breakpoint on the function and I made an IDApython script to make the executable do this job for me, by making re_hashing
function execute normally and saving both the plain character (the function argument rcx
) and its hash (the return value eax
) in a text file!:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import idc
from idaapi import *
FUNC_ADDR = 0x40185D
with open("hashes.txt", "a") as f:
for l in range(39):
plain = Byte(GetRegValue("rcx"))
step_over()
GetDebuggerEvent(WFNE_SUSP, -1)
enc_hash = GetRegValue("eax")
f.write(chr(plain) + " " + hex(enc_hash)[2:] + "\n")
for _ in range(10):
run_to(FUNC_ADDR)
GetDebuggerEvent(WFNE_SUSP, -1)
print("=== [%d] hashing letter '%c' ===" % (l, chr(plain)))
if plain == ord("}"):
break
I entered the input as the possible printable character that can show in a flag consisting of 70 characters:
It did ran until letter C
because the flag is 39 characters so It didn’t complete so I provided it with the rest in the second time:
Now what’s left is to decrypt the flag hashes and map them to original letters and here comes the part for this little python script:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
def xor(val, key):
return val ^ key
def ror(val, shamt):
return ((val & (2**64 - 1)) >> shamt % 64) | (val << (64 - (shamt % 64)) & (2**64 - 1))
def decrypt(val, key, shamt):
return ror(xor(val, key), shamt)
hashes = {}
# adding the letters and their hashes in the dictionary
with open("hashes.txt", "r") as f:
for line in f.readlines():
line = line.rstrip('\n').split()
if "L" in line[1]:
line[1] = line[1][:-1]
hashes[int(line[1],16)] = line[0]
values = []
# getting the encrypted hash values from the shellcode instructions
with open("instructions.txt", "r") as f:
insts = f.readlines()
for i in range(0, len(insts)):
inst = insts[i].rstrip('\n').split()
if inst[-1] == "1":
mov_r11_val = insts[i-3].rstrip('\n').split()
val = int(mov_r11_val[-1][:-1], 16)
values.append(val)
KEY = 0x1337
SHAMT = 0xD
flag = ""
for value in values:
enc_val = decrypt(value, KEY, SHAMT)
flag += hashes[enc_val]
print(flag)
Running the script reveals the flag:
Flag: FWORDctf{W3_4t_th3_t0p_4g41n_n0w_wh4t?}
Now, back to the future!