公司动态
奇安信Windows客户端笔试深度拆解:从服务编程到安全对抗
奇安信做安全的同行应该都不陌生国内终端安全领域绕不开的一家。2020年那一阵子客户端开发工程师的笔试题目在网上被传了不少我扫了一遍第一感觉是这题和普通互联网公司考的那套完全不是一个路子。它不是在考你会不会写界面、调接口而是在考你懂不懂Windows系统本身懂不懂一个安全软件在用户机器上是怎么活下来的。尤其有意思的是从相关的搜索记录里能看到几类非常典型的需求有人搜“vs2015 开发windows服务”有人搜“奇安信 输入验证路径遍历”还有一大片关于“天擎卸载”的搜索。把这些线索串起来基本就能还原出这套题目背后的岗位画像要做的不是一个普通桌面软件而是一个常驻系统、要和各种恶意程序贴身对抗、还得在千奇百怪的机器上稳定运行多年的Windows客户端。这篇文章就针对这套题目背后真正的技术考点做一个盘点和拆解。不管你是打算投奇安信还是准备任何安全厂商的Windows客户端岗位甚至只是想做一款用户量还不错的Windows桌面软件都值得对照着往下看看。1. 奇安信这批题目背后藏着一个什么样的客户端团队1.1 先搞清楚公司做什么再看题目考什么奇安信的产品线很宽但和客户端开发工程师直接相关的主要是这几块天擎终端安全管理系统、企业安全浏览器、代码卫士源码静态扫描、安全准入、终端检测与响应EDR之类的模块。其中天擎是绝对的主力产品企业用户装的最多。而客户端开发工程师进去之后大概率就是跟这些终端软件打交道。终端安全软件的业务特征是什么我用一句话总结它是个常住在操作系统里的特殊房客要看得到别的进程在干什么要管得住别的进程不让它们乱来还得保证自己不被恶意软件干掉更要在用户重启、升级、杀毒软件冲突、磁盘快满、网络断连这些乱七八糟的情况下继续工作。这样的业务属性决定了笔试出题的口味。普通互联网公司考的是数据结构、网络协议、业务架构设计而奇安信这类安全厂商的Windows客户端岗位题目一定会往操作系统底层靠而且会夹杂大量“如果遇到XXX怎么办”的实战场景题。1.2 安全客户端和普通桌面软件技术侧重点差在哪我见过不少做桌面软件出身的朋友去面安全厂商的时候特别不适应。原因很简单日常做业务软件很多系统级的东西是可以绕过去的但安全软件绕不过去。举个例子。普通桌面软件想监控一个目录下的文件变化调ReadDirectoryChangesW就够了拿到通知更新一下界面就算完事。但安全软件要监控全盘文件行为要拦截可疑进程对文件的修改光靠用户态API根本不够Windows在Vista之后提供了更底层的文件系统过滤机制想做得深就得往驱动层走。虽然笔试不会要求你直接写一个minifilter驱动但候选人得知道这条路的存在知道用户态方案和内核态方案的边界在哪。再说进程管理。普通软件想结束一个进程调TerminateProcess就完了。但安全软件这么做会被反杀你得先想办法把自己提权到足够高的权限处理掉目标进程的令牌再考虑守护进程怎么避免被杀后无法自愈。所以看奇安信这批题目的时候我建议别只盯着具体某道题的答案而是先建立一个大框架这类公司需要的是懂系统机制、能处理对抗场景、代码习惯极其严谨的工程师。下面的内容全部围绕这个大框架展开。2. 2020年前后Windows客户端开发的看家本领2.1 C是基本盘但这里考的和学校教的不一样Windows客户端开发尤其是安全终端方向的客户端C依然是绝对的主力语言。这不是说学校里教的《C程序设计》没用而是说到了岗位笔试和实际开发层面考察的重心完全变了。学校重点考语法、考面向对象三大特性、考设计模式而实际岗位里最要命的是这几块内存管理谁申请谁释放、RAII是不是形成习惯了、STL的底层行为和线程安全性、C11/14之后引入的新特性你会不会在实际项目里用、异常安全、以及最容易被忽略的——各种隐式类型转换和未定义行为。我见过一道很典型的题给一段在Windows服务里跑多线程任务队列的代码让候选人指出哪里有内存泄漏、哪里有数据竞争。内核里那些对象还好说最坑的是STL容器在多线程下又没加锁又发生了迭代器失效这种代码在笔试阶段靠眼睛看不出来得上手写才能暴露问题。所以我的建议是准备这类岗位时把C的基础牢靠程度放在第一位算法题反而不是决胜点。2.2 Win32与系统机制安全产品绕不开的底层能力如果说C是武器那Win32编程就是战场地图。这里说的Win32不是说你得把每一条API签名都背下来而是要对Windows提供的几个核心机制有系统级理解。第一是窗口与消息机制。消息循环不是简单的GetMessage/DispatchMessage循环要理解消息的优先级、队列消息与非队列消息的区别、SendMessage和PostMessage对调用线程的影响以及为什么安全软件里大量后台逻辑要尽量少依赖UI线程的消息循环。第二是进程与线程同步。临界区、互斥量、事件、信号量这四样东西各适用什么场景临界区不跨进程互斥量可以跨进程还可以设等待超时事件适合做一对多通知信号量适合做资源计数。安全软件里到处都是多进程多线程协作这些东西是必考。第三是动态链接库。为什么安全软件要把核心逻辑抽成DLLDLL的导出表、加载顺序、依赖地狱问题还有DLL注入与劫持——这对安全软件既是能力也是风险。2020年那会儿DLL侧加载已经是很常见的攻击手法了客户端开发工程师如果连LoadLibrary的搜索顺序都说不清楚肯定是不及格的。第四是文件、注册表、服务控制管理器这三类对象的安全属性。安全软件有自己的文件要保护有自己的服务要管理对这些对象的安全描述符要有概念否则别说对抗恶意软件连自己产品的自我保护都做不好。2.3 组件化开发、UI框架与工程化构建的取舍很多准备笔试的人会有个误区觉得客户端开发就是做界面所以花大量时间去刷Qt或Electron的UI特效。但实际上安全类Windows客户端岗位笔试基本不考UI特效反而会关心你对工程化的理解。以天擎这类终端产品为例它的客户端是个非常复杂的程序起码包括托盘界面进程、核心服务进程、升级进程、日志上报模块、各类策略执行模块有些模块还是驱动态。这堆东西要用什么UI框架不同团队有不同的选择。老一代的产品大量用MFC因为历史包袱重、稳定、体积小换个新框架成本极高。新一点的模块会自然倾向Qt跨平台、开发效率高、现代感强。少数团队用DuiLib或自研DirectUI方案因为界面皮肤自由度大。我把这几条路线列个表方便你理解为什么面试官喜欢问“你在项目里怎么选型”而不是“你会不会用某个框架”UI路线优势劣势典型场景MFC稳定、资料多、对Win32封装浅、体积小界面老旧、开发效率低历史遗留模块、维护为主Qt跨平台、组件丰富、信号槽机制清晰体积略大、需要处理发布依赖新模块、工具类界面DuiLib/DirectUI自绘、皮肤效果强、灵活性高坑多、社区生态弱安全软件主界面、需要酷炫交互的模块纯Win32 SDK最轻量、无依赖开发效率极低小工具、测试程序2020年那阵子Windows 7和Windows 10还处于共存期Win10占比持续上升所以UI工程还要考虑DPI缩放、高对比度主题这些兼容性问题。笔试里直接考框架API的可能性很小但你得说得清楚“自己项目里为什么选这个框架怎么解决它带来的问题”这才是面试官想听的。3. 从热搜里的线索看这些题真正在考什么3.1 “vs2015 开发windows服务”服务程序是隐藏大项搜索热词里出现“vs2015 开发windows服务”绝对不是偶然。Windows服务是安全终端软件最常用的载体。为什么道理很简单服务可以设置为开机自启动而且能在用户登录之前就跑起来服务默认以SYSTEM或Administrator权限运行能拿到比普通用户进程更高的权限服务还能配置失败自动恢复。对于需要第一时间接管系统、始终在线监控的安全软件来说服务几乎是唯一选择。很多人第一次写Windows服务会对着VS2015发愣因为C工程和C#工程不太一样C#有现成的Windows服务项目模板一键生成C这边没有那种省事的模板。通常是在一个Win32工程里手写服务的入口逻辑也就是实现ServiceMain、注册Service Control Handler、调用StartServiceCtrlDispatcher然后用SCM相关API去安装和启动。笔试如果出服务相关题目最常踩的坑有两个。一是服务没有在启动后的30秒内调用StartServiceCtrlDispatcherSCM直接判定启动超时服务起不来。一个成熟的客户端工程会把这个流程放在尽量靠前的位置避免在入口函数里做太多耗时初始化。二是服务在Vista之后跟用户桌面不在同一个会话里也就是所谓的会话0隔离所以服务不能直接弹窗给用户看必须通过一个用户态进程再转一道。这也是为什么终端安全产品通常都是“服务托盘程序”两个进程配合服务管业务托盘管交互两者通过IPC通信。如果你在准备这类岗位我强烈建议你用vs2015手写一个最小的Windows服务真实跑一遍安装、启动、停止、卸载的完整流程再试试在服务里创建一个线程做定时任务、把日志写到指定文件。这一套做完你对服务控制管理器、服务状态机、会话隔离的理解就远比背概念扎实。3.2 “输入验证路径遍历”安全编码意识是必选项热搜词里这个“输入验证路径遍历”非常能说明问题。虽然路径遍历Path Traversal这个词在Web漏洞里更常见但它在客户端开发里也一样存在而且安全厂商自己的客户端要是出现这类漏洞影响面会被放大很多倍。路径遍历的本质是什么就是程序拿到外部输入文件名、路径字符串、压缩包内的条目路径、服务器下发的策略路径没有做校验就拼到了文件系统操作里。攻击者构造..\\..\\Windows\\system32\\xxx这样的路径就能让程序去读写设计者本不想让它碰的位置。Windows下面的路径规则和Unix有差异这导致过滤变得更麻烦Windows的文件系统不区分大小写、同时接受/和\作为分隔符、还存在短文件名8.3名、Unicode等价字符、NTFS备用数据流等一堆容易被忽略的旁路。修复思路倒不复杂但要求开发者有那个意识拿到外部路径先做规范化用PathCchCanonicalize之类的函数把路径转成标准形式然后校验规范化后的结果是否仍然位于允许的根目录之下而不是简单地用黑名单去匹配..。奇安信自己就有代码卫士这种做静态代码扫描的产品他们比谁都清楚这类漏洞有多普遍。所以笔试里出这个考点本质上是在考察候选人有没有“所有外部输入都不可信”的安全编码习惯。3.3 “天擎卸载”相关搜索背后的自我保护机制搜索热词里关于“天擎卸载”的讨论特别多这本身就从侧面反映了一件事这类终端安全产品的自我保护机制做得很重不是随手就能卸掉的。站在普通用户的角度确实会觉得烦但站在客户端开发的角度这里的每一个“烦”背后都是一套技术设计。终端安全软件为什么要拼命保护自己因为恶意软件在攻击你的安全软件之前一定会先尝试干掉这个“看门狗”。如果病毒能通过结束进程、删除文件、停掉服务的方式把杀毒软件废掉那整个防线就没意义了。所以安全客户端常见的自我保护手段包括但不限于双进程互相守护一个被杀另一个立刻拉起来服务配置失败自动重启SCM层面就有自愈能力对关键文件、注册表键、进程对象设置DACL拒绝非授权用户的删除、修改和终止操作利用内核驱动做进程保护让用户态无法直接结束受保护进程卸载流程需要验证码、密码或后台策略审批。我特意强调一下这里讲的是原理和设计思路而不是教人怎么绕过卸载。作为一个候选人你如果能在面试里把这些自我保护机制讲清楚并说明白“为什么这么设计会牺牲一部分用户体验但这是安全产品必须付出的代价”面试官会认为你对业务场景有真实理解而不是只会背API。4. 如果让我重写这套笔试题我会重点押这四个方向前面聊的是岗位背景和技术栈这一节进入实战。如果让我站在出题人的角度或者用今天的话说“重新梳理一套终端安全客户端笔试题的重点”我觉得大致会落在下面这四个方向上。4.1 线程同步进程守护和多线程日志都要用它线程同步几乎是Windows客户端笔试必出的考点因为安全软件里到处都是多线程。以一个进程守护模块为例监控线程每隔几秒检查被保护进程是否存活如果死了就拉起业务线程不停地往共享队列里写入新的告警还有一个线程专门从队列里取数据上传。这里至少有三种同步对象可以考队列用临界区还是互斥量保护怎么通知等待线程有数据了多个工作线程怎么限制并发数量我拿生活场景做个类比临界区就像麦当劳的取餐口同一时间只能有一个人取餐但大家都堵在窗口前排队互斥量就像公共厕所的门锁一个人进去把门锁上其他人等而且这个锁可以跨楼层跨进程事件就像闹钟你设定好了到点叫人直接通知等待它的人“活干完了”信号量就像停车场入口的剩余车位计数每进一辆车减一车满了后面就得等。笔试中经常追问的还有死锁。产生死锁的四个条件——互斥、持有并等待、不可剥夺、循环等待——背出来只是第一步更重要的是你能不能在一个具体的场景里指出为什么会出现循环等待以及怎么用锁的排序或超时机制解掉它。我在实际项目里见过最典型的死锁就是A线程持锁1去要锁2B线程持锁2去要锁1两边都卡死。解决方式要么是全局定一个加锁顺序要么是WaitForSingleObject时给超时时间拿不到锁就主动退出再重试。这类问题面试官非常爱深挖因为能看出来候选人有没有真的处理过线上问题。4.2 共享内存与IPC客户端各模块之间怎么对话安全软件的客户端不是单一进程前面说过它有服务、有托盘界面、有各业务模块。这些进程之间怎么通信就是一个高频考点。剪贴板肯定不行那是给人用的Socket又太重在本地通信里还要走协议栈没必要。最常见的方案是命名管道、共享内存加事件通知以及具名互斥量做一些简单的单实例控制。其中共享内存很适合大批量数据传输的场景比如扫描引擎把结果列表全部传给界面层用命名管道一条条传效率太低了。典型做法是创建一个共享内存文件映射对象服务端往里写数据同时创建一个事件对象写完以后发信号客户端等待事件并读取共享内存里的内容。这里面试官很可能追问多个客户端同时读怎么办共享内存的大小怎么定如果写了一半进程崩溃了另一端怎么发现还有一个点容易被忽略安全客户端和驱动之间的通信。这通常不是用共享内存而是要往驱动发IOCTL通过DeviceIoControl调用驱动暴露的设备接口。这个链路已经超出纯用户态笔试范围了但很多岗位JD里会写着“有驱动开发经验优先”。你哪怕没有实际写过驱动也要知道IOCTL的基本交互模型不然面试聊到“底层拦截”的时候会卡壳。4.3 DLL注入与API钩子分得清IAT Hook和Inline Hook吗终端安全软件自己要监测别的进程手段通常就是注入和钩子。虽然今天很多EDR产品已经尽量把采集能力下沉到驱动层但用户态的DLL注入和API Hook依然是绕不开的知识点。笔试不太会让你凭空设计一个免杀方案但会问基础原理以及让你评价几种注入方式各自的利弊。常见的用户态DLL注入方式包括SetWindowsHookEx注入依赖窗口消息机制无窗口进程注入不了、CreateRemoteThreadLoadLibrary在目标进程里创建远程线程加载DLL最经典但也很容易被安全软件拦截、QueueUserAPC注入目标线程必须处于可告警状态、AppInit_DLLs注册表项开机时加载到加载user32.dll的进程里兼容性差且影响面大。2020年那个时间点上微软对AppInit_DLLs的限制已经挺严格了所以实际产品用得越来越少。然后就是Hook。IAT Hook和Inline Hook的区别一定要讲清楚。IAT Hook是修改PE文件导入地址表让目标API调用跳转到自己的函数实现简单、稳定性高但只能拦截通过导入表调用的API如果目标程序通过LoadLibraryGetProcAddress动态获取函数地址再调用IAT Hook就拦不到。Inline Hook更狠直接修改目标函数开头的机器码跳转到自己的代码里能拦得更彻底但问题也更多要处理指令长度对齐问题要处理多线程同时调用时的竞态还要注意被Hook的系统函数可能有热补丁机制。一个笔试里常出现的问题是“Inline Hook为什么有可能导致目标程序崩溃”答案就在于你劈开的那几个字节是不是一条完整指令边界。4.4 内存管理与崩溃排查客户端要能在复杂环境活下去安全软件最怕什么不是被病毒干死而是自己崩了。普通软件崩了大不了重启一下安全软件如果经常崩溃用户第一时间就会把它卸了。所以说客户端开发工程师必须会排查崩溃问题这也是笔试和面试里经常被包装成场景题的一块。Windows上排查崩溃的标准链路是等崩溃发生用LocalDumps注册表项配置让系统自动生成dump然后用Windbg打开dump文件执行!analyze -v查看分析结果关键是找出出错线程的栈回溯和异常状态。这个过程里有两个非常实操的细节一是配置LocalDumps时要写成64位和32位两套取决于目标进程是64位还是32位二是分析dump时必须保证exe和pdb版本跟崩溃现场的版本一致否则栈是还原不出来的。如果把这道题延伸成面试场景面试官会问“如果崩溃只在用户机器上出现你自己机器复现不了怎么处理”这时候你如果能说出“上线前给客户端开好崩溃转储策略拿到用户侧dump检查模块版本和符号用Windbg分析出崩溃栈”就已经比绝大多数候选人强了。再往深一点还会追问内存损坏的常见原因比如缓冲区越界、悬空指针、重复释放以及C里怎么防止这些问题——RAII、智能指针、边界检查、不要裸new/delete。这些习惯在安全软件的长期维护中非常重要。5. Windows客户端开发求职者的备考路线与避坑建议5.1 从API调用层面往系统机制层面去理解准备这类岗位最大的忌讳就是只背API。CreateFile、ReadFile、WriteFile这些API谁都会调但你得理解它们背后的内核对象、句柄表、用户态到内核态的切换开销、缓冲区是怎么拷贝的。要养成一种习惯每用一个系统API就想想它的内部机制大概是什么样瓶颈在哪为什么需要传入这个参数。比如调用WaitForSingleObject时为什么核心对象就分“已通知”和“未通知”两个状态比如打开注册表键为什么最好用RegOpenKeyEx而不是RegOpenKey。这些理解比多背二十个API更有价值。Windbg的使用建议尽早学起来。不是要你成为内核调试专家而是至少会用!analyze -v、dt、dps这些命令能看清一个崩溃现场的调用栈。Windows客户端开发岗位调试能力几乎可以直接等同于工程能力面试聊深了很容易聊到“你之前是怎么定位一个线上崩溃的”这时候没有实战经验是编不出来的。5.2 动手做一个小而完整的终端安全Demo比刷一百道题有效如果时间允许我特别建议你在准备期间动手写一个“迷你版终端安全客户端”。范围不需要大具体可以做这几层一个能通过SCM安装启动的Windows服务服务里创建一个线程做定时扫描比如扫描某个目录下是否新增了指定扩展名的文件检测到异常时通过命名管道把一个告警消息发给托盘程序托盘程序用一个简单窗口显示告警列表并且提供一键退出按钮但退出时必须先向服务验证一个“授权码”而不是直接结束进程。这个Demo麻雀虽小但几乎覆盖了前面说的所有考点服务编程、线程同步、进程间通信、消息循环、安全校验。做完之后你手上有真实的代码可以讲、有真实的踩坑经历可以聊比抽象地背一堆概念要有说服力得多。我见过很多候选人笔试成绩不错但一聊项目就露怯根本原因就是没有亲手跑通过一条完整的系统链路。5.3 别把精力浪费在过度追逐新框架上2020年那会儿很多准备客户端岗位的同学都在纠结要不要学Electron、Flutter、乃至Rust。我给的答复一直是如果目标是安全厂商的Windows客户端开发岗重点一定放在C、Win32、系统机制、调试和排查能力上。新框架年年有但底层技术永远是最稳的护城河。一个能把Win32、服务、Hook、注入、崩溃分析讲得头头是道的人学什么新框架都快反过来只会在框架里做界面的人遇到系统级问题就抓瞎。当然这里也不是说新东西完全不用看。像新版本的Visual Studio工程配置、C20范围库的用法、Windows 10以后引入的部分新特性都可以当作锦上添花了解。但主次关系一定要摆对别本末倒置。5.4 我记得的几个高频失误你对照一下最后分享几个我在实际面试里反复见到的高频失误你备考的时候可以专门对照检查失误类型具体表现应对思路不检查API返回值调完CreateFile不判断句柄是否有效直接往下走养成每调一次关键API就检查返回值和GetLastError的习惯忽略UNICODE字符集工程还是多字节字符集遇到中文路径就崩新项目一律UNICODE字符串处处用TCHAR对应宏或直接wchar_t混淆DLL安全细节不知道LoadLibrary搜索顺序、不了解KnownDLLs理解DLL搜索路径排序写代码时尽量用绝对路径加载对崩溃dump无从下手程序崩了就加日志、加打印而不是用调试器分析学会配置LocalDumps学会用Windbg还原现场线程同步意识薄弱共享数据不加锁总以为“大概不会同时访问”所有跨线程访问的变量先问自己是否加锁我在实际项目中看到过太多次因小失大一个DWORD共享变量没加volatile或者没加锁线上偶发数据错乱排查了两天最后发现就是线程同步问题。安全客户端的运行环境极其复杂用户的机器上可能同时跑着多家安全产品、各种插件、老旧的第三方驱动任何一个不起眼的错误都可能被放大成难以复现的线上故障。所以你如果真心想在这个方向做长久一定要从一开始就养成严谨的系统级编程习惯。关于这类笔试的内容一篇还讲不完。线程同步的具体场景题、IOCTL通信的完整流程、dump分析的实战演示这些都可以单独拆开写成一篇。如果后面有机会我再把手头做过的几个真实排查案例整理出来继续往下聊。