2025年10月22日星期三

ARM架构UEFI静态漏洞分析

一、ASUS安全名人堂 & HP安全致谢

     efiXplorer工具是binarly-io团队开发的UEFI固件分析 IDA Pro插件,其中提供了x86 UEFI静态扫描漏洞功能:

        https://github.com/binarly-io/efiXplorer/

       参考了该工具,笔者完成了ARM架构的检测代码开发,之后又OEM厂商的高通笔记本设备进行了一轮测试,最终在ASUSHP某型号BIOS固件中检测到了几个漏洞,但由于这些漏洞代码均属于Insyde且已修复,因此ASUSHP未申请新漏洞编号。

不过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_detectiontrue,表示可能发生了错误的检测。
            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_oktrue),代码将查找包含当前地址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的使用情况,以及是否存在可能破坏分析的指令(如callniretnjmpjmpniDataSize被多次使用)。

○ 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 x86arm汇编代码对比

3.1.1 X86相关指令

如下图,x86GetVariableDataSize参数通过“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''ARMGetVariableDataSize参数&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_addARM_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 ProIDAPython插件进行逆向工程时,检测两次GetVariable调用之间是否更改了DataSize大小,通常涉及到对程序数据流和修改操作的监控。由于IDA ProGetVariable函数主要用于获取某个特定变量在内存中的值,而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厂商的高通笔记本设备进行了一轮测试,最终在ASUSHP某型号BIOS固件中检测到了几个漏。



ARM架构UEFI静态漏洞分析

一、ASUS安全名人堂 & HP安全致谢       efiXplorer工具是 binarly-io团队开发的UEFI固件分析 IDA Pro插件,其中提供了x86 UEFI静 态扫描漏洞功能:           https://github.com/binarly-...