|| Date: 19-01-20 || Back to index ||
|| Tag: write-up ||

PicoCTF 2018 Write-up

grep 1

[0] % cgrep -RiE "pass|api|key|secret|pico" *
picoCTF{grep_and_you_will_find_cdf2e7c2}


[0] % nc 2018shell.picoctf.com 10854
That wasn't so hard was it?
picoCTF{NEtcat_iS_a_NEcESSiTy_c97963fe}

be-quick-or-be-dead-1

Using Frida:

// agent.js
'use strict';

//const target_module = "be-quick-or-be-dead-1";
/*
 * NOTE: this only works since the binary is not compiled with ASLR.
 *
 * $ rabin2 -I be-quick-or-be-dead-1 | grep pic
 *  pic false
 */
const target_addr = "0x400742"

var old_func = new NativeFunction(
    ptr(target_addr),
    'void',
    ['void']
);

var new_func = new NativeCallback(
    function(args) {
        console.log('[+] Doing nothing...');
    },
    'void',
    ['void']
);

Interceptor.replace(old_func, new_func);

Invoking with:

$ frida -f be-quick-or-be-dead-1 -l agent.js
%resume

[Local::be-quick-or-be-dead-1]-> %resume
Be Quick Or Be Dead 1
=====================

Calculating key...
[+] Doing nothing...
[Local::be-quick-or-be-dead-1]-> Done calculating key
Printing flag:
picoCTF{why_bother_doing_unnecessary_computation_402ca676}
Process terminated
[Local::be-quick-or-be-dead-1]->

Assembly-1

Answer is:

.intel_syntax noprefix
.bits 32

.global asm1

asm1:
    push    ebp
    mov ebp,esp
    cmp DWORD PTR [ebp+0x8],0x9a 
    jg  part_a   # if arg1 > 0x9a, jmp to part_a
    cmp DWORD PTR [ebp+0x8],0x8
    jne part_b
    mov eax,DWORD PTR [ebp+0x8]
    add eax,0x3
    jmp part_d
part_a:
    cmp DWORD PTR [ebp+0x8],0x2c
    jne part_c # if arg1 != 0x2c, jmp part_c
    mov eax,DWORD PTR [ebp+0x8]
    sub eax,0x3
    jmp part_d
part_b:
    mov eax,DWORD PTR [ebp+0x8]
    sub eax,0x3
    jmp part_d
    cmp DWORD PTR [ebp+0x8],0xc8
    jne part_c
    mov eax,DWORD PTR [ebp+0x8]
    sub eax,0x3
    jmp part_d
part_c:
    mov eax,DWORD PTR [ebp+0x8] # we end up here
    add eax,0x3 
part_d:
    pop ebp
    ret

quackme

After a bit of debugging, it turns out that the following bit is the one responsible for writing “You are Winner” string.

│       │   ; CODE XREF from sym.do_magic (0x8048711)
│      ┌──> 0x080486bd      8b45e8         mov eax, dword [local_18h]       ; Jump is taken since size > 0
│      ╎│   0x080486c0      0558880408     add eax, obj.sekrutBuffer       ; 0x8048858 ; ")\x06\x16O+50\x1eQ\x1b[\x14K\b]+S\x10TQCM\T]"
│      ╎│   0x080486c5      0fb608         movzx ecx, byte [eax]
│      ╎│   0x080486c8      8b55e8         mov edx, dword [local_18h]
│      ╎│   0x080486cb      8b45ec         mov eax, dword [s]
│      ╎│   0x080486ce      01d0           add eax, edx
│      ╎│   0x080486d0      0fb600         movzx eax, byte [eax]
│      ╎│   0x080486d3      31c8           xor eax, ecx
│      ╎│   0x080486d5      8845e3         mov byte [local_1dh], al
│      ╎│   0x080486d8      8b1538a00408   mov edx, dword obj.greetingMessage       ; [0x804a038:4]=0x80487f0 str.You_have_now_entered_the_Duck_Web__a
│      ╎│   0x080486de      8b45e8         mov eax, dword [local_18h]
│      ╎│   0x080486e1      01d0           add eax, edx
│      ╎│   0x080486e3      0fb600         movzx eax, byte [eax]
│      ╎│   0x080486e6      3a45e3         cmp al, byte [local_1dh]
│     ┌───< 0x080486e9      7504           jne 0x80486ef               ;[3]   ; likely ; (if local_1ch == 0x19, we win)
│     │╎│   0x080486eb      8345e401       add dword [local_1ch], 1       ; local_1ch increments everytime local_1dh == al
│     │╎│   ; CODE XREF from sym.do_magic (0x80486e9)
│     └───> 0x080486ef      837de419       cmp dword [local_1ch], 0x19       ; if local_1ch == 0x19, we win
│     ┌───< 0x080486f3      7512           jne 0x8048707               ;[4]   ; likely
│     │╎│   0x080486f5      83ec0c         sub esp, 0xc
│     │╎│   0x080486f8      68ab880408     push str.You_are_winner       ; 0x80488ab ; "You are winner!" ; const char *s
│     │╎│   0x080486fd      e86efdffff     call sym.imp.puts           ;[5]   ; int puts(const char *s)
│     │╎│   │                                                            ; int puts(const char * s : (*0xffffffff)0x00177fec = .........................................................................................................
│     │╎│   0x08048702      83c410         add esp, 0x10
│    ┌────< 0x08048705      eb0c           jmp 0x8048713               ;[6]
│    ││╎│   ; CODE XREF from sym.do_magic (0x80486f3)
│    │└───> 0x08048707      8345e801       add dword [local_18h], 1
│    │ ╎│   ; CODE XREF from sym.do_magic (0x80486bb)
│    │ ╎└─> 0x0804870b      8b45e8         mov eax, dword [local_18h]
│    │ ╎    0x0804870e      3b45f0         cmp eax, dword [size]
│    │ └──< 0x08048711      7caa           jl 0x80486bd                ;[7]   ; unlikely ; (Jump is taken since size > 0)
│    └────> 0x08048713      c9             leave
└           0x08048714      c3             ret

We can solve this with symbolic execution (TODO), but looking at the code with a debugger, we figured out the following:

So, Our Input Buffer “B” gets XOR-ed with secret buffer “S”, and the result has to be the greeting message “G”:

B X S = G

We need “B”. To get it, we can just XOR “S” and “G” together. The following Python snippet does just that.

import sys
import base64
from pwn import *


def main():
    str_to_encode = "You have now entered the Duck Web, and you're in for"

    # Taken from gdb:
    # gef➤  x/8xw 0x8048858
    # 0x8048858 <sekrutBuffer>:       0x4f160629      0x1e30352b      0x145b1b51      0x2b5d084b
    # 0x8048868 <sekrutBuffer+16>:    0x51541053      0x545c4d43      0x6f4e005d      0x6e696c20

    xor_key = p32(0x4f160629)
    xor_key += p32(0x1e30352b)
    xor_key += p32(0x145b1b51)
    xor_key += p32(0x2b5d084b)
    xor_key += p32(0x51541053)
    xor_key += p32(0x545c4d43)
    xor_key += p32(0x6f4e005d)

    print('Input: ' + str(str_to_encode))
    print('XOR key: ' + str(xor_key))
    print('--------------')

    # Encode
    plaintext_key_list = [hex(ord(elem)) for elem in str_to_encode]
    xor_key_list = [hex(ord(elem)) for elem in xor_key]
    cipher_str = ''
    for idx in range(len(xor_key_list)):
        a = int(plaintext_key_list[idx], 16)
        b = int(xor_key_list[idx], 16)

        x = a ^ b
        cipher_str += chr(x)

    print('Output: ' + cipher_str)

    cipher_str = base64.b64encode(cipher_str.encode('utf-8')).decode('utf-8')
    print('b64 Output: ' + cipher_str)


if __name__ == '__main__':
    main()

Running it gives us the following:


[0] % py2 encoder.py
Input: You have now entered the Duck Web, and you're in for
XOR key: )\x06\x16O+50\x1e]+S\x10TQCM\T]\x00No
--------------
Output: picoCTF{qu4ckm3_6b15c941}D;\x0c
b64 Output: cGljb0NURntxdTRja20zXzZiMTVjOTQxfUQ7DA==

Flag is picoCTF{qu4ckm3_6b15c941}

assembly-2

I think the best way to solve those assembly mazes is not to trace it but to simply run/debug it.

Challenge assembly file. The header is modified a bit to accomodate for yasm syntax

SECTION .TEXT
    GLOBAL asm2

asm2:
    push    ebp
    mov     ebp,esp
    sub     esp,0x10
    mov     eax,DWORD [ebp+0xc]
    mov     DWORD [ebp-0x4],eax
    mov     eax,DWORD [ebp+0x8]
    mov     DWORD [ebp-0x8],eax
    jmp     part_b
part_a: 
    add     DWORD [ebp-0x4],0x1
    add DWORD [ebp+0x8],0x64
part_b: 
    cmp     DWORD [ebp+0x8],0x1d89
    jle     part_a
    mov     eax,DWORD [ebp-0x4]
    mov esp,ebp
    pop ebp
    ret

C file to run the function asm2

#include <stdio.h>

extern unsigned int asm2(unsigned int, unsigned int);

int main(void) {
    printf("%d\n", asm2(0x4, 0x2d));
    return 0;
}

Compilation and linkage

[0] % yasm -f elf32 loop_asm_rev.S

[0] % gcc -m32 -c -o main.o main.c

[0] % gcc -m32 -o run loop_asm_rev.o main.o

[0] % ./run
121

[0] % rax2 121
9x79

Answer is 0x79

be-quick-or-be-dead-2

This one is really cool. Running the binary, shows the following:

[0] % ./be-quick-or-be-dead-2
Be Quick Or Be Dead 2
=====================

Calculating key...
You need a faster machine. Bye bye.

Plugging the setTimer with Frida just like in the be-quick-or-be-dead-1 doesn’t work either:

[0] % frida -f be-quick-or-be-dead-2 -l agent.js
     ____
    / _  |   Frida 12.2.30 - A world-class dynamic instrumentation toolkit
   | (_| |
    > _  |   Commands:
   /_/ |_|       help      -> Displays the help system
   . . . .       object?   -> Display information about 'object'
   . . . .       exit/quit -> Exit
   . . . .
   . . . .   More info at http://www.frida.re/docs/home/
Spawned `be-quick-or-be-dead-2`. Use %resume to let the main thread start executing!
[Local::be-quick-or-be-dead-2]-> %resume

Be Quick Or Be Dead 2
=====================

Calculating key...

And it just keeps going for a very long time. It looks like the calculation method is either broken or it really does take a long time.

Taking a look with Radare yields the following:

$ r2 -A be-quick-or-be-dead-2
[0x0040074b 20% 345 be-quick-or-be-dead-2]> pd $r @ sym.calculate_key
┌ (fcn) sym.calculate_key 16
│   sym.calculate_key ();
│           ; CALL XREF from sym.get_key (0x4007e1)
│           0x0040074b      55             push rbp
│           0x0040074c      4889e5         mov rbp, rsp
│           0x0040074f      bf3b040000     mov edi, 0x43b
│           0x00400754      e8adffffff     call sym.fib                ;[1]  ; sym.fib(0x43b)
│           0x00400759      5d             pop rbp
└           0x0040075a      c3             ret                         ; rsp

The calculate_key function apparently calculates the Fibonacci series of 0x43b (1083). From this link, the already calculated Fibonacci series of 1083 is:

fib
fib

My computer would need a lot of time to calculate that. If we take a look at the fib function, we’ll actually see that the return value is stored in EAX register. That means that the result of fib(0x43b) will actually be warped into a 32-bit register:

[0x00400706 19% 345 be-quick-or-be-dead-2]> pd $r @ sym.fib
┌ (fcn) sym.fib 69
│   sym.fib (unsigned int arg1);
│           ; var unsigned int local_24h @ rbp-0x24
│           ; var unsigned int local_14h @ rbp-0x14
│           ; arg unsigned int arg1 @ rdi
│           ; CALL XREFS from sym.fib (0x400728, 0x400737)
│           ; CALL XREF from sym.calculate_key (0x400754)
│           0x00400706      55             push rbp
...
...
...
│           0x00400741      8b45ec         mov eax, dword [local_14h]
│           0x00400744      4883c428       add rsp, 0x28               
│           0x00400748      5b             pop rbx
│           0x00400749      5d             pop rbp
└           0x0040074a      c3             ret

We would need then to just have Frida insert that warped figure as the return value of fib function and we’re good to go.

In python:

$ python
>>> # Number shortened to save space
>>> fib(0x43b) % int(math.pow(2,32))
3228060226

The Frida agent would look like this now:

'use strict';

const fib_0x43b = 3228060226;
decrypt_flag_func_addr = ptr("0x00400696");
fib_func_addr = ptr("0x00400706");

// Replacing fib with our own implementation that just returns the number without calculation
var old_fib = new NativeFunction(
    fib_func_addr,
    'uint',
    ['uint']
);

var new_fib = new NativeCallback(
    function(args) {
        return fib_0x43b;
    },
    'uint',
    ['uint']
);

Interceptor.replace(old_fib, new_fib);

// We'll just print `this.context` which allows us to see the register values to confirm that 
// the supplied argument, usually in the RDI register, is the correct Fibonacci number we returned
Interceptor.attach(decrypt_flag_func_addr, {
    onEnter: function (args) {
        console.log('Context  : ' + JSON.stringify(this.context));
    }
});

Running everything would yield:

[1] % frida -f be-quick-or-be-dead-2 -l agent.js
     ____
    / _  |   Frida 12.2.30 - A world-class dynamic instrumentation toolkit
   | (_| |
    > _  |   Commands:
   /_/ |_|       help      -> Displays the help system
   . . . .       object?   -> Display information about 'object'
   . . . .       exit/quit -> Exit
   . . . .
   . . . .   More info at http://www.frida.re/docs/home/
Spawned `be-quick-or-be-dead-2`. Use %resume to let the main thread start executing!
[Local::be-quick-or-be-dead-2]-> %resume
Be Quick Or Be Dead 2
=====================

Calculating key...
Done calculating key
Printing flag:
picoCTF{the_fibonacci_sequence_can_be_done_fast_88f31f48}
[+] Doing nothing...
Context  : {"pc":"0x400696","sp":"0x7ffd678750e8","rax":"0xc0684a42","rcx":"0x7f7c76fd184f","rdx":"0x7f7c770a5720","rbx":"0x0","rsp":"0x7ffd678750e8","rbp":"0x7ffd678750f0","rsi":"0x1715ac0","rdi":"0xc0684a42","r8":"0x0","r9":"0xffffffff","r10":"0x7f7c75255530","r11":"0x0","r12":"0x4005a0","r13":"0x7ffd678751f0","r14":"0x0","r15":"0x0","rip":"0x400696"}

The flag is picoCTF{the_fibonacci_sequence_can_be_done_fast_88f31f48}

be-quick-or-be-dead-3

Let’s plug the setTimer function to get rid of it.

[0x004008c4]> s 0x004008c4
[0x004008c4]> pd 10
Free fake stack
│           0x004008c4      e8f8feffff     call sym.set_timer
│           0x004008c9      b800000000     mov eax, 0
│           0x004008ce      e842ffffff     call sym.get_key
│           0x004008d3      b800000000     mov eax, 0
│           0x004008d8      e863ffffff     call sym.print_flag
│           0x004008dd      b800000000     mov eax, 0
│           0x004008e2      c9             leave                       ; rsp
└           0x004008e3      c3             ret
            0x004008e4      662e0f1f8400.  nop word cs:[rax + rax]
            0x004008ee      6690           nop
[0x004008c4]> wao nop
Failed to write
[0x004008c4]> oo+
[0x004008c4]> wao nop
[0x004008c4]> pd 10
Free fake stack
│           0x004008c4      90             nop
│           0x004008c5      90             nop
│           0x004008c6      90             nop
│           0x004008c7      90             nop
│           0x004008c8      90             nop
│           0x004008c9      b800000000     mov eax, 0
│           0x004008ce      e842ffffff     call sym.get_key
│           0x004008d3      b800000000     mov eax, 0
│           0x004008d8      e863ffffff     call sym.print_flag
│           0x004008dd      b800000000     mov eax, 0
[0x004008c4]>

wao nop show do the job. Basically, it modifies the current opcode with the supplied operand.

sym.calculate_key is different now:

[0x004008c4]> s sym.calculate_key
[0x00400792]> pdf
Free fake stack
┌ (fcn) sym.calculate_key 16
│   sym.calculate_key ();
│           ; CALL XREF from sym.get_key (0x400828)
│           0x00400792      55             push rbp
│           0x00400793      4889e5         mov rbp, rsp
│           0x00400796      bf9f8e0100     mov edi, 0x18e9f
│           0x0040079b      e866ffffff     call sym.calc; sym.calc(0x18e9f)
│           0x004007a0      5d             pop rbp
└           0x004007a1      c3             ret                         ; rsp

[0x00400706]> s sym.calc
[0x00400706]> pds
Free fake stack
0x00400711 arg1
0x00400733 call sym.calc
0x00400742 call sym.calc
0x00400751 call sym.calc
0x00400761 call sym.calc
0x00400776 call sym.calc
0x0040079b call sym.calc
0x004007aa arg1
0x004007ad const char *s
0x004007ad str.You_need_a_faster_machine._Bye_bye.
0x004007b2 call sym.imp.puts
0x004007b7 int status
0x004007bc call sym.imp.exit
0x00000000 [30] ---- section size 741 named .strtab

sym.calc(0x18e9f) is our function. pds prints a function summary since the function was too big to print. It looks like it uses quite a bit of recursion which is slowing down the calculation.

After taking a look at the function, it looks like the algorithm itself is broken. It could use some memoization. Python is pretty good for this type of issue. First of all, we’ll have to get a general overview of what the function actually does. Visually tracing the assembly should be enough. I got stuck on a couple of instructions. The built-in pdc and r2dec-js were fantastic in sorting those out.

Here is the resultant python code which performs technically the same implementation as sym.calc:

import sys
import math

memo = {}

# Hack to get the recursion to work in python
sys.setrecursionlimit(100000)

def calc(edi):
    if edi <= 0x4:
        return edi * edi + 0x2345
    else:
        if edi not in memo:
            memo[edi] = calc(edi - 0x5) * 0x1234 + (calc(edi - 0x1) - calc(edi - 0x2)) + (calc(edi - 0x3) - calc(edi - 0x4));
        return memo[edi]

# Wrap it to max of 32-bits since the result is stored
# in an EAX register, which means it can only fit in a 32-bit register
print(calc(0x18e9f) % int(math.pow(2, 32)))

Result is 3610015907. I could’ve replaced the function to return the but I wanted to use Frida again:

'use strict';

const calc_ret = 3610015907;
const calc_addr = ptr("0x00400706");
const decrypt_flag_addr = ptr("0x00400696");

var old_calc = new NativeFunction(
    calc_addr,
    'uint',
    ['int']
);

var new_calc = new NativeCallback(
    function(args) {
        return calc_ret;
    },
    'uint',
    ['int']
);

Interceptor.replace(old_calc, new_calc);
        //
Interceptor.attach(decrypt_flag_addr, {
    onEnter: function (args) {
        console.log('Context  : ' + JSON.stringify(this.context));
    }
});

Running it yields:

[0] % frida -f be-quick-or-be-dead-3 -l agent.js
     ____
    / _  |   Frida 12.2.30 - A world-class dynamic instrumentation toolkit
   | (_| |
    > _  |   Commands:
   /_/ |_|       help      -> Displays the help system
   . . . .       object?   -> Display information about 'object'
   . . . .       exit/quit -> Exit
   . . . .
   . . . .   More info at http://www.frida.re/docs/home/
Spawned `be-quick-or-be-dead-3`. Use %resume to let the main thread start executing!
[Local::be-quick-or-be-dead-3]-> %resume
Be Quick Or Be Dead 3
=====================

Calculating key...
Done calculating key
Printing flag:
picoCTF{dynamic_pr0gramming_ftw_1ffc009d}
Context  : {"pc":"0x400696","sp":"0x7ffc539a3e28","rax":"0xd72c78a3","rcx":"0x7f50bac8584f","rdx":"0x7f50bad59720","rbx":"0x0","rsp":"0x7ffc539a3e28","rbp":"0x7ffc539a3e30","rsi":"0x8b6ac0","rdi":"0xd72c78a3","r8":"0x0","r9":"0xffffffff","r10":"0x7f50b8f09530","r11":"0x0","r12":"0x4005a0","r13":"0x7ffc539a3f30","r14":"0x0","r15":"0x0","rip":"0x400696"}
Process terminated
[Local::be-quick-or-be-dead-3]-> 

Flag is picoCTF{dynamic_pr0gramming_ftw_1ffc009d}

quackme-up

Results of the “quack it” sequence after running it with different inputs:

hello:              11 80 20 E0 22 53 72 A1 01 41 55 20 A0 C0 25 E3 95 20 15 35 20 15 00 70 C1
helloworld:         11 80 20 E0 22 53 72 A1 01 41 55 20 A0 C0 25 E3 95 20 15 35 20 15 00 70 C1
helloworld2:        11 80 20 E0 22 53 72 A1 01 41 55 20 A0 C0 25 E3 95 20 15 35 20 15 00 70 C1
a:                  11 80 20 E0 22 53 72 A1 01 41 55 20 A0 C0 25 E3 95 20 15 35 20 15 00 70 C1

The “quack it” sequence looks stable. I think we basically have to figure out the encryption mechanism to solve this one.

Looks like the encryption algorithm is not reversible:

Radare2 pro-tip: Use afc for analyzing function calling convention. Use afC? for analyzing function complexity. Very, very useful for sizing-up a critical function.

The following python snippet should perform the same encryption as the challenge:

# py2 encrypt.py "hello"

import binascii
from pwnlib.util.fiddling import *
import sys

input = sys.argv[1]

cipher_text = []
for c in input:
    # Convert an ascii character to a hex number
    c = binascii.hexlify(c)
    c = int(c, 16)

    c = rol(c, 4, 8)
    c = c ^ 0x16
    c = ror(c, 8, 8)
    cipher_text.append(c)

cipher_text = [hex(n)[2:] for n in cipher_text]
print(cipher_text)

The decryption is actually just the reverse of this operation:

encryption: rol4 -> xor -> ror8
decryption: ror8 -> xor -> rol4

The following script outlines the solution:

import binascii
from pwnlib.util.fiddling import *
import sys


def encrypt(str):
    """
    str: string
    ret: list of hexadecimal strings, without the leading 0x
    """
    cipher_text = []
    for c in str:
        # Convert an ascii character to a number of base 16
        c = binascii.hexlify(c)
        c = int(c, 16)

        c = rol(c, 4, 8)
        c = c ^ 0x16
        c = ror(c, 8, 8)
        cipher_text.append(c)

    cipher_text = [hex(n)[2:] for n in cipher_text]
    return cipher_text


def decrypt(arr):
    """
    arr: list of hexadecimal strings, without the leading 0x
    ret: string
    """

    plaintext = []
    for i in arr:
        i = int(i, 16)

        i = ror(i, 8, 8)
        i = i ^ 0x16
        i = rol(i, 4, 8)
        plaintext.append(i)

    plaintext = [hex(n)[2:] for n in plaintext]
    plaintext = b''.join([binascii.unhexlify(n) for n in plaintext])
    return plaintext


def test():
    str = "hellodarknessmyoldfriend"
    cipher = encrypt(str)
    plain = decrypt(cipher)
    assert str == plain


cipher = ["11", "80", "20", "E0", "22", "53", "72", "A1", "01", "41", "55", "20",
          "A0", "C0", "25", "E3", "95", "20", "15", "35", "20", "15", "00", "70", "C1"]

print(decrypt(cipher))

Running it yields:

cheese@291233 : ~/course/CNIT124/picoctf/quackmeme-up
[0] % py2 decrypt.py
picoCTF{qu4ckm3_8c02c0af}

Solution is picoCTF{qu4ckm3_8c02c0af}