dexsim浅析

dexsim简介

dexsim 是利用动态调用实现字符串解密的工具,需要配合 DSS 使用。作者为 mikusjelly

dexsim 源码浅析

dexsim 源码结构如下图所示,

其中关键解密方法在 dexsim/Plugins 中,当我们需要添加一个解密方法时直接在该目录中添加对应插件既可。

首先来看 main 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if __name__ == "__main__":
parser = argparse.ArgumentParser(prog='dexsim', description='')
parser.add_argument('f', help='APK 文件')
parser.add_argument('-i', '--includes', nargs='*',
help='仅解密包含的类,如abc, a.b.c')
parser.add_argument('-o', help='output file path')
parser.add_argument('-d', '--debug', action='store_true', help='开启调试模式')
parser.add_argument('-s', required=False, help='指定smali目录')
parser.add_argument('-p', '--pname', required=False, help='加载指定插件,根据插件名字')
# TODO parser.add_argument('-b', action='store_true', help='开启STEP_BY_STEP插件')

args = parser.parse_args()

start = time.time()
main(args)
finish = time.time()
print('\n%fs' % (finish - start))

该方法主要是解析参数

  • -i 仅解密包含的类 参数格式为 a.b.c a.b, 后面会将其转换为 a/b/c.smali a/b.smali
  • -o 解密后输出文件的路径
  • -s 指定smali目录
  • -p 加载指定插件,根据插件名字

main() 方法中,将 apk 中的多个 dex 合并为一个名为 new.dex 文件

1
2
3
4
5
6
7
8
9
10
ptn = re.compile(r'classes\d*.dex')

zipFile = zipfile.ZipFile(apk_path)
for item in zipFile.namelist():
if ptn.match(item):
output_path = zipFile.extract(item, tempdir)
baksmali(output_path, smali_dir)
zipFile.close()

dex_file = os.path.join(tempdir, 'new.dex')

然后在使用 smali 将合并的 dex 转为 samli 文件, 并进一步解析。

1
2
smali(smali_dir, dex_file)
dexsim_apk(args.f, smali_dir, includes, output_dex)

上面的方法可以进行一下优化,没有必要将合并的 dex 进行反编译为 smali 文件,并且只能针对 apk 文件进行解密,可以增加对 dex 文件的解密。

接着看 dexsim_apk 方法.

1
2
3
4
5
6
dexsim(apk_file, smali_dir, includes)
if output_dex:
smali(smali_dir, output_dex)
else:
smali(smali_dir,
os.path.splitext(os.path.basename(apk_file))[0] + '.sim.dex')

发现该方法直接调用 dexsim 方法,然后解密完成,那么关键方法为 dexsim

1
2
3
4
5
6
7
8
9
10
11
12
13
def dexsim(apk_file, smali_dir, includes):
"""推送到手机/模拟器,动态解密

Args:
apk_file (TYPE): Description
smali_dir (TYPE): Description
includes (TYPE): Description
"""
driver = Driver()
driver.push_to_dss(apk_file)

oracle = Oracle(smali_dir, driver, includes)
oracle.divine()

在该方法中将 dex 文件推送到手机中,然后调用创建 Oracle 对象向,调用该对象的 divine 进行解密.

接下来看 Oracle 对象的 __init__ 方法

1
2
3
4
5
6
7
8
9
10
11
12
def __init__(self, smali_dir, driver, includes):
'''
'''
self.driver = driver
# 下面一段代码可以删除,因为我们传的includes参数已经去掉了 L
paths = []
if includes:
for item in includes:
paths.append(item[1:].split(';')[0])

self.smalidir = SmaliDir(smali_dir, include=paths, exclude=FILTERS)
self.plugin_manager = PluginManager(self.driver, self.smalidir)

调用 SmaliDir 读取 smali 代码, 然后调用 PluginManager 加载插件。

接下来看看 PluginManager 如何加载所有插件的

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
def __init__plugins(self):
for path in sys.path:
if path and path in __file__:
pkg = __file__.replace(path, '')
break
module_path = os.path.dirname(pkg)[1:].replace(
os.sep, '.') + '.' + self.plugin_dir + '.'

tmp = [None] * len(self.plugin_filenames)
# 开始加载所有插件
for name in self.plugin_filenames:
spec = importlib.util.find_spec(module_path + name)
mod = spec.loader.load_module()
clazz = getattr(mod, mod.PLUGIN_CLASS_NAME)
if not issubclass(clazz, Plugin):
continue

if not clazz.enabled:
print("Don't load plugin", clazz.name)
continue
tmp[clazz.index] = clazz(self.driver, self.smalidir)

for item in tmp:
if item:
self.__plugins.append(item)

首先获取插件名,然后调用 importlib.util.find_spec(module_path + name) 加载插件,完成插件的加载。

接下来回到 oracle.divine() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def divine(self):
plugins = self.plugin_manager.get_plugins()

flag = True
smali_mtds = set() # 存放已被修改的smali方法
while flag:
flag = False
for plugin in plugins:
# 调用插件的run方法
plugin.run()
# 更新smali_mtds 文件
smali_mtds = smali_mtds.union(plugin.smali_mtd_updated_set)
print(plugin.make_changes)
flag = flag | plugin.make_changes
plugin.make_changes = False

self.driver.adb.run_shell_cmd(['rm', DSS_APK_PATH])

关键的加密方法还是要看插件。

接下来看 Plugin 类,该类是所有插件的基类。先看看两个关键的成员变量

1
2
3
4
5
6
# [{'className':'', 'methodName':'', 'arguments':'', 'id':''}, ..., ]
json_list = [] # 存放解密对象

# [(mtd, old_content, new_content), ..., ]
# [(方法体, 原始的内容,解密后的内容),...,]
target_contexts = {}

json_list 存放解密对象,将转成文件推送到手机中让 DSS 解析并动态执行,其格式如下

1
2
3
4
5
6
7
8
9
10
11
12
[{
"className": "othn.iclauncher",
"methodName": "Ez",
"arguments": ["java.lang.String:FK9FD0004670751372201EA6"],
"id": "a439b0d815c9a0a972c6b0dc69ec7bee5663ae9b65294b2828fbb8aaa098ce70"
}, {
"className": "othn.iclauncher",
"methodName": "EA",
"arguments": ["java.lang.String:FKBEFCC3DA309EDA1B6FC62DF7E3EBECB5"],
"id": "cdfcbfd5a872408ba4cc06b6f5a1fb48f1c5e18d5c36deb6e6fe41bd6b3d5c8c"
},
]

target_contexts 存放解密前后的代码和方法体,方便后续替换。

几个关键的成员变量高清楚之后,剩下的东西也比较好理解,所以就不多说了,最后就是看看替换方法体

1
2
3
4
5
6
7
8
9
10
11
12
13
for key, value in outputs.items():
if key not in self.target_contexts:
print(key, value, "not in")
continue
for mtd, old_content, new_content in self.target_contexts[key]:
old_body = mtd.get_body()
new_content = old_content + "\n" + new_content.format(value[0])
body = old_body.replace(old_content, new_content)
mtd.set_body(body)
self.make_changes = True


self.smali_files_update()

outputs 为动态执行后的结果,主要格式如下:

1
2
3
4
5
6
{
"7b842f01264dc1d1a5089da9e86f531e90f5affe9ef36ecade2e2878a306ae7a": ["sender"],
"5f0edfa5e4249ff38f5918e9b27197aec7aaeeed6c7c604a109bdfb21d9b7dc5": ["ss"],
"642ed422a84d5ccab9e8fb27813c17d80b346af15295ffdc72dbd09d8662e34c": ["raw_data"],
"e19e1215be04291d5a0c61232a7ae933a3ad6c6e760e7b86ccc2800f0350730a": ["SUCCEED"],
}

通过相同的key进行替换,上面为了避免回编译为dex文件的时报错,直接使用的追加方式。

整个代码的原理大概就是这样,关键就是写插件,这一块就不详细说了,有兴趣可以看看 Plugin 目录中的插件.

最后看看解密后的效果吧。

解密前

解密后