手把手教你逆向分析 Android 程序
注:本文转自企鹅的Bugly社区


ok,那我们从头说起……
1.反编译
我们先看一下 Apk 文件的结构吧,如下图:
2.res:资源文件,里面的 xml 格式文件在编译过程中由文本格式转化为二进制的 AXML 文件格式。
3.AndroidManifest.xml:Android 配置文件,编译过程依然被转换为 AXML 格式。
4.classes.dex:java 代码编译后产生的类似字节码的文件(dalvik 字节码)。
5.resources.arsc:具有 id 值资源的索引表(asserts 文件夹中的资源不会生成索引)。
6.其他文件:可由开发者自己添加,诸如 assets 等,或者 lib(native so 代码)等目录。


反编译后的目录结构如下:

1.Smali 是 Android 的 Dalvik 虚拟机所使用的一种 dex 格式的中间语言。
2.可以理解为,C 语言和汇编语言的编译与反编译,把 smali 理解为一种汇编语言。

1
2
3
|
.classpublicLcom/example/hacktest/MainActivity;
.super Landroid/app/Activity;
.source”MainActivity.java”
|
我们看一下,反编译后的关键代码:

我们可以把第一个红框位置的 if-eqz 改成 if-nez,这样你输入除了11的任何字符都会返回 true。
或者把第二个红框位置的 0x1,改成 0x0,(0代表 true),这样这个函数不管输入什么都返回 true。

以手机XX应用为例,当你按照上述步骤反编译,重新编译,签名之后,进入 APP 会出现这个页面,无法正常使用。
因为你并没有这个 APP 的正版签名文件(关于签名相关的东西,在后面我再仔细讲)。


1
2
3
|
PackageInfo pi = context.getPackageManager.().getPackageInfo(packname,packageManager.GET_SIGNATURES);
对应的smali代码类似是:
Landroid/content/pm/PackageInfo;->signatures:[Landroid/content/pm/Signature
|
1
2
|
const-string/jumbo v3,”dbbf****96b326003″
const-string/jumbo v0,”c388a****1578d5″
|
(PS:关于签名检测的除了 java 层的,可能还有再 so 里面校验的和服务器验证的方式,在 so 里的用 IDA 打开 so 跟踪修改,服务器验证的抓包查看,再模拟发包重放攻击就好了,这里就不具体介绍了)
2. Android 的签名保护机制到底是什么?
Echo Auto-sign Created By Dave Da illest 1
Echo Update.zip is now being signed and will be renamed to update_signed.zip
EXIT

Created-By: 1.0 (Android)
SHA1-Digest: AfPh3OJoypH966MludSW6f1RHg4=
SHA1-Digest: NaPhUBH5WO7uGk/CfRu/SHsCvW0=
SHA1-Digest: RRxOSvpmhVfCwiprVV/wZlaqQpw=
SHA1-Digest: Nq8q3HeTluE5JNCBpVvNy3BXtJI=
SHA1-Digest: kxwMyILwF2K+n9ziNhcQqcCGWIU=
SHA1-Digest: q7Ystu6WoSWih53RGKXtE3LeTdc=
SHA1-Digest: Ao1WOs5PXMxsWTDsjSijS2tfnHo=
SHA1-Digest: GVIfdEOBv4gEny2T1jDhGGsZOBo=
Created-By: 1.0 (Android)
SHA1-Digest-Manifest: pNZ9UXN9GMqTgqAwKD6uEN6aD34=
SHA1-Digest: cIga++hy5wqjHl9IHSfbg8tqCug=
SHA1-Digest: oRzzLkwuvxC78suvJcAEvTqcjSA=
SHA1-Digest: VY7kOF8E3rn8EUTvQC/DcBEN6kQ=
SHA1-Digest: stS7pUucSY0GgAVoESyO3Y7SanU=
SHA1-Digest: Yr3img6SqiKB+1kwcg/Fga2fwcc=
SHA1-Digest: j1g8I4fI9dM9hAFKEtS9dHsqo5E=
SHA1-Digest: Sci9MmGXNGnZ1d04rCrEEV7MWn4=
SHA1-Digest: KKqaLh/DVvFp+v1KoaDw7xETvrI=
3. Android 系统如何获取签名
1
2
3
4
|
packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES );
Signature[] signs = packageInfo.signatures;
md5 = getMD5Str(signs[0].toByteArray());
context.getPackageManager()其实拿到的是ApplicationPackageManager
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
@Override
publicPackageManager getPackageManager() {
if(mPackageManager !=null) {
returnmPackageManager ;
}
IPackageManager pm = ActivityThread.getPackageManager ();
if(pm !=null) {
// Doesn’t matter if we make more than one instance.
return(mPackageManager =newApplicationPackageManager(this, pm));
}
returnnull;
}
|
01
02
03
04
05
06
07
08
09
10
11
|
publicstaticIPackageManager getPackageManager() {
if(sPackageManager !=null) {
//Slog.v(“PackageManager”, “returning cur default = ” + sPackageManager);
returnsPackageManager ;
}
IBinder b = ServiceManager.getService(“package”);
//Slog.v(“PackageManager”, “default service binder = ” + b);
sPackageManager = IPackageManager.Stub.asInterface(b);
//Slog.v(“PackageManager”, “default service = ” + sPackageManager);
returnsPackageManager;
}
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
@Override
publicPackageInfo getPackageInfo(String packageName,intflags)
throwsNameNotFoundException {
try{
PackageInfo pi = mPM.getPackageInfo(packageName, flags, mContext.getUserId());
if(pi !=null) {
returnpi;
}
}catch(RemoteException e) {
thrownewRuntimeException(“Package manager has died”, e);
}
thrownewNameNotFoundException(packageName);
}
|

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
@Override
publicPackageInfo getPackageInfo(String packageName,intflags,intuserId) {
if(!sUserManager.exists(userId))returnnull;
enforceCrossUserPermission(Binder.getCallingUid (), userId,false,”get package info”);
// reader
synchronized(mPackages) {
PackageParser.Package p = mPackages.get(packageName);
if(DEBUG_PACKAGE_INFO)
Log.v (TAG,”getPackageInfo “+ packageName +”: “+ p);
if(p !=null) {
returngeneratePackageInfo(p, flags, userId);
}
if((flags & PackageManager. GET_UNINSTALLED_PACKAGES ) !=0) {
returngeneratePackageInfoFromSettingsLPw(packageName, flags, userId);
}
}
returnnull;
}
|

1
2
3
4
5
6
7
|
File dataDir = Environment. getDataDirectory();
mAppDataDir =newFile(dataDir,”data”);
mAppInstallDir =newFile(dataDir,”app”);
mAppLibInstallDir =newFile(dataDir,”app-lib”);
mAsecInternalPath =newFile(dataDir,”app-asec”).getPath();
mUserAppDataDir =newFile(dataDir,”user”);
mDrmAppPrivateInstallDir =newFile(dataDir,”app-private”);
|
1
2
3
4
5
6
7
8
|
if((flags&PackageManager. GET_SIGNATURES ) !=0) {
intN = (p.mSignatures !=null) ? p.mSignatures.length :0;
if(N >0) {
pi.signatures =newSignature[N];
System.arraycopy (p.mSignatures,0, pi. signatures,0, N);
}
}
returnpi;
|
好了,现在就是看这个PackageParser.Package是从哪来的了,通过跟踪代码,installPackageLI 和 scanPackageLI 中的:
1
|
finalPackageParser.Package pkg = pp.parsePackag (tmpPackageFile,null, mMetrics, parseFlags);
|

1.Cydia substrate : http://www.cydiasubstrate.com/
2.Xposed : http://repo.xposed.info/
网上也有很多教程可以看看。
下图就是我用了上面的方法产生的效果,还差点被微信部门的人请去喝茶。

4. 关于如何注入?
经过一系列的跟踪代码定位,最终定位到了这个类 cn.ledongli.ldl.cppwrapper.DailyStats 里的 f 方法(f 是因为代码混淆了)然后我们注入并 hook 方法,让它返回66666,ok,我们看到了如下效果:

1
2
3
4
5
6
7
|
if( packageName.equals(“cn.ledongli.ldl”)){
if( resultinstanceofPackageInfo) {
PackageInfo info = (PackageInfo) result;
info. signatures[0] =newSignature( myHexLedongli);
param.setResult( info);
}
}
|
1.信息反馈:通过界面的一些弹出信息,界面特点寻找突破点。
2.特征函数:比如搜 Toast,Log,getSignature 等。
3.代码注入:把 toast 或者 log 函数注入到程序中,跟踪位置。
4.打印堆栈:插入 new Exception(“定位”).printStackTrace();
5.网络抓包:通过抓包得到的关键字段,在代码中定位。
这篇文章整理了有一段时间了,觉得还是应该写出来,也不是什么高深的技术文章,就是个人总结的一点心得而已。
