题目背景

题目要求输入n1ctf{flag}格式的字符串,点击check按钮进行验证,是典型的Android逆向题目。
初始分析:DEX反编译
使用jadx-gui分析APK主逻辑:
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
public native String enc(String str);
public native String stringFromJNI();
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
ActivityMainBinding inflate = ActivityMainBinding.inflate(getLayoutInflater());
this.binding = inflate;
setContentView(inflate.getRoot());
this.binding.CheckButton.setOnClickListener(new View.OnClickListener() {
@Override
public final void onClick(View view) {
MainActivity.this.m157lambda$onCreate$0$comn1ctf2024ezapkMainActivity(view);
}
});
}
public void m157lambda$onCreate$0$comn1ctf2024ezapkMainActivity(View view) {
String obj = this.binding.flagText.getText().toString();
if (obj.startsWith("n1ctf{") && obj.endsWith("}")) {
if (enc(obj.substring(6, obj.length() - 1)).equals("iRrL63tve+H72wjr/HHiwlVu5RZU9XDcI7A=")) {
Toast.makeText(this, "Congratulations!", 1).show();
return;
} else {
Toast.makeText(this, "Try again.", 0).show();
return;
}
}
Toast.makeText(this, "Try again.", 0).show();
}
static {
System.loadLibrary("native2");
System.loadLibrary("native1");
}
}
核心加密逻辑位于enc函数中,该函数通过JNI调用两个native库:libnative1.so和libnative2.so。
JNI机制分析
IDA打开libnative.so会发现大量指针数组调用,这是典型的JNI调用模式。
JNI调用机制说明:
- Native代码访问Java VM特性需要通过JNI函数
- JNI interface pointer是native函数的第一个参数
- JNI interface pointer是指向指针的指针,包含JNI函数地址数组
JNI函数调用示例:
jdouble Java_pkg_Cls_f__ILjava_lang_String_2(
JNIEnv *env, // interface pointer
jobject obj, // "this" pointer
jint i, // argument #1
jstring s) // argument #2
{
const char *str = (*env)->GetStringUTFChars(env, s, 0);
// process the string
(*env)->ReleaseStringUTFChars(env, s, str);
return ...;
}
Frida动态Hook分析
定位enc函数
Hook enc函数并监控JNI调用:
Java.perform(() => {
const MainActivity = Java.use("com.n1ctf2024.ezapk.MainActivity");
MainActivity.enc.implementation = function(input) {
console.log("enc called with input:", input);
const result = this.enc(input);
startHook();
console.log("enc returned:", result);
return result;
};
});
function startHook(){
const lib_art = Process.findModuleByName('libart.so');
const symbols = lib_art.enumerateSymbols();
for (let symbol of symbols) {
var name = symbol.name;
if (name.indexOf("art") >= 0) {
if ((name.indexOf("CheckJNI") == -1) && (name.indexOf("JNI") >= 0)) {
if (name.indexOf("GetStringUTFChars") >= 0) {
console.log('start hook', symbol.name);
Interceptor.attach(symbol.address, {
onEnter: function (arg) {
console.log('GetStringUTFChars called from:\n' +
Thread.backtrace(this.context, Backtracer.ACCURATE)
.map(DebugSymbol.fromAddress).join('\n') + '\n');
},
onLeave: function (retval) {
console.log('onLeave GetStringUTFChars:', ptr(retval).readCString())
}
})
}
}
}
}
}
运行结果确认sub_1b148为enc函数。
函数调用追踪
Hook所有native函数调用:
Java.perform(() => {
const MainActivity = Java.use("com.n1ctf2024.ezapk.MainActivity");
MainActivity.enc.implementation = function(input) {
console.log("enc called with input:", input);
const result = this.enc(input);
startHooklib();
console.log("enc returned:", result);
return result;
};
});
function startHooklib(){
var functions_lib1 = Module.enumerateExports("libnative1.so");
functions_lib1 = []
var functions_lib2 = Module.enumerateExports("libnative2.so");
functions_lib1 = functions_lib1.map(item => {
return { ...item, module: "libnative1.so" };
})
functions_lib2 = functions_lib2.map(item => {
return { ...item, module: "libnative2.so" };
})
var functions = [...functions_lib1,...functions_lib2];
functions.forEach(function(func) {
var moduleBase_lib1 = Module.findBaseAddress(func.module);
var moduleBase_lib2 = Module.findBaseAddress(func.module);
if ( moduleBase_lib1 && moduleBase_lib2) {
var address = func.address
Interceptor.attach(address, {
onEnter: function(args) {
console.log(func.module + " function called at " + func.address + " " + func.name);
},
onLeave: function(retval) {
console.log(func.module + " function returned at "+ func.address + " " + func.name);
}
});
}
});
}
调用顺序分析结果为:EOR → RC4 → Base64加密流程。
随机数函数劫持分析
libnative2.so中的加密函数使用rand()生成密钥:
_BYTE *__fastcall iusp9aVAyoMI(__int64 a1, size_t a2)
{
size_t i;
_BYTE *v4;
v4 = malloc(a2);
__memcpy_chk(v4, a1, a2, -1LL);
for ( i = 0LL; i < a2; ++i )
v4[i] ^= rand();
return v4;
}
_BYTE *__fastcall SZ3pMtlDTA7Q(__int64 a1, int a2)
{
// RC4密钥生成
for ( i = 0; i < 16; ++i )
*((_BYTE *)v20 + i) = rand();
}
libnative2.so的.init.array初始化随机种子:
void init(){
srand(0x134DAD5u);
}
但实际解密失败,因为rand函数被libnative1.so劫持替换为恒定返回233的函数sub_1B140。
动态分析方案
方案一:Cheat Engine + Frida Hook
环境配置要点:
- CE server版本匹配问题
- ADB端口转发:
adb forward tcp:52736 tcp:52736
Hook dlopen监控so加载时机:
Interceptor.attach(Module.findExportByName('libc.so', 'android_dlopen_ext'), {
onEnter: function(args) {
var libraryPath = Memory.readUtf8String(args[0]);
console.log('android_dlopen_ext called to load library: ' + libraryPath);
if (libraryPath.indexOf('native1.so') !== -1) {
console.log('Pausing for 10 seconds before loading native1.so...');
var sleep_string = Module.findExportByName('libc.so', 'sleep');
var sleep_address = parseInt(sleep_string, 16);
new NativeFunction(ptr(sleep_address), 'void', ['int'])(20);
}
},
onLeave: function(retval) {
console.log('android_dlopen_ext returned: ' + retval);
}
});

方案二:Frida Hook mprotect(推荐)
监控内存保护修改:
function hook_mprotect(){
var module = Process.findModuleByName('libnative2.so');
console.log('libnative2.so loaded at: ' + module.base);
Interceptor.attach(Module.findExportByName("libc.so", 'mprotect'), {
onEnter: function(args) {
this.addr = args[0];
this.len = args[1];
this.prot = args[2];
console.log('mprotect called');
console.log('Address: ' + this.addr,'Length: ' + this.len + 'Protection: ' + this.prot);
}
});
}
运行结果显示mprotect修改了0x6d55c3c3f8地址,偏移0x43f8对应rand_ptr位置。

技术总结
本次分析涉及的关键技术点:
- DEX静态反编译分析
- JNI机制理解与动态Hook
- Frida多场景应用
- 函数劫持与GOT表修改检测
- 加密算法识别(EOR、RC4、Base64)
通过综合运用静态分析和动态调试技术,成功解析了加密流程并定位了关键函数劫持点。
标签: Android逆向,Frida,JNI,hook,CE,libnative,加密分析