转载

Android应用逆向——分析反编译代码之大神器

Android应用逆向——分析反编译代码之大神器 分类:

作者同类文章 X

版权声明:本文为博主原创文章,未经博主允许不得转载。

如果说使用dex2jar和JD-GUI获得了一个APP反编译后的JAVA代码,再结合smali代码调试器来进行调试还不够爽,不够畅快的话,下面将介绍一个帮助分析代码执行流程的大神器。这个神器优点很多,不过遗憾的是它有一个致命的缺点!就是威力太大,能让使用它的人快速分析出一个复杂APP的执行流程,快速定位关键之处进行修改以达到各种目的,尤其对于像我一样的Android逆向新手来说,这是非常致命的。为什么非常致命?因为使用了该神器后,1个小时就找到了关键代码,弄清楚执行逻辑,1天之内就实现了程序,解决了外行人看来难度很高的问题。由此带来的后果就是自我感觉良好,自己感觉自己很牛逼,蒙蔽了自己的双眼,终日沉溺在这种骄傲的状态中,从而不能继续虚心刻苦学习技术知识,久而久之,在技术水平上落后别人一大截,对自身发展造成严重影响!所以使用该神器前必须清楚地认识到可能带来的这些弊端,确认自己能调整好心态以后再继续往下看,否则请按ALT+F4关闭。

一般的商业APP代码量巨大,而且做过混淆处理。我所面对的这个APP反编译后仅JAVA代码文本就达到了100多MB,做过混淆处理后,代码里几乎看不见一个局部变量的名字,大部分的函数名、类成员变量名都是abcdefg之类,且反编译后的代码看起来怪怪的。如果仅仅是静态分析,读这些代码,将会是一件非常痛苦的事情。尤其是对我这种Android正向开发都不会的新手来说,某个按钮点击的响应函数在哪里,下拉刷新的响应函数在哪里,找起来很困难。好不容易找到了登录按钮的响应函数,顺着函数调用一层一层往里看,又遇到了一些抽象方法和异步操作,无法仅从按钮响应函数的调用栈上找到最后发送网络请求的关键代码。虽然可以结合smali调试器来分析,下断点后查看实现了抽象方法的具体对象是什么,查看调用栈理清调用结构,但整个工作还是进行得很缓慢。再加上调试器各种奔溃和不准,搞去搞来各种心烦,导致了一个严重的后果,就是搞着搞着就不由自主的打开了游戏,以调节郁闷的心情,从而搁置了项目进度。

思来想去,我觉得与其主动去分析它的代码执行流程,不如让它来主动告诉我它的代码执行流程。怎么告诉?首先想到的是打日志,在上篇文章中我们通过打开调试开关,修改它的smali代码重定向它的日志到android.util.Log,然后打开DDMS在LogCat中看到了它的全部日志。不过这样还是远远不够,因为它的日志只记录了一些运行中的状况和错误。我的目的是想让它的日志告诉我,它调用了哪些函数,以及调用的先后顺序。在上篇文章中讲到,我们也可以通过TraceView来分析它从登陆按钮点下到登录结果出来的这个过程中调用的所有函数,不过TraceView给出的结果充斥着很多很多的系统函数,而且难以看出调用顺序,非常不好用。如果能让这个app通过日志主动告诉我们它调用了哪些函数,且不包含系统函数,仅仅是它自身代码的函数,那该多好。

首先想到的方法就是手工修改它的smali代码,插入日志。假如目标APP有一个函数的smali代码如下:

.method protected getLoginPassword()Ljava/lang/String;
    .registers 2
    .prologue
    .line 809
    iget-object v0, p0, Lcom/ali/user/mobile/login/ui/AliUserLoginActivity;->mPasswordInput:Lcom/alipay/mobile/commonui/widget/keyboard/APSafeEditText;
    invoke-virtual {v0}, Lcom/alipay/mobile/commonui/widget/keyboard/APSafeEditText;->getSafeText()Landroid/text/Editable;
    move-result-object v0
    invoke-interface {v0}, Landroid/text/Editable;->toString()Ljava/lang/String;
    move-result-object v0
    return-object v0
.end method
在这段smali代码里插入三行代码:
const-string v0, "InjectLog"
const-string v1, "com.ali.user.mobile.login.ui.AliUserLoginActivity.getLoginPassword()"
invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
这三行smali代码对应的JAVA代码是:
android.util.Log.d("InjectLog", "com.ali.user.mobile.login.ui.AliUserLoginActivity.getLoginPassword()");

当然你可以可以插入其它各种各样的代码,你可以先创建一个Android项目,把JAVA代码写出来,编译,再反编译,再把对应的smali代码复制出来粘贴进去。

好了,插入完成以后,重新打包,安装,运行。每当执行这个函数的时候,我们就可以在LogCat中看到输出,我们就知道它调用了这个函数,包名、类名、函数名都有了。 试想,如果我们把它整个app的所有smali代码中的所有函数全部插入这三行 smali代码,重新打包,安装,运行。然后,就可以冲一杯咖啡,打开DDMS,打开LogCat,静静的看着它把所有的函数调用流程输出来。然后我们在APP上点一下登录,整个登录过程暴露无遗!函数的调用顺序就是日志输出的时间顺序,没有任何杂质,没有系统函数,那么纯净,那么完美。此刻它仿佛没有了任何秘密,一丝不挂的站在你面前,任由你炽热的目光在它身上那些精致的部位上扫来扫去。

且慢,先擦干口水。事情没有那么容易,就像追一个漂亮的女生一样,你送一次礼物就想追到她,欣赏她美丽的酮体?做白日梦吧!至少得送上万次礼物才行。也就是说你需要在这个app的上万个函数中插入这三行smali代码,看到这里,首先要做的事情就是先把你手中的铁锤收起来,不要打我。

我们当然不可能手工去完成这上万次操作,我们是程序员,这种重复枯燥的工作自然让程序来完成。我们可以写一个文本分析的小程序,遍历反编译出来的代码目录下的所有smali文件,利用字符串搜索法和正则表达式,找出一个个的函数,并且从smali文件第一行.class的定义中获取包名和类名,然后从.method的定义中获取函数名、参数、返回值信息,生成上面的三行smali代码,插进去。

"自动化"、"批量"、"文本处理"、"脚本",想到这些关键词,自然就想到了Python,用它来干这个事情将会更加得心应手。 经过几小时奋战,完成了这个批量插代码的Python脚本。自动批量插入后对APP重新打包,安装,运行,奔溃了。仔细想想,对,还有寄存器的问题没有处理,有的函数本来是没有局部变量,没有使用寄存器的,".method"定义的函数块中的第一行为".registers 0",而我插入的代码里用到了两个寄存器。后来修改了Python脚本,在分析每个函数的时候也检查registers,如果registers小于2个则改为2个。重新插入代码后,对APP重新打包,安装,运行,还是奔溃了。后来看了这篇文章: https://liuzhichao.com/p/919.html 了解了registers的意义,发现我自己写的Android APP反编译以后每个函数第一行都是.locals,而我反编译的这个商业APP的代码中每个函数第一行都是.registers。使用.registers声明寄存器数量有个很大的不好之处就是参数寄存器也包含在.registers声明的寄存器中,如果一个函数有两个参数,没有局部变量,那么这个函数会声明.registers 2,我的脚本检测到这里认为寄存器够用,然后就插入了代码,而我插入的代码里用到了v0和v1寄存器,在赋值的时候把参数寄存器的内容覆盖了,因此带来了一些问题。而且如果它的代码里本来就使用了v0、v1寄存器,我直接这样把代码插进去,也会带来一些影响。

要解决这个问题,可以继续完善Python脚本,对它的smali代码进行更多的分析,分析这个函数已经用了多少个寄存器,有多少个参数寄存器,序号分别是什么,然后再生成合适的插入代码并修改.registers数量,但这样做的话就比较麻烦了。偷懒是我的作风,而这种做法显然不符合我的作风,于是我觉得不应该再继续完善Python脚本,而是精简Python脚本。Python脚本不再分析它smali代码中的函数,而是直接不管三七二十在每个函数中插入一行对void PrintFunc()的调用代码。void PrintFunc()是我自己写的函数,无参数无返回值,调用它对应的smali代码大概是这样的:

invoke-static {}, Lcom/hook/testsmali/InjectLog;->PrintFunc()V

不使用任何寄存器,没有返回值,显然这样的代码插入到目标APP中的函数中,不会对宿主函数造成任何影响。同时

新建一个Android应用项目,写下如下JAVA代码:

package com.hook.testsmali;
import android.util.Log;
public class InjectLog
{
    public static void PrintFunc()
    {
        Thread cur_thread = Thread.currentThread();
        StackTraceElement stack[] = cur_thread.getStackTrace();
        Log.d("InjectLog", stack[3].toString() + "[" + cur_thread.getId() + "]");
    }
}

注意,我的包名是com.hook.testsmali,类名是InjectLog,函数名是PrintFunc()。你写的JAVA代码不必和我一样,但调用PrintFunc()的smali代码要和JAVA代码的包名、类名、函数名一致。

从上面的 代码 可以看到,在PrintFunc()的实现上,先获取调用栈的信息,然后取出调用栈中下标为3的元素,这个正是调用void PrintFunc()的调用者的函数名、包名、类名,然后随同线程ID信息,通过日志输出。 随后精简我们的Python脚本,Python脚本需要干的事情就是遍历所有smali文件,向smali中的每一个函数中插入 invoke-static {}, Lcom/hook/testsmali/InjectLog;->PrintFunc()V   完毕! 然后我们对自己写的这个APP进行反编译,拿到这个void PrintFunc()函数的smali代码文件InjectLog.smali,在目标APP的代码文件夹中创建这个路径com/hook/testsmali然后把InjectLog.smali放进去,重新打包目标APP,安装。然后打开DDMS,设置LogCat过滤器过滤出Tag为InjectLog的日志。

接下来,嘘!!运行目标APP,此后在目标APP中的每一次点击,都是那么酣畅淋漓, LogCat中的日志如潮水般涌出,四处喷溅。对,就是那么的丝滑,那么的畅快,那么的清澈,有没一丝杂质。效果如下图所示,每一个过程,每一个步骤,都赤裸裸的展现在你面前,看得你面红耳赤!

Android应用逆向——分析反编译代码之大神器

我们也可以不输出线程ID,因为LogCat已经标示出TID来了,但是导出日志后这个TID就不见了,所以还是输出一下。日志的输出时间顺序就是函数的调用顺序,顺着这个调用顺序看dex2jar和JD-GUI反编译出的JAVA代码,定位超快、效率超高,分分种种搞出些事情来。什么?你问我搞什么事情?它都一丝不挂在你面前了,你还问我搞什么事情!

最后贴上用于批量插smali代码的Python脚本,虽然丑陋但是简单粗暴、野蛮有效:

import os

class ParserError(Exception):
    pass

# 注入代码到一个函数块中
def inject_code_to_method_section(method_section):
    # 静态构造函数,无需处理
    if method_section[0].find("static constructor") != -1:
        return method_section
    # synthetic函数,无需处理
    if method_section[0].find("synthetic") != -1:
        return method_section
    # 抽象方法,无需处理
    if method_section[0].find("abstract") != -1:
        return method_section
    # 生成待插入代码行
    inject_code = [
        '/n',
        '    invoke-static {}, Lcom/hook/testsmali/InjectLog;->PrintFunc()V/n',
        '/n'
    ]
    #插入到.prologue的下一行
    is_inject = False
    for i in range(0, len(method_section)):
        if method_section[i].find(".prologue") != -1:
            is_inject = True
            method_section[i + 1: i + 1] = inject_code
            break
    if not is_inject:
        raise ParserError("找不到.prologue")
    return method_section


def inject_log_code(content):
    new_content = []
    method_section = []
    is_method_begin = False
    for line in content:
        if line[:7] == ".method":
            is_method_begin = True
            method_section.append(line)
            continue
        if is_method_begin:
            method_section.append(line)
        else:
            new_content.append(line)
        if line[:11] == ".end method":
            if not is_method_begin:
                raise ParserError(".method不对称")
            is_method_begin = False
            new_method_section = inject_code_to_method_section(method_section)
            new_content.extend(new_method_section)
            method_section.clear()
    return new_content


def main():
    walker = os.walk("./")
    for root, directory, files in walker:
        for file_name in files:
            if file_name[-6:] != ".smali":
                continue
            file_path = root + "/" + file_name
            print(file_path)
            file = open(file_path)
            lines = file.readlines()
            file.close()
            new_code = inject_log_code(lines)
            file = open(file_path, "w")
            file.writelines(new_code)
            file.close()


if __name__ == '__main__':
    main()

需要注意的是在向目标APP的smali代码的每个函数中插代码时,并不是所有函数都需要插,类的静态构造函数一般不需要插,因为它是用来初始化类的静态成员的,没有太多的关键代码。synthetic函数也不需要插,我不知道synthetic函数是什么意思,但是发现smali中的synthetic函数并没有被dex2jar和JD-GUI反编译为JAVA函数,所以忽略它。另外抽象方法也不需要插,这个不用解释。当然这个批量自动插代码的程序可以使用任何编程语言实现,它只是个文本处理工具。

另外在实际使用中,并不一定要给目标APP的所有函数插代码,我们可以先根据包名猜测一下它的功能,然后对这个包下的所有函数进行插代码。

最后,千万不要低估此神器的威力,我在使用过程中屡试不爽,结合smali代码调试器,很快就分析出登录按钮点下去后干了什么,以及最终发送了什么HTTP请求,收到什么响应内容。亲手试一试,相当亦可赛艇!

本文由CharlesSimonyi发表于CSDN博客: http://blog.csdn.net/charlessimonyi/article/details/52027563 转载请注明出处

原文  http://blog.csdn.net/charlessimonyi/article/details/52027563
正文到此结束
Loading...