좀 열심히 쓴 글

RDP Client fuzzer 개선 보고서

ch4rli3kop 2020. 6. 14. 18:00
반응형

[BoB8] RDP Client fuzzer 개선 보고서2 - 박찬희

Index

  1. 환경 구성

  2. Audio Output Virtual Channel Extension fuzzing 과정

  3. 타겟 선정

  4. File System Virtual Channel Extension fuzzer 구현

  5. 결과

# 환경 구성

  • Windows 7 Professional 64bit

  • mstsc.exe

  • mstscax.dll


# Audio Output Virtual Channel Extension fuzzing 과정

오디오 출력과 관련된 기능은 다음 세가지 단계를 거칩니다.

  1. Initialization Sequence

  2. Data Transfer Sequences

  3. Audio Setting Transfer Sequences

첫 번째 과제로 구현한 퍼저는 이 부분 중 가장 첫 번째인 초기화 과정에서 퍼징을 시도하는 방식이었습니다.

## Initialization Sequence

초기화 단계는 다음과 같은 과정으로 수행됩니다.

가장 처음으로 클라이언트와 서버 사이에서 원활한 통신을 위해 다음과 같이 프로토콜의 버전과 사용할 수 있는 기능들에 대한 정보 교환을 시도합니다.

맨 처음 서버에서 클라이언트로 버전 정보와 지원하는 오디오 포멧에 대한 정보를 보내기 위해 SERVER_AUDIO_VERSION_AND_FORMATS PDU를 보내게 되는데, 앞서 구현했던 퍼저의 경우 이 부분에 대해 공격을 시도했습니다.

Server Audio Formats and Version PDU 의 구조는 다음과 같습니다.

헤더는 RDPSND PDU에서 사용하는 공통적인 헤더인데 다음과 같은 구조로 되어있습니다.

Server Audio Formats and Version PDU 의 경우 msgType이 반드시 SNDC_FORMATS (0x07)가 되어야하기 때문에, 해당 영역과 BodySize만 맞춰서 퍼징을 시도했습니다.

# 타겟 선정

첫 과제로 구현한 퍼저의 사례를 기반으로 퍼징 구현에 용이한 조건들을 고려하였습니다. 해당 조건들을 기반으로 퍼징을 시도할 다른 공격 벡터를 찾기위해 우선순위를 선정하였습니다.

  1. Server -> Client 과정이 앞설수록 서버에서 클라이언트로 데이터를 보냄으로써 퍼징을 시도해야 하는데, 퍼징을 시도하는 데이터 교환 이전에, 서버와 클라이언트 간의 통신이 존재한다면 해당 데이터 교환까지 구현해야하는 불편함이 있습니다. 따라서 Server Audio Formats and Version PDU와 같이 가장 첫번째로 서버가 클라이언트에게 보내는 PDU를 가장 높은 우선순위를 주었습니다.

  2. 데이터의 규격이 일정하게 정해진 것보다는 가변적인 것을 우선으로 퍼징을 시도하는 데이터의 규격이 일정한 것보다 가변적으로 변하는 경우(필드에 body size가 포함되는 경우)가 크래시가 발생할 확률이 더 높다고 판단하였습니다. 데이터의 규격이 일정하게 정해진다면, 코드 상에서 if, switch case 등으로 예외처리가 쉽게 될 것이라고 생각하였기 때문에, 가변적인 경우를 우선순위에 두었습니다.

  3. 통신의 시작을 서버가 하는 것 클라이언트에서 통신을 시작하는 경우라면 어떻게 구현을 할 수가 없는 것 같으므로...

  4. UDP 통신보다는 VC(Virtual Channel) 통신을 우선으로 클라이언트의 결정에 따라서 VC 혹은 UDP로 PDU 교환을 하게 되는데, 테스트 환경에서 확인해본 결과, UDP 데이터 교환이 나타나지 않아, 데이터 교환의 방식으로 VC를 선택한다고 판단하였습니다. 따라서 VC 통신을 UDP 통신보다 우선순위에 놓았습니다. (UDP 통신의 경우 통신에 앞서 클라이언트와 통신에 이용할 포트 번호 교환 등의 작업이 있다는 이유도 있습니다.)

위의 우선 순위를 바탕으로 기존 Audio Output에서 다른 공격 벡터를 찾으려 했지만, 멘토님이 선정하신 타겟 외에 사전 통신(initialization)없이 첫 번째로 서버에서 클라이언트로 데이터를 보내는 과정은 존재하지 않는 것 같아 다른 채널들 중에서 찾아보기로 하였습니다.

다음은 분석한 채널들입니다.

## Basic Connectivity and Graphics Remoting Specification

클라이언트가 먼저 보내므로 Fail

## Dynamic Channel Virtual Channel Extension

데이터 교환 전에 위의 Basic Connectivity and Graphics Remoting Specification의 connection sequence가 완료되어야하기 때문에 Fail

## Print Virtual Channel Extension

File System Virtual Channel Extension 을 사용합니다. Fail


## File System Virtual Channel Extension

처음에는 Server Announce Request를 타겟으로 하려 했으나, 앞선 퍼저 구현의 사례를 볼 때 Capability가 가변적인 데이터이므로 퍼징을 진행할 시 유의미한 결과를 얻을 확률이 높아보여, Server Core Capability Request를 대상으로 선정하였습니다.

보내는 데이터를 자세히 보면 다음과 같습니다.

CapabilityMessage 부분은 다음과 같습니다.

# File System Virtual Channel Extension fuzzer 구현

위에서 언급했다시피 File System Virtual Channel Extension의 initial 과정은 다음과 같습니다.

여기서 직접 보내줘야할 부분은 Server Announce Request, Server Core Capability Request 입니다. 첫 번째는 직접 만들어서 보내주어야 하기 때문에 해당 데이터의 포맷들을 살펴볼 필요가 있습니다.

File System Virtual Channel Extension의 경우 "RDPDR"로 설정해야 합니다.

## Server Announce Request


Header는 다음과 같습니다.

VersionMinor

Server Announce Request에 대한 example도 존재합니다.

예시에 맞춰 보내면 응답이 제대로 오는 것을 확인할 수 있습니다.

## Server Core Capability Request

헤더는 주어진 값(\x72\x44\x50\x53)으로 잘 맞춰줍니다.

Server Core Capability Request에 대한 example을 참고하여 seed 파일을 생성할 수 있습니다.

## fuzzer 구현하기

주어진 헤더 타입(\x72\x44\x50\x53)으로 잘 맞춘 뒤, 앞서 구현했었던 퍼저와 똑같은 방식으로 데이터를 보내면 됩니다.

python 서버와 Cpp 라이브러리는 다음과 같습니다.

### python server
import socket, ctypes
import time

vcdll = ctypes.WinDLL(".\\virtual_channel_library2.dll")
trData = vcdll['protocolInitialize']

sc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sc.bind( ("192.168.41.134", 55555) )
sc.listen(5)

print("Server Start!!")
conn, addr = sc.accept()

time.sleep(20)

i = 0
while True:
   print("id" + str(i))
   data = conn.recv(4)
   print(data)
   if len(data) > 0:
       length = int.from_bytes(data, "little" )
       print(length)
       data = conn.recv(length)
       if len(data) > 0:
           data = b'\x72\x44\x50\x53' + data
           print(data)
           try:
               trData(data, length)
           except:
               a = 0
               #print("..")
       i+=1
### cpp library
/*
By ch4rli3kop
Remote Desktop Protocol: File System Virtual Channel Extension
----------------------------------
 Server Announce Request ->
 <- Client Announce Reply
 <- CLient Name Request
 Server Core Capability Request ->
----------------------------------
*/

#include <stdio.h>
#include <Windows.h>
#include <WtsApi32.h>

#pragma comment(lib, "wtsapi32.lib")

extern "C" __declspec(dllexport) void _stdcall protocolInitialize(char* data, int len);

void _stdcall protocolInitialize(char* data, int len)
{
HANDLE virtual_channel_handle;
char buffer[0x100];
unsigned long buf_size;
long count_buf;
long received_buf;

try {
while (true) {
virtual_channel_handle = WTSVirtualChannelOpen(WTS_CURRENT_SERVER_HANDLE, WTS_CURRENT_SESSION, (LPSTR)"RDPDR");
if (!virtual_channel_handle) {
fprintf(stderr, "virtual channel handle open error");
continue;
}

char buf[] = "\x72\x44\x6e\x49\x01\x00\x0c\x00\x01\x00\x00\x00";
if (!WTSVirtualChannelWrite(virtual_channel_handle, buf, 12, (PULONG)&count_buf)) {
fprintf(stderr, "virtual channel write1 error");
continue;
}
printf("Server Announce Request ->\n");

if (!WTSVirtualChannelRead(virtual_channel_handle, 2000, buffer, 0x100, (PULONG)&received_buf)) {
fprintf(stderr, "virtual channel read1 error");
continue;
}
printf(" <- Client Announce Reply\n");


if (!WTSVirtualChannelRead(virtual_channel_handle, 0, buffer, 0x100, (PULONG)&received_buf)) {
fprintf(stderr, "virtual channel read2 error");
continue;
}
printf(" <- CLient Name Request\n");

if (!WTSVirtualChannelWrite(virtual_channel_handle, data, len, (PULONG)&count_buf)) {
fprintf(stderr, "virtual channel write2 error");
continue;
}
printf("Server Core Capability Request ->\n");

if (!WTSVirtualChannelClose(virtual_channel_handle)) {
fprintf(stderr, "virtual channel close error");
}
break;
}
}
catch (...) {
printf("Error!\n");
//transferData(data, len);
}
}

통신의 데이터가 서로 연관성을 지니기 때문에, 통신 과정에서 하나라도 오류가 발생할 경우 처음부터 다시 핸들을 가져와서 진행하도록 하였습니다.

### afl-fuzz.c
void sendData(char* data, int len) {
char buf[4] = "";
memcpy(buf, (char*)&len, 4);
send(sc, buf, 4, 0);
send(sc, data, len, 0);
}

static void write_to_testcase(void* mem, u32 len){
  ... 중략
   ck_write(fd, mem, len, out_file);
   sendData(mem, len);
  ... 중략
}

....
   
void connection() {
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
fprintf(stderr, "WSAStartip() error");
exit(1);
}

sc = socket(PF_INET, SOCK_STREAM, 0);
if (sc == INVALID_SOCKET) {
fprintf(stderr, "socket open error");
exit(1);
}

servAddr.sin_family = AF_INET;
servAddr.sin_port = htons(PORT);
inet_pton(AF_INET, SERVER_IP, &(servAddr.sin_addr.s_addr));

if (connect(sc, (SOCKADDR*)&servAddr, sizeof(servAddr)) < 0) {
fprintf(stderr, "socket connect error");
exit(1);
}
}

int main(int argc, char** argv) {

connection();
  ... 중략
}

# 결과

example에 있던 예시를 testcase로 주었음에도 불구하고 자꾸 아래와 같은 에러가 나네요.. 신속히 수정해보겠습니다..

[+] 추가

과제 기간이 연장된만큼 기존 아쉬웠던 점들에 대해 개선한 내용입니다. 다음 두 가지 항목을 추가하였습니다.

  1. Server Core Capability Request 개선

  2. Server Drive I/O Request 추가


# Server Core Capability Request 개선

앞서 구현했었던 퍼저의 경우 Server Core Capability Request를 대상으로 진행하였습니다.

가끔 비정상적인 데이터를 보내는 경우에 Virtual Channel을 닫아도 마치 연결이 계속 유지되는 것처럼 다음 시도에 Virtual Channel Write에 오류가 생기는 현상이 있었는데, 해당 이슈에 대한 해결 방안입니다.

Server Core Capability Request는 다음과 같은 데이터 구조를 갖습니다.

이 때 보이는 numCapabilities는 CapabilityMessage array의 크기를 나타내는데, 이 데이터를 제대로 맞춰주어야 합니다. 또한, Capability Message의 헤더에서 보내는 데이터 크기에 맞춰 CapabilityLength 역시 맞춰주어야 정상적으로 채널을 종료시킬 수 있습니다. (추정컨데 사이즈 필드에 존재하는 데이터만큼 입력을 계속 대기받는 형태인 것 같습니다.)

CapabiltiyMessage는 5가지의 타입이 존재합니다.


해당 타입 별로 메세지의 데이터의 크기가 정해져 있기 때문에, 각각의 타입별로 퍼징을 시도할 수 있을 것 같습니다. 각 타입별로 유사하기 때문에, 서버에서 추가적으로 붙여주는 바이트와 testcase만 다르게 넣어주면 될 것 같습니다.

다음은 General Type에 대한 python 서버 코드의 예시입니다.

import socket, ctypes
import time

vcdll = ctypes.WinDLL(".\\virtual_channel_library2.dll")
trData = vcdll['protocolInitialize']

sc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sc.bind( ("192.168.41.134", 55555) )
sc.listen(5)

print("Server Start!!")
print(" [ File System Capability General Type ] ")
conn, addr = sc.accept()

time.sleep(40)

captype = b'\x01\x00'   # CAP_GENERAL_TYPE
caplength = b'\x2c\x00' # CapabilityLength

i = 0
while True:
   print("id" + str(i))
   data = conn.recv(4)
   print(data)
   if len(data) > 0:
       length = int.from_bytes(data, "little" )
       print(length)
       data = conn.recv(length)
       if len(data) > 0:
           data = b'\x72\x44\x50\x53\x01\x00\x00\x00' + captype + caplength + data
           print(data)
           try:
               trData(data, length + 0xc)
           except:
               a = 0
               #print("..")
       i+=1

captype과 caplength만 수정하면 다른 type에 대해서도 퍼징을 시도할 수 있습니다.

# Server Drive I/O Request 추가

추가적인 실험과 분석을 통해 사용자가 로그인 되어있을 경우 임의로 Server Announce Request만 보내면 User Logged On 과정이 RDP Server를 통해 자동으로 이루어지는 것을 확인하였습니다.

따라서 사용자 임의로 Server Drive I/O Request를 보낼 수 있습니다. 아래의 예시는 Server Drive Create Request를 보낸 사례입니다. server의 request에 맞춰 client의 response가 오는 것을 확인할 수있습니다.


아래와 같이 다양한 동작을 지원하는 만큼 좋은 타겟이라는 생각을 하였습니다.

다음은 I/O Request 중 Server Drive Write Request를 대상으로 만든 퍼저입니다.

## server.py

import socket, ctypes
import time

vcdll = ctypes.WinDLL(".\\virtual_channel_library2.dll")
trData = vcdll['FS_Request']

sc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sc.bind( ("192.168.41.134", 55555) )
sc.listen(5)

print("Server Start!!")
print(" [ File System Server Drive Write Request Fuzz ] ")
conn, addr = sc.accept()

time.sleep(40)

IOreq = b'\x72\x44\x52\x49'
deviceID = b'\x01\x00\x00\x00'
fileID = b'\x23\x02\x00\x00'
completionID = b'\x06\x00\x00\x00'
majorFunc = b'\x04\x00\x00\x00'    # WRITE 0x00000004
minorFunc = b'\x00\x00\x00\x00'


i = 0
while True:
   print("id" + str(i))
   data = conn.recv(4)
   print(data)
   if len(data) > 0:
       length = int.from_bytes(data, "little" )
       print(length)
       data = conn.recv(length)
       if len(data) > 0:
           data = IOreq + deviceID + fileID + completionID + majorFunc + minorFunc + data
           print(data)
           try:
               trData(data, length + 0xc)
           except:
               a = 0
               #print("..")
       i+=1

deviceID, fileID, completionID 등을 대상으로도 퍼징을 진행할 수도 있지만, 우선 Write request에 대해 퍼징을 수행하기 위해 헤더 정보는 임의로 설정하였습니다.

## virtual_channel_library2.cpp

/*
By ch4rli3kop
Remote Desktop Protocol: File System Virtual Channel Extension
----------------------------------
 Server Announce Request ->
 <- Client Announce Reply
 <- CLient Name Request
 Server Core Capability Request ->
 <- Client Core Capability Respone
 ...
 Server Drive I/O Request ->               <-- this is target!
----------------------------------
*/

#include <stdio.h>
#include <io.h>
#include <Windows.h>
#include <WtsApi32.h>

#pragma comment(lib, "wtsapi32.lib")

extern "C" __declspec(dllexport) void _stdcall protocolInitialize(char* data, int len);
extern "C" __declspec(dllexport) void _stdcall test();
extern "C" __declspec(dllexport) void _stdcall FS_Request(char* data, int len);

void printHex(char* data, int len) {
char r;
for (int i = 0; i < len; i++) {
if (i % 16 == 0 && i != 0) printf("\n");
r = data[i];
printf("%X ", r);
}
printf("\n");
}

void _stdcall FS_Request(char* data, int len)
{
HANDLE virtual_channel_handle;
char buffer[0x100];
unsigned long buf_size;
long count_buf;
long received_buf;
int i;
int r;

try {
while (true) {
virtual_channel_handle = WTSVirtualChannelOpen(WTS_CURRENT_SERVER_HANDLE, WTS_CURRENT_SESSION, (LPSTR)"RDPDR");
if (!virtual_channel_handle) {
fprintf(stderr, "virtual channel handle open error");
//break;
continue;
}


printf("Server Announce Request ->\n");
char buf[] = "\x72\x44\x6e\x49\x01\x00\x0c\x00\x04\x00\x00\x00";
if (!WTSVirtualChannelWrite(virtual_channel_handle, buf, 12, (PULONG)&count_buf)) {
fprintf(stderr, "virtual channel write1 error");
//break;
continue;
}
printHex(buf, 12);


//printf(" <- Client Announce Reply\n");
//if (!WTSVirtualChannelRead(virtual_channel_handle, 2000, buffer, 0x100, (PULONG)&received_buf)) {
// fprintf(stderr, "virtual channel read1 error");
//break;
// continue;
//}
//printHex(buffer, received_buf);


//printf(" <- Client Name Request\n");
//if (!WTSVirtualChannelRead(virtual_channel_handle, 2000, buffer, 0x100, (PULONG)&received_buf)) {
// fprintf(stderr, "virtual channel read1 error");
//break;
// continue;
//}
//printHex(buffer, received_buf);

//printf("Server Core Capability Request ->\n");
//char buf2[] = "\x72\x44\x50\x53\x05\x00\x00\x00\x01\x00\x2c\x00\x02\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x01\x00\x0c\x00\xff\xff\x00\x00\x00\x00\x00\x00\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x08\x00\x01\x00\x00\x00\x03\x00\x08\x00\x01\x00\x00\x00\x04\x00\x08\x00\x02\x00\x00\x00\x05\x00\x08\x00\x01\x00\x00\x00";
//if (!WTSVirtualChannelWrite(virtual_channel_handle, buf2, 84, (PULONG)&count_buf)) {
// fprintf(stderr, "virtual channel write1 error");
// //break;
// continue;
//}
//printHex(buf2, 84);


//printf(" <- CLient Core Capability Response\n");
//if (!WTSVirtualChannelRead(virtual_channel_handle, 2000, buffer, 0x100, (PULONG)&received_buf)) {
// fprintf(stderr, "virtual channel read2 error");
// //break;
// continue;
//}
//printHex(buffer, received_buf);


printf("Server Drive Request ->\n");
if (!WTSVirtualChannelWrite(virtual_channel_handle, data, len, (PULONG)&count_buf)) {
fprintf(stderr, "virtual channel write2 error");
//break;
continue;
}
printHex(data, count_buf);


//printf(" <- Client Drive Response\n");
//if (!WTSVirtualChannelRead(virtual_channel_handle, 2000, buffer, 0x100, (PULONG)&received_buf)) {
// fprintf(stderr, "virtual channel read2 error");
// //break;
// continue;
//}
//printHex(buffer, received_buf);


if (!WTSVirtualChannelClose(virtual_channel_handle)) {
fprintf(stderr, "virtual channel close error");
}
break;
}
}
catch (...) {
printf("Error!\n");
//transferData(data, len);
}
}

Server Announce Request를 임의로 전송하였고, Drive Request에서 퍼징을 수행하게 됩니다.

## 결과

반응형