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

試崗項目

項目內容

開發一個 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 的經驗,而且我在這里也給新人們說一個事情,那就是面試官非常喜歡在看雪發表過優秀文章的人,就比如說我之前的帖子被加為優秀后被我寫在了簡歷里,之后面試的每一個面試官都對這個事情非常的感興趣,好吧,我承認是我的簡歷平平無奇沒有別的看點,但在這里也還是希望和我一樣的新人在看雪多發文章一起交流,一起進步。