Android渗透测试从零开始⑧:调用Native

admin 2024年4月22日07:20:39评论13 views字数 10225阅读34分5秒阅读模式

这篇是原理类的,后面会有一篇对应的实战。周五了,建议提肛摸鱼下班,有什么事下周再说吧^ ^


1 相关知识

1.1 Native方法

native方法,实际上就是在Java中调用C++的底层方法,实际方法的实现在APP的so文件中,作为二进制存储。如果要举一个贴切的例子,我们可以看C#中调用dll函数的范例(它们的原理一样):

using System;using System.Runtime.InteropServices;
class Program{ [DllImport("CppLibrary.dll")] public static extern void HelloWorld();
static void Main() { HelloWorld(); }}

在Java中则大可能是这样:

public class Aes {    public static native String aesDecrypt(String str);
public static native String aesDecrypt1(String str);
public static native String aesDecrypt3(String str);
public static native String aesDecryptKey(String str, String str2);
public static native String aesEncrypt(String str);
public static native String aesEncrypt2(String str);
public static native String aesEncryptKey(String str, String str2);
private static native void native_signature(Context context);
public Aes() { }
public static void signature(Context context) { native_signature(context); }
static { System.loadLibrary("aes"); // Catch: UnsatisfiedLinkError -> L6 return; L6: e = move-exception; PrintStream printStream = System.out; printStream.println("loadLibrary ses: " + e.getMessage()); }}

总而言之,只要函数只有声明,且声明中带有native关键词,我们就可以认为它的函数体在so文件内,也就是它是一个native函数。

2 Native方法溯源

2.1 寻找so文件

我们知道native函数的函数体在so文件内,而对应的so文件名则是lib[xxx].so。这个xxx取决于System.loadLibrary传入的值。例如如下情况:

Android渗透测试从零开始⑧:调用Native

则对应的so文件为libaes.so。

2.2 寻找静态Native函数体

在静态注册的情况下,native方法名与so文件中的函数名存在一一对应的关系,如Java包com.a中的B类中的函数c,在so文件中的名字则为Java_com_a_B_c。

由于总会带关键词Java,我们可以在IDA函数列表中先搜索单词Java以判断当前方法是否为静态注册。

Android渗透测试从零开始⑧:调用Native

不开IDA的情况下也可以搜导出表。

如果搜索中没有函数命中,则代表native函数使用动态绑定。

2.3 寻找动态Native函数体

2.3.1 静态溯源法

在未经混淆的情况下我们可以搜索native函数的原名,然后根据出现位置进行查找,也可以直接读JNI_Onload。以下是一个例子:

首先,已知native函数名为aesEncrypt2,则直接搜索text:

Android渗透测试从零开始⑧:调用Native

有两个结果,一个只读数据段一个数据段,都可以看看。

rodata段没有什么好看的,就是一些杂乱的数据:

Android渗透测试从零开始⑧:调用Native

data段,可以看到这里挨着函数名定义了函数的跳转位置,为0x3745C+1,即0x3745D(这几个数据位置没有必然性,但是实战时可以推断猜测一下)。

Android渗透测试从零开始⑧:调用Native

2.3.2 frida挂钩法

我们使用一个frida脚本打印出所有被注册的native函数位置(需要调用此native函数才能打印出位置)。

注入frida后作出能调用native函数的操作(这个前期应该都定位过,这里就不赘述了):

frida -H 192.168.3.181:65000 --no-pause -f com.nursinghome.monitor -l F:TTTTN8APP_MiniProgramtelefonofridanative.js

Android渗透测试从零开始⑧:调用Native

如果使用frida继续Hook,我们需要拿到的则是module base(模块基地址)和offset(偏移量)(请参考dll分析)。

也就是:

Android渗透测试从零开始⑧:调用Native

如果我们要在IDA中定位函数,则只需要关注offset。获取offset后,在IDA的Jump->Jump to address填入偏移量跳转到函数的位置:

Android渗透测试从零开始⑧:调用Native

Android渗透测试从零开始⑧:调用Native

可以看到直接跳转到了一个sub的开头:

Android渗透测试从零开始⑧:调用Native

按下F5反编译:

Android渗透测试从零开始⑧:调用Native

可以看到此函数对应的参数有三个。由于IDA无法识别函数的类型,所以这三个参数类型都为int,实际上应该是一个JNIEnv*和两个未知类型。

3 使用IDA调试so文件

IDA调试和frida一样,需要在手机端另外安装一个server并转发端口。

由于笔者使用了lamda,就直接使用lamda脚本打开IDA-server,如果自行使用server只要push到手机内存并运行即可。

启动IDA-server的lamda脚本:

#启动IDAdebug = d.stub("Debug")# 启动 IDA 32 服务端(端口可自定义)64位后面接一个64就行debug.start_ida64(port=23946)

然后使用adb命令转发端口:

adb forward tcp:23946 tcp:23946


先切到Debugger->select debugger,开始编辑调试选项:

Android渗透测试从零开始⑧:调用Native

这里选择Remote ARM Linux/Android debugger(远程ARM Linux或安卓调试器)

Android渗透测试从零开始⑧:调用Native

在这里配置一下对应的IP端口,默认为虚拟机IP 和 23946

Android渗透测试从零开始⑧:调用Native

然后Cancel,选择Debugger->Attach to process:

Android渗透测试从零开始⑧:调用Native

出现运行的进程列表,即连接成功。选择对应的运行中APK包名即可开始调试。

Android渗透测试从零开始⑧:调用Native

由于运行进程数量偏多,我们可以使用frida查找APK运行对应的pid,然后快速选择进程,frida-ps -U一下然后接一个管道findstr即可:

Android渗透测试从零开始⑧:调用Native

4 使用frida Hook native方法

其实跟hook Java方法的步骤差不多,只不过寻找函数的方式不同,需要获取native函数的指针来引用。

如果函数有导出,可以使用Process.getModuleByName("lib名").enumerateSymbols()枚举所有Symbols然后通过函数导出名,然后通过寻找导出名对应的偏移量做指针。

var ishook_libart = false;
function hook_libart() { if (ishook_libart === true) { return; } var symbols = Module.enumerateSymbolsSync("libart.so"); var addrGetStringUTFChars = null; var addrNewStringUTF = null; var addrFindClass = null; var addrGetMethodID = null; var addrGetStaticMethodID = null; var addrGetFieldID = null; var addrGetStaticFieldID = null; var addrRegisterNatives = null; var addrAllocObject = null; var addrCallObjectMethod = null; var addrGetObjectClass = null; var addrReleaseStringUTFChars = null; for (var i = 0; i < symbols.length; i++) { var symbol = symbols[i]; if (symbol.name == "_ZN3art3JNI17GetStringUTFCharsEP7_JNIEnvP8_jstringPh") { addrGetStringUTFChars = symbol.address; console.log("GetStringUTFChars is at ", symbol.address, symbol.name); } else if (symbol.name == "_ZN3art3JNI12NewStringUTFEP7_JNIEnvPKc") { addrNewStringUTF = symbol.address; console.log("NewStringUTF is at ", symbol.address, symbol.name); } else if (symbol.name == "_ZN3art3JNI9FindClassEP7_JNIEnvPKc") { addrFindClass = symbol.address; console.log("FindClass is at ", symbol.address, symbol.name); } else if (symbol.name == "_ZN3art3JNI11GetMethodIDEP7_JNIEnvP7_jclassPKcS6_") { addrGetMethodID = symbol.address; console.log("GetMethodID is at ", symbol.address, symbol.name); } else if (symbol.name == "_ZN3art3JNI17GetStaticMethodIDEP7_JNIEnvP7_jclassPKcS6_") { addrGetStaticMethodID = symbol.address; console.log("GetStaticMethodID is at ", symbol.address, symbol.name); } else if (symbol.name == "_ZN3art3JNI10GetFieldIDEP7_JNIEnvP7_jclassPKcS6_") { addrGetFieldID = symbol.address; console.log("GetFieldID is at ", symbol.address, symbol.name); } else if (symbol.name == "_ZN3art3JNI16GetStaticFieldIDEP7_JNIEnvP7_jclassPKcS6_") { addrGetStaticFieldID = symbol.address; console.log("GetStaticFieldID is at ", symbol.address, symbol.name); } else if (symbol.name == "_ZN3art3JNI15RegisterNativesEP7_JNIEnvP7_jclassPK15JNINativeMethodi") { addrRegisterNatives = symbol.address; console.log("RegisterNatives is at ", symbol.address, symbol.name); } else if (symbol.name.indexOf("_ZN3art3JNI11AllocObjectEP7_JNIEnvP7_jclass") >= 0) { addrAllocObject = symbol.address; console.log("AllocObject is at ", symbol.address, symbol.name); } else if (symbol.name.indexOf("_ZN3art3JNI16CallObjectMethodEP7_JNIEnvP8_jobjectP10_jmethodIDz") >= 0) { addrCallObjectMethod = symbol.address; console.log("CallObjectMethod is at ", symbol.address, symbol.name); } else if (symbol.name.indexOf("_ZN3art3JNI14GetObjectClassEP7_JNIEnvP8_jobject") >= 0) { addrGetObjectClass = symbol.address; console.log("GetObjectClass is at ", symbol.address, symbol.name); } else if (symbol.name.indexOf("_ZN3art3JNI21ReleaseStringUTFCharsEP7_JNIEnvP8_jstringPKc") >= 0) { addrReleaseStringUTFChars = symbol.address; console.log("ReleaseStringUTFChars is at ", symbol.address, symbol.name); } }
if (addrRegisterNatives != null) { Interceptor.attach(addrRegisterNatives, { onEnter: function (args) { console.log("[RegisterNatives] method_count:", args[3]); var env = args[0]; var java_class = args[1]; var funcAllocObject = new NativeFunction(addrAllocObject, "pointer", ["pointer", "pointer"]); var funcGetMethodID = new NativeFunction(addrGetMethodID, "pointer", ["pointer", "pointer", "pointer", "pointer"]); var funcCallObjectMethod = new NativeFunction(addrCallObjectMethod, "pointer", ["pointer", "pointer", "pointer"]); var funcGetObjectClass = new NativeFunction(addrGetObjectClass, "pointer", ["pointer", "pointer"]); var funcGetStringUTFChars = new NativeFunction(addrGetStringUTFChars, "pointer", ["pointer", "pointer", "pointer"]); var funcReleaseStringUTFChars = new NativeFunction(addrReleaseStringUTFChars, "void", ["pointer", "pointer", "pointer"]);
var clz_obj = funcAllocObject(env, java_class); var mid_getClass = funcGetMethodID(env, java_class, Memory.allocUtf8String("getClass"), Memory.allocUtf8String("()Ljava/lang/Class;")); var clz_obj2 = funcCallObjectMethod(env, clz_obj, mid_getClass); var cls = funcGetObjectClass(env, clz_obj2); var mid_getName = funcGetMethodID(env, cls, Memory.allocUtf8String("getName"), Memory.allocUtf8String("()Ljava/lang/String;")); var name_jstring = funcCallObjectMethod(env, clz_obj2, mid_getName); var name_pchar = funcGetStringUTFChars(env, name_jstring, ptr(0)); var class_name = ptr(name_pchar).readCString(); funcReleaseStringUTFChars(env, name_jstring, name_pchar);
//console.log(class_name);
var methods_ptr = ptr(args[2]);
var method_count = parseInt(args[3]); for (var i = 0; i < method_count; i++) { var name_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3)); var sig_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize)); var fnPtr_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize * 2));
var name = Memory.readCString(name_ptr); var sig = Memory.readCString(sig_ptr); var find_module = Process.findModuleByAddress(fnPtr_ptr); console.log("[RegisterNatives] java_class:", class_name, "name:", name, "sig:", sig, "fnPtr:", fnPtr_ptr, "module_name:", find_module.name, "module_base:", find_module.base, "offset:", ptr(fnPtr_ptr).sub(find_module.base));
} }, onLeave: function (retval) { } }); }
ishook_libart = true;}
hook_libart();

使用脚本跑出各个Symbols的偏移量,获取输出里的相对偏移量,然后在脚本中获取模块偏移量(因为模块偏移量会变化)加上相对偏移量,即为函数对应的指针。在frida脚本里则这样写:

var baseAdd=Module.findBaseAddress("模块so文件");var realAdd=baseAdd.add(ptr(之前获取到的相对偏移量));

指针的使用方式和普通对象相同,就像这样:

Interceptor.attach(指针,{ //这是Hook函数        onEnter:function(args){        },onLeave:function (retval) {        }    });

5 使用unidbg和frida主动调用native方法

unidbg和frida各有千秋,frida不需要补环境,unidbg可以在无apk无安卓模拟器的情况下运行。

5.1 使用unidbg调用native方法

我直接上代码了,下载下来在TTEncrypt下面改也可以(TTEncrypt这个示例会给一个日志记录,很详细),新建一个也可以。

这里的代码是新建的实例:

package com.augusttheodor.abc;
import com.github.unidbg.AndroidEmulator;import com.github.unidbg.Module;import com.github.unidbg.arm.backend.Unicorn2Factory;import com.github.unidbg.linux.android.AndroidEmulatorBuilder;import com.github.unidbg.linux.android.AndroidResolver;import com.github.unidbg.linux.android.dvm.DalvikModule;import com.github.unidbg.linux.android.dvm.DvmClass;import com.github.unidbg.linux.android.dvm.StringObject;import com.github.unidbg.linux.android.dvm.VM;import com.github.unidbg.memory.Memory;import com.github.unidbg.virtualmodule.android.AndroidModule;
import java.io.File;import java.util.ArrayList;import java.util.List;
public class abcMain {
public static void main(String[] args) { AndroidEmulator emulator = AndroidEmulatorBuilder.for32Bit() .setProcessName("进程名,随意设置或保持你要模拟的APK进程名") .addBackendFactory(new Unicorn2Factory(true)) .build(); // 创建模拟器实例,要模拟32位或者64位,在这里区分 final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口 memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析 VM vm = emulator.createDalvikVM(new File("此处可删除,或设置为要模拟的APK路径")); // 创建Android虚拟机 new AndroidModule(emulator,vm).register(memory); DalvikModule dm = vm.loadLibrary(new File("so文件路径"), false); // 加载libttEncrypt.so到unicorn虚拟内存,加载成功以后会默认调用init_array等函数 dm.callJNI_OnLoad(emulator); // 手动执行JNI_OnLoad函数 Module module = dm.getModule(); DvmClass dvc = vm.resolveClass("要加载的方法对应的类名"); List<Object> argss = new ArrayList<>(10); //加载参数,为JNIEnv*,args... argss.add(vm.getJNIEnv()); argss.add(vm.addLocalObject(new StringObject(vm,""))); argss.add(vm.addLocalObject(new StringObject(vm,"Aa123456"))); Number result=module.callFunction(emulator,0x3745d,argss.toArray()); //我感觉这个是指针 //返回执行结果的指针 System.out.println(vm.getObject(result.intValue()).getValue().toString()); //这里将指针转回String然后打印 }
}

5.2 使用frida调用native方法

使用frida调用native方法则简便许多,不需要做初始的进程生成之类的。但与此相对的则需要按照参数类型声明一个NativeFunction。以下是frida的脚本范例:

var env = Java.vm.getEnv(); //获取环境var baseAdd=Module.findBaseAddress("so文件名");var realAdd=baseAdd.add(ptr(之前获取到的偏移量));var enc_native=new NativeFunction(realAdd,"pointer"(返回值,一般都是指针),["pointer"(JNIEnv*),参数表...]);console.log(enc_native(a3,env.newStringUtf(""),env.newStringUtf(""))); 然后直接调用


原文始发于微信公众号(重生之成为赛博女保安):Android渗透测试从零开始⑧:调用Native

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年4月22日07:20:39
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Android渗透测试从零开始⑧:调用Nativehttps://cn-sec.com/archives/2576952.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息