Unveiling the Underground World of Anti-Cheats 요약

ladins9·2024년 9월 19일
post-thumbnail

Black Hat Europe 2019 슬라이드
Youtube 링크

Black Hat Europe 2019의 브리핑 중 흥미있는 주제가 있어서 보고 기술적 부분 위주로 요약해보려 한다.

Anti-Cheat Components

안티치트의 보호 방법을 이해하려면 먼저 구성요소를 이해해야 한다.

먼저 인터넷에 연결된 서버가 존재한다. 서버는 각 클라이언트와 연결되어 있으며, 어떤 식으로든 연결이 끊어질 경우 안티치트에 문제가 있다고 판단되어 게임을 종료시킨다. 이를 통해 안티치트 없이 실행하는 부정행위를 방지한다.

의심스러운 행동을 수집하고 보고한다. 예를 들어 프로세스나 DLL을 덤프하고, 추후에 분석할 수 있도록 서버에서 보관한다.


두 번째는, 커널 드라이버가 있다.
Ring0에서 동작함으로서 강력한 기능을 제공한다.

Register kernel callback을 통해 시스템 이벤트를 모니터링 하고 대응한다. 예를 들어 새로운 프로세스, 스레드, DLL 등이 새롭게 생성된다면 이때 특정 행동을 수행한다.

Access Control을 통해 치트 프로그램이 메모리 접근을 위해 핸들을 요구할 때 모니터링 하고 핸들을 거절한다. 그 외에 누군가 리버싱하는지 모니터링하고 이를 어렵게한다.


세 번째로, 게임 내에 DLL로 삽입되는 구성 요소가 있다.
외부 프로세스로 하기 힘든 내부적인 안티치트 기능을 주로 수행한다. 게임의 각 메모리 영역을 탐색하여 새로운 실행 가능 영역이 생성되었거나 스레드가 예상치 못한 영역에서 실행된 경우를 감지한다. 또한 내부적으로 프로그램 흐름이 예상과 벗어나지 않는지를 감지한다.


마지막으로, 외부 프로세스 요소가 있다.
실행중인 모든 프로세스에 접근 요청을 하여 알려진 시그니처등을 탐색하고, 안티치트 드라이버의 로직과 인터넷 서버와의 연결도 관리한다. 이런 기능은 꼭 외부프로세스에 있지 않을 수도 있다.

Internal(DLL) vs External(Process)

Internal의 경우 메모리에 직접 접근이 가능하므로 성능이 좋다. 또한 DLL 인젝팅 기법이 너무 다양한 만큼 탐지하기도 어렵다.
만약 작은 버퍼를 사용할 수 있다면 거기에 쉘코드를 작성하여
스레드를 하이재킹한 다음 임의 코드를 실행할 수 있다.

External의 경우 외부에서 동작하는 만큼 성능이 떨어지고, 일반적으로 핸들이 필요하기 때문에 감지될 가능성도 크다.

Bypass Anti-Cheats

Hijacking Techniques

외부 프로세스가 메모리에 접근하려면 핸들이 필요하다. 안티치트는 핸들을 요구하는 프로세스를 검증한다. 하지만 윈도우에는 Whitelisted 프로세스가 존재한다. (lsass, csrss, etc...)

LSASS는 이미 모든 프로세스의 핸들을 가진 상태이다.
그렇다면 LSASS에 DLL 인젝션을 하면 LSASS를 통해 게임 메모리에 접근할 수 있다.
External Cheat와의 Communication을 위해 NamedPipe를 사용한다. 이를 통해 응답과 요청을 DLL에 전달하고 보내는 식으로 External Cheat의 핸들 문제를 해결한다.


다만 이 방법은 다음과 같은 문제점이 있다.
LSASS라는 잘 알려진 프로세스에서 수상한 새로운 핸들이 생성된다.
안티치트는 일반적으로 유저모드 WIN API를 후킹하므로 WIN API를 사용하기 힘들다.
또한 스레드를 생성하더라도 일반적인 context를 벗어나 생성된다.
마지막으로 안티치트는 핸들 권한을 최소한으로 다운그레이드 하기 때문에 Full Access가 불가능하다.


앞선 방법을 보완하기 위해 FileMapping을 사용할 수 있다.
FileMapping은 핸들을 만들지 않고도 공유메모리를 가질 수 있다.


CreateFileMapping() 함수는 핸들과 포인터를 반환하는데, CloseHandle() 함수로 핸들을 반환하더라도 UnmapViewOfFile 함수를 실행하기 전에는 공유 메모리를 참조하는 포인터가 유효하다.
해당 원리로 핸들 없이 공유 메모리를 가질 수 있다.


Race Condition을 피하기 위해 마지막 1바이트가 1인지 0인지에 따라 Spinlock을 건다.


이렇게 첫 번째 수상한 핸들 문제를 해결했다.


EAC(Easy Anti Cheat)는 lsass도 후킹한다. 따라서 lsass를 통해 WINAPI로 READ WRITE ALLOCATE 등을 수행한다면 탐지될 것이다.


하지만 WINAPI들을 리버싱해보면 생각보다 간단한 함수이다. WINAPI를 사용하지 않고 몇 줄의 어셈블리 코드로 직접 SYSCALL을 호출한다면 두 번째 문제도 해결할 수 있다.

Hooking


사진 우측과 같이 적들을 빨간색으로 칠하는 월핵의 경우 그래픽 엔진을 후킹해야 한다. 안티치트는 이런 행위를 방지하고자 import가 stable한지 함수 도입부가 온전한지를 지속적으로 확인한다.

하지만 스팀이나 OBS같은 대중적인 서드파티 라이브러리들은 화이트리스트로 지정될 수 밖에 없다. 만약 스팀게임을 해봤다면 Shift+Tab으로 스팀 오버레이를 띄운 경험이 있을 것이다. 이것도 스팀이 해당 게임에 DLL을 인젝션하여 그래픽 엔진을 후킹한 것이다.


사진에는 어떻게 Steam과 OBS가 후킹하는지 담겨있다.
Direct X의 메인 렌더링 함수의 도입부를 후킹한다.

이러한 서드파티 DLL을 조작하는 것이 게임을 직접 조작하는 것 보다 쉬울 수 있다. 다양한 버전의 서드파티 라이브러리들에 대한 모든 서명을 생성하는 것은 단순히 게임의 서명을 생성하는 것 과는 비교가 안 될 정도로 복잡하기 때문이다.

또한 타사에서 개발했기 때문에 게임을 조작하는데 도움이 되는 몇 가지 실수가 있을 가능성도 존재한다.

사진의 예는 OBS가 주입하는 DLL인데, 0으로 채워진 실행 가능한 영역의 Code Cave가 존재하는 모습이다. 여기에 쉘코드를 주입할 수 있다.

또한 스레드를 하이재킹하거나 새로 생성할 때 화이트리스트로 지정된 DLL 내부에서 실행되므로 안티치트 입장에서는 더이상 수상한 context에서 실행되는 DLL이 아니게 되는 것이다.

그리고 OBS에는 NamedPipe도 존재하기 때문에 이걸 재사용하거나 조작하여 임의 코드 삽입이 가능하다.


이런 방법으로 3번째 문제점도 해결할 수 있다.
마지막 문제는 커널 영역으로 들어가야 한다. 왜냐하면 안티치트는 해당 기능을 커널 드라이버를 통해 수행하기 때문에 우회하기 위해서는 같은 권한이 필요하다.

Drivers


먼저 드라이버를 로드해야 하는데 현실적인 방법은 서명된 취약한 드라이버를 이용하는 방법이다.
다양한 Manual Mapper들을 사용해도 되고, UEFI 부트킷을 사용하여 로드할 수도 있다.

사진을 보면 Apex Legend의 클라이언트의 핸들 권한이 다른 프로세스에 비해 downgrade 된 것을 볼 수 있다.

취약한 드라이버를 이용한다는 것은 DKOM(Direct Kernel Object Manipulation) 을 한다는 것과 같다.
먼저 LSSAS 프로세스를 커널 메모리 내부에서 찾아야 한다.

  • EPROCESS Struct를 LSSAS의 패턴으로 물리 메모리를 스캔하여 정확한 포인터를 찾는다.
  • EPROCESS에서 HANDLE_TABLE을 가져온다.
  • ExpLookupHandleTableEntry(HandleTable, Handle) 함수를 호출하여 핸들이 할당된 특정 주소를 반환받는다.
    단, 해당 함수는 커널 내부에서 직접 호출해야 하므로 앞선 과정처럼 물리 메모리를 스캔하여 해당 함수를 찾아낸 후 CallKernelFunction을 수행해야한다.
  • 그렇게 찾아낸 핸들의 권한을 수정 후 커널 메모리를 덮어 씌운다.


그렇게 핸들 권한을 승격함으로써 4가지 Disadvantages를 극복했지만 여전히 문제가 하나 존재한다.
바로 LSSAS의 핸들 권한이 승격된 것을 안티치트가 감시한다면 무용지물이 된다는 것이다.


결국 목표는 감지되지 않은 메모리 읽기/쓰기/할당 이기 때문에 커널 영역에서 해당 목표를 수행할 수 있다면 제일 이상적일 것이다.
이미 드라이버를 이용해서 물리메모리를 읽고 쓸 수 있으므로 조작하고자 하는 메모리를 물리 메모리로 변환할 수만 있다면 해당 값을 읽고 쓸 수 있다.

  • NtQuerySystemInformation의 첫번째 인자로 SystemExtendedHandleInformation(0x40)을 전달하면 모든 Handle pointer를 반환한다. 여기에는 Process Handle 뿐 아니라 File Handle이나 Event Handle도 포함된다.

  • Handle의 First bite를 하나씩 살펴보면 0x00B6003을 찾을 수 있다. 이는 해당 핸들이 valid KPROCESS pointer 임을 나타낸다. 이걸로 랜덤한 프로세스의 KPROCESS 포인터를 알아낼 수 있고, EPROCESS의 위치까지 특정할 수 있다.

  • EPROCESS.ActiveProcessLinks는 연결 리스트이므로 원하는 프로세스를 찾을 때 까지 이를 순회한다. 예를 들어 실행 파일 이름으로 비교하면서 찾을 수 있다.

  • 링3 가상 주소를 물리 주소로 변환하기 위해서 _EPROCESS.PEB.DirectoryBaseTable과 _EPROCESS.SectionBaseAddress이 필요하다. 이 두개의 정보로 어떤 프로세스든 가상주소를 물리주소로 변환할 수 있다.

결론

  • 파일명 바꾸기 같은 사소한 취약점으로도 안티치트가 우회될 수 있다.
  • 모든 취약한 드라이버를 사전에 블랙리스트에 올리는 것은 불가능하다.
  • 서드파티 프로그램은 늘 치트 개발자에게 좋은 기회가 된다.


해당 주제는 초심자 레벨에서 진입 장벽이 높기 때문에 초심자의 학습을 위한
AntiCheat-Testing-Framework를 배포함.

추후 포스팅은 해당 깃허브를 분석하여 정리할 것이다.

profile
잡학다식에서 박학다식으로

0개의 댓글