PlaidCTF 2016: rev175 "quick"

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}