安卓逆向面試題彙總 技術篇
首發安全客 連結:https://www.anquanke.com/post/id/246020
因為之前發完之後發現某些地方,描述不精確,所以這裡做了一點微調
大家好,我是王鐵頭 一個乙方安全公司搬磚的菜雞
持續更新移動安全,iot安全,編譯原理相關原創視訊文章。
因為本人水平有限,文章如果有錯誤之處,還請大佬們指出,誠心誠意的接受批評。
簡介
這篇文章詳細講解了,安卓面試經常會問到的幾個技術問題。
以及相關的背景知識,技術原理。
文章中用到的資料程式碼 和作者的其他技術文章 看這裡:https://github.com/wangtietou/Wtt_Mobile_Security
本菜雞大概面試了30多家公司,因為學歷比較差(大專),很多公司看了簡歷直接就把我刷了。或者簡歷沒看就把我刷了,在boss直聘上看到大佬已讀不回 簡直是常規操作了。
很多時候根本走不到技術那裡。
走到技術那裡後,面試失敗的概率大概30%左右,有時候是因為我技術菜,有時候是因為要做的細分領域不太一致不太想幹,有時候是因為談不攏工資(我想多要一點,對方不給,哈哈哈哈)。
除了面試經驗比較多,面試別人的經驗也比較多。
因為我在公司時間也比較長,把之前招我進來的同事成功熬走了,所以現在android逆向面試,移動安全面試這塊也是我當面試官。
所以,不管是面試還是被面試,我鐵頭多少也有一些經驗。
安卓逆向面試題彙總 技術篇
面試官經常問的幾個問題如下:
- 常見的加固手段有哪些
- 安卓反除錯一般有哪些手段,怎麼去防範
- arm彙編 b bl bx blx 這些指令是什麼意思
- ida xx操作的快捷鍵是哪個?
- Xposed hook 原理 frida hook 原理
- inline hook原理
- ollvm 程式碼混淆你瞭解嗎?要怎麼去處理
上面是一個彙總的目錄,下面一個一個仔細拆分 詳細說說
安卓逆向面試題詳解
1)常見的加固手段
網上有的人把安卓殼分成五代殼,有人分成三代殼。
不同的人對這塊的,具體的區分和看法不同,但是五代殼更細分一些。
在加固廠商內部,用的是五代殼的標準,當然他們PPT已經出現了第6 ,7 ,8代殼。
我入行以及搬磚的時候,周圍人用的基本都是下圖的標準,所以我這裡用五代殼來描述。
上面的圖把安卓五代殼的優缺點,實現邏輯講的非常好。大佬們理解了上面這兩張圖,回答第一個問題基本就ok了。
但是,大哥們既然看到了我這個文章,大佬們就可以風騷一點多說一些,說些面試官也不知道的。
畢竟, 唬不住5k,唬得住50k
說完上面的大概就是個及格分,說點下面的,面試官如果不瞭解這塊的話當時就被你給唬住了。
大佬們如果在公司負責甲方安全,採購過企業版加固,或者在加固廠商搬過磚的話就會知道。
加固雖然大體上分為免費版和企業版。
免費版裡面有的公司基本沒啥加固選項,上傳個apk應用包梭哈就完事了。
比如這種。
有的公司還是 比較人性化的,使用者可以根據自己需求選擇加固選項。比如這種
可以看到,免費版這裡,廠商玩的花樣並不多,有的就是上傳一個包,啥加固選項沒有,有的雖然有,加固選項也就幾個。
但是企業版這裡,廠商們花樣都比較多。
假設某加固公司,企業版實現了6個功能(一般是十幾個 二十幾個 我這裡做個比喻)。
功能如下:
- 簽名校驗。
- 金鑰白盒
- 反xposed frida
- 原始碼深度混淆
- h5加固
- 記憶體防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代殼子是怎麼結合的。
-
dex比較重要的部分,比如演算法部分,登入模組,這塊的方法內容被抽取轉換成自定義的指令格式,然後呼叫系統底層的jni方法執行。(4代殼dexvmp)
-
其他不重要方法體直接抽空, 單獨加密,執行的時候方法體內容再動態還原(3代抽取)。
-
載入這個dex的時候(現在的dex已經經過了上面2步處理 裡面的方法很多被抽空,一些被dexvmp保護), 並不是寫出到檔案系統用 dexclassloader這樣的api去載入, 而是讀到記憶體中直接載入,直接呼叫c層API載入記憶體中的dex(2代不落地載入)
還有一些更深度的定製,反正有錢就是大爺,你錢多幹啥都可以商量,一般企業殼加固後你還是可以看到廠商的特徵加固檔案。比如你看到libjiagu.so就覺得是360 ,深度定製後,特徵檔案你一個都找不到,而且還可以實現一些定製化的需求。
企業版功能外掛化,套餐化,加殼方式組合這些東西,一般來說很多人是不知道的,所以說說這些,能很快的把你從眾多普通面試者中區分出來。
把這一點說上,到時候面試官說不定因為過於欣賞你,把他大學剛畢業,沒有男朋友的妹妹介紹給你了。
所以,當面試官問加固方式這塊的時候,你除了把兩張圖的內容說清楚,還可以清清嗓子,一臉高手寂寞的神情。
悠悠地說:
其實吧,很多我搞過的企業殼,看的出來挺多都是定製化的,有的是2代殼結合4代殼的加固,有的是2代3代混合4代。
感覺很多企業殼根據不同的業務場景,買了不同的加固套餐,比如xx應用,我脫殼的時候,發現有 清場sdk, ollvm混淆。 另一個企業殼根本就沒有這些,大部分邏輯在後端,不過也搞了金鑰白盒和H5加固。
還有一些遊戲的企業殼,記憶體讀寫明顯防護是比較厲害的。金融這塊的也基本都有安全鍵盤,和防截圖的一些保護。
這時候,狀若無意的對面試官說:“你說是吧”。
perfect.
2)安卓逆向反除錯的手段有哪些
這裡比較常用的反除錯手段有
-
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.
例項演示
-
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定位。
繞過手法:
- 直接手動patch, nop掉呼叫
- 編譯核心,修改linux kernel原始碼,讓 TracerPid永久為0. 修改方法 https://cloud.tencent.com/developer/article/1193431
例項演示:
這裡用android studio 除錯app 檢視app程序對應的 status,status裡檢視TracerPid的值
可以看到TracerPid的值 是偵錯程式的程序id。
沒被除錯的時候,TracerPid的值是0。
-
自帶除錯檢測函式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.
- 檢測偵錯程式埠 比如 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對應的埠。
繞過手法:換個埠就可。
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
-
根據時間差反除錯
背景知識:在關鍵邏輯的開始和結束的地方,獲取當前的秒數。結束時間減去開始時間,如果超過一定時間,認定是在除錯。因為程式執行速度很快的,卡到2-3秒執行完,除非你邏輯好多,演算法很複雜,要不基本不大可能。繞過方法:手動nop掉。
案例演示:
這裡不用說的太全,說幾個常見的就行了。說全了時間也不太夠。
3)arm彙編 B、BL、BX、BLX區別和指令含義
這裡對這幾條指令有個簡單記憶的方法 那就是對幾條指令中的字母單獨記憶,然後遇到字母的組合,就把字母代表的含義加起來就可了。
單獨記憶法:
字母 B: 跳轉 類似jmp
字母 L: 把下一條指令地址存入LR暫存器
字母 X: arm和thumb指令的切換
注意:這樣去記 是為了快速記住上面幾條指令的含義 而不是 單字母本身在彙編裡面有這些含義
所以,4條指令的的含義就是
-
B 這裡跟x86彙編的 jmp比較像,可以理解成無條件跳轉
-
BL :這裡理解成 字母B + 字母L 作用是 把下一條指令地址存入LR暫存器 然後跳轉。 像x86彙編裡面的 call , 只不過call指令把下一條指令的地址壓入棧,BL是把下一條指令的地址放到 LR暫存器。
-
BX 這裡理解成 字母B + 字母X 這裡表示跳轉到一個地址,同時切換指令模式 當前如果是arm 就會切換成 Thumb 如果是Thumb 就會切換成arm
-
BLX 這裡是 字母B + 字母L + 字母X 表示跳轉到一個新的地址,跳轉的時候把下一條指令地址存入LR暫存器 同時切換指令模式 arm轉thumb thumb轉arm
可以這樣去理解: blx = call + 切換指令模式
4)ida 使用 快捷鍵
G :跳轉到指定地址
Shift + F12:字串視窗,用於字串搜尋
Y:修改變數型別 函式宣告快捷鍵
除了修改變數型別 也可以修改函式的返回值型別 和 引數型別
X : 檢視 變數 常量 函式 的引用
在定位演算法的時候 用x檢視關鍵變數的引用也是很有效的一種方式
同樣可以按X檢視常量的引用 定位一些字串到底在哪個函式還是蠻好用的
Ctrl+S:檢視節表
5)frida hook原理 xposed注入原理
- frida注入原理
frida 注入是基於 ptrace實現的。frida 呼叫ptrace向目標程序注入了一個frida-agent-xx.so檔案。後續騷操作是這個so檔案跟frida-server通訊實現的
ida除錯也是基於 ptrace實現的。
那為什麼有人能動靜結合用 frida 和 ida一起除錯哪?一個程序只能被ptrace一次,那這裡為啥兩個能結合?
答案是:先用frida注入,然後用偵錯程式除錯。
frida在使用完ptrace之後 馬上就釋放了,並沒有一直佔用,所以ida後續是可以附加,繼續使用ptrace的。
- xposed注入原理
安卓所有的APP程序是用 Zygote(孵化器)程序啟動的。
Xposed替換了 Zygote 程序對應的可執行檔案/system/bin/app_process,每啟動一個新的程序,都會先啟動xposed替換過的檔案,都會載入xposed相關程式碼。這樣就注入了每一個app程序。
6)inline hook原理
這裡 我畫了一個圖,大佬們自己看圖
原理描述:修改函式頭,跳轉到自定義函式,自定義函式就是自己想執行的邏輯,執行完自己的邏輯再跳轉回來。
7) ollvm 程式碼混淆瞭解過嗎 ,一般怎麼處理
一般這個難度的問題會放到靠後,除非你在簡歷裡就寫了自己錘過很多 ollvm混淆過的程式碼.
這裡大佬們要是實在不會 對這塊沒啥瞭解,也建議大佬們掙扎一下,把下面我列的說一下 。也能爭取點卷面分
ollvm是一個程式碼混淆的框架
這個框架通過以下三種方式實現了程式碼混淆
英文全稱 | 簡稱/引數表示 | |
---|---|---|
控制流平坦化 | Control Flow Flattening | fla |
虛假控制流 | Bogus Control Flow | bcf |
指令替換 | Instructions Substitution | sub |
這三種可以全部選擇。也可以隨意組合,具體怎樣組合看具體根據具體場景去決定。
下面一個一個詳細講解
-
被混淆前的原始碼 在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後 流程圖如下 這裡可以看到流程還是很清晰的
下面是 原始碼 加了不同引數後 被ollvm混淆後的樣子
這裡我用自己的話簡單描述 ollvm的3種混淆方式
-
fla 控制流平坦化:
混淆前混淆後如下圖所示:混淆前:
混淆後:
程式碼本來是依照邏輯順序執行的,控制流平坦化是把,原來的程式碼的基本塊拆分。
把本來順序執行的程式碼塊用 switch case打亂分發,根據case值的變化,把原本程式碼的邏輯連線起來。讓你不知 道程式碼塊的原本順序。讓逆向的小老弟一眼看過去不知道所以然,不知道怎麼去分析。
-
bcf 虛假控制流:一般是通過全域性變數,構造恆等式(一定會成立),和恆不等式(一定不成立),插入大量這種看似有用,實際上就是在為難你的程式碼。
if(x == 0) { ...程式碼A } if(y == 0) { ...程式碼B }
上面寫了兩段虛擬碼。 假設 x的值是0 y的值是1
那麼 在上面的程式碼中
if(x == 0) 這個條件一定是成立的
if(y == 0)這個條件是一定不成立的。bcf虛假控制流,通過構造x,y 這種全域性變數。讓編譯器不能推斷x,y的值.(不透明維詞)
通過大量插入一些跟上面類似的恆等式,和恆不等式(不可達分支),然後在這些分支在裡面寫一些程式碼,把原邏輯串聯起來。 -
sub指令替換
指令替換對程式的基本塊架構沒有任何影響。對比下面兩個圖 混淆跟沒有混淆進行對比之後,可以發現。
程式控制流和基本塊的順序,執行流程沒有什麼變化。當然這也跟這個函式基本沒啥運算指令有關係。
只是把 x = x + 1 這樣的程式碼 替換成類似於 x = x + 2 + 1 - 2 這樣的程式碼
增大程式碼體積,把簡單的指令變複雜。增大分析的難度
這裡,大佬們在回答 ollvm這塊的話 把我上面寫的說一下就大概差不多了。
面試官如果問大佬們怎麼解決:
大佬們可以這麼說
-
通過unicorn 模擬執行去除控制流平坦化
https://bbs.pediy.com/thread-252321.htm -
通過angr 符號執行 去除控制流平坦化
https://security.tencent.com/index.php/blog/msg/112 -
通過angr 符號執行 去除虛假控制流
https://bbs.pediy.com/thread-266005.htm -
通過Miasm符號執行移除OLLVM虛假控制流
https://www.52pojie.cn/thread-995577-1-1.html
總結:
上面講解了安卓逆向面試中,經常問的幾個技術問題,背後的原理,該怎麼回答。
當然除了技術篇,還會問一些發展方向,技術追求,看你穩定性之類的。
希望大佬們都能順利拿到 offer, 如果看完文章有所收穫,而且還順利入職的話,大佬們可以過來還願下。
以上
2021.7.1 王鐵頭於公司辦公樓
大佬們可以這麼說
-
通過unicorn 模擬執行去除控制流平坦化
https://bbs.pediy.com/thread-252321.htm -
通過angr 符號執行 去除控制流平坦化
https://security.tencent.com/index.php/blog/msg/112 -
通過angr 符號執行 去除虛假控制流
https://bbs.pediy.com/thread-266005.htm -
通過Miasm符號執行移除OLLVM虛假控制流
https://www.52pojie.cn/thread-995577-1-1.html
相關參考:
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