第5章 CHAPTER 5 静态逆向工具 5.1Apktool工具 5.1.1Apktool基础与用法 视频8 Apktool是最常用的反编译Apk文件的工具,它可以将Apk包中的Dex文件和资源文件解码,并在修改后重新构建并打包。在GitHub上下载它的源码和发布的版本,网址为https://github.com/iBotPeaches/Apktool,直接下载Jar包。 如图5.1所示为Apktool的下载页面。 图5.1Apktool的下载页面 下载完毕后在命令行工具中使用Java命令执行,查看它的功能与用法。 $java -jar apktool.jar 如图5.2所示为Apktool所使用的参数。 图5.2Apktool所使用的参数 从图5.2中可以看到以下参数: usage: apktool -advance,--advancedprints advance information. -version,--versionprints the version then exits usage: apktool if|install-framework [options] -p,--frame-path Stores framework files into . -t,--tag Tag frameworks using . usage: apktool d[ecode] [options] -f,--forceForce delete destination directory. -o,--output The name of folder that gets written. Default is apk.out -p,--frame-path Uses framework files located in . -r,--no-resDo not decode resources. -s,--no-srcDo not decode sources. -t,--frame-tag Uses framework files tagged by . usage: apktool b[uild] [options] -f,--force-all Skip changes detection and build all files. -o,--output The name of apk that gets written. Default is dist/name.apk -p,--frame-path Uses framework files located in . Apktool主要有两种用法: 一个是d参数,代表解码Apk包; 另一个是b参数,代表构建Apk包,其中d参数下有两个选项需要注意。  r,nores: 这个参数指定了在反编译Apk的过程中不去处理包内的资源文件,也就是不解码resource.arsc文件和AndroidManifest.xml文件。Apktool在处理某些应用的时候可能会因为资源文件解码错误而导致反编译失败。如果只是对源码进行修改可以绕过资源的反编译,只对Dex文件进行反编译处理,在二次打包时Apktool会将资源文件原封不动地复制回包内。  s,nosrc: 这个参数指定了在反编译过程中不去处理包内的源码文件,这样包内的Dex文件就不会被反编译成Smali,而AndroidManifest.xml与resource.arsc会被解码。 这两个参数在实际使用Apktool的过程中十分有用。还要注意另一个参数,这个参数在读Apktool源码的时候会看到: forcemanifest,虽然这个参数不在usage列表中,但是可以使用的,这个参数的作用是无论是否处理资源文件都强制将AndroidManifest.xml文件进行解码。 5.1.2Apktool源码分析 一般逆向工作中所使用的Apktool工具是已经编译好的发布版,但是由于各家的Apk包情况不同,部分应用针对Apktool工具做了防护,发布版会出现反编译失败的情况。这时可以根据自己的需要动手修改源码,对Apktool工具进行自定义。首先从GitHub将项目下载或者clone下来。 如图5.3所示为Apktool源码目录。 图5.3Apktool源码目录 分析Apktool源码的出发点在brut.Apktool目录。其中的Apktoolcli目录下有一个Main.java文件,这就是Apktool的程序入口。 如图5.4所示为Apktool的main()函数。 图5.4Apktool的main()函数 Main.java文件负责处理运行程序时输入的参数,并根据参数选择对应的功能。 如图5.5所示为Apktool处理参数的代码逻辑。 图5.5Apktool处理参数的代码逻辑 使用Apktool时添加参数“d decode”,程序会调用cmdDecode方法进行反编译Apk相关操作。在cmdDecode方法中会看见一个对象ApkDecoder,这是Apktool负责具体反编译工作的对象,它的定义在Apktool/brut.apktool/apktoollib/src/main/java/brut/androlib/目录下的ApkDecoder.java文件中。接下来继续深入讨论ApkDecoder对象。 如图5.6所示为Apktool项目的Androidlib目录。 图5.6Apktool项目的Androidlib目录 下面从ApkDecoder类的decode方法入手分析Apktool的反编译功能。 校验Apk文件初始化反编译目录: if (!mForceDelete && outDir.exists()) { throw new OutDirExistsException(); } if (!mApkFile.isFile() || !mApkFile.canRead()) { throw new InFileNotFoundException(); } try { OS.rmdir(outDir); } catch (BrutException ex) { throw new AndrolibException(ex); } outDir.mkdirs(); 上述代码负责判断输入的Apk文件是否有错以及初始化反编译目录。如图5.7所示为处理资源文件的代码逻辑。 图5.7处理资源文件的代码逻辑 这段代码先判断Apk包中是否有资源文件,也就是resources.arsc文件。如果存在,则根据是否使用了参数“r,nores”指定了不解码资源文件,默认是DECODE_RESOURCES_FULL,也就是解码所有资源文件,如果有Manifest文件,则调用mAndrolib.decodeManifestWithResources()方法解码AndroidManifest.xml文件,然后调用mAndrolib.decodeResourcesFull()方法解码资源文件。如果指定了不解码资源文件,则调用mAndrolib.decodeResourcesRaw()方法,接着判断是否设置了强制解码AndroidManifest.xml文件; 如果设置了强制解码,则调用mAndrolib.decodeManifestWithResources()方法解码文件。 接下来进入mAndrolib对象的类Androlib,看看Apktool是怎么具体处理资源文件的。 如图5.8所示为Androlib类的部分逻辑截图。 图5.8Androlib类的部分逻辑截图 首先来看decodeResourcesRaw()方法。 decodeResourcesRaw()方法: public void decodeResourcesRaw(ExtFile apkFile, File outDir) throws AndrolibException { try { LOGGER.info("Copying raw resources..."); apkFile.getDirectory().copyToDir(outDir, APK_RESOURCES_FILENAMES); } catch (DirectoryException ex) { throw new AndrolibException(ex); } } 这个方法将包内的资源文件全部直接复制到输出目录下。也就是说,如果通过参数“r,nores”指定不解码资源文件,则直接将资源文件复制出来。 再来看一下用来解码资源文件的方法decodeResourcesFull()。 decodeResourcesFull()方法: public void decodeResourcesFull(ExtFile apkFile, File outDir, ResTable resTable) throws AndroidException{ mAndRes.decode(resTable, apkFile, outDir); } decodeResourcesFull()方法调用mAndRes对象的decode()方法,对资源文件进行解码: public void decode(ResTable resTable, ExtFile ApkFile, File outDir) throws AndrolibException { Duo duo = getResFileDecoder(); ResFileDecoder fileDecoder = duo.m1; ResAttrDecoder attrDecoder = duo.m2.getAttrDecoder(); attrDecoder.setCurrentPackage(resTable.listMainPackages().iterator().next()); Directory inApk, in = null, out; try { out = new FileDirectory(outDir); inApk = ApkFile.getDirectory(); out = out.createDir("res"); if (inApk.containsDir("res")) { in = inApk.getDir("res"); } if (in == null && inApk.containsDir("r")) { in = inApk.getDir("r"); } if (in == null && inApk.containsDir("R")) { in = inApk.getDir("R"); } } catch (DirectoryException ex) { throw new AndrolibException(ex); } ExtMXSerializer xmlSerializer = getResXmlSerializer(); for (ResPackage pkg : resTable.listMainPackages()) { attrDecoder.setCurrentPackage(pkg); LOGGER.info("Decoding file-resources..."); for (ResResource res : pkg.listFiles()) { fileDecoder.decode(res, in, out); } LOGGER.info("Decoding values */* XMLs..."); for (ResValuesFile valuesFile : pkg.listValuesFiles()) { generateValuesFile(valuesFile, out, xmlSerializer); } generatePublicXml(pkg, out, xmlSerializer); } AndrolibException decodeError = duo.m2.getFirstError(); if (decodeError != null) { throw decodeError; } } 接着来分析负责处理AndroidManifest.xml文件的方法mAndRes.decodeManifest WithResources(): public void decodeManifestWithResources(ExtFile apkFile, File outDir, ResTable resTable) throws AndrolibException { mAndRes.decodeManifestWithResources(resTable, apkFile, outDir); } decodeManifestWithResources()同样调用了mAndRes对象中的同名方法,来解析AndroidManifest.xml: public void decodeManifestWithResources(ResTable resTable, ExtFile apkFile, File outDir) throws AndrolibException { Duo duo = getResFileDecoder(); ResFileDecoder fileDecoder = duo.m1; ResAttrDecoder attrDecoder = duo.m2.getAttrDecoder(); attrDecoder.setCurrentPackage(resTable.listMainPackages().iterator().next()); Directory inApk, in = null, out; try { inApk = apkFile.getDirectory(); out = new FileDirectory(outDir); LOGGER.info("Decoding AndroidManifest.xml with resources..."); fileDecoder.decodeManifest(inApk, "AndroidManifest.xml", out, "AndroidManifest.xml"); if (!resTable.getAnalysisMode()) { adjustPackageManifest(resTable, outDir.getAbsolutePath() + File.separator + "AndroidManifest.xml"); ResXmlPatcher.removeManifestVersions(new File( outDir.getAbsolutePath() + File.separator + "AndroidManifest.xml")); mPackageId = String.valueOf(resTable.getPackageId()); } } catch (DirectoryException ex) { throw new AndrolibException(ex); } } 如果Apk中不存在resources.arsc文件,则不参照属性的引用对AndroidManifest文件解码: else { // if there's no resources.arsc, decode the manifest without looking // up attribute references if (hasManifest()) { if (mDecodeResources == DECODE_RESOURCES_FULL || mForceDecodeManifest == FORCE_DECODE_MANIFEST_FULL) { mAndrolib.decodeManifestFull(mApkFile, outDir, getResTable()); } else { mAndrolib.decodeManifestRaw(mApkFile, outDir); } } } 资源文件处理完毕后再处理源码文件。一个Apk中可能有多个Dex文件,所以decode()方法先处理第一个Dex文件classes.dex,然后再根据是否存在其他的Dex文件进行下一步。 如图5.9所示为Apktool判断是否存在其他Dex文件的代码。 图5.9判断是否存在其他Dex文件 针对每个Dex文件检查传入的参数,如果参数指定了不处理Dex文件,则调用decodeSourcesRaw()方法,直接将Dex文件复制到目标目录下。 public void decodeSourcesRaw(ExtFile apkFile, File outDir, String filename) throws AndrolibException { try { LOGGER.info("Copying raw " + filename + " file..."); apkFile.getDirectory().copyToDir(outDir, filename); } catch (DirectoryException ex) { throw new AndrolibException(ex); } } 如果是默认情况,也就是需要解码Dex文件,则会调用decodeSourcesSmali()方法: public void decodeSourcesSmali(File apkFile, File outDir, String filename, boolean bakdeb, int api) throws AndrolibException { try { File smaliDir; if (filename.equalsIgnoreCase("classes.dex")) { smaliDir = new File(outDir, SMALI_DIRNAME); } else { smaliDir = new File(outDir, SMALI_DIRNAME + "_" + filename.substring(0, filename.indexOf("."))); } OS.rmdir(smaliDir); smaliDir.mkdirs(); LOGGER.info("Baksmaling " + filename + "..."); SmaliDecoder.decode(apkFile, smaliDir, filename, bakdeb, api); } catch (BrutException ex) { throw new AndrolibException(ex); } } decodeSourcesSmali()方法调用Baksmali组件将Dex文件反编译成Smali文件。 5.2JEB工具 JEB是一款强大的跨平台的Android静态分析工具,提供了类似于IDA Pro的方法交叉引用与重命名功能,同时提供脚本化功能,用于自动化分析和对抗代码混淆。 5.2.1JEB安装 从JEB官网https://www.pnfsoftware.com/jeb/下载软件包并解压。如图5.10所示为JEB程序目录。 图5.10JEB程序目录 JEB的运行需要依赖JDK8或以上的JDK环境。提前下载安装JDK并设置系统变量JAVA_HOME。JEB提供了可在Windows、Linux、macOS上运行的UI客户端,在相应的系统执行对应文件: Windows执行jeb_wincon.bat; Linux执行jeb_linux.sh; macOS执行jeb_macos.sh。 如图5.11所示为JEB运行起来的效果。 图5.11JEB运行起来的效果 5.2.2JEB静态分析 接下来通过使用JEB分析一个应用来熟悉它的用法。本书将从JEB官网https://www.pnfsoftware.com/jeb/manual/下载官方实例应用Raasta.Apk作为此章节的测试应用。 如图5.12所示为下载Raasta.Apk的页面。 图5.12下载Raasta.Apk的页面 从文件菜单中打开Raasta.Apk文件,JEB会使用这个Apk文件创建一个新的项目,并通过以下几个Android分析组件去处理它:  Apk组件负责拆解应用文件,解码它的AndroidManifest文件和资源文件。  Dex组件负责解析应用包中的Dex字节码文件。  Xml组件负责处理Android应用资源目录下的XML资源文件。  签名组件负责分析应用的签名。 打开项目后,左侧的项目浏览树会展示Apk包内的各类文件和目录,左侧下方的Bytecode结构树会显示Dex字节码反编译出来的源码项目结构。主要的Dex窗口是在项目分析完毕后自动打开的,展示Dex文件反编译出来的Smali语句。 如图5.13所示为JEB的主界面截图。 图5.13JEB的主界面截图 接下来介绍JEB在进行静态分析时的常用功能。 1. 反编译功能 在主界面的Dex字节码窗口,滚动到需要反编译的部分区域,按Tab键就可以将Smali源码反编译成Java源码。这时会打开另一个窗口,里面是选择区域所属的Java类的Java源码。 如图5.14所示为JEB反编译应用的效果。 图5.14JEB反编译应用的效果 在Java源码界面再按Tab键就会回到对应的Smali语句。 2. 重命名 静态分析时,可能会遇到代码被混淆的情况,多个方法名和变量被转化成类似于a()、ab()之类毫无意义的形式。为了应对这种情况,一个比较重要的需求是可以对代码中的各项,比如类型、方法、例程、类变量、数据项或者包名进行重命名。JEB提供了这一功能。  定位并单击需要重命名的项目。  按N键或者选择Action栏中的Rename选项。  输入新的名称。 如图5.15所示为JEB对反编译代码的项目进行重命名操作。 在“重命名”窗口中按Ctrl+空格键可以查看之前的重命名历史记录。 3. 添加注释 在代码中的任意位置,按“/”键打开添加注释界面,输入注释内容。注释会附加在所选择的语句的后面。 如图5.16所示为JEB为代码添加注释。 4. 导航 在做静态分析时经常需要去找某个调用方法或结构体的定义。在JEB中,单击选中项目并按回车键或者在项目上双击,就会跳转到显示该项定义的窗口。可以使用快捷键“Alt+左箭头”或“Alt+右箭头”进行向前或者向后导航。 图5.15JEB对反编译代码的项目进行重命名操作 图5.16JEB为代码添加注释 如图5.17所示为选择set_lastSplashSequence()函数调用。 图5.17选择set_lastSplashSequence()函数调用 如图5.18所示为JEB跳转到set_lastSplashSequence()函数的定义。 图5.18JEB跳转到set_lastSplashSequence()函数的定义 5. 交叉引用 除了查看某个引用项的定义,有时逆向人员还需要查看某个引用项在整个项目中的引用情况。在JEB中选择某项,按X键打开交叉引用窗口,双击窗口中的项目跳转到引用点。 如图5.19所示为查看函数的交叉引用。 图5.19查看函数的交叉引用 6. 重构 JEB提供了强大的重构能力,允许使用者在项目中创建新的包并把某个包内的类移动到新的或者是已存在的其他包中。 作为示范,此处将使用这个功能创建一个新包com.newPack,然后将com.pnfsoftware.raasta包内的AppHelp类移动到新包com.newPack中。首先按K键新建一个包com.newPack。 如图5.20所示为在JEB中新建一个包。 图5.20在JEB中新建一个包 然后按L键将AppHelp类移动到com.newPack包中。 如图5.21所示为在JEB中移动类。 图5.21在JEB中移动类 7. 修改常量 这个功能允许选择整数常量以哪种进制形式显示。选择常量后,按B键循环选择插件提供的进制。通常插件提供八进制、十进制、十六进制方式,其他的插件可能会额外提供二进制方式,或者是非常规的显示方式,比如基于字符。 如图5.22所示为以十进制显示选中的整数常量。 图5.22以十进制显示选中的整数常量 如图5.23所示为以八进制显示选中的整数常量。 图5.23以八进制显示选中的整数常量 如图5.24所示为以十六进制显示选中的整数常量。 图5.24以十六进制显示选中的整数常量 视频9 5.3Jadxgui工具 Jadx是一个将Dex字节码文件反编译成Java的开源工具,作者同时提供了UI客户端方便进行动态调试。Github地址为https://github.com/skylot/jadx。下载Jar包到本地后运行Java命令启动: java-jarjadx-gui.jar 如图5.25所示为Jadxgui的启动界面。 图5.25Jadxgui的启动界面 Jadxgui的主要特性是:  处理Apk、Dex、Aar和Zip文件,将其中的Dalvik字节码反编译成Java类。  反编译AndroidManifest.xml和解码其他的资源文件和resources.arsc文件。  添加了反混淆功能。  使用高亮语法显示反编译出来的代码。 如图5.26所示为Jadxgui反编译代码的截图。 图5.26Jadxgui反编译代码的截图  跳转到项目的定义。 在类、方法和类变量项目上按Ctrl+鼠标左键跳转到该项目的定义。如图5.27所示为选择set_lastTraceFileName()函数。 图5.27选择set_lastTraceFileName()函数 如图5.28所示为跳转到set_lastTraceFileName()函数的定义。 图5.28跳转到set_lastTraceFileName()函数的定义 在AndroidManifest.xml中可以直接跳转到类的定义,如图5.29所示为在AndroidManifest.xml中找到TraceList类。 图5.29在AndroidManifest.xml中找到TraceList类 如图5.30所示为从AndroidManifest中跳转到TraceList类。 图5.30从AndroidManifest中跳转到TraceList类  查找项目的引用。 在项目上右击,在弹出的快捷菜单中选择Find Usage命令,就会弹出该项目的所有引用。 如图5.31所示为在Jadxgui中查找引用。 图5.31在Jadxgui中查找引用  文本搜索。 Jadxgui提供全局文本搜索,在整个项目的范围内查找字符串,可以匹配类名、方法名、变量名、代码。 如图5.32所示为在Jadxgui中进行文本搜索。 图5.32在Jadxgui中进行文本搜索 Jadxgui功能相比较于JEB要少一些,但是Jadxgui是开源项目,轻量小巧,足够应付静态分析的任务。 5.4010editor工具 010editor可以说是目前最强大的一款十六进制编辑器,可以编辑与查看各种十六进制文件,可以通过加载不同的文件模板解析各种文件格式。 5.4.1010editor解析so文件 010editor官网有elf的文件模板网址为https://www.sweetscape.com/010editor/repository/files/ELF.bt,将其复制下来,单击Templates选项,选择New Templates打开模板。使用010editor打开so文件后按F5键即可使用对应的模板。 010editor模板对照elf文件定义的格式从十六进制中直接解析出so文件的各段表。010editor的优点是可以更加直观地去定位某个结构的值在整个文件中的位置,可以配合Hex editor等十六进制文件编辑器对so文件直接进行修改。 如图5.33所示为使用010editor打开so文件的界面。 图5.33使用010editor打开so文件的界面 5.4.2010editor解析Dex文件 Dex的文件模板可以在010editor官网上找到,网址为https://www.sweetscape.com/010editor/repository/files/DEX.bt,按照上面的做法加载模板,打开Dex文件。可以将Apk包用Zip工具解压缩获得Dex文件。 如图5.34所示为使用010editor打开Dex文件的界面。 图5.34使用010editor打开Dex文件的界面 5.5本章小结 本章介绍了对Android应用进行静态分析常用的工具以及使用方法,包括各种工具对Native层与Java层的静态逆向分析。互联网中还有许多集成了图形化操作界面的Android分析工具,比如Android Killer,这些分析工具本质上是对Baksmali、Apktool等工具的集成。这些工具还会在后面实战篇中陆续出现,本书也会结合具体需求介绍更多的用于逆向分析的实战工具。