For the past few months, I’ve been learning to use the Binary Ninja api to level up my reversing skills. Here’s a demo of how powerful scripting is.

For this writeup, I will demostrate as much as I can using the binaryninja api. But also so I can have a reference if I ever forget

Initial Findings

Challenge files can be found here

I pulled up to a party but they wouldn’t let me in until I named 40 functions! 😡

Opening this up in binaryninja, we see a very large number of functions.

>>> len(list(bv.functions))
28970

But yet, main does not seem to do anything 🤔

$ ./final
⢀⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿[how am i gonna function]⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⡏⠉⠛⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⣿
⣿⣿⣿⣿⣿⣿⠀⠀⠀⠈⠛⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠛⠉⠁⠀⣿
⣿⣿⣿⣿⣿⣿⣧⡀⠀⠀⠀⠀⠙⠿⠿⠿⠻⠿⠿⠟⠿⠛⠉⠀⠀⠀⠀⠀⣸⣿
⣿⣿⣿⣿⣿⣿⣿⣷⣄⠀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⠏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠠⣴⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⡟⠀⠀⢰⣹⡆⠀⠀⠀⠀⠀⠀⣭⣷⠀⠀⠀⠸⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⠃⠀⠀⠈⠉⠀⠀⠤⠄⠀⠀⠀⠉⠁⠀⠀⠀⠀⢿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⢾⣿⣷⠀⠀⠀⠀⡠⠤⢄⠀⠀⠀⠠⣿⣿⣷⠀⢸⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⡀⠉⠀⠀⠀⠀⠀⢄⠀⢀⠀⠀⠀⠀⠉⠉⠁⠀⠀⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣧⠀⠀⠀⠀⠀⠀⠀⠈⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿

and main seems to only call puts

>>> bv.get_functions_by_name('main')[0].callees
[<func: x86_64@0x1050>, <func: x86_64@0x1050>, <func: x86_64@0x1050>, <func: x86_64@0x1050>, <func: x86_64@0x1050>, <func: x86_64@0x1050>, <func: x86_64@0x1050>, <func: x86_64@0x1050>, <func: x86_64@0x1050>, <func: x86_64@0x1050>, <func: x86_64@0x1050>, <func: x86_64@0x1050>, <func: x86_64@0x1050>, <func: x86_64@0x1050>]
>>> bv.get_functions_by_name('main')[0].callees[0].name
'puts'

Ok. let’s take a look at one of these functions

>>> print(bv.functions[20].hlil)
sub_7cdd1f(0x751, 0x13)
sub_89c82e(0x516, 0x6e)
sub_3bb211(0x3eb, 0x7f)
sub_292aa2(0x7a0, 0x20)
sub_7051d8(0x609, 0x44)
sub_59214d(0x36e, 0x55)
sub_41e778(0x70b, 0x1e)
sub_48006e(0x851, 0x7b)
sub_58d1cf(0x2b2, 0x1c)
sub_6fcd0a(0x752, 0x54)
sub_2f4c47(0x417, 0x5b)
sub_bc41b(0x18a, 0x1f)
sub_63281d(0x6a, 0x6e)
sub_45478c(0x57d, 0x30)
sub_73dbf3(0x59a, 0x41)
return zx.q(arg1 << arg2)

It seems to call a bunch of other functions, and return arg1 << arg2.

In fact, it seems like this pattern follows for all of these functions. Call a ton of functions and return.

>>> def check(func):
... 	assert len(list(func.hlil)) == 1
... 	block = list(func.hlil)[0]
... 	for hlil in block:
... 		print(hlil.operation)
... 
... check(bv.functions[20])
... 
HighLevelILOperation.HLIL_CALL
HighLevelILOperation.HLIL_CALL
HighLevelILOperation.HLIL_CALL
HighLevelILOperation.HLIL_CALL
HighLevelILOperation.HLIL_CALL
HighLevelILOperation.HLIL_CALL
HighLevelILOperation.HLIL_CALL
HighLevelILOperation.HLIL_CALL
HighLevelILOperation.HLIL_CALL
HighLevelILOperation.HLIL_CALL
HighLevelILOperation.HLIL_CALL
HighLevelILOperation.HLIL_CALL
HighLevelILOperation.HLIL_CALL
HighLevelILOperation.HLIL_CALL
HighLevelILOperation.HLIL_CALL
HighLevelILOperation.HLIL_RET

Ok, let’s do a sanity check

>>> def check(func):
... 	assert len(list(func.hlil)) == 1
... 	block = list(func.hlil)[0]
... 	for hlil in block[:-1]:
... 		if hlil.operation != HighLevelILOperation.HLIL_CALL:
... 			print(func)
... 			return False
... 	if block[-1].operation != HighLevelILOperation.HLIL_RET:
... 		print(func)
... 	return block[-1].operation == HighLevelILOperation.HLIL_RET
... 
... funcs = list(bv.functions)[11:-1]
... print(all(list(map(check,funcs))))
... 
int64_t sub_5e347b()
False

Ok, all but one function seems to follow this behaviour (We will see later this function is important).

What does Well Connected mean?

If you happen to have a background in Computer Science or Competitive Programming, “Well Connected” could refer to a Well Connected Graph (aka Strongly Connected Graph). So then I had the idea

lightbulb!

Before constructing the graph, I looked at the callees of every function to see if any of them would be the “start” node.

>>> funcs = list(bv.functions)[11:-1]
... the_func = None
... for func in funcs:
...     if len(func.callers) == 0:
...         print("Start:", func)
... 
Start: int64_t sub_5e347b()

Ok, this function stands out. It is the only function which is not being called, with no arguements. Let’s look at the decompilation.

>>> func = bv.get_function_at(0x5e347b)
... print(func.hlil)
... 
int32_t rdi
int32_t var_c = rdi
int32_t rsi
int32_t var_10 = rsi
sub_469bc9(0x4e, 7)
sub_469bc9(0x46, 7)
sub_390418(0x57, 0x13)
sub_469bc9(0x3c, 7)
sub_751551(0x54, 1)
sub_792291(0x40, 6)
sub_154813(0x7b, 0)
sub_469bc9(0x61, 7)
sub_469bc9(0x2c, 7)
sub_390418(0x85, 0x13)
sub_390418(0x46, 0x13)
sub_154813(0x35, 0)
sub_154813(0x5f, 0)
sub_469bc9(0x66, 7)
sub_d31fe(0x790000, 0x10)
sub_154813(0x5f, 0)
sub_390418(0x81, 0x13)
sub_154813(0x75, 0)
sub_751551(0x6d, 1)
sub_154813(0x62, 0)
sub_792291(0x35, 6)
sub_792291(0x74, 6)
sub_d31fe(0x5f000000, 0x18)
sub_390418(0x48, 0x13)
sub_792291(0x16, 0x26)
sub_469bc9(0x58, 7)
sub_469bc9(0x5c, 7)
sub_d31fe(0x34000, 0xc)
sub_469bc9(0x2a, 7)
sub_469bc9(0x2a, 7)
sub_751551(0x5f, 1)
sub_792291(0x6a, 7)
sub_154813(0x33, 0)
sub_d31fe(0x5f10000, 0x14)
sub_792291(0x2b, 0x46)
sub_154813(0x34, 0)
sub_d31fe(0x79000000, 0x18)
sub_751551(0x62, 1)
sub_d31fe(0x3304481, 0x14)
sub_469bc9(0x76, 7)
return 0

Remember those return values in those functions? Let’s see what sub_469bc9(0x4e, 7) returns

>>> func = bv.get_function_at(0x5e347b)
... func.callees[0].hlil[-1]
... 
... 
<HLIL_RET: return zx.q(arg2 + arg1)>
>>> chr(0x4e + 7)
'U'

This is likely the flag! Trying out the next few letters gives UMDCTF{ the flag prefix.

What we can do now is loop through all the instructions and calculate the return values

>>> flag_func = bv.get_function_at(0x5e347b)
... 
... ans = []
... for instr in list(list(flag_func.hlil)[0]):
...     if instr.operation != highlevelil.HighLevelILOperation.HLIL_CALL:
...         continue
...     args = instr.params
...     a, b = int(args[0].value), int(args[1].value)
...     func = int(instr.dest.value)
...     op = str(bv.get_function_at(func).hlil[-1])
...     if '+' in op:
...         ans.append(a+b)
...     elif '-' in op:
...         ans.append(a-b)
...     elif '*' in op:
...         ans.append(a*b)
...     elif '^' in op:
...         ans.append(a^b)
...     elif '<<' in op:
...         ans.append(a<<b)
...     elif '>>' in op:
...         ans.append(a>>b)
...     else:
...         print(op)
... 
... print(''.join(list(map(chr, ans))))
... 
UMDCTF{h3r35_my_numb3r_50_c411_m3_m4yb3}

Flag: UMDCTF{h3r35_my_numb3r_50_c411_m3_m4yb3}