本文为看雪论坛优秀文章
看雪论坛作者ID:简单的简单
由于笔者在此之前完全没有安卓逆向的工作经验,所以对方在面试结束后提出远程试岗七天,从而观察笔者的工作能力,结果因为笔者设备的问题无法达到对方的要求,只能说这大概就是有缘无份吧,这里做一份零基础的总结,会一步一步记录自己踩得每一个坑以及心路历程,希望能给后来的新人一些指引。
试岗项目
项目内容
开发一个 xposed 插件,可以在 whatsApp 中导入通讯录功能,输入是手机号,输出是这个手机号对应的id和个人信息,对方还跟贴心的给出了项目预览图,应该是对方近期接到的项目,也可以看出对方没有白嫖我的意思。
关键代码定位
点开APP随便浏览了一下功能,根据对方给出的预览图,可以知道首先是需要定位这个界面的 onCreat 界面,首先考虑的就是直接搜字符串,比如“邀请使用”这四个字,但是拖入 jadx 一番搜索后什么也没有。
这时我就想到,会不是因为是国外的app,默认是英文所以没搜到,于是我把软件调整为英文,观察到英文界面存在Contants Help 等字样,并逐一进行了搜索,但依然没有结果,这里推测可能是对字符串进行了加密处理。
字符串走不通就换条路,既然是定位界面,那么显然通过 adb 命令查看最上层的界面是个好办法,这里得有点耐心,多翻一翻找到 whatsapp 相关的地方,可以看到当前界面为 ACTIVITY com.whatsapp/.contact.picker.ContactPicker
C:UsersAdministrator>adb shell dumpsys activity top
........
TASK com.whatsapp id=167 userId=0
ACTIVITY com.whatsapp/.contact.picker.ContactPicker 9264d37 pid=4208
Local Activity 7762ff6 State:
mResumed=true mStopped=false mFinished=false
mChangingConfigurations=false
mCurrentConfig={1.0 ?mcc?mnc [zh_CN] ldltr sw392dp w392dp h714dp 440dpi nrml long port finger -keyb/v/h -nav/h winConfig={ mBounds=Rect(0, 0 - 1080, 2030) mAppBounds=Rect(0, 0 - 1080, 2030) mWindowingMode=fullscreen mActivityType=standard} s.8 themeChanged=0 themeChangedFlags=0}
mLoadersStarted=true
Active Fragments in 59ae076:
#0: 05m{499b677 #0 androidx.lifecycle.LifecycleDispatcher.report_fragment_tag}
mFragmentId=#0 mContainerId=#0 mTag=androidx.lifecycle.LifecycleDispatcher.report_fragment_tag
mState=5 mIndex=0 mWho=android:fragment:0 mBackStackNesting=0
mAdded=true mRemoving=false mFromLayout=false mInLayout=false
mHidden=false mDetached=false mMenuVisible=true mHasMenu=false
mRetainInstance=false mRetaining=false mUserVisibleHint=true
mFragmentManager=FragmentManager{59ae076 in HostCallbacks{21522e4}}
mHost=android.app.Activity$HostCallbacks@21522e4
Child FragmentManager{3f9834d in 05m{499b677}}:
FragmentManager misc state:
mHost=android.app.Activity$HostCallbacks@21522e4
mContainer=android.app.Fragment$1@1cc8e02
mParent=05m{499b677 #0 androidx.lifecycle.LifecycleDispatcher.report_fragment_tag}
mCurState=5 mStateSaved=false mDestroyed=false
在 jadx 中找到 ContactPicker 的 onCreat 方法,接下来只要直接 HOOK onCreat 方法就成功一半了。
XPosed插件安装
xposed的开发环境配置其实我在另一篇笔记里写过,这里为了大家方便就粘贴过来一份。
环境配置
环境配置较为繁琐,分为以下步骤
◆复制 XposedBridgeApi-82.jar 到工程中供使用;
◆配置依赖;
◆新建 Empty Activity 并在 AndroidManifest.xml 中添加代码;
<meta-data android:name = "xposedmodule" android:value="true"/>
<meta-data android:name = "xposeddescription" android:value="Xposed模块示例"/>
<meta-data android:name = "xposedminversion" android:value="54"/>
◆新建入口类 Main.java 并实现 IXposedHookLoadPackage 接口;
package com.example.xposeddemo;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.callbacks.XC_LoadPackage;
public class Main implements IXposedHookLoadPackage {
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable {
}
}
◆复制入口类名;
右键入口类 Main — Copy Path/Reference — Copy Reference
◆配置入口类名文件。
app/src/main 文件夹下新建文件夹 assets,app/src/main/assets 文件夹下新建文件 xposed_init,将复制的入口类名粘贴在文件中即可。
Hook函数
这里就不讲过多的理论了,jadx中右键想要hook的方法可以直接生成xposed的代码片段,这样我们就有了现成的框架。
Main.java
XposedHelpers.findAndHookMethod("com.whatsapp.contact.picker.ContactPicker", classLoader, "onCreate", android.os.Bundle.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
}
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
Log.d("lxz","hook start");
}
});
本以为简单的hook却成了噩梦的开始,因!为!有!反!调!试!
反调试对抗
最开始笔者是在nexus5中直接安装的xposed框架,应该是软件检测了这个框架,在jadx中可以看到是有一个Native层AbortHook方法的。
正在笔者一筹莫展的时候,对方询问了一下我的进度,好嘛,打了瞌睡就有枕头,对方直接给了反调对抗思路,那就是用面具刷edxposed。
Magisk Root
刷机
1、进入开发者模式,打开USB调试。
2、执行 ./adb reboot bootloader 或关机状态同时按住“音量减”和“电源”直到手机开机,进入 bootloader。
3、按“音量减”,直到选项移至“Recovery mode”。
4、按“电源”启动恢复模式。此时屏幕上会显示带有红色感叹号的 Android 机器人。
5、按住“电源”,在按住电源的同时,按一次“音量加”按钮,然后马上松开“电源”。
6、按“音量减”,选中“Wipe data/factory reset”进行双清。
7、刷机压缩包后解压,解压后里面的压缩包还要再解压(system.img、boot.img、recovery.img 要看到这几个文件),注意,解压出来的文件要复制到上一层目录中,不然会提示找不到文件。
8、修改 flash-all.bat 中的 fastboot -w update image-hammerhead-lmy48b.zip 为:
① fastboot flash recovery recovery.img
② fastboot flash boot boot.img
③ fastboot flash system system.img
9、清除个人数据的(双WIPE),根据个人情况上添加在 flash-all.bat 中
① fastboot flash cache cache.img
② fastboot flash userdata userdata.img
10、双击 flash-all.bat。
我觉着在MagiskRoot前最好先刷一下机,因为这样才能保证你提取的boot文件和手机的系统是对应的,其实刷机还是蛮简单的,谷歌的手机双击bat就可以,小米手机官网有现成的刷机工具,这里就说两个坑,一个是fastboot模式中遇到 wait for devices 的问题,这其实是你的电脑还缺少一个驱动,根据我的经验,下载驱动精灵,它会提示你再安装一个驱动就可以了,另一个坑就是小米的刷机工具右下角默认是刷机后lock,这tm就简直是坑爹,记得改成双清,不然又tm把bl给锁上了(lock再刷机会有0s问题,需要重新解锁)。
提取boot.img
在官网下载手机的刷机包,反复解压,直到找到其中的boot.img文件,把这个文件拷贝到手机中。
修改boot.img
在手机中安装 magisk.apk,依次点击,安装 — 选项 — 下一步 — 方式 — 选择修补一个文件 — 选择刚刚存放在手机中的 boot.img 文件 — 开始,等待执行结束你会发现在 boot.img 所在的目录中多了一个文件(有时候这个文件在电脑中看不见,在手机中重命名后就能看见了,不知道为啥),将这个文件拷贝到刷机包 boot.img 所在的目录,将刷机包原本的boot.img 重命名为 boot.img.bak ,将magisk 生成的这个文件重命名为 boot.img,此时刷机包中的 boot.img 就被 magisk 生成的 boot.img 替换了。
刷入boot.img
有两种方式刷入修改后的 boot.img ,我喜欢偷懒直接刷机,毕竟点击鼠标更简单。
安装edxposed
nexus5
笔者最开始使用的设备是nexus5,笔者先后经历了:
小米6X
事情的转机来自于我老妈说她的小米6X电池不太行了,此时的我转念一想,换个手机我手里不就有个安卓9的手机了么,就这样,小米6X就变成了我的Android逆向工程机。
小米6X的edxposed安装依然遇到版本问题,这里我总结一下使用的版本:
安装xposed插件后,可以看到此时已经成功Hook(nexus5坑我不浅!!!)
Xposed插件开发
此时我们先考虑在改界面添加一个TextView,那么问题就变成了获取Context的问题,根据之前学习的经验,可以通过 findAndHookConstructor来解决,下面上代码:
package com.example.xposeddemo;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.Gravity;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.TextView;
import android.widget.Toast;
import android.content.ContentResolver;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;
public class Main implements IXposedHookLoadPackage {
private String packageName = "com.whatsapp";
private String className = packageName + ".contact.picker.ContactPicker";
Context context;
@Override
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable {
hookMainAcivityInit(loadPackageParam);
XposedHelpers.findAndHookMethod(className,
loadPackageParam.classLoader, "onCreate", android.os.Bundle.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
Log.d("lxz", "hook start");
//获取界面
final Activity mActivity = (Activity) param.thisObject;
//创建一个 TextView
TextView textView = new TextView(context);
// 创建布局,设置参数
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
// 设置控件到底端的距离
params.bottomMargin = 100;
// 设置控件的位置
params.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
// 设置控件文本
textView.setText("hook text");
// 添加 TextView 到 Activity 中
mActivity.addContentView(textView,params);
}
});
}
private void hookMainAcivityInit(XC_LoadPackage.LoadPackageParam loadPackageParam)
{
String packageName = loadPackageParam.packageName;
if(!packageName.equals(packageName))
return;
Class hookClass = XposedHelpers.findClass(
className,loadPackageParam.classLoader);
XposedHelpers.findAndHookConstructor(
hookClass,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
context = (Context) param.thisObject;
}
}
);
}
}
重启手机后也如预期般的,显示了 hook text 字样。
接下来我们只要遍历通讯录后把内容设置到 textView 上就可以了,这部分的内容在之前的 Android 安全笔记中也有提过,读写系统应用通讯录的ContentProvider,其重点在于以下几点:
如果不会的话请移步我之前的笔记中 ContentProvider 部分,这里我们就不啰嗦了,直接上完整代码。
package com.example.xposeddemo;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.Gravity;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.TextView;
import android.widget.Toast;
import android.content.ContentResolver;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;
public class Main implements IXposedHookLoadPackage {
private String packageName = "com.whatsapp";
private String className = packageName + ".contact.picker.ContactPicker";
Context context;
@Override
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable {
hookMainAcivityInit(loadPackageParam);
hookAnonymousInternalClass(loadPackageParam);
}
private void hookMainAcivityInit(XC_LoadPackage.LoadPackageParam loadPackageParam)
{
String packageName = loadPackageParam.packageName;
if(!packageName.equals(packageName))
return;
Class hookClass = XposedHelpers.findClass(
className,loadPackageParam.classLoader);
XposedHelpers.findAndHookConstructor(
hookClass,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
context = (Context) param.thisObject;
}
}
);
}
private void hookAnonymousInternalClass(XC_LoadPackage.LoadPackageParam loadPackageParam) {
if(loadPackageParam.packageName.equals(packageName)){
Log.d("lxz","xposed loading");
final Class<?> mMainActivity = XposedHelpers.findClass(className,loadPackageParam.classLoader);
XposedHelpers.findAndHookMethod(mMainActivity, "onCreate", Bundle.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
final Activity mActivity = (Activity) param.thisObject;
Log.d("lxz","onCreate已加载...");
// 创建一个 TextView
TextView textView = new TextView(context);
// 创建布局,设置参数
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
// 设置控件到底端的距离
params.bottomMargin = 0;
// 设置控件的位置
params.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
List<PersonInfo> personInfoList = new ArrayList<>();
ContentResolver resolver = context.getContentResolver();
Uri uri = Uri.parse("content://com.android.contacts/raw_contacts");
Cursor cursor = resolver.query(uri,new String[]{"_id","display_name"},null,null,null,null);
if(cursor != null)
{
while (cursor.moveToNext())
{
int id = cursor.getInt(0);
String name = cursor.getString(1);
Log.d("lxz","id = " + id + " name = " + name);
uri = Uri.parse("content://com.android.contacts/raw_contacts/"+id+"/data");
Cursor cursor2 = resolver.query(uri,new String[]{"mimetype","raw_contact_id","data1"},null,null,null,null);
PersonInfo personInfo = new PersonInfo();
while(cursor2.moveToNext())
{
String mimetype = cursor2.getString(0);
int raw_contact_id = cursor2.getInt(1);
String data1 = cursor2.getString(2);
Log.d("lxz", "minetype = " + mimetype + " address = " + data1);
personInfo.set_id(raw_contact_id);
if(mimetype.equals("vnd.android.cursor.item/phone_v2"))
{
personInfo.setNumber(data1);
} else if (mimetype.equals("vnd.android.cursor.item/postal-address_v2")) {
personInfo.setAddress(data1);
} else if (mimetype.equals("vnd.android.cursor.item/email_v2")) {
personInfo.setEmail(data1);
} else if (mimetype.equals("vnd.android.cursor.item/name")) {
personInfo.setName(data1);
}
}
personInfoList.add(personInfo);
}
}
String ss = new String();
for (PersonInfo personInfo : personInfoList)
{
Log.d("lxz",personInfo.toString());
ss = ss + personInfo.toString();
}
textView.setText(ss);
// 添加 TextView 到 Activity 中
mActivity.addContentView(textView,params);
}
});
}
}
}
重启后可以看到通讯录的详细信息已经出现在了 whatsapp 中,主体框架已经搭建完毕,剩下就是一些排版和琐碎的工作,这里就不继续演示了(毕竟已经退出群聊了,而且我发现我好像 hook 错界面了,尴尬ing…)
总结与收获
这次的试岗可以说收获颇丰,学习(踩坑)并巩固了非常多的知识点,这都是之前逆向 creakme 不曾遇到的问题,最重要的是 whatsapp 也算是知名度较高的 app 了,今后的面试官问起来也算是有逆向分析过大型 app 的经验,而且我在这里也给新人们说一个事情,那就是面试官非常喜欢在看雪发表过优秀文章的人,就比如说我之前的帖子被加为优秀后被我写在了简历里,之后面试的每一个面试官都对这个事情非常的感兴趣,好吧,我承认是我的简历平平无奇没有别的看点,但在这里也还是希望和我一样的新人在看雪多发文章一起交流,一起进步。
看雪ID:简单的简单
https://bbs.kanxue.com/user-home-950902.htm
# 往期推荐
3、安卓加固脱壳分享
球分享
球点赞
球在看
原文始发于微信公众号(看雪学苑):记一次试岗实战项目