import ghidra.app.script.GhidraScript; import ghidra.app.decompiler.*; import ghidra.program.model.address.AddressIterator; import ghidra.program.model.listing.*; import ghidra.program.model.pcode.*; import ghidra.program.model.address.Address; import java.util.ArrayList; import java.util.List; import java.util.*; public class SmmCalloutHunter_v4_0_1 extends GhidraScript { // 디컴파일러 private DecompInterface decomp; // 이미 방문한 함수 주소를 저장하여 무한 루프 방지 private Set
visitedFunctions = new HashSet<>(); // Def-Use Chain에서 찾은 gBS 주소를 저장할 변수 private Address foundGbsAddr = null; private long startTime; @Override protected void run() throws Exception { startTime = System.currentTimeMillis(); println("=================================================="); println("Starting SMM Callout using gBS...v4.0.1"); println("=================================================="); // ================================================== // [Phase 0-1] Triage: 이 파일이 SMM 드라이버가 맞는지 감별 // ================================================== if (!isSmmDriver()) { println("Could not find SMM_BASE2_PROTOCOL"); println("일반 DXE 드라이버이므로 종료."); String info = "SMM 드라이버가 아니어 조기 종료됨."; saveJsonToFileIfError(info); return; } println("SMM 드라이버 확인 완료. 본격적인 분석 시작.️"); // 디컴파일러 초기화 decomp = new DecompInterface(); decomp.openProgram(currentProgram); // ================================================== // [Phase 0] find Handlers: 숨어있는 핸들러를 찾아내기 // ================================================== // v4 수정 : SMI 핸들러을 찾는 로직을 완전히 새롭게 작성. 이제는 전체 함수를 뒤져서 CALLIND를 찾고, 그 주변에 0xE0이나 GUID가 있는지 보는 방식으로 변경 List
smiHandlers = findAndCreateSmiHandlers(); println("=================================================="); println("탐색 및 생성 완료!"); // ================================================== // [Phase 1] Def-Use: gBS 전역 변수 위치 찾기 // ================================================== println("gBS 전역 변수 탐색 시작"); // 진입점 찾기 AddressIterator entryPoints = currentProgram.getSymbolTable().getExternalEntryPointIterator(); if (entryPoints.hasNext()) { // 보통 UEFI는 _ModuleEntryPoint이 진입점이므로, 첫 번째 엔트리 포인트를 사용 Function entryFunc = getFunctionAt(entryPoints.next()); // System Table 포인터는 entry 함수의 두번쨰 인자. Varnode systemTableNode = getParameterVarnode(entryFunc, 1); if (systemTableNode != null) { visitedFunctions.add(entryFunc.getEntryPoint()); // SystemTable 포인터를 통해 gBS 추적 boolean found = trackSystemTable(entryFunc, systemTableNode, 0); // 찾지 못했을 경우 if (!found) { println("gBS 추적 실패. 스크립트를 종료합니다."); String info = "gBS 전역 변수 위치를 찾지 못해 분석을 종료함."; saveJsonToFileIfError(info); return; } } else { println("Entry Point 파라미터 분석 실패."); String info = "Entry Point의 시스템 테이블 포인터를 찾지 못해 분석을 종료함."; saveJsonToFileIfError(info); return; } } else { println("Entry Point를 찾을 수 없습니다."); String info = "Entry Point를 찾지 못해 분석을 종료함."; saveJsonToFileIfError(info); return; } println("타겟 gBS 주소: " + foundGbsAddr); println("--------------------------------------------------"); // ================================================== // [Phase 2] Use-Def: SMM Callout 취약점 스캔 // ================================================== println("SMM Callout 취약점 스캔 시작!"); println("--------------------------------------------------"); scanForSmmCallouts(smiHandlers); println("모든 분석이 완료되었습니다."); } // Def-Use Chain을 타고 gBS 전역 변수의 위치를 찾는 함수들 private boolean trackSystemTable(Function func, Varnode node, int depth) { if (node == null || depth > 10) { return false; } // 이 노드를 사용하는 PcodeOp를 순회 Iterator uses = node.getDescendants(); while (uses.hasNext()) { PcodeOp op = uses.next(); int opcode = op.getOpcode(); // PTRADD : 포인터 값에 정수 오프셋을 더해 새 메모리 주소 계산 // INT_ADD : PTRADD 말고 단순 정수 덧셈으로 계산하는 경우도 있을 수 있다 if (opcode == PcodeOp.PTRADD || opcode == PcodeOp.INT_ADD || opcode == PcodeOp.PTRSUB) { Varnode offsetNode = op.getInput(1); // 더하는 값이 상수이면서 0x60인지 확인 (gBS의 오프셋이 0x60이므로) if (offsetNode != null && offsetNode.isConstant() && offsetNode.getOffset() == 0x60) { println("[+0x60 Catch!] 함수: " + func.getName()); if (checkIfStoredToGlobal(op.getOutput(), 0)) { return true; } } } // COPY나 CAST로 포인터가 한 번 더 꼬여있으면 더 깊게 파고들기 else if (opcode == PcodeOp.COPY || opcode == PcodeOp.CAST) { if (trackSystemTable(func, op.getOutput(), depth)) { return true; } } // CALL로 다른 함수에 매개변수로 넘어가는 경우도 추적 (recursive call) else if (opcode == PcodeOp.CALL) { Address targetAddr = op.getInput(0).getAddress(); Function targetFunc = getFunctionAt(targetAddr); // 만약 이미 방문 한 곳이라면 패스 if (targetFunc != null && !visitedFunctions.contains(targetAddr)) { // paramIndex : 이 노드가 CALL에서 몇 번째 인자로 넘어가는지 찾기 (0번은 함수 포인터이므로 1번부터 시작) // 이거는 한번 찾아보는걸로 int paramIndex = -1; for (int i = 1; i < op.getNumInputs(); i++) { if (op.getInput(i) == node) { paramIndex = i - 1; break; } } if (paramIndex != -1) { visitedFunctions.add(targetAddr); Varnode nextNode = getParameterVarnode(targetFunc, paramIndex); if (trackSystemTable(targetFunc, nextNode, depth + 1)) { return true; } } } } } return false; } // 전역 변수로 들어가는지 추적하는 함수 private boolean checkIfStoredToGlobal(Varnode ptrNode, int depth) { if (ptrNode == null || depth > 5) { return false; } Iterator uses = ptrNode.getDescendants(); while (uses.hasNext()) { PcodeOp op = uses.next(); int opcode = op.getOpcode(); // CAST 나 COPY로 한 번 더 꼬여있을 수 있으니 계속 추적 if (opcode == PcodeOp.CAST || opcode == PcodeOp.COPY) { if (checkIfStoredToGlobal(op.getOutput(), depth + 1)) { return true; } } // LOAD를 통해 메모리에서 들어오는 과정 역시 추적 else if (opcode == PcodeOp.LOAD) { if (trackValueToMemory(op.getOutput(), 0)) { return true; } } } return false; } // 메모리에서 읽어온 값이 gBS 전역 변수로 이어지는지 추적하는 함수 // node : LOAD된 값을 받는 노드 (메모리에서 읽어온 값이 저장되는 곳) // depth : 재귀 깊이 (무한 루프 방지) private boolean trackValueToMemory(Varnode node, int depth) { if (node == null || depth > 5) { return false; } Iterator uses = node.getDescendants(); while (uses.hasNext()) { PcodeOp op = uses.next(); int opcode = op.getOpcode(); if (opcode == PcodeOp.STORE) { Varnode destNode = op.getInput(1); if (destNode != null && destNode.isConstant()) { foundGbsAddr = currentProgram.getAddressFactory().getDefaultAddressSpace().getAddress(destNode.getOffset()); println("gBS 전역 변수 발견: " + foundGbsAddr); try { // createLabel을 통해 시각화 createLabel(foundGbsAddr, "gBS_Global_Variable", true); } catch (Exception e) {} return true; } } else if (opcode == PcodeOp.COPY || opcode == PcodeOp.CAST || opcode == PcodeOp.MULTIEQUAL || opcode == PcodeOp.INDIRECT) { Varnode destNode = op.getOutput(); if (destNode != null) { if (destNode.getAddress().isMemoryAddress()) { foundGbsAddr = destNode.getAddress(); println("gBS 전역 변수 발견: " + foundGbsAddr); try { createLabel(foundGbsAddr, "gBS_Global_Variable", true); } catch (Exception e) {} return true; } else { if (trackValueToMemory(destNode, depth + 1)) { return true; } } } } } return false; } // ------------------------------------------------------- // [Phase 2 Logic] 역방향 추적 (Use-Def) - SMM Callout 탐지 // ------------------------------------------------------- // v3 수정 : 모든 함수를 받아서 스캔하는 것이 아닌 찾은 핸들러들을 대상으로 수정 // smiHandlers : Phase 1에서 찾아낸 SMI 핸들러들의 주소 리스트 private void scanForSmmCallouts(List
smiHandlers) { int vulnCount = 0; List locationsList = new ArrayList<>(); // 전체 함수를 스캔하는 대신, SMI 핸들러들과 그 자식 함수들만 스캔하기 위해 Set으로 중복 제거 Set runtimeFunctions = new HashSet<>(); // 찾아낸 SMI 핸들러들을 시작점으로 해서, 호출되는 모든 자식 함수(Call Graph)를 수집합니다. for (Address addr : smiHandlers) { Function handlerFunc = getFunctionAt(addr); if (handlerFunc != null) { buildRuntimeCallGraph(handlerFunc, runtimeFunctions); } } println("런타임 스캔 대상 함수 총 " + runtimeFunctions.size() + "개 추출 완료!"); // 수집된 런타임 함수(SMI 핸들러 + 자식 함수)들만 스캔 for (Function func : runtimeFunctions) { DecompileResults results = decomp.decompileFunction(func, 30, monitor); HighFunction highFunc = results.getHighFunction(); if (highFunc == null) continue; Iterator ops = highFunc.getPcodeOps(); while (ops.hasNext()) { PcodeOp op = ops.next(); if (op.getOpcode() == PcodeOp.CALLIND) { Varnode targetFuncPtr = op.getInput(0); if (isTaintedByGBS(targetFuncPtr, 0)) { println("[SMM Callout 취약점 의심부 발견!]"); println("함수: " + func.getName()); println("주소: " + op.getSeqnum().getTarget()); println("원인: gBS 전역 변수를 참조하여 외부 함수를 호출함!"); vulnCount++; String locJson = String.format(" {\n \"function_name\": \"%s\",\n \"pcode_address\": \"%s\"\n }", func.getName(), op.getSeqnum().getTarget()); locationsList.add(locJson); } } } } // ... (아래 JSON 저장 로직은 기존과 100% 동일하게 유지) ... if (locationsList.isEmpty()) { String defaultNullLoc = " {\n \"function_name\": null,\n \"pcode_address\": null\n }"; locationsList.add(defaultNullLoc); } if (vulnCount == 0) { println("\nSafe : 런타임(SMI)에서 gBS를 사용하는 Callout 패턴이 발견되지 않았습니다."); } else { println("\nWarning : 총 " + vulnCount + "개의 런타임 취약점 의심 지점이 발견되었습니다."); } String scriptName = "SmmCalloutHunter_v4_0_0"; String binaryName = currentProgram.getName(); Boolean vulnerabilityFound = vulnCount > 0 ? true : false; String timestamp = (System.currentTimeMillis() - startTime) / 1000.0 +""; String info = vulnCount > 0 ? "런타임(SMI)에서 gBS를 참조하여 간접 호출하는 패턴이 발견됨" : "취약점 의심 패턴이 발견되지 않음"; String locationsArrayString = String.join(",\n", locationsList); saveJsonToFile(scriptName, binaryName, vulnerabilityFound, timestamp, locationsArrayString, info); } // SMI 핸들러 내부에서 호출되는 모든 자식 함수(런타임 함수)를 재귀적으로 수집. private void buildRuntimeCallGraph(Function func, Set runtimeFunctions) { if (func == null || runtimeFunctions.contains(func)) return; runtimeFunctions.add(func); // 수집 목록에 추가 // 이 함수가 호출하는 다른 함수들(Called Functions)을 가져옵니다. Set calledFunctions = func.getCalledFunctions(monitor); for (Function childFunc : calledFunctions) { buildRuntimeCallGraph(childFunc, runtimeFunctions); // 재귀 추적 } } // Taint Analysis를 통해 거꾸로 추적하는 함수 private boolean isTaintedByGBS(Varnode node, int depth) { if (node == null || depth > 10) { return false; } if (node.getAddress() != null && node.getAddress().isMemoryAddress()) { if (node.getAddress().equals(foundGbsAddr)) { // System.out.println(" -> [디버그] 메모리 변수에서 gBS 역추적 성공!"); return true; } } // 역추적을 해 연산자 가져오기 PcodeOp defOp = node.getDef(); if (defOp == null) { return false; } int opcode = defOp.getOpcode(); // 메모리에서 읽어온거면 주소 확인 if (opcode == PcodeOp.LOAD) { Varnode addrNode = defOp.getInput(1); // 읽어온 메모리 주소 // 주소가 상수라면 그대로 비교 if (addrNode != null && addrNode.isConstant()) { Address sourceAddr = currentProgram.getAddressFactory().getDefaultAddressSpace().getAddress(addrNode.getOffset()); // 동일하다면 gBS에서 유래한 주소가 맞으므로 오염된 것으로 간주 if (sourceAddr.equals(foundGbsAddr)) { return true; } } // 계산이 된 경우라면 그 주소가 gBS에서 유래했는지 계속 추적 else { return isTaintedByGBS(addrNode, depth + 1); } } else if (opcode == PcodeOp.COPY || opcode == PcodeOp.CAST || opcode == PcodeOp.INT_ADD || opcode == PcodeOp.PTRADD || opcode == PcodeOp.PTRSUB || opcode == PcodeOp.MULTIEQUAL || opcode == PcodeOp.INDIRECT) { // 입력값들 중 하나라도 gBS에서 왔다면 오염된 것으로 간주 for (Varnode input : defOp.getInputs()) { if (isTaintedByGBS(input, depth + 1)) { return true; } } } return false; } // 유틸리티 함수 private Varnode getParameterVarnode(Function func, int paramIndex) { DecompileResults results = decomp.decompileFunction(func, 30, monitor); HighFunction highFunc = results.getHighFunction(); if (highFunc == null) { return null; } LocalSymbolMap lsm = highFunc.getLocalSymbolMap(); if (paramIndex >= lsm.getNumParams()) { return null; } HighSymbol paramSym = lsm.getParamSymbol(paramIndex); if (paramSym != null && paramSym.getHighVariable() != null) { return paramSym.getHighVariable().getRepresentative(); } return null; } // JSON 저장 함수 /* { "script_name": "string", "binary_name": "string", "vulnerability_found": boolean, "timestamp": "ISO 8601 format", "locations": [ { "function_name": "string", "pcode_address": "string" } ], "info" : "string" */ private void saveJsonToFile(String script_name, String binary_name, Boolean vulnerability_found, String timestamp, String locationsArrayString, String info) { String json = String.format("{\n" + " \"script_name\": \"%s\",\n" + " \"binary_name\": \"%s\",\n" + " \"vulnerability_found\": %s,\n" + " \"timestamp\": \"%s\",\n" + " \"locations\": [\n" + "%s\n" + " ],\n" + " \"info\": \"%s\"\n" + "}", script_name, binary_name, vulnerability_found, timestamp, locationsArrayString, info); try { String userHome = System.getProperty("user.home") + "/Desktop"; String fileName = String.format("%s/%s_report.json", userHome, script_name); java.nio.file.Files.write(java.nio.file.Paths.get(fileName), json.getBytes()); println("분석 결과가 JSON 파일로 저장되었습니다: " + fileName); } catch (Exception e) { println("JSON 파일 저장 중 오류 발생: " + e.getMessage()); } } // 타겟 함수들을 찾는 함수 private List
findAndCreateSmiHandlers() { List
handlers = new ArrayList<>(); // SW_DISPATCH2 GUID 바코드 byte[] swDispatchGuid = new byte[] { (byte)0xdc, (byte)0xc6, (byte)0xa3, (byte)0x18, (byte)0xea, (byte)0x5e, (byte)0xc8, (byte)0x48, (byte)0xa1, (byte)0xc1, (byte)0xb5, (byte)0x33, (byte)0x89, (byte)0xf9, (byte)0x89, (byte)0x99 }; long guidOffset = -1; // SW_DISPATCH guid가 찍혀있는 위치가 있다면 offset을 저장 try { Address guidAddr = currentProgram.getMemory().findBytes(currentProgram.getMinAddress(), swDispatchGuid, null, true, monitor); if (guidAddr != null) guidOffset = guidAddr.getOffset(); } catch (Exception e) {} // 기존 방식 폐기. 전체 함수를 뒤지는 방향으로 수정 FunctionIterator funcs = currentProgram.getFunctionManager().getFunctions(true); while (funcs.hasNext()) { Function func = funcs.next(); DecompileResults results = decomp.decompileFunction(func, 30, monitor); HighFunction highFunc = results.getHighFunction(); if (highFunc == null) continue; boolean containsGuid = false; // 해당 함수 안에 guid가 있었는가? List callIndOps = new ArrayList<>(); Iterator ops = highFunc.getPcodeOps(); while (ops.hasNext()) { PcodeOp op = ops.next(); if (op.getOpcode() == PcodeOp.CALLIND) callIndOps.add(op); for (int i = 0; i < op.getNumInputs(); i++) { Varnode input = op.getInput(i); if (input != null && input.isConstant() && input.getOffset() == guidOffset) { containsGuid = true; // 함수 P-Code 안에 guid가 있었다면 true } } } for (PcodeOp op : callIndOps) { Varnode targetFuncPtr = op.getInput(0); // [타겟 1] Root 핸들러 (0xE0) if (hasOffsetE0(targetFuncPtr, 0)) { Address handlerAddr = traceAddress(op.getInput(1)); // isConstant() 제거, 역추적 도입! if (handlerAddr != null && currentProgram.getMemory().contains(handlerAddr)) { handlers.add(handlerAddr); println("\n[!] Root 핸들러(0xE0) 포착! (위치: " + func.getName() + ")"); createFuncIfNeeded(handlerAddr, "RootSmiHandler_" + handlerAddr.toString()); } } // [타겟 2] Child 핸들러 (GUID 포함 함수 내 간접호출) else if (containsGuid && op.getNumInputs() > 2) { Address handlerAddr = traceAddress(op.getInput(2)); // isConstant() 제거, 역추적 도입! if (handlerAddr != null && currentProgram.getMemory().contains(handlerAddr)) { handlers.add(handlerAddr); println("\n[!] Child 핸들러(SW_DISPATCH) 포착! (위치: " + func.getName() + ")"); createFuncIfNeeded(handlerAddr, "ChildSmiHandler_" + handlerAddr.toString()); } } } } return handlers; } //단순 상수(Constant)뿐만 아니라, 레지스터 연산(LEA, PTRSUB)에 담긴 주소까지 추적 private Address traceAddress(Varnode node) { if (node == null) return null; // 운 좋게 상수로 바로 들어온 경우 if (node.isConstant()) { return currentProgram.getAddressFactory().getDefaultAddressSpace().getAddress(node.getOffset()); } PcodeOp def = node.getDef(); if (def == null) return null; int opcode = def.getOpcode(); // 레지스터 복사(COPY)나 형변환(CAST)이 일어났다면 그 이전 출처를 쫓아감 if (opcode == PcodeOp.COPY || opcode == PcodeOp.CAST) { return traceAddress(def.getInput(0)); } // 어셈블리 LEA 명령어 (P-Code로는 보통 PTRSUB로 번역됨)를 통해 주소를 계산한 경우 else if (opcode == PcodeOp.PTRSUB || opcode == PcodeOp.PTRADD) { Varnode offset = def.getInput(1); if (offset != null && offset.isConstant()) { return currentProgram.getAddressFactory().getDefaultAddressSpace().getAddress(offset.getOffset()); } } return null; } // 연산 트리를 재귀적으로 파고들며 0xE0(SmmHandlerRegister)를 찾는 함수 private boolean hasOffsetE0(Varnode node, int depth) { // 너무 깊게 들어가면 추적 종료 if (node == null || depth > 10) { return false; } // 상수가 0xE0 (십진수 224) 이면 추적 성공 if (node.isConstant() && node.getOffset() == 0xE0) { return true; } PcodeOp def = node.getDef(); // 정의가 없는 노드면 추적 종료 if (def == null) { return false; } // LOAD, COPY, CAST, PTRADD 등 이 노드를 만든 부모 연산자의 입력값들을 전부 추적 for (int i = 0; i < def.getNumInputs(); i++) { if (hasOffsetE0(def.getInput(i), depth + 1)) { return true; } } return false; } // ------------------------------------------------------- // [Phase 0 Logic] SMM 드라이버 감별 함수 (GUID 스캔) // ------------------------------------------------------- private boolean isSmmDriver() { // gEfiSmmBase2ProtocolGuid = { 0xf4ccbfb7, 0xf6e0, 0x47fd, { 0x9d, 0xd4, 0x10, 0xa8, 0xf1, 0x50, 0xc1, 0x91 } } // 메모리에 올라갈 때는 리틀 엔디안(Little-Endian) 방식으로 뒤집혀서 저장됩니다. byte[] smmBase2Guid = new byte[] { (byte)0xb7, (byte)0xbf, (byte)0xcc, (byte)0xf4, // 0xf4ccbfb7 (byte)0xe0, (byte)0xf6, // 0xf6e0 (byte)0xfd, (byte)0x47, // 0x47fd (byte)0x9d, (byte)0xd4, (byte)0x10, (byte)0xa8, (byte)0xf1, (byte)0x50, (byte)0xc1, (byte)0x91 }; try { // 프로그램 전체 메모리에서 해당 16바이트 GUID 패턴을 싹 뒤집니다. Address foundAddr = currentProgram.getMemory().findBytes( currentProgram.getMinAddress(), smmBase2Guid, null, true, monitor ); // 발견되었다면 이 녀석은 SMM 환경에 진입하려 한 SMM 드라이버. if (foundAddr != null) { println("[SMM_BASE2_PROTOCOL GUID] 발견! 위치: " + foundAddr); return true; } } catch (Exception e) { println("GUID 검색 중 오류: " + e.getMessage()); } return false; } private void saveJsonToFileIfError(String errorMessage) { String scriptName = "SmmCalloutHunter_v4_0_0"; String binaryName = currentProgram.getName(); Boolean vulnerabilityFound = false; String timestamp = (System.currentTimeMillis() - startTime) / 1000.0 +""; String locJson = String.format(" {\n \"function_name\": \"%s\",\n \"pcode_address\": \"%s\"\n }", null, null); saveJsonToFile(scriptName, binaryName, vulnerabilityFound, timestamp, locJson, errorMessage); } // 함수 강제 생성 유틸리티 private void createFuncIfNeeded(Address addr, String name) { if (getFunctionAt(addr) == null) { try { disassemble(addr); createFunction(addr, name); println(" -> 기드라에 새 함수 생성 완료: " + name); } catch (Exception e) {} } } }