1 工具篇
反编译和回编用到的一些工具:
apktool
是解包APK 文件最常用的工具
keytool
是一个Java数据证书的管理工具
jarsigner
是JDK提供的针对jar包签名的通用工具
apksigner
是Google官方提供的针对Android apk签名及验证的专用工具
zipalign
是对zip包对齐的工具,使APK包内未压缩的数据有序排列对齐,从而减少APP运行时内存消耗
2 调试包回编操作
通过apktool d xxx.apk得到反编译后smali文件和manifest文件,进行修改后,利用apktool build命令进行重新打包。
2.1 apktool编译时错误
资源文件找不到:
\res\values-v19\styles.xml:11: error: Error: No resource found that matches the given name: attr 'android:actionModeFindDrawable'.
\res\values-v19\styles.xml:10: error: Error: No resource found that matches the given name: attr 'android:actionModeShareDrawable'.
\res\values-v19\styles.xml:21: error: Error: No resource found that matches the given name: attr 'android:actionModeFindDrawable'.
Exception in thread "main" brut.androlib.AndrolibException: brut.androlib.AndrolibException: brut.common.BrutException: could not exec (exit code = 1): [/tmp/brut_util_Jar_7346229083806801174.tmp, p, --forced-package-id, 127, --min-sdk-version, 15, --target-sdk-version, 26, --version-code, 1, --version-name, 1.0, --no-version-vectors, -F, /tmp/APKTOOL6548347216162541619.tmp, -0, arsc, -0, arsc, -I, /root/.local/share/apktool/framework/1.apk, -S, /root/Desktop/test1/res, -M, /root/Desktop/test1/AndroidManifest.xml]
at brut.androlib.Androlib.buildResourcesFull(Androlib.java:492)
at brut.androlib.Androlib.buildResources(Androlib.java:426)
at brut.androlib.Androlib.build(Androlib.java:305)
at brut.androlib.Androlib.build(Androlib.java:270)
at brut.apktool.Main.cmdBuild(Main.java:227)
at brut.apktool.Main.main(Main.java:75)
Caused by: brut.androlib.AndrolibException: brut.common.BrutException: could not exec (exit code = 1): [/tmp/brut_util_Jar_7346229083806801174.tmp, p, --forced-package-id, 127, --min-sdk-version, 15, --target-sdk-version, 26, --version-code, 1, --version-name, 1.0, --no-version-vectors, -F, /tmp/APKTOOL6548347216162541619.tmp, -0, arsc, -0, arsc, -I, /root/.local/share/apktool/framework/1.apk, -S, /root/Desktop/test1/res, -M, /root/Desktop/test1/AndroidManifest.xml]
at brut.androlib.res.AndrolibResources.aaptPackage(AndrolibResources.java:456)
at brut.androlib.Androlib.buildResourcesFull(Androlib.java:478)
... 5 more
Caused by: brut.common.BrutException: could not exec (exit code = 1): [/tmp/brut_util_Jar_7346229083806801174.tmp, p, --forced-package-id, 127, --min-sdk-version, 15, --target-sdk-version, 26, --version-code, 1, --version-name, 1.0, --no-version-vectors, -F, /tmp/APKTOOL6548347216162541619.tmp, -0, arsc, -0, arsc, -I, /root/.local/share/apktool/framework/1.apk, -S, /root/Desktop/test1/res, -M, /root/Desktop/test1/AndroidManifest.xml]
at brut.util.OS.exec(OS.java:95)
at brut.androlib.res.AndrolibResources.aaptPackage(AndrolibResources.java:450)
... 6 more
错误原因很清楚,大致是资源文件和一些api版本兼容的问题,比如style,manifest一些application属性等识别不了。
通过google有些说需要升级新版本,或者低版本反编译高版本打包,网上的一些答案我一般是不信的(哈哈哈)。
通过查apktool的命令帮助,可以通过-r参数来避免resc的反编译,见下图。这样在打包的时候就不会重新编译resc文件包括xml。
usage: apktool
-advance,--advanced prints advance information.
-version,--version prints the version then exits
usage: apktool if|install-framework [options] <framework.apk>
-p,--frame-path <dir> Stores framework files into <dir>.
-t,--tag <tag> Tag frameworks using <tag>.
usage: apktool d[ecode] [options] <file_apk>
-f,--force Force delete destination directory.
-o,--output <dir> The name of folder that gets written. Default is apk.out
-p,--frame-path <dir> Uses framework files located in <dir>.
-r,--no-res Do not decode resources.
-s,--no-src Do not decode sources.
-t,--frame-tag <tag> Uses framework files tagged by <tag>.
usage: apktool b[uild] [options] <app_path>
-f,--force-all Skip changes detection and build all files.
-o,--output <dir> The name of apk that gets written. Default is dist/name.apk
-p,--frame-path <dir> Uses framework files located in <dir>.
那么,我们需要修改xml的时候还是需要反编译resc,或者通过二进制写入hex中。其实上面编译失败的原因是不同level的API造成的。
查看apktool.jar可发现android-framework.jar:apktool.jar\brut\androlib\android-framework.jar
而android-framework.jar其实是用来指定默认的反编译和编译的framework(不同手机和不同版本有不同的framework,但一般都是向下兼容的)。
执行apktool命令会检查/Users/Quan/Library/apktool/framework/
路径下是否包含1.apk,否则会通过android-framework.jar生成1.apk,然后通过该apk作为framework的路径。通过apktool -p可以指定 framework的路径。
结论:导致以上编译失败的原因很可能是,前期用过的apktool版本比较低,在编译api level较高的apk时候出现资源找不到的情况,我们只需要删除/Users/Quan/Library/apktool/framework/1.apk
然后最新版本的apktool重新执行编译命令就可以。
在重新打包我们的APK后,其实app是不能够正常运行的,首页会在application初始化中抛出apk签名错误的异常,然后退出。
2.2 修改smali的技巧
可以直接修改后或者通过编写java文件先生成dex然后通过反编译smali之后进行在源文件中插入。
javac helloworld.java #编译生成.class文件
dx --dex --output=helloworld.dex helloworld.class #生成dex
apktool d helloworld.dex #反编译生成smali
2.3 调试APK
1.反编译插入xml android:debuggable="true"
2.修改系统默认的配置ro.debuggable=1
此文不赘述。
参考:https://www.bodkin.ren/index.php/archives/533/
3 关于apk的签名
3.1 签名和数字证书
签名:
消息发送方生成一对公私钥,消息发送过程中:
1)对要发送的原始消息提取消息摘要;
2)对提取的信息摘要用自己的私钥加密。
数字证书
一般包含以下一些内容:
1)证书的发布机构(Issuer)
2)证书的有效期(Validity)
3)消息发送方的公钥
4)证书所有者(Subject)
5)数字签名所使用的算法
6)数字签名
3.2 APK签名工具
jarsign
是Java本生自带的一个工具,可以对jar进行签名signapk
是后面专门为了Android应用程序apk进行签名的工具,7.0之后强制要求使用该工具进行签名
区别:
- 使用的签名文件不一样
jarsign工具签名时使用的是
signapk工具签名时使用的是pk8,x509.pem文件 -
生成的签名文件名不一样
jarsign的签名文件名包含了keystore的别名
signapk固定的名字,默认为CERT
- 使用的默认签名算法不一样
jarsign默认使用sha-256做摘要算法
signapk 默认使用sha-1做摘要算法
3.3 keystore和pk8,x509.pem可互相转换
在线转换地址:https://myssl.com/cert_convert.html
3.4 APK签名后文件
signapk签名apk之后会包含以下文件,位于META-INF。
1.MANIFEST.MF
Manifest-Version: 1.0
Built-By: Generated-by-ADT
Created-By: Android Gradle 3.0.1
Name: AndroidManifest.xml
SHA1-Digest: OOwo62rpBBm58uyA9DaVke/pjYM= 对资源和源码文件sha1散列,base64加密
Name: META-INF/android.arch.core_runtime.version
SHA1-Digest: BeF7ZGqBckDCBhhvlPj0xwl01dw=
Name: META-INF/android.arch.lifecycle_extensions.version
SHA1-Digest: BeF7ZGqBckDCBhhvlPj0xwl01dw=
Name: META-INF/android.arch.lifecycle_livedata-core.version
SHA1-Digest: BeF7ZGqBckDCBhhvlPj0xwl01dw=
Name: META-INF/android.arch.lifecycle_livedata.version
SHA1-Digest: BeF7ZGqBckDCBhhvlPj0xwl01dw=
Name: META-INF/android.arch.lifecycle_runtime.version
SHA1-Digest: BeF7ZGqBckDCBhhvlPj0xwl01dw=
Name: META-INF/android.arch.lifecycle_viewmodel.version
SHA1-Digest: BeF7ZGqBckDCBhhvlPj0xwl01dw=
Name: META-INF/proguard/com.rong360.cccredit.base.comInputWidget.BaseV
iewHolder.pro
SHA1-Digest: 2A1Gn5+G2UZfIRuOe+B8eayD3h4=
Name: META-INF/proguard/com.rong360.cccredit.base.view.BaseItemView.pr
o
SHA1-Digest: 5NlorzmAOWo2s54IOAzJ6fpbRso=
Name: META-INF/rxjava.properties
SHA1-Digest: vKB1Ac/XQ8wiPI/th9N8DZ/+T9Y=
Name: assets/getui_popup_bg.9.png
SHA1-Digest: 0i2ug9zD61+mUJM+VRU0su+rZBA=
Name: assets/getui_popup_close.png
SHA1-Digest: 3PxcPZ1vRVg/shO9Q8m4kCl+++0=
Name: assets/home_producing.json
SHA1-Digest: F7l2niyrW1QfhVDBJwGIfu9OIqU=
Name: assets/loading_blue.json
SHA1-Digest: aARfFQziHhwO3GhIK9Yy1aowXYc=
2.CERT.SF
Signature-Version: 1.0
Created-By: 1.0 (Android)
SHA1-Digest-Manifest: wMk2wph8ittVFJEdaSigTm0sCiU=
//对MANIFEST.MF SHA1+base64
Name: AndroidManifest.xml
SHA1-Digest: hgUK+DK9MfGkBwlilMAQGpievZ8=
//对MANIFEST.MF中块+2次回车 SHA1+base64
Name: META-INF/android.arch.core_runtime.version
SHA1-Digest: OPQCkzMXJVPQryHeMowVNZmfRMw=
Name: META-INF/android.arch.lifecycle_extensions.version
SHA1-Digest: 6wRGJv7GN0UPnHsBck6G9DEz/wQ=
Name: META-INF/android.arch.lifecycle_livedata-core.version
SHA1-Digest: TSBGEIW1zN2n2sraHWcuRYSO8JU=
Name: META-INF/android.arch.lifecycle_livedata.version
SHA1-Digest: 2SW0lUzWE/vv2diFzKyGpyt9JW8=
Name: META-INF/android.arch.lifecycle_runtime.version
SHA1-Digest: yMBVn3OtS/Z9YTo02MlcvU6yfFM=
Name: META-INF/android.arch.lifecycle_viewmodel.version
SHA1-Digest: 5aVTTikyBvFlGq287kN/q6fe0kA=
3.CERT.RSA
证书文件,包含了.sf文件的签名信息(sha256+私钥加密)公钥信息和发布机构信息
3.5 查看证书信息
通过openssl工具可以查看rsa文件,该文件包含了摘要算法、加密算法、签名和公钥等信息。
如下:
4 项目中APK的签名验证
验证应用的签名证书信息和release包真实的签名是否一致,因为私钥不可能被人获取。这也是app store上认领app的方式。
5 通过xposed绕过签名验证机制
由于目标的apk有在初始化进行证书hash的认证,我需要通过以下方法绕过签名验证机制。
两方式:
1.修改Signature
的方法
so中可以用IDA进行搜索,通过IDA分析so库中签名散列值。
通过hook android.content.pm.Signature#hashCode
方法的返回值,将其置为和通过逆向得到的值一致,从而达到了绕过签名验证的机制。
2.Hook系统的PMS服务
public static void hookPms(Context context, String signed, int hashCode){
try{
// 获取全局的ActivityThread对象
Class<!--?--> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod =
activityThreadClass.getDeclaredMethod("currentActivityThread");
Object currentActivityThread = currentActivityThreadMethod.invoke(null);
// 获取ActivityThread里面原始的sPackageManager
Field sPackageManagerField = activityThreadClass.getDeclaredField("sPackageManager");
sPackageManagerField.setAccessible(true);
Object sPackageManager = sPackageManagerField.get(currentActivityThread);
// 准备好代理对象, 用来替换原始的对象
Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
new Class<!--?-->[] {Class.forName("android.content.pm.IPackageManager") },
new PmsHookBinderInvocationHandler(sPackageManager, signed, hashCode));
// 替换掉ActivityThread里面的 sPackageManager 字段
sPackageManagerField.set(currentActivityThread, proxy);
// 替换 ApplicationPackageManager里面的 mPM对象
PackageManager pm = context.getPackageManager();
Field mPmField = pm.getClass().getDeclaredField("mPM");
mPmField.setAccessible(true);
mPmField.set(pm, proxy);
}catch (Exception e){
}
}
public class PmsHookBinderInvocationHandler implements InvocationHandler{
private Object base;
//应用正确的签名信息
private String SIGN;
private int hashCode = 0;
public PmsHookBinderInvocationHandler(Object base, String sign, int hashCode) {
try {
this.base = base;
this.SIGN = sign;
this.hashCode = hashCode;
} catch (Exception e) {
}
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if("getPackageInfo".equals(method.getName())){
Integer flag = (Integer)args[1];
if(flag == PackageManager.<em>GET_SIGNATURES</em>){
Signature sign = new Signature(SIGN);
if(hashCode != 0){
try{
Class<!--?--> clazz = sign.getClass();
Field mHaveHashCodeF = clazz.getDeclaredField("mHaveHashCode");
mHaveHashCodeF.setAccessible(true);
mHaveHashCodeF.set(sign, true);
Field mHashCodeF = clazz.getDeclaredField("mHashCode");
mHashCodeF.setAccessible(true);
mHashCodeF.set(sign, hashCode);
}catch(Exception e){
}
}
PackageInfo info = (PackageInfo) method.invoke(base, args);
info.signatures[0] = sign;
return info;
}
}
return method.invoke(base, args);
}
}
private void getSignature() {
try {
PackageInfo packageInfo = getPackageManager().getPackageInfo(
getPackageName(), PackageManager.<em>GET_SIGNATURES</em>);
if (packageInfo.signatures != null) {
Log.d("", "sig:"+packageInfo.signatures[0].toCharsString()+
"hashcode:"+packageInfo.signatures[0].hashCode());
}
} catch (Exception e2) {
}
}
该方法不需要借助xposed也无需root,将方法插入application首行(通过smali来插入),然后编译成apk。因为获取签名服务的在Security#init中所以我们可以在此之前hook 住pms,从而修改签名的信息。Hook部分的代码参考Android Hook技术实践。