面試復(fù)盤:底層機(jī)制與安全對(duì)抗)
我之前復(fù)盤過不少安全廠商的客戶端開發(fā)崗面試但奇安信這輪Windows開發(fā)方向的面試確實(shí)是值得單獨(dú)拎出來寫一篇的。原因很簡(jiǎn)單它考的不是那種“你背過八股就能過”的題而是把Windows底層機(jī)制和你日常寫代碼的習(xí)慣揉在一起考稍不留神就會(huì)暴露出“只會(huì)調(diào)API、不懂原理”的問題。這篇文章我會(huì)把2020年這輪面試的準(zhǔn)備過程、考到的核心知識(shí)點(diǎn)、以及我當(dāng)時(shí)踩過的坑完整梳理一遍重點(diǎn)是拆解“安全類Windows客戶端開發(fā)”到底在考什么、應(yīng)該怎么準(zhǔn)備而不是簡(jiǎn)單羅列面試題。這篇文章適合兩類人一類是準(zhǔn)備投奇安信或類似安全廠商客戶端開發(fā)崗位的候選人另一類是已經(jīng)入行但想補(bǔ)一補(bǔ)Windows底層和安全基礎(chǔ)的后端或應(yīng)用層開發(fā)。因?yàn)槊嬖嚴(yán)锬切﹩栴}本質(zhì)上都是在檢驗(yàn)?zāi)銓?duì)Windows這個(gè)系統(tǒng)本身的理解深度。1. 崗位畫像與備考思路拆解1.1 奇安信客戶端開發(fā)的“安全底色”先說結(jié)論奇安信的客戶端開發(fā)崗位不是普通的企業(yè)應(yīng)用客戶端它帶有強(qiáng)烈的安全產(chǎn)品基因。安全廠商的Windows客戶端通常承載的是終端安全管控、病毒查殺、主機(jī)防護(hù)、EDR端點(diǎn)檢測(cè)與響應(yīng)這類能力。這就決定了面試官考察的不只是你會(huì)不會(huì)寫MFC、Qt或WPF界面而是你是否理解Windows的系統(tǒng)機(jī)制比如進(jìn)程、線程、內(nèi)存、DLL、注冊(cè)表、服務(wù)。你是否知道安全產(chǎn)品在系統(tǒng)里的存活方式比如自我保護(hù)、驅(qū)動(dòng)加載、Hook、事件監(jiān)控。你是否具備排查復(fù)雜系統(tǒng)問題的能力比如內(nèi)存泄漏、句柄泄漏、藍(lán)屏關(guān)聯(lián)分析。你是否了解攻擊者常用的Windows機(jī)制利用手法比如DLL劫持、注入、提權(quán)。因?yàn)樽霭踩目蛻舳吮举|(zhì)上是在和攻擊者搶這座系統(tǒng)大樓的控制權(quán)。我在準(zhǔn)備這輪面試時(shí)把“客戶端開發(fā)”四個(gè)字拆成了三層來看最表面的一層是UI和業(yè)務(wù)邏輯中間一層是Windows系統(tǒng)編程能力最底層是安全對(duì)抗思維。2020年的奇安信面試恰恰是跳過表面那層UI直接往中間和底層扎。1.2 面試考察的核心能力模型根據(jù)我這輪面試的經(jīng)驗(yàn)奇安信Windows客戶端開發(fā)崗的考察點(diǎn)可以概括為四個(gè)維度維度考察內(nèi)容常見出題形式語言基礎(chǔ)C/C語法、內(nèi)存模型、STL、智能指針、RAII概念問答、手寫代碼Windows核心機(jī)制進(jìn)程線程、內(nèi)存管理、PE結(jié)構(gòu)、消息機(jī)制、DLL原理追問、場(chǎng)景分析系統(tǒng)與網(wǎng)絡(luò)編程文件操作、注冊(cè)表、服務(wù)、Socket、加密接口調(diào)用項(xiàng)目拷問、方案設(shè)計(jì)安全敏感度常見漏洞原理、Hook機(jī)制、對(duì)抗思路、防護(hù)方案案例分析、開放性問題這里要特別提醒一句不要以為安全廠商的客戶端開發(fā)只考安全知識(shí)C和Windows基礎(chǔ)永遠(yuǎn)是第一關(guān)。我當(dāng)時(shí)一面被問了至少20分鐘的C多態(tài)和內(nèi)存布局后面的安全相關(guān)問題反而是穿插在項(xiàng)目經(jīng)歷里問的?;A(chǔ)不扎實(shí)連提問環(huán)節(jié)都撐不到。1.3 我當(dāng)時(shí)的學(xué)習(xí)路線規(guī)劃準(zhǔn)備周期大概是三周左右我按“由底向上、先基礎(chǔ)后對(duì)抗”的順序排的第一周主攻C核心和Win32編程基礎(chǔ)。重點(diǎn)復(fù)習(xí)了虛函數(shù)表、智能指針源碼實(shí)現(xiàn)、STL容器底層結(jié)構(gòu)、內(nèi)存對(duì)齊規(guī)則同時(shí)把《Windows核心編程》里進(jìn)程、線程、內(nèi)存映射、DLL相關(guān)章節(jié)重新刷了一遍。第二周主攻Windows機(jī)制和安全技術(shù)專題。包括PE結(jié)構(gòu)手動(dòng)解析、消息鉤子、IAT Hook、內(nèi)聯(lián)Hook的原理與實(shí)現(xiàn)、DLL注入的幾種常見方式、進(jìn)程自我保護(hù)思路。這一周信息量最大也是面試中值錢的部分。第三周整理項(xiàng)目經(jīng)歷和模擬面試。把之前做過的客戶端項(xiàng)目按“背景-技術(shù)棧-難點(diǎn)-解決方案”重新梳理把可能被追問的細(xì)節(jié)全部寫下來然后對(duì)著鏡子練表達(dá)。這一周最容易被忽視但實(shí)際作用非常明顯?,F(xiàn)在回看這個(gè)順序的核心邏輯是先用基礎(chǔ)題建立信心再用安全專題拉開和普通客戶端開發(fā)者的差距最后用項(xiàng)目表達(dá)把技術(shù)點(diǎn)串成線。2. Windows核心機(jī)制考點(diǎn)拆解2.1 進(jìn)程與線程從API到內(nèi)核對(duì)象的“道與術(shù)”面試官問進(jìn)程和線程時(shí)不是聽你背“進(jìn)程是資源分配的最小單位線程是CPU調(diào)度的最小單位”這句話只值一分。真正值錢的是你能否說清楚以下幾個(gè)層級(jí)第一層是API層。創(chuàng)建進(jìn)程用CreateProcess創(chuàng)建線程用CreateThread線程同步用CriticalSection、Mutex、Semaphore、Event這些API的參數(shù)和返回值必須門兒清。我當(dāng)時(shí)被追問了CreateProcess的第十個(gè)參數(shù)lpStartupInfo以及CREATE_SUSPENDED標(biāo)志的用途。第二層是內(nèi)核對(duì)象層。你要明白CreateProcess、CreateThread創(chuàng)建的不是“進(jìn)程”或“線程”本身而是返回一個(gè)內(nèi)核對(duì)象句柄。內(nèi)核對(duì)象有引用計(jì)數(shù)有安全描述符句柄是否可繼承決定了子進(jìn)程能否訪問父進(jìn)程的內(nèi)核對(duì)象。第三層是調(diào)度與同步層。比如臨界區(qū)是用戶態(tài)對(duì)象進(jìn)入臨界區(qū)失敗時(shí)會(huì)在用戶態(tài)自旋等待而互斥量會(huì)陷入內(nèi)核態(tài)等待所以臨界區(qū)在短臨界區(qū)內(nèi)性能更好但無法跨進(jìn)程。我當(dāng)時(shí)把五類同步原語臨界區(qū)、互斥量、信號(hào)量、事件、可等待計(jì)時(shí)器的適用場(chǎng)景和性能差異做了個(gè)表面試時(shí)直接背出來面試官明顯比較滿意。第四層是高級(jí)話題。比如線程池、纖程、用戶模式調(diào)度UMS、線程局部存儲(chǔ)TLS。TLS這個(gè)問題我印象很深面試官問“如果要在DLL里維護(hù)每個(gè)線程獨(dú)立的變量你會(huì)怎么做”其實(shí)就是在考TLS我當(dāng)時(shí)一下愣住了后來才想起來。2.2 內(nèi)存管理虛擬內(nèi)存、堆和棧的“地盤之爭(zhēng)”Windows下內(nèi)存管理這塊面試官喜歡用連環(huán)問的方式把人問崩潰。我當(dāng)時(shí)遇到的連環(huán)問是這樣的先是“一個(gè)進(jìn)程能訪問另一個(gè)進(jìn)程的地址空間嗎”答案是正常情況下不能因?yàn)槊總€(gè)進(jìn)程有獨(dú)立的虛擬地址空間。接著問“那Windows怎么實(shí)現(xiàn)跨進(jìn)程內(nèi)存共享”答案是內(nèi)存映射文件或共享內(nèi)存底層是物理頁映射到多個(gè)進(jìn)程的虛擬地址空間。再問“VirtualAlloc和HeapAlloc有什么區(qū)別”答案是VirtualAlloc直接以頁為單位分配虛擬內(nèi)存適合大塊內(nèi)存和特殊用途HeapAlloc是在堆上做小塊內(nèi)存分配內(nèi)部會(huì)調(diào)用VirtualAlloc維護(hù)堆段。然后問到了堆的實(shí)現(xiàn)細(xì)節(jié)。Windows的堆管理器Heap Manager會(huì)維護(hù)空閑鏈表、塊頭信息分配小于某閾值的塊時(shí)用未提交后備列表lookaside list加速。這塊如果不看《深入解析Windows操作系統(tǒng)》很難回答得清楚我當(dāng)時(shí)也因?yàn)槎褖K結(jié)構(gòu)說得不夠細(xì)被追問了兩輪。還有一個(gè)小細(xì)節(jié)內(nèi)存對(duì)齊。面試官給了個(gè)結(jié)構(gòu)體讓算sizeof這種題目考的其實(shí)就是對(duì)齊規(guī)則。我當(dāng)時(shí)快速回答了默認(rèn)對(duì)齊系數(shù)是8字節(jié)且以結(jié)構(gòu)體成員中的最大對(duì)齊數(shù)為準(zhǔn)面試官點(diǎn)頭之后又追加了一個(gè)問題——“如果你要設(shè)計(jì)一個(gè)網(wǎng)絡(luò)協(xié)議包結(jié)構(gòu)該怎么處理對(duì)齊問題”答案是必須用#pragma pack(push, 1)或按字節(jié)序列化因?yàn)榭缙脚_(tái)傳輸時(shí)不同編譯器的對(duì)齊規(guī)則會(huì)造成結(jié)構(gòu)體大小不一致。這個(gè)問題后來成了我復(fù)盤時(shí)的重點(diǎn)筆記。2.3 DLL機(jī)制與導(dǎo)出符號(hào)安全視角下的“后門入口”DLL這塊安全廠商面試官問的角度通常很刁鉆。常見的問題鏈?zhǔn)堑谝粏朌LL的加載順序是什么答案大概是應(yīng)用程序目錄、系統(tǒng)目錄、Windows目錄、當(dāng)前目錄、PATH環(huán)境變量目錄但你需要補(bǔ)一句“開啟SafeDllSearchMode后當(dāng)前目錄會(huì)被調(diào)整到系統(tǒng)目錄之后”這一點(diǎn)能體現(xiàn)你真的理解而不是背過。第二問什么是DLL劫持攻擊者利用加載順序或搜索路徑把一個(gè)同名的惡意DLL放到受害應(yīng)用優(yōu)先加載的目錄里從而讓應(yīng)用加載惡意代碼。預(yù)防手段包括用絕對(duì)路徑加載、調(diào)用SetDllDirectory移除當(dāng)前目錄、加載前校驗(yàn)DLL的數(shù)字簽名、使用延遲加載或Manifest指定依賴。第三問如果要在不修改目標(biāo)進(jìn)程代碼的前提下讓目標(biāo)進(jìn)程加載你自己的DLL有哪些方法這就是在考DLL注入。常規(guī)答案有遠(yuǎn)程線程注入CreateRemoteThread LoadLibrary、注入注冊(cè)表AppInit_DLLs鍵、SetWindowsHookEx消息鉤子、使用Windows消息WM_COPYDATA觸發(fā)LoadLibrary、APC注入、手動(dòng)映射注入manual map。我當(dāng)時(shí)主動(dòng)提到了手動(dòng)映射注入不調(diào)用LoadLibrary所以不產(chǎn)生模塊加載通知面試官補(bǔ)了一句“那你怎么隱藏內(nèi)存中的代碼”我答了內(nèi)存掃描檢測(cè)和Unhook EAT/IAT等思路。說實(shí)話DLL這塊是安全客戶端開發(fā)的分水嶺。普通桌面應(yīng)用開發(fā)者可能幾年不碰DLL注入但安全客戶端幾乎天天跟這些東西打交道因?yàn)榻K端側(cè)的任何代碼執(zhí)行監(jiān)控、Hook、防御動(dòng)作都繞不開DLL。2.4 消息機(jī)制與窗口過程Windows應(yīng)用的“心臟跳動(dòng)”Windows的窗口消息機(jī)制也是面試必問。我當(dāng)時(shí)被問了這么幾個(gè)問題消息隊(duì)列分幾種答系統(tǒng)消息隊(duì)列和線程消息隊(duì)列每個(gè)GUI線程有一條自己的線程消息隊(duì)列。PostMessage和SendMessage有什么區(qū)別答前者把消息投遞到隊(duì)列后立即返回屬于異步后者直接調(diào)用窗口過程等窗口過程返回后才返回屬于同步。注意SendMessage是直接調(diào)用的所以即使在消息循環(huán)阻塞時(shí)也能送達(dá)而PostMessage依賴消息循環(huán)。如果要在窗口線程外更新UI應(yīng)該怎么做答不能直接跨線程操作UI對(duì)象要用SendMessage或PostMessage通知UI線程或者使用更上層的Invoke機(jī)制。什么是消息鉤子SetWindowsHookEx能干什么答它能在消息到達(dá)目標(biāo)窗口過程之前攔截消息可以對(duì)特定線程或系統(tǒng)全局安裝鉤子全局鉤子必須放在DLL里因?yàn)殂^子回調(diào)會(huì)被系統(tǒng)注入到其他進(jìn)程。這塊還延伸到了性能問題。比如消息循環(huán)里如果執(zhí)行了耗時(shí)操作會(huì)卡界面正確的做法是耗時(shí)操作放到后臺(tái)線程完成后通過消息通知UI線程更新。面試官那時(shí)候問了“如果后臺(tái)線程瘋狂發(fā)送消息UI線程會(huì)不會(huì)出問題”答案是消息堆積會(huì)導(dǎo)致界面響應(yīng)變慢需要用SendMessage的同步特性來做流控或合并刷新消息防止頻繁重繪。3. 安全類客戶端的技術(shù)專項(xiàng)準(zhǔn)備3.1 Hook技術(shù)族監(jiān)控與反監(jiān)控的“矛與盾”安全客戶端要想實(shí)現(xiàn)“監(jiān)控能力”核心手段就是Hook。面試?yán)颒ook問題幾乎必出你需要至少準(zhǔn)備三條線第一條線是消息鉤子對(duì)應(yīng)SetWindowsHookEx可以攔截鼠標(biāo)鍵盤消息這算是入門級(jí)Hook。但面試官隨后會(huì)追問“消息鉤子能監(jiān)控其它進(jìn)程的所有API調(diào)用嗎”答案是不能消息鉤子只對(duì)消息類事件有效對(duì)文件讀寫、網(wǎng)絡(luò)通信、注冊(cè)表操作這類行為無能為力。第二條線是IAT Hook想法是修改PE導(dǎo)入地址表中的函數(shù)地址讓進(jìn)程調(diào)用某API時(shí)跳到自己的函數(shù)里。實(shí)現(xiàn)步驟大概是先通過GetModuleHandle拿到目標(biāo)模塊基地址解析PE頭找到導(dǎo)入表遍歷每個(gè)DLL的IAT找到目標(biāo)函數(shù)地址后用VirtualProtect修改內(nèi)存頁保護(hù)屬性為可寫再把地址替換為自己的函數(shù)地址。聽完這套流程面試官通常會(huì)緊接著問“IAT Hook有什么局限”答案是只能攔截通過IAT調(diào)用的API如果目標(biāo)程序在運(yùn)行時(shí)自己GetProcAddress拿地址IAT Hook就失效了。第三條線是Inline Hook也叫內(nèi)聯(lián)鉤子直接在目標(biāo)函數(shù)入口寫jmp指令跳轉(zhuǎn)到自定義函數(shù)。這是安全產(chǎn)品里非常常用的技術(shù)因?yàn)樗诖a層攔截不依賴導(dǎo)入表覆蓋面更廣。但代價(jià)是需要處理函數(shù)入口字節(jié)的保存和還原還要考慮多線程并發(fā)調(diào)用時(shí)機(jī)器碼被修改導(dǎo)致崩潰的風(fēng)險(xiǎn)。我當(dāng)時(shí)補(bǔ)充說現(xiàn)代安全工具還會(huì)用EAT Hook、SSDT Hook、驅(qū)動(dòng)層回調(diào)等更底層手段面試官聽完就沒有繼續(xù)在技術(shù)深度上施壓。3.2 PE文件結(jié)構(gòu)安全工程師的“解剖學(xué)”安全客戶端開發(fā)崗考PE結(jié)構(gòu)就像外科醫(yī)生考解剖學(xué)是基本功。需要掌握的關(guān)鍵部分包括DOS頭位于文件最前e_magic字段是MZ找到e_lfanew偏移后跳到PE頭。NT頭包含PE\0\0簽名、文件頭、可選頭在32位和64位下可選頭結(jié)構(gòu)不同。節(jié)表每個(gè)節(jié)的名稱、虛擬大小、文件偏移、特性標(biāo)志。導(dǎo)入表記錄依賴的DLL和函數(shù)。導(dǎo)出表記錄對(duì)外提供的函數(shù)序號(hào)和名稱。重定位表用于加載基址不一致時(shí)修正地址。資源段包含圖標(biāo)、字符串、版本信息等。光背結(jié)構(gòu)還不夠你得能說清楚“一個(gè)PE文件是如何被加載進(jìn)內(nèi)存的”。我當(dāng)時(shí)是這樣串的系統(tǒng)讀取DOS頭找到NT頭簽名校驗(yàn)通過后讀取節(jié)表信息按照節(jié)表的虛擬地址和虛擬大小用映射方式把文件內(nèi)容映射到內(nèi)存指定位置對(duì)于需要補(bǔ)零的節(jié)設(shè)置好零填充的結(jié)束位置然后遍歷導(dǎo)入表加載依賴的DLL并修正IAT地址如果有重定位需求則在加載地址與首選基址不符時(shí)修正最后調(diào)用入口點(diǎn)。這套“加載流水線”講清楚之后面試官通常會(huì)在上述某一步停下來深挖比如“IAT修正發(fā)生在什么時(shí)候會(huì)不會(huì)因?yàn)镈LL加載失敗而終止進(jìn)程”。3.3 權(quán)限與自我保護(hù)終端安全產(chǎn)品的“生存法則”安全客戶端在產(chǎn)品層面有個(gè)特殊需求就是自我保護(hù)。你不可能讓一個(gè)殺毒軟件被一個(gè)惡意進(jìn)程輕松結(jié)束掉否則它還防什么。這個(gè)方向的問題通常這樣問“如果你的客戶端進(jìn)程被別的進(jìn)程結(jié)束或關(guān)停了你怎么辦”常規(guī)答案先列幾條部署驅(qū)動(dòng)級(jí)保護(hù)由內(nèi)核態(tài)驅(qū)動(dòng)保護(hù)進(jìn)程不被OpenProcess、TerminateProcess。進(jìn)程雙守護(hù)兩個(gè)進(jìn)程互相拉起檢測(cè)到對(duì)方退出立即創(chuàng)建新進(jìn)程。使用Job Object限制子進(jìn)程設(shè)置JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE來避免子進(jìn)程被殺。啟動(dòng)服務(wù)并設(shè)置服務(wù)恢復(fù)策略通過服務(wù)控制管理器實(shí)現(xiàn)自動(dòng)重啟。把關(guān)鍵邏輯下沉到驅(qū)動(dòng)或服務(wù)進(jìn)程UI進(jìn)程被結(jié)束不影響核心功能。回答時(shí)還要注意分寸不要炫技過度。我當(dāng)時(shí)強(qiáng)調(diào)了“安全產(chǎn)品自我保護(hù)的前提是合規(guī)不能變成流氓軟件”面試官對(duì)此沒有反對(duì)反而認(rèn)為你考慮問題比較全面。3.4 加密接口與證書校驗(yàn)客戶端通信的“守門員”安全廠商的客戶端要和服務(wù)器通信通信安全是個(gè)必問點(diǎn)。當(dāng)年的提問集中在客戶端和服務(wù)端的通信如何保證客戶端不被仿冒可用基于證書的雙向認(rèn)證或設(shè)備指紋動(dòng)態(tài)令牌??蛻舳舜鎯?chǔ)敏感數(shù)據(jù)時(shí)用哪種加密比如使用DPAPI數(shù)據(jù)保護(hù)API或Windows的CNG接口將密鑰存儲(chǔ)在安全邊界內(nèi)獲取的MasterKey中避免硬編碼密鑰。為什么不能只用自定義異或或Base64“加密”因?yàn)檫@是混淆不是加密攻擊者只要逆向代碼就能還原算法。證書校驗(yàn)時(shí)要注意什么要注意禁止跳過證書鏈校驗(yàn)、禁止信任任意證書、要校驗(yàn)主機(jī)名。這部分我相信大多數(shù)客戶端開發(fā)都有基礎(chǔ)但安全廠商會(huì)追問得更深。比如“如果服務(wù)端證書過期了客戶端該怎么處理”答案是必須按證書鏈校驗(yàn)失敗處理并發(fā)告警不能讓客戶端自動(dòng)信任或繼續(xù)連接除非有明確的運(yùn)維變更流程。4. 算法與系統(tǒng)設(shè)計(jì)實(shí)戰(zhàn)4.1 手寫算法題安全場(chǎng)景下的高頻題目奇安信面試?yán)锸謱懰惴}通常放在一面或二面的中段難度大概是LeetCode中等偏下但會(huì)結(jié)合場(chǎng)景包裝。我整理幾個(gè)高頻方向字符串類。比如“判斷一個(gè)字符串是否是另一個(gè)字符串的旋轉(zhuǎn)字符串”“找出字符串中最長(zhǎng)不重復(fù)子串”。字符串是安全產(chǎn)品里最常見的處理對(duì)象因?yàn)閻阂馕募奶卣鞔a、日志解析、協(xié)議解析都離不開字符串處理。鏈表類。比如“反轉(zhuǎn)鏈表”“判斷鏈表是否有環(huán)”“合并兩個(gè)有序鏈表”。這類題考察的是指針操作基本功C面試必出。二叉樹類。比如“二叉樹層序遍歷”“判斷二叉樹是否對(duì)稱”“求二叉樹深度”。層序遍歷在安全產(chǎn)品的配置解析、規(guī)則樹遍歷里都有應(yīng)用場(chǎng)景面試官會(huì)順帶問“如果樹的節(jié)點(diǎn)特別多遞歸會(huì)棧溢出嗎”這實(shí)際上是在考察你能否用非遞歸方式實(shí)現(xiàn)。動(dòng)態(tài)規(guī)劃類。比如“最長(zhǎng)公共子序列”“編輯距離”“背包問題”。這類題不??嫉f一考了你會(huì)DP就能拉開差距。我面試那年沒碰到DP倒是碰到了二分查找和哈希表設(shè)計(jì)所以備考時(shí)別只盯著某一類題型。4.2 手寫線程池最經(jīng)典的并發(fā)設(shè)計(jì)題線程池是我那輪面試?yán)镂ㄒ坏氖謱懺O(shè)計(jì)題面試官給的需求非常明確實(shí)現(xiàn)一個(gè)固定大小線程池提供提交任務(wù)的方法任務(wù)是用函數(shù)對(duì)象封裝的無參數(shù)無返回值調(diào)用。核心設(shè)計(jì)點(diǎn)有這些線程數(shù)組或vector每個(gè)工作線程在構(gòu)造函數(shù)啟動(dòng)。任務(wù)隊(duì)列用std::queue存放std::functionvoid()?;コ怄i和條件變量任務(wù)隊(duì)列為空時(shí)工作線程阻塞等待提交任務(wù)時(shí)喚醒一個(gè)線程。關(guān)閉邏輯設(shè)置停止標(biāo)志喚醒所有線程然后逐個(gè)join。手寫的時(shí)候有個(gè)細(xì)節(jié)要注意就是條件變量的虛假喚醒問題。用while而不是if來檢查隊(duì)列是否為空這是面試官會(huì)盯著看的一個(gè)點(diǎn)。另外析構(gòu)函數(shù)里必須先置停止標(biāo)志再喚醒否則線程可能永遠(yuǎn)阻塞下去導(dǎo)致析構(gòu)卡死。我當(dāng)時(shí)的實(shí)現(xiàn)大致是下面這樣#include vector #include queue #include thread #include mutex #include condition_variable #include functional class ThreadPool { public: explicit ThreadPool(size_t n) { for (size_t i 0; i n; i) { workers_.emplace_back([this] { for (;;) { std::functionvoid() task; { std::unique_lockstd::mutex lock(mutex_); cv_.wait(lock, [this] { return stop_ || !tasks_.empty(); }); if (stop_ tasks_.empty()) return; task std::move(tasks_.front()); tasks_.pop(); } task(); } }); } } ~ThreadPool() { { std::unique_lockstd::mutex lock(mutex_); stop_ true; } cv_.notify_all(); for (auto t : workers_) t.join(); } void submit(std::functionvoid() f) { { std::unique_lockstd::mutex lock(mutex_); tasks_.push(std::move(f)); } cv_.notify_one(); } private: std::vectorstd::thread workers_; std::queuestd::functionvoid() tasks_; std::mutex mutex_; std::condition_variable cv_; bool stop_ false; };寫完這段之后面試官追問了兩個(gè)問題一個(gè)是“提交任務(wù)時(shí)用notify_one還是notify_all為什么”另一個(gè)是“如果任務(wù)本身拋了異常會(huì)怎樣”。第一個(gè)問題答案是用notify_one更高效因?yàn)橹恍枰獑拘岩粋€(gè)線程處理新任務(wù)第二個(gè)問題要補(bǔ)try-catch或讓任務(wù)對(duì)象自己處理異常否則std::terminate會(huì)導(dǎo)致進(jìn)程崩潰。4.3 場(chǎng)景設(shè)計(jì)題如何實(shí)現(xiàn)進(jìn)程模塊枚舉與可信校驗(yàn)這輪面試?yán)镉幸坏篱_放設(shè)計(jì)題我印象很深“如果讓你設(shè)計(jì)一個(gè)功能掃描指定進(jìn)程加載了哪些模塊并判斷模塊是否可信你會(huì)怎么設(shè)計(jì)”我的思路分幾步。第一步用CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, pid)拿到模塊快照后逐個(gè)遍歷Module32First和Module32Next就能得到模塊路徑和地址范圍。第二步是校驗(yàn)文件簽名可以用WinVerifyTrust在系統(tǒng)校驗(yàn)?zāi)夸浝锏臄?shù)字簽名或者Windows的Wintrust API來驗(yàn)證模塊的發(fā)布者信息。第三步是把模塊路徑規(guī)范化后與已知白名單和黑名單比對(duì)。白名單包括系統(tǒng)目錄下的已知模塊黑名單包括已知惡意DLL的名稱和哈希。面試官隨后追問“如果惡意代碼手動(dòng)映射注入模塊列表里根本看不到它怎么辦”。我答了“需要從內(nèi)核態(tài)獲取進(jìn)程內(nèi)存的可執(zhí)行頁面列表或使用用戶態(tài)的內(nèi)存掃描技術(shù)Walk可執(zhí)行內(nèi)存區(qū)域并比對(duì)特征”面試官點(diǎn)了點(diǎn)頭。這道題的考察邏輯其實(shí)是先用標(biāo)準(zhǔn)API做常規(guī)方案再考慮對(duì)抗場(chǎng)景的非常規(guī)手段這正好是安全客戶端開發(fā)最重要的工作方式。5. 面試過程中的實(shí)戰(zhàn)實(shí)錄與避坑經(jīng)驗(yàn)5.1 一面實(shí)錄基礎(chǔ)問答與代碼題一面大概50分鐘前半段是C和Windows基礎(chǔ)問答后半段是手寫代碼。電話結(jié)束時(shí)面試官問“你有沒有什么想問的”這是所有面試環(huán)節(jié)里最不能掉以輕心的問題。我當(dāng)時(shí)問的是“奇安信終端安全產(chǎn)品的Hook方案里用戶態(tài)和內(nèi)核態(tài)的邊界是怎么劃分的”這個(gè)問題既表現(xiàn)了對(duì)安全技術(shù)的興趣也暗示了自己的技術(shù)棧已經(jīng)觸及內(nèi)核層面。面試官很樂意回答這類問題因?yàn)樗芨杏X得出你做了功課。5.2 二面實(shí)錄項(xiàng)目深挖與場(chǎng)景設(shè)計(jì)二面是現(xiàn)場(chǎng)面試面試官級(jí)別更高風(fēng)格也更直接。上來先讓我花15分鐘講一個(gè)最拿得出手的項(xiàng)目然后全程打斷追問。我講的是一個(gè)Windows平臺(tái)的日志采集客戶端技術(shù)棧是C、Winsock、SQLite、多線程隊(duì)列。面試官追問的細(xì)節(jié)包括日志采集的隊(duì)列滿了怎么辦多線程寫SQLite怎么處理鎖競(jìng)爭(zhēng)斷網(wǎng)期間的日志會(huì)不會(huì)丟網(wǎng)絡(luò)模塊用的阻塞還是非阻塞Socket如果服務(wù)器端很慢你怎么防止客戶端堆積大量日志這些問題每個(gè)都能往深處挖我當(dāng)時(shí)的應(yīng)對(duì)策略是用真實(shí)實(shí)踐說話能具體到代碼就具體到代碼不能確定的地方就坦白說“這個(gè)場(chǎng)景我還沒遇到過如果現(xiàn)在讓我設(shè)計(jì)我會(huì)考慮…”。坦誠(chéng)但又能給出合理方案比強(qiáng)行編造要好得多。二面后半段就是場(chǎng)景設(shè)計(jì)題也就是上面說的模塊枚舉與可信校驗(yàn)?zāi)堑馈U麍?chǎng)面試最大的感受是項(xiàng)目經(jīng)歷不是背稿子而是檢驗(yàn)?zāi)闶欠裾娴睦斫庾约簩懙拿恳恍写a背后的代價(jià)。5.3 HR面實(shí)錄軟技能與匹配度HR面相對(duì)輕松主要問職業(yè)規(guī)劃、為什么選擇安全行業(yè)、對(duì)加班的態(tài)度、上家離職原因。但我也翻過一次車。那一次HR問“你覺得自己在Windows開發(fā)上最大的不足是什么”我當(dāng)時(shí)答的是“我還沒有系統(tǒng)地讀過Windows內(nèi)核源碼對(duì)內(nèi)核機(jī)制理解停留在原理層面”。結(jié)果HR緊跟著問“那你打算怎么補(bǔ)”我愣了一下才說“計(jì)劃用三個(gè)月時(shí)間看完《深入解析Windows操作系統(tǒng)》并做筆記”。雖然也不算是很差但如果當(dāng)時(shí)能說得更有步驟和可執(zhí)行性會(huì)好得多。后來我總結(jié)出一個(gè)答這類問題的公式承認(rèn)不足具體原因補(bǔ)救計(jì)劃正在執(zhí)行中的動(dòng)作。比如“我在驅(qū)動(dòng)開發(fā)方向經(jīng)驗(yàn)比較少因?yàn)橹耙恢弊黾冇脩魬B(tài)客戶端最近已經(jīng)把環(huán)境搭起來了正在參照網(wǎng)上公開的驅(qū)動(dòng)示例做驅(qū)動(dòng)加載和卸載的小實(shí)驗(yàn)計(jì)劃下個(gè)月完成一個(gè)簡(jiǎn)單的文件過濾驅(qū)動(dòng)”。這樣的回答才能傳遞出“我是行動(dòng)派”的信號(hào)。5.4 避坑清單這些雷我都替你踩過整理一下我面試前后犯過的錯(cuò)誤和總結(jié)的要點(diǎn)坑點(diǎn)具體情況正確做法只背API不背原理被問CreateProcess內(nèi)部步驟答不上來系統(tǒng)學(xué)習(xí)API到內(nèi)核對(duì)象的完整鏈路遺忘TLS被問線程獨(dú)立存儲(chǔ)沒想起來TLS復(fù)習(xí)Windows基礎(chǔ)章節(jié)TLS要會(huì)使用結(jié)構(gòu)體對(duì)齊不熟sizeof題算錯(cuò)多練習(xí)包含數(shù)組、指針、位域的布局計(jì)算Hook只知一個(gè)SetWindowsHookEx技術(shù)深度不足系統(tǒng)性掌握IAT Hook和Inline Hook思路項(xiàng)目經(jīng)理論述太籠統(tǒng)沒有具體到代碼和決策場(chǎng)景用STAR法每個(gè)難點(diǎn)要能講出兩條可選方案反問環(huán)節(jié)放棄或問得很空浪費(fèi)了展示機(jī)會(huì)準(zhǔn)備兩三個(gè)與崗位強(qiáng)相關(guān)的技術(shù)問題HR面暴露短板但沒有補(bǔ)救計(jì)劃印象分下降用“不足原因行動(dòng)計(jì)劃”公式回答6. 寫在最后一點(diǎn)個(gè)人復(fù)盤心得面試結(jié)束后我最大的體會(huì)是安全廠商的Windows客戶端開發(fā)面試表面考的是C和Windows編程實(shí)際考的是“你是否具備研究底層機(jī)制的習(xí)慣”。那些能說出來“為什么DLL劫持會(huì)存在怎么防御”“為什么消息鉤子不能攔截API調(diào)用”“線程池里條件變量為什么用while不用if”的候選人就是在日常開發(fā)中養(yǎng)成了刨根問底的習(xí)慣而不是臨時(shí)背題。另一個(gè)體會(huì)是項(xiàng)目經(jīng)歷的價(jià)值不在于“做過”而在于“做過之后你總結(jié)出了什么”。同樣是寫一個(gè)日志采集客戶端有人只是調(diào)接口實(shí)現(xiàn)了功能有人卻能把隊(duì)列設(shè)計(jì)、斷線重傳、SQLite鎖競(jìng)爭(zhēng)講得清清楚楚。面試官都是老手幾句話就能判斷出你是哪個(gè)層級(jí)。最后再說一個(gè)小技巧。準(zhǔn)備Windows開發(fā)面試時(shí)強(qiáng)烈建議自己動(dòng)手做一個(gè)“進(jìn)程模塊枚舉器”或“API Monitor”小工具不需要多復(fù)雜但凡是自己動(dòng)手寫過CreateToolhelp32Snapshot、VirtualProtectEx、ReadProcessMemory、EnumProcessModules這些API的人面試被問到底層機(jī)制時(shí)明顯更有底氣。技術(shù)這東西面試前是寫在簡(jiǎn)歷上的面試時(shí)是長(zhǎng)在腦子里的區(qū)別就在于你有沒有親手敲過那行代碼。