//Freeda — Native Hook (hard)
Challenge summary
-
Category: Android / Native (C/C++) + Java (hard)
-
Goal: find the password to open the vault (flag format:
^Hero{\S+}$). -
Files provided:
app-ctf.apk(decompiled asapktool_out_ctf/).
Short summary of approach
-
The important logic is moved into a native library
libv3.soloaded byNativeGate. -
I extracted
libv3.so, listed its symbols and discovered exported functionsget_flagandcheck_root. -
Instead of rebuilding or statically reversing the entire native function, I used symbolic/native emulation via
angr— stubbed out the root-detection and allowed the library to runget_flagand return the flag string.
High-level steps (what I did)
- Decompile APK and inspect Java wrappers:
- CheckFlag.checkFlag calls NativeGate.nCheck(...).
- NativeGate loads v3 (System.loadLibrary("v3")).
- Extract native libraries from
apktooloutput:
- apktool d -f app-ctf.apk -o apktool_out_ctf
- Native libs are in apktool_out_ctf/lib/<arch>/libv3.so.
- Inspect native symbols with
nm/objdump/strings:
- Found exported symbols: get_flag and check_root and Java_com_heroctf_freeda3_utils_NativeGate_nCheck.
- Either run the library (hard locally due to Android bionic dependencies) or emulate it.
- Running the shared object on a desktop directly usually fails because the library expects Android bionic environment.
- I used angr to emulate execution of get_flag and stub the check_root function to always return success.
Relevant commands I used
apktool d -f app-ctf.apk -o apktool_out_ctf
nm -D apktool_out_ctf/lib/x86_64/libv3.so
objdump -d apktool_out_ctf/lib/x86_64/libv3.so > libv3_x86_64.txt
strings apktool_out_ctf/lib/x86_64/libv3.so | grep -i get_flag
readelf -d apktool_out_ctf/lib/x86_64/libv3.so
Key observations from libv3.so
-
get_flagis present and returns a pointer to an ASCII string in.rodata. -
check_rootis a helper that detects rooting;get_flagcalls it and will likely bail if root is detected. We can either patch/stub it or emulate and return safe values. -
Running
libv3.sodirectly on x86_64 Linux requires either Android compatibility or robust shimming of bionic; easier is to emulate.
Angr-based solution (how I executed it)
- I used
angrto loadlibv3.soand call itsget_flaglanding address. To make execution succeed I:
- Hooked/stubbed check_root to return "non-root".
- Hooked __stack_chk_fail to avoid aborting the analysis.
- Created a call-state at get_flag's address and executed until dead-ended states returned a pointer to the computed flag.
Angr script (used locally)
# solver_native_hook_angr.py
import angr, claripy
class CheckRoot(angr.SimProcedure):
# emulate check_root(context) -> non-zero (not rooted)
def run(self):
return claripy.BVV(1, self.state.arch.bits)
class StackChkFail(angr.SimProcedure):
# if the binary would call stack_chk_fail, abort emulation cleanly
def run(self):
raise Exception("__stack_chk_fail called")
proj = angr.Project('apktool_out_ctf/lib/x86_64/libv3.so', auto_load_libs=False)
proj.hook_symbol('check_root', CheckRoot())
proj.hook_symbol('__stack_chk_fail', StackChkFail())
# find rebased address of get_flag
addr = proj.loader.find_symbol('get_flag').rebased_addr
state = proj.factory.call_state(addr)
simgr = proj.factory.simgr(state)
# run until something dead-ends
simgr.run()
# The returned pointer will be in rax on x86_64. For each deadended state, read string
for dead in simgr.deadended:
retval = dead.solver.eval(dead.regs.rax)
flag_bytes = dead.mem[retval].string.concrete
print(flag_bytes)
What I got (local success)
- The angr run returned the flag string in memory:
Hero{F1NAL_57EP_Y0U_KN0W_H0W_TO_R3V3R53_4NDR01D}
Alternative runtime (Frida) method (optional)
- If the app is installed on a device/emulator you can either:
- Hook Java_com_heroctf_freeda3_utils_NativeGate_nCheck with Frida and call get_flag from the native library using Module.findExportByName('libv3.so', 'get_flag') and new NativeFunction(...) to call it and read the returned pointer.
- Or hook get_flag directly with Frida (if exported) and intercept the returned pointer to read the string from process memory.
Notes / pitfalls
-
Running Android native binaries on Linux directly with
ctypesusually fails because they depend on bionic (liblog, libm from bionic) and symbol versions differ; I wrote a small shim approach earlier but it is fragile. -
angremulation is robust since it does not require executing system calls; hooking sensitive functions lets us avoid environment differences.
Final note
- I used a hybrid of static inspection (to locate
get_flagand confirmcheck_root) and symbolic/native emulation (angr) to recover the flag without having to manually reverse the whole native function.