一些零零散散的学习笔记

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] # android heap search instances dalvik.system.DexClassLoader
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] = [];

// tslint:disable:only-arrow-functions
// tslint:disable:object-literal-shorthand
// tslint:disable:no-empty
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(),
});
},
});
// tslint:enable

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

-