一、ASUS安全名人堂 & HP安全致谢
efiXplorer工具是binarly-io团队开发的UEFI固件分析 IDA Pro插件,其中提供了x86 UEFI静态扫描漏洞功能:
https://github.com/binarly-io/efiXplorer/
参考了该工具,笔者完成了ARM架构的检测代码开发,之后又对OEM厂商的高通笔记本设备进行了一轮测试,最终在ASUS和HP某型号BIOS固件中检测到了几个漏洞,但由于这些漏洞代码均属于Insyde且已修复,因此ASUS与HP未申请新漏洞编号。
不过ASUS已认可并列入安全名人堂:
https://www.asus.com/security-advisory/
HP安全公告致谢:
https://support.hp.com/us-en/document/ish_13143750-13143772-16/hpsbhf04067
二、efiXplorer检测原理
efiXplorer静态扫描漏洞功能代码如下,目前只支持x86:
https://github.com/binarly-io/efiXplorer/blob/master/efiXplorer/efiAnalyzerX86.cpp#L2207
潜在栈溢出检测:
○ 代码特别关注NN_lea指令(加载有效地址),因为它常用于计算栈上变量的地址。
○ 通过分析NN_lea指令的目标地址和操作数,代码检查是否有可能与预期的dataSizeStackAddr)相匹配。
▪ dataSizeStackAddr 被初始化为0,然后在循环中,如果找到了一个 NN_lea 指令,且这个指令的第一个操作数是一个寄存器(o_reg)且该寄存器是 REG_R9,那么 dataSizeStackAddr 就会被赋值为该指令的第二个操作数的地址(insn.ops[1].addr)。
○ 如果发现匹配或潜在匹配,则认为可能存在栈溢出,并将当前地址记录为潜在的溢出点。
Bash //-------------------------------------------------------------------------- // Find potential stack/heap overflow with double GetVariable calls bool EfiAnalysis::EfiAnalyzer::findGetVariableOveflow(std::vector<json> allServices) { msg("[%s] Looking for GetVariable stack/heap overflow\n", plugin_name); //收集GetVariable服务调用地址:遍历allServices,如果服务名称是GetVariable,则将其地址添加到getVariableServicesCalls向量中。 std::vector<ea_t> getVariableServicesCalls; std::string getVariableStr("GetVariable"); for (auto j_service : allServices) { json service = j_service; std::string service_name = static_cast<std::string>(service["service_name"]); ea_t addr = static_cast<ea_t>(service["address"]); if (service_name.compare(getVariableStr) == 0) { getVariableServicesCalls.push_back(addr); } } //排序并检查GetVariable调用数量:对getVariableServicesCalls中的地址进行排序,并检查是否有至少两次GetVariable调用。如果没有,则输出一条消息并返回false。 sort(getVariableServicesCalls.begin(), getVariableServicesCalls.end()); if (getVariableServicesCalls.size() < 2) { msg("[%s] less than 2 GetVariable calls found\n", plugin_name); return false; } //检查两次连续的GetVariable调用:这里开始一个循环,从第二次GetVariable调用开始,并与前一次调用进行比较。 ea_t prev_addr = getVariableServicesCalls.at(0); ea_t ea; insn_t insn; for (auto i = 1; i < getVariableServicesCalls.size(); ++i) { ea_t curr_addr = getVariableServicesCalls.at(i); msg("[%s] GetVariable_1: 0x%016llX, GetVariable_2: 0x%016llX\n", plugin_name, u64_addr(prev_addr), u64_addr(curr_addr));
//获取dataSizeStackAddr:这部分代码寻找与GetVariable调用相关的特定指令,并获取dataSizeStackAddr的值。它反向遍历代码,直到找到特定的lea指令或达到最大尝试次数(10次)。 int dataSizeStackAddr = 0; uint16 dataSizeOpReg = 0xFF; ea = prev_head(curr_addr, 0); for (auto i = 0; i < 10; ++i) { decode_insn(&insn, ea); if (insn.itype == NN_lea && insn.ops[0].type == o_reg && insn.ops[0].reg == REG_R9) { dataSizeStackAddr = insn.ops[1].addr; dataSizeOpReg = insn.ops[1].phrase; break; } ea = prev_head(ea, 0); }
//检查GetVariable调用之间的代码: GetVariable_1 to GetVariable_2,这部分代码初始化一些变量来检查两个GetVariable调用之间的代码 //遍历从prev_addr到curr_addr之间的所有指令。它查找使用dataSizeStackAddr和dataSizeOpReg的指令,并计算它们的数量。如果在遍历过程中遇到特定的调用(如NN_callni且参数为0x48),返回、跳转指令,或者dataSize的使用次数超过1次,则设置ok为false并退出循环。 ea = next_head(prev_addr, BADADDR); bool ok = true; size_t dataSizeUseCounter = 0; while (ea < curr_addr) { decode_insn(&insn, ea); if (((dataSizeStackAddr == insn.ops[0].addr) && (dataSizeOpReg == insn.ops[0].phrase)) || ((dataSizeStackAddr == insn.ops[1].addr) && (dataSizeOpReg == insn.ops[1].phrase))) { dataSizeUseCounter++; } if ((insn.itype == NN_callni && insn.ops[0].addr == 0x48) || insn.itype == NN_retn || insn.itype == NN_jmp || insn.itype == NN_jmpni || dataSizeUseCounter > 1) { ok = false; break; } ea = next_head(ea, BADADDR); } if (ok) {
// 检查错误的GetVariable检测:这部分代码检查GetVariable调用之前的8条指令,寻找可能表示错误检测的mov指令。如果某个mov指令将内存地址的值移动到寄存器中,并且该内存地址在gBsList向量中,则设置wrong_detection为true,表示可能发生了错误的检测。 bool wrong_detection = false; ea = prev_head(curr_addr, 0); for (auto i = 0; i < 8; ++i) { decode_insn(&insn, ea); if (insn.itype == NN_mov && insn.ops[0].type == o_reg && insn.ops[1].type == o_mem) { ea_t mem_addr = insn.ops[1].addr; if (addrInVec(gBsList, mem_addr)) { wrong_detection = true; break; } } ea = prev_head(ea, 0); }
//检查DataSize的初始化:这部分代码检查GetVariable调用之前的指令,看dataSizeStackAddr是否正确地初始化为栈指针(REG_RSP)或基指针(REG_RBP)的偏移量。如果是这样,init_ok被设置为true。 bool init_ok = false; decode_insn(&insn, prev_head(curr_addr, 0)); if (!wrong_detection && !(insn.itype == NN_mov && insn.ops[0].type == o_displ && (insn.ops[0].phrase == REG_RSP || insn.ops[0].phrase == REG_RBP) && (insn.ops[0].addr == dataSizeStackAddr))) { init_ok = true; }
//检查DataSize参数变量的一致性:如果DataSize的初始化是正确的(init_ok为true),代码将查找包含当前地址prev_addr的函数入口点func_start。如果找不到函数的起始点,则函数返回是否存在潜在的栈溢出(getVariableOverflow是否非空) if (init_ok) { ea = prev_head(prev_addr, 0); // for (auto i = 0; i < 10; ++i) { func_t *func_start = get_func(ea); if (func_start == nullptr) { return (getVariableOverflow.size() > 0); } //确定栈基址寄存器,这段代码尝试确定栈基址寄存器。它检查函数起始处的第一条指令,看是否为将REG_RSP(栈指针寄存器)的值移动到另一个寄存器的mov指令。如果是这样,它将该寄存器的编号存储在stack_base_reg中。 uint16 stack_base_reg = 0xFF; decode_insn(&insn, func_start->start_ea); if (insn.itype == NN_mov && insn.ops[1].is_reg(REG_RSP) && insn.ops[0].type == o_reg) { stack_base_reg = insn.ops[0].reg; } //遍历函数内的指令:代码遍历从当前地址ea到函数起始地址func_start->start_ea的所有指令。对于每条指令,它解码指令并检查是否满足特定的条件。 while (ea >= func_start->start_ea) { decode_insn(&insn, ea); if (insn.itype == NN_call) break; //检查潜在的栈溢出:这段代码特别关注NN_lea指令,该指令通常用于计算有效地址。如果目标寄存器是REG_R9,代码会进一步检查该地址是否与dataSizeStackAddr有关。get_spd函数可能是用来获取从当前指令到函数起始点的栈深度差。如果计算出的地址与dataSizeStackAddr相等,或者通过栈基址寄存器和栈深度差计算出的地址与dataSizeStackAddr相等,则认为存在潜在的栈溢出,并将当前地址curr_addr添加到getVariableOverflow列表中,并输出一条消息。 if (insn.itype == NN_lea && insn.ops[0].type == o_reg && insn.ops[0].reg == REG_R9) {
ea_t stack_addr = insn.ops[1].addr; sval_t sval = get_spd(func_start, ea) * -1;
if ((insn.ops[1].phrase == stack_base_reg && (sval + stack_addr) == dataSizeStackAddr) || (dataSizeStackAddr == insn.ops[1].addr)) { getVariableOverflow.push_back(curr_addr); msg("[%s] \toverflow can occur here: 0x%016llX\n", plugin_name, u64_addr(curr_addr)); break; } } ea = prev_head(ea, 0); } } } //更新prev_addr并返回结果:prev_addr被更新为当前的curr_addr,以便在下一次迭代中使用。函数返回getVariableOverflow列表是否非空,即是否存在潜在的栈溢出。 prev_addr = curr_addr; } return (getVariableOverflow.size() > 0); } |
1. 变量声明:
○ ea_list_t get_variable_services_calls:一个用于存储GetVariable服务调用地址的列表。
○ std::string get_variable_str("GetVariable"):一个字符串,用于比较服务名称。
2. 遍历所有服务:
○ 遍历m_all_services(一个包含所有服务信息的JSON对象列表)。
○ 如果服务名称是GetVariable,则将其地址添加到get_variable_services_calls列表中。
3. 排序和检查:
○ 对get_variable_services_calls列表进行排序。
○ 如果列表中的地址数量少于2,则返回false,因为没有足够的GetVariable调用来进行分析。
4. 遍历GetVariable调用:
○ 使用一个循环遍历排序后的GetVariable调用地址列表。
○ 对于每一对连续的GetVariable调用,执行以下分析:
○ a. 日志输出:输出两个GetVariable调用的地址。
○ b. 寻找DataSize的栈地址和操作数寄存器:
▪ 从第二个调用的地址向前遍历指令,寻找一个lea指令,该指令将DataSize的栈地址加载到R9寄存器中。
○ c. 检查第一个调用和第二个调用之间的代码:
▪ 从第一个调用的地址向后遍历指令,直到第二个调用的地址。
▪ 检查DataSize的使用情况,以及是否存在可能破坏分析的指令(如callni、retn、jmp、jmpni或DataSize被多次使用)。
○ d. 检查错误的GetVariable检测:
▪ 从第二个调用的地址向前遍历指令,检查是否存在将内存地址移动到寄存器的指令,并且该内存地址在m_bs_list中(可能表示错误的GetVariable检测)。
○ e. 检查DataSize的初始化:
▪ 检查DataSize的栈地址是否被正确初始化。
○ f. 检查两个调用中DataSize参数变量是否相同:
▪ 从第一个调用的地址向前遍历到函数开始,检查是否存在一个lea指令,该指令将DataSize的栈地址加载到R9寄存器中,并且该地址与先前找到的地址相同。
5. 记录可能的溢出点:
○ 如果满足所有条件,将第二个GetVariable调用的地址添加到m_double_get_variable列表中,并输出日志信息。
6. 返回结果:
○ 如果m_double_get_variable列表不为空,则返回true,表示找到了可能的双重GetVariable调用。
三、 ARM检测功能开发
分析efiXplorer检测x86缓冲区溢出漏洞后,根据其原理,笔者完成了对ARM架构UEFI固件缓冲区溢出漏洞的检测功能开发。
3.1 x86与arm汇编代码对比
3.1.1 X86相关指令
如下图,x86中GetVariable的DataSize参数通过“lea r9, [rsp+5B8h+var_588];DataSize"来传递
对应检测代码,判断是否是lea,第0参数是否是R9寄存器
X86修改Datasize参数指令: DataSize = 16LL;
Tcg2Smm_.text:0000000001A3BCA5 mov [rbp+DataSize], r14
3.1.2 ARM相关指令
下图中,' ADD X3, X29, #0x50 ; 'P''为ARM的GetVariable的DataSize参数&v17
除了“ ADD X3, X29, #0x50”,还有“MOV X3, X21”等
.text:0000000000048B74 ADD X3, X29,#0x98
.text:000000000004A1C4 MOV X3, X21
.text:0000000000040438 MOV X3, X22
.text:000000000001A024 MOV X3, X26
idasdk90/include/allins.hpp
ARM_add、ARM_mov
ARM修改datasize 汇编指令有如下几种:
.text:0000000000003A90 STR XZR, [X29,#0x70+var_20]
NN_str
.text:00000000000010E0 STR X1, [X19]
ARM初始化datasize参数指令
v13 = v4;
.text:0000000000007320 STR X20, [X29,#0xA0+var_28]
v17 = sub_569BC(*(_QWORD *)v7[1].Data4);
.text:0000000000007694 STR X0, [X29,#0x60+var_10]
v45 = 8LL;
.text:0000000000015CD4 MOV X24, #8
.text:0000000000015CD8 STR X24, [X29,#0x70+var_28]
3.2 idapython开发
最开始准备在源码基础上修改,但efiXplorer编译需要sdk
https://github.com/binarly-io/efiXplorer/wiki/Build-instruction-and-installation
所以先尝试了使用IDAPython替代ida sdk进行开发。在使用IDA Pro的IDAPython插件进行逆向工程时,检测两次GetVariable调用之间是否更改了DataSize大小,通常涉及到对程序数据流和修改操作的监控。由于IDA Pro的GetVariable函数主要用于获取某个特定变量在内存中的值,而DataSize的变化通常与内存分配、修改等操作相关,需要结合程序的执行流程和内存操作来进行分析。
经过一番工作后,完成了IDApython静态UEFI缓冲区溢出检测代码改写,但也发现以下问题:
1. idapython输入框有长度限制,超过500-600会被自动截断;
2. 执行效率低下(通常需要等待数小时甚至更久,代码本身逻辑是先按照GUID偏移等逐一递增匹配还原UEFI函数再去做检测,在IDApython环境中执行速度本身确实很慢)
3.3 idasdk工具开发
之后重新改为idasdk进行开发,其编译过程如下:
3.3.1 win编译过程
ida版本:8.3.230623 Windows x64
sdk版本:idasdk_pro83_230623
Developer Command Prompt for VS 2019
Python git clone https://github.com/binarly-io/efiXplorer.git cd efiXplorer git submodule update --init --recursive
mkdir build && cd build cmake .. -DIdaSdk_ROOT_DIR=C:\Users\nirva\Desktop\test\002-uefi\idasdk_pro83 -DHexRaysSdk_ROOT_DIR=C:\IDAPro8.3\plugins\hexrays_sdk cmake .. -DIdaSdk_ROOT_DIR=/home/gandalf/test/005-idapython/idasdk90 -DHexRaysSdk_ROOT_DIR=/home/gandalf/idapro-9.0/plugins/hexrays_sdk cmake --build . --config Release |
生成文件
C:\Users\nirva\Desktop\test\002-uefi\efiXplorer\build\efiXloader\Release
C:\Users\nirva\Desktop\test\002-uefi\efiXplorer\build\efiXplorer\Release
修改路径
C:\Users\nirva\AppData\Roaming\Hex-Rays\IDA Pro\loaders
C:\Users\nirva\AppData\Roaming\Hex-Rays\IDA Pro\plugins
3.3.2 Linux编译过程
ida版本:beta Version 9.0.240807 Linux x86_64 (64-bit address size)
sdk版本:idasdk90 beta
编译过程
Bash git clone https://github.com/binarly-io/efiXplorer.git cd efiXplorer git submodule update --init --recursive
mkdir build && cd build cmake .. -DIdaSdk_ROOT_DIR=/home/gandalf/test/005-idapython/idasdk90 -DHexRaysSdk_ROOT_DIR=/home/gandalf/idapro-9.0/plugins/hexrays_sdk cmake --build . --config Release |
开发完成并编译通过后,执行效果如下图:
四、检测结果
工具开发完成后,已经可以在实际的ARM UEFI固件中检测出疑似存在的双重GetVariable调用时第二次未初始化导致的漏洞,如下图。 之后又对OEM厂商的高通笔记本设备进行了一轮测试,最终在ASUS和HP某型号BIOS固件中检测到了几个漏。
没有评论:
发表评论