제가 분석한 바로는 EMET 4.1 에서는 ROP를 차단하기 위해 주요 함수들 - VirtualProtect / VitualAlloc 등을 후킹 한 뒤, 해당 함수들의 호출 지점을 디스어셈블링 하여 해당 함수가 Call로 호출되는지 판단합니다.

패킹된 EXE면 항상 Call 한다는 보장이 없기 때문에, 테스트를 해보았습니다.

 

해당 EXE를 빌드 후, CodeVirtualizing 하였습니다

그 다음 해당 EXE를 EMET로 보호 후, 디버거를 붙인 다음 VirtualProtect 지점에 BP를 걸었습니다.

 

ESP가 가리키는 리턴 주소가 0x40A277입니다. 따라가봅니다.

 

0x40A277 부분이 CALL이 아닌 JMP군요. 논패킹 EXE라면 아래 그림과 같이 되어있습니다.

 

EMET는 호출 지점이 Call이 아니면 진단합니다.

  

보통 ROP는 RETN / JMP 로 함수를 호출하기 때문에 이런 방어법은 강력한 차단 수단이 될 수 있지만, 패킹된 프로그램의 경우 오진이 발생하니 패킹 여부를 인식할 필요가 있을 것 같습니다. 

 

항상 JMP로 4바이트 점프만 해오다 JMP SHORT으로 1바이트 점프할 일이 생겨 정리를 하게 되었습니다.

결론부터 말씀드리면, 기준주소에서 -0x7E(126바이트), +0x81(129바이트) 까지 총 0xFF(255바이트) 점프할 수 있습니다.

 

# 앞 주소로 점프하는 경우

기준 주소 0x7243F281입니다. 최대 0x7243F203, 즉 -0x7E까지 점프할 수 있습니다.

오프셋 계산은 점프 할 주소 - 기준 주소 - 2 = 0x80

위 그림을 확인하여 주세요.

 

 # 뒷 주소로 점프하는 경우

 기준 주소 0x7243F281입니다. 최대 0x7243F302, 즉 +0x81까지 점프할 수 있습니다.

 

오프셋 계산은 점프 할 주소 - 기준 주소 - 2 = 0x7F

위 그림을 확인하여 주세요.

 

 

최근 자주 쓰이는 공격 기법중에 하나가 ROP 공격이며, exploit들을 검색해 보셨다면 한번 쯤은 접해보셨을 것입니다.

ROP는 Return Oriented Programming의 약자이며,

흔히 볼 수 있는 위와 같은 유형을 리턴 기반의 프로그래밍이라고 합니다..

그러나 통상적으로 ROP를 일컬을 때는 메모리 상의 명령어들을 모아서 리턴 기반으로 명령어들을 순차적으로 실행하는 것을 말합니다

무슨말인지 감이 잘 안오시죠? 예제 소스를 보면서 설명하겠습니다.

 

attack() 함수를 보면 전형적으로 리턴 주소를 덮어씌우는 공격입니다.

attack()의 수행 후 return 시 main() 함수로 되돌아 가야 하지만 &a+5로 인하여 main()으로 가는 리턴 주소는 rop_gadget 변수로 덮어 씌어졌습니다.

이로써 attack() 함수가 리턴 될 때 스택은 rop_gadget 변수를 가리키고 있습니다.

보시다시피, RETN Breakpoint를 걸고 스택을 살펴보면 rop_gadget 변수로 덮어 씌여져 있습니다.

RETN을 수행하면 현재 스택이 가리키고 있는 0x759900B5로 가게 됩니다.

RETN이 수행되고 0x75990215를 가리키고 있습니다.

Attack() 함수에서 RETN이 수행되었으니 스택은 4가 증가하게 됩니다.

순차적으로 NOP RETN이 실행되고, RETN 시 스택을 살펴보면 0x75990215를 가리키고 있습니다.

마찬가지로 RETN을 수행하면 스택이 가리키고 있는 주소로 가게 됩니다.

0x75990215입니다. 순차적으로 NOP, RETN이 실행되고, 마찬가지로 RETN 시 스택을 살펴보면 0x759DEA4D를 가리키고 있습니다.

 

 

rop_gadget 변수의 마지막입니다. 원리는 같으므로, 설명은 생략하겠습니다.

RETN 시 되돌아 가는 지점을 구성하지 않았기 때문에 RETN을 하면 0x00000000으로 가게 되어 crash가 발생됩니다.

이렇게 구성한 주소의 명령어들을 순차적으로 실행하게 하는 것이 ROP라 하고 이 주소들을 연속적으로 이은 것을 ROP CHAIN, 혹은 ROP GADGET이라고 합니다.

보통 ROP GADGET을 만들 때는 기존 메모리 상에 존재하는 명령어들로 구성하게 됩니다.

이 예제 소스와 같이 NOP RETN을 구성하고 싶다면 메모리 상에서 90 C3을 찾으면 됩니다.

통상적으로 ASLR이 적용되면 모듈이 메모리 주소 공간에 랜덤하게 매핑되므로, JRE 같은 ASLR이 적용되지 않은 모듈을 찾아서 재구성을 해야 합니다. (해당 주소들은 제 환경에 맞게 하드코딩 된 것이므로 직접 찾아서 구성해보시길 바랍니다. 물론 XP의 경우는 해당사항이 없을 것 입니다.)

이로써 ROP의 개념을 살펴보았고 다음 편에서는 좀 더 심화된 내용을 다루겠습니다.

MMORPG 유저라면 게임을 띄워놓고 치트엔진을 사용했던 경험이 한번씩은 있을겁니다. (..없나?)

만약 핵쉴드 혹은 게임가드가 붙어있는 게임에 치트엔진을 사용했다면 "프로세스를 열 수 없습니다" 메시지를 확인할 수 있습니다.

 

<그림 1 : 프로세스 열기 실패>

 

위와 같은 범용 치트엔진은 Win32 API인 OpenProcess를 통해서 프로세스에 접근을 하게 됩니다.

이 같은 방법을 막는데엔 여러가지 방법이 있겠지만, 백신같은 같은 보안 프로그램이 프로세스 핸들(오브젝트)를 보호하고 있다고 가정해보죠.

 HANDLE hProcess = NULL;
 hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, 3752);
 printf("%x %x",hProcess, GetLastError());

본 소스는 특정 프로세스(pid : 3752)의 핸들을 얻는 코드로, 치트엔진도 이와 같은 초벌작업을 합니다.

컴파일하여 출력하면 NULL과 5가 출력되는데, 5는 ERROR_ACCESS_DENIED 으로 액세스 거부를 뜻합니다.

(시스템 에러 코드 표 : http://msdn.microsoft.com/en-us/library/windows/desktop/ms681382(v=vs.85).aspx)

이처럼 정상적인 경로로 핸들을 얻지 못하는건 보안 프로그램에서 뭔가 작업(?)을 했다는 건데요.

지금부터 무슨 작업을 했길래 핸들을 얻지 못하는지 썰을 풀어보도록 하겠습니다.

 

# 본격 작업에 앞서서 핸들을 짚어보죠.

타겟 프로세스의 핸들을 찾는 과정을 windbg를 통해 트레이싱 해보겠습니다.

타겟은 국민 프로세스 지뢰찾기(winmine.exe)입니다.

치트엔진으로 winmine.exe을 오픈했으며, 프로세스 익스플로러로 이를 확인할 수 있습니다. 

 

< 그림 2 : Process Explorer로 프로세스 핸들 확인>

치트엔진에서 winmine.exe의 프로세스 핸들(오브젝트)를 가지고 있습니다. 여기서는 0x89AF5DA0이며, 프로세스 핸들이기 때문에  EPROCESS 구조체로도 표현이 가능합니다.

 EProcess.txt

 

프로세스 핸들을 보호해 볼까요?

우선 OpenProcess의 내부부터 살펴볼까요,

유저모드에서 sysenter를 타서 커널로 진입을 하여 NtOpenProcess를 여는건 누구나가 다 아는 사실일텐데요(모름 검색 고고~)

XP 커널 기준으로 NtOpenPorcess는 내부에서 ObOpenObjectByPointer, PsLookupProcessByProcessId, ObOpenObjectByPointer등과 같은 여러가지 API 들을 호출합니다.

자세한건 아래 첨부파일을 참조하세요.

NtOpenProcessCode.txt

여기서 ObOpenObjectByPointer라는 흥미로운 API가 있습니다.

MSDN의 설명을 빌리자면,  ( http://msdn.microsoft.com/en-us/library/windows/hardware/ff550985(v=vs.85).aspx )

The ObOpenObjectByPointer function opens an object referenced by a pointer and returns a handle to the object.

오브젝트를 열고 핸들(HANDLE)을 반환한다는군요.

NTSTATUS ObOpenObjectByPointer(
  _In_      PVOID Object,
  _In_      ULONG HandleAttributes,
  _In_opt_  PACCESS_STATE PassedAccessState,
  _In_      ACCESS_MASK DesiredAccess,
  _In_opt_  POBJECT_TYPE ObjectType,
  _In_      KPROCESSOR_MODE AccessMode,
  _Out_     PHANDLE Handle
);

중요한 것은 NtOpenProcess 실행 시 이 API의 Object값은 프로세스 핸들(오브젝트)이 된다는 점입니다.

뭔가 감이 오지 않나요? 이 API를 후킹해서 아래와 같은 코드를 작성하면 지뢰찾기 프로세스의 액세스 거부를 반환할 수 있습니다.

if (Object == 0x89AF5DA0) return STATUS_ACCESS_DENIED;

 

# ObOpenObjectByPointer 후킹 실전 코드

아래 링크는 대륙의 해커 dayed가 만든 후킹 소스입니다.

http://bbs.pediy.com/archive/index.php?t-67286.html

후킹 함수인 fake_ObOpenObjectByPointer() 함수를 살펴보면,

Object가 널이 아니면 -> Object가 프로세스이면 -> 보호된 프로세스가 자기자신 프로세스가 아니면 -> 보호된 프로세스와 Object가 일치하면 액세스 거부

이렇게 구성되어 있습니다.

wdk/ddk로 컴파일시 소스의 Notepad.exe부분만 타겟 프로세스로 바꿔주면 치트엔진에서 액세스가 거부되는 것을 볼 수 있습니다.

위와 언급한 코드처럼 하드코딩해서 입력해도 동작은 합니다.(많이 번거롭지만요)

 

# 마치며

이 외에도 오브젝트 관련 함수인 ObReferenceObjectByHandle() 함수를 후킹하는 방법도 있습니다.

만약 핸들을 보호하는것이 아닌 OpenProcess를 막고 싶다면 NtOpenProcess()를 후킹하는 게 가장 손쉬운 방법입니다.

+ Recent posts