5.24大比武

P.S.本文不涉及任何与案情相关内容 完全是以记录frida的综合应用为主 并且只截取了部分题目 侵删

Hook

题目要我们干三件事情

1.app加密用户聊天记录的数据库文件的加密方式

2.app加密用户聊天记录的数据库文件使用的私钥字符串

3.记录用户聊天记己录的数据库表名

题目提供的镜像是vmdk格式文件 我们雷电模拟器仿真起来 很显然这个数据库被加密了(题目告诉我们的) 意味着一般的数据库取证套路都失败了 我们祭出frida

frida简单来说是一个支持java和python语言的hook动态调试框架 我们首先要先让模拟器跑起来这个框架

1
adb push D:\Work\网安十百千\frida环境\frida-server-15.0.13-android-x86\frida /data/local/tmp

传输成功之后 还要赋予他777

1
chmod 777 frida

进入root之后 在tmp目录下执行该文件

1
./frida

这个框架目前就运行在了模拟器上 接下来回到咱电脑 模拟器那边的框架建立好了 我们需要在本机上运行frida.exe 建立连接 个人因为环境原因 将frida.exe 放在了python3/script里 进去之后

1
./frida.exe --version

出现版本则代表frida版本暂时没问题 接下来可以进行hook 一个通用脚本是进行hook看看他在运行的时候读取了哪些文件 这样说不定可以找到他的数据库文件

hook_open_files.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Java.perform(function () {
var openPtr = Module.findExportByName("libc.so", "open");
send("操作文件: " + openPtr);
Interceptor.attach(openPtr, {
onEnter: function(args) {
var path = Memory.readUtf8String(args[0]);
if (path.search("data") != -1) {
send("路径:" + path);
}
},
onLeave:function(retval) {
}
});
});

基本上这个脚本不用改 我们给他跑起来

1
./frida.exe -UF -l hook_open_files.js

注意 -UF的意思是hook当前正在运行的程序 所以我们事先得先把程序跑起来 然后再执行脚本 而且注意 这个.js脚本得和frida.exe放在一个文件夹里 我的环境是这样

1
2
3
4
5
message: {'type': 'send', 'payload': '路径:/data/data/cn.keke.chat/cache/w9wIwIoo.session'} data: None
message: {'type': 'send', 'payload': '路径:/data/data/cn.keke.chat/cache/MXMXMXKK.session'} data: None
message: {'type': 'send', 'payload': '路径:/data/data/cn.keke.chat/cache/w0wJwJpp.session'} data: None
message: {'type': 'send', 'payload': '路径:/data/misc/profiles/cur/0/cn.keke.chat/primary.prof'} data: None
message: {'type': 'send', 'payload': '路径:/data/data/cn.keke.chat/cache/CLClClXX.session'} data: None

我们虽然读取到了他读取的文件 但是我们发现这都是.session文件 如果我们去看一眼也会发现 这个.session是没有加密的 他只是将文本按一定格式进行了储存 那就不对了 说明很可能这个程序有多个进程 目前我们看到的聊天的这个进程他是没有读取数据库的 我们还需要另一个进程的数据 这个时候就需要多进程hook了 由于这个脚本是python的 所以我们需要开放一下27042端口才行 27042是默认的通信端口

1
adb forward tcp:27042 tcp:27042

打开了之后 我们在进行hook 我们用hook_thread_open_file.py 用的时候需要改一下包名

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
# -*- coding: utf-8 -*-
import codecs
import frida
import sys
import threading

#device = frida.get_remote_device()
device = frida.get_device_manager().enumerate_devices()[-1]
#print(device)


pending = []
sessions = []
scripts = []
event = threading.Event()

jscode = """

Java.perform(function () {
var openPtr = Module.findExportByName("libc.so", "open");
send("操作文件: " + openPtr);
Interceptor.attach(openPtr, {
onEnter: function(args) {
var path = Memory.readUtf8String(args[0]);
if (path.search("data") != -1) {
send("路径:" + path);
// send(Thread.backtrace(this.context, Backtracer.ACCURATE));
var module = Process.findModuleByAddress(this.returnAddress);
var name = module.name;

send("加载文件: " + name);
}

},
onLeave:function(retval) {
}
});
});
"""


def on_spawned(spawn):
print('on_spawned:', spawn)
pending.append(spawn)
event.set()

def spawn_added(spawn):
print('spawn_added:', spawn)
event.set()
if(spawn.identifier.startswith('cn.keke.chat')):
# if(spawn.identifier.startswith('cn.keke.chat')):
session = device.attach(spawn.pid)
script = session.create_script(jscode)
script.on('message', on_message)
script.load()
device.resume(spawn.pid)

def spawn_removed(spawn):
print('spawn_added:', spawn)
event.set()

def on_message(spawn, message, data):
print('on_message:', spawn, message, data)

def on_message(message, data):
if message['type'] == 'send':
print("[*] {0}".format(message['payload']))
else:
print(message)

device.on('spawn-added', spawn_added)
device.on('spawn-removed', spawn_removed)
device.on('child-added', on_spawned)
device.on('child-removed', on_spawned)
device.on('process-crashed', on_spawned)
device.on('output', on_spawned)
device.on('uninjected', on_spawned)
device.on('lost', on_spawned)
device.enable_spawn_gating()
event = threading.Event()
print('Enabled spawn gating')

pid = device.spawn(["cn.keke.chat"])

#pid = device.spawn(["cn.keke.chat"])

session = device.attach(pid)
print("[*] Attach Application id:",pid)
device.resume(pid)
# print("[*] Application onResume")
# script = session.create_script(jscode)
# script.on('message', on_message)
# print('[*] Running CTF')
# script.load()
sys.stdin.read()

这个脚本是python的 所以我们得在python.exe路径下运行 并且我们还需要安装frida-tools库

1
pip install firda-toolspython hook_thread_open_file.py
1
Enabled spawn gating[*] Attach Application id: 2324spawn_added: Spawn(pid=2351, identifier="cn.keke.chat:marsservice")spawn_added: Spawn(pid=2377, identifier="cn.keke.chat:pushservice")[*] 操作文件: 0xc3bdb690[*] 路径:/data/app/cn.keke.chat-1/base.apk[*] 加载文件: libandroidfw.so[*] 操作文件: 0xc3bdb690[*] 路径:/data/app/cn.keke.chat-1/base.apk[*] 加载文件: libandroidfw.so[*] 路径:/data/app/cn.keke.chat-1/lib/x86/libstlport_shared.so[*] 加载文件: libjavacore.so[*] 路径:/data/app/cn.keke.chat-1/base.apk[*] 加载文件: libopenjdkjvm.so[*] 路径:/data/app/cn.keke.chat-1/lib/x86/libmarsxlog.so[*] 加载文件: libjavacore.so[*] 路径:/data/app/cn.keke.chat-1/lib/x86/libmarsstn.so[*] 加载文件: libjavacore.so[*] 路径:/data/app/cn.keke.chat-1/lib/x86/libstlport_shared.so[*] 加载文件: libjavacore.so[*] 路径:/data/app/cn.keke.chat-1/lib/x86/libmarsxlog.so[*] 加载文件: libjavacore.so[*] 路径:/data/app/cn.keke.chat-1/lib/x86/libmarsstn.so[*] 加载文件: libjavacore.so[*] 路径:/data/user/0/cn.keke.chat/files/mipush_region.lock[*] 加载文件: libjavacore.so[*] 路径:/data/user/0/cn.keke.chat/files/mipush_region[*] 加载文件: libopenjdkjvm.so[*] 路径:/data/user/0/cn.keke.chat/shared_prefs/mipush_extra.xml[*] 加载文件: libopenjdkjvm.so[*] 路径:/data/user/0/cn.keke.chat/files/BLBwBwMM/host/ipportrecords2.xml[*] 加载文件: libc.so[*] 路径:/data/user/0/cn.keke.chat/files/BLBwBwMM/Heartbeat.ini[*] 加载文件: libc.so[*] 路径:/data/user/0/cn.keke.chat/databases/geofencing.db[*] 加载文件: libsqlite.so[*] 路径:/data/user/0/cn.keke.chat/shared_prefs/sp_client_report_status.xml[*] 加载文件: libopenjdkjvm.so[*] 路径:/data/user/0/cn.keke.chat/databases/geofencing.db-journal[*] 加载文件: libsqlite.so[*] 路径:/data/user/0/cn.keke.chat/databases/geofencing.db-journal[*] 加载文件: libsqlite.so[*] 路径:/data/user/0/cn.keke.chat/databases/geofencing.db-journal[*] 加载文件: libsqlite.so[*] 路径:/data/user/0/cn.keke.chat/databases/geofencing.db-journal[*] 加载文件: libsqlite.so[*] 路径:/data/user/0/cn.keke.chat/databases/geofencing.db-journal[*] 加载文件: libsqlite.so[*] 路径:/data/user/0/cn.keke.chat/shared_prefs/mipush_extra.xml[*] 加载文件: libopenjdkjvm.so[*] 路径:/data/user/0/cn.keke.chat/shared_prefs/cn.keke.chat_preferences.xml[*] 加载文件: libopenjdkjvm.so[*] 路径:/data/user/0/cn.keke.chat/files/BLBwBwMM/169c2404d30b18c81620889828374/data[*] 加载文件: libmarsstn.so[*] 路径:/data/misc/keychain/pubkey_blacklist.txt[*] 加载文件: libjavacore.so[*] 路径:/data/misc/keychain/serial_blacklist.txt[*] 加载文件: libjavacore.so[*] 路径:/data/misc/keychain/pins[*] 加载文件: libjavacore.so
1
[*] 路径:/data/user/0/cn.keke.chat/files/BLBwBwMM/169c2404d30b18c81620889828374/data[*] 加载文件: libmarsstn.so

经确认 这个data就是被加密的数据库了 但是我们要怎么获取私钥key呢 我们进IDA分析一下这个apk是怎么读取数据库的 我们要注意 是这个libmarsstn.so文件读取的数据库文件 我们分析的时候就先分析这个文件 不过注意是x86的 不清楚就都看一下 我们字符串搜索 查看sqlite 发现他是sqlite3的数据库 然后我们找到了一个很有价值的字符串:SQLite format 3

1
.rodata:0032363E aSqliteFormat3  db 'SQLite format 3',0  ; DATA XREF: sub_23E940+158↑o

更重要的是后面的函数 sub_23E940 我们跟进 查看伪代码 这里就需要思考了 就算这个函数就是加密的函数 那么我们要怎么解密 如果直接硬刚肯定是不现实的 咱没这个实力

1
https://www.cnblogs.com/qxxnxxFight/p/4096074.html

这个大佬的博客里面写着一些SQL加密的 可以说是源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1 void sqlite3pager_free_codecarg(void *pArg);
2 int sqlite3_key_interop(sqlite3 *db, const void *pKey, int nKeySize);
3 int sqlite3_rekey_interop(sqlite3 *db, const void *pKey, int nKeySize);
4 int sqlite3CodecAttach(sqlite3 *db, int nDb, const void *pKey, int nKeyLen);
5 int sqlite3_rekey(sqlite3 *db, const void *pKey, int nKey);
6 int sqlite3_key(sqlite3 *db, const void *pKey, int nKey);
7 void sqlite3pager_set_codec(Pager *pPager,void *(*xCodec)(void*,void*,Pgno,int),void *pCodec);
8 static LPCryptBlock CreateCryptBlock(unsigned char* hKey, Pager *pager, LPCryptBlock pExisting);
9 static unsigned char * DeriveKey(const void *pKey, int nKeyLen);
10 static void * sqlite3pager_get_codecarg(Pager *pPager);
11 static void DestroyCryptBlock(LPCryptBlock pBlock);
12 void * sqlite3Codec(void *pArg, unsigned char **data_addr, Pgno nPageNum, int nMode);
13 void sqlite3_activate_see(const char* right );
14 void sqlite3CodecGetKey(sqlite3* db, int nDB, void** Key, int* nKey);

列出的总共是14种 我们看看IDA里面的加密函数

1
char *__cdecl sub_23E940(_DWORD *a1, char *a2, int a3, int a4)

cdecl 有点像第12个 毕竟你要看他传入的是4个参数 这个可能只是个子函数 我们看看谁调用的他 看看调用它的是调用了几个参数 跟进sub_23E940 看看谁调用的他

1
int __usercall sub_20F0E0@<eax>(int a1@<eax>, int a2@<edx>, int a3, int a4)

还是4个的 而且你看名字也有点像12 暂定是他吧 然后我们看第12个 对应的第三个参数就是我们需要的私钥 而这个私钥在调用时会被存放在内存里 上脚本

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
# -*- coding: utf-8 -*-
import codecs
import frida
import sys
import threading
#device = frida.get_remote_device()
device = frida.get_device_manager().enumerate_devices()[-1]
#print(device)
pending = []
sessions = []
scripts = []
event = threading.Event()
jscode = """
Java.perform(function () {
var Mars = Java.use("com.tencent.mars.Mars");
Mars.onCreate.implementation = function(args) {
var base = Process.findModuleByName("libmarsstn.so").base;
Interceptor.attach(base.add(0x20F0E0+1), {
onEnter: function(sql_args) {
send("key:" + Memory.readUtf8String(sql_args[1]));
}
});
}
});
"""
def on_spawned(spawn):
print('on_spawned:', spawn)
pending.append(spawn)
event.set()
def spawn_added(spawn):
print('spawn_added:', spawn)
event.set()
if(spawn.identifier.startswith('cn.keke.chat')):
# if(spawn.identifier.startswith('cn.keke.chat')):
session = device.attach(spawn.pid)
script = session.create_script(jscode)
script.on('message', on_message)
script.load()
device.resume(spawn.pid)
def spawn_removed(spawn):
print('spawn_added:', spawn)
event.set()
def on_message(spawn, message, data):
print('on_message:', spawn, message, data)

def on_message(message, data):
if message['type'] == 'send':
print("[*] {0}".format(message['payload']))
else:
print(message)
device.on('spawn-added', spawn_added)
device.on('spawn-removed', spawn_removed)
device.on('child-added', on_spawned)
device.on('child-removed', on_spawned)
device.on('process-crashed', on_spawned)
device.on('output', on_spawned)
device.on('uninjected', on_spawned)
device.on('lost', on_spawned)
device.enable_spawn_gating()
event = threading.Event()
print('Enabled spawn gating')

pid = device.spawn(["cn.keke.chat"])
#pid = device.spawn(["cn.keke.chat"])
session = device.attach(pid)
print("[*] Attach Application id:",pid)
device.resume(pid)
# print("[*] Application onResume")
# script = session.create_script(jscode)
# script.on('message', on_message)
# print('[*] Running CTF')
# script.load()
sys.stdin.read()

相比上一个改的不多 就是改了一下jscode 注意 我们的地址都是偏移地址 base.apk的基地址+函数的地址=偏移地址 只不过还有一个要注意的地方

根据数据库密钥附加的源码分析 判断左数第三个变量即为私钥 又因为传参的顺序从右向左 则有如下对应关系 为args[0]:nKeyLen, args[1]:pKey; args[2]:nDb; args[3] *db 所以args[1]为私钥pKey 咱hook一下

1
Enabled spawn gating[*] Attach Application id: 2150spawn_added: Spawn(pid=2178, identifier="cn.keke.chat:marsservice")spawn_added: Spawn(pid=2204, identifier="cn.keke.chat:pushservice")[*] key:216bf0c8-75a3-4bc4-841e-87cabf6bac9aspawn_added: Spawn(pid=2270, identifier="cn.keke.chat:marsservice")[*] key:216bf0c8-75a3-4bc4-841e-87cabf6bac9aspawn_added: Spawn(pid=2319, identifier="cn.keke.chat:marsservice")[*] key:216bf0c8-75a3-4bc4-841e-87cabf6bac9aspawn_added: Spawn(pid=2359, identifier="cn.keke.chat:marsservice")[*] key:216bf0c8-75a3-4bc4-841e-87cabf6bac9aspawn_added: Spawn(pid=2401, identifier="cn.keke.chat:marsservice")[*] key:216bf0c8-75a3-4bc4-841e-87cabf6bac9aspawn_added: Spawn(pid=2441, identifier="cn.keke.chat:marsservice")[*] key:216bf0c8-75a3-4bc4-841e-87cabf6bac9a

Jackpot! 然后就可以解密数据库了