
제로-심볼 정적 분석 엔진으로, AHP 기반 위험 모델을 사용하여 Windows RPC 공격 표면을 추출하고 수학적으로 순위를 매깁니다.
정적 vs 동적, 먼저 읽어보세요. 동적 분석과 정적 분석의 경계를 처음부터 분명히 해 둘 필요가 있습니다: 이 도구는 순전히 정적 분석을 통해서만 순위를 매깁니다. 컴파일된 MIDL / NDR 구조를 바이너리에서 직접 읽어 내며 어떤 것도 실행하지 않습니다.
Windows RPC 공격 표면에 대한 정적 트라이지(triage). PE 바이너리가 들어 있는 폴더를 지정하면 RPC 서버를 등록하는 모든 파일을 찾아내고, 각 인터페이스의 NDR 메서드 시그니처, 전송 바인딩 및 등록 플래그를 컴파일된 MIDL 구조에서 직접 복구한 다음, 모든 인터페이스를 도달 가능성 x 위험도 기준으로 순위를 매깁니다. 모든 점수에는 완전한 산술 계산 내역(receipt)이 첨부되어 있어 수기로 계산을 검증할 수 있습니다.
기존 RPC 도구들도 인터페이스의 메서드 시그니처와 등록 플래그를 복구할 수 있습니다. 그러나 어느 도구도 이 둘을 모두 취합하여 실제로 시간을 어디에 쓸지 결정하는 단 한 가지 질문에 답하지는 못합니다: 이 인터페이스에 도달할 수 있고, 그 메서드들이 입력으로 무엇을 받는지를 고려할 때, 이 인터페이스를 시스템의 다른 모든 것과 비교해 얼마나 긴급하게 살펴봐야 하는가? 이 도구가 바로 그 공백을 메웁니다.
필터링 - 대상 폴더를 필터링하여 Ghidra가 실제로 RPC 서버를 등록하는 바이너리만 자동 분석하게 합니다 (rpcrt4.dll을 임포트하고 RpcServerRegisterIf\* API 중 하나를 호출). System32 기준으로 이 차이는 하루 분량과 일주일 분량의 차이입니다.
추출 - 인터페이스별로 다음을 추출합니다: UUID, RPC_SERVER_INTERFACE / MIDL_SERVER_INFO 체인, 권위 있는 DispatchTableCount, 각 메서드의 opnum + 파라미터 방향 + 디코딩된 NDR opcode, 등록 플래그(R9), 보안 콜백("바운서") 존재 여부, 보안 설명자(최선 노력), 엔드포인트 / 전송 바인딩.
순위 산정 - 모든 정상(clean) 인터페이스를 두 개의 독립적인 축으로 평가하고 이를 곱하여 0-100 복합 점수를 만든 뒤 Critical / High / Moderate / Low 버킷으로 분류합니다.
자체 설명 - 모든 점수에는 각 구성 요소와 최종 숫자를 산출한 산술식을 나열한 영수증(receipt) 문자열이 포함됩니다.
대상 디렉터리 --(pefile 필터)--> RPC 등록 PE만 남김
--(Ghidra headless 자동 분석)--> 분석된 프로그램 DB
--(extract_rpc_interfaces.py 스크립트)--> 인터페이스 + NDR + 플래그 + 엔드포인트
--(2축 AHP 순위 엔진)--> 순위가 매겨진 인터페이스 + 영수증
--> 단일 JSON 리포트
심볼 제로 의존성: 이 엔진의 핵심 기술적 차별점은 디버그 심볼 없이 완전히 동작한다는 점입니다. 디스패치 테이블을 프로그래밍 방식으로 탐색하고 원시 NDR(Network Data Representation) 바이트코드와 컴파일된 MIDL 구조를 메모리에서 직접 파싱함으로써 Microsoft의 .pdb 파일이나 라이브 엔드포인트 매퍼 쿼리가 필요 없습니다. 따라서 실제 배포된 그대로의 스트리핑된 프로덕션 System32 바이너리에서도 즉시 동작함이 보장됩니다.
Ghidra 11.x (번들된 support/analyzeHeadless 사용). PATH에 JDK 17+ 필요.
드라이버 측 Python 3.8+ 및 pefile.
추출 스크립트는 Ghidra 번들 Jython 2.7에서 실행됩니다 - 타사 임포트 없음, 설치할 것 없음.
대상: Windows x64 PE 파일. 분석 자체는 OS에 독립적입니다(Ghidra는 크로스 플랫폼). 따라서 Windows에서 실행할 필요는 없습니다.
git clone https://github.com/talha-nazeef-ahmed/RPC-Triage
cd RPC-Triage/
python -m pip install -r requirements.txt # pefile만 설치됨
requirements.txt: pefile>=2023.2.7
모든 것은 orchestrator.py가 처리합니다: 필터링, 임포트, 분석, 추출기 실행을 모두 대신 수행합니다.
python orchestrator.py \
-t \"C:\Windows\System32\" \
-g \"C:\ghidra_12.1.2_PUBLIC\" \
-s \".\extract_rpc_interfaces.py\" \
-o \".\out\report.json\" \
--stagedir \".\out\staged\" \
--projdir \".\out\ghidra_proj\" \
--projname RPC_Atlas
첫 실행 vs 재실행 (중요). 첫 실행은 느린 부분을 수행합니다: 필터링하고, 일치하는 바이너리를 임포트하고, 전체 자동 분석을 실행한 다음 --projdir에 .analysisComplete 마커를 기록합니다. 이후 동일한 프로젝트에 대한 재실행은 임포트/분석을 건너뛰고(-process -noanalysis) 이미 분석된 프로그램들에 대해 스크립트만 다시 실행합니다. 따라서 System32 분석은 일회성 비용이고 출력을 반복하는 것은 저렴합니다. 분석이 중단되면 마커가 기록되지 않으므로 부분 프로젝트(.rep / .gpr)를 삭제하고 새로 시작합니다.
JSON 파일 하나: 바이너리 목록이며 각 바이너리는 Interfaces 배열을 가집니다. System32 전체에 대한 완전한 실행 결과가 이 저장소의 output/FullBatchRun.json에 포함되어 있습니다. 이는 가공되지 않은 원본 출력이므로 도구가 대규모로 무엇을 생성하는지 정확히 확인할 수 있습니다. 인터페이스별:
태그(Tag): 데이터 위생 관문:
Clean: 깨끗하게 복구됨; 정상적으로 순위가 매겨짐.
Needs-Review: 순위는 매겨졌으나 디스패치 테이블 탐색이 저장된 수를 초과함(보통 끝부분의 썽크(thunk) 블록 때문). 점수는 실제지만 [Provisional] 표시가 붙습니다. 인용 전에 메서드 수를 확인하십시오.
Diagnostics / Diagnostics (2): 해당 행은 추출 아티팩트입니다(ASCII 문자열이 UUID로 오인되거나/또는 손상된 MIDL 포인터). 점수 없음 (Rank: N/A). 이것들은 의도적으로 유지됩니다: 도구 상태를 보고할 뿐 공격 표면이 아닙니다.
FLAG: Walked X != Stored Y 표기. 저장된 DispatchTableCount는 권위 있는 값이며 모든 점수가 이를 사용합니다. 탐색된(walked) 수는 독립적인 실행 가능성 교차 검증입니다. 둘이 일치하지 않을 때(흔히 깔끔한 2배수), 인터페이스는 Needs-Review로 태그되어 직접 확인해야 함을 알려줍니다. 점수를 조용히 바꾸는 일은 없습니다.
이 부분이 점수에 대해 논쟁할 수 있게 만드는 핵심입니다:
Moderate/35 | Gate:35 [ncacn_np:41, MultiEndpointBonus:15, HasBouncer:-46, BouncerIsNotCaching:25] | Surface:100 [HasBogusStruct:1x(opnums 5):[in]:61, HasCallerSizedBuffer:5x(opnums 0,6,11):[in]:49, InPtrs:6:18, Count:12:6 -> raw:134 [capped to 100] * 1.0 -> 100] | (35 * 100) / 100 = 35 [Provisional]
왼쪽에서 오른쪽으로 읽습니다:
Moderate/35: 등급과 복합 점수.
Gate:35 [...]: 도달 가능성 축. 전송 기본값(ncacn_np:41, named pipe)에서 시작한 다음 각 등록 수정자와 부호가 있는 기여도를 나열합니다: MultiEndpointBonus:15(여러 전송에 등록됨), HasBouncer:-46(보안 콜백이 존재하므로 도달 가능성이 낮아짐), BouncerIsNotCaching:+25. 합산 후 [5,100] 범위로 클램프 -> 35.
Surface:100 [...]: 위험도 축. 각 신호는 Name:count x(opnums):direction:weight 형식입니다. 예: HasBogusStruct는 1개 파라미터(opnum 5)에서 발생했고 가중치는 61입니다. 그 다음 count 기반 기여도: InPtrs:6:18 = 호출자가 제어하는 in-포인터 6개가 +18 기여, Count:12:6 = 메서드 12개가 +6 추가. raw:134는 상한 적용 전 합계, [capped to 100], 는 신뢰도 배율(시그니처가 불확실하면 0.5로 떨어짐); 최종 Surface .
클램프와 낮은 신뢰도 할인을 보여 주는 두 번째 예제:
Low/3 | Gate:5 [Dynamic / epmapper:29, LocalCallOnly:-100, SecureOnly:-65, HasBouncer:-46, BouncerIsNotCaching:25] | Surface:50 [... -> raw:140 [capped to 100] * 0.5 -> 50] | (5 * 50) / 100 = 3
LocalCallOnly:-100 하나만으로 게이트가 0 아래로 내려가 최저값 5로 클램프됩니다; 시그니처가 불확실하여 Surface가 절반으로 줄고(* 0.5); 복합 점수는 3이 됩니다. 위험한 입력 공간이지만 사실상 도달 불가능 -> 올바르게 우선순위가 낮아집니다.
Critical >= 75, High >= 50, Moderate >= 25, 그 외는 Low (복구된 표면이 전혀 없는 인터페이스는 게이트와 무관하게 Low). 임계값은 복합 점수에 적용됩니다. 그 뒤의 모델은 docs/Surface_Scoring_Methadology.md에 있습니다.
세 가지 가중치 테이블(전송 기본값, 게이트 수정자, 표면 신호)은 모두 Analytic Hierarchy Process로 도출되었습니다; 쌍별 비교, 기하 평균 가중치, 측정된 일관성 비율(consistency ratio)을 사용합니다. 전체 유도 과정, 행렬, 일관성 수치 및 수기 작업 노트는 docs/Surface_Scoring_Methadology.md에 있습니다.
추출이 스스로 확인만 하는 것이 아니도록, 이 도구가 복구한 인터페이스는 동일한 바이너리에 대해 독립적이고 잘 정립된 RPC IDL 추출기(James Forshaw의 NtObjectManager에 있는 RpcServer 파서)와 교차 검증되었습니다. lsass, samsrv, winlogon에 대한 참조 덤프는 validation/에 있고 전체 워크스루는 validation/VALIDATION.md에 있습니다. 어느 것이든 output/FullBatchRun.json의 대응 바이너리와 비교해 보십시오: 인터페이스 UUID, 메서드/opnum 수, 파라미터별 방향이 일치합니다(예를 들어 winlogon의 12E65DD8-... 인터페이스는 둘 다 5개 메서드 Proc0-Proc4를 보여 줍니다). 참조 도구는 IDL 복구에서 멈춥니다; 이 도구는 동일하게 복구된 표면을 가져와 그 위에 도달 가능성 x 위험도 순위를 추가합니다. 해당 프로젝트와는 제휴가 없으며 순전히 독립적인 ground-truth 검증으로 사용됩니다.
정적 분석 전용. 아무것도 실행되지 않습니다. 도달 가능성은 런타임이 아닌 등록 정보에서 추론됩니다.
Needs-Review 점수는 잠정적(provisional) 메서드 수를 육안으로 확인하기 전까지입니다.
이 도구는 ALPC/RPC 및 Windows 내부 구조에 대한 진행 중인 연구의 일부입니다. 가까운 시일 내에 추가 연구 결과를 게시하고 동반 도구를 출시할 예정입니다. 유용하게 사용하셨다면 GitHub에서 저를 팔로우하거나 아래 소셜 미디어를 통해 향후 릴리스 알림을 받아 보시기 바랍니다.
소유한 시스템에 대한 취약점 연구용 정적 트라이지/매핑 도구입니다. 공격 표면(surface) 을 보고할 뿐 취약점을 보고하지 않습니다. 이 도구가 강조한 인터페이스에서 이후 발견하게 될 모든 내용은 공개 세부 정보가 공유되기 전에 조율된 공개(coordinated disclosure, MSRC)를 거쳐야 합니다.
MIT
| 플래그 | 의미 |
|---|
-t / --target | 스캔할 바이너리가 있는 폴더 |
-g / --ghidra | Ghidra 설치 폴더 (support/analyzeHeadless가 포함된 폴더) |
-s / --script | extract_rpc_interfaces.py 경로 |
-o / --output | 작성할 JSON 리포트 경로 |
--stagedir | 필터링된 RPC 바이너리가 복사되는 폴더 (유지됨) |
--projdir | 영구 Ghidra 프로젝트용 폴더 |
--projname | Ghidra 프로젝트 이름 (예: RPC_Atlas) |
| 필드 | 의미 |
|---|
CallSite | RpcServerRegisterIf\* 호출 주소 |
Tag / TagDesc | 데이터 품질 버킷 (아래 참조) |
Rank | \"{Tier}/{Composite}\" 형식, 예: Critical/91 |
RankDetail | 전체 점수 영수증 (아래 참조) |
UUID | 인터페이스 UUID |
InterfaceAddress / DispatchAddress | 복구된 구조체 주소 |
FunctionsCount | 권위 있는 저장 메서드 수; (FLAG: Walked X != Stored Y)가 붙을 수 있음 |
Endpoints | 전송 / 엔드포인트 바인딩 |
Security | HasBouncer, SecurityDescriptor, SecureOnly, LocalCallOnly |
Methods | 디코딩된 NDR opcode가 포함된 opnum별 파라미터 목록 |
* 1.0(35 * 100) / 100 = 35 [Provisional]: 복합 점수 = Gate x Surface / 100. 도달 가능성과 위험도는 평균이 아니라 곱합니다. 위험도는 도달할 수 있을 때만 의미 있기 때문입니다: 도달할 수 없는 최대 위험 인터페이스는 최상위로 올라가면 안 됩니다. 끝의 [Provisional] 플래그는 동적 메모리 탐색이 저장된 메서드 수와 약간 불일치하여 사람이 경계를 확인해야 한다는 경고입니다.