2.1 개요
해커에게 있어 디버거는 매우 중요하다. 디버거를 이용해 실행 중인 프로세스를 트레이스 하거나 동적 분석을 수행할 수 있다. 동적 분석은 악성코드 조사나 fuzzer 적용에 있어 필수적인 요소다.
디버거는 소프트웨어의 취약점에 접근하고자 할 때 유용하게 사용할 수 있는 기능을 많이 제공한다. 프로세스를 실행시키거나 일시 정지시킬 수 있으며, 브레이크 포인트를 설정하거나 레지스터와 메모리의 값을 수정할 수 있다. 또한 한 대상 프로세스 내부에서 발생하는 예외를 잡아낼 수 있다.
화이트박스 디버거와 블랙박스 디버거에 대해 알아보자. 먼저 대부분의 개발 플랫폼이나 IDE는 개발자가 자신이 작성한 소스코드를 트레이스 할 수 있게 디버거를 자체적으로 내장하고 있다. 이런 종류의 디버거를 화이트박스 디버거라 부른다. 반대로 리버스 엔지니어링이나 숨겨진 버그를 찾아내는 것 같이 소스코드가 없는 환경에서 사용되는 디버거를 블랙박스 디버거라 부른다. 즉, 블랙박스 디버거는 해커가 조사 대상 소프트웨어에 대한 어셈블리 언어 코드만을 알 수 있을 때 사용되는 디버거다.
블랙박스 디버거에는 유저 모드 디버거와 커널 모드 디버거가 있다. 유저 모드 디버거는 사용자 애플리케이션이 동작하는 프로세서 모드다. 이는 최소한의 특권을 가지고 실행된다. 커널 모드는 가장 높은 수준의 특권을 가진다. 이는 드라이버와 기타 로우레벨 컴포넌트 같은 운영체제의 핵심 부분이 동작하는 모드다. 와이어샤크 프로그램을 이용해 패킷을 스니핑한다면 커널 모드에서 동작하는 드라이버를 이용하고 있는 것이다. 만약 그 드라이버의 특정 시점 상태를 확인하고자 한다면 커널 모드 디버거를 사용해야 한다.
2.2 범용 CPU 레지스터
레지스터는 CPU의 작은 저장 공간으로 CPU가 데이터에 접근하는 가장 빠른 방법을 제공한다. 주로 8개의 범용 레지스터를 사용하며 이를 살펴보도록 하겠다.
(1) EAX
산술 연산에 주로 사용되며 함수 리턴 값을 전달하기 위해 사용된다. 명령 셋에서 최적화된 많은 명령이 데이터 계산과 저장을 위해 EAX 레지스터를 사용하게 설계됐다. 즉, 더하기, 빼기, 비교 연산 같은 대부분의 기본적인 연산이 EAX 레지스터를 사용하게 최적화 됐다.
또한 함수의 리턴 값은 EAX 레지스터에 저장돼 전달된다. 따라서 EAX에 저장된 값을 조사하면 호출한 함수가 성공했는지, 실패했는지 여부를 쉽게 판단할 수 있으며, 함수가 반환한 리턴값이 무엇인지 알 수 있다.
(2) EDX
데이터 레지스터이며, 기본적으로 EAX의 확장 개념으로 사용된다. 즉, 곱하기나 나누기 같이 복잡한 연산을 위해 추가적으로 데이터를 저장할 때 사용된다. 대부분의 경우 EAX 레지스터와 함께 연동해서 수행되는 연산에 사용된다.
(3) ECX
카운트 레지스터라 불리며, 반복적으로 수행되는 연산에 주로 사용된다. 중요한 점은 ECX 레지스터는 값을 증가시키면서 카운트를 세는 것이 아니라 값을 감소시키면서 카운트를 센다는 점이다.
counter = 0
while counter < 10:
print(f"Loop number : {counter}")
counter += 1
위 코드를 수행하면 ECX 레지스터의 값이 첫 번째 반복 연산을 수행할 때는 10이고 수행하면서 1씩 감소한다. 즉, 어셈블리 언어에서의 카운트 값은 항상 감소하는 식으로 동작한다는 점을 기억하면 혼동되지 않는다.
(4) ESI
데이터 연산을 위한 Source Index를 나타내거나 입력 데이터 스트림의 위치를 나타내기 위해 사용된다. 읽기 위해 사용되는 레지스터로 이해하면 쉽다.
(5) EDI
데이터 연산의 Destination Index를 나타내거나 데이터 연산의 결과가 저장되는 위치를 나타내는 데 사용된다. 쓰기 위해 사용된다고 생각하면 쉽다.
(6) ESP
Stack Pointer의 의미로 스택의 가장 높은 위치를 가리킨다. 따라서 함수 호출 시 ESP 레지스터는 리턴 주소를 가리킨다.
(7) EBP
Base Pointer의 의미로 호출 스택의 가장 낮은 위치를 가리킨다. 스택 프레임의 포인터로 사용된다.
(8) EIP
현재 실행중인 명령의 주소를 가리킨다. CPU가 바이너리 코드를 실행시킴에 따라 EIP 레지스터는 CPU가 현재 어느 코드를 실행시키는 중인지 나타내기 위해 계속적으로 실행되는 코드의 주소를 갱신한다.
2.3 스택
스택은 FILO 구조이며, 함수를 호출할 때 해당 함수에 전달되는 파라미터를 스택에 PUSH하고, 함수가 리턴할 때는 POP한다. ESP 레지스터는 스택 프레임의 꼭대기를 가리키기 위해 사용되고, EBP 레지스터는 스택 프레임의 바닥을 가리키기 위해 사용된다. 스택은 상위 메모리 주소에서 하위 메모리 주소 방향으로 커진다.
C언어 에서의 함수 호출
int my_socks(color_one, color_two, color_three);
x86 어셈블리 언어에서의 함수 호출
push color_three
push color_two
push color_one
call my_socks
위 C언어의 스택 프레임을 그리면 아래와 같다.
my_socks() 함수가 리턴할 때는 스택의 모든 값을 POP한다. 그리고 리턴 주소로 점프해 명령 코드를 계속적으로 실행한다.
스택에서 고려해야 하는 또 한 가지로는 지역 변수가 있다. 지역 변수는 실행되고 있는 함수 내부에서만 유효한 작은 메모리 조각이다. my_socks() 함수 내부 문자 배열에 color_one 파라미터를 복사하게 해당 함수를 조금 확장해보자.
int my_socks(color_one, color_two, color_three) {
char stinky_sock_color_one[10];
...
}
2.4 디버그 이벤트
디버거는 디버그 이벤트가 발생할 때까지 계속적으로 루프를 돌면서 대기한다. 일반 디버그 이벤트가 발생하면 루프를 종료하고 해당 이벤트에 맞는 핸들러를 호출한다. 다음은 디버거가 반드시 처리해야하는 일반적인 이벤트들이다.
- 브레이크 포인트
- 메모리 충돌(메모리 접근 에러 또는 세그먼트 폴트)
- 디버깅 중인 프로그램에 의해서 발생한 예외
운영체제에 따라 디버거에 이벤트를 전달하는 방식은 모두 다르다. 일부 운영체제에서는 스레드나 프로세스가 생성되는 이벤트, 동적 라이브러리가 로딩되는 이벤트를 제공하기도 한다.
스크림트 가능한 디버거의 장점은 특정 디버그 이벤트에 대한 핸들러를 나름대로 변경할 수 있다는 점이다. 일반적인 디버그 수행 중에 버퍼 오버플로우로 인해 메모리 충돌 에러가 발생하면 디버거를 통해 직접 그 때 상태 정보를 추출해야 한다. 하지만 스크립트 가능한 디버거를 사용하면 그런 에러가 발생했을 때 자동으로 필요한 정보를 수집하게 하는 핸들러를 작성해 사용할 수 있다. 이렇게 필요한 기능의 핸들러를 직접 작성해 사용하면 시간을 절약할 수 있고 디버깅 대상 프로세스에 대한 제어를 폭넓게 수행할 수 있다.
2.5 브레이크포인트
실행중인 디버깅 대상 프로세스를 멈추게 하려면 브레이크포인트를 설정한다. BP에 의해 프로세스가 일시중지되면 중지된 시점의 변수나 스택 파라미터, 특정 메모리 위치의 값들을 조사해 볼 수 있다.
2.5.1 소프트 브레이크포인트
소프트 브레이크포인트는 명령을 실행하는 CPU를 일시 중지 시키는데 사용되며 애플리케이션을 디버깅할 때 가장 흔하게 사용되는 형태의 브레이크포인트이다. 소프트 브레이크포인트는 한 바이트 명령을 사용해 디버깅 대상 프로세스의 실행을 중지시킨다. 이를 이해하기 위해 어셈블리 언어에서의 명령과 opcode의 차이점을 알아야한다.
어셈블리 명령은 CPU를 실행시키기 위한 명령을 "하이레벨" 수준으로 표현한 것이다. 예를 들면 다은과 같다.
MOV EAX, EBX
이 명령은 CPU가 EBX 레지스터에 있는 값을 EAX 레지스터에 저장하라는 의미이다. 하지만 CPU는 사실 이 명령을 어떻게 해석해야 하는지 모른다. 따라서 어셈블리 명령은 opcode 형태로 변환되어야 한다. opcode가 바로 CPU를 실행하는 기계어 명령이다. 위의 어셈블리 명령을 opcode로 변환하면 다음과 같다.
8BC3
이런 형태로 CPU와의 대화가 이루어진다.(DNS의 원리와 비슷하다.) 이걸 직접 사용해야 하는 경우는 거의 없지만 소프트 브레이크포인트를 이해하려면 중요하다. 예를들어 0x44332211 주소의 어셈블리 명령이 다음과 같다고 가정해보자.
0x44332211: 8BC3 MOV EAX, EBX
이는 주소와 opcode, 어셈블리 명령을 보여준다. 이 주소 위치에 소프트 브레이크포인트를 설정해 CPU를 일시중지 시키려면 2바이트 opcode인 8BC3 중 1바이트를 교체해야 한다. CPU를 일시정지 시키기위해 새롭게 교체되는 1바이트는 인터럽트 3(INT 3) 명령의 opcode이다. opcde 0xCC로 교체되는 것이다. 다음은 소프트 브레이크포인트 설정 전과 후의 상태를 보여준다.
브레이크 포인트를 설정하기 전의 opcode
0x44332211: 8BC3 MOV EAX, EBX
브레이크 포인트를 설정한 이후의 opcode
0x44332211: CCC3 MOV EAX, EBX
CPU가 이 바이트를 만나게 되면 INT 3 이벤트를 발생시킨다.
- 디버거는 특정 주소에 브레이크 포인터를 설정하라는 명령을 받으면 해당 주소의 첫 번째 opcode바이트를 읽어 그걸 저장하고 그 위치에 CC 바이트를 써 넣는다.
- CC opcode로 인해 CPU가 브레이크포인트나 INT 3 이벤트를 발생시키면 디버거는 그 이벤트를 전달 받는다.
- 그러면 디버거는 EIP가 자신이 이전에 설정한 브레이크포인트 주소를 가리키고 있는지 확인한다. EIP 레지스터가 기리키는 주소가 디버거 내부의 브레이크포인트 리시트에 존재하면 디버거는 실행이 다시 재개될 때 올바로 실행되게 하기 위해 이전에 저장해두었던 원래의 opcode 바이트를 해당 주소 위치에 써 넣는다.
소프트 브레이크포인트 종류는 2가지가 있다. 하나는 일회성 브레이크포인트이고 다른 하나는 지속적인 브레이크포이트 이다. 일회성은 한 번만 브레이크포인트 이벤트가 발생하고 리스트에서 정보가 제거된다. 지속적인 브레이크포인트는 이벤트 발생하고 CPU가 원래의 opcode를 실행한 다음에 다시 브레이크포인트 설정을 수행한다.
소프트 브레이크포인트는 한 가지 단점이 있다. 그것은 메모리상의 실행 바이너리의 바이트를 변경하기 때문에 CRC 체크섬 값이 변경된다는 것이다. CRC는 무결성을 확인하기 위한 기법으로 특정 범위의 데이터 영역으로부터 그것의 해시 값을 산출해낸다. 이를 비교하여 무결성을 검증한다. 이러한 단점을 극복하려면 하드웨어 브레이크포인트를 사용해야 한다.
2.5.2 하드웨어 브레이크포인트
하드웨어 브레이크포인트는 설정할 브레이크포인트의 개수가 적을 때나 디버깅할 소프트웨어의 코드가 변경되면 안될 때 유용하게 사용할 수 있다. 이런 형태의 브레이크포인트는 CPU 레벨에서 브레이크 포인트를 설정하는 것이다. 디버그 레지스터라고 불리는 특별한 레지스터를 이용한다. CPU에는 일반적으로 디버그 레지스터가 8개(DR0 ~ DR7)가 있다.
- DR0 ~ DR3
- 브레이크포인트의 주소를 저장하기 위해 사용된다. 이는 한 번에 최대 4개까지의 하드웨어 브레이크포인트를 설정할 수 있다는 의미이다. - DR4와 DR5
- 예약된 레지스터 - DR6
- 브레이크포인트에 의해 발생되는 디버깅 이벤트의 종류를 판단하기 위해 사용되는 상태 레지스터이다. - DR7
- 하드웨어 브레이크포인트의 on/off 스위치로 사용되며, 서로 다른 브레이크포인트의 조건도 저장한다.
- DR7의 특정 플래그 값을 설정하면 다음과 같은 조건의 브레이크포인트를 만들어 낼 수 있다.- 지정된 주소의 명령이 실행될 때
- 데이터가 어느 주소에 써질 때
- 어느 주소에 대한 읽기 또는 쓰기 작업이 수행될 때
그림 2-4는 DR7 레지스터의 각 필드가 하드웨어 브레이크포인트의 종류, 길이, 주소와 어떻게 연관되는지 보여준다.
브레이크포인트 플래그 | 브레이크포인트 길이 플래그 |
00 - Break on execution | 00 - 1바이트 |
01 - Break on data writes | 01 - 2바이트(워드) |
11 - Break on reads or writes but not execution | 11 - 4바이트(더블 워드) |
각 비트에 대해서 하나씩 알아보겠다.
- 0 ~ 7비트
- 설정된 브레이크포인트에 대한 on/off 스위치 역할을 수행한다.
- L과 G필드는 번위를 나타내며 각각 지역(Local)과 전역(Global)을 의미한다. - 8 ~ 15비트
- 일반적인 디버깅 목적으로 사용되지 않는다. - 16 ~ 31비트
- 디버그 레지스터에 설정된 브레이크포인트 종류와 길이를 나타내는데 사용된다.
하드웨어 브레이크포인트는 INT 1을 사용한다. INT 1은 하드웨어 브레이크포인트를 위한 이벤트이며 단일 스텝 이벤트이다. 단일 스텝은 각 명령을 하나씩 수행할 수 있다는 의미이다.
하드웨어 브레이크포인트는 소프트 브레이크포인트와 동일한 방법으로 처리되지만 '로우레벨'에서 수행된다. CPU는 명령을 실행하기 전에 해당 주소가 하드웨어 브레이크포인트로 설정되어 있는지 확인한다. 또한 수행할 명령이 하드웨어 브레이크포인트가 설정된 주소에 접근하는지 여부를 확인한다. 해당 주소가 DR0 ~ DR3 레지스터에 저장되어 있고 읽기나 쓰기 실행 조건이 설정되어 있다면 CPU는 명령에 대한 실행을 중지하고 INT 1 이벤트를 발생시킨다.
하드웨어 브레이크포인트는 몇 가지 제약이 있다.
- 최대 4개의 개별적인 브레이크포인트를 설정할 수 있다.
- 브레이크포인트를 설정할 수 있는 데이터의 최대 크기는 4바이트이다.
위의 메모리 크기에 대한 한계를 극복하려면 메모리 브레이크포인트를 사용해야 한다.
2.5.3 메모리 브레이크포인트
메모리 브레이크포인트는 사실 브레이크포인트가 아니다. 디버거가 메모리 브레이크포인트를 설정하면 해당 메모리 영역이나 페이지에 대한 접근 권한이 변경된다. 메모리 페이지에 접근 권한은 다음과 같다.
- 페이지 실행
- 페이지를 실행시킬 수 있지만 데이터를 읽거나 쓰려고 하면 접근 위반 예외가 발생한다. - 페이지 읽기
- 메모리의 내용을 읽을 수 있지만 데이터를 쓰거나 실행시키려고 하면 접근 위반 예외가 발생한다. - 페이지 쓰기
- 해당 메모리 페이지에 데이터를 쓰는것만 허용한다. - 보호 페이지
- 어떤 종류의 접근이라도 발생하면 예외를 발생시킨다.
이 중 관심사는 보호 페이지 권한이다. 이 권한은 스택에서 힙을 분리해내거나 특정 메모리 영역이 어떤 범위 이상으로 커지는지 확인하는 데 유용하다. 또한 특정 메모리 영역에 대한 접근이 발생할 때 프로세스를 중단시키고자 할 때 매우 유용하게 사용된다.
예를들어 네트워크 서버 애플리케이션을 리버싱 할 때 애플리케이션에 전달된 패킷의 페이로드가 저장되는 메모리 영역에 메모리 브레이크포인트를 설정할 수 있다. 이렇게 하면 브레이크포인트를 설정한 메모리에 대한 접근이 발생했을 때 CPU가 보호 페이지 지버그 예외를 발생시키기 때문에 애플리케이션이 전달된 패킷 내용을 언제, 어떻게 사용하는지 판단할 수 있게 된다. 그리고 해당 메모리 페이지에 접근하는 접근하는 명령을 조사해 애플리케이션이 패킷의 내용으로 어떤 작업을 수행하는 것인지 알아낼 수 있다.
메모리 브레이크포인트는 실행되는 어떤 코드도 변경하지 않기 때문에 소프트 브레이크포인트가 갖고 있는 코드 변경으로 인한 제약을 극복할 수 있다.
'게임해킹 > 파이썬 해킹 프로그래밍' 카테고리의 다른 글
3장 윈도우 디버거 개발(1) - 디버기 (0) | 2025.01.31 |
---|---|
1장 개발 환경 구축 (1) | 2024.12.27 |