1. 브레이크포인트(BreakPoint)
브레이크포인트는 소프트웨어 개발 과정에서 프로그램의 실행을 일시적으로 중단시킬 수 있는 지점을 의미하며 중단점이라고 불린다. 디버깅 도중 특정 코드 줄이나 조건에서 실행을 멈추고 해당 시점에서의 변수 값, 메모리 상태, 레지스터 값 등을 분석할 수 있게 한다.
브레이크포인트는 구현 방법과 사용 목적에 따라 소프트웨어 브레이크포인트(Software BreakPoint), 하드웨어브레이크포인트(Hardware BreakPoint), 메모리 브레이크포인트(Memory BreakPoint)로 구분된다.
2. 소프트웨어 브레이크포인트(Software BreakPoint)
소프트웨어 브레이크포인트(Software BreakPoint)는 소프트웨어 개발 및 디버깅 과정에서 프로그램의 실행을 특정 지점에서 일시적으로 중단시키기 위해 사용되는 방법이다. 이는 프로그램을 디버깅할 때 가장 일반적으로 활용되는 디버깅 기술 중 하나이며, BreakPoint 횟수에 제한이 없다. 우리가 x64dbg나 Ollydbg를 사용할 때 F2를 눌러 사용하는 브레이크포인트 방식이 바로 소프트웨어 브레이크포인트 방식이다.
- 동작 방식
소프트웨어 브레이크포인트는 프로그램 코드 내 특정 지점(opcode)에 한 바이트 명령('INT 3' 명령어, 0xCC)을 삽입함으로써 작동한다. 이 명령은 CPU가 해당 지점에 도달했을 때, 프로그램의 실행을 중단시키고 디버거에 제어권을 넘긴다. 디버깅 세션이 종료되면, 원래의 opcode로 복원되어 프로그램이 정상적으로 실행된다.
'INT 3(Interrupt 3, 0xCC)' 명령은 프로그램 실행을 일시적으로 중지시키고 디버거에 제어를 넘기기 위해 사용되는 인터럽트 명령어이다.
그림 1의 0x7FF6CD93155D 위치의 opcode와 어셈블리 명령을 살펴보자.
어셈블리 명령어 'MOV EAX, EBX'의 opcode는 2바이트 '8BC3'이다. 해당 위치에 소프트웨어 브레이크포인트를 설정하면, opcode의 첫 번째 바이트 '8B'는 'INT 3' 명령어를 나타내는 'CC'로 교체된다. 프로그램을 다시 실행할 때, 해당 브레이크포인트에 도달하면 실행이 일시 중단되고, 디버거가 프로그램의 상태를 분석하기 위해 제어권을 인계받는다.
디버거가 제어권을 인계받게 되면, 프로그램의 현재 상태를 검사하고 조작할 수 있게 된다. 이 상태에는 변수의 값, 메모리의 상태, CPU 레지스터의 상태 등이 포함된다. 디버거는 이러한 정보를 사용하여 프로그램의 실행 흐름을 임시로 관리한다. 디버거가 제어권을 인계받게 된 이후, 개발자는 디버거를 통해 다음과 같은 작업을 수행할 수 있다.
① 프로그램을 단계별로 실행할 수 있다.
② 특정 지점으로 실행을 점프시키거나, 변수의 값을 변경하여 코드의 동작 방식에 영향을 줄 수 있다.
디버깅 세션이 종료되면, 디버거는 소프트웨어 브레이크포인트 설정 과정에서 변경된 opcode를 원래의 값으로 복원한다. 변경된 opcode를 원래대로 복원하는 이유는, 코드에 삽입된 'INT 3' 명령이 실제 프로그램의 기능과는 무관하기 때문이다. 디버깅이 완료된 후에 이러한 임시 변경 사항을 제거하여, 프로그램이 원래의 기능을 정상적으로 수행할 수 있도록 보장하기 위해 이러한 방식으로 설계되었다.
다음은 소프트웨어 브레이크포인트 설정 전과 설정 후의 상태이다.
BreakPoint를 설정하기 전의 opcode
0x7FF6CD93155D 8BC3 MOV EAX, EBX
BreakPoint를 설정한 이후의 opcode
0x7FF6CD93155D CCC3 MOV EAX, EBX
x64dbg나 Ollydbg와 같은 디버깅 도구에서 소프트웨어 브레이크포인트를 설정하면, 실제로는 opcode가 'CC'로 변경되지만, 디버거의 사용자 인터페이스(UI)에서는 이 변경을 직접 보여주지 않는다. (그림 1 확인!)
이는 디버깅 과정을 단순화하고 사용자가 원본 코드를 계속 볼 수 있도록 하기 위함이다. 내부적으로 브레이크포인트가 활성화되어 있지만, 사용자에게 혼란을 주지 않고 코드의 무결성을 유지하기 위해 이러한 방식으로 설계되었다.
이러한 소프트웨어 브레이크포인트는 사용 방식에 따라 조건부 브레이크포인트(Conditional Break Point), 일회성 브레이크포인트(One-shot Break Point), 지속적인 브레이크포인트(Persistent Break Point)로 나뉜다.
2.1 조건부 브레이크포인트(Conditional BreakPoint)
조건부 브레이크포인트는 특정 조건이 만족될 때만 실행을 중지시키는 브레이크포인트이다. 예를 들어, 변수의 값이 특정 숫자에 도달했을 때, 또는 특정 함수가 특정 횟수 이상 호출되었을 때 등의 조건을 지정하여 브레이크포인트를 설정할 수 있다.
x64dbg에서는 그림 2와 같이 조건부 브레이크포인트를 설정하려고 하는 위치에 마우스 우클릭 후, 중단점 -> "Set Conditional Breakpoint"를 클릭하여 설정할 수 있다.
2.2 일회성 브레이크포인트(One-shot BreakPoint)
일회성 브레이크포인트는 프로그램 실행 중에 딱 한 번만 활성화되며, 해당 이벤트 처리가 완료되면 디버거의 내부 브레이크포인트 리스트에서 자동으로 제거된다. 이는 디버거가 더 이상 해당 브레이크포인트를 사용하지 않음을 의미하며, 프로그램의 실행이 해당 지점에 다시 도달해도 더 이상 중지되지 않는다. 이러한 일회성 브레이크포인트는 특정 조건이나 함수가 호출되는 첫 번째 시점을 분석하고 싶을 때 유용하다.
x64dbg에서는 그림 2와 같이 마우스 우클릭 후, Set Conditional Breakpoint를 클릭한 뒤, 일회성을 체크하면 된다.
2.3 지속적인 브레이크포인트(Persistent BreakPoint)
지속적인 브레이크포인트는 프로그램 실행 중에 설정된 특정 지점에서 반복적으로 활성화되며, 개발자가 명시적으로 제거하거나 디버깅 세션이 종료될 때까지 유지된다. 이는 디버거의 내부 브레이크 포인트 리스트에 계속해서 등록되어 있으며, 프로그램의 실행이 해당 지점에 도달할 때마다 실행을 중지시키고 디버거에 제어권을 넘긴다.
지속적인 브레이크포인트는 x64dbg나 Ollydbg와 같은 디버깅 도구에서 F2 키를 사용하여 활성하는 일반적인 브레이크포인트 방식에 해당한다.
소프트웨어 브레이크포인트 설정할 때 사용되는 'INT 3, 0xCC' 명령은 실행 바이너리의 특정 바이트를 변경한다. 이 변경으로 인해 프로그램의 CRC(Cyclic Redundancy Check) 체크섬 값에 변화가 생긴다. 악성 코드는 이러한 CRC 체크섬의 변화를 자신의 코드가 변조되었다고 판단하고, 분석을 회피하기 위해 스스로를 종료시켜 분석을 어렵게 만든다. 이 문제에 대한 해결책으로 하드웨어 브레이크포인트가 있다. 하드웨어 브레이크포인트는 CPU의 내장 기능을 사용하여, 코드 자체를 변경하지 않고도 실행을 중단시킨다. 이 접근 방식은 CRC 체크섬 값에 영향을 주지 않으므로, 악성 코드의 분석이나 보안 소프트웨어의 디버깅에 효과적이다.
CRC 체크섬은 데이터의 무결성을 검증하기 위해 사용되는 기술로, 데이터의 변경 여부를 감지하는 데 활용된다. 파일, 메모리, 텍스트, 네트워크 패킷 등 다양한 데이터 유형에 적용될 수 있다. CRC는 특정 알고리즘에 따라 데이터로부터 해시 값(체크섬)을 생성하고, 이 값을 원본 데이터와 비교함으로써 데이터가 변경되었는지 여부를 판단한다. 체크섬 값이 일치하면 데이터가 변경되지 않았다고 간주하고, 값이 불일치하면 데이터가 손상되었거나 변경되었다고 판단한다.
3. 하드웨어 브레이크포인트(Hardware BreakPoint)
하드웨어 브레이크포인트는 디버깅 과정에서 프로그램 코드의 실제 변경 없이 실행을 중단시킬 수 있는 방법이다. 이는 CPU 내장 기능을 활용하여 구현되며, 디버그 레지스터를 통해 관리된다. CPU는 8개의 디버그 레지스터(DR0 ~ DR7)를 가지고 있으며, 이 중 DR0에서 DR3까지는 브레이크포인트 주소 저장에 사용된다. DR4와 DR5는 예약되어 있어 사용하지 않고, DR6는 디버깅 이벤트의 종류를 판단하기 위한 상태 레지스터로 사용된다. DR7은 하드웨어 브레이크포인트의 활성화/비활성화를 관리하며, 다른 브레이크포인트 조건들도 여기에 저장된다.
- 동작 방식
CPU는 명령을 실행하기 전에 디버그 레지스터를 확인하여 해당 주소에 하드웨어 브레이크포인트가 설정되어 있는지 검사한다. 이는 명령 실행 주소가 디버그 레지스터(DR0 ~ DR3)에 저장된 주소와 일치하는지를 검사함으로써 이루어진다. 설정된 브레이크포인트 주소에 접근하는 명령이 실행될 경우, CPU는 명령 실행을 중지시키고 'INT 1(디버그 인터럽트)' 이벤트를 발생시킨다. 이를 통해 디버거에 제어권이 넘어간다. 만약 실행 주소가 브레이크포인트로 설정되지 않은 주소라면, CPU는 해당 명령을 정상적으로 실행하고 다음 명령으로 이동한다.
디버그 레지스터에 대한 자세한 설명은 더보기를 클릭하세요.
디버그 레지스터는 CPU에 내장된 특별한 레지스터로, 프로그램의 실행을 특정 조건에서 중단시키거나 프로그램의 상태를 모니터링하는 데 사용된다. 주로 하드웨어 브레이크포인트 설정에 활용된다.
DR0 ~ DR3
해당 레지스터들은 브레이크포인트의 주소를 저장하는 데 사용된다. 최대 4개의 독립적인 브레이크포인트를 설정이 가능하다.
DR4 ~ DR5
DR4와 DR5는 예약된 레지스터로, 현재는 직접적으로 사용되지 않는다. 초기 x86 아키텍처에서 이들이 DR0 ~ DR3의 확장이나 DR6과 DR7의 별칭으로 일부 환경에서 사용될 수 있었지만, x64 아키텍처에서는 이러한 용도로 활용되지 않는다. x64 아키텍처에서 대부분의 디버깅 작업은 DR0 ~ DR3, DR6, DR7을 중심으로 이루어진다.
DR6(디버그 상태 레지스터)
디버그 이벤트가 발생했을 때의 상태를 저장한다. 어떤 브레이크포인트(DR0 ~ DR3)가 이벤트를 발생시켰는지, 발생한 이벤트 유형(ex: 읽기, 쓰기, 실행) 등의 정보를 포함한다.
DR7(디버그 제어 레지스터)
하드웨어 브레이크포인트의 동작을 제어한다. 이 레지스터를 통해 각 브레이크포인트의 활성화 여부, 반응 조건(읽기, 쓰기, 실행), 브레이크포인트의 크기(1byte ~ 16byte) 등을 설정할 수 있다.
x64dbg에서는 그림 3과 같이 브레이크포인트를 설정하고자 하는 부분(여기서는 hex 값 부분)에서 마우스 우클릭 후, 원하는 브레이크포인트 방식을 설정하면 된다.
그림 4와 같이 코드 영역에서 마우스 우클릭 후, 하드웨어 브레이크포인트를 설정할 수도 있다. 이 경우 실행 시 하드웨어 브레이크포인트 설정만 가능하다.
액세스 방식과 실행 방식으로 하드웨어 브레이크 포인트를 설정한 결과는 그림 5와 같다.
하드웨어 브레이크포인트 설정은 CPU의 디버그 레지스터(DR0 ~ DR3)에 의존한다. 이 레지스터들은 총 4개로 제한되어 있기 때문에, 한 번에 활성 가능한 하드웨어 브레이크포인트도 최대 4개이다.
4. 메모리 브레이크포인트(Memory BreakPoint)
메모리 브레이크포인트는 프로그램의 실행 중 특정 메모리 영역이나 페이지에 대한 접근을 모니터링하여, 해당 접근 시 디버거에 제어를 넘기는 기법이다. 이는 코드 라인에 설정하는 브레이크포인트와 다르게, 프로그램 코드 자체를 변경하지 않으며 메모리의 접근 권한 조정을 통해 작동한다.
메모리 페이지는 운영체제가 메모리를 관리하는 기본 단위로, 각 페이지에는 특정 접근 권한이 부여된다.
① Page Execution : 메모리 페이지가 실행 가능하나, 읽거나 쓰기 시 접근 위반 예외가 발생한다.
② Page Read : 페이지의 내용을 읽을 수 있으나, 쓰기나 실행 시 접근 위반 예외가 발생한다.
③ Page Write : 페이지에 데이터를 쓸 수 있으나, 읽거나 실행 시 접근 위반 예외가 발생한다.
④ Guard Page : 어떠한 접근이라도 발생하면 예외를 발생시키며, 예외 처리 후 원래 상태로 복귀한다.
- 동작 방식
디버거에서 분석하고자 하는 메모리 영역에 대해 메모리 브레이크포인트를 설정한다. 디버거는 설정된 메모리 영역의 접근 권한을 'Guard Page(보호 페이지)'로 변경하여, 모든 접근을 추적할 수 있게 한다. 프로그램 실행 후 해당 메모리 영역에 대한 접근이 감지되면, CPU는 'Guard Page' 예외를 발생시키고 디버거에 제어를 넘긴다. 디버거는 이 예외를 통해 해당 메모리 영역에 대한 접근을 분석하고, 프로그램의 메모리 사용 패턴을 이해할 수 있다.
이러한 메모리 브레이크포인트 방식은 네트워크 패킷 처리, 메모리 관리 버그 분석, 스택 오버플로우 탐지 등 다양한 상황에서 유용하게 활용된다. 하지만, 이 방식은 디버거가 프로세스의 메모리 접근을 지속적으로 모니터링해야 하므로, 디버깅 과정에서 성능 저하가 발생할 수 있다.
x64dbg에서 그림 6과 같이 메모리 브레이크포인트를 설정하고자 하는 부분(Hex 값 부분)에서 마우스 우클릭 후, 원하는 브레이크포인트 방식을 설정하면 된다.
참고자료 및 출처
[같은 블로그 사이트]
[추가]
- https://tribal1012.tistory.com/114
- https://to-paz.tistory.com/106
- https://chojinyoung.tistory.com/97
- https://g0pher.tistory.com/266
- https://chat.openai.com/
'3. Reversing & Cryptography' 카테고리의 다른 글
Unity를 이용한 CTF 게임 제작 및 풀이 과정 (0) | 2024.01.31 |
---|