一些零零散散的学习笔记
Objection源码分析
由于这部分分析是因为我想要使用objection的get Instance
功能,我希望可以查找到目前heap中所有的DexClassLoader实例,因为实例中会有dex file的路径。
先简单说一下objection的原理,objection的所有功能都是依靠一个agent.js实现的,通过将agent注入到目标进程中,使用rpc的方式调用agent中的对应函数
通过阅读objection的wiki,想要在外部调用objection的命令的话可以通过rpc的方法,但是似乎不够优雅,并且只能使用限制的api。
https://github.com/sensepost/objection/blob/master/agent/src/rpc/android.ts
以脱样本为例(针对自定义class loader样本需要进行特殊处理):
1 2 3 4 5
| com.car.cloth on (google: 10) [usb] Class instance enumeration complete for dalvik.system.DexClassLoader Hashcode Class toString() -------- ---------------------------- ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 20991800 dalvik.system.DexClassLoader dalvik.system.DexClassLoader[DexPathList[[zip file "/data/user/0/com.car.cloth/app_DynamicOptDex/fiZ.json"],nativeLibraryDirectories=[, /system/lib64, /vendor/lib64, /system/product/lib64]]]
|
换种方式,使用RPC进行调用,通过在objection中开启enable-api
参数可以进行外部api调用
1 2 3
| curl -s http://127.0.0.1:8888/rpc/invoke/androidHookingListActivities ------------------------------------------------------------- ["com.cgv.cn.movie.MainActivity","com.facebook.react.devsupport.DevSettingsActivity","com.cgv.cn.movie.wxapi.WXEntryActivity","com.cgv.cn.movie.wxapi.WXPayEntryActivity","com.tencent.tauth.AuthActivity","com.tencent.connect.common.AssistActivity","com.umeng.socialize.media.WBShareCallBackActivity","com.sina.weibo.sdk.web.WeiboSdkWebActivity","com.sina.weibo.sdk.share.WbShareTransActivity","com.unionpay.uppay.PayActivity","com.unionpay.UPPayWapActivity","com.tencent.captchasdk.TCaptchaPopupActivity","com.alipay.sdk.app.H5PayActivity","com.alipay.sdk.app.H5AuthActivity","com.alipay.sdk.app.PayResultActivity","com.alipay.sdk.app.AlipayResultActivity","com.alipay.sdk.app.H5OpenAuthActivity","com.cmic.sso.sdk.activity.LoginAuthActivity","cn.jiguang.verifysdk.CtLoginActivity","cn.jpush.android.ui.PopWinActivity","cn.jpush.android.ui.PushActivity","cn.jpush.android.service.JNotifyActivity","cn.jpush.android.service.DActivity","com.yalantis.ucrop.UCropActivity","com.google.android.gms.common.api.GoogleApiActivity"]
|
整体一览
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128
| objection-master ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── agent │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── android │ │ ├── generic │ │ ├── index.ts │ │ ├── ios │ │ ├── lib │ │ └── rpc │ ├── tsconfig.json │ └── tslint.json ├── images │ ├── android_ls.png │ ├── android_ssl_pinning_bypass.png │ ├── api.png │ ├── frida_logo.png │ ├── ios_keychain.png │ ├── ios_ls.png │ ├── ios_ssl_pinning_bypass.png │ ├── objection.png │ └── sqlite_example.png ├── objection │ ├── __init__.py │ ├── api │ │ ├── __init__.py │ │ ├── app.py │ │ ├── rpc.py │ │ └── script.py │ ├── commands │ │ ├── __init__.py │ │ ├── android │ │ ├── command_history.py │ │ ├── custom.py │ │ ├── device.py │ │ ├── filemanager.py │ │ ├── frida_commands.py │ │ ├── http.py │ │ ├── ios │ │ ├── jobs.py │ │ ├── memory.py │ │ ├── mobile_packages.py │ │ ├── plugin_manager.py │ │ ├── sqlite.py │ │ └── ui.py │ ├── console │ │ ├── __init__.py │ │ ├── cli.py │ │ ├── commands.py │ │ ├── completer.py │ │ ├── helpfiles │ │ └── repl.py │ ├── state │ │ ├── __init__.py │ │ ├── api.py │ │ ├── app.py │ │ ├── connection.py │ │ ├── device.py │ │ ├── filemanager.py │ │ └── jobs.py │ └── utils │ ├── __init__.py │ ├── agent.py │ ├── assets │ ├── helpers.py │ ├── patchers │ ├── plugin.py │ └── update_checker.py ├── plugins │ ├── README.md │ ├── api │ │ ├── __init__.py │ │ └── index.js │ ├── flex │ │ ├── README.md │ │ ├── __init__.py │ │ ├── index.js │ │ ├── libFlex.h │ │ └── libFlex.m │ ├── mettle │ │ ├── README.md │ │ ├── __init__.py │ │ └── index.js │ └── stetho │ ├── README.md │ ├── __init__.py │ └── index.js ├── requirements-dev.txt ├── requirements.txt ├── setup.py └── tests ├── __init__.py ├── commands │ ├── __init__.py │ ├── android │ ├── ios │ ├── test_command_history.py │ ├── test_device.py │ ├── test_filemanager.py │ ├── test_frida_commands.py │ ├── test_jobs.py │ ├── test_memory.py │ ├── test_mobile_packages.py │ ├── test_plugin_manager.py │ └── test_ui.py ├── console │ ├── __init__.py │ ├── test_cli.py │ ├── test_completer.py │ └── test_repl.py ├── data │ └── plugin ├── helpers.py ├── state │ ├── __init__.py │ ├── test_app.py │ └── test_jobs.py └── utils ├── __init__.py ├── patchers └── test_helpers.py
|
其中python对应的部分在objection目录下,启动入口为cli.py;agent目录下存放的是真正实现功能的frida js代码(真正使用pip 安装时下载的agent代码为编译后的,可读性交叉较差)
向上溯源分析
我们知道遍历目前堆上所有class实例的命令为 android heap search instances xxx.class
,我们可以先找到对应的command代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| def instances(args: list) -> None: """ Asks the agent to print the currently live instances of a particular class
:param args: :return: """
if len(args) < 1: click.secho('Usage: android heap search instances <class> (eg: com.example.test)', bold=True) return
target_class = args[0]
api = state_connection.get_api() instance_results = api.android_heap_get_live_class_instances(target_class)
if len(instance_results) <= 0: return
click.secho(tabulate( [[ entry['hashcode'], entry['classname'], entry['tostring'], ] for entry in instance_results], headers=['Hashcode', 'Class', 'toString()'], ))
|
最终是调用了api中的android_heap_get_live_class_instances
function,api存在于connection中,使用get api,state在cli.py中被设置参数并且初始化,具体初始化参数与启动frida的参数类似.
初始化一个objection的connection需要的是
get api函数实现如下
1 2 3 4 5 6 7 8 9 10 11
| def get_api(self): """ Return a Frida RPC API session
:return: """
if not self.agent: raise Exception('No session available to get API')
return self.agent.exports()
|
api函数返回的是agent的exports,可以理解为agent.js中导出的可用的函数,再去看一下export这个函数的本质
1 2 3 4 5 6 7 8 9 10 11
| def exports(self): """ Returns the RPC exports exposed by the Frida agent
:return: """
if not self.script: raise Exception('Need a script created before reading exports()')
return self.script.exports
|
返回的是scrit对象的export属性,再去看一下script是在哪里初始化.
1 2 3
| self.script = self.session.create_script(source=self._get_agent_source()) self.script.on('message', self.handlers.script_on_message) self.script.load()
|
script在agent的attach函数中被初始化,其中脚本路径是从_get_agent_source中获得的,其中主要的变量为agent_path,这个变量用于加载脚本文件,二次开发的话可以在这里添加脚本文件.
script、session、device这几个关键的对象都是来自于frida core,这部分和常规使用frida api一样,不做赘述.
1 2 3
| device: frida.core.Device = None session: frida.core.Session = None script: frida.core.Script = None
|
最终调用android_heap_get_live_class_instances
时也是通过rpc调用的,rpc调用时会进行函数的重命名,将函数从蛇形变为小驼峰?(应该是这样的),最终对应的export关系如下
1 2 3
| androidHeapGetLiveClassInstances: (clazz: string): Promise<IHeapObject[]> => heap.getInstances(clazz), androidHeapPrintFields: (handle: number): Promise<IJavaField[]> => heap.fields(handle), androidHeapPrintMethods: (handle: number): Promise<string[]> => heap.methods(handle),
|
因此最后实际调用的函数为js中的getInstances
函数,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| export const getInstances = (clazz: string): Promise<any[]> => { return wrapJavaPerform(() => {
handles[clazz] = [];
Java.choose(clazz, { onComplete: function () { c.log(`Class instance enumeration complete for ${c.green(clazz)}`); }, onMatch: function (instance) { handles[clazz].push({ instance: instance, hashcode: instance.hashCode(), }); }, });
return handles[clazz].map((h): IHeapNormalised => { return { hashcode: h.hashcode, classname: clazz, tostring: h.instance.toString(), }; }); }); };
|
原理也比较简单,就是通过java.choose去获取相关的类实例,并且通过rpc返回给console端.
总结
第一步我打算使用objection内存漫游的一些功能,比如说获取一些敏感的类、可疑的实例等,同时需求中的批量hook功能,我也打算使用objection实现,当然不是通过console,我打算仅使用它的agent的功能,因此理解objection项目架构与运行原理是有必要的.
后续
打算使用objection的批量hook功能,以及查找可疑类、方法、实例的功能,最后将unpack功能集成进去,同时阅读下frida core代码
Android 10下脱壳的不同点
- 首先loadDexFile之类的实现从之前的libart.so变成了libdexfile.so,这一点在源码中也可以看出来。
dex_file_loader.cc
现在位于art/libdexfile/dex
下,获取so时路径也发生了改变,现在的路径位于/apex/com.android.runtime.release/lib
以及 /apex/com.android.runtime.release/lib64
下
1 2 3 4 5 6 7 8 9
| sailfish:/system/apex/com.android.runtime.release/lib64 # ls bionic libartbase.so libdexfile.so libjavacore.so libopenjdkjvm.so libadbconnection.so libartpalette.so libdexfile_external.so libjdwp.so libopenjdkjvmti.so libandroidicu.so libbacktrace.so libdexfile_support.so liblzma.so libpac.so libandroidio.so libbase.so libdt_fd_forward.so libnativebridge.so libprofile.so libart-compiler.so libc++.so libdt_socket.so libnativehelper.so libsigchain.so libart-dexlayout.so libc_malloc_debug.so libexpat.so libnativeloader.so libunwindstack.so libart-disassembler.so libc_malloc_hooks.so libicui18n.so libnpt.so libvixl.so libart.so libcrypto.so libicuuc.so
|
- copy出来nm看一下函数的签名
1 2 3 4 5
| ┌──(kali㉿kali)-[~/Desktop] └─$ nm libdexfile.so | grep OpenCommon 0001bb51 T _ZN3art13DexFileLoader10OpenCommonEPKhjS2_jRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEjPKNS_10OatDexFileEbbPS9_NS3_10unique_ptrINS_16DexFileContainerENS3_14default_deleteISH_EEEEPNS0_12VerifyResultE 000149a9 T _ZN3art16ArtDexFileLoader10 EPKhjS2_jRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEjPKNS_10OatDexFileEbbPS9_NS3_10unique_ptrINS_16DexFileContainerENS3_14default_deleteISH_EEEEPNS_13DexFileLoader12VerifyResultE
|
- 64位32位差异化处理,需要具体分析脱壳点的函数签名,一般来说在获取不到调用参数的情况下,可以根据arm64调用规则去context中获取寄存器和内存数据,以脱壳获取dex加载的base address为例,一般来说可以获取context.x0
脱二代壳第一步–check
通过python解析dex file,并且获取dex file中的signature校验判断是否加了二代壳。
signature在magic和check sum的后面,也就是dex偏移12个字节开始,长度为20个字节,这部分还是比较好算的,现在还需要考虑的一个问题就是multi dex的情况
处理
先重构了一下代码,之前的main函数过于臃肿,重构之后分为了init_agent
获取device和agent等,现在的main函数
1 2 3 4 5 6 7 8 9 10 11
| if __name__ == "__main__": logging.basicConfig(level=logging.INFO) package_path = sys.argv[1] install_app(package_path) package_name = static_analysis(package_path) extract_dex_file( package_path , package_name) check_packaed_2() print(is_packed_with_2) agent,session,device,pid = init_agent(package_name) unpack_and_hook(agent,session,package_name,device,pid) sys.stdin.read()
|
最后把得到的所有的dex读取出来,把所有的bytes读取到一个全局的list中,最后进行signature的check。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| def extract_dex_file(filepath,package_name): apkfile = zipfile.ZipFile(filepath,'r') target_dict = './' + package_name if os.path.isdir(target_dict) == False: os.mkdir(target_dict) for tempfile in apkfile.namelist(): if tempfile.endswith('.dex'): bs = apkfile.read(tempfile) dex_bs.append(bs)
def check_packaed_2(): for dex_bytes in dex_bs: signature = dex_bytes[12:32] data_source = dex_bytes[32:] sha1 = hashlib.sha1() sha1.update(data_source) test1 =sha1.digest() if test1 != signature: logger.info('this apk seems be packed with ins_extraction') is_packaed_with_2 = True
|
使用Objection Agent进行自动搜索与hook
大体思路
可以使用objection agent中提供的功能进行批量hook,以下面三个api为例
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| com.cgv.cn.movie on (google: 10) [usb] # android hooking watch class_method android.net.wifi.WifiInfo.getMacAddress --dump-backtrace (agent) Attempting to watch class android.net.wifi.WifiInfo and method getMacAddress. (agent) Hooking android.net.wifi.WifiInfo.getMacAddress() (agent) Registering job 419365. Type: watch-method for: android.net.wifi.WifiInfo.getMacAddress com.cgv.cn.movie on (google: 10) [usb] # android hooking watch class_method android.telephony.gsm.SmsManager.sendTextMessage --dump-backtrace (agent) Attempting to watch class android.telephony.gsm.SmsManager and method sendTextMessage. (agent) Hooking android.telephony.gsm.SmsManager.sendTextMessage(java.lang.String, java.lang.String, java.lang.String, android.app.PendingIntent, android.app.PendingIntent) (agent) Registering job 426391. Type: watch-method for: android.telephony.gsm.SmsManager.sendTextMessage com.cgv.cn.movie on (google: 10) [usb] # android hooking watch class_method android.webkit.WebView.loadUrl --dump-backtrace (agent) Attempting to watch class android.webkit.WebView and method loadUrl. (agent) Hooking android.webkit.WebView.loadUrl(java.lang.String) (agent) Hooking android.webkit.WebView.loadUrl(java.lang.String, java.util.Map) (agent) Registering job 039921. Type: watch-method for: android.webkit.WebView.loadUrl
|
在objection中,hook有三个选项,--dump-args
代表返回参数,--dump-return
返回值以及--dump-backtrace
打印调用栈,通过这三项可以实现对目标方法的全方面监控,
使用
不使用objection自带的console应用,注入objection的agent,自己暴露rpc接口调用,并且打算添加一个脱壳rpc调用接口,目前实现的:安装apk,静态分析拿到axml文件(要用到里面的activity、包名信息判断是否加壳)、读取文件中的api列表并且批量hook以及初步脱壳功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
| __version__ = "2.0.1" import argparse import logging import sys import time import frida import subprocess from wallbreaker.connection import Connection from apkutils import APK from agent import HookCenter from apkutils.axml import AXMLPrinter from apkParse import Manifest import zipfile logger = logging.getLogger("hook-center")
class SessionConnection(Connection):
def __init__(self, device, session): self.device = device self.session = session self.process = str(self.session)
def _fixup_version(parser: argparse.ArgumentParser): if not hasattr(parser, "_actions"): return
for action in parser._actions: if "--version" in action.option_strings \ and action.dest == "version": action.version = __version__
def batch_hook_method(agent): f = open("api.txt") lines = f.readlines() for line in lines: agent.watch_mathods(line) f.close()
def install_app(path): p = subprocess.Popen("adb install " + path, shell=True, stdout=subprocess.PIPE) time.sleep(5) logger.info("install app") r = p.stdout.read() if 'Success' in r : logger.info("app installed successfully") else: logger.info("fail to install app") exit(1)
def apk_parser(filename): ''' Returns: Manifest(Class) ''' with zipfile.ZipFile(filename, 'r') as file: manifest = file.read('AndroidManifest.xml')
return Manifest(AXMLPrinter(manifest).get_xml())
def parse_apk_info(apkFilePath): parser = apk_parser(apkFilePath) print("package name " + parser.package_name) print("version name " + parser.version_name) print("version code " + parser.version_code) permissions = parser.permissions for permission in permissions: print(permission) return if __name__ == "__main__": package_path = sys.argv[1] axml = parse_apk_info(package_path) print("package name " + axml.package_name) print("version name " + axml.version_name) print("version code " + axml.version_code) permissions = axml.permissions for permission in permissions: print(permission)
device = frida.get_usb_device() session = device.spawn(axml.package_name) connection = SessionConnection(device,session) agent = HookCenter(connection) print(agent) result = agent.get_instance('dalvik.system.DexClassLoader') print(result)
|
运行结果
1 2 3 4
| PS C:\Users\yiren_lu\Desktop\Hook Center\hookCenter> python .\main.py HookCenter<Connection(pid=Session(pid=12666), connected:True), attached=True> Class instance enumeration complete for dalvik.system.DexClassLoader [{'hashcode': 239431238, 'classname': 'dalvik.system.DexClassLoader', 'tostring': 'dalvik.system.DexClassLoader[DexPathList[[zip file "/data/user/0/com.car.cloth/app_DynamicOptDex/fiZ.json"],nativeLibraryDirectories=[, /system/lib64, /vendor/lib64, /system/product/lib64]]]'}]
|
TODO
api文件中的不用写全api,只需要写部分后自动模糊搜索对应的method并且全部watch
-