Notice
Recent Posts
Recent Comments
Link
«   2026/07   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Tags more
Archives
Today
Total
관리 메뉴

방망이

[프로젝트]UEFI DXE 바이너리 취약점 분석기 15주차 - P-code Phase3 본문

2026_졸업프로젝트

[프로젝트]UEFI DXE 바이너리 취약점 분석기 15주차 - P-code Phase3

망구입니다 2026. 7. 2. 15:54

- 오늘의 주제 및 키워드

  > 데이터 흐름 기초

  > [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을 반환하는 경우도 있는데, 이는 더 이상 거슬러 올라갈 곳이 없다는 뜻으로 아래 두 가지 경우에 해당함.

  1. 함수 인자(HighParam) → 호출자가 넘겨준 값 → Taint Source!
  2. 전역 변수 → 외부 상태(현재 분석 중인 함수 바깥에서 관리되는 값. 함수 입장에서는 어떤 값이 들어있는지 알 수 없는 외부의 상태라는 뜻)  → 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 분석과 취약점 탐지 스크립트에 적용하면 더 좋은 결과를 낼 수 있을 것 같다.