您的位置:首页 > 移动开发 > Android开发

分析Android程序之破解第一个程序

2015-05-06 00:00 399 查看
摘要: 反编译android程序入门必备博文

破解Android程序通常的方法是将apk文件利用ApkTool反编译,生成Smali格式的反汇编代码,然后阅读Smali文件的代码来理解程序的运行机制,找到程序的突破口进行修改,最后使用ApkTool重新编译生成apk文件并签名,最后运行测试,如此循环,直至程序被成功破解。
1. 反编译APK文件
ApkTool是跨平台的工具,可以在windows平台与linux平台下直接使用。使用前到:http://code.google.com/p/android-apktool/ 下载ApkTool,目前最新版本为1.4.3,Windows平台需要下载apktool1.4.3.tar.bz2apktool-install-windows-r04-brut1.tar.bz2两个压缩包,如果是linux系统则需要下载apktool1.4.3.tar.bz2apktool-install-linux-r04-brut1.tar.bz2,将下载后的文件解压到同一目录下。进入到命令行的解压目录下,执行apktool命令会列出程序的用法:
反编译apk文件的命令为: apktool d[ecode] [OPTS] <file.apk> [<dir>]
编译apk文件的命令为: apktool b[uild] [OPTS] [<app_path>] [<out_file>]
那么在命令行下进入到apktool工具目录,输入命令:
?

1
$ .

/apktool

d

/home/fuhd/apk/gnapk/nice/com

.

nice

.main.apk  outdir

稍等片刻,程序就会反编译完成,如图:



2. 分析APK文件
如上例,反编译apk文件成功后,会在当前的outdir目录下生成一系列目录与文件。其中smali目录下存放了程序所有的反汇编代码,res目录则是程序中所有的资源文件,这些目录的子目录和文件与开发时的源码目录组织结构是一致的。
如何寻找突破口是分析一个程序的关键。对于一般Android来说,错误提示信息通常是指引关键代码的风向标。以书中的注册示例为例,在错误提示附近一般是程序的核心验证代码,分析人员需要阅读这些代码来理解软件的注册流程。
错误提示是Android程序中的字符串资源,开发Android程序时,这些字符串可能硬编码到源码中,也可能引用 自“res/values”目录下的strings.xml文件,apk文件在打包时,strings.xml中的字符串被加密存储为“resources.arsc”文件保存到apk程序包中,apk被成功反编译后这个文件也被解密出来了。
以书中2.1.2节运行程序时的错误提示,在软件注册失败时会Toast弹出“无效用户名或注册码”,我们以此为线索来寻找关键代码。打开“res/values/strings.xml”文件,内容如下:
?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17
<?

xml

version

=

"1.0"

encoding

=

"utf-8"

?>


<

resources

>



<

string

name

=

"app_name"

>Crackme0201</

string

>



<

string

name

=

"hello_world"

>Hello world!<

string

>



<

string

name

=

"menu_settings"

>Settings</

string

>



<

string

name

=

"title_activity_main"

>crackme02</

string

>



<

string

name

=

"info"

>Android程序破解演示实例</

string

>



<

string

name

=

"username"

>用户名:</

string

>



<

string

name

=

"sn"

>注册码:</

string

>



<

string

name

=

"register"

>注册</

string

>



<

string

name

=

"hint_username"

>请输入用户名</

string

>



<

string

name

=

"hint_sn"

>请输入16位的注册码</

string

>



<

string

name

=

"unregister"

>程序未注册</

string

>



<

string

name

=

"registered"

>程序已注册</

string

>



<

string

name

=

"unsuccessed"

>无效用户名或注册码</

string

>

<!-- 就是这一行 -->



<

string

name

=

"successed"

>恭喜您!注册成功</

string

>


</

resources

>

开发Android程序时,strings.xml文件中的所有字符串资源都在“gen/<packagename>/R.java”文件的String类中被标识,每个字符串都有唯一的int类型索引值,使用Apktool反编译apk文件后,所有的索引值保存在strings.xml文件同目录下的public.xml文件中。
从上面列表中找到“无效用户名或注册码”的字符串名称unsuccessed。打开public.xml文件,它的内容如下:
?

1

2

3

4

5

6

7

8

9

10

11
<?

xml

version

=

"1.0"

encoding

=

"utf-8"

?>


<

resources

>



<

public

type

=

"drawable"

name

=

"ic_launcher"

id

=

"0x7f020001"

/>



<

public

type

=

"drawable"

name

=

"ic_action_search"

id

=

"0x7f020000"

/>



.......



<

public

type

=

"string"

name

=

"unsuccessed"

id

=

"0x7f05000c"

/> 

<!-- 这是这一行 -->



.......



<

public

type

=

"id"

name

=

"edit_sn"

id

=

"0x7f080002"

/>



<

public

type

=

"id"

name

=

"button_register"

id

=

"0x7f080003"

/>



<

public

type

=

"id"

name

=

"menu_settings"

id

=

"0x7f080004"

/>


</

resources

>

unsuccessed的id值为0x7f05000c,在smali目录中搜索含有内容为0x7f05000c的文件,最后发现只有MainActivity$1.smali文件一处调用,代码如下:
?

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
# virtual methods


.method

public

onClick(Landroid/view/View;)V



.locals

4



.parameter

"v"



.prologue



const

/

4

v3,

0x0



......



.line

32



#calls:



Locm/droider/crackme0201/MainActivity;->checkSN(Ljava/lang/String;Ljava/lang/String;)Z



invoke-

static

{v0,v1,v2}, Lcom/droider/crackme0201/MainActivity;->



#检查注册码是否合法



access$

2

(Lcom/droider/crackme0201/MainActivity;Ljava/lang/string;Ljava/lang/String;)Z



move-result v0



if

-nez v0, :cond_0  #如果结果不为

0

,就跳转到cond_0标号处



.line

34



iget-object v0, p0, Lcom/droider/crackme0201/MainActivity$

1

;->



this

$

0

:Lcom/droider/crackme0201/MainActivity;



.line

35



const

v1,

0x7f05000c

#unsuccessed字符串,就是这一句



.line

34



invoke-

static

{v0, v1,v3}, Landroid/widget/Toast;->



makeText(Landroid/content/Context;II) Landroid/widget/Toast;



move-result-object v0



.............



.............(略)



.............


.end method

Smali代码中添加的注释使用“”号开头,".line32"行调用了checkSN()函数进行注册码的合法检查,接着下面有如下两行代码:
?

1

2
move-result v0


if

-nez v0,  :cond_0

checkSN()函数返回Boolean类型的值。这里的第一行代码将返回的结果保存到v0寄存器中,第二行代码对v0进行判断,如果v0的值不为零,即条件为真的情况下,跳转到cond_0标号处,反之,程序顺序向下执行。
如果代码不跳转,会执行如下几行代码:
?

1

2

3

4

5

6

7

8

9

10

11

12

13

14
.line

34


iget-object v0, p0, Lcom/droider/crackme0201/MainActivity$

1

;->



this

$

0

:Lcom/droider/crackme0201/MainActivity;


.line

35


const

v1,

0x7f05000c

#unsuccessed字符串


.line

34


invoke-

static

{v0, v1,v3}, Landroid/widget/Toast;->



makeText(Landroid/content/Context;II)Landroid/widget/Toast;


move-result-object v0


.line

35


invoke-virtual {v0}, Landroid/widget/Toast;->show()V


.line

42


:goto_0


return

-

void

“.line34”行使用iget-object指令获取MainActivity实例的引用。代码中的->this$0是内部类MainActivity$1中的一个synthetic字段,存储 的是父类MainActivity的引用,这是Java语言的一个特性,类似的还有->access$0,这一类代码会在后面进行详细介绍。“.line35”行将v1寄存器传入unsuccessed字符串的id值,接着调用Toast;->makeText()创建字符串,然后调用Toast;->show()V方法弹出提示,最后.line40行调用return-void函数返回。
如果代码跳转,会执行如下代码:
?

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
:cond_0


iget-object v0, p0, Lcom/droider/crackme0201/MainActivity$

1

;->



this

$

0

:Lcom/droider/crackme0201/MainActivity;


.line

38


const

v1,

0x7f05000d

#successed字符串


.line

37


invoke-

static

{v0, v1,v3}, Landroid/widget/Toast;->



makeText(Landroid/content/Context;II)Landroid/widget/Toast;


move-result-object v0


.line

38


invoke-virtual {v0}, Landroid/widget/Toast;->show()V


.line

39


iget-object v0, p0, Lcom/droider/crackme0201/MainActivity$

1

;->



this

$

0

:Lcom/droider/crackme0201/MainActivity;


#getter

for

:Lcom/droider/crackme0201/MainActivity;->btn_register:Landroid/widger/Button;


invoke-

static

{v0}; Lcom/droider/crackme0201/MainActivity;->



access$

3

(Lcom/droider/crackme0201/MainActivity;)Landroid/widget/Button;


move-result-object v0


invoke-virtual {v0, v3}, Landroid/widget/Button;->setEnabled(Z)V#设置注册按钮不可用


.line

40


iget-object v0, p0, Lcom/droider/crackme0201/MainActivity$

1

;->



this

$

0

:Lcom/droider/crackme0201/MainActivity;


const

v1,

0x7f05000b

# registered字符串,模拟注册成功


invoke-virtual {v0, v1}, Lcom/droider/crackme0201/MainActivity;->setTitle(I)V


goto

:goto_0

这段代码的功能是弹出注册成功提示,也就是说,上面的跳转如果成功意味着程序会成功注册。
3. 修改Smali文件代码
经过上一小节的分析可以发现,“.line32”行的代码“if-nez v0,:cond_0”是程序的破解点。if-nez是Dalvik指令集中的一个条件跳转指令。类似的还有if-eqz,if-gez,if-lez等。这些指令会在后面blog中进行介绍,在这里只需要知道,与if-nez指令功能相反的指令为if-eqz,表示比较结果为0或相等时进行跳转。
用任意一款文本编辑器打开MainActivity$1.smali文件,将“.line32”行的代码“if-nez v0,:cond_0”修改为“if-eqz v0,:cond_0”,保存后退出,代码就算修改完成了。
4. 重新编译APK文件并签名
修改完Smali文件代码后,需要将修改后的文件重新进行编译打包成apk文件。编译apk文件的命令格式为:
apktool b[uild] [OPTS] [<app_path>] [<out_file>],打开命令行进入到apktool工具的目录,执行以下命令:
?

1
$ .

/apktool

b

/home/fuhd/apk/gw/outdir/

不出意外的话,程序就会编译成功。编译成功 后会在outdir目录下生成dist目录,里面存放着编译成功的apk文件。编译生成的crackme02.apk没有签名,还不能安装测试,接下来需要使用signapk.jar工具对apk文件进行签名。signapk.jar是Android源码包中的一个签名工具。代码位于Android源码目录下的/build/tools/signapk/SignApk.java文件中,源码编译后可以在/out/host/linux-x86/framework目录中找到它。使用signapk.jar签名时需要提供签名文件,我们在此可以使用Android源码中提供的签名文件 testkey.pk8与testkey.x509.pem,它们位于Android源码的build/target/product/security目录。将signapk.jar,testkey.x509.pem,testkey.pk8,3个文件放到同一目录,然后在命令提示符下输入如下命令对APK文件进行签名:
?

1

2
$ java -jar signapk.jar testkey.x509.pem testkey.pk8



/home/fuhd/apk/gw/outdir/crackme02

.apk  crackme02Sign.apk

签名成功后会在同目录下生成crackme02sign.apk文件。
4. 安装测试
现在是时候测试修改后的成果了。启动一个Android AVD,或者使用数据线连接手机与电脑,然后在命令提示符下执行以下命令安装破解后的程序:
?

1
$ adb

install

crackme02sign.apk

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  反编译 android apktool