
由于筆者在此之前完全沒有安卓逆向的工作經驗,所以對方在面試結束后提出遠程試崗七天,從而觀察筆者的工作能力,結果因為筆者設備的問題無法達到對方的要求,只能說這大概就是有緣無份吧,這里做一份零基礎的總結,會一步一步記錄自己踩得每一個坑以及心路歷程,希望能給后來的新人一些指引。
試崗項目

項目內容
開發一個 xposed 插件,可以在 whatsApp 中導入通訊錄功能,輸入是手機號,輸出是這個手機號對應的id和個人信息,對方還跟貼心的給出了項目預覽圖,應該是對方近期接到的項目,也可以看出對方沒有白嫖我的意思。

關鍵代碼定位
點開APP隨便瀏覽了一下功能,根據對方給出的預覽圖,可以知道首先是需要定位這個界面的 onCreat 界面,首先考慮的就是直接搜字符串,比如“邀請使用”這四個字,但是拖入 jadx 一番搜索后什么也沒有。

這時我就想到,會不是因為是國外的app,默認是英文所以沒搜到,于是我把軟件調整為英文,觀察到英文界面存在Contants Help 等字樣,并逐一進行了搜索,但依然沒有結果,這里推測可能是對字符串進行了加密處理。

字符串走不通就換條路,既然是定位界面,那么顯然通過 adb 命令查看最上層的界面是個好辦法,這里得有點耐心,多翻一翻找到 whatsapp 相關的地方,可以看到當前界面為 ACTIVITY com.whatsapp/.contact.picker.ContactPicker
C:\Users\Administrator>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 到工程中供使用;
切換至 Project 模式,在app目錄下新建文件夾lib,將 XposedBridgeApi-82.jar 復制到 app/lib 文件夾下。
◆配置依賴;
右鍵工程 — Open Module Setting — Dependencies — app — Declared Dependencies — 點擊加號 — JAR/ARR Dependencies
Step 1:lib/XposedBridgeApi-82.jar
Step 2:compileOnly — OK
◆新建 Empty Activity 并在 AndroidManifest.xml 中添加代碼;
"xposedmodule" android:value="true"/>"xposeddescription" android:value="Xposed模塊示例"/>"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 { @Override 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 ,我喜歡偷懶直接刷機,畢竟點擊鼠標更簡單。
因為已經將刷機包中的boot.img進行了替換,所以可以簡單的再重新刷一下機就可以了。
使用 fastboot flash boot boot.img命令僅刷入boot.img。
安裝edxposed
nexus5
筆者最開始使用的設備是nexus5,筆者先后經歷了:
安卓4.4系統無法安裝新版magisk(安裝舊版解決);
安裝 riru 提示sdk版本過低(手機升級安卓6.0解決);
安裝新版 riru 模塊需要android 8.0(安裝舊版riru);
找不到舊版本的 edxposed (百度了一下午的帖子,找了資源);
edxposed 和 riru 的版本不匹配導致安裝失敗(又翻了一天的帖子,找了一大堆資源挨個試);
好不容易edxposed和riru都安裝上了,手機重啟無法開機;
這個安裝過程大概歷時三天,此時我的心態已然崩潰(因為試崗七天已經過了4天,買設備也來不及),直接開始躺平,這種狀態一直持續到試崗失敗,退出群聊。

小米6X
事情的轉機來自于我老媽說她的小米6X電池不太行了,此時的我轉念一想,換個手機我手里不就有個安卓9的手機了么,就這樣,小米6X就變成了我的Android逆向工程機。
小米6X的edxposed安裝依然遇到版本問題,這里我總結一下使用的版本:
◆Magisk-v23.0.apk
◆riru-v25.4.4-release.zip
◆EdXposed-v0.5.2.2_4683-master-release.zip
◆EdXposedManager-4.6.2-46200-org.meowcat.edxposed.manager-release.apk
安裝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需要權限,分別為android.permission.READ_CONTACTS 和 android.permission.READ_CONTACTS。
◆數據庫中直接看到的 mimetype_id 項并不存在,該項為多表查詢,真實字段為 mimetype,可以通過在代碼中遍歷列名觀察到。
◆mimetype 是 String類型,而不是在數據庫中看到的 int 類型。
◆添加聯系人時,應先在 raw_contacts 中添加一個空項,然后再在 data 中添加各種數據。
◆注意:雖然添加了讀寫通訊錄的權限,但依然要在手機中手動配置應用讀寫通訊錄的權限,這里我踩過坑!
如果不會的話請移步我之前的筆記中 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 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 的經驗,而且我在這里也給新人們說一個事情,那就是面試官非常喜歡在看雪發表過優秀文章的人,就比如說我之前的帖子被加為優秀后被我寫在了簡歷里,之后面試的每一個面試官都對這個事情非常的感興趣,好吧,我承認是我的簡歷平平無奇沒有別的看點,但在這里也還是希望和我一樣的新人在看雪多發文章一起交流,一起進步。
看雪學苑
安全圈
嘶吼專業版
看雪學苑
系統安全運維
安全圈
GoUpSec
黑白之道
關鍵基礎設施安全應急響應中心
安全圈
LemonSec
看雪學苑