방망이
[프로젝트]UEFI DXE 바이너리 취약점 분석기 15주차 - P-code Phase3 본문
- 오늘의 주제 및 키워드
> 데이터 흐름 기초
> [Reaching Def, Def-Use, Live Variable]
데이터 흐름 (Data Flow Analysis)
P-code로 취약점을 분석할 때 핵심 포인트는 "외부에서 들어온 값이 위험한 함수까지 도달할 수 있는가?"에 대한 것.
이 질문에 대한 답을 하기 위해서는 코드를 실행하지 않고도 값이 어디서 왔고, 어디로 흘러가는지 파악할 수 있어야 함.
1. Def-Use Chain
변수의 정의(Def)와 사용(Use)을 연결한 체인
- Def - 변수에 값이 처음 할당되는 지점(의미 있는 값이 처음 들어오는 시점)
- Use - 그 값이 입력으로 읽히는 지점
아래 코드를 예시로 들어보자.
void func(int sock) {
char buf[64];
recv(sock, buf, 64, 0); // 외부에서 데이터 수신 (Def 지점)
strcpy(dst, buf); // buf를 그대로 복사 (Use 지점)
}
- recv() → buf 정의(Def)
- recv()가 외부 네트워크에서 데이터를 읽어서 buf에 저장. 이 순간 buf라는 변수에 값이 처음 할당됨. 이 부분이 Def(정의) 지점.
- → strcpy에 전달(Use)
- buf에 담긴 값이 strcpy의 두 번째 인자로 전달. 이 순간 buf의 값이 읽힘. 이 부분이 Use(사용) 지점.
- → 취약점!
- strcpy는 길이 검사를 하지 않음. buf에 외부에서 온 데이터가 담겨 있는데, 그 길이를 확인하지 않고 그대로 복사하면 버퍼 오버플로가 발생할 수 있음.
Ghidra에서는 API로 접근 가능
varnodeAST.getDef() // 이 Varnode의 값이 어떤 op에 의해 만들어졌는가 (역방향)
varnodeAST.getDescendants() // 이 Varnode의 값이 어떤 op들에서 사용되는가 (순방향)
SSA에서는 변수가 딱 한 번만 정의되므로 getDef()가 항상 하나의 op만 반환.
비SSA 코드라면 같은 변수가 여러 곳에서 정의될 수 있어서 추적이 복잡하지만, High P-code는 SSA form이라 체인 추적이 단순하고 명확함.
getDef()
위에서 예시로 든 C 코드를 기반으로 봤을 때
buf.getDef()
호출시 CALL recv() op 반환 → buf의 값은 recv()가 만들었다는 의미
getDef()가 null을 반환하는 경우도 있는데, 이는 더 이상 거슬러 올라갈 곳이 없다는 뜻으로 아래 두 가지 경우에 해당함.
- 함수 인자(HighParam) → 호출자가 넘겨준 값 → Taint Source!
- 전역 변수 → 외부 상태(현재 분석 중인 함수 바깥에서 관리되는 값. 함수 입장에서는 어떤 값이 들어있는지 알 수 없는 외부의 상태라는 뜻) → Taint Source 가능
둘 다 이 함수 안에서 정의된 것이 아니므로 더 이상 역추적할 수 없음. 그래서 탐색을 멈추고 Taint source로 판정하는 조건이 됨.
따라서 이는 역방향 슬라이싱의 탐색 종료 조건이 됨.
getDescendants
이는 하나의 값이 여러 곳에서 쓰일 수 있으므로 Iterator로 반환됨.
VarnodeAST buf = ...; // buf Varnode
Iterator<PcodeOp> uses = buf.getDescendants();
while (uses.hasNext()) {
PcodeOp useOp = uses.next();
println(useOp.getMnemonic()); // buf를 쓰는 op들 출력
}
getDef와 getDescendants를 함께 쓰는 이유는 아래와 같은 로직을 사용하여 외부 입력으로 인한 위험 함수 경로를 파악.
- getDef() → "이 값이 외부 입력에서 왔는가?" 판단 → Taint source 확인
- getDescendants() → "이 tainted 값이 어디까지 퍼지는가?" 추적 → sink 도달 여부 확인
2. Reaching Definitions
x = input; // 정의 d1
if (cond) {
x = 0; // 정의 d2
}
use(x); // 여기서 도달 가능한 정의는?
// → d1과 d2 둘 다 가능
특정 지점에서 "어떤 정의가 여기까지 도달 가능한가" 를 분석. 변수의 출처를 판단하는 데 사용.
High P-code는 SSA(Static Single Assignment) 형태라서 Reaching Definition 개념을 곧바로 적용하기 애매함.
SSA에서는 변수가 딱 한 번만 정의됨. 같은 변수를 여러 번 대입하는 코드가 있어도, SSA로 변환되는 과정에서 x#1, x#2, x#3처럼 매번 새로운 이름이 붙음. 그래서 getDef()를 호출하면 언제나 정확히 하나의 op만 반환되고, "여러 정의가 도달 가능하다"는 상황 자체가 애초에 생기지 않게 됨.
분기가 있으면 합류점에서 여러 정의가 동시에 도달할 수 있는데, High P-code에서는 이 상황을 MULTIEQUAL(φ노드) 로 표현.
이건 사실 "이 지점에 x#1과 x#2라는 정의가 동시에 도달 가능하다"는 Reaching Definitions 분석의 결과가 이미 코드 구조에 반영되어 있는 것. 즉, Ghidra가 Raw P-code를 High P-code(SSA)로 변환하는 과정 자체가, 내부적으로 Reaching Definitions와 본질적으로 같은 계산을 수행한 결과물이라는 뜻.
x#3 = MULTIEQUAL(x#1_input, x#2_zero)
그래서 SSA 위에서 GEN/KILL/IN/OUT 알고리즘을 직접 돌리는 건 의미가 없음. — 이미 계산되어 있는 답을 다시 계산하는 셈이기 때문. MULTIEQUAL의 입력 목록을 확인하는 것만으로 "이 지점에 어떤 정의가 도달하는가"에 대한 답을 바로 얻을 수 있음.
컴파일러개론 수업에서 배운 GEN/KILL/IN/OUT 고정점 반복 알고리즘을 직접 구현하고 확인해보려면, 정의가 여러 번 일어날 수 있는 비SSA 코드, 즉 Raw P-code로 내려가야 함. Raw P-code는 명령어 단위 그대로의 저수준 표현이라 같은 레지스터/스택 위치에 값이 여러 번 쓰일 수 있기 때문.
분석 알고리즘은 각 블록의 IN/OUT 집합을 계산하며 변화가 없을 때까지 반복. (고정점 반복)
OUT[B] = GEN[B] ∪ (IN[B] - KILL[B])
IN[B] = ∪ OUT[pred] for all predecessors
3. Live Variable Analysis
특정 지점에서 "이후에 사용될 변수가 무엇인가" 를 분석. Reaching Definitions가 과거→현재 방향의 순방향 분석이라면, Live Variable은 미래→현재 방향의 역방향 분석.
x = input; // x: live (아래에서 쓰임)
y = x + 1; // x: 이후 dead, y: live
z = 10; // z: dead (이후 z를 읽는 곳 없음)
use(y); // y: 이후 dead
z는 이후에 전혀 사용되지 않으므로 Dead Variable. Taint 분석에서 z를 추적하는 것은 불필요.
Live Variable Analysis는 취약점 분석에서 세 가지 역할을 함.
- 불필요한 Taint 추적 제거 — Dead Variable은 이후 취약점을 유발할 수 없으므로 분석 대상에서 제외해 속도를 높임.
- False Positive 감소 — tainted 변수가 sink 이전에 Dead가 된다면 실제로는 위험하지 않음. 이를 걸러내면 잘못된 정보를 줄일 수 있음.
- Sanitizer 탐지 — tainted 변수가 검증 함수를 거친 뒤 새 변수에 할당되면 이전 tainted 버전이 Dead가 됨. 이 패턴으로 입력 검증 코드를 자동 인식할 수 있음.
char* sanitize(char* input) {
// 길이 검사, 특수문자 제거 등
return cleaned_input;
}
void func(char* input) { // input: tainted
char* safe = sanitize(input); // 검증 후 새 변수에 할당
strcpy(dst, safe); // safe를 사용
}
sanitize함수는 사용자 입력을 검증하거나 정제하는 함수.
sanitizer를 거치는 경우 검증 후 새 변수에 할당되므로 sanitizer 호출 이후 이 변수는 Dead variable이 됨.
tainted 변수(input#1)가
→ 어떤 함수(sanitize)를 거쳐
→ 새 변수(safe#1)에 할당되고
→ 기존 tainted 변수(input#1)가 Dead가 됨
→ 이 함수가 Sanitizer일 가능성이 높다!
즉 "tainted 변수가 Dead가 되는 지점에 함수 호출이 있다면" 그 함수를 Sanitizer 후보로 자동 인식할 수 있음.
이런 로직으로 분석기가 Sanitizer를 탐지할 수 있게 됨.
이런식으로 Live Variable Anaysis로 Sanitizer를 탐지하게 되면, 그 이후의 변수를 tainted 목록에서 제거할 수 있으므로 tainted 변수 추적에 도움을 줄 수 있음.
정리
| 기법 | 기 | 방향 | 역할 |
| Def-Use 체인 | 값이 어디서 왔고 어디로 가는가 | 양방향 | 흐름 추적 |
| Reaching Definitions | 어떤 정의가 여기까지 도달 가능한가 | 순방향 | 출처 판단 |
| Live Variable | 이 변수가 이후에 사용되는가 | 역방향 | 분석 범위 축소 |
위 세가지 기법은 독립적으로 동작하는 것이 아니라 Def-Use로 흐름을 추적하고, Reaching Definitions로 출처를 판단하고, Live Variable로 분석 범위를 좁히는 방식으로 함께 동작.
실습
=> DVRF(Damn Vulnerable Router Firmware)를 다운 받아서 진행
https://github.com/praetorian-inc/DVRF
/DVRF/Firmware/_DVRF_v03.bin.extracted/squashfs-root/pwnable/Intro/stack_bof_01 파일 사용
1. Def-Use 체인

Cluade에서 Def-Use 체인을 실습할 수 있는 스크립트를 생성해서 돌려본 결과

- Taint Source: param_2(=argv)가 main의 함수 인자로서 이 함수 안에서 정의되지 않음 → getDef() = null → Taint Source 판정
- 즉, "함수 인자로 들어온 외부 입력이 검증 없이 strcpy까지 도달"하는 것을 찾아냄.
2. Reaching Definitions
컴파일러개론 수업에서 배웠던 GEN/KILL, IN/OUT을 스크립트로 구해보았다.

수업 때 배웠던 내용을 직접 스크립트로 구해보고 볼 수 있는 것이 굉장히 신기하고 유익하다..
3. Live Variable Analysis

이 스크립트에서는 Dead Variable을 찾는다. 해당 블록에서 벗어나는 순간 Dead 상태가 되는 변수를 찾아내는 스크립트이다.
정리
실습은 간단하게 각 개념을 적용하는 함수를 사용해보는 수준의 실습이지만, 위에서 정리한 개념과 실습 내용을 바탕으로 Taint 분석과 취약점 탐지 스크립트에 적용하면 더 좋은 결과를 낼 수 있을 것 같다.
'2026_졸업프로젝트' 카테고리의 다른 글
| [프로젝트]UEFI DXE 바이너리 취약점 분석기 16주차 - 스크립트 업데이트를 위하여.. (0) | 2026.07.23 |
|---|---|
| [프로젝트]UEFI DXE 바이너리 취약점 분석기 14주차 - P-code Phase2 (0) | 2026.05.31 |
| [프로젝트]UEFI DXE 바이너리 취약점 분석기 13주차 - P-code Phase1 (0) | 2026.05.12 |
| [프로젝트]UEFI DXE 바이너리 취약점 분석기 12주차 - P-code 이론 (0) | 2026.05.04 |
| [프로젝트]UEFI DXE 바이너리 취약점 분석기 11주차 - Ghidra Script 수정 및 검증 (0) | 2026.04.02 |
