코드 개선사항
이전에 짰던 gBS 탐지 스크립트는 아래와 같은 문제를 가지고 있었다.
- ghidra가 SMM 런타임 함수를 찾지 못한다.
이는 entry부터 찾아들어가는 ghidra의 특성과 달리 SMI를 통해 강제로 점프하여 들어오는 곳이라 참조가 끊겨 생기는 문제로 추측한다. - 부팅 중에는 gBS가 실행되어도 된다.
하지만 기존 코드는 모든 함수를 다 돌아버리면서 부팅용 함수든 런타임용 함수든 전부 다 검사해버려 오탐(False Positive)가 발생한다. - 나중에 자동화 과정에서 이게 SMM 단에서 실행되는지, 일반 DXE 드라이버인지 알 수 없다.
추후 자동화 통합 과정에서 검사를 수행할 드라이버엔 SMM 드라이버도, DXE 드라이버도 모두 존재한다. 그러므로 gBS를 사용해도 되는 일반 DXE 드라이버들과 구별을 하기 위해 초반 처리 로직이 필요하다.
개선 로직
개선 로직은 다음과 같이 개선했다.
- 먼저 검사 로직을 수행하기 전에 프로그램 내에 gSmmBase2ProtocolGuid가 있는지 확인한다.
만약 없다면 이는 SMM 드라이버가 아닌 것으로 간주, 검사를 수행하지 않고 스크립트를 종료한다. - SMM 드라이버라면 검사 시작 전 해당 프로그램의 모든 바이너리를 순회한다.
- 순회한 각 코드의 P-Code를 뽑아내고, 뽑아낸 P-Code에 간접 호출(CALLIND)가 있는지 찾는다.
- 만약 간접 호출이 있고, 0xE0 오프셋을 더한다면 SMM 핸들러를 등록하는 것으로 간주한다.
- 만약 해당 함수가 등록되어있지 않으면 등록을 해준다.
- 이후 gBS 주소 찾기 및 Taint Analysis를 수행한다.
이 때 모든 함수를 찾던 기존 Taint Analysis가 아닌 찾아낸 핸들러들을 대상으로 한다.
왜?
(굉장히 휴리스틱한 접근들로 로직을 잡고 있다…)
먼저 어떻게 함수들을 찾았는지다. SMI이 울렸을 때 실행되는 함수들은 gSmst->SmmHandlerRegister에 저장되어 있다. 이 때 이는 gSmst에서 0xE0만큼 떨어져 있다. 원래라면 추적을 두번, 세번, 네번 이어 가야 gSmst를 찾아낼 수 있지만, 일단 빠르게 결과부터 보기 위해 0xE0을 더해서 호출하는 로직만 찾아서 찾는, 일종의 찍기를 한 것이다.(……)
다음으로 런타임 함수들만 잡는 방법이였다. 바이너리 자체에는 어떤게 부팅 중 실행되는지, 어떤게 런타임에 실행되는지 알 수 없다. 하지만 부팅 중에 실행되지 않는 함수들은 entry에 연결되지 않는다는 점을 생각했다. 그러면 어디에 저장되는가. 사실 이 저장되는 위치가 바로 gSmst->SmmHandlerRegister다! 이 안에 있는 모든 CALL들을 가져와 리스트, 또는 이터레이터 형태로 저장 후 순회하는 방식으로 바꾸면 된다.
마지막으로 SMM 드라이버인지 판단하여 SMM 드라이버가 아닌 일반 DXE 드라이버라면 로직을 나가는 경우에 대한 처리다. SMM 드라이버는 gEfiSmmBase2ProtocolGuid를 코드 내에 반드시 가지고 있다. 이는 EFI 전역 환경 내에서 SMM 환경에서 제공하는 서비스들을 담은 프로토콜의 GUID로, SMM 드라이버는 gEfiSmmBase2ProtocolGuid를 통해 SmmBase2라는 프로토콜에 접근이 가능하다. 이 프로토콜에 특정 SMM 함수를 요청하면 프로토콜은 InSmm() 함수를 통해 해당 코드가 SMRAM에 들어있는지를 확인하게 된다. 이 InSmm()이 TRUE로 나오면 프로토콜은 GetSmstLocation()을 통해 gSmst의 실제 주소를 넘겨주게 되고, SMM 드라이버는 gSmst에 있는 함수를 간접 호출함으로써 SMM 코드를 실행하게 된다.
이 때 gSmst의 위치를 찾는 것이 아닌 gEfiSmmBase2ProtocolGuid를 찾는 이유는 다음과 같다. gSmst는 펌웨어가 실행된 뒤 주소가 결정이 되는, 즉 매번 주소값이 바뀌는 값이다. 이 값을 찾기 위해선 스크립트를 실행할 때 마다 코드를 역추적해가며 값을 찾아야 한다. 하지만 gEfiSmmBase2ProtocolGuid는 전 세계 모든 펌웨어가 공통적으로 가지는 고유값이므로 코드를 디컴파일할 필요도 없이 프로그램 내에 해당 GUID가 존재하는지 아닌지만 찾으면 된다.
수정 후 각 함수별 코드 요약
| 함수명 | 역할 |
|---|---|
| trackSystemTable() | Def-Use Chain을 타면서 gBS를 찾는 함수 |
| checkIfStoredToGlobal() | Def-Use Chain을 타다가 전역 변수로 넘어가는지 여부를 추적하는 함수 |
| trackValueToMemory() | 메모리에서 읽은 값(LOAD)이 gBS 전역 변수로 이어지는지 확인하는 함수 |
| scanForSmmCallouts() | 찾은 SMI 핸들러들의 주소 리스트를 받아 Call-Graph를 수집하여 gBS를 호출하는지 파악한 뒤 JSON으로 결과를 출력하는 함수 |
| buildRuntimeCallGraph() | 핸들러들이 호출하는 모든 런타임 함수들을 수집하는 함수 |
| isTaintedByGBS() | 인자로 받은 node에서부터 역추적을 해나가며 끝이 gBS인지 확인하는 함수 |
| findAndCreateSmiHandlers() | 코드 전체를 순회하며 SMI 핸들러를 찾고, 발견 즉시 기드라에 함수로 등록하는 함수 |
| hasOffsetE0() | 0xE0을 더하는 연산이 존재하는지 확인하는 함수 |
| getParameterVarnode() | 특정 함수의 몇 번째 인자가 가지는 노드를 반환하는 함수 |
| saveJsonToFile() | 결과를 JSON으로 변환하는 함수 |
| saveJsonToFileIfError() | 오류가 발생한 분기의 에러를 JSON으로 반환하는 함수 |
아마 다음 주? 이번 주? 부터는 지금까지 너무 코드 짜는 것에 치중을 해서 짰던 것 같아서 잠시 이론적인 정리를 해두고, 취약점 코드를 만들고 검증하는 팀원들을 조금 도와줘야 할 것 같다.