UEFI 스터디 8차 - 기드라를 사용한 드라이버 정적분석 스크립트 작성 (2)

5 minute read

Published:

테스트할 과거의 드라이버 구하기

이전 주에 edk2외의 테스트할 드라이버를 구하기 위해 상용 바이오스 설치 프로그램에서 UEFI 이미지를 추출해보았다.
그럼 이번엔 레노버 ‘Yoga 7 14lAL7’의 취약점이 있었던 드라이버를 찾아보자.

중요한 보안 이슈라면 무조건 벤더에서 공지를해서 바이오스 업데이트를 권고할 것이라 생각했고, 레노버 역시
Lenovo Product Security Advisories and Announcements 에서 CVE, 문제에대한 간략한 설명, 문제가 있는 모델과 최소 권장 업데이트 버전을 명시해두었다.

이때 무조건 한국 유통용 사이트가 아닌 해외용 사이트로 접속하자 (이유는 모르지만 배포하는 버전이 다르고, 보안 이슈 페이지에서도 보이지 않는다.)

이제 이전에 보았던 레노버의 바이오스 드라이버 설치용 exe 파일을 배포하는 사이트로 가서 설치하는 버튼에 우클릭하고 해당 링크를 복사한다.
UI에는 최신버전의 BIOS만 설치할 수 있도록 해놓았다. 하지만, 설치 링크 끝의 버전을 아까 명시되었던 버전보다 더 낮은 버전으로 교체하여 해당 주소로 접속하면 해당 버전의 exe를 받을 수 있다!



패턴매칭으로 OOB 유발 코드 탐지하기.

이전주에 EDK2 외의 드라이버를 가져와서, 파라미터 개수를 이용해 문제가 되는 함수까지 특정을 해보았다.

해당 이슈의 근본적으로 가면 문제는 검증되지 않는 값을 이용해서 버퍼 할당하기이다.
해당함수의 Xref를 따라가다보면 보고서에서 언급된 AllocatePool UEFI 서비스를 이용하여 버퍼를 할당하는 부분을 볼 수 있다.

현재 패턴 매칭만으로는 allocatepool을 호출한 함수 내부에서 직접 NVRAM의 로고의 정보에 접근하지 않는 이상 패턴만으로 이게 별의미 없는 32비트 정수인지 로고의 크기 정보인지 알수가 없다. 여기서도 지역 변수에 인수의 값을 저장해서 연산을한다.

탐지 로직
주로 이미지 파일을 위한 버퍼를 할당할때 사용하는 연산 형태 [4(1픽셀 4바이트) * 변수(가로) * 변수(세로)]를 통해 후보를 추려낸다.

  1. 피연산자*피연산자*4 형태의 패턴을 매칭하여 큐에 넣는다.
  2. 이 패턴이 등장한 함수에서 피연산자를 비교하는 p코드 인스트럭션(비교하는것을 if문으로 변수가 유효한지 검사하는 것으로 가정함)
    이 한 피연산자라도 없다면 취약점으로 판단하여 출력하도록 하였다.

해당 로직과, 해당 부분의 p코드를 바탕으로 제미나이를 이용해 작성한 기드라 스크립트 코드이다.

import ghidra.app.script.GhidraScript;
import ghidra.program.model.listing.*;
import ghidra.program.model.pcode.*;

public class OOBDetection extends GhidraScript {

    @Override
    protected void run() throws Exception {
        if (currentProgram == null) {
            println("[-] 에러: 현재 기드라 화면에 열려있는 바이너리가 없습니다.");
            return;
        }

        println("==================================================");
        println("[*] 5단계: OOB 패턴(곱셈 후 잘림) 및 크기 검증 누락 탐지");
        println("[+] 현재 파일: " + currentProgram.getName());
        println("==================================================");

        int count = 0;
        int vulnCount = 0;
        FunctionIterator functionIter = currentProgram.getFunctionManager().getFunctions(true);

        while (functionIter.hasNext()) {
            if (monitor.isCancelled()) break;
            Function function = functionIter.next();

            Varnode lastMultOut = null;
            PcodeOp lastMultOp = null;

            InstructionIterator instIter = currentProgram.getListing().getInstructions(function.getBody(), true);
            while (instIter.hasNext()) {
                Instruction inst = instIter.next();

                for (PcodeOp op : inst.getPcode()) {
                    int opcode = op.getOpcode();

                    // 1. 곱셈 연산 기억하기
                    if (opcode == PcodeOp.INT_MULT) {
                        lastMultOut = op.getOutput();
                        lastMultOp = op;
                    }
                    // 2. 자르기 연산 매칭 확인
                    else if (opcode == PcodeOp.SUBPIECE && lastMultOut != null) {
                        Varnode subpieceIn = op.getInput(0);

                        if (isSameVarnode(lastMultOut, subpieceIn)) {
                            println("--------------------------------------------------");
                            println("[+] 완벽 매칭! 주소: " + inst.getAddress() + " (함수: " + function.getName() + ")");
                            println("    1. 곱셈 연산: " + lastMultOp.toString());
                            println("    2. 자르기 연산: " + op.toString());
                            count++;

                            // 3. 콜러 함수 내 피연산자 크기 검증 로직 확인
                            Varnode operand1 = lastMultOp.getInput(0);
                            Varnode operand2 = lastMultOp.getInput(1);

                            boolean isOp1Checked = hasComparisonCheck(function, operand1);
                            boolean isOp2Checked = hasComparisonCheck(function, operand2);

                            if (!isOp1Checked || !isOp2Checked) {
                                println("    [!] 취약점 경고: 피연산자에 대한 크기 검증(비교) 로직이 누락되었습니다!");
                                if (!isOp1Checked) println("        -> 누락 확인된 피연산자 1: " + operand1.toString());
                                if (!isOp2Checked) println("        -> 누락 확인된 피연산자 2: " + operand2.toString());
                                vulnCount++;
                            } else {
                                println("    [*] 안전: 피연산자들에 대한 비교 검증 로직이 존재합니다.");
                            }

                            lastMultOut = null; // 다음 탐지를 위해 초기화
                        }
                    }
                }
            }
        }
        println("==================================================");
        println("[*] 탐색 완료. 총 " + count + "건의 패턴 중, " + vulnCount + "건의 취약점 의심 포인트를 찾았습니다.");
    }

    /**
     * 두 Varnode가 정확히 같은 임시 변수($U)나 레지스터를 가리키는지 확인
     */
    private boolean isSameVarnode(Varnode v1, Varnode v2) {
        if (v1 == null || v2 == null) return false;
        if (v1.isUnique() && v2.isUnique() && v1.getOffset() == v2.getOffset()) return true;
        if (v1.isRegister() && v2.isRegister() && v1.getOffset() == v2.getOffset()) return true;
        return false;
    }

    /**
     * 콜러 함수 내부 전체를 스캔하여, 해당 피연산자가 비교 연산에 사용된 적이 있는지 확인
     */
    private boolean hasComparisonCheck(Function func, Varnode targetVar) {
        // 상수는 검사할 필요가 없으므로 true 반환
        if (targetVar == null || targetVar.isConstant()) return true;

        InstructionIterator instIter = currentProgram.getListing().getInstructions(func.getBody(), true);
        while (instIter.hasNext()) {
            Instruction inst = instIter.next();
            for (PcodeOp op : inst.getPcode()) {
                int opcode = op.getOpcode();

                // 크기 비교를 수행하는 P-code 연산자들
                if (opcode == PcodeOp.INT_LESS || opcode == PcodeOp.INT_LESSEQUAL ||
                        opcode == PcodeOp.INT_SLESS || opcode == PcodeOp.INT_SLESSEQUAL ||
                        opcode == PcodeOp.INT_EQUAL || opcode == PcodeOp.INT_NOTEQUAL) {

                    // 비교 연산의 입력값 중 하나라도 타겟 피연산자와 일치하는지 확인
                    for (int i = 0; i < op.getNumInputs(); i++) {
                        if (isSameVarnode(targetVar, op.getInput(i))) {
                            return true; // 검증 로직이 존재함
                        }
                    }
                }
            }
        }
        return false; // 함수 전체를 뒤졌으나 해당 변수에 대한 검증 로직이 없음
    }
}

결과

문제가 있었던 p코드 (어셈블리 기준 7e5라인)이 잘 탐지 되었다.
참고로 하단에 추가로 탐지된 부분은


위에 검증되지 않았던 버퍼에 값을 할당하는 부분이다.(실제 문제 터지는 부분)

문제는 없을까?

패턴 매칭만을 사용하니 생기는 가장 큰 문제는 이 변수가 정말 내가 찾는 변수 함수인가 이다.

  • 위의 이미지 크기 계산 연산과 동일한 형태의 연산은 모두 의심한다. => 오탐률이 높다.
  • 이전에 그냥 넘어간 AllocatePool 연산, 왜 gBS-> AllocatePool 형태가 아니지? => 이건 edk2에서 제공하는 래퍼함수 형태임. => 같은 함수라도 조금만 감싸놓으면 패턴 매칭이 안됨.

따라서 더욱 견고하게 해당 함수와 변수의 정체를 확인하려면 bottom up으로 이것이 무엇이었는지 따라가는 데이터 흐름 분석이 선행되어야할 것이다.


다음주에 할 것

  • 기드라 자체로 제공하는 데이터 흐름 분석을 이용하여 코드 보완하기
  • UEFI DXE 단계에만 볼 법한 가치있는 취약점 찾아보기
  • 용진이형, 일광이 쪽 도와주기