Skip to content
KitploitKITPLOIT
도구블로그
제출
도구블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
CVE-2020-15368 — CVE-2020-15368, 일명 "취약한 드라이버를 악용하는 방법" | Kitploit
도구/GitHubGitHub/stong/cve-2020-15368
Privilege EscalationVulnerability AnalysisExploitationShellcodeLearning & EducationPayload DevelopmentBinary Exploitation
GitHubstong/cve-2020-15368

CVE-2020-15368

CVE-2020-15368, 일명 "취약한 드라이버를 악용하는 방법"

저장소 보기
5154914년 전Kitploit 검토 완료

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

취약한 Windows 드라이버를 익스플로잇하는 방법

CVE-2020-15368에 대한 익스플로잇 및 개념 증명(PoC)입니다. Asrock은 RGB 컨트롤러 구성 도구를 위해 rweverything 드라이버를 다시 패키징하고 서명했습니다. 그들은 ioctl을 암호화하여 "보호"합니다...ㅋㅋ. 우리는 지난여름에 이 CVE를 우연히 발견했고, 아는 한 이 드라이버는 여전히 패치되지 않았습니다. 물론 영향은 커널에서의 임의 코드 실행 등입니다. 그럼 이 "0day"를 즐기세요 ㅋㅋ.

이것이 진짜 '진정한' CVE인지에 대해 나와 논쟁하고 싶다면, 부담 없이 Twitter로 연락하세요. 공개 소셜 미디어에서 큰 싸움을 벌일 수 있고, 관련된 모두에게 정말 흥미진진할 거예요! 당신이 원한다면 이 버그를 위해 도메인도 사겠습니다. 전부 마케팅이니까!!!!

어쨌든, 이 버그는 꽤 구려서, 평범한 취약 드라이버를 pwn하는 방법에 대한 튜토리얼로 사용하려고 합니다. 그래서 이 글은 초보자를 대상으로 합니다. 취약한 드라이버를 익스플로잇하는 방법을 배우게 될 것입니다. 이와 같은 다른 형편없는 드라이버가 많이 있습니다. 세상은 당신의 것입니다. 재미있게 보내세요.

고지 사항: 이 게시물은 교육 목적으로만 제공됩니다. 모든 관련 지역, 주, 연방 법률을 준수할 책임은 독자에게 있습니다. 이 게시물의 저자는 이 게시물에 포함된 소프트웨어로 인한 오용이나 손해에 대해 책임을 지지 않으며, 어떤 책임도 지지 않습니다.

배경 이야기

격리 생활에 갇혀 있던 중, 룸메이트들(Pear0, Codetector)과 나는 Pear0의 새 Asrock 마더보드로 장난치고 있었습니다. 새빨간 LED가 매우 거슬렸고, Linux에서는 그것을 구성할 수 없었습니다. 그래서 우리는 그것을 제어하는 Windows 드라이버를 리버스 엔지니어링하고 Linux에서 I/O 작업을 재현하기로 계획했습니다.

요컨대, 드라이버가 문자 그대로 무엇이든 임의 읽기/쓰기 액세스를 허용하는 평범한 드라이버라는 것을 깨닫는 데 오래 걸리지 않았습니다. 여기에는 CR3, CR4 같은 제어 레지스터, 물리 메모리 등이 포함됩니다. 이와 같은 드라이버는 디버깅 도구로 사용하기 위한 것이며, 공급업체 웹사이트에도 명확히 명시되어 있습니다.

docs/lol.png

우리는 이것이 매우 재미있다고 생각했습니다. 사용자 공간에서 컴퓨터를 트리플 폴트(triple fault) 시켜 하드 리부팅하게 만드는 것은 처음에는 꽤 짜릿합니다. (20번째는 덜 흥미로울 수도 있습니다.) 어쨌든 우리는 버그를 신고한 후 1년 동안 잊고 있었습니다.

셋업

커널 초보자로서, 드라이버를 실제로 로드하고 상호작용하는 방법이 궁금했습니다. 알고 보니 매우 쉽습니다.

Process Hacker에서 드라이버에 대한 서비스를 만들기만 하면 됩니다. (드라이버를 로드하려면 당연히 관리자 권한이 필요합니다.) 그런 다음 마우스 오른쪽 버튼을 클릭하고 시작하면 됩니다. 네, 정말 그렇게 간단합니다.

docs/processhacker.png

WinObjEx64에서 Device 개체를 볼 수 있습니다.

docs/processhacker.png

FileTest에서 장치를 가지고 놀 수도 있습니다.

docs/filetest.png

docs/filetest2.png

이 세 가지 도구는 모두 훌륭합니다. 특히 PH와 FileTest는 더욱 그렇습니다. 이들은 스위스 군용 칼과 같아서 모든 Windows 리버서의 도구 상자에 있어야 합니다. 예를 들어, 제가 이해하기로 Jonas L은 FileTest에서 장난치다가 수많은 Windows LPE 취약점을 발견했습니다. Windows에는 정말로 놀기에 좋은 훌륭한 도구들이 있습니다. Linux에도 이런 것이 있었으면 좋겠습니다.

"보안" 우회

Rweverything에는 파라미터로 ioctl을 받는 ioctl이 있습니다. 이 내부 ioctl은 수행할 작업(메모리 읽기, 메모리 쓰기, MSR 읽기 등)과 소스 주소, 대상 주소 등의 작업별 파라미터의 공용체(union)를 제어합니다. 두 드라이버의 코드를 비교해 보면:

docs/comparison.png

🤔🤔🤔🤔🤔🤔🤔🤔🤔

그럼에도 불구하고 드라이버는 모든 ioctl 호출이 하드코딩된 AES 키로 제대로 암호화되도록 요구함으로써 난독화를 통한 보안(security by obscurity)에 어설픈 시도를 합니다. (약간 정리한) 코드는 다음과 같습니다:

root@kitploit:~
if ( IoControlCode == 0x22EC00 )
{

  char enc_key[32];
  memset(enc_key, 0, sizeof(enc_key));
  memmove(enc_key, "C110DD4FE9434147B92A5A1E3FDBF29A", 32ui64);
  memcpy(enc_key + 13, ioctl_args->key, 16);
  
  size_t cb_decrypted = 0;
  my_decrypted_cmd* decryptedCmd = NULL;
  DWORD iv_size = ioctl_args->iv_size;
  DWORD input_size = *(DWORD*)(ioctl_args + bufferLen - 6);

  // really just calls BCrypt API to get an AES implementation
  if ( (unsigned int)decrypt_ioctl_params(enc_key, 32, ioctl_args->iv, iv_size, ioctl_args + bufferLen - input_size - 6, input_size, &decryptedCmd, &cb_decrypted) )
  {
    // Decryption failed
    if ( decryptedCmd )
      ExFreePoolWithTag(decryptedCmd, 0);
    irp->IoStatus.Status = 0xC000000D;
    goto Fail_Out;
  }
  IoControlCode = decryptedCmd->opcode;
  Rweverything_Args = &decryptedCmd->args;
}
else // not 0x22EC00
{
  if ( IoControlCode != 0x22E858 &&
       IoControlCode != 0x22E860 &&
       IoControlCode != 0x22E800 &&
       IoControlCode != 0x22E804 )// whitelisted control codes
    IoControlCode = 0; // block everything else
}

드라이버는 일부 PMIO를 수행하는 지루한 작업을 허용하지만, "재미있는" 제어 코드는 모두 이 암호 해독 루틴 뒤에 가로막혀 있습니다. 이 명시적인 허용 목록에도 불구하고 여전히 위험한 Rweverything 기능이 모두 포함되어 있습니다. 이 위험한 기능들을 숨기는 대신 아예 제거했어야 했을 것입니다.

또한 흥미롭게도, 사용자가 키의 일부를 지정할 수도 있습니다(???). 이유는 전혀 모르겠습니다. 그저 코드가 매우 형편없게 작성되었습니다.

어쨌든, 이 이상한 암호화 API를 사용하고 원하는 임의의 ioctl 호출을 전달하는 클라이언트 코드를 작성하는 것은 비교적 쉽습니다. 자세한 내용으로 여러분을 지루하게 만들지는 않겠습니다.

드라이버와 통신하기

드라이버에 대한 핸들을 열고 DeviceIoControl을 사용하여 ioctl을 호출합니다. 아주 표준적인 방식입니다.

root@kitploit:~
HANDLE hDevice = CreateFileA("\\\\.\\GlobalRoot\\Device\\AsrDrv104", GENERIC_READ | GENERIC_WRITE | SYNCHRONIZE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

// ... set up the encrypted ioctl data

BOOL result = DeviceIoControl(hDevice, 0x22EC00, ioctl_data, in_buf_size, out_buf, sizeof(out_buf), &bytes_returned, NULL);

이제 드라이버의 숨겨진 Rweverything 부분과 통신할 수 있게 되었으니, 처음 해보고 싶었던 것은 드라이버 클라이언트가 작동하는지 알기 위해 크래시를 유발하는 것이었습니다.

가장 간단한 방법은 CR3를 쓰레기 값으로 덮어쓰는 것입니다. 이 글을 읽는 여러분 중 일부는 초보자일 거라는 것을 알고 있고, 괜찮습니다. 그래서 자세히 설명하겠습니다. 저도 멍청하니까 이게 여러분의 학습에 도움이 될지도 모릅니다. 무엇을 하고 있는지 안다면 건너뛰어도 됩니다.

x86에서 페이징이 활성화되면(현대 운영 체제에서는 거의 항상 활성화됩니다), CR3는 최상위 페이지 테이블 디렉터리의 물리적 베이스 주소를 가리킵니다. 그것이 무엇을 의미하는지 모르겠다면 가상 메모리에 대한 Wikipedia 문서를 읽어보세요.

CR3를 쓰레기 값, 예를 들어 0x0000000000000000으로 덮어쓰면 TLB가 플러시되고, 다음 명령을 실행하려고 시도할 때 프로세서(구체적으로 MMU)는 명령 포인터를 물리 주소로 변환하려고 합니다. 주소 변환은 본질적으로 CR3에서 시작하는 일련의 페이지 테이블 워크(walk)로 생각할 수 있습니다. 이제 CR3는 0의 물리 메모리를 가리키며, 이는 실제로 존재하고 접근 가능합니다. 그러나 그것이 유효한 페이지 테이블일 가능성은 극히 낮습니다. (페이지 테이블 엔트리, 줄여서 PTE는 따라야 할 특정 구조를 가지고 있습니다.)

이렇게 되면 주소 변환 중 페이지 폴트(page fault)가 발생합니다. 일반적으로 CPU는 페이지 폴트 핸들러의 주소로 우리를 데려갈 것입니다. 그런데 페이지 폴트 핸들러 함수가 어디에 있는지 어떻게 알 수 있을까요? 이것은 IDT(Interrupt Descriptor Table)로 알려진 메모리 데이터 구조에 저장됩니다. 프로세서에는 IDT의 가상 주소를 보관하는 레지스터(sidt 및 lidt 명령으로 읽고 씀)가 있습니다. 이제 문제가 보이시나요? 페이지 폴트를 처리하려면 먼저 또 다른 가상 메모리 접근, 즉 또 다른 주소 변환을 수행해야 합니다.

물론 두 번째 주소 변환도 폴트가 발생합니다. 이제 첫 번째 페이지 폴트를 처리하는 동안 발생한 폴트인 Double Fault(이중 오류)가 발생합니다. 이것은 꽤 심각하지만 여전히 복구가 가능합니다. 프로세서가 마지막 복구 기회를 한 번 더 줍니다. 물론 이 시도 역시 세 번째이자 마지막 페이지 폴트인 Triple Fault(삼중 오류)로 무참히 끝납니다. 이 시점에서 CPU는 포기하고 머신을 하드 리셋합니다. 물리 머신에서 이 절차를 수행했다면, 지금쯤 BIOS 스플래시 화면을 보게 될 것입니다.

이제 질문이 있다면, 저에게 항상 주어졌던 것과 같은 답변을 드리겠습니다. 바로 Intel Manual Volume 3A를 읽으라는 것입니다. (일명 성경).

드라이버 익스플로잇하기

좋아요, 이제 실제로 드라이버를 어떻게 익스플로잇할까요? 주변을 살펴보면, 임의 물리 메모리 읽기/쓰기 프리미티브를 볼 수 있습니다. 기본적으로 MmMapIoSpace를 사용해 원하는 물리 주소를 매핑하고, 버퍼를 해당 주소로 복사하거나(또는 그 반대로) 주소의 매핑을 해제합니다.

작은 참고: 커널 디버거가 연결된 상태에서 우리처럼 어리석은 인자로 MmMapIoSpace를 호출하려 하면 버그체크가 발생합니다. WinDbg에서 매직 바이트를 쓰면 이를 우회할 수 있습니다. 자세한 내용은 exploit.cpp에서 MiShowBadMapper를 참조하는 주석을 찾아보세요. 저는 그게 뭔지 잘 모르고, 사실 알고 싶지도 않습니다.

이 프리미티브를 활용하여 커널에서 코드 실행을 얻으려면 어떻게 해야 할까요? 이 프리미티브의 주요 문제는 물리 메모리에서 작동한다는 것입니다. 사용자 모드 프로그램으로서 우리는 물리 메모리의 레이아웃이 어떤지 전혀 알 수 없습니다. 운영 체제가 그 모든 것을 처리해 주기 때문입니다. 일부 커널 데이터 구조나 커널 함수 포인터의 가상 주소를 얻을 수 있다고 해도, 그것들이 물리 주소 공간의 어디에 있는지는 전혀 알 수 없습니다.

한 가지 아이디어는 CR3를 읽고, 페이지 테이블을 읽고, 가상 주소 변환을 직접 수행하는 것입니다. 훌륭한 아이디어입니다. 하지만 작동하지 않습니다. Windows가 더 이상 MmMapIoSpace로 페이지 테이블을 매핑하는 것을 허용하지 않기 때문입니다. 그래서 더 영리해져야 합니다.

저는 VDM에서 xeroxz의 기술을 사용했습니다. 상당히 간단하지만, 그 기술은 꽤 영리합니다. 물리 메모리의 레이아웃을 알지 못하더라도, 원하는 것을 찾을 때까지 모든 물리 메모리를 스캔할 수 있습니다. 활용할 수 있는 한 가지는 페이지 내용이 물리적으로나 가상으로나 항상 동일하다는 것입니다. 페이지 경계에 대한 모든 오프셋은 항상 보존됩니다. 예를 들어, 페이지 0x7fff000000000XXX가 물리 프레임 0x0000000123456XXX에 매핑되어 있다면, 모든 주소의 XXX는 물리 주소와 가상 주소 모두에서 동일합니다. 페이지 내부의 모든 구조가 보존됩니다. 따라서 덮어쓰고 싶은 흥미로운 페이지를 스캔할 수 있습니다.

덮어쓰기 가장 쉬운 대상은 아마도 쉽게 도달할 수 있는 syscall이나 ioctl 핸들러일 것입니다. Windows에는 컴퓨터를 비프음 나게 하는 표준 Beep() 함수가 있습니다. 믿거나 말거나, 이것은 Beep 장치를 제공하는 드라이버인 Beep.sys에 구현되어 있습니다. (사실 이전의 WinObjEx64 스크린샷에서 볼 수 있습니다.) 누구나 Beep 장치를 사용할 수 있고, 거의 호출되지 않습니다. 그럼 Beep ioctl 핸들러를 덮어씁시다.

Beep.sys를 IDA에 넣고 DeviceIoControl 핸들러를 확인할 수 있습니다.

docs/beep.png

페이지 오프셋 0x270에 바이트가 40 53 48 ...인 이 코드가 있습니다. 이 바이트들은 재배치되지 않았으므로 이 함수를 스캔하는 것은 매우 쉽습니다. 만약 재배치된 바이트가 있었다면 와일드카드로 처리해야 했을 것입니다. 게임 핵을 작성할 때의 시그니처 스캐닝과 같은 아이디어입니다.

그래서 물리 메모리를 스캔하여 이 코드를 찾은 후에는 우리만의 셸코드로 덮어쓰기만 하면 됩니다. 또한 물리 메모리에 이 페이지의 복사본이 여러 개 존재할 수 있으므로 주의해야 합니다(!). 그러니 모든 복사본을 찾으세요.

이 시점에서 우리는 프로세스의 보안 토큰을 시스템 프로세스의 토큰과 교체하여 nt authority\system 권한을 얻음으로써 꽤 쉽게 권한을 상승시킬 수 있습니다. 안타깝게도 Asrock 드라이버는 어차피 열려면 관리자 권한이 필요하므로 별로 흥미롭지 않습니다.

우리의 경우, 스테이지 2 페이로드를 할당하고 복사한 다음 새 커널 스레드를 생성하는 기본 셸코드를 작성합니다. 덮어쓴 Beep 핸들러에서 모든 것을 할 수는 없습니다. 1) 페이지 1개로 제한되고, 2) Beep 장치의 나머지 코드도 망가뜨렸기 때문에 Beep 장치에 대한 핸들을 닫으려고 하면 시스템이 크래시되기 때문입니다. 커널 포인터를 얻는 것은 사실 쉽습니다. NtQuerySystemInformation이 정중하게 요청하면 공짜로 제공해 주기 때문입니다.

root@kitploit:~
__int64 __declspec(dllexport) __fastcall MyIRPHandler(struct _DEVICE_OBJECT* a1, IRP* irp)
{
    MyIrpStruct* user_data = (MyIrpStruct*)irp->AssociatedIrp.SystemBuffer;

    void* my_rwx = user_data->nt_ExAllocatePoolWithTag(NonPagedPoolExecute, user_data->payload_size, 'lmao');

    user_data->nt_memcpy(my_rwx, user_data->payload, user_data->payload_size);

    HANDLE hThread;
    user_data->nt_PsCreateSystemThread(&hThread, THREAD_ALL_ACCESS, NULL, NULL, NULL, (PKSTART_ROUTINE)my_rwx, NULL);

    user_data->nt_IofCompleteRequest(irp, 0);

    return 0;
}

그래서 우리는 Beep를 빠르게 패치하고, 덮어쓴 ioctl 핸들러를 호출하고, Beep의 패치를 해제합니다. 이제 시스템의 다른 것을 망가뜨리지 않고 우리 코드를 실행하는 커널 스레드를 안전하게 생성했습니다. 이 시점에서 우리만의 드라이버를 매핑하거나 무엇이든 할 수 있습니다.

결론

저는 형편없는 보안 연구원이며, 우연히 쓸모없는 버그만 찾습니다. 모두 읽어주셔서 감사합니다. 제 OnlyFans를 구독해 주세요.

도구 다운로드