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
目录中的插件.
最后看看解密后的效果吧。
解密前
解密后