--- title : 11_Socket API --- # rsctool by CY, Tasi Date : 2021-10-22 --- # 網路程式設計 ## 壹、介紹 網路程式設計(Sokcet)在勒索程式中扮演很重要的角色。勒索程式使用AES + RSA的混合式加密,AES用來加密文件,RSA用來加密AES。RSA屬於非對稱式加密,所以必然存在兩個加密金鑰,而金鑰又不可能存在加密的電腦中,不然加密就沒意義了。金鑰在伺服端與客戶端之間傳送就要靠socket,所以socket可以說是勒索病毒不可或缺的一部份。 --- ## 貳、Sokcet API 網路通訊協定(TCP/IP)的API稱為sokcet,原本只在UNIX系統上運行,microsoft因為使用的通訊協定不同,所以針對windows sockets api比較晚成型,現在使用windows socket還是和原本的api不太一樣,但是稍微修改過還是可以在linux上使用。 --- ## Socket函式 1. 註冊動態連結函式庫 -- WSAStartup 不同於linux平台的socket,windows要先準備相關的動態連結函式庫。 ```cpp= int WSAStartup(WORD wVersionRequired, LPWSADATA lpWSAData); ``` * 參數定義: * wVersionRequired : WORD這個型態不太常見,大小為2 bytes,通常使用MAKEWORD函數合成WORD,方法是MAKEWORD(主版本號, 次版本號)。 * lpWSAData : WSADATA是一個結構,裡面存放DLL執行相關的數據,詳細內容在MSDN都有說明。呼叫完WSAStartup後會將一些資訊放入這個結構中。 * 回傳值: 執行成功回傳0,失敗回傳錯誤代碼。這邊我們實作過程有遇到一個小錯誤,因為我們沒有注意到這個函數是直接回傳錯誤代碼,而不是透過WSAGetLastError取得錯誤代碼。大部分wocket函式出錯會回傳SOCKET_ERROR,之後再使用WSAGetLastError取得錯誤資訊,但是不知道為什麼這個函數直接回傳錯誤代碼。 * 使用片段: ```cpp= WORD wVersionRequired; WSADATA wsaData; int err; wVersionRequired = MAKEWORD(2, 2); err = WSAStartup(wVersionRequired, wsaData); if (err != 0) { printf("WSAStartup failed with error: %d", err); return 1; } ``` --- 2. 網路位址及通訊埠的轉換 -- getaddrinfo(DNS查詢) 在做連結時需要兩個東西,一個是主機名或IP,另一個是要使用的服務或通訊埠號,而服務會占用幾個特定的埠號,例如HTTP服務的通訊埠號為80。 1. DNS(Domain Name System): 因為全世界的網址很多,所以會根據規則區分成很多網域(Domain),並且是從上到下的層級關係。分別為 : Root Domain, Top Level Domain, Second Level Domain, Host Domain,詳細內容還蠻複雜的,DNS查詢就是在這之間進行的,以台大www.ntu.edu.tw為例,查詢步驟為: 1. 向DNS伺服器查詢www.ntu.edu.tw的ip位址。 2. DNS查看記憶體,如果有被查詢過就回傳紀錄,如果沒有就會往Root Domain。 3. Root Domain 的 DNS 伺服器回傳 "管理tw的DNS伺服器的ip位址"。 4. 轉向管理tw的伺服器位址,然後伺服器回傳 "管理edu.tw的DNS伺服器位址"。 5. 轉向管理edu.tw的DNS伺服器位址,然後伺服器回傳"管理ntu.edu.tw的DNS伺服器位址"。 6. 轉向管理ntu.edu.tw的伺服器後,就會得到要查詢的ip了。 以下使用getaddrinfo查詢www.google.com的ip位址。 ```cpp= #include <stdio.h> #include <stdlib.h> #include <string.h> #include <WinSock2.h> #include <WS2tcpip.h> #include <Windows.h> #pragma comment (lib, "Ws2_32.lib"); #define BUF_SIZE 500 int main(int argc, char* argv[]) { WORD wVersionRequired = MAKEWORD(2, 2); WSADATA wsaData; int err; err = WSAStartup(wVersionRequired, &wsaData); if (err != 0) exit(EXIT_FAILURE); struct addrinfo hints; struct addrinfo* result, * rp; int sfd, s, j; size_t len; size_t nread; char buf[BUF_SIZE]; struct sockaddr_in* ipv4; struct sockaddr_in6* ipv6; /* Obtain address(es) matching host/port */ memset(&hints, 0, sizeof(struct addrinfo)); hints.ai_family = AF_UNSPEC; /* Allow IPv4 or IPv6 */ hints.ai_socktype = SOCK_STREAM; hints.ai_flags = AI_PASSIVE; hints.ai_protocol = IPPROTO_TCP; s = getaddrinfo("www.google.com", "80", &hints, &result); if (s != 0) { printf("%d", s); exit(EXIT_FAILURE); } for (rp = result; rp != NULL; rp = rp->ai_next) { switch (rp->ai_family) { case AF_INET: ipv4 = (struct sockaddr_in*)rp->ai_addr; inet_ntop(rp->ai_family, &ipv4->sin_addr, buf, sizeof(buf)); break; case AF_INET6: ipv6 = (struct sockaddr_in6*)rp->ai_addr; inet_ntop(rp->ai_family, &ipv6->sin6_addr, buf, sizeof(buf)); break; } printf("[IPv%d]%s\n", rp->ai_family == AF_INET ? 4 : 6, buf); } /* No longer needed */ freeaddrinfo(result); exit(EXIT_SUCCESS); } ``` 回到函數定義: ```cpp= INT WSAAPI getaddrinfo(PCSTR pNodeName, PCSTR pServiceName, const ADDRINFOA *pHints, PADDRINFOA *ppResult); ``` * 參數定義 : * pNodeName : 可以是名稱或IP * pServiceName : 服務名稱或埠號,例如"http", "80" * pHints : 這是一個addrinfo結構,要事先填上一些值,例如要使用的socktype、要用ipv4或 ipv6等等。在填入值之前要先將結構清空,如果沒有確保其他位置為0會造成錯誤。 * ppResult : 這是指向指標的指標,原因是addrinfo本身是一個linked list,所以要將他以參數的型式傳遞要使用指標的指標。而函數的結果就是存在這裡。 * 回傳值 : 成功回傳0,失敗直接回傳錯誤代碼。錯誤代碼可以到Windows Sockets Error Codes查詢錯誤原因。 * addrinfo : 這是一個結構,裡面存放連線時需要的參數,定義如下: ```cpp= typedef struct addrinfo { int ai_flags; int ai_family; int ai_socktype; int ai_protocol; size_t ai_addrlen; char *ai_canonname; struct sockaddr *ai_addr; struct addrinfo *ai_next; } ADDRINFOA, *PADDRINFOA; ``` 另外有一個配合的函數freeaddrinfo,用來把getaddrinfo取得的linked list資料釋放。參數只有一個,就是getaddrinfo的ppResult。 --- 3. 開啟socket api -- socket 如同CreateFile()那類的函數,要取得内核物件的HANDLE一樣。socket回傳的SOCKET就如同HANDLE。 ```cpp= SOCKET WSAAPI socket(int af, int type, int protocol); ``` * 參數定義: * af : mircosoft說明文件列出很多address family,但我沒有全部了解,常用到的三個: | 名稱 | 解釋 | | -------- | -------- | | AF_UNSPEC | 不管是IPV4或IPV6都可以 | | AF_INET | 使用IPV4 | | AF_INET6 | 使用IPV6 | * type : 指定要連線的型態,在AF_INET下有兩個可以選擇: | 名稱 | 解釋 | | -------- | -------- | | SOCK_STREAM | 可以確保內容準確無誤,並且依照傳送順序,但是速度較慢 | | SOCK_DGRAM | 不一定可以完整地傳送,並且沒有傳送順序,但是速度較快 | * protocol : 放0代表要根據上面type參數使用預設值。 * 回傳值 : 成功回傳SOCKET,失敗回傳INVALID_SOCKET,使用WSAGetLastError取得錯誤代碼,再參照mirosoft列出的錯誤進行排除。 --- 4. 設定socket選項 -- setsockopt socket裡面有很多選項,我也沒有全部都熟悉,但是如果要更改就要使用這個函數,其中一個用法是用來設定通訊埠,使它可以重複使用。 ```cpp= int setsockopt(SOCKET s, int level, int optname, const char *optval, int optlen); ``` * 參數定義 : * s : 由socket()取得的SOCKET * level : 因為要設定SOCKET的值,所以使用SOL_SOCKET,另外還有不同的值可以設定 * optname : 各種操作的名稱,有很多,無法一一列舉,而我用到的是SO_REUSEADDR,如果不使用這個,在與伺服器段開連線的兩分鐘內,如果要重新連上伺服器,會出現"Address already in use"的錯誤,如果加上這個,可以讓伺服器立刻重啟。 * *optval : 可能指向整數,或是一個結構。 * optlen : 選項的值佔記憶體大小。從這邊可以知道選項的值是由optval 與 optlen 配成,再根據optname解釋這兩個參數的意義。 * 回傳值 : 成功回傳SOCKET,失敗回傳SOCKET_ERROR,使用WSAGetLastError取得錯誤代碼,再參照mirosoft列出的錯誤進行排除。 * 使用片段 : ```cpp= DWORD optval = 1; INT iResult = setsockopt(ListenSocket, SOL_SOCKET, SO_REUSEADDR, (char* ) &optval, sizeof(optval)); ``` --- 5. 綁定通訊埠 -- bind 以google map來說,再出發前往目的地之前,要先查到目的地的地址,這個步驟由前面的getaddrinfo完成,而為什麼有辦法查到這個地址?是因為這個目的地先前有在map上註冊,如果沒有註冊就無法查詢。bind的動作就是註冊的動作。 ```cpp= int bind(SOCKET s, const sockaddr *addr, int namelen); ``` * 參數定義 : * s : 呼叫socket()取得的SOCKET * addr : 這裡要放的是位址,而取得位址的方式是使用getaddrinfo後,addrinfo裡面的ai_addr。 * namelen : 位址的長度,同樣可以用addrinfo裡的ai_addrlen。 * 回傳值 : 成功回傳0,失敗回傳SOCKET_ERROR,使用WSAGetLastError取得錯誤代碼,再參照mirosoft列出的錯誤進行排除。 * 使用片段 : ```cpp= iResult = bind(ListenSocket, result->ai_addr, (int)result->ai_addrlen); if (iResult == SOCKET_ERROR) { printf("bind failed with error: %d\n", WSAGetLastError()); freeadrinfo(result); closesocket(ListenSocket); WSACleanup(); return 1; } ``` --- 6. 設定等候連線的queue -- listen 這個queue的用意是等待,因為伺服器一次只能服務一個,那超過一個的ip我們還是希望可以成功連線,所以就需要這個queue,每次伺服器結束一個服務就可以queue取出另一個器續服務。 ```cpp= int WSAAPI listen(SOCKET s, int backlog); ``` * 參數定義 : * s : 伺服器呼叫socket()回傳的SOCKET * backlog : 設定queue長度最大值,也就是做多有幾個等待中的連線。通常設定成5,至於原因是什麼我還沒想出來... * 回傳值 : 成功回傳0,失敗回傳SOCKET_ERROR,使用WSAGetLastError取得錯誤代碼,再參照mirosoft列出的錯誤進行排除。 * 使用片段 : ```cpp= iResult = listen(ListenSocketm SOMAXCONN); if (iResult == SOCKET_ERROR) { printf("listen failed with error: %d\n", WSAGetLastError()); closesocket(ListenSock); WSACleanup(); return 1; } ``` 其中SOMAXCONN是定義在Winsock2.h裡的巨集。 ```cpp= #define SOMAXCONN 0x7fffffff #define SOMAXCONN_HINT(b) (-(b)) ``` --- 7. 客戶端連線 -- connect 從getaddrinfo一直到被伺服端從queue取出的過程都叫做connect。因為上面說過,addrinfo是一個linked list,所以通常是使用迴圈從頭開始掃描,一直到成功連線。 ```cpp= int WSAAPI connect(SOCKET s, const sockaddr *name, int namelen); ```