Shellcode初探[1]: 什么是shellcode? 用chatGPT构造简易shellcode
Shellcode初探[1]: 什么是shellcode? 用chatGPT构造简易shellcode
from:https://blog.restkhz.com/post/glance-at-shellcode-1前言
我可能又要开一个系列文, 希望这次不要太监了.
最近正在重新自学一些比较二进制的东西. 一边学一边探索. 并且做一些记录分享给各位.
这个系列可能会需要你具备一点前置知识:
- 一点点的计算机架构知识,比如: 函数是如何被调用的, 栈是什么, 常用寄存器之类的.
- 一点点的操作系统知识.
- 一点点的汇编基础
- 一点点的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栏, 我们便可以分析.
- \x31\xc0:XOR EAX, EAX;将EAX寄存器的值与自身进行异或,将EAX清零。
- \x50:PUSH EAX;将EAX寄存器的值(0)压入栈。
- \x68\x2f\x2f\x73\x68:PUSH 0x68732f2f;将字符串//sh的ASCII编码压入栈。
- \x68\x2f\x62\x69\x6e:PUSH 0x6e69622f;将字符串/bin的ASCII编码压入栈。
- \x89\xe3:MOV EBX, ESP;将栈顶指针(ESP)的值复制到EBX寄存器,这样EBX将指向字符串/bin//sh。
- \x50:PUSH EAX;再次将EAX(0)压入栈,作为execve系统调用参数列表的终止符。
- \x53:PUSH EBX;将EBX的值(指向/bin//sh的指针)压入栈。
- \x89\xe1:MOV ECX, ESP;将栈顶指针(ESP)的值复制到ECX寄存器,这样ECX将指向参数列表的起始位置。
- \x31\xd2:XOR EDX, EDX;将EDX寄存器的值与自身进行异或,将EDX清零。EDX将作为execve系统调用的环境参数传递给内核。
- \xb0\x0b:MOV AL, 0x0B;将0x0B(即11,代表execve系统调用)放入AL寄存器(EAX的低字节)。
- \xcd\x80:INT 0x80;通过中断0x80触发系统调用
逐行理解, 不知道你读懂了没有.
问题来了, 我们如何运行这段shellcode呢?
加载并运行shellcode
我们需要把以上的shellcode放进一段可执行内存, 并且让程序去执行这段内存. 具体点, 就是:
- 在Linux中我们可以用
mmap
让系统为我们分配一小块内存, 标记为可以执行. - 我们可以用
memcpy
把shellcode复制进那块内存. - 定义一个函数指针, 也就是把那块有shellcode的内存作为一个函数.
- 调用上面写好的函数, 让机器去执行那段代码.
然后我们可以用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.