리버싱 종강. 디버깅 실전 4.(Debug Blocker)

이립·2025년 4월 1일

리버싱

목록 보기
32/32

종강종강

Debug Blocker

자기 자신 또는 다른 실행파일을 디버그 모드로 실행하는 기법.

  • CreateProcess() API에서 DEBUG_PROCESSDEBUG_ONLY_THIS_PROCESS 옵션을 사용해 부모 프로세스를 디버거, 자식 프로세스를 디버기로 설정.
  • 디버거-디버기 관계가 안티 디버깅 환경을 만듬.

짚고 가자. 안티디버깅

  1. 단일 디버거 제한: Windows는 한 프로세스에 하나의 디버거만 허용. 자식 프로세스를 다른 디버거로 Attach 불가.
  2. 종속 종료: 디버거 프로세스를 종료하면 디버기 프로세스도 함께 종료됨.
  3. 디버거가 실행 흐름을 제어: 디버기 프로세스는 디버거가 코드 분기, 복호화, 예외처리를 하지 않으면 정상 실행 불가.
  4. 디버기 프로세스의 예외를 디버거가 처리: 예외(예: Illegal Instruction)가 발생하면 디버거가 이를 받아 처리.

리얼월드에선 어떤 느낌일까?

해커가 디버거 블로킹을 적용한 악성 프로세스를 제작했다고 해보자.
이 프로세스를 실행하면 여기선 자식 프로세스가 디버기가 되고 부모가 디버거가 되는 디버거-디버기 관계이다.

리버싱 보안전문가가 이 프로세스를 분석하려고 한다. 하지만 단일 디버깅 정책으로 인해 이 프로세스를 디버깅하기 어렵게 된다. (물론 뭐 방법이 있겠지)

핵심원리

① 자기 자신을 디버그 모드로 실행 (부모-자식 프로세스 관계 설정)
악성코드가 자기 자신을 DEBUG_PROCESS 옵션으로 실행

부모 프로세스가 디버거, 자식 프로세스가 디버기 역할

② 자식 프로세스(디버기)가 예외를 발생시킴
예: LEA EAX, EAX 같은 Illegal Instruction 실행

이 예외는 부모 프로세스로 전달됨 (WaitForDebugEvent()로 대기 중)

③ 부모 프로세스(디버거)가 이 예외를 처리함
예외가 발생한 주소 확인 → 사전에 정해둔 예외 처리 루틴으로 진입

  1. ReadProcessMemory()로 암호화된 코드를 읽음.
  2. XOR 등으로 예외 위치에 있는 암호화된 코드 복호화함.
  3. WriteProcessMemory()로 디버기(자식) 메모리에 정상 코드 덮어씀.
  4. GetThreadContext() / SetThreadContext()로 EIP 조작해 악성 코드 실행 위치로 점프해서 실행 흐름 이어감. (실질적인 악성행위 발생 지점이 여기야)

실질적인 악성 행위가 뭐겠어.
파일 드롭, C2 통신, 키로깅 등등이다.

디버거에서 실행이 아니라 한줄씩 트레이싱 하면 안되는거임?

ㅇㅇ 안됨. 결국 트레이싱도 한줄씩 실행하는 건데 적당히까진 되다가, 자식프로세스를 생성하는 CreateProcess에서 막혀.

CreateProcess(
  ..., 
  DEBUG_PROCESS | DEBUG_ONLY_THIS_PROCESS, 
  ...
)

이 순간 자식 프로세스는 이미 "부모가 디버깅하는 상태"로 실행되는데 문제는!!
이미 OllyDbg가 부모를 디버깅 중인데, 이 부모가 또 다른 프로세스를 디버깅하려 하면
Windows의 "단일 디버거 정책"에 위배되어 충돌이 나거나 자식이 제대로 실행되지 않는다는 거지. 그래서 안된다~~

실습

DebugMe4.exe가지고 해보셔요

1. main (1차 시도)

핵심은 뮤텍스(활용)이다.

뮤텍스. mutex

Mutual Exclusion Object이다.
어렵게 말하면, 멀티스레드 환경에서 동시에 접근하지 못하게 막는 동기화 객체
쉽게 말하면, "지금 이 자원은 내가 쓰고 있어! 다른 애들 오지 마!" 라고 알려주는 잠금장치. 쉬운게 낫지?

뮤텍스 왜 쓰는데?

핵심은 지금 실행하는 저 프로세스가 스스로에게 묻는거야.
나는 부모로서 존재하는가? 자식으로서 존재하는가?

1. 프로그램이 실행되면

"ReverseCore:DebugMe4"라는 이름의 Mutex 객체를 생성하려고 시도

2. If, Mutex 생성 성공!

아 나는 처음 실행된 최초의 존재구나 -> 나는 부모로서 행동하겠다. 부모의 행동 분기 -> 401037 주소로 가고

3. If, Mutex 생성 실패!

아 이미 나와 같은 것이 있구나 -> 나는 자식으로 행동하겠다. 자식의 행동 분기 -> 40103F 주소

2. 자식 프로세스 분기 디버깅 실습 (2차 시도)

뮤텍스의 결과에 따라 점프가 달라지는 구간이 401035이다.
여기에 브레이크 포인트 걸어봐.

JE는 zero flag의 결과에 따라 가는 거거든?
equal이면 40103F(자식 분기)로 점프해라.

equal로 만들어주려면 올리디버거에서 레지스트창의 ZF를 1(True)로 편집하면 된다.

그리로 가면 자식 분기으 첫 코드가 나온다.

걍 레전드 에러 코드 모음집 되실게여.
ILLEGAL INSTRUCTION

LEA EAX, EAX가 뭔데

문법부터 틀려먹은 놈이다.
원래 이 명령어는
LEA | "레지스터" | "메모리" 이런 순서의 문법이다.
근데 LEA EAX, EAX는
LEA | "레지스터" | "레지스터" 라는 거라 문법 자체가 틀려서 레전드 오류

이렇게 예외가 발생하면 실행의 제어권이 부모(디버거)에게 넘어간다. 그럼 부모는 이 예외를 처리하고 바로 다른 작업(보통은 뭐 악성행위가 숨어있는)을 수행한다.

여기까지는 좀 쉬운 디버깅. 왜냐면 우리는 자식 프로세스로 진입해서 자식이 어떤걸 하는지 명확히 알 수 없어. (코드가 암호화됨) 이제는 빡센걸 해야해. 부모로서의 코드를 좀 상세히 들여보는 것

3. 부모 프로세스 디버깅(3차 시도)

여기서는 부모가 예외를 어떻게 처리하는지만 익혀도 다 이해한거다.

Step 1. CreateProcess 찾기

내려가다 가면 보여.

이 API를 호출해서 자기 자신을 Debug Mode로 실행한다.

Step 2. Debug 이벤트 발생 대기


여기선 디버기(자식)에서 Debug 이벤트가 발생하는 걸 기다린다.

WaitForDebugEvent( ) API

WaitForDebugEvent( ) 를 구조체 뜯어보면

정해진 시간(dwMilliseconds)에서 자식(디버기)의 이벤트 기다림

예외가 발생하면 WaitForDebugEvent( ) API가 리턴을 하겠지?
어디로 하냐면

DEBUG_EVENT라는 객체에 함

DEBUG_EVENT


결국 이 객체도 보면 이벤트의 타입에 따라 정해진 숫자를 u(union)이란 멤버에 저장한다.

Step 3. Debug 이벤트 확인


WaitForDebugEvent( ) API 호출 전에는 DebugEvent 타입의 값이 비어있다.

WaitForDebugEvent( ) API 호출하고 나면? (stepover로 실행)

3이라는 값이 차있다. 3은 뭐였지? CREATE_PROCESS_DEBUG_EVENT로서 디버기가 최초로 실행될 때 생기는 이벤트다. 개노잼

Step 4. 루프 등장


파란 부분: 이벤트 타입을 EAX에 저장한다. 맨처음엔 3이었으니까 3
노란 부분: 그게 1이니?
보라색: 1 아니면 4012C0으로 가


주황색: 어서와, 너 근데 5니?
검정색: 5 아니면 내려가라
핑크색 : 다시 한번 디버기에서 이벤트 생길때까지 숨 참음
남색: 발생했어? 그럼 아까 그 파란색 부분으로 돌아가! (루프)

Step 5. 루프 탈출

이 루프는 언제까지 돌까. 바로 파란색, 노란색, 보라색을 부분을 통과할 때지. 즉 eax에 1이 들어갈 때다. 1이 대체 뭐냐고? EXCEPTION_DEBUG_EVENT(예외 발생)의 u 멤버값이거든.

탈출시키는 방법은 강제로 4011FD에 브레이크포인트 걸고 실행해도 되고, 아니면 여기에 conditional log break point(CLBP)를 해도 된다.

4. CLBP 상세(4차 시도)

여긴 그냥 dwDebugEventCode 값을 실시간으로 로그 출력하고 어떤 예외들이 발생하는지 전체 흐름을 추적하는 것이다.

CLBP가 뭔지만 알려줄게.

일반 BPCLBP
프로그램을 멈춤프로그램을 안 멈추고 로그만 출력 가능
조건 못 줌“조건이 맞을 때만 로그 출력해줘!” 같은 조건 설정 가능
눈에 안 띄는 변화조건 충족 시 로그창에 표시됨 (예외가 언제 어디서 생겼는지 쉽게 추적 가능!)

루프의 처음 파란색 부분으로 가봐. 4011F0

Condition: DWORD PTR DS:[12F9D8] == 1
Explanation: EXCEPTION_DEBUG_EVENT 발생!
Log value: Always
Pause: Never

ollydbg에서 shift+F4해서 이렇게 설정해주렴. 그리고 나서 실행하면 프로그램은 멈추지 않은 채 로그만 나오지.

004011F0 COND: WFDE() == 00000001 ← 예외 발생!
004011F0 COND: WFDE() == 00000006 ← DLL 로딩 이벤트

이런 조건부 브레이크포인트는 어떤 점이 좋은거냐구?
어떤 예외가 언제 발생했는지 모르겠음 ->> CLBP 로그로 실시간 확인 가능
어디서 멈춰야 분석할 수 있을지 모르겠음 ->> 정확한 타이밍에만 멈추게 설정 가능
프로그램을 멈추지 않고 흐름만 보고 싶음 ->> Pause: Never 설정으로 멈추지 않음

5. 루프 탈출 후 예외 분석 (5차 시도)

CLBP 설정을 조금 바꿔서, 예외 1이 나오면 프로그램이 pause하게 하자. 그리고 좀 코드를 살펴보자.


이거 잘 이해해야 하는게 파란색 부분에 이벤트가 1이 되어서 루프를 통과한 건 이해가 되는데 왜 갑자기 주황색이랑 보라색을 신경쓰는가?

파란색 이벤트와 보라색 예외는 뜻이 다르다. 기본적으로 이벤트 vs 예외 혼용하지마

12F9D8의 00 00 00 01 -> 디버기의 이벤트 중에서도 "예외(Exception)"이란게 발생했다.

772DE60E의 80 00 00 03 -> 하고 많은 예외 중에서도 "Break Point" 예외가 발생했다.

즉, Exception Address(772DE60E)에서 Exception Code( 80 00 00 03 )를 따져야 하는 건, 그 예외 중에서도 우리가 찾는 예외인지 알고 싶어서다.

지금의 브레이크 포인트 예외는 디버기 모드로 실행하면 무조건 발생하는 예외일 뿐이다. 이런 시스템 브레이크 포인트는 우리 알바가 아니라서 계속 넘어가.

우리가 찾는 순간

사실 4차 시도에서 로그를 보면 디버기 프로세스에서 여러 이벤트 중에 exception이라는 이벤트는 총 3번 일어난다.

근데 한번은 시스템 브레이크 포인트 예외였지. 그럼 두번이 더 남았다. 즉 우리는 두번의 EXCEPTION_ILLEGAL_INSTRUCTION을 기다려야 한다.

다시 F9으로 실행 돌려봐. 그럼 조건부 브레이크포인트 땜에 event=1일때 또 pause 한다.


바로 이런거야.

Exception code가 C0 00 00 1D 래. 이건 EXCEPTION_ILLEGAL_INSTRUCTION 를 의미한다. 잘못된 부분이지. 어디에? 40103F인데 여기는 이 부분이다.

암튼 이 루프 탈출 부분의 아랫쪽을 보면 또 여러개의 조건 분기가 나온다.

CMP EAX,40103F. 예외 발생 주소가 40103F냐고 묻는데 거기였지? 그럼 True네.
JNZ 401299를 통과하고 40121D 명령어를 수행

아직 한발 남았다.

이 대사 떠오르면 나이든거임? CLBP에서 예외 이벤트는 총 3번인데 시스템 브레이크 포인트로 한번, 방금 한번 썼으니까 한번 더 볼 수 있지.

이제 C0 00 00 1D는 알잖아. 주소만 확인하자
401048은 가보면 암호화된 코드가 나와서 의미를 알기 힘들다. 그치만 이번 경우에도
얘는 거치겠지

근데 이번엔 주소가 40103F가 아니니까 JNZ 를 통과하지 못해서 점프에 걸릴거야. 그럼 401299로 점프 하겠지.

5차 시도 결과 정리

두번의EXCEPTION_ILLEGAL_INSTRUCTION을이 있는데 한번은 40121D, 한번은 401299의 명령어를 수행한다.

6. 주소 40121D부터 분석 (6차 시도)

Step 1. ReadProcessMemory() 호출

부모 프로세스가 자식의 메모리를 읽는다.

PUSH 0                     ; pBytesRead = NULL
PUSH 14h                  ; 읽을 길이 = 20 바이트
LEA EDX, [ESP+...]
PUSH EDX                  ; 읽어들일 버퍼 주소
PUSH 401041               ; 자식의 복호화 대상 시작 주소
PUSH hProcess             ; 디버기 프로세스 핸들
CALL ReadProcessMemory

즉, 디버기 프로세스의 401041부터 암호화된 코드 20바이트를 읽어온다.

Step 2. 복호화

XOR BYTE PTR [...+ECX], 0x7F
INC ECX
CMP ECX, 14
JL Loop

아까 읽은 20바이트를 0x7F로 XOR 복호화한다.
→ 401041에 있던 암호화된 코드가 원래 명령어로 복구됨

Step 3. WriteProcessMemory() 호출

write 기능을 통해 복호화된 코드를 디버기 프로세스의 원래 위치(401041)에 덮어쓴다.

PUSH 복호화된 버퍼 주소
PUSH 401041
PUSH hProcess
CALL WriteProcessMemory

이제 디버기(자식)의 암호화된 부분이 정상적으로 보인다.

Step 4. GetThreadContext() + SetThreadContext()

디버기 프로세스를 이제 본격적으로 실행할 준비를 한다. 실행하려면 EIP를 거기로 둬야겠지?

EIP 레지스터 값을 조작해보자.

CALL GetThreadContext      ; 디버기 스레드의 context를 읽어옴

GetThreadContext엔 lpContext가 있지.

CONTEXT 구조체 포인터 안에는 EIP, ESP, EBP, EAX, EBX 등 레지스터 값이 다 들어 있다.

typedef struct _CONTEXT {
  ...
  DWORD Eip; // 현재 실행 중인 명령어 주소
  ...
} CONTEXT;
ADD Context.Eip, 2         ; EIP += 2 → 잘못된 명령어 건너뜀
CALL SetThreadContext      ; 수정된 context 적용

왜 EIP를 +2 하냐고?
→ 문제의 명령어 LEA EAX, EAX는 2바이트니까 그건 건너뛰고 그 다음부터 가려고!

EIP를 여기로 옮긴다? = 이제 암호도 없는 멀쩡한 자식 프로세스의 명령어들을 수행하겠다

7. 주소 401299부터 분석

그 전에!

이 실행파일은 당연히도 동적 분석 해야한다.
실행시켜보지 않고 뜯어보기엔 불가능한 이유가 많다.

  • 실행되지 않으면 코드가 복호화되지 않음
  • PE 파일 내부엔 암호화된 쓰레기 명령어만 존재
  • 예외 처리를 통해서만 코드가 복호화되기 때문에, 정적으로는 진짜 명령어가 뭔지 볼 수 없음

그래서 한줄씩 실행하는 동적 분석을 하는거다.

시작.

사실 6차 시도랑 동작은 같다.

항목6차 시도7차 시도
예외 주소40103F401048
복호화 처리 루틴 주소40121D401299
복호화 대상401041~401048~
전체 흐름디버기 초기 실행 준비디버기 두 번째 단계 실행

ContinueDebugEvent()

사실 매번 사용되는 함수인데 7차에서만 다뤄지길래 여기서 적을게.

디버깅 루프는 이벤트를 처리하지 않아도 ContinueDebugEvent()는 호출해야 한다.
왜냐면

Windows는 이벤트가 발생하면 프로세스를 일시 정지시킴
이 정지를 풀어주지 않으면, 디버기 프로세스는 다음 명령어로 절대 못 넘어감.
예외건, DLL 로딩이건, BP건 간에 디버거가 “나 처리했어~”라고 알려줘야 함

→ 그게 바로 ContinueDebugEvent()의 역할

그래서 이걸 호출하지 않으면 자식 프로세스는 그대로 멈춘다.

부모(디버거)-자식(디버기) 관계 끊기

프로세스는 두번째 예외인 401048까지 처리하고 나면 ContinueDebugEvent()를 호출하고 루프를 break한다.

DebugActiveProcessStop()를 호출해서 디버깅이 종료되었음을 알린다.

디버거(부모)가 종료되면 디버기(자식)도 함께 종료됩니다. 순서 중요.

디버기의 이벤트는 디버거가 처리해야 하는데,
디버거가 사라졌다? = 디버기 누가 책임져, 디버기도 죽여.

profile
Cybersecurity Consultant

0개의 댓글