Android 反编译教程(三)
六、反编译实现
现在,您已经学会了处理单个字节码,并将操作码反编译成部分语句和表达式,最终(无论如何,这是计划),返回完整的源代码块。
如果我对我的读者判断正确的话,这一章,可能还有第五章会吸引大量的读者。这是如何使用 ANTLR 构建的解析器实现反编译器的核心问题。
为了使本章尽可能实用,我们使用了一个简单程序的测试套件,每个程序都有不同的语言结构。对于每一个程序,你都要逐步重建原始源代码,在进行过程中构建反编译器。每个程序都是先编译再反汇编。然后查看 Java 源代码和相应的方法字节码,并为每个示例创建一个解析器规范,将字节码转换回源代码。
因为classes.dex文件不仅仅是方法字节码,您还需要能够将剩余的信息合并到类文件中,以便从文件的非数据部分恢复导入语句、包名和变量名。
通过完成第三章中的 DexToXML 的实现,您可以开始更加熟悉 ANTLR。DexToXML 是一个基本的 ANTLR 解析器,没有额外的功能。之后,查看 DexToSource(反编译器)将字节码指令反编译回 Java 源代码。
德克斯托 XML
DexToXML 是 ANTLR 解析的简单介绍。它使用 ANTLR 作为解析器技术。这本书的早期版本使用了 JLex 和 CUP,它们很难工作,甚至更难调试——您可能会花费数小时试图找出为什么向规则添加一个简单的更改会破坏整个解析器。自 2004 年以来发生了很多变化,ANTLR 现在提供了与 Eclipse 以及 ANTLRWorks 的出色集成,ANTLRWorks 是一个独立的 ANTLR 工具,它将创建解析器的艺术转变为更简单的编码任务。
ANTLR 也是一种优秀的技术,可以为各种领域特定语言(DSL)工具创建自己的解析器。这些通常是一次性的微型编程语言、规则引擎、绘图工具等等,它们是为解决特定问题而创建的;当 grep、sed 和 awk 等脚本工具不能胜任工作时,通常会用到它们。
让我们首先看看解析dex.log,它是 dedexer 工具的输出之一。Dedexer 是一个 dex 文件反汇编程序,通常用于在 DDX 文件中生成类似 smali 的反汇编程序输出,但也可以在dex.log文件中给出classes.dex的完整输出。您也可以使用 Android SDK 中的 dexdump 文件的输出,但是我个人更喜欢更简单的dex.log文件的输出。
解析 dex.log 输出
dex.log是在编译版本的Casting.java文件上运行以下 dedexer 命令时创建的日志文件:
c:\temp>java -jar ddx1.18.jar -o -d c:\temp casting\classes.dex
dex.log是一个classes.dex文件的原始输出,它允许你在没有解析字节开销的情况下进行反编译,这正是你想要的。清单 6-1 显示了classes.dex文件头的输出。
清单 6-1。 类的头
00000000 : 64 65 78 0A 30 33 35 00 magic: dex\n035\0 00000008 : 62 8B 44 18 checksum 0000000C : DA A9 21 CA 9C 4F B4 C5 21 D7 77 BC 2A 18 4A 38 0D A2 AA FE signature 00000020 : 50 04 00 00 file size: 0x00000450 00000024 : 70 00 00 00 header size: 0x00000070 00000028 : 78 56 34 12 00 00 00 00 link size: 0x00000000 00000030 : 00 00 00 00 link offset: 0x00000000 00000034 : A4 03 00 00 map offset: 0x000003A4 00000038 : 1A 00 00 00 string ids size: 0x0000001A 0000003C : 70 00 00 00 string ids offset: 0x00000070 00000040 : 0A 00 00 00 type ids size: 0x0000000A 00000044 : D8 00 00 00 type ids offset: 0x000000D8 00000048 : 07 00 00 00 proto ids size: 0x00000007 0000004C : 00 01 00 00 proto ids offset: 0x00000100 00000050 : 03 00 00 00 field ids size: 0x00000003 00000054 : 54 01 00 00 field ids offset: 0x00000154 00000058 : 09 00 00 00 method ids size: 0x00000009 0000005C : 6C 01 00 00 method ids offset: 0x0000016C 00000060 : 01 00 00 00 class defs size: 0x00000001 00000064 : B4 01 00 00 class defs offset: 0x000001B4 00000068 : 7C 02 00 00 data size: 0x0000027C 0000006C : D4 01 00 00 data offset: 0x000001D4
让我们先来看看幻数部分,它在文件的开头;参见清单 6-2 。
清单 6-2。 幻数
00000000 : 64 65 78 0A 30 33 35 00 magic: dex\n035\0
所有classes.dex文件的格式都是一样的。目标是解析幻数和输出
<root><header><magic>dex\n035\0</magic></header></root>
使用这些信息:
- 八个十六进制数字,文件中的地址
- 一个冒号
- 两组八个十六进制数字
magic关键字- 另一个冒号
classes.dex神奇的数字
工作中的 ANTLR
ANTLR 的工作方式是首先对输入进行标记,然后通过一系列解析规则,产生所需的输出。第一步是将信息分解成令牌。一个显而易见的标记是一个十六进制数字(HEX_DIGIT)以及您希望解析器忽略的WS或空格。用于标记幻数头信息的 ANTLR 解析器如清单 6-3 所示。语法、选项、@header和@lexer分别告诉解析器语法的名称、生成解析器的语言以及解析器和词法分析器的包名。
**清单 6.3。**ANTLR 幻数解析器
`grammar DexToXML; options {language = Java;} @header {package com.riis.decompiler;} @lexer::header {package com.riis.decompiler;}
rule : header ;
header : magic ;
magic: address eight_hex eight_hex IDENT ':' MAGIC_NUM ;
hex_address: '0x' eight_hex ;
address : eight_hex ':' ;
eight_hex : DIGIT DIGIT DIGIT DIGIT DIGIT DIGIT DIGIT DIGIT ;
IDENT: ('a'..'z')+; MAGIC_NUM: 'dex\n035\0'; DIGIT : ('0'..'9'|'A'..'F'); WS: (' ' | '\t' | '\n' | '\r' | '\f' | ',')+ {$channel = HIDDEN;};`
在清单 6-3 中,从下往上看,WS定义了发送到隐藏通道并被忽略的空白是什么意思。DIGIT将十六进制数字定义为 0–f。MAGIC_NUM是dex\n035\0的转义版本;最后,IDENT是任何字符串。解析器规则接受这些标记,并将它们排列成预期的模式。例如,从清单 6-2 中可以知道,十六进制数字被分成八组,分别代表地址和幻数。地址后面有一个冒号。清单 6-2 被标记后看起来有点像清单 6-4 。
清单 6-4。 记号化的幻数
address eight_hex eight_hex IDENT MAGIC_NUM
规则
在 ANTLR 中,和所有解析器一样,您需要一个rule来告诉解析器从哪里开始解析。rule表示传入的文件由一个header组成;而那只header,目前有一个神奇的数字。magic规则期望传入文件的格式如清单 6-4 中的所示。关于如何在 Eclipse 中建立 ANTLR 项目的指导,请参见[vimeo.com/8001326](http://vimeo.com/8001326)。使用 Eclipse,来自清单 6-2 的的输入令牌被解析,如图 6-1 中的所示。
图 6-1。??【幻数解析规则】??
既然已经成功解析了幻数,下一步就是以正确的格式输出它。
输出幻数
清单 6-5 用System.out.println语句更新。使用@init和@after ANTLR 语句以正确的顺序打印<root>和<header>语句。如果您不喜欢解析器中所有多余的 Java 代码,也可以使用 ANTLR StringTemplate s 删除所有的println语句。
清单 6-5。 DexToXML 幻数解析器
**grammar** DexToXML; **options** {language = Java;} @header {**package** com.riis.decompiler;} @lexer::header {**package** com.riis.decompiler;}
`rule
@init {System.out.println("");}
@after {System.out.println("");}
: header
;
header @init {System.out.println("
");} @after {System.out.println("");} : magic ;magic: address eight_hex eight_hex IDENT ':' id=MAGIC_NUM
{System.out.println("" + id.getText() + "");}
; hex_address: '0x' eight_hex
;
address : eight_hex ':' ;
eight_hex : DIGIT DIGIT DIGIT DIGIT DIGIT DIGIT DIGIT DIGIT ;
IDENT: ('a'..'z')+; MAGIC_NUM: 'dex\n035\0'; DIGIT : ('0'..'9'|'A'..'F'); WS: (' ' | '\t' | '\n' | '\r' | '\f' | ',')+ {$channel = HIDDEN;};`
清单 6-6 有从 Eclipse 外部的命令行调用 ANTLR 代码所必需的 Java 代码。这从c:\temp\input.log获取输入。
清单 6-6。??DexToXML.java
`package com.riis.decompiler;
import java.io.*;
import org.antlr.runtime.ANTLRInputStream; import org.antlr.runtime.CommonTokenStream; import org.antlr.runtime.RecognitionException; import org.antlr.runtime.TokenStream;`
`public class DexToXML {
public static void main(String[] args) throws RecognitionException, IOException { DexToXMLLexer lexer = new DexToXMLLexer(new ANTLRInputStream(System.in)); TokenStream tokenStream = new CommonTokenStream(lexer); DexToXMLParser parser = new DexToXMLParser(tokenStream);
parser.rule(); }
}`
使用以下命令编译com\riis\decompiler目录中的代码,确保 ANTLR v3.4 库在您的类路径中。第一个命令生成 lexer 和解析器,第二个命令编译 DexToXML 代码:
java org.antlr.Tool DexToXML.g javac DexToXMLLexer.java DexToXMLParser.java DexToXML.java
将清单 6-2 保存为magic.log,在顶层目录中运行下面的命令,得到如清单 6-7 所示的 DexToXML 输出:
java com.riis.decompiler.DexToXML < magic.log
清单 6-7。 DexToXML 输出
`
dex\n035\0 `接下来,为报头的其余部分创建语法。最初,您可以设置规则来拆分标题,如清单 6-8 中的所示。
清单 6-8。 表头规则
header @init {System.out.println("<header>");} @after {System.out.println("</header>");} : magic checksum signature file_size header_size link_size link_offset map_offset string_ids_size string_ids_offset type_ids_size type_ids_offset proto_ids_size proto_ids_offset fields_ids_size fields_ids_offset method_ids_size method_ids_offset class_defs_size class_defs_offset data_size data_offset ;
但是许多模式是重复的——例如,大小和偏移量非常相似,所以您可以通过提取节点的名称来重构解析器。你可以把这些模式放在一起,如清单 6-9 所示,它匹配不同的头条目。
**清单 6-9。**重构了header_entry规则
header_entry : address eight_hex *IDENT* | address eight_hex xml_id ':' hex_address | address eight_hex eight_hex xml_id ':' hex_address ;
将这些代码放到修改后的解析器中,您会得到清单 6-10 中所示的完整头部。
清单 6-10。 重构的 DexToXML 头语法
`grammar DexToXML; options {language = Java;} @header {package com.riis.decompiler;} @lexer::header {package com.riis.decompiler;}
rule : header ;
header : magic
header_entry
signature
header_entry+
;
magic: address eight_hex eight_hex IDENT ':' MAGIC_NUM ;
header_entry : address eight_hex IDENT | address eight_hex xml_id ':' hex_address | address eight_hex eight_hex xml_id ':' hex_address ;
xml_id : IDENT IDENT | IDENT IDENT IDENT ;
signature: address signature_hex 'signature' ; signature_hex: eight_hex eight_hex eight_hex eight_hex eight_hex
;
hex_address: '0x' eight_hex ;
address : eight_hex ':' ;
eight_hex : DIGIT DIGIT DIGIT DIGIT DIGIT DIGIT DIGIT DIGIT ;
IDENT: ('a'..'z')+; MAGIC_NUM: 'dex\n035\0'; DIGIT : ('0'..'9'|'A'..'F'); WS: (' ' | '\t' | '\n' | '\r' | '\f' | ',')+ {$channel = HIDDEN;};`
清单 6-11 中的解析了文件的更多内容,一直到code_item部分。我还包含了输出 XML 的代码,所以您可以看到这是如何完成的。
清单 6-11。 DexToXML ANTLR 语法
`grammar DexToXML;
options { language = Java; }
@header { package com.riis.decompiler; }
@lexer::header { package com.riis.decompiler; }
rule
@init {System.out.println("");}
@after {System.out.println("");}
: header
string_ids
type_ids
proto_ids
field_ids
method_ids
class_defs
data ; header
@init {System.out.println("
magic: address eight_hex eight_hex IDENT ':' id=MAGIC_NUM {System.out.println("" + id.getText() + "");} ;
header_entry : address id1=eight_hex id2=IDENT {System.out.println("<" + id1.text
- "</" + id2.text + ">");} | address eight_hex id3=xml_id ':' id4=hex_address {System.out.println("<" + id3.result + ">" + id3.result + ">");} | address eight_hex eight_hex id5=xml_id ':' id6=hex_address {System.out.println("<" + id6.text + "</" + $id5.result + ">");} ;
xml_id returns [String result] : id1=IDENT id2=IDENT {result = id1.getText() + "_" + id2.getText();} | id1=*IDENT* id2=*IDENT* id3=*IDENT* {result = id1.getText() + "" + id2.getText() + ""
- id3.getText();} ;
signature: address id=signature_hex 'signature' {System.out.println("" + $id.text + "");} ;
signature_hex: eight_hex eight_hex eight_hex eight_hex eight_hex ;
string_ids
@init {System.out.println("<string_ids>");}
@after {System.out.println("</string_ids>");}
: string_address+
; string_address
: address eight_hex IDENT id1=array_digit ':' 'at'
id2=hex_address
{System.out.println("\n" + id2.text + "\n");}
;
type_ids @init {System.out.println("<type_ids>");} @after {System.out.println("</type_ids>");} : type_address+ ;
type_address : address eight_hex IDENT id1=array_digit 'index:' id2=eight_hex '(' id3=proto_type_string ')' {int addr = Integer.parseInt(id1.result + "\n<string_id>"
- addr + "</string_id>\n"
- $id3.text + "\n");} ;
proto_ids @init {System.out.println("<proto_ids>");} @after {System.out.println("</proto_ids>");} : proto_address+ ;
proto_address : address eight_hex eight_hex eight_hex IDENT id1=array_digit ':' 'short signature:' id2=proto_type_string ';' 'return type:' id3=proto_type_string ';' 'parameter block offset:' eight_hex {System.out.println("\n" + $id1.result + "\n"
- id2.text + "\n");} ;
field_ids @init {System.out.println("<field_ids>");} @after {System.out.println("</field_ids>");} : field_address+ ;
field_address : address eight_hex eight_hex IDENT id1=array_digit ':' id2=proto_type_string id3=proto_type_string {System.out.println("\n" + $id1.result + "\n"
- id3.text +
"\n");}
;
method_ids @init {System.out.println("<method_ids>");} @after {System.out.println("</method_ids>");} : method_address+ ;
method_address : address eight_hex eight_hex IDENT id1=array_digit ':' id2=proto_type_string '(' id3=proto_type_string ')' {System.out.println("\n" + $id1.result + "\n"
- id3.text + "\n");} ;
class_defs @init {System.out.println("");} @after {System.out.println("");} : class_address+ ;
class_address : address id1=eight_hex id2=eight_hex id3=eight_hex id4=eight_hex id5=eight_hex id6=eight_hex id7=eight_hex id8=eight_hex id9=IDENT id10=IDENT {System.out.println("\n" +"<class_id>" + id10.text + "</class_id>\n" +"<type_id>" + $id1.text + "</type_id>\n"
+"<access_flags>" + id3.text +
"\n"
+"<interfaces_offset>" + id5.text +
"<source_file_id>\n"
+"<annotations_offset>" + id7.text +
"<class_data_offset>\n"
+"<static_values_offset>" + $id8.text + +"<static_values_offset>\n" +
"");}
;
data @init {System.out.println("");} @after {System.out.println("");} : class_+ ;
class_ @init {System.out.println("");} @after {System.out.println("");} : class_data_items ;
class_data_items @init {System.out.println("<class_data_items>");} @after {System.out.println("</class_data_items>");} : class_data_item ;
class_data_item @init {System.out.println("<class_data_item>");} @after {System.out.println("</class_data_item>");} : class_data_item_header static_fields //instance_methods direct_methods // virtual_methods encoded_arrays ;
class_data_item_header : address HEX_DOUBLE 'static fields size:' id1=DIGIT address HEX_DOUBLE 'instance fields size:' id2=DIGIT address HEX_DOUBLE 'direct methods size:' id3=DIGIT address HEX_DOUBLE 'virtual methods size:' id4=DIGIT {System.out.println("<static_field_size>" + $id1.getText()
- "</static_field_size>\n" +"<instance_field_size>" + id3.getText() + "</direct_methods_size>\n" +"<virtual_methods_size>" + $id4.getText() + "</virtual_methods_size>");} ;
static_fields
@init {System.out.println("<static_fields>");}
@after {System.out.println("</static_fields>");} : static_field+
;
static_field @init {System.out.println("<static_field>");} @after {System.out.println("</static_field>");} : address id1=HEX_DOUBLE id2=HEX_DOUBLE {System.out.println("<field_id>" + id2.getText() + "</access_flags>");} ;
direct_methods @init {System.out.println("<direct_methods>");} @after {System.out.println("</direct_methods>");} : direct_method+ ;
direct_method @init {System.out.println("<direct_method>");} @after {System.out.println("</direct_method>");} : address id1=HEX_DOUBLE id2=HEX_DOUBLE id3=HEX_DOUBLE id4=HEX_DOUBLE id5=HEX_DOUBLE id6=HEX_DOUBLE {System.out.println("<method_id>" + id2.getText() + $id3.getText()
- $id4.getText()
- "</access_flags>\n" +"0x" + id6.getText() + "");} | address id1=HEX_DOUBLE id2=HEX_DOUBLE id3=HEX_DOUBLE id4=HEX_DOUBLE {System.out.println("<method_id>" + id2.getText() + "</access_flags>\n" +"0x" + id4.getText() + "");} ;
encoded_arrays : address HEX_DOUBLE 'array item count:' DIGIT encoded_array+ ;
encoded_array
: address HEX_DOUBLE HEX_DOUBLE IDENT IDENT array_digit ':'
'"' IDENT '"' ;
proto_type_string : IDENT | IDENT ';' | IDENT '.' IDENT | IDENT '/' IDENT | IDENT '/' '<' IDENT '>' | '<' IDENT '>' '()' IDENT | IDENT '/' IDENT '/' IDENT ';' | '[' IDENT '/' IDENT '/' IDENT ';' | IDENT '()' IDENT '/' IDENT '/' IDENT ';'
| IDENT '/' IDENT '/' IDENT '.' IDENT | IDENT '/' IDENT '/' IDENT '/' IDENT
| IDENT '/' IDENT '/' IDENT '/' '<' IDENT '>' | IDENT '(' IDENT '/' IDENT '/' IDENT ';' ')' IDENT | IDENT '(' '[' IDENT '/' IDENT '/' IDENT ';' ')' IDENT | IDENT '(' IDENT ')' IDENT '/' IDENT '/' IDENT ';' | IDENT '(' IDENT '/' IDENT '/' IDENT ';' ')' IDENT '/' IDENT '/' IDENT ';'
hex_address: '0x' eight_hex ;
address : eight_hex ':' ;
eight_hex : HEX_DOUBLE HEX_DOUBLE HEX_DOUBLE HEX_DOUBLE ;
array_digit returns [String result] : id=ELEMENT {String str = id.getText(); $result = str.substring(1, str.length()-1);} ;
HEX_DOUBLE:
('0'..'9')('0'..'9')|('0'..'9')('A'..'F')|('A'..'F')('0'..'9')|('A
'..'F')('A'..'F');
MAGIC_NUM: 'dex\n035\0';
IDENT: ('a'..'z'|'A'..'Z')+;
DIGIT: ('0'..'9');
ELEMENT: ('[')('0'..'9')+(']');
CONST_4: 'const/4'; CONST_16: 'const/16';
CONST_HIGH_16: 'const/high16';
COMMENT: '//' ~( '\r' | '\n' )* {channel = HIDDEN;};
*WS*: (' ' | '\t' | '\n' | '\r' | '\f' | ',' | '-' | '*')+ {channel = HIDDEN;};`
清单 6-12 显示了来自清单 6-11 的语法的 XML 输出。这并不包括所有的 XML 节点,因为我们已经在第三章中介绍过了,而且篇幅很长..解析所有classes.dex文件而不仅仅是Casting.java的更大更完整的 DexToXML 可以在 Apress 网站([www.apress.com](http://www.apress.com))的源代码中找到。
清单 6-12。 DexToXML 输出
`
dex\n035\0 62 8B 44 18 DA A9 21 CA 9C 4F B4 C5 21 D7 77 BC 2A 18 4A 38 0D A2 AA FE 0x00000450 0x00000070 0x00000000 0x00000000 0x000003A4 0x0000001A 0x00000070 0x0000000A 0x000000D8 0x00000007 0x00000100 0x00000003 0x00000154 0x00000009 0x0000016C 0x00000001 0x000001B4 0x0000027C 0x000001D4 0 0x00000272 1` `0x0000027F 2 0x00000287 3 0x0000028A 4 0x00000298 5 0x0000029B 6 0x0000029E 7 0x000002A2 8 0x000002AD 9 0x000002B1 10 0x000002B5 11 0x000002CC 12 0x000002E0 13 0x000002F4` ` 14 0x0000030F 15 0x00000323 16 0x00000326 17 0x0000032A 18 0x0000033F 19 0x00000347 20 0x0000034F 21 0x00000357 22 0x0000035F 23 0x00000365 24 0x0000036A 25 0x00000373 ` ` 0 2 C 1 4 I 2 7 LCasting; 3 10 Ljava/io/PrintStream; 4 11 Ljava/lang/Object; 5 12 Ljava/lang/String; 6 13 Ljava/lang/StringBuilder; 7 14 Ljava/lang/System; 8 15 V 9` `17 Ljava/lang/String; 0 Ljava/lang/String; L 1 Ljava/lang/StringBuilder; LC 2 Ljava/lang/StringBuilder; LI 3 Ljava/lang/StringBuilder; LL 4 V V 5 V VL 6 V VL 0 Casting.ascStr Ljava/lang/String; 1` `Casting.chrStr Ljava/lang/String; 2 java/lang/System.out Ljava/io/PrintStream; `地塞米松资源
为了实现 Android 反编译器 DexToSource,这一节看三个例子,说明代码是如何编译到classes.dex文件中的;然后将它逆向工程回 Java,并编写 ANTLR 解析器来自动化这个过程。这三个例子是来自[第二章和第三章的Casting.java代码;Hello World 安卓;以及来自 WordPress Android 应用(一个开源 Android 应用)的if声明,可在[android.svn.wordpress.org/trunk/src/org/wordpress/android/](http://android.svn.wordpress.org/trunk/src/org/wordpress/android/)获得。
对每个例子的分析都从原始字节码开始,然后分解并解析成类似于原始 Java 源代码的东西。在分离字节码时,有两个资源非常有用:Google 在[www.netmite.com/android/mydroid/dalvik/docs/dalvik-bytecode.html](http://www.netmite.com/android/mydroid/dalvik/docs/dalvik-bytecode.html)为 Dalvik 虚拟机(DVM)提供的字节码;Gabor Paller 在他的博客中发表了精彩的“Dalvik 操作码”论文,你可以在[pallergabor.uw.hu/androidblog/dalvik_opcodes.html](http://pallergabor.uw.hu/androidblog/dalvik_opcodes.html)找到。
例 1:Casting.java
每个例子都从原始 Java 代码开始,然后是您想要从classes.dex逆向工程的字节码、解析器,最后是逆向工程的 Java 源代码。关于Casting.java代码,参见清单 6-13 。
清单 6-13。??Casting.java
`public class Casting {
static final String ascStr = "ascii "; static final String chrStr = " character ";
public static void main(String args[]){
for(char c=0; c < 128; c++) { System.out.println(ascStr + (int)c + chrStr + c); } } }`
将Casting.java编译成classes.dex并通过 dedexer 运行它会产生如清单 6-14 所示的字节码。
清单 6-14。Casting.ddx
`.class public Casting .super java/lang/Object .source Casting.java
.field static final ascStr Ljava/lang/String; = "ascii " .field static final chrStr Ljava/lang/String; = " character "
.method public ()V .limit registers 1 ; this: v0 (LCasting;) .line 1 invoke-direct {v0},java/lang/Object/ ; ()V return-void .end method
.method public static main([Ljava/lang/String;)V
.limit registers 5
; parameter[0] : v4 (Ljava/lang/String;)
.line 8
const/4 v0,0
l1fe:
const/16 v1,128
if-ge v0,v1,l252
.line 9
sget-object v1,java/lang/System.out
Ljava/io/PrintStream;
new-instance v2,java/lang/StringBuilder
invoke-direct {v2},java/lang/StringBuilder/ ;
()V
const-string v3,"ascii "
invoke-virtual
{v2,v3},java/lang/StringBuilder/append ;
append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v2
invoke-virtual
{v2,v0},java/lang/StringBuilder/append ;
append(I)Ljava/lang/StringBuilder;
move-result-object v2
const-string v3," character "
invoke-virtual
{v2,v3},java/lang/StringBuilder/append ;
append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v2
invoke-virtual
{v2,v0},java/lang/StringBuilder/append ;
append(C)Ljava/lang/StringBuilder;
move-result-object v2 invoke-virtual
{v2},java/lang/StringBuilder/toString ;
toString()Ljava/lang/String;
move-result-object v2
invoke-virtual {v1,v2},java/io/PrintStream/println
; println(Ljava/lang/String;)V
.line 8
add-int/lit8 v0,v0,1
int-to-char v0,v0
goto l1fe
l252:
.line 11
return-void
.end method`
字节码分析
在开始编写解析器之前,您需要理解字节码。[表 6-1 显示了原始字节码以及相应的操作码和操作数,以及程序计数器(PC)、v0、v1、v2 和 v3 DVM 寄存器中的运行计数。
解析器
Java 代码的大部分外壳,比如类名、字符串名、方法名、字段名等等,见Casting.ddx文件。6-14 中的清单可以使用清单 6-15 中的解析器进行转换。输出如清单 6-16 中的所示。
清单 6-15。 Casting.java没有字节码解析器
`grammar DexToSource;
options {language = Java;} @header {package com.riis.decompiler;} @lexer::header {package com.riis.decompiler;} @members{String flag_result = "";}
rule @after {System.out.println("}");} : class_name super_ source fields methods+ ;
class_name : CLASS f1=flags id2=IDENT {System.out.println(id2.text
- " {");}
;
super_: SUPER package_; source: SOURCE IDENT '.java';
fields: field+ ;
methods: method_start method_end;
field: FIELD f1=flags id2=IDENT p1=package_ ';' '=' '"' id4=IDENT '"' {System.out.println(p1.result + " " + id4.text + """ );} ;
method_start: METHODSTRT f1=flags INIT p1=params r1=return_ {System.out.println(r1.result + " init "
- p1.result + " {");} | *METHODSTRT* f1=flags id1=*IDENT* p1=params r1=return_ {System.out.println(f1.text + " " + id1.text + " (" + $p1.result + ") {");} ;
method_end @after {System.out.println("}");} : METHODEND ;
flags: flag+;
flag returns [String flag_result] : f1='public' {flag_result += f1.text;} | f1='static' {flag_result += f1.text;} | f1='final' {flag_result += $f1.text;} ;
params returns [String result] : '(' ')' {result = "()";} | '(' '[L' id1=package_ ';' ')' {result = $id1.result + " args[]";} //([Ljava/lang/String;) ;
package_ returns [String result] : IDENT '/' IDENT '/' id1=IDENT {$result = id1.getText();} ;
return_ returns [String result]
: 'V' {$result = "void";} ;
CLASS: '.class'; PUBLIC: 'public'; STATIC: 'static'; FINAL: 'final'; SUPER: '.super'; SOURCE: '.source'; FIELD: '.field'; METHODSTRT: '.method'; METHODEND: '.end method'; INIT: ''; IDENT: ('a'..'z'|'A'..'Z')+; COMMENT: '//' ~( '\r' | '\n' )* {channel = HIDDEN;}; *WS*: (' ' | '\t' | '\n' | '\r' | '\f' | ',')+ {channel = HIDDEN;};`
在解析任何字节码之前,文件的结构如清单 6-16 所示。
清单 6-16。 Casting.java不带 Pytecode
public class Casting { static final String ascStr = "ascii" static final String chrStr = "character" public void init () { } public static void main (String args[]) { } }
但是 Java 代码的核心逻辑在 DDX 文件末尾的操作码中。看表 6-1 ,操作码如何映射到目标 Java 代码Casting.java应该更清楚了;参见清单 6-13 ,这是你在整本书中一直使用的。
方法代码有两部分:for循环和for循环中的System.out.println语句。从解析器的角度来看,您可以创建for循环,如清单 6-17 所示。注意,保留的关键字如return有一个下划线,所以生成的 ANTLR 代码编译时没有任何错误。
清单 6-17。 for循环解析器
`rule: class_name super_ source fields methods+ ;
class_name : CLASS flags IDENT ;
super_: SUPER package_; source: SOURCE IDENT '.java';
fields: field+ ;
field: FIELD flags IDENT package_ ';' '=' '"' IDENT '"';
methods: method_start scrap* method_end | method_start scrap* for_start for_body scrap* for_end method_end ;
method_start: METHODSTRT flags INIT params return_ | METHODSTRT flags IDENT params return_ ;
method_end: METHODEND;
for_start : put_in_reg label put_in_reg if_ge scrap*;
const_string: CONST_STRING reg ddx_string;
ddx_string: '"' IDENT '"';
for_end : add_int int_to_char goto_ label scrap*;
new_instance: NEW_INSTANCE reg package_ scrap*;
add_int: ADD_INT reg reg DIGIT;
int_to_char: INT_TO_CHAR reg reg;
goto_: GOTO label;
if_ge: IF_GE reg reg label;
put_in_reg: const_ reg DIGIT;
reg_args: '{' reg+ '}';
label: LABEL | LABEL ':' ;
invoke_direct: INVOKE_DIRECT regs package_ ;
flags: flag+;
flag : f1='public' | f1='static'
| f1='final'
;
params : '(' ')' | '(' '[L' package_ ';' ')' | '(' IDENT ';' ')' | IDENT '(' package_ ';' ')' | IDENT '(' IDENT ')' | IDENT '(' ')' ;
package_ : IDENT '/' IDENT '/' IDENT | IDENT '/' IDENT '/' IDENT '/' IDENT | IDENT '/' IDENT '/' IDENT '.' IDENT | IDENT '/' IDENT '/' IDENT '/' '' | 'L' IDENT '/' IDENT '/' IDENT ;
return_ : 'V';
regs: '{' reg+ '}';
reg : 'v' DIGIT;
const_ : CONST_4 | CONST_16 | CONST_HIGH_16 ;
scrap: LIMIT REGISTERS DIGIT | ';' 'this:' reg params | LINE DIGIT+ | invoke_direct ';' '' params return_ | RETURN_VOID | ';' 'parameter[' DIGIT ']' ':' reg params ;
CLASS: '.class';
PUBLIC: 'public';
STATIC: 'static';
FINAL: 'final';
SUPER: '.super';
SOURCE: '.source';
FIELD: '.field';
METHODSTRT: '.method';
METHODEND: '.end method';
INIT: ''; LIMIT: '.limit';
REGISTERS: 'registers';
LINE: '.line';
INVOKE_DIRECT: 'invoke-direct';
RETURN_VOID: 'return-void';
IF_GE: 'if-ge';
ADD_INT: 'add-int/lit8';
INT_TO_CHAR: 'int-to-char';
GOTO: 'goto';
CONST_STRING: 'const-string';
CONST_4: 'const/4';
CONST_16: 'const/16';
CONST_HIGH_16: 'const/high16';
DIGIT: ('0'..'9')+;
IDENT: ('a'..'z'|'A'..'Z')+;
LABEL: 'l' ('0'..'9'|'a'..'f')('0'..'9'|'a'..'f')('0'..'9'|'a'..'f');
COMMENT: '//' ~( '\r' | '\n' )* {channel = HIDDEN;};
WS: (' ' | '\t' | '\n' | '\r' | '\f' | ',')+ {channel = HIDDEN;};`
lexer 标记是大写的,解析器规则是小写的。for_start是一个大于或等于条件,后跟一个标签,如果条件为真,则跳转到该标签。正如您在细分表中看到的,for_end规则给变量c加 1,然后跳回到for_start条件。注意,这不是通用的:它不适用于任何其他 for 循环。我展示它是为了让您了解如何组装解析器。
接下来,您需要为System.out.println或for_body语句添加解析器代码,将它们放在for_loop规则的for_start和for_end部分之间;参见清单 6-18 。
清单 6-18。 Casting.java解析器
`for_body: sget stmt_builder invoke_virtual;
stmt_builder returns : new_instance invoke_move+;
invoke_move
: invoke_virtual move_result
| const_string invoke_virtual move_result
; move_result: MOVE_RESULT_OBJECT reg;
const_string: CONST_STRING reg ddx_string;
ddx_string: '"' IDENT '"';
new_instance: NEW_INSTANCE reg package_ scrap*;
sget : SGET_OBJECT reg package_ package_ ';';
invoke_virtual : INVOKE_VIRTUAL reg_args package_ ';' params 'V' | INVOKE_VIRTUAL reg_args package_ ';' params package_ ';' ;`
现在可以解析操作码了,您可以添加自己的println语句来输出 Java 代码;参见清单 6-19 。尽管这个清单很长,但它是所提供的最完整的解析器之一,因此完整地回顾它是很重要的。
清单 6-19。 Casting.ddx解析器
`grammar DexToSource;
options {language = Java;} @header {package com.riis.decompiler;} @lexer::header {package com.riis.decompiler;} @members{String flag_result = "";}
rule @after {System.out.println("}");} : class_name super_ source fields methods+ ;
class_name : CLASS f1=flags id2=IDENT {System.out.println(id2.text
- " {");} ;
super_: SUPER package_; source: SOURCE IDENT '.java'; fields: field+ ;
field: FIELD f1=flags id2=IDENT p1=package_ ';' '=' '"' id4=IDENT
'"'
{System.out.println(p1.result + " " +
id4.text + """ );}
; methods: method_start scrap* method_end
| method_start scrap* for_start for_body scrap* for_end
method_end
;
method_start: METHODSTRT f1=flags INIT p1=params r1=return_ {System.out.println(r1.result + " init "
- p1.result + " {");} | *METHODSTRT* f1=flags id1=*IDENT* p1=params r1=return_ {System.out.println(f1.text + " " + id1.text + " (" + $p1.result + ") {");} ;
method_end @after {System.out.println("}");} : METHODEND
for_start : id1=put_in_reg label id2=put_in_reg if_ge scrap* {System.out.println("for(a=" + $id1.result + "; a < "
- $id2.result + "; a++){");} ;
for_body: id1=sget id3=stmt_builder id2=invoke_virtual {System.out.println(id2.result + "(" + $id3.result);} ;
stmt_builder returns [String result] : new_instance id1=invoke_move id2=invoke_move id3=invoke_move id4=invoke_move id5=invoke_move {id1.result + "" + " + id3.result + "" +" + $id4.result + ")";} ;
invoke_move returns [String result] : id1=invoke_virtual move_result {id1.result;} | id1=const_string invoke_virtual move_result {id1.result;} ;
move_result: MOVE_RESULT_OBJECT reg ;
const_string returns [String result] : CONST_STRING reg id1=ddx_string {id1.result;}
;
ddx_string returns [String result] : '"' id1=IDENT '"' {id1.getText();} ;
for_end : add_int int_to_char goto_ label scrap* {System.out.println("}");} ;
new_instance: NEW_INSTANCE reg package_ scrap*;
sget returns [String result] : SGET_OBJECT reg id1=package_ id2=package_ ';' {id1.result;} ;
invoke_virtual returns [String result] : INVOKE_VIRTUAL reg_args id1=package_ ';' params 'V' {id1.result;} | INVOKE_VIRTUAL reg_args package_ ';' id1=params package_ ';' {if (id1.result.compareTo("I") == 0) { result = "(int)a"; } else {$result = "(char)a";}} ;
add_int: ADD_INT reg reg DIGIT ;
int_to_char: INT_TO_CHAR reg reg ;
goto_: GOTO label ;
if_ge: IF_GE reg reg label ;
put_in_reg returns [String result] : const_ reg id1=DIGIT {id1.getText();} ;
reg_args: '{' reg+ '}' ;
label: LABEL
| LABEL ':'
; invoke_direct: INVOKE_DIRECT regs package_
;
flags: flag+;
flag returns [String flag_result] : f1='public' {flag_result += f1.text;} | f1='static' {flag_result += f1.text;} | f1='final' {flag_result += $f1.text;} ;
params returns [String result] : '(' ')' {result = "()";} | '(' '[L' id1=package_ ';' ')' {result = id1.result + " args[]";} | '(' id2=*IDENT* ';' ')' {result = $id2.getText();}
| IDENT '(' id3=package_ ';' ')' {id3.result;}
| IDENT '(' id4=IDENT ')' {id4.getText();}
| IDENT '(' ')' {$result = "()";} ;
package_ returns [String result] : IDENT '/' IDENT '/' id1=IDENT {result = id1.getText();} | *IDENT* '/' *IDENT* '/' *IDENT* '/' id1=*IDENT* {result = id1.getText();} | IDENT '/' IDENT '/' id1=IDENT '.' id2=IDENT{result = id1.getText() + "." + id2.getText();} | *IDENT* '/' *IDENT* '/' *IDENT* '/' '<init>' {result = "init";} | 'L' IDENT '/' IDENT '/' id1=IDENT {id1.getText();} ;
return_ returns [String result] : 'V' {$result = "void";} ;
regs: '{' reg+ '}';
reg : 'v' DIGIT;
const_ : CONST_4
| CONST_16 | CONST_HIGH_16
;
scrap: LIMIT REGISTERS DIGIT | ';' 'this:' reg params | LINE DIGIT+ | invoke_direct ';' '' params return_ | RETURN_VOID | ';' 'parameter[' DIGIT ']' ':' reg params ;
CLASS: '.class'; PUBLIC: 'public'; STATIC: 'static'; FINAL: 'final'; SUPER: '.super'; SOURCE: '.source'; FIELD: '.field'; METHODSTRT: '.method'; METHODEND: '.end method'; INIT: ''; LIMIT: '.limit'; REGISTERS: 'registers'; LINE: '.line'; INVOKE_DIRECT: 'invoke-direct'; INVOKE_VIRTUAL: 'invoke-virtual'; MOVE_RESULT_OBJECT: 'move-result-object'; NEW_INSTANCE: 'new-instance'; RETURN_VOID: 'return-void'; IF_GE: 'if-ge'; SGET_OBJECT: 'sget-object'; ADD_INT: 'add-int/lit8'; INT_TO_CHAR: 'int-to-char'; GOTO: 'goto'; CONST_STRING: 'const-string'; CONST_4: 'const/4'; CONST_16: 'const/16'; CONST_HIGH_16: 'const/high16'; DIGIT: ('0'..'9')+; IDENT: ('a'..'z'|'A'..'Z')+; LABEL: 'l' ('0'..'9'|'a'..'f')('0'..'9'|'a'..'f')('0'..'9'|'a'..'f'); COMMENT: '//' ~( '\r' | '\n' )* {channel = HIDDEN;}; *WS*: (' ' | '\t' | '\n' | '\r' | '\f' | ',')+ {channel = HIDDEN;};`
Java
生成的 Java 代码如清单 6-20 所示。注意,dedexer 对操作码做了一些细微的修改,因此您丢失了print语句中的变量。Java 代码也需要一些标签来提高可读性,但是您应该看到classes.dex已经被转换回 Java。
清单 6-20。 生成Casting.java
public class Casting { static final String ascStr = "ascii" static final String chrStr = "character" public void init () { } public static void main (String args[]) { for(a=0; a < 128; a++){ System.out.println("ascii" + (int)a + "character" +(char)a) } } }
例 2: Hello World
Android SDK 附带了一个简单的 Hello World 应用,如图 6-2 所示。下一个例子获取代码并对其进行逆向工程。
**图 6-2。**你好安卓屏幕
原始的 Java 代码如清单 6-21 所示。
清单 6-21。??Hello.java
`package org.example.Hello;
import android.app.Activity; import android.os.Bundle;
public class Hello extends Activity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); } }`
相应的 DDX 文件如列表 6-22 所示。
清单 6-22。??HelloWorld.ddx
.class public org/example/Hello/Hello .super android/app/Activity .source Hello.java
`.method public ()V
.limit registers 1
; this: v0 (Lorg/example/Hello/Hello;)
.line 6
invoke-direct {v0},android/app/Activity/ ; ()V
return-void
.end method
.method public onCreate(Landroid/os/Bundle;)V
.limit registers 3
; this: v1 (Lorg/example/Hello/Hello;)
; parameter[0] : v2 (Landroid/os/Bundle;)
.line 10
invoke-super {v1,v2},android/app/Activity/onCreate ;
onCreate(Landroid/os/Bundle;)V
.line 11
const/high16 v0,32515
invoke-virtual
{v1,v0},org/example/Hello/Hello/setContentView ;
setContentView(I)V
.line 12 return-void
.end method`
字节码分析
表 6-2 解释了来自清单 6-22 的每个字节码段的含义,并给出了 v0、v1 和 v2 DVM 寄存器中值的运行计数。
解析器
为了解析HelloWorld,您需要添加对invoke-super和new const16/high关键字以及contentView结构的支持。解析器如清单 6-23 所示。
清单 6-23。 Hello World 和 Casting Parser
rule : for_loop return_ `| super_stmt return_
;
super_stmt : invoke_super invoke_virtual_content
; for_loop : put_in_reg+ for_start println for_end
;
for_start: 'if-ge' reg reg HEX_DIGIT+ ;
for_end: add_int int_to_char goto_ ;
put_in_reg : const_ reg HEX_DIGIT+ ;
reg : 'v' HEX_DIGIT ;
const_ : CONST_4 | CONST_16 | CONST_HIGH_16 ;
add_int : ADD_INT reg reg HEX_DIGIT ;
int_to_char: 'int-to-char' reg reg ;
goto_: 'goto' HEX_DIGIT+ ;
return_: 'return-void' ;
println: sget new_instance invoke_direct const_string invoke_virtual_move+ ;
sget: SGET reg obj ;
new_instance: NEW_INSTANCE reg obj ;
invoke_direct: INVOKE_DIRECT obj param ;
invoke_super: INVOKE_SUPER param ;
invoke_virtual_move: invoke_virtual | invoke_virtual move_result_object | invoke_virtual move_result_object const_string
;
invoke_virtual_content: content_view invoke_virtual ;
content_view: const_ reg HEX_DIGIT+ ;
invoke_virtual: INVOKE_VIRTUAL obj param | INVOKE_VIRTUAL param ;
move_result_object: MOVE_RESULT_OBJECT reg ;
const_string: CONST_STRING reg obj ;
obj : IDENT '[' HEX_DIGIT+ ']' ;
param : '{' reg '}' | '{' reg reg '}' ;
INVOKE_DIRECT: 'invoke-direct'; INVOKE_SUPER: 'invoke-super'; INVOKE_VIRTUAL: 'invoke-virtual'; NEW_INSTANCE: 'new-instance'; MOVE_RESULT_OBJECT: 'move-result-object'; SGET: 'sget-object'; CONST_STRING: 'const-string'; HEX_DIGIT: ('0'..'9'|'A'..'F'|'a'..'f'); IDENT: ('a'..'z')+; ADD_INT: 'add-int/lit8'; CONST_4: 'const/4'; CONST_16: 'const/16'; CONST_HIGH_16: 'const/high16';`
Java
生成的 Java 代码如清单 6-24 所示。classes.dex告诉你当方法第一次被调用时savedInstanceState在 v2 中,并且setContentView正在调用R.layout.Main的数值。
清单 6-24。 生成HelloWorld.java
super.onCreate(savedInstanceState); setContentView(32515);
例 3: if 语句
为了完成这些例子,你需要一个if语句。WordPress 的开源 Android 应用是一个很好的资源,因为它是一个专业的应用,可以让你访问源代码。escapeHTML.java中的清单 6-25 有一个简单的if条件。
清单 6-25。 escapeHTML法
public static void escapeHtml(Writer writer, String string) throws IOException { if (writer == null ) { throw new IllegalArgumentException ("The Writer must not be null."); } if (string == null) { return; } Entities.HTML40_escape.escape(writer, string); }
来自 dex 文件的字节码如清单 6-26 中的所示。
清单 6-26。??escapeHTML.ddx
.method public static escapeHtml(Ljava/io/Writer;Ljava/lang/String;)V .throws Ljava/io/IOException; .limit registers 4 ; parameter[0] : v2 (Ljava/io/Writer;) ; parameter[1] : v3 (Ljava/lang/String;) .line 27 if-nez v2,l7ba4c .line 28 new-instance v0,java/lang/IllegalArgumentException const-string v1,"The Writer must not be null." invoke-direct {v0,v1},java/lang/IllegalArgumentException/<init> ; <init>(Ljava/lang/String;)V throw v0 l7ba4c: .line 30 if-nez v3,l7ba52 l7ba50: .line 34 return-void l7ba52: .line 33 sget-object v0,org/wordpress/android/util/Entities.HTML40_escape Lorg/wordpress/android/util/Entities; invoke-virtual {v0,v2,v3},org/wordpress/android/util/Entities/escape; escape(Ljava/io/Writer;Ljava/lang/String;)V goto l7ba50 .end method
字节码分析
表 6-3 解释了清单 6-23 中每个字节码段的含义,以及 v0、v1、v2 和 v3 DVM 寄存器中值的运行计数。字节码逆向工程中唯一真正的难题是最后一条指令:28fa。28操作码翻译成带有操作数fa的goto。操作数以二进制补码格式存储(更多信息见[en.wikipedia.org/wiki/Twos_complement](http://en.wikipedia.org/wiki/Twos_complement))。要获得地址,您需要将每个十六进制数字转换为二进制,翻转这些位,然后加 1。在这个例子中,fa = 11111010,当比特翻转时,它变成 00000101。如果加上 1,数字就是 00000110 或十进制 6。回溯六个单词,你就有了你的地址:000c。
解析器
为了解析if not equal then分支和goto语句,您需要将它们添加到解析器中。你还需要给invoke-virtual语句添加更多的参数选项;参见清单 6-27 。
清单 6-27。 Hello World、Casting 和 If 解析器
`rule : for_loop return_ | super_stmt return_ | if_stmt+ ;
if_stmt: if_ new_instance const_string invoke_direct throw_ | if_ return_ goto_stmt ;
goto_stmt: sget invoke_virtual goto_ ;
if_ : IF_NEZ reg HEX_DIGIT+ ;
throw_ : THROW reg
; super_stmt : invoke_super invoke_virtual_content
;
for_loop : put_in_reg+ for_start println for_end ;
for_start: 'if-ge' reg reg HEX_DIGIT+ ;
for_end: add_int int_to_char goto_ ;
put_in_reg : const_ reg HEX_DIGIT+ ;
reg : 'v' HEX_DIGIT ;
const_ : CONST_4 | CONST_16 | CONST_HIGH_16 ;
add_int : ADD_INT reg reg HEX_DIGIT ;
int_to_char: 'int-to-char' reg reg ;
goto_: 'goto' HEX_DIGIT+ ;
return_: 'return-void' ;
println: sget new_instance invoke_direct const_string invoke_virtual_move+ ;
sget: SGET reg obj ;
new_instance: NEW_INSTANCE reg obj ;
invoke_direct: INVOKE_DIRECT obj param
| INVOKE_DIRECT param
; invoke_super: INVOKE_SUPER param
;
invoke_virtual_move: invoke_virtual | invoke_virtual move_result_object | invoke_virtual move_result_object const_string ;
invoke_virtual_content: content_view invoke_virtual ;
content_view: const_ reg HEX_DIGIT+ ;
invoke_virtual: INVOKE_VIRTUAL obj param | INVOKE_VIRTUAL param | INVOKE_VIRTUAL param obj ;
move_result_object: MOVE_RESULT_OBJECT reg ;
const_string: CONST_STRING reg obj ;
obj : IDENT '[' HEX_DIGIT+ ']' ;
param : '{' reg '}' | '{' reg reg '}' | '{' reg reg reg '}' ;
CONST_STRING: 'const-string';
IF_NEZ: 'if-nez';
INVOKE_DIRECT: 'invoke-direct';
INVOKE_SUPER: 'invoke-super';
INVOKE_VIRTUAL: 'invoke-virtual';
NEW_INSTANCE: 'new-instance';
MOVE_RESULT_OBJECT: 'move-result-object';
SGET: 'sget-object';
THROW: 'throw';
HEX_DIGIT: ('0'..'9'|'A'..'F'|'a'..'f');
IDENT: ('a'..'z')+;
ADD_INT: 'add-int/lit8';
CONST_4: 'const/4';
CONST_16: 'const/16';
CONST_HIGH_16: 'const/high16'; WS: (' ' | '\t' | '\n' | '\r' | '\f' | ',')+ {$channel = HIDDEN;};`
Java
生成的 Java 代码如清单 6-28 所示。
清单 6-28。 生成escapeHTML.java
if (writer == null ) { throw new IllegalArgumentException ("The Writer must not be null."); } if (string == null) { return; } Entities.HTML40_escape.escape(writer, string);
重构
当您构建解析器时,很快就可以清楚地看到,您需要将指令放入不同的系列中。否则,解析器可能会变得不可管理,您也将不得不对结构进行硬编码以匹配输入文件。没有一些重构,你永远不会有一个通用的解决方案来逆向工程 Android APKs。Gabor Paller 对指令进行了拆分,如表 6-4 所示。
重构后的解析器如清单 6-29 所示。现在您已经有了示例中的一个小型测试代码套件,您可以用它来测试是否有任何更改破坏了解析器。
**清单 6-29。**重构的解析器
`rule : for_loop return_ | stmt return_ | stmt+ ;
for_loop : put_in_reg+ for_start stmt for_end ;
stmt : if_stmt | super_stmt | println ;
if_stmt: if_ new_instance const_string invoke throw_ | if_ return_ goto_stmt ;
println: sget new_instance invoke const_string invoke_move+ ;
super_stmt : invoke invoke_content ;
goto_stmt: sget invoke goto_ ;
for_start: 'if-ge' reg reg HEX_DIGIT+ ;
for_end: add_int int_to_char goto_ ;
put_in_reg : const_ reg HEX_DIGIT+ ;
add_int : ADD_INT reg reg HEX_DIGIT ;
int_to_char: 'int-to-char' reg reg ;
invoke_move: invoke
| invoke move_result_object
| invoke move_result_object const_string
; invoke_content: content_view invoke
;
invoke : invoke_virtual
| invoke_direct
| invoke_super
; invoke_virtual: INVOKE_VIRTUAL obj param
| INVOKE_VIRTUAL param
| INVOKE_VIRTUAL param obj
;
invoke_direct: INVOKE_DIRECT obj param | INVOKE_DIRECT param ;
invoke_super: INVOKE_SUPER param ;
content_view: const_ reg HEX_DIGIT+ ;
sget: SGET reg obj ;
new_instance: NEW_INSTANCE reg obj ;
if_ : IF_NEZ reg HEX_DIGIT+ ;
reg : 'v' HEX_DIGIT ;
const_ : CONST_4 | CONST_16 | CONST_HIGH_16 ;
move_result_object: MOVE_RESULT_OBJECT reg ;
const_string: CONST_STRING reg obj ;
obj : IDENT '[' HEX_DIGIT+ ']' ;
//helper functions param : '{' reg+ '}' ;
goto_: 'goto' HEX_DIGIT+ ;
throw_ : THROW reg ;
return_: 'return-void' ;
CONST_STRING: 'const-string'; IF_NEZ: 'if-nez'; INVOKE_DIRECT: 'invoke-direct'; INVOKE_SUPER: 'invoke-super'; INVOKE_VIRTUAL: 'invoke-virtual'; NEW_INSTANCE: 'new-instance'; MOVE_RESULT_OBJECT: 'move-result-object'; SGET: 'sget-object'; THROW: 'throw'; HEX_DIGIT: ('0'..'9'|'A'..'F'|'a'..'f'); IDENT: ('a'..'z')+; ADD_INT: 'add-int/lit8'; CONST_4: 'const/4'; CONST_16: 'const/16'; CONST_HIGH_16: 'const/high16'; WS: (' ' | '\t' | '\n' | '\r' | '\f' | ',')+ {$channel = HIDDEN;};`
目前,解析器只处理 3 个简单的程序结构,没有涵盖所有 Dalvik 字节码。如果包括在内的话,这一章会比书的其余部分更长。但是对于那些想了解更多的人来说,完整的反编译器可以在 Apress 网站([www.apress.com](http://www.apress.com))上找到,还有一个更大的测试套件和如何运行它的说明。
总结
在本章中,您已经使用 dedexer 输出创建了 DexToXML 和 DexToSource,这两个输出都可以在 Apress 网站上找到。这些可以用来将classes.dex文件分别分解成 XML 和 Java 源代码。对于更复杂的测试套件示例,网站上的 DexToSource 代码使用 AST 和StringTemplate s。
下一章以支持和反对混淆的案例研究以及使用开源或商业混淆器混淆代码的最佳实践来结束本书。
七、非礼勿听、非礼勿视:案例研究
你现在几乎已经到了旅程的终点。到目前为止,您应该对如何反编译以及如何尝试保护代码的总体原则有了很好的理解。话虽如此,我从与客户和同事的合作中发现,即使你理解了反编译和模糊处理的真正含义,它也不能帮助你弄清楚可以采取什么实际措施来保护你的代码。一知半解往往能创造出更多的问题而不是答案。
正如 Java 能力中心(JCC)在其 deCaf 网站 FAQ 上所说:
真的没有人能够反编译我的无咖啡因保护应用吗?不。脱咖啡因不会使反编译成为不可能。这使得真的没有人能够反编译我的无咖啡因保护的应用变得很困难。让反编译不可能是不可能的。
本书的目标是帮助提高门槛,让任何人都更难反编译你的代码。目前在 Android 世界里,似乎有一种“听不到邪恶,看不到邪恶”的反编译方法,但这迟早会改变。读完这本书后,你应该预先得到警告,更重要的是,在给定的具体情况下,你应该预先准备好保护代码的最佳实用方法。
本章通过一个案例研究来帮助解决这个难题。几乎每个试图保护自己代码的人都会使用某种混淆工具。案例研究更详细地研究了这种方法,以帮助您得出如何最好地保护代码的结论。它具有以下格式:
- 问题描述
- 神话
- 建议的解决方案:ProGuard 和 DashO
混淆案例研究
对于许多人来说,担心有人反编译他们的 Android 应用远不是他们最担心的事情。它的排名远远低于安装最新版本的 Maven 或 Ant。当然,他们想防止反编译,但是没人有时间——而且 ProGuard 不处理这个问题吗?
在这种情况下有两个简单的选择:使用模糊处理来保护应用,或者忽略反编译,就好像这不是问题一样。当然,由于显而易见的原因,后者并不是一个值得推荐的选择。
神话
这些年来,我听到了许多不同的关于保护你的代码是否有意义的争论。今天最常见的一条是,如果你创建了一个好的 Android 应用,并继续改进它,这将保护你免受任何人反编译你的代码。人们普遍认为,如果你编写了好的应用,源代码会自我保护——升级和良好的支持是比使用模糊处理或本书中讨论的任何其他技术更好的保护代码的方式。
其他的争论是软件开发是关于你如何应用你的知识,而不是使用别人的应用。如今的原始代码可能来自一个描述良好的设计模式,所以没人在乎它是否被黑了。所有的开发人员(无论如何是好的)在事情完成后总能想到更好的方法,所以为什么要担心呢?很有可能,如果有人缺乏想象力,不得不求助于窃取您的代码,他们将无法在代码的基础上进行构建,并将其转化为有用的东西。而且你也不可能在自己的代码开发出来六个月后阅读它,那么其他人又如何理解它呢?
混乱的代码也很难调试。来自现场的错误报告需要追溯到正确的方法,以便开发人员可以调试和修复代码。如果处理不当,这可能会成为维护的噩梦,并使支持变得困难。
但问题肯定是有人破解了程序——这在 iOS 和 Android 上都可能发生。报纸上并不全是关于人们反编译一个产品并把它重新包装成他们自己的产品的报道;我们总是听到最新的微软漏洞,所以这不成问题。对我来说,这个论点对运行在 web 服务器上的代码有效,但对运行在 Android 设备上的代码无效。在第四章中,您看到了在 APK 中获取代码和资源是多么容易。如果它包含任何访问后端系统的线索,比如 API 密钥或数据库登录,或者如果您的应用有任何需要保护的客户信息,那么您有责任为您的客户采取基本步骤来保护您的代码。
如果使用正确,模糊处理会显著提高门槛,阻止大多数人恢复您的源代码。本章的案例研究使用了上一章中的开源 WordPress Android 应用作为一个很好的混淆示例应用。因为它是开源的,所以你有原始的源代码,你可以和混淆后的代码进行比较,看看混淆是否有效。案例研究考察了 ProGuard(Android SDK 附带的)和 DashO(一个商业混淆器)如何管理类文件。
从[android.svn.wordpress.org/](http://android.svn.wordpress.org/)下载 WordPress 源代码。案例研究使用了 2012 年 3 月 17 日的版本。
使用android update project为您的环境更新项目:
android update project -t android-15 -p ./
解决方案 1:Prog guard
默认情况下,ProGuard 不打开。要为混淆启用 ProGuard,请编辑project.properties文件并添加以下行:
proguard.config=proguard.cfg
我们将在本章后面更详细地介绍proguard.cfg中的设置。只有生产或发布的 apk 会被混淆,所以确保在AndroidManifest.xml文件中android:debuggable标志被设置为false。使用ant release命令编译应用,假设 Ant 是您的构建工具。
SDK 输出
ProGuard 在 Java jar 文件被转换成classes.dex文件之前对其进行模糊处理。如果你使用 Ant,原始文件和混淆文件可以在bin\proguard文件夹中找到;如果你使用 Eclipse,可以在project文件夹下找到\proguard。
ProGuard 还输出以下文件:
dump.txtseeds.txtusage.txtmapping.txt
有用的是一个类似于代码覆盖工具的混淆覆盖工具,向您显示有多少代码被混淆。但是这样的工具还不存在,所以这些文件是最接近你拥有的覆盖工具。
包含类文件中所有信息的输出,不像 Java 类文件反汇编器;这对你的目的没有多大帮助。seeds.txt列出没有混淆的类和方法。理解为什么一些代码是模糊的而另一些代码不是非常重要;稍后在“复查你的工作”部分会有更多的介绍。但是你需要检查,例如,带有你的 API 键的方法不在seeds.txt中,因为否则它们将不会受到任何保护。
ProGuard 不仅会混淆 jar 文件,还会通过删除任何日志文件、类或在原始代码中但从未被调用过的代码来缩小 jar 文件,等等。usage.txt列出从原始 jar 中去除的所有不必要的信息。因为存储在 Android 设备上非常珍贵,这本身就是在代码上使用混淆器的一个很好的理由。但是要小心,它不会删除您可能想要保留的代码。
mapping.txt可能是这个目录中最有用的文件,因为它将原始方法名映射到模糊的方法名。与大多数混淆器一样,ProGuard 大量重命名方法;如果您需要在字段中进行任何调试,那么mapping.txt就有必要追溯到最初的方法。您将在下一节中使用它来看看模糊处理对 WordPress 应用有多有效。
清单 7-1 ,使用 ProGuard 4.4,显示了构建期间的Ant输出;这对于查看 ProGuard 做了多少工作也很有用。如果obfuscate部分是空白的,你可以确定 ProGuard 没有被正确地调用。如果您自己尝试这样做,不要担心数字是否略有不同:您可能使用的是更高版本的 ProGuard 和/或 WordPress 代码。
清单 7-1。 蚂蚁输出
-obfuscate: [mkdir] Created dir: G:\clients\apress\chap7\wordpress\bin\proguard [jar] Building jar: G:\clients\apress\chap7\wordpress\bin\proguard\original.jar [proguard] ProGuard, version 4.4 [proguard] ProGuard is released under the GNU General Public License. The authors of all [proguard] programs or plugins that link to it (com.android.ant, ...) therefore [proguard] must ensure that these programs carry the GNU General Public License as well. [proguard] Reading input... [proguard] Reading program jar [G:\clients\apress\chap7\wordpress\bin\proguard\original.jar] [proguard] Reading program jar [G:\clients\apress\chap7\wordpress\libs\CWAC-AdapterWrapper.jar] [proguard] Reading program jar [G:\clients\apress\chap7\wordpress\libs\CWAC-Bus.jar] [proguard] Reading program jar [G:\clients\apress\chap7\wordpress\libs\CWAC-Task.jar] [proguard] Reading program jar [G:\clients\apress\chap7\wordpress\libs\android-support-v4.jar] [proguard] Reading program jar [G:\clients\apress\chap7\wordpress\libs\httpmime-4.1.2.jar] [proguard] Reading program jar [G:\clients\apress\chap7\wordpress\libs\tagsoup-1.2.1.jar] [proguard] Reading library jar [C:\Program Files (x86)\Android\android-sdk\platforms\android-14\android.jar] [proguard] Initializing... [proguard] Note: the configuration refers to the unknown class 'com.android.vending.licensing.ILicensingService' [proguard] Note: there were 1 references to unknown classes. [proguard] You should check your configuration for typos. [proguard] Ignoring unused library classes... [proguard] Original number of library classes: 3133 [proguard] Final number of library classes: 888 [proguard] Printing kept classes, fields, and methods... [proguard] Shrinking... [proguard] Printing usage to [G:\clients\apress\chap7\wordpress\bin\proguard\usage.txt]... [proguard] Removing unused program classes and class elements... [proguard] Original number of program classes: 644
[proguard] Final number of program classes: 469 [proguard] Optimizing... [proguard] Number of finalized classes: 331 [proguard] Number of vertically merged classes: 0 (disabled) [proguard] Number of horizontally merged classes: 0 (disabled) [proguard] Number of removed write-only fields: 0 (disabled) [proguard] Number of privatized fields: 520 (disabled) [proguard] Number of inlined constant fields: 1196 (disabled) [proguard] Number of privatized methods: 163 [proguard] Number of staticized methods: 61 [proguard] Number of finalized methods: 1062 [proguard] Number of removed method parameters: 98 [proguard] Number of inlined constant parameters: 61 [proguard] Number of inlined constant return values: 15 [proguard] Number of inlined short method calls: 9 [proguard] Number of inlined unique method calls: 169 [proguard] Number of inlined tail recursion calls: 2 [proguard] Number of merged code blocks: 6 [proguard] Number of variable peephole optimizations: 1434 [proguard] Number of arithmetic peephole optimizations: 0 (disabled) [proguard] Number of cast peephole optimizations: 31 [proguard] Number of field peephole optimizations: 3 [proguard] Number of branch peephole optimizations: 416 [proguard] Number of simplified instructions: 196 [proguard] Number of removed instructions: 1074 [proguard] Number of removed local variables: 184 [proguard] Number of removed exception blocks: 8 [proguard] Number of optimized local variable frames: 493 [proguard] Shrinking... [proguard] Removing unused program classes and class elements... [proguard] Original number of program classes: 469 [proguard] Final number of program classes: 455
再次检查你的工作
为了了解 ProGuard 有多有效,让我们看看它对你在第六章中使用的EscapeUtils.java方法做了什么。清单 7-2 显示了原始的 WordPress 源代码。
清单 7-2。 原文EscapeUtils.java代号
package org.wordpress.android.util;
`import java.io.IOException; import java.io.StringWriter; import java.io.Writer;
public class EscapeUtils { public static String escapeHtml(String str) { if (str == null) {`
`return null; } try { StringWriter writer = new StringWriter ((int)(str.length() * 1.5)); escapeHtml(writer, str); return writer.toString(); } catch (IOException e) { //assert false; //should be impossible e.printStackTrace(); return null; } }
public static void escapeHtml(Writer writer, String string) throws IOException { if (writer == null ) { throw new IllegalArgumentException ("The Writer must not be null."); } if (string == null) { return; } Entities.HTML40_escape.escape(writer, string); }
public static String unescapeHtml(String str) { if (str == null) { return null; } try { StringWriter writer = new StringWriter ((int)(str.length() * 1.5)); unescapeHtml(writer, str); return writer.toString(); } catch (IOException e) { //assert false; //should be impossible e.printStackTrace(); return null; } }
public static void unescapeHtml(Writer writer, String string)
throws IOException {
if (writer == null ) { throw new IllegalArgumentException ("The Writer must
not be null.");
} if (string == null) {
return;
}
Entities.HTML40.unescape(writer, string);
}
}`
清单 7-3 显示了由 JD-GUI 反编译的非模糊代码。查看模糊处理效果的最佳方式是,在 jar 文件被转换成classes.dex文件之前,首先查看来自 jar 文件的反编译代码。这消除了 dx 过程引入的任何意外混淆。你可以看到它和原始代码是一样的。唯一不同的是反编译版本中没有注释。
清单 7-3。EscapeUtils.java
`package org.wordpress.android.util;
import java.io.IOException; import java.io.StringWriter; import java.io.Writer;
public class EscapeUtils { public static String escapeHtml(String str) { if (str == null) return null; try { StringWriter writer = new StringWriter((int)(str.length() * 1.5D)); escapeHtml(writer, str); return writer.toString(); } catch (IOException e) { e.printStackTrace(); }return null; }
public static void escapeHtml(Writer writer, String string)
throws IOException
{
if (writer == null) {
throw new IllegalArgumentException("The Writer must not be
null.");
} if (string == null) {
return;
}
Entities.HTML40_escape.escape(writer, string);
}
public static String unescapeHtml(String str) { if (str == null) return null; try { StringWriter writer = new StringWriter((int)(str.length() * 1.5D)); unescapeHtml(writer, str); return writer.toString(); } catch (IOException e) { e.printStackTrace(); }return null; }
public static void unescapeHtml(Writer writer, String string) throws IOException { if (writer == null) { throw new IllegalArgumentException("The Writer must not be null."); } if (string == null) { return; } Entities.HTML40.unescape(writer, string); } }`
清单 7-4 显示了被 ProGuard 混淆的代码。我用mapping.txt文件得到了混淆文件的名字,是t.java。文件名的选择有一定的随机性,如果你自己混淆了 WordPress 代码,它很可能不会是t.java。
清单 7-4。 装糊涂t.java ( EscapeUtils.java )
`package org.wordpress.android.util;
import java.io.IOException; import java.io.StringWriter;
public final class t {
public static String a(String paramString)
{
if (paramString == null)
return null;
try
{
StringWriter localStringWriter;
String str = paramString;
paramString = localStringWriter = new
StringWriter((int)(paramString.length() * 1.5D));
if (str != null)
r.b.a(paramString, str);
return localStringWriter.toString();
}
catch (IOException localIOException)
{
localIOException.printStackTrace();
}
return null;
}
public static String b(String paramString) { if (paramString == null) return null; try { StringWriter localStringWriter; String str = paramString; paramString = localStringWriter = new StringWriter((int)(paramString.length() * 1.5D)); if (str != null) r.a.b(paramString, str); return localStringWriter.toString(); } catch (IOException localIOException) { localIOException.printStackTrace(); } return null; } }`
public static String escapeHtml(String str)和public static String unescapeHtml(String str)方法看起来与原始方法非常相似。但是public static void escapeHtml(Writer writer, String string)和public static void unescapeHtml(Writer writer, String string) 方法已经被推送到一个单独的文件r.java中,让人不知所云(参见清单 7-5 )。
清单 7-5。 r.java类
`package org.wordpress.android.util;
import java.io.Writer;
final class r
{
private static final String[][] c = { { "quot", "34" }, { "amp",
"38" }, { "lt", "60" }, { "gt", "62" } };
private static final String[][] d = { { "apos", "39" } };
private static String[][] e = { { "nbsp", "160" }, { "iexcl",
"161" }, { "cent", "162" }, { "pound", "163" }, { "curren", "164"
}, { "yen", "165" }, { "brvbar", "166" }, { "sect", "167" }, {
"uml", "168" }, { "copy", "169" }, { "ordf", "170" }, { "laquo",
"171" }, { "not", "172" }, { "shy", "173" }, { "reg", "174" }, {
"macr", "175" }, { "deg", "176" }, { "plusmn", "177" }, { "sup2",
"178" }, { "sup3", "179" }, { "acute", "180" }, { "micro", "181"
}, { "para", "182" }, { "middot", "183" }, { "cedil", "184" }, {
"sup1", "185" }, { "ordm", "186" }, { "raquo", "187" }, {
"frac14", "188" }, { "frac12", "189" }, { "frac34", "190" }, {
"iquest", "191" }, { "Agrave", "192" }, { "Aacute", "193" }, {
"Acirc", "194" }, { "Atilde", "195" }, { "Auml", "196" }, {
"Aring", "197" }, { "AElig", "198" }, { "Ccedil", "199" }, {
"Egrave", "200" }, { "Eacute", "201" }, { "Ecirc", "202" }, {
"Euml", "203" }, { "Igrave", "204" }, { "Iacute", "205" }, {
"Icirc", "206" }, { "Iuml", "207" }, { "ETH", "208" }, { "Ntilde",
"209" }, { "Ograve", "210" }, { "Oacute", "211" }, { "Ocirc",
"212" }, { "Otilde", "213" }, { "Ouml", "214" }, { "times", "215"
}, { "Oslash", "216" }, { "Ugrave", "217" }, { "Uacute", "218" },
{ "Ucirc", "219" }, { "Uuml", "220" }, { "Yacute", "221" }, {
"THORN", "222" }, { "szlig", "223" }, { "agrave", "224" }, {
"aacute", "225" }, { "acirc", "226" }, { "atilde", "227" }, {
"auml", "228" }, { "aring", "229" }, { "aelig", "230" }, {
"ccedil", "231" }, { "egrave", "232" }, { "eacute", "233" }, {
"ecirc", "234" }, { "euml", "235" }, { "igrave", "236" }, {
"iacute", "237" }, { "icirc", "238" }, { "iuml", "239" }, { "eth",
"240" }, { "ntilde", "241" }, { "ograve", "242" }, { "oacute",
"243" }, { "ocirc", "244" }, { "otilde", "245" }, { "ouml", "246"
}, { "divide", "247" }, { "oslash", "248" }, { "ugrave", "249" },
{ "uacute", "250" }, { "ucirc", "251" }, { "uuml", "252" }, {
"yacute", "253" }, { "thorn", "254" }, { "yuml", "255" } };
private static String[][] f = { { "fnof", "402" }, { "Alpha",
"913" }, { "Beta", "914" }, { "Gamma", "915" }, { "Delta", "916"
}, { "Epsilon", "917" }, { "Zeta", "918" }, { "Eta", "919" }, {
"Theta", "920" }, { "Iota", "921" }, { "Kappa", "922" }, {
"Lambda", "923" }, { "Mu", "924" }, { "Nu", "925" }, { "Xi", "926"
}, { "Omicron", "927" }, { "Pi", "928" }, { "Rho", "929" }, {
"Sigma", "931" }, { "Tau", "932" }, { "Upsilon", "933" }, { "Phi", "934" }, { "Chi", "935" }, { "Psi", "936" }, { "Omega", "937" }, {
"alpha", "945" }, { "beta", "946" }, { "gamma", "947" }, {
"delta", "948" }, { "epsilon", "949" }, { "zeta", "950" }, {
"eta", "951" }, { "theta", "952" }, { "iota", "953" }, { "kappa",
"954" }, { "lambda", "955" }, { "mu", "956" }, { "nu", "957" }, {
"xi", "958" }, { "omicron", "959" }, { "pi", "960" }, { "rho",
"961" }, { "sigmaf", "962" }, { "sigma", "963" }, { "tau", "964"
}, { "upsilon", "965" }, { "phi", "966" }, { "chi", "967" }, {
"psi", "968" }, { "omega", "969" }, { "thetasym", "977" }, {
"upsih", "978" }, { "piv", "982" }, { "bull", "8226" }, {
"hellip", "8230" }, { "prime", "8242" }, { "Prime", "8243" }, {
"oline", "8254" }, { "frasl", "8260" }, { "weierp", "8472" }, {
"image", "8465" }, { "real", "8476" }, { "trade", "8482" }, {
"alefsym", "8501" }, { "larr", "8592" }, { "uarr", "8593" }, {
"rarr", "8594" }, { "darr", "8595" }, { "harr", "8596" }, {
"crarr", "8629" }, { "lArr", "8656" }, { "uArr", "8657" }, {
"rArr", "8658" }, { "dArr", "8659" }, { "hArr", "8660" }, {
"forall", "8704" }, { "part", "8706" }, { "exist", "8707" }, {
"empty", "8709" }, { "nabla", "8711" }, { "isin", "8712" }, {
"notin", "8713" }, { "ni", "8715" }, { "prod", "8719" }, { "sum",
"8721" }, { "minus", "8722" }, { "lowast", "8727" }, { "radic",
"8730" }, { "prop", "8733" }, { "infin", "8734" }, { "ang", "8736"
}, { "and", "8743" }, { "or", "8744" }, { "cap", "8745" }, {
"cup", "8746" }, { "int", "8747" }, { "there4", "8756" }, { "sim",
"8764" }, { "cong", "8773" }, { "asymp", "8776" }, { "ne", "8800"
}, { "equiv", "8801" }, { "le", "8804" }, { "ge", "8805" }, {
"sub", "8834" }, { "sup", "8835" }, { "sube", "8838" }, { "supe",
"8839" }, { "oplus", "8853" }, { "otimes", "8855" }, { "perp",
"8869" }, { "sdot", "8901" }, { "lceil", "8968" }, { "rceil",
"8969" }, { "lfloor", "8970" }, { "rfloor", "8971" }, { "lang",
"9001" }, { "rang", "9002" }, { "loz", "9674" }, { "spades",
"9824" }, { "clubs", "9827" }, { "hearts", "9829" }, { "diams",
"9830" }, { "OElig", "338" }, { "oelig", "339" }, { "Scaron",
"352" }, { "scaron", "353" }, { "Yuml", "376" }, { "circ", "710"
}, { "tilde", "732" }, { "ensp", "8194" }, { "emsp", "8195" }, {
"thinsp", "8201" }, { "zwnj", "8204" }, { "zwj", "8205" }, {
"lrm", "8206" }, { "rlm", "8207" }, { "ndash", "8211" }, {
"mdash", "8212" }, { "lsquo", "8216" }, { "rsquo", "8217" }, {
"sbquo", "8218" }, { "ldquo", "8220" }, { "rdquo", "8221" }, {
"bdquo", "8222" }, { "dagger", "8224" }, { "Dagger", "8225" }, {
"permil", "8240" }, { "lsaquo", "8249" }, { "rsaquo", "8250" }, {
"euro", "8364" } };
private static r g;
private static r h;
public static final r a;
public static final r b;
private s i = new ag();
private void a(String[][] paramArrayOfString) {
for (int j = 0; j < paramArrayOfString.length; j++)
{
int k = Integer.parseInt(paramArrayOfString[j][1]);
String str = paramArrayOfString[j][0];
this.i.a(str, k);
}
}
public final void a(Writer paramWriter, String paramString) { int j = paramString.length(); for (int k = 0; k < j; k++) { int m = paramString.charAt(k); int n = m; String str; if ((str = this.i.a(n)) == null) { if (m > 127) { paramWriter.write("&#"); paramWriter.write(Integer.toString(m, 10)); paramWriter.write(59); } else { paramWriter.write(m); } } else { paramWriter.write(38); paramWriter.write(str); paramWriter.write(59); } } }
public final void b(Writer paramWriter, String paramString)
{
int j;
if ((j = paramString.indexOf('&')) < 0)
{
paramWriter.write(paramString);
return;
}
int k = j;
String str1 = paramString; paramString = paramWriter;
paramWriter = this;
paramString.write(str1, 0, k);
int m = str1.length();
while (k < m)
{
int n;
String str2;
if ((n = str1.charAt(k)) == '&')
{
int i1 = k + 1;
String str4;
if ((str4 = str1.indexOf(';', i1)) == -1)
{
paramString.write(n);
}
else
{
int i2;
if (((i2 = str1.indexOf('&', k + 1)) != -1) && (i2 <
str4))
{
paramString.write(n);
}
else
{
str2 = str1.substring(i1, str4);
n = -1;
if ((i1 = str2.length()) > 0)
if (str2.charAt(0) == '#')
{
if (i1 > 1)
{
n = str2.charAt(1);
try
{
switch (n)
{
case 88:
case 120:
n = Integer.parseInt(str2.substring(2), 16);
break;
default:
n = Integer.parseInt(str2.substring(1), 10);
}
if (n > 65535)
n = -1;
} catch (NumberFormatException
localNumberFormatException)
{
n = -1;
}
}
}
else
{
String str3 = str2;
n = paramWriter.i.a(str3);
}
if (n == -1)
{
paramString.write(38);
paramString.write(str2);
paramString.write(59);
}
else
{
paramString.write(n);
}
str2 = str4;
}
}
}
else
{
paramString.write(n);
}
str2++;
}
}
static { (r.g = new r()).a(c); g.a(d); (r.h = new r()).a(c); h.a(e); r localr; (localr = r.a = new r()).a(c); localr.a(e); localr.a(f); b = new r(); (localr = a).a(e); localr.a(f); } }`
从第四章中,你可以看到 ProGuard 通过重命名变量来使用布局混淆,这只是稍微有效。但是它也采用了一些令人印象深刻的数据混淆技术,通过分割变量和将静态数据转换成过程数据。第一轮晋级。
看图 7-1 中左边的菜单,显示了在 JD-GUI 中打开的混淆的 jar 文件。大量的类名没有被混淆。这些方法有一些布局混淆,但是类名包含的信息使人们很容易理解这些方法在做什么。默认情况下,manifest.xml文件中列出的所有Activity、Application、Service、BroadcastReceiver和ContentProvider类都不会被 ProGuard 混淆。最好的解决方案是尽量减少这种类型的类。
**图 7-1。**混淆了 JD-GUI 中的 WordPress jar 文件
配置
ProGuard 在proguard.cfg文件中配置。默认配置文件如清单 7-6 所示。最简单的是,该文件告诉 ProGuard 不要使用大小写混合的类名(当 jar 文件被解压缩时,这会在 Windows 上引起问题);不执行预验证步骤;保留Activity、Application、Service、BroadcastReceiver和ContentProvider类的类名;不删除任何本机类;还有更多。
清单 7-6。 proguard.cfg文件为 WordPress App
`-optimizationpasses 5 -dontusemixedcaseclassnames -dontskipnonpubliclibraryclasses -dontpreverify -verbose -optimizations !code/simplification/arithmetic,!field/,!class/merging/
-keep public class * extends android.app.Activity -keep public class * extends android.app.Application -keep public class * extends android.app.Service -keep public class * extends android.content.BroadcastReceiver -keep public class * extends android.content.ContentProvider -keep public class * extends android.app.backup.BackupAgentHelper -keep public class * extends android.preference.Preference -keep public class com.android.vending.licensing.ILicensingService
-keepclasseswithmembernames class * { native ; }
-keepclasseswithmembers class * { public (android.content.Context, android.util.AttributeSet); }
-keepclasseswithmembers class * { public (android.content.Context, android.util.AttributeSet, int); }
-keepclassmembers class * extends android.app.Activity { public void *(android.view.View); }
-keepclassmembers enum * {
public static **[] values(); public static ** valueOf(java.lang.String);
}
-keep class * implements android.os.Parcelable { public static final android.os.Parcelable$Creator *; }`
在[proguard.sourceforge.net/manual/examples.html#androidapplication](http://proguard.sourceforge.net/manual/examples.html#androidapplication)可以找到一个很好的 Android APKs 配置设置资源,它反映了许多这些设置。这是非常有用的,特别是如果你的 APK 在使用 ProGuard 后在你的设备上失败了。
一个更简单的选择是使用 ProGuard GUI,它会带您浏览配置设置,并提供更多解释。例如,proguard.cfg中的优化设置很神秘,但是在 GUI 中更容易理解和设置(见图 7-2 )。
图 7-2。 ProGuard GUI
要启动 GUI,首先要确保你已经在[proguard.sourceforge.net](http://proguard.sourceforge.net)从 SourceForge 下载了。解压缩它并在lib文件夹中执行下面的命令,假设您已经将目标proguard.cfg文件复制到了proguard\lib文件夹中。你应该看到许多优化选项直接来自第四章的模糊转换:
java -jar proguardgui.jar proguard.cfg
调试
你可能会发现你的 APK 在被混淆后在战场上失败了。调试代码很困难,因为许多方法的名称都被 ProGuard 更改了。幸运的是,ProGuard 有一个retrace选项,可以让你回到原来的名字。该命令如下所示:
java -jar retrace.jar mapping.txt stackfile.trace
mapping.txt在bin\proguard文件夹中,stackfile.trace是应用崩溃时保存的堆栈跟踪。
解决方案 2: DashO
ProGuard 不是你唯一的混淆选项。商业混淆器,如抢先的 DashO,可在[www.preemptive.com](http://www.preemptive.com)获得,是有价值的替代品,比 ProGuard 做更多的控制流和字符串加密混淆。图 7-3 显示了 DashO 界面,包括控制流、重命名和字符串加密混淆选项。
图 7-3。 妫办公会
控制流选项对字节码进行重新排序,目的是使其不可能被反编译。String Encryption 选项对许多字符串进行加密,这对于防止有人窃取 API 密钥或密码非常有用。重载归纳(重命名选项之一)是一种更强烈的类重命名形式:可以将多个类命名为a()或b(),因为这样做是合法的 Java,只要这些类具有不同的方法参数。
在 DashO 中混淆 Android 项目最简单的方法是使用 DashO 向导(见图 7-4 )。稍后,您可以使用 GUI 来调整您想要设置的任何选项。
图 7-4。 巫师办公会
输出
DashO 输出一个项目报告文件和一个mapreport或映射文件到ant-bin\dasho-results文件夹。例如,mapreport文件告诉我EscapeUtils.class已经改名为i_:
org.wordpress.android.i_ public org.wordpress.android.util.EscapeUtils
清单 7-7 显示了反编译后的 JD-GUI 输出。
清单 7-7。 EscapeUtils,被办公会搞得晕头转向
`package org.wordpress.android;
import java.io.IOException; import java.io.StringWriter; import java.io.Writer;`
`public class i_ { public static String e(String paramString) { if (paramString != null); try { StringWriter localStringWriter = new StringWriter((int)(paramString.length() * 1.5D)); o(localStringWriter, paramString); return localStringWriter.toString(); return null; } catch (IOException localIOException) { localIOException.printStackTrace(); } return null; }
public static void o(Writer paramWriter, String paramString) throws IOException { if (paramWriter == null) break label24; do return; while (paramString == null); xd.v.v(paramWriter, paramString); return; label24: throw new IllegalArgumentException(R.endsWith("Rom)]yeyk}0|g``5xxl9x~<sksl/" , 554 / 91)); }
// ERROR //
public static String f(String paramString)
{
// Byte code:
// 0: aload_0
// 1: ifnonnull +16 -> 17
// 4: goto +10 -> 14
// 7: astore_1
// 8: aload_1
// 9: invokevirtual 33 java/io/IOException:printStackTrace
()V
// 12: aconst_null
// 13: areturn
// 14: aconst_null // 15: areturn
// 16: areturn
// 17: new 7 java/io/StringWriter
// 20: dup
// 21: aload_0
// 22: invokevirtual 29 java/lang/String:length ()I
// 25: i2d
// 26: ldc2_w 3
// 29: dmul
// 30: d2i
// 31: invokespecial 30 java/io/StringWriter: (I)V
// 34: astore_1
// 35: aload_1
// 36: aload_0
// 37: invokestatic 37 org/wordpress/android/i_:d
(Ljava/io/Writer;Ljava/lang/String;)V
// 40: aload_1
// 41: invokevirtual 32
java/io/StringWriter:toString()Ljava/lang/String;
// 44: goto -28 -> 16
//
// Exception table:
// from to target type
// 17 477 java/io/IOException
}
public static void d(Writer paramWriter, String paramString) throws IOException { if (paramWriter != null) { if (paramString != null) { xd.t.h(paramWriter, paramString); return; } } else throw new IllegalArgumentException(d9.insert(49 * 25, "\035".l\032<&$4 s9 %#x75/|?;• .4./j")); } }`
代码中最明显的是escapeHTML或unescapeHTML方法无法反编译。Java 还有一些有趣的用途,比如变量名标签和字符串加密。下面的代码片段是使用 JD-GUI 反编译时令人困惑的代码的一个很好的例子:
label24: throw new IllegalArgumentException(R.endsWith("Rom)]yeyk}0|g``5xxl9x~<sksl/" , 554 / 91));
重新编译这段代码需要一些努力。第二回合达索获胜。
回顾案例研究
我希望这个案例研究向您展示了 JD-GUI 可以从未受保护的代码中恢复多少代码,以及这些代码与原始源代码有多接近。在本章的随机样本中,两个源文件之间唯一的区别是缺少注释。ProGuard 和 DashO 使得任何反编译的代码更加难以理解。最起码,你要把proguard.config=proguard.cfg加到你的project.properties文件里;商业混淆器可以提供额外的保护。
始终仔细检查任何敏感信息是否已通过下载生产 APK 并反编译得到保护。如果你有任何后端系统的 API 密钥或用户名,并且它们没有被 ProGuard 或 DashO 隐藏到令你满意的程度,你可能要考虑使用 Android 原生开发工具包(NDK;更多信息见第四章。
总结
当构思这本书的时候,Java 反编译似乎是一个重要的问题。但这从未发生过。当然,有一些桌面应用;但是大部分代码是为 web 服务器编写的,jar 文件被牢牢地锁在防火墙后面。
公平地说,有了 Android,Java 已经超越了它早期的根基。Android APKs 在用户的设备上很容易访问,并且首先为 Java 开发的反编译技术现在使得恢复任何未受保护的 apk 变得非常容易。这些 apk 通常足够小,程序员或黑客可以很快理解它们是如何工作的。如果你想在 APK 藏东西,你需要保护它。
这种情况在不久的将来会改变吗?如果 DVM、JVM 和 Java 代码之间仍然存在链接,就不会这样。我预测工具将会转移到 DVM 上,如果有什么不同的话,情况可能会变得更糟。混淆和反编译之间的军备竞赛将会以很快的速度进行,复制过去 10 年中发生的许多相同的步骤——但这次是在 DVM 上。
这本书的前提是向个人用户展示如何从classes.dex文件反编译代码,有哪些保护方案,以及它们的含义。一般来说,人们的好奇心比欺诈性强得多,而且不太可能有人会使用反编译器来窃取软件公司的皇冠上的宝石。相反,他们只是想看一眼,看看它们是如何组合在一起的——Java 反编译器使普通程序员能够更深入地了解通常是黑箱的东西。这本书帮助你越过那个边缘。
从这里开始尝试的事情包括使用 ANTLR 和扩展代码,如果它不能反编译您的特定classes.dex文件的话。网上也有一些开源的反编译器——比如 JODE,可以在[jode.sourceforge.net](http://jode.sourceforge.net)找到——它们提供了丰富的信息。位于[code.google.com/p/smali/](http://code.google.com/p/smali/)的 Smali 和 baksmali 也是你开始研究的绝佳地点。
我尽了最大努力使这本书易于阅读。我有意识地决定让它更实用,而不是理论上的,同时努力避免它成为傻瓜的 Android 反编译器。我希望你我的努力是值得的。请记住,这里的事情变化很快,所以请关注 Apress 网站的更新。