专栏名称: 看雪学苑
致力于移动与安全研究的开发者社区,看雪学院(kanxue.com)官方微信公众帐号。
目录
相关文章推荐
嘶吼专业版  ·  RomCom 黑客利用 Firefox 和 ... ·  2 天前  
蚁景网络安全  ·  Flutter框架APP绕过SSL验证 ·  2 天前  
蚁景网络安全  ·  Flutter框架APP绕过SSL验证 ·  2 天前  
安天集团  ·  明日直播丨软件供应链安全没那么难了? ·  2 天前  
太星小升初  ·  心仪哪所?西城初中校热度Top10来了 ·  6 天前  
51好读  ›  专栏  ›  看雪学苑

N1CTF-ezapk 解题思路

看雪学苑  · 公众号  · 互联网安全  · 2024-11-28 17:59

正文

本篇文章总下我的心路历程吧,包括但不限于:


1.ezapk的解体思路

2.环境问题搭建tips

3.完整的脚本


总之,完全是新手向的文章,遇到的坑,一步步怎么做,我都会说清楚,即使你是新手也没关系。这也是我第一次真真切切深入安卓逆向,之前只是静态反编译解决,这次学习了frida,CE(cheat engine),安卓模拟器等工具使用,写篇文章算是自己的一个阶段性总结。


看完这篇文章你将学到:


◆安卓逆向基本思路

  • dex反编译

  • frida hook

  • CE 找出是谁在暗中修改函数


◆深入JNI机制

◆frida 环境搭建以及基本使用

◆CE 使用 以及 CE server 构建


题目背景


题目非常简单,输入n1ctf{flag}, 点击check检查,很正规的安卓题


开门见山-dex反编译

放入jadx-gui查看一下主逻辑:


public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;

public native String enc(String str);

public native String stringFromJNI();

/* JADX INFO: Access modifiers changed from: protected */
@Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
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() { // from class: com.n1ctf2024.ezapk.MainActivity$$ExternalSyntheticLambda0
@Override // android.view.View.OnClickListener
public final void onClick(View view) {
MainActivity.this.m157lambda$onCreate$0$comn1ctf2024ezapkMainActivity(view);
}
});
}

/* JADX INFO: Access modifiers changed from: package-private */
/* renamed from: lambda$onCreate$0$com-n1ctf2024-ezapk-MainActivity, reason: not valid java name */
public /* synthetic */ 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中,enc 属于native 函数,通过JNI调用,enc位于通过System.loadLibrary()的两个so中。

迷雾重重-JNI逆向

ida 打开libnative.so反编译会发现有大量的类似指针数组的调用,其实这是JNI调用

关于JNI调用可以看:


[https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/jniTOC.html]


(https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/jniTOC.html)


native code 想要访问java VM的特性就需要调用JNI函数,调用JNI函数需要JNI interface pointer


并且JNI interface pointer是native函数的第一个参数,如下:


package pkg;  

class Cls {

native double f(int i, String s);

...

}


这里 double 会经过名称混淆变为Java_pkg_Cls_f_ILjava_lang_String_2


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 */
{
/* Obtain a C-copy of the Java string */
const char *str = (*env)->GetStringUTFChars(env, s, 0);

/* process the string */
...

/* Now we are done with str */
(*env)->ReleaseStringUTFChars(env, s, str);

return ...
}


JNI interface pointer是一个pointer to pointer,具体来说就是一个指针数组,这个数组保存着JNI函数的地址,包括:


const struct JNINativeInterface ... = {

NULL,
NULL,
NULL,
NULL,
GetVersion,

DefineClass,
//... 太长省略

GetJavaVM,

GetStringRegion,
GetStringUTFRegion,
//...
GetObjectRefType
};


但是ida中并没有JNIEnv等等结构体,一个个倒入自动识别太麻烦,手动计算又太蠢

该怎么办呢?


拨云见日-frida hook

定位enc

其实真正常用的JNI 函数就那几个,可以看到enc中传入了字符串,所以native函数想要获取这个字符串,会调用关于String的JNI调用 一般为GetStringUTFChars


关于环境:我的pc是mac m1,手头也没有安卓设备,最后选择mu mu pro模拟器(啥都好,就是要花钱)


在mu mu pro模拟器中安装好 frida server后,运行frida server后就可以hook了


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();
// startHooklib();
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())
}
})
}
}
}
}
}


运行结果


// frida -U -f com.n1ctf2024.ezapk -l hook.js
GetStringUTFChars called from:
0x6d55c7117c libnative1.so!0x1b17c
//没有 多点几次 hook和输出在一起 所有你需要hook了 再点几次


这里就知道sub_1b148是enc了

stacktrace

接下来,定位enc调用了哪些函数,还是hook


ava.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();
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];

// {
// "address": "0x6d56602ca8",
// "name": "aE7KMLpKuUbB",
// "type": "function"
// }


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
// console.log("Attaching to function at " + func.module + "!" + func.addr);
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);
}
});
} else {
console.log("Module " + func.module + " not found!");
}
});
}


运行结果


libnative2.so function called at 0x6d55c0306c iusp9aVAyoMI
libnative2.so function returned at 0x6d55c0306c iusp9aVAyoMI
libnative2.so function called at 0x6d55c032c0 SZ3pMtlDTA7Q
libnative2.so function returned at 0x6d55c032c0 SZ3pMtlDTA7Q
libnative2.so function called at 0x6d55c03ab0 UqhYy0F049n5
libnative2.so function returned at 0x6d55c03ab0 UqhYy0F049n5


这就获取了调用顺序,在ida里看一下,一眼丁真,分别是EOR,rc4,base64


九九八十难-谁改了我的rand()

_BYTE *__fastcall iusp9aVAyoMI(__int64 a1, size_t a2)
{
size_t i; // [xsp+0h] [xbp-40h]
_BYTE *v4; // [xsp+8h] [xbp-38h]

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)
{
v20[2] = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
v16 = malloc(a2);
__memcpy_chk(v16, a1, a2, -1LL);
v20[1] = 0LL;
v20[0] = 0LL;
for ( i = 0; i < 16; ++i )
*((_BYTE *)v20 + i) = rand();
// ....
}


可以看到EOR和rc4的密钥都是rand()获取的,libnative2.so中的.init.array中有个init函数,初始化了随机种子


void init()
{
srand(0x134DAD5u);
}


真正解密会发现解密失败,实际上这里rand被修改了,如法炮制,在libnative1.so的.init.array中有三个函数


__int64 sub_1B540()
{
FILE *v0; // x20
char *v1; // x0
unsigned __int64 v2; // x19
__int64 v3; // x24
__int64 v4; // x22
__int64 v5; // x23
__int64 v6; // x25
__int64 v7; // x8
__int64 (**v8)(); // x19
__int64 result; // x0
char filename[4096]; // [xsp+8h] [xbp-1008h] BYREF
__int64 v11; // [xsp+1008h] [xbp-8h]

v11 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
sub_1B6C4(filename);
v0 = fopen(filename, "r");
if ( v0 )
{
while ( fgets(filename, 4096, v0) )
{
if ( strstr(filename, "libnative2.so") )
{
v1 = strtok(filename, "-");
v2 = strtoull(v1, 0LL, 16);
goto LABEL_6;
}
}
}
v2 = 0LL;
LABEL_6:
fclose(v0);
sub_1B000(v2, &qword_40F70);
if ( (int)(qword_40FC8 / 0x18uLL) < 1 )
{
LABEL_10:
v7 = 0LL;
}
else
{
v3 = (unsigned int)(qword_40FC8 / 0x18uLL);
v4 = qword_40F78;
v5 = unk_40F80;
v6 = qword_40FC0 + 8;
while ( strcmp((const char *)(v5 + *(unsigned int *)(v4 + 24LL * *(unsigned int *)(v6 + 4))), "rand") )
{
v6 += 24LL;
if ( !--v3 )
goto LABEL_10;
}
v7 = *(_QWORD *)(v6 - 8);
}
v8 = (__int64 (**)())(v7 + v2);
result = mprotect(v8, 8uLL, 3);
*v8 = sub_1B140;
return result;
}


这里可以很明显的是一个rand的替换操作,rand替换为了sub_1B140,这个函数恒定返回233,就是真正的密钥了。


on more thing-我的思考

如果这个修改rand got表的操作不在.init.got表中,如何找到他呢?

方案一 CE + CE server + frida hook(小坑+小坑+大坑)

tips:


官网只给了mac版本的7.5.2 CE 但是给的CE server是7.5,所以连接的时候会报错,最后只能自己编译,编译的时候ndk版本别太高,不然一堆报错(血的教训)

如果主机和模拟器不能ping通 那连接ce server时 使用127.0.0.1的话 记得 adb forwar tcp:52736 tcp:52736


要看so 在哪被修改了,CE 扫描的时机很重要,要在native2加载的时候扫描一次,然后native1加载后或者再往后的一个时机扫描改变的字节


所以要hook System.loadLibrary()


这里真是大坑了,查看github issues 才知道System.loadLibrary()是不可以hook的函数之一,因为你在Java.perfrom()里使用,但它会修改classloadrer,导致报错


所以最根本的方法就是hook dlopen 或者 android_dlopen_ext


这里我选择 hook android_dlopen_ext,在 native1加载的时候暂停一会,方便CE 扫描


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...');
// 暂停 10 秒
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 (推荐)

hook mprotect的调用,关注地址在so地址范围的地址


 hook_mprotect()
var module = Process.findModuleByName('libnative2.so');
console.log('libnative2.so loaded at: ' + module.base);

function hook_mprotect(){
// 使用 Frida 钩取 mprotect 函数
Interceptor.attach(Module.findExportByName("libc.so", 'mprotect'), {
onEnter: function(args) {
// 获取函数参数
this.addr = args[0]; // addr 参数:指向内存区域的指针
this.len = args[1]; // len 参数:内存区域的长度
this.prot = args[2]; // prot 参数:内存保护标志

// console.log(this.len.toString());

// if(this.len.toString() === '0x8'){
console.log('mprotect called');
console.log('Address: ' + this.addr,'Length: ' + this.len + 'Protection: ' + this.prot);
// }

},
onLeave: function(retval) {
// 你可以在此修改函数的返回值,或者在返回时打印一些信息
// console.log('mprotect return value: ' + retval);
}
});

}


运行结果


mprotect called
Address: 0x6d55c3c3f8 Length: 0x8Protection: 0x3


计算偏移 正好是0x43f8 也就是 rand_ptr的位置


CE 查看修改后的内容



正好是 native1 中 sub_1B140

总结

整体难度不大但是很有趣,这个过程中探索了各种工具的使用、各种环境的搭建还是学到了很多。





看雪ID:SleepAlone

https://bbs.kanxue.com/user-home-9950548.htm

*本文为看雪论坛优秀文章,由 SleepAlone 原创,转载请注明来自看雪社区


# 往期推荐

1、PWN入门-SROP拜师

2、一种apc注入型的Gamarue病毒的变种

3、野蛮fuzz:提升性能

4、关于安卓注入几种方式的讨论,开源注入模块实现

5、2024年KCTF水泊梁山-反混淆




球分享

球点赞

球在看



点击阅读原文查看更多