In the following we summarize our solutions for a reversing task that appeared last weekend during the PlaidCTF 2016.
This task is the perfect example that it is possible to obtain a secret from a compiled program even without having any clue about the internal mechanics of the code.
We are given a compiled SWIFT program that asks for the flag to be entered on stdin, does some transformation on the input and compares it to an hardcoded array:
403698: mov BYTE PTR [rdx],0x81 40369b: mov BYTE PTR [rdx+0x1],0x74 40369f: mov BYTE PTR [rdx+0x2],0x87 4036a3: mov BYTE PTR [rdx+0x3],0x79 4036a7: mov BYTE PTR [rdx+0x4],0xb0 4036ab: mov BYTE PTR [rdx+0x5],0x6a 4036af: mov BYTE PTR [rdx+0x6],0xaa 4036b3: mov BYTE PTR [rdx+0x7],0xa7 4036b7: mov BYTE PTR [rdx+0x8],0x68 4036bb: mov BYTE PTR [rdx+0x9],0x94 4036bf: mov BYTE PTR [rdx+0xa],0xa4 4036c3: mov BYTE PTR [rdx+0xb],0x7b 4036c7: mov BYTE PTR [rdx+0xc],0xaf 4036cb: mov BYTE PTR [rdx+0xd],0x89 4036cf: mov BYTE PTR [rdx+0xe],0xd4 4036d3: mov BYTE PTR [rdx+0xf],0xd1 4036d7: mov BYTE PTR [rdx+0x10],0x92 4036db: mov BYTE PTR [rdx+0x11],0xbe 4036df: mov BYTE PTR [rdx+0x12],0x94 4036e3: mov BYTE PTR [rdx+0x13],0xd8 4036e7: mov BYTE PTR [rdx+0x14],0x92 4036eb: mov BYTE PTR [rdx+0x15],0xcc 4036ef: mov BYTE PTR [rdx+0x16],0xda 4036f3: mov BYTE PTR [rdx+0x17],0xd1 4036f7: mov BYTE PTR [rdx+0x18],0xd3 4036fb: mov BYTE PTR [rdx+0x19],0xa9 4036ff: mov BYTE PTR [rdx+0x1a],0xd3 403703: mov BYTE PTR [rdx+0x1b],0xaa 403707: mov BYTE PTR [rdx+0x1c],0xec 40370b: mov BYTE PTR [rdx+0x1d],0xa8 40370f: mov BYTE PTR [rdx+0x1e],0xdd 403713: mov BYTE PTR [rdx+0x1f],0xef 403717: mov BYTE PTR [rdx+0x20],0xfa
As comparison is done byte by byte, we simply fire up gdb and set a breakpoint on
the cmp
:
40389D lea rdi, [rbp+var_168] 4038A4 mov rsi, [rbp+idx] 4038AB mov rdx, [rbp+is] 4038B2 call __TTSg5Vs5UInt8___TFSag9subscriptFSix 4038B7 lea rdi, [rbp+var_170] 4038BE mov al, [rbp+var_168] 4038C4 mov rsi, [rbp+idx] 4038CB mov rdx, [rbp+should] 4038D2 mov [rbp+var_201], al 4038D8 call __TTSg5Vs5UInt8___TFSag9subscriptFSix 4038DD mov al, [rbp+var_170] 4038E3 mov cl, [rbp+var_201] 4038E9 cmp cl, al <= break here
This was the part that confused us the most: Since the “transformations”
applied to the flag simply consist of the addition of the charcodes of each input
character, targeting an uneven value like 0x81
seems impossible. Reading up on
SWIFT strings a little bit we learnt that this language is unicode-aware and
does string comparison on their canonical versions.
This led to the idea of entering letters and combining unicode characters, and
indeed a string like a ̀
would result in 0x61
and 0x20
being added together
in the transformation step, satisfying the first check. (We figure that SWIFT
accesses the string once canonicalizing the input value and once simply reads
the raw bytes.) The problem with this idea was still that the 0x81
value
would occur twice, making the program abort the comparison at the second index
(0x74
).
At this point, we switched over to brainless guessing, which turned out to work astonishingly well: There is a second hardcoded array in the main function that is checked only if the comparison with the first array succeeded. As we failed hard on the first array, we reverted to trying to satisfy the second check to get some helpful insights for the problem mentioned above.
403C7C mov byte ptr [rdx], 50h 403C7F mov byte ptr [rdx+1], 43h 403C83 mov byte ptr [rdx+2], 8Ah 403C87 mov byte ptr [rdx+3], 64h 403C8B mov byte ptr [rdx+4], 0EDh 403C8F mov byte ptr [rdx+5], 0A6h 403C93 mov byte ptr [rdx+6], 0ABh 403C97 mov byte ptr [rdx+7], 93h 403C9B mov byte ptr [rdx+8], 0CCh 403C9F mov byte ptr [rdx+9], 0EBh 403CA3 mov byte ptr [rdx+0Ah], 0C2h 403CA7 mov byte ptr [rdx+0Bh], 9Ah 403CAB mov byte ptr [rdx+0Ch], 0FAh 403CAF mov byte ptr [rdx+0Dh], 6Ah 403CB3 mov byte ptr [rdx+0Eh], 0ABh 403CB7 mov byte ptr [rdx+0Fh], 93h 403CBB mov byte ptr [rdx+10h], 0CCh 403CBF mov byte ptr [rdx+11h], 0EBh 403CC3 mov byte ptr [rdx+12h], 6Ah 403CC7 mov byte ptr [rdx+13h], 0BBh 403CCB mov byte ptr [rdx+14h], 62h 403CCF mov byte ptr [rdx+15h], 33h 403CD3 mov byte ptr [rdx+16h], 0D1h 403CD7 mov byte ptr [rdx+17h], 0F5h 403CDB mov byte ptr [rdx+18h], 0C2h 403CDF mov byte ptr [rdx+19h], 9Ah 403CE3 mov byte ptr [rdx+1Ah], 0FAh 403CE7 mov byte ptr [rdx+1Bh], 6Ah 403CEB mov byte ptr [rdx+1Ch], 0BBh 403CEF mov byte ptr [rdx+1Dh], 62h 403CF3 mov byte ptr [rdx+1Eh], 33h 403CF7 mov byte ptr [rdx+1Fh], 0D1h 403CFB mov byte ptr [rdx+20h], 0D7h
It turned out that this was not needed at all: Flipping the conditional jump at
0x403896
, the program compares our (again somehow transformed) input to the
second array when execution reaches 0x403eed
. As it’s doing this in a
byte-by-byte way, we simply monkey-patched the binary and hacked together the
following python script that prints out the flag. (Note that this only works
because the program echoes “Good Job!” if our input is a prefix of the
solution.)
#!/usr/bin/env python3
import subprocess
flag = 'P'
while flag[-1] != '}':
for i in range(0x20, 0x7f):
p = subprocess.Popen(['./quick.sh'], stdout=subprocess.PIPE, stdin=subprocess.PIPE)
p.stdin.write("{}{}".format(flag, chr(i)).encode())
out = p.communicate()[0].decode()
if "Good" in out:
flag += chr(i)
break
print(flag)
~/2016_04_pCTF/rev175 % ./pwn.py PC PCT PCTF PCTF{ PCTF{5 PCTF{5u PCTF{5ur PCTF{5ur3 PCTF{5ur3_ PCTF{5ur3_a PCTF{5ur3_a5 PCTF{5ur3_a5_ PCTF{5ur3_a5_5 PCTF{5ur3_a5_5u PCTF{5ur3_a5_5ur PCTF{5ur3_a5_5ur3 PCTF{5ur3_a5_5ur3_ PCTF{5ur3_a5_5ur3_5 PCTF{5ur3_a5_5ur3_5w PCTF{5ur3_a5_5ur3_5w1 PCTF{5ur3_a5_5ur3_5w1f PCTF{5ur3_a5_5ur3_5w1ft PCTF{5ur3_a5_5ur3_5w1ft_ PCTF{5ur3_a5_5ur3_5w1ft_a PCTF{5ur3_a5_5ur3_5w1ft_a5 PCTF{5ur3_a5_5ur3_5w1ft_a5_ PCTF{5ur3_a5_5ur3_5w1ft_a5_5 PCTF{5ur3_a5_5ur3_5w1ft_a5_5w PCTF{5ur3_a5_5ur3_5w1ft_a5_5w1 PCTF{5ur3_a5_5ur3_5w1ft_a5_5w1f PCTF{5ur3_a5_5ur3_5w1ft_a5_5w1ft PCTF{5ur3_a5_5ur3_5w1ft_a5_5w1ft}