HellsGate

Disclaimer

이곳은 제 개인 공부 + 연구 섹션입니다. 오펜시브 시큐리티 관련 블로그 글을 읽고, 요약한 뒤, 거기에 있는 코드들 따라치기 정도의 낮은 퀄리티 글 밖에 없을거기 때문에 안 읽으셔도 무방합니다. 이 섹션의 모든 페이지들의 내용 및 코드는 제것이 아닙니다.

페이퍼 링크: https://vxug.fakedoma.in/papers/VXUG/Exclusive/HellsGate.pdf

툴/코드 링크: https://github.com/am0nsec/HellsGate

문제

  • AV/EDR의 유저랜드 후킹을 우회하기 위해 시스템 콜을 이용하는데, 이를 위해서는 각 NTAPI 함수들의 시스템 콜 번호를 알아야한다. Hell's Gate 발표 전까지 이 번호들을 런타임 중 프로그래매틱 하게 찾아낼 수 있는 방법이 없었다.

  • 따라서 레드팀들은 모든 윈도우 버전의 모든 시스템 콜 번호들을 스태틱하게 구조체로 만들어 하드코딩 + 수많은 조건문을 이용해 시스템 콜을 사용하고 있었다. 물론, 이는 매우 불안정하고 비효율적인 방법이였다.

해결

  • HellsGate (헬즈 게이트)는 NTAPI 함수들의 시스템 콜 번호를 런타임 도중 프로그래매틱하게 찾아주는 기법을 문서화 + POC화 시킨 연구다. 헬즈 게이트 덕분에 추후 수많은 XYZ 게이트 기법들이 생겨나며 시스템 콜 시대가 시작된다.

중요 개념 - 프로그래매틱하게 시스템 콜 번호 찾기

  • HellsGate의 중요 개념은 ntdll의 export된 NTAPI 함수의 시스템 콜 관련 바이트 패턴(mov r10, rcx // mov eax, <syscall-number>)의 시작지점으로부터 index로 5번째 (0부터 시작 기준, 실제로는 6번째 바이트) 바이트를 bitwise shift left 8 연산을 한 뒤, index로 4번째 (0부터 시작 기준, 실제로는 5번째 바이트) 바이트를 OR 연산을 하면 시스템 콜 번호를 프로그래매틱하게 구할 수 있다는 것이다.

페이퍼의 예시를 보자면, NtPlugPlayControl 함수의 시작 지점으로 부터의 명령어들은 4c 8b d1 b8 32 01 00 00 이다. 각각의 명령어들은 다음과 같이 풀이된다.

0:000> uf ntdll!NtPlugPlayControl  
ntdll!NtPlugPlayControl:  
00007fff`b040d3b0 4c8bd1 mov r10,rcx       // 인덱스 기준 0,1,2     - 4c 8b d1
00007fff`b040d3b3 b832010000 mov eax,132h  // 인덱스 기준 3,4,5,6,7 - b8 32 01 00 00

이때, 0부터 시작하는 인덱스 기준으로 5번째 0x01 바이트를 bitwise shift left 8 연산 후, 인덱스로 4번째 바이트 0x32 로 OR 연산을 하면 다음과 같이 된다.

페이퍼에서도 Windbg를 통한 풀이를 다음과 같이 보여준다.

코드 - 시스템 콜 번호 찾기 - 개념

위 개념/공식은 Hell's Gate 의 공식 깃헙 리포 main.cGetVxTableEntry 함수에 적용되어 있다.

먼저, 제대로 시스템 콜 번호 관련 명령어가 있는 부분까지 왔는지 조건문을 통해 확인한다. pFunctionAddress의 0, 1, 2, 3 번째 오프셋이 4c 8b d1 b8 인지 알아본다. 이는 move r10, rcx, mov 까지의 어셈 코드를 나타낸다. 그 뒤, 6번째와 7번째 오프셋이 00 00 인지도 확인하고 있다. 6번째와 7번째 바이트를 확인하는 이유는 mov eax, <syscall>h 가 실행될 경우 인덱스로 6번째와 7번째 오프셋의 바이트들은 무조건 00 00 이 되기 때문이다.

0,1,2,3,6,7 번의 바이트를 모두 확인했고, 조건문을 통과했다면, 우리가 찾고 있던 시스템 콜 관련 명령어 "패턴" 을 찾은 것이다.

이제 남은 4번째, 5번째 바이트와 Hell's Gate의 시스템 콜 번호 찾기 공식을 이용해 시스템 콜 번호를 찾는다. 앞서 얘기한대로, 인덱스(NT함수의 시작지점)로부터 5번째 바이트는 bitwise shift left 8 연산을, 그 뒤에 4번째 바이트는 OR 연산을 실행한다. 그 뒤, 찾아낸 시스템 콜 번호를 pVxTableEntry->wSystemCall 에다가 저장한다.

적용

  • 시스템 콜 번호는 찾았다. 근데 이건 어떻게 사용되는걸까?

  • Hell's Gate 가 NT 함수들의 메모리 주소, 함수 해시, 시스템 콜 번호를 편리하게 저장하기 위한 _VX_TABLE_ENTRY_VX_TABLE 에 관련된 설명은 생략한다.

  • 일단 VX_TABLE 및 엔트리들을 최대한 설정해준 뒤, GetVxTableEntry 함수를 이용해 위에서 설명한 개념을 이용해 시스템 콜을 찾는다.

  • 그 뒤, HellsGate 과 HellDescent 어셈블리 코드들을 이용해 실제로 시스템 콜을 실행한다.

HellsGate 는 GetVxTableEntry 함수에서 찾아낸 시스템 콜을 wSystemCall 이라는 변수에 저장한다. HellDescent 는 실제 NT 함수들처럼 mov r10, rcx // mov eax, <systemcall>h // syscall 를 통해 eax 레지스터에 시스템 콜 번호를 올려놓은 뒤, syscall 명령어를 실행해 CPU에게 커널 모드로 진입해 해당 syscall 을 실행시킨다.

예시

페이퍼에서 나온 간단한 예시다. 예를 들어, NtAllocateVirtualMemory 의 시스템 콜 번호를 알아낸 뒤, 이를 직접 어셈블리 코드를 이용해 사용해보자.

마치며

헬즈 게이트는 2018~2020년 유저랜드 후킹 및 시스템 콜 관련된 포스트들이 많이 나올때 프로그래매틱하게 시스템 콜 번호를 찾아내는 방법을 깔끔한 문서화 + POC 코드까지 제공한 전설적인(?) 연구다.

헬즈 게이트 이후 수많은 XYZ게이트 기법들이 쏟아져 나오기도 했다. 추후 나온 기법들에 비하면 부족하기는 하지만, 게이트 기법들의 선조격이자 처음으로 나온 기법이니 이해된다. 다시 한 번 페이퍼를 읽으면서 저자들의 지식에 감탄했다.

MISC - 혼잣말

  • GetVxTableEntry 에서 cw 변수가 왜 quick and dirty fix in case the function has been hooked 인지 몰랐는데, 말이 된다. NT함수의 시작지점 오프셋이라고도 볼 수 있는 cw를 하나씩 증가하며 NTAPI 함수의 메모리를 시작부터 1바이트씩 쭉 훑으면서 조건문을 통해 mov r10, rcx // mov 패턴의 명령어들을 찾는다. 이렇게 되면 NT 함수 초반 부분에 훅을 걸어놓건 뭘 하던 간에 어쨌든 "난 mov r10, rcx // mov 패턴을 가진 바이트들만 찾는다" 느낌으로 쭉 메모리를 훑게 된다. 후킹을 우회할 수 있게 된다는게 무슨 말인지 알겠다.

Last updated