360手机助手中软件更新
0x01:分析过程
使用wireshark抓包分析更新时请求的是一个配置文件,请求url:update.api.sj.360.cn/mintf/getAppsByPackNames 后面是参数,请求方式为POST,内容如下:
可以看到其中包含了几个关键的参数down_url,apk_md5,signature_md5,size,其中比较关键的一个校验是signature_md5,其他都可以轻松替换,所以我们的重点就是对signature_md5,校验的破解,去分析他对signature_md5加密的方式,以及如何模拟算法,
0x02:知识补充
关于安卓apk数字签名校验的知识补充,安卓apk数字校验使用的是rsa非对称加密算法也就是我们常说的公钥私钥加密,这个算法
指数运算谁都懂,不必说了,先说说模运算。模运算是整数运算,有一个整数m,以n为模做模运算,即m mod n。怎样做呢?让m去被n整除,只取所得的余数作为结果,就叫做模运算。例如,10 mod 3=1;26 mod 6=2;28 mod 2 =0等等。
模指数运算就是先做指数运算,取其结果再做模运算。如
好,现在开始正式讲解RSA加密算法。
算法描述:
(1)选择一对不同的、足够大的素数p,q。
(2)计算n=pq。
(3)计算f(n)=(p-1)(q-1),同时对p, q严加保密,不让任何人知道。
(4)找一个与f(n)互质的数e,且1<e<f(n)。
(5)计算d,使得de≡1 mod f(n)。这个公式也可以表达为d ≡e-1 mod f(n)
这里要解释一下,≡是数论中表示同余的符号。公式中,≡符号的左边必须和符号右边同余,也就是两边模运算结果相同。显而易见,不管f(n)取什么值,符号 右边1 mod f(n)的结果都等于1;符号的左边d与e的乘积做模运算后的结果也必须等于1。这就需要计算出d的值,让这个同余等式能够成立。
(6)公钥KU=(e,n),私钥KR=(d,n)。
(7)加密时,先将明文变换成0至n-1的一个整数M。若明文较长,可先分割成适当的组,然后再进行交换。设密文为C,则加密过程为:。
(8)解密过程为:。
Android的签名机制,通过分析signapk.jar这个可执行包可以知晓签名APK的整个过程
1.程序遍历整个apk文件包中的所有文件(entry),对非文件夹非签名文件的文件,逐个生成SHA1的数字签名信息,再用Base64进行编码,之后将生成的签名写入MANFEST.MF文件
2.对前一步生成的Manifest,使用SHA1-RSA算法,用私钥进行签名,生成CERT.SF文件
3.生成MANIFEST.MF没有使用密钥信息,生成CERT.SF文件使用了私钥文件。那么我们可以很容易猜测到,CERT.RSA文件的生成肯定和公钥相关CERT.RSA文件中保存了公钥、所采用的加密算法等信息
知晓了加密流程,我们可以认识到
1、 Android签名机制其实是对APK包完整性和发布机构唯一性的一种校验机制。
2、 Android签名机制不能阻止APK包被修改,但修改后的再签名无法与原先的签名保持一致。(拥有私钥的情况除外)。
3、 APK包加密的公钥就打包在APK包内,且不同的私钥对应不同的公钥。换句话言之,不同的私钥签名的APK公钥也必不相同。所以我们可以根据公钥的对比,来判断私钥是否一致。
也就是说360手机助手也不可能知道需要升级软件数字签名的私钥,所以可以猜测它进行的是对公钥的加密,那么接下来要验证这个观点
0x03逆向分析
这里我们使用一个强大的安卓反编译软件ApkIDE.exe,然后把360手机助手逆向分析一下:
1.搜索signature_md5我们发现了另一个量sign_md5_default_value,那么我们来定位一下:在Apk改之理中搜索sign_md5_default_value 我们分析出现的位置
分析中在d.smali文件中有一段很关键
method private static b(Landroid/content/Context;Lcom/qihoo/appstore/resource/app/App;)V
.locals 3 // private static void b(Context paramContext, App paramApp)
invoke-virtual {p1}, Lcom/qihoo/appstore/resource/app/App;->Z()Ljava/lang/String;
move-result-object v0
invoke-static {v0}, Lcom/qihoo/appstore/j/d;->c(Ljava/lang/String;)Z
move-result v0
if-eqz v0, :cond_0
sget-object v0, Lcom/qihoo/appstore/j/d;->a:Landroid/content/Context;
invoke-virtual {p1}, Lcom/qihoo/appstore/resource/app/App;->Z()Ljava/lang/String;
move-result-object v1
invoke-static {v0, v1}, Lcom/qihoo/appstore/utils/de;->f(Landroid/content/Context;Ljava/lang/String;)Ljava/lang/String;
move-result-object v0
invoke-virtual {p1}, Lcom/qihoo/appstore/resource/app/App;->bL()Ljava/lang/String;
move-result-object v1
const-string v2, "sign_md5_default_value"
invoke-virtual {v2, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v2
if-nez v2, :cond_1
const-string v2, ""
invoke-virtual {v1, v2}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v2
if-nez v2, :cond_1
sget-object v2, Lcom/qihoo/appstore/j/d;->a:Landroid/content/Context;
invoke-static {v2, v0, v1}, Lcom/qihoo/appstore/utils/de;->a(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;)Z
move-result v0
if-nez v0, :cond_1
const/4 v0, 0x1
invoke-virtual {p1, v0}, Lcom/qihoo/appstore/resource/app/App;->r(Z)V
:cond_0
:goto_0
return-void
:cond_1
const/4 v0, 0x0
invoke-virtual {p1, v0}, Lcom/qihoo/appstore/resource/app/App;->r(Z)V
goto :goto_0
.end method
前面那几句大概意思就是用一个boolean方法判断App成员的Z()方法也就是说那个是不是一个String,换句话我们可以大胆猜测这里是判断有没有获取到程序的MD5,
所以定位到这一句关键的代码Lcom/qihoo/appstore/utils/de;->f(Landroid/content/Context;Ljava/lang/String;)Ljava/lang/String;
其中使用了de的 f() 方法对Context, String 参数进行处理 我们可以猜测这里的String代表的就是我们的MD5:
.method public static f(Landroid/content/Context;Ljava/lang/String;)Ljava/lang/String;
.locals 2
:try_start_0
invoke-virtual {p0}, Landroid/content/Context;->getPackageManager()Landroid/content/pm/PackageManager;
move-result-object v0
const/16 v1, 0x40
invoke-virtual {v0, p1, v1}, Landroid/content/pm/PackageManager;->getPackageInfo(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;
move-result-object v0
iget-object v0, v0, Landroid/content/pm/PackageInfo;->signatures:[Landroid/content/pm/Signature;
const/4 v1, 0x0
aget-object v0, v0, v1
invoke-virtual {v0}, Landroid/content/pm/Signature;->toByteArray()[B
move-result-object v0
invoke-static {v0}, Ljava/util/Arrays;->toString([B)Ljava/lang/String;
move-result-object v0
invoke-static {v0}, Lcom/qihoo/appstore/utils/de;->e(Ljava/lang/String;)Ljava/lang/String;
move-result-object v0
invoke-virtual {v0}, Ljava/lang/String;->toLowerCase()Ljava/lang/String;
:try_end_0
.catch Ljava/lang/Exception; {:try_start_0 .. :try_end_0} :catch_0
move-result-object v0
:goto_0
return-object v0
:catch_0
move-exception v0
sget-boolean v1, Lcom/qihoo360/mobilesafe/a/a;->a:Z
if-eqz v1, :cond_0
invoke-virtual {v0}, Ljava/lang/Exception;->printStackTrace()V
:cond_0
const/4 v0, 0x0
goto :goto_0
.end method
首先调用系统的Context.getPackageManager().getPackageInfo(String,256/4).signature[0].toByteArray() 然后返回一个String对象然后再调用de类的e方法
.method public static e(Ljava/lang/String;)Ljava/lang/String;
.locals 2
:try_start_0
const-string v0, "MD5"
invoke-static {v0}, Ljava/security/MessageDigest;->getInstance(Ljava/lang/String;)Ljava/security/MessageDigest;
move-result-object v0
invoke-virtual {p0}, Ljava/lang/String;->getBytes()[B
move-result-object v1
invoke-virtual {v0, v1}, Ljava/security/MessageDigest;->update([B)V
invoke-virtual {v0}, Ljava/security/MessageDigest;->digest()[B
move-result-object v0
invoke-static {v0}, Lcom/qihoo/appstore/utils/de;->a([B)Ljava/lang/String;
:try_end_0
.catch Ljava/security/NoSuchAlgorithmException; {:try_start_0 .. :try_end_0} :catch_0
move-result-object p0
:goto_0
return-object p0
:catch_0
move-exception v0
invoke-virtual {v0}, Ljava/security/NoSuchAlgorithmException;->printStackTrace()V
goto :goto_0
.end method
这段话就是对String进行一次MD5运算然后调用a方法,其中我们看到的是调用的a的byte[]参数的方法
method public static a([B)Ljava/lang/String;
.locals 4
new-instance v1, Ljava/lang/StringBuilder;
array-length v0, p0
mul-int/lit8 v0, v0, 0x2
invoke-direct {v1, v0}, Ljava/lang/StringBuilder;-><init>(I)V
const/4 v0, 0x0
:goto_0
array-length v2, p0
if-ge v0, v2, :cond_0
sget-object v2, Lcom/qihoo/appstore/utils/de;->f:[C
aget-byte v3, p0, v0
and-int/lit16 v3, v3, 0xf0
ushr-int/lit8 v3, v3, 0x4
aget-char v2, v2, v3
invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(C)Ljava/lang/StringBuilder;
sget-object v2, Lcom/qihoo/appstore/utils/de;->f:[C
aget-byte v3, p0, v0
and-int/lit8 v3, v3, 0xf
aget-char v2, v2, v3
invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(C)Ljava/lang/StringBuilder;
add-int/lit8 v0, v0, 0x1
goto :goto_0
:cond_0
invoke-virtual {v1}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v0
return-object v0
.end method
这段代码的作用就是对一个Byte[]数组进行格式化处理返回一个String对象
那么现在可以猜测360手机助手是对软件公钥的数字签名放到一个数组中,然后取它的第1个成员sinagture[0]进行MD5加密,下面我们来验证猜想
0x04 验证猜想
首先搭建好google sdk 环境 然后我们来写一个安卓程序来模拟360手机助手加密的过程,这里我们用google skd自带的Eclipse进行操作
其中代码如下:
import java.lang.reflect.Array;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import android.app.Activity;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.Signature;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.Menu;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
public class MainActivity extends Activity {
private EditText et_pkgname;
private TextView tv_signature;
private PackageManager manager;
private PackageInfo packageInfo;
private Signature[] signs;
private StringBuilder builder;
private String signature;
private static final char[] f = new char[] { 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 65, 66, 67, 68, 69, 70 };
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
et_pkgname = (EditText) findViewById(R.id.et_pkgname);
tv_signature = (TextView) findViewById(R.id.tv_signature);
manager = getPackageManager();
builder = new StringBuilder();
}
public void getSignature(View view) {
String pkgname = et_pkgname.getText().toString();
boolean isEmpty = TextUtils.isEmpty(pkgname);
if (isEmpty) {
Toast.makeText(this, "应用程序的包名不能为空!", Toast.LENGTH_SHORT);
} else {
try {
/** 通过包管理器获得指定包名包含签名的包信息 **/
packageInfo = manager.getPackageInfo(pkgname, PackageManager.GET_SIGNATURES);
/******* 通过返回的包信息获得签名数组 *******/
signs = packageInfo.signatures;
/******* 循环遍历签名数组拼接应用签名 *******/
/************** 得到应用签名 **************/
String str = e(Arrays.toString(packageInfo.signatures[0].toByteArray()).toLowerCase());
builder.append(str);
signature = builder.toString();
tv_signature.setText(signature);
} catch (NameNotFoundException e) {
e.printStackTrace();
}
}
}
public static String e(String paramString)
{
try
{
MessageDigest localMessageDigest = MessageDigest.getInstance("MD5");
localMessageDigest.update(paramString.getBytes());
String str = a(localMessageDigest.digest());
return str;
}
catch (NoSuchAlgorithmException localNoSuchAlgorithmException)
{
localNoSuchAlgorithmException.printStackTrace();
}
return paramString;
}
public static String a(byte[] paramArrayOfByte)
{
StringBuilder localStringBuilder = new StringBuilder(2 * paramArrayOfByte.length);
for (int i1 = 0; i1 < paramArrayOfByte.length; i1++)
{
localStringBuilder.append(f[((0xF0 & paramArrayOfByte[i1]) >>> 4)]);
localStringBuilder.append(f[(0xF & paramArrayOfByte[i1])]);
}
return localStringBuilder.toString();
}
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.activity_main, menu);
return true;
}
}
我们要在onCreate()函数调用我们的方法也就是我们构造出来的计算signature[]的 MD5的方法
其中
localStringBuilder.append(f[((0xF0 & paramArrayOfByte[i1]) >>> 4)]);
localStringBuilder.append(f[(0xF & paramArrayOfByte[i1])]);
这两句代码是模拟360手机助手的格式化MD5的方法取前四位放到StringBuilder然后取后四位放到StringBuilder中。。
然后我们来用安卓模拟器进行模拟计算新浪微博的md5算法,获得新浪包名,adb shell下查看就行了:adb shell , cd data,cd app, ls,
包名是:com.sina.weibo
运行我们写好的安卓程序来计算我们的签名:
我们来查看与360计算的结果是否相同:
0x05 后记
在后面的入侵测试中,由于本地签名保存,会导致360提示签名非法
从图中我们可以猜测360手机助手是在本地保存了原有的数字签名公钥,然后会对比公钥是否一致,而在实际的代码分析中360的确是有读取sigaature到一个数组中然后保存到了本地,虽然这次入侵结果是失败了,不过过程很迷人,结论就是:安卓公钥私钥的数字签名校验确保了安卓软件的安全性。