首發安全客 鏈接: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 王鐵頭於公司辦公樓
相關參考:
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