安卓逆向面試題匯總 技術篇


首發安全客 鏈接:https://www.anquanke.com/post/id/246020
因為之前發完之后發現某些地方,描述不精確,所以這里做了一點微調

大家好,我是王鐵頭 一個乙方安全公司搬磚的菜雞
持續更新移動安全,iot安全,編譯原理相關原創視頻文章。
因為本人水平有限,文章如果有錯誤之處,還請大佬們指出,誠心誠意的接受批評。

簡介
這篇文章詳細講解了,安卓面試經常會問到的幾個技術問題。
以及相關的背景知識,技術原理。
文章中用到的資料代碼 和作者的其他技術文章 看這里:https://github.com/wangtietou/Wtt_Mobile_Security


本菜雞大概面試了30多家公司,因為學歷比較差(大專),很多公司看了簡歷直接就把我刷了。或者簡歷沒看就把我刷了,在boss直聘上看到大佬已讀不回 簡直是常規操作了。

很多時候根本走不到技術那里。

走到技術那里后,面試失敗的概率大概30%左右,有時候是因為我技術菜,有時候是因為要做的細分領域不太一致不太想干,有時候是因為談不攏工資(我想多要一點,對方不給,哈哈哈哈)。


除了面試經驗比較多,面試別人的經驗也比較多。

因為我在公司時間也比較長,把之前招我進來的同事成功熬走了,所以現在android逆向面試,移動安全面試這塊也是我當面試官。

所以,不管是面試還是被面試,我鐵頭多少也有一些經驗。


安卓逆向面試題匯總 技術篇

面試官經常問的幾個問題如下:

  1. 常見的加固手段有哪些
  2. 安卓反調試一般有哪些手段,怎么去防范
  3. arm匯編 b bl bx blx 這些指令是什么意思
  4. ida xx操作的快捷鍵是哪個?
  5. Xposed hook 原理 frida hook 原理
  6. inline hook原理
  7. ollvm 代碼混淆你了解嗎?要怎么去處理

上面是一個匯總的目錄,下面一個一個仔細拆分 詳細說說


安卓逆向面試題詳解

1)常見的加固手段

網上有的人把安卓殼分成五代殼,有人分成三代殼。

不同的人對這塊的,具體的區分和看法不同,但是五代殼更細分一些。

在加固廠商內部,用的是五代殼的標准,當然他們PPT已經出現了第6 ,7 ,8代殼。
我入行以及搬磚的時候,周圍人用的基本都是下圖的標准,所以我這里用五代殼來描述。

安卓逆向面試問什么 安卓五代殼

安卓逆向面試問題 安卓五代殼詳細
上面的圖把安卓五代殼的優缺點,實現邏輯講的非常好。大佬們理解了上面這兩張圖,回答第一個問題基本就ok了。


但是,大哥們既然看到了我這個文章,大佬們就可以風騷一點多說一些,說些面試官也不知道的。
畢竟, 唬不住5k,唬得住50k
說完上面的大概就是個及格分,說點下面的,面試官如果不了解這塊的話當時就被你給唬住了。


大佬們如果在公司負責甲方安全,采購過企業版加固,或者在加固廠商搬過磚的話就會知道。
加固雖然大體上分為免費版和企業版。
免費版里面有的公司基本沒啥加固選項,上傳個apk應用包梭哈就完事了。

比如這種。

逆向面試題匯總 加固選項

有的公司還是 比較人性化的,用戶可以根據自己需求選擇加固選項。比如這種

逆向面試題匯總 加固選項 可選擇

可以看到,免費版這里,廠商玩的花樣並不多,有的就是上傳一個包,啥加固選項沒有,有的雖然有,加固選項也就幾個。

但是企業版這里,廠商們花樣都比較多。
假設某加固公司,企業版實現了6個功能(一般是十幾個 二十幾個 我這里做個比喻)。
功能如下:

  1. 簽名校驗。
  2. 密鑰白盒
  3. 反xposed frida
  4. 源代碼深度混淆
  5. h5加固
  6. 內存防dump

這上面的功能是插件化的,你可以根據實際應用場景選擇其中幾個功能,也可以都要。
比如你的app根本么有h5頁面,你選個h5加固不是白花錢嗎。
這里套餐不同,價格也是不同的。(企業殼大概一年幾萬吧)
銷售那里不同的功能組合有不同的報價,就像A公司選了1,3,5。 你選了 2,4,6. 雖然都是企業版,但是你和別人的企業版還是有區別的。

說這些就是表示,不同apk即使用了同一家廠商的企業版加固,選擇的加固方式也是不一樣的。

而且,一些行業的客戶,加固廠商各自也會有針對行業的一些加固手法。
比如一些手游,加固廠商就會有一些反外掛的操作,針對內存讀寫的強檢測,一些金融客戶哪,因為對用戶信息保密程度要求高,就會做一些安全鍵盤和防錄屏截屏操作。

這里一些加固公司還把加固方式也做成了插件化,比如一個apk,同時用2代殼和4代殼的加固方式都用上。2代殼不落地加載結合4代殼dexvmp,或者3代殼指令抽取結合4代殼dexvmp,這里混合也是他們的常用套路,不會影響app正常運行。

說到這里有的大佬可能會疑惑,2代3代4代不是不同的加固方式嗎?是怎么結合的哪?這里我解釋一下
假設加固廠商拿到了一個未加固的dex, 那么2 3 4代殼子是怎么結合的。

  1. dex比較重要的部分,比如算法部分,登錄模塊,這塊的方法內容被抽取轉換成自定義的指令格式,然后調用系統底層的jni方法執行。(4代殼dexvmp)

  2. 其他不重要方法體直接抽空, 單獨加密,運行的時候方法體內容再動態還原(3代抽取)。

  3. 加載這個dex的時候(現在的dex已經經過了上面2步處理 里面的方法很多被抽空,一些被dexvmp保護), 並不是寫出到文件系統用 dexclassloader這樣的api去加載, 而是讀到內存中直接加載,直接調用c層API加載內存中的dex(2代不落地加載)

還有一些更深度的定制,反正有錢就是大爺,你錢多干啥都可以商量,一般企業殼加固后你還是可以看到廠商的特征加固文件。比如你看到libjiagu.so就覺得是360 ,深度定制后,特征文件你一個都找不到,而且還可以實現一些定制化的需求。

image-20210615203933776

企業版功能插件化,套餐化,加殼方式組合這些東西,一般來說很多人是不知道的,所以說說這些,能很快的把你從眾多普通面試者中區分出來。

安卓逆向要問的問題

把這一點說上,到時候面試官說不定因為過於欣賞你,把他大學剛畢業,沒有男朋友的妹妹介紹給你了。

所以,當面試官問加固方式這塊的時候,你除了把兩張圖的內容說清楚,還可以清清嗓子,一臉高手寂寞的神情。

悠悠地說:
其實吧,很多我搞過的企業殼,看的出來挺多都是定制化的,有的是2代殼結合4代殼的加固,有的是2代3代混合4代。
感覺很多企業殼根據不同的業務場景,買了不同的加固套餐,比如xx應用,我脫殼的時候,發現有 清場sdk, ollvm混淆。 另一個企業殼根本就沒有這些,大部分邏輯在后端,不過也搞了密鑰白盒和H5加固。
還有一些游戲的企業殼,內存讀寫明顯防護是比較厲害的。金融這塊的也基本都有安全鍵盤,和防截屏的一些保護。
這時候,狀若無意的對面試官說:“你說是吧”。

perfect.

2)安卓逆向反調試的手段有哪些

這里比較常用的反調試手段有

  1. ptrace檢測

    背景知識:ptrace是linux提供的API, 可以監視和控制進程運行,可以動態修改進程的內存,寄存器值。一般被用來調試。ida調試so,就是基於ptrace實現的。

    因為一個進程只能被ptrace一次, 所以進程可以自己ptrace自己,這樣ida和別的基於ptrace的工具和調試器或就無法調試這個進程了。
    實現代碼:

int check_ptrace()
{
	// 被調試返回-1,正常運行返回0
	int n_ret = ptrace(PTRACE_TRACEME, 0, 0, 0);
	if(-1 == n_ret)
	{
		printf("阿偶,進程正在被調試\n");
		return -1;
	}

	printf("沒被調試 返回值為:%d\n",n_ret);
	return 0;
}

定位方法:直接在ptrace函數下斷點。
繞過方法:手動patch,或者用frida之類的工具hook ptrace直接返回0.
實例演示

逆向面試問的問題 反調試 ptrace


  1. TracerPid檢測:

背景知識:TracerPid是進程的一個屬性值,如果為0,表示程序當前沒有被調試,如果不為0,表示正在被調試, TracerPid的值是調試程序的進程id。
實現代碼:

#define MAX_LENGTH 260

//獲取tracePid
int get_tarce_pid()
{
    //初始化緩沖區變量和文件指針
    char c_buf_line[MAX_LENGTH] = {0};
    char c_path[MAX_LENGTH] = {0};
    FILE* fp = 0;

    //初始化n_trace_pid 獲取當前進程id
    int n_pid = getpid();
    int n_trace_pid = 0;

    //拼湊路徑 讀取當前進程的status
    sprintf(c_path, "/proc/%d/status", n_pid);
    fp = fopen(c_path, "r");

    //打不開文件就報錯
    if (fp == NULL)
    {
        return -1;
    }

    //讀取文件 按行讀取 存入緩沖區
    while (fgets(c_buf_line, MAX_LENGTH, fp))
    {
        //如果沒有搜索到TracerPid 繼續循環
        if (0 == strstr(c_buf_line, "TracerPid"))
        {
            memset(c_buf_line, 0, MAX_LENGTH);
            continue;
        }

        //初始化變量
        char *p_ch = c_buf_line;
        char c_buf_num[MAX_LENGTH] = {0};

        //把當前文本行 包含的數字字符串 轉成數字
        for (int n_idx = 0; *p_ch != '\0'; p_ch++)
        {
            //比較當前字符的ascii碼  看看是不是數字
            if (*p_ch >= 48 && *p_ch <= 57)
            {
                c_buf_num[n_idx] = *p_ch;
                n_idx++;
            }
        }
        n_trace_pid = atoi(c_buf_num);
        break;
    }

    fclose(fp);
    return n_trace_pid;
}

相關特征 定位方法: 一般檢測TracerPid都會讀取 /proc/進程號/status 這個文件
所以可以直接搜索 /status 這種字符串,這里也會用到getpid, fgets這種API,所以也可以通過這兩個api定位。
繞過手法

  1. 直接手動patch, nop掉調用
  2. 編譯內核,修改linux kernel源代碼,讓 TracerPid永久為0. 修改方法 https://cloud.tencent.com/developer/article/1193431
    實例演示

這里用android studio 調試app 查看app進程對應的 status,status里查看TracerPid的值

TracerPid 反調試原理

反調試 TracerPid原理

可以看到TracerPid的值 是調試器的進程id。

沒被調試 TracerPid是0

沒被調試的時候,TracerPid的值是0。


  1. 自帶調試檢測函數android.os.Debug.isDebuggerConnected()

背景知識:自帶調試檢測api, 被調試時候返回 true, 否則返回 false。

import static android.os.Debug.isDebuggerConnected;

public static boolean is_debug()
{
    boolean b_ret = isDebuggerConnected();
    return b_ret;
}

相關特征 定位方法: 直接搜索isDebuggerConnected函數名即可。
繞過手法:frida之類的工具直接hook函數,直接返回false.

自帶調試函數 isDebuggerConnected


  1. 檢測調試器端口 比如 ida 23946 frida 27042 之類的
    背景知識:調試器服務端默認會打開一些特定端口,方便客戶端通過電腦usb線,或者直接通過局域網進行連接。
    實現代碼:
//返回找到的特征端口數量
int check_debug_port()
{
    //特征端口字符串數組  0x5D8A是23946的十六進制 69a2是27042十六進制
    //這里為了提高精確度 加個 :
    char* p_strPort_ary[] = {":5D8A", ":69A2"};
    int n_port_num = 2;  //特征端口數量

    //找到特征端口數量 返回值
    int n_find_num = 0;

    //初始化文件指針  路徑  和緩沖區
    FILE* fp = 0;
    char c_line_buf[MAX_LENGTH] = {0};
    char* p_str_tcp = "/proc/net/tcp";

    fp = fopen(p_str_tcp, "r");
    if(NULL == fp )
    {
        return -1;
    }

    //讀取文件 看當前文件包含了幾個特征端口號
    while(fgets(c_line_buf, MAX_LENGTH - 1, fp))
    {
        for (int i = 0; i < n_port_num; ++i)
        {
            //如果從當前文本行 找到特定端口號
            char* p_line = p_strPort_ary[i];
            if(NULL != strstr(c_line_buf, p_line))
            {
                n_find_num++;
            }
        }
        memset(c_line_buf, 0, MAX_LENGTH);
    }

    fclose(fp);
    //返回找到的特征端口數量
    return n_find_num;
}

相關特征 定位方法:讀取端口時,一般都會讀取 /proc/net/tcp文件,所以可以搜索關鍵字,或者 popen(管道執行命令) fgets(讀取文件行)這種api進行定位。

案例演示

這里啟動 frida_server,然后查看/proc/net/tcp文件內容,果然發現了frida_server對應的端口。

frida_server /proc/net/tcp 反調試

/proc/net/tcp 調試端口

067a2 27042 反調試

繞過手法:換個端口就可。

android_server 換端口
這里注意 -p 和 端口之間是沒有空格的 直接連接

/data/local/tmp/android_server -p8888 //運行android_server  以端口8888運行
adb forward tcp:8888 tcp:8888 		  //轉發端口 8888

frida-server 切換端口 這里切換成 6666端口

/data/local/tmp/frida_server -l 0.0.0.0:6666      //啟動frida_server 監聽6666
adb forward tcp:6666 tcp:6666				      //轉發6666端口
frida -H 127.0.0.1:6666 package_name -l hook.js   //注入js

  1. 根據時間差反調試
    背景知識:在關鍵邏輯的開始和結束的地方,獲取當前的秒數。結束時間減去開始時間,如果超過一定時間,認定是在調試。因為程序運行速度很快的,卡到2-3秒執行完,除非你邏輯好多,算法很復雜,要不基本不大可能。

    繞過方法:手動nop掉。

    案例演示

安卓逆向反調試 時間反調試

這里不用說的太全,說幾個常見的就行了。說全了時間也不太夠。

3)arm匯編 B、BL、BX、BLX區別和指令含義

這里對這幾條指令有個簡單記憶的方法 那就是對幾條指令中的字母單獨記憶,然后遇到字母的組合,就把字母代表的含義加起來就可了。

單獨記憶法:

字母 B: 跳轉 類似jmp
字母 L: 把下一條指令地址存入LR寄存器
字母 X: arm和thumb指令的切換

注意:這樣去記 是為了快速記住上面幾條指令的含義 而不是 單字母本身在匯編里面有這些含義


所以,4條指令的的含義就是

  1. B 這里跟x86匯編的 jmp比較像,可以理解成無條件跳轉

  2. BL :這里理解成 字母B + 字母L 作用是 把下一條指令地址存入LR寄存器 然后跳轉。 像x86匯編里面的 call , 只不過call指令把下一條指令的地址壓入棧,BL是把下一條指令的地址放到 LR寄存器。

  3. BX 這里理解成 字母B + 字母X 這里表示跳轉到一個地址,同時切換指令模式 當前如果是arm 就會切換成 Thumb 如果是Thumb 就會切換成arm

  4. BLX 這里是 字母B + 字母L + 字母X 表示跳轉到一個新的地址,跳轉的時候把下一條指令地址存入LR寄存器 同時切換指令模式 arm轉thumb thumb轉arm
    可以這樣去理解: blx = call + 切換指令模式

4)ida 使用 快捷鍵

G :跳轉到指定地址

ida快捷鍵G 跳轉到指定地址

Shift + F12:字符串窗口,用於字符串搜索

ida快捷鍵 搜索字符串

Y:修改變量類型 函數聲明快捷鍵

ida快捷鍵Y 修改變量類型

除了修改變量類型 也可以修改函數的返回值類型 和 參數類型

ida快捷鍵Y 修改函數聲明

X : 查看 變量 常量 函數 的引用

ida快捷鍵 X查看函數引用

在定位算法的時候 用x查看關鍵變量的引用也是很有效的一種方式

ida快捷鍵 X查看變量引用

同樣可以按X查看常量的引用 定位一些字符串到底在哪個函數還是蠻好用的

ida快捷鍵 X查看常量引用

Ctrl+S:查看節表

ida快捷鍵 查看節表

5)frida hook原理 xposed注入原理

  1. frida注入原理
    frida 注入是基於 ptrace實現的。frida 調用ptrace向目標進程注入了一個frida-agent-xx.so文件。后續騷操作是這個so文件跟frida-server通訊實現的
    ida調試也是基於 ptrace實現的。
    那為什么有人能動靜結合用 frida 和 ida一起調試哪?一個進程只能被ptrace一次,那這里為啥兩個能結合?
    答案是:先用frida注入,然后用調試器調試。
    frida在使用完ptrace之后 馬上就釋放了,並沒有一直占用,所以ida后續是可以附加,繼續使用ptrace的。

ida frida聯合調試apk

  1. xposed注入原理
    安卓所有的APP進程是用 Zygote(孵化器)進程啟動的。
    Xposed替換了 Zygote 進程對應的可執行文件/system/bin/app_process,每啟動一個新的進程,都會先啟動xposed替換過的文件,都會加載xposed相關代碼。這樣就注入了每一個app進程。

6)inline hook原理

這里 我畫了一個圖,大佬們自己看圖
原理描述:修改函數頭,跳轉到自定義函數,自定義函數就是自己想執行的邏輯,執行完自己的邏輯再跳轉回來。
inline hook原理

安卓逆向面試 inline hook 原理

7) ollvm 代碼混淆了解過嗎 ,一般怎么處理

一般這個難度的問題會放到靠后,除非你在簡歷里就寫了自己錘過很多 ollvm混淆過的代碼.
這里大佬們要是實在不會 對這塊沒啥了解,也建議大佬們掙扎一下,把下面我列的說一下 。也能爭取點卷面分

ollvm是一個代碼混淆的框架
這個框架通過以下三種方式實現了代碼混淆

英文全稱 簡稱/參數表示
控制流平坦化 Control Flow Flattening fla
虛假控制流 Bogus Control Flow bcf
指令替換 Instructions Substitution sub

這三種可以全部選擇。也可以隨意組合,具體怎樣組合看具體根據具體場景去決定。

下面一個一個詳細講解

  1. 被混淆前的源代碼 在ida中的樣子

    在沒有使用控制流平坦化之前 代碼在反編譯工具里面看的都是比較清晰的

#include <cstdio>

int main(int n_argc, char** argv)
{
	int n_num = n_argc * 2;
	//scanf("%2d", &n_num);

	if (20 == n_num)
	{
		puts("20");
	}
	if(10 == n_num)
	{
	  	puts("10");
	}
	if(2 == n_num)
	{
	  	puts("2");
	}

	puts("error");

	return -1;
}

拖入ida后 流程圖如下 這里可以看到流程還是很清晰的

沒有被混淆的ida流程圖

下面是 源代碼 加了不同參數后 被ollvm混淆后的樣子

源代碼被 ollvm -fla混淆后的樣子

源代碼被 ollvm -bcf混淆后的樣子

源代碼被 ollvm -sub混淆后的樣子
源代碼被 ollvm -fla -bcf -sub混淆后的樣子

這里我用自己的話簡單描述 ollvm的3種混淆方式

  1. fla 控制流平坦化
    混淆前混淆后如下圖所示:

    混淆前:
    ollvm fla混淆基本塊之前

混淆后:ollvm fla混淆基本塊之后

​ 代碼本來是依照邏輯順序執行的,控制流平坦化是把,原來的代碼的基本塊拆分。

​ 把本來順序執行的代碼塊用 switch case打亂分發,根據case值的變化,把原本代碼的邏輯連接起來。讓你不知 道代碼塊的原本順序。讓逆向的小老弟一眼看過去不知道所以然,不知道怎么去分析。

  1. bcf 虛假控制流:一般是通過全局變量,構造恆等式(一定會成立),和恆不等式(一定不成立),插入大量這種看似有用,實際上就是在為難你的代碼。

    if(x == 0)
    {
       ...代碼A 
    }
    if(y == 0)
    {
      ...代碼B
    }
    

    上面寫了兩段偽代碼。 假設 x的值是0 y的值是1
    那么 在上面的代碼中
    if(x == 0) 這個條件一定是成立的
    if(y == 0)這個條件是一定不成立的。

    bcf虛假控制流,通過構造x,y 這種全局變量。讓編譯器不能推斷x,y的值.(不透明維詞)
    通過大量插入一些跟上面類似的恆等式,和恆不等式(不可達分支),然后在這些分支在里面寫一些代碼,把原邏輯串聯起來。

  2. sub指令替換
    指令替換對程序的基本塊架構沒有任何影響。對比下面兩個圖 混淆跟沒有混淆進行對比之后,可以發現。
    程序控制流和基本塊的順序,執行流程沒有什么變化。

    當然這也跟這個函數基本沒啥運算指令有關系。

    源代碼被 ollvm 混淆之前

    源代碼被 ollvm -sub混淆后

    只是把 x = x + 1 這樣的代碼 替換成類似於 x = x + 2 + 1 - 2 這樣的代碼

    增大代碼體積,把簡單的指令變復雜。增大分析的難度

這里,大佬們在回答 ollvm這塊的話 把我上面寫的說一下就大概差不多了。

面試官如果問大佬們怎么解決:

大佬們可以這么說

  1. 通過unicorn 模擬執行去除控制流平坦化
    https://bbs.pediy.com/thread-252321.htm

  2. 通過angr 符號執行 去除控制流平坦化
    https://security.tencent.com/index.php/blog/msg/112

  3. 通過angr 符號執行 去除虛假控制流
    https://bbs.pediy.com/thread-266005.htm

  4. 通過Miasm符號執行移除OLLVM虛假控制流
    https://www.52pojie.cn/thread-995577-1-1.html

總結:

上面講解了安卓逆向面試中,經常問的幾個技術問題,背后的原理,該怎么回答。

當然除了技術篇,還會問一些發展方向,技術追求,看你穩定性之類的。

希望大佬們都能順利拿到 offer, 如果看完文章有所收獲,而且還順利入職的話,大佬們可以過來還願下。

以上

2021.7.1 王鐵頭於公司辦公樓

相關參考:
https://segmentfault.com/a/1190000037697547
https://blog.csdn.net/earbao/article/details/82379117
https://blog.csdn.net/qq_42186263/article/details/113711359

--文章結束--

持續更新移動安全,iot安全,編譯原理相關原創視頻文章
演示視頻:https://space.bilibili.com/430241559


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM