Skip to content

SECURE_CONNECTION//PRESS[CTRL+J]FOR ROOT ACCESS

BACK TO INTEL
AndroidHard

Freeda — Native Hook (Hard)

CTF writeup for Freeda — Native Hook (Hard) from heroCTF

//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 as apktool_out_ctf/).

Short summary of approach

  • The important logic is moved into a native library libv3.so loaded by NativeGate.

  • I extracted libv3.so, listed its symbols and discovered exported functions get_flag and check_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 run get_flag and return the flag string.

High-level steps (what I did)

  1. Decompile APK and inspect Java wrappers:

   - CheckFlag.checkFlag calls NativeGate.nCheck(...).

   - NativeGate loads v3 (System.loadLibrary("v3")).

  1. Extract native libraries from apktool output:

   - apktool d -f app-ctf.apk -o apktool_out_ctf

   - Native libs are in apktool_out_ctf/lib/<arch>/libv3.so.

  1. Inspect native symbols with nm/objdump/strings:

   - Found exported symbols: get_flag and check_root and Java_com_heroctf_freeda3_utils_NativeGate_nCheck.

  1. 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

bash

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_flag is present and returns a pointer to an ASCII string in .rodata.

  • check_root is a helper that detects rooting; get_flag calls it and will likely bail if root is detected. We can either patch/stub it or emulate and return safe values.

  • Running libv3.so directly 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 angr to load libv3.so and call its get_flag landing 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)

python

# 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 ctypes usually 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.

  • angr emulation 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_flag and confirm check_root) and symbolic/native emulation (angr) to recover the flag without having to manually reverse the whole native function.