---
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);
```