23-10-23

本周学习总结

本周开始恶补Pwn基础,一边学一边打招新赛。

Pwn入门教程地址为:

https://www.bilibili.com/video/BV1854y1y7Ro?p=1&vd_source=46ff77891392a04a7d9d93e9037151db

全局常量声明:新手上路,文章内容仅是由教程观点和自己总结获得,仅供参考。

第一周(视频1-2集)

一、准备工作

1.前置技能需求:

1)C语言基础(入门时不需要完全会,但是要知道基础函数的作用以及C的一些特性)

2)Linux基础(会使用命令行基础命令就行了,Linux环境可以找现成的)

3)python入门基础(大概了解python编程时的语法就可以了)

4)汇编入门基础(学一点点的汇编,让你Pwn的更轻松)

2.工具准备(入门):

1)ida(目前地表最强反编译软件,必备)

2)gdb(一些Linux环境里面会预装)

3)pwntools库(python库,必备)

4)python3.x版本(必备)

二、解题目的

1.通过服务器提供给外界的远程程序服务,控制程序流(程序运行流程)执行system(“/bin/sh”);,获得服务器的控制权,用cat指令打印目录下的flag

三、Pwn手法

1.栈溢出

C语言的特点之一,某些输入语句未限制用户输入或者错误的限制导致输入语句将用户输入的所有数据全部存入栈中,即使输入的数据已经超过看合法申请的栈中局部变量的大小。栈溢出的示例语句如下:

1
2
3
4
5
6
7
8
#include <stdio.h>

int main() {
// 0x114514 system("/bin/sh");
char c[20]; // 申请的栈大小只有20字节
read(0, c, 50); // 从用户输入读取50字节
return 0;
}

我们都知道程序执行时需要调入内存中执行,那么程序在内存中是什么样的呢?栈又在内存中的那个位置?如何通过栈溢出来使得程序流可控?

先展示一下程序调入内存时的情况:

图片展示的是32位和64位程序运行时在内存中的情况,程序运行时同一采用虚拟地址来寻找指令和数据的地址,所有的程序设计时按照直接独占所有内存来设计,当运行时再由系统来分配实际内存。就和程序员编写程序一样,具体的实现过程已经由库函数帮你搞定了,你尽管调用就是了。

在这张图片中,我们现在只需要关注两个区域就可以了,分别是Text Segment (指令段,用来存放程序执行语句的地方,可读可执行)和Stack (栈区,用来存放局部变量的地方,可读可写(可执行?))

我们先来看程序开始运行时栈的情况(栈是从高地址到低地址存放的):

1.假设这是main函数运行时为main函数开辟的栈帧,其中ebp和esp(把他们当做C中的指针就行了)分别指向了栈底和栈顶,eip则是下一条指令的地址。(ebp,esp,eip都在cpu中,寄存器) |

2.当main函数运行完成后准备返回上一个主调函数时(main函数并不一定是最后退出的函数)程序会让esp移动到ebp的位置,然后执行pop ebp(将esp所指向地址中的内容取出来赋值给ebp),esp自动向上移动一格,然后执行pop eip(将esp所指向地址中的内容取出来赋值给eip),esp自动向上移动一格,程序开始执行eip(此时eip应该跳回Text Segment段了)所指向的指令。 |

我们该如何利用这个过程来控制程序的执行流呢?关键在于eip。前面说过了,如何输入语句对我们的输入没有做出合适的限制,我们可以输入超过字符数组C长度的数据,如果我们先输入20个字节的数据,在输入一个字节数据,那么这多出的一个字节会被存放到ebp的框中(不是放到ebp指针里面),同理也可以改变eip框中的值。如果我们先写21个字节的任意数据,在写入一个已知指令的地址(0x114514 system(“/bin/sh”),不就可以控制程序执行我们指定的指令了吗?

但是如果直接运行程序,我们没办法直接输入地址型数据(0x114514),这个时候就轮到pwntools出场了,因为pwntools在python中使用,所以我们通常会一次性写好python代码来执行多个攻击动作,这样的python代码被我们叫做exp(攻击脚本)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *
# 导入pwntools库

context(arch='i386', os='linux', log_level='debug')
# arch有两个选项'i386'和'amd64'表示攻击的系统是32位或64位

p = remote('远程服务器IP', 远程服务器端口)
# 连接远程服务器,p为连接口

payload = b'A' * 21 + p32(0x114514)
# 'A'*21表示创建21个字符'A',前面的b表示将21个'A'转成byte流数据
# p32(0x114514)的意思是针对32位程序转换0x114514

p.sendline(payload)
# 向p发送一行数据,将payload发送出去

p.interactive()
# 成功执行system("/bin/sh")后进入交换模式

使用python3 exp.py来运行脚本,如果不出错的话我们已经成功取得了控制权,一般直接使用cat flag就可以得到flag了。

当然,一个程序运行时不会只有一个main函数,当一个函数调用其他函数时栈又会发生什么变化呢?由于静态不好展示这里插入一个视频链接:https://www.bilibili.com/video/BV1By4y1x7Yh/?spm_id_from=333.337.search-card.all.click&vd_source=46ff77891392a04a7d9d93e9037151db

视频中我们可以看到在函数发生调用时,每个函数的栈帧之间是相邻的(或许我们可以用这点可以改变其他函数局部变量的值?),这样就给了我们控制程序流的时机。

函数调用完成返回主函数的流程:
将esp移动到ebp的位置,将esp指向的地址的内容复制给ebp,esp自动向上走4个字节,然后将esp指向的地址的内容弹到eip中,esp自动向上走4个字节,完成现场恢复 |

四、关于一些对程序的保护措施的介绍

如果你认为Pwn就这啊,那你就完全错了。就像我们出寝室要锁门一样,程序也会给自己加上一些保护,

对于不同保护的程序我们也需要不同的手法来绕过他,在Linux下使用checksec(自己装)命令查看程序的保护。

如图所示,这就是程序保护全开的情况(阿里嘎多,出题人)。

1.Arch

可以看出来此程序是amd64或x86-64(反正都是64位)的程序,little是程序使用小端字序(大端小端的区别也是Pwn中重要的知识点),如果是32位程序,应该会显示关键字i386.

2.RELRO

分别有三种状态

1)No RELRO:在这种状态下,重定位表(PLT)和全局偏移表(GOT)都是可写的。这意味着攻击者可以修改这些结构中的指针,从而进行各种类型的攻击。

2)Partial RELRO:在这种状态下,重定位表被设置为只读,但全局偏移表(GOT)仍然是可写的。这种状态提供了一定程度的保护,防止了一些简单的攻击手段,但仍存在一些漏洞。

3)Full RELRO:这是最高级别的 RELRO 状态。在这种状态下,重定位表(PLT)和全局偏移表(GOT)都被设置为只读。这样做可以防止攻击者修改这些结构中的指针,从而有效地阻止了许多常见的攻击手段。

重定位表(PLT)和全局偏移表(GOT)都是在发生库函数(如stdio.h)调用时才会有的,他们为指明了库函数加载到内存中后的具体位置。

其实你写的printf根本不在Text Segment段,猜猜他在哪?

3.Stack(拒绝栈溢出,从我做起。)

程序这小子在栈中末尾的某个地方插入了一段随机的校验数据,当函数调用完成返回时,会先检查这段数据是否完整,当你精心构造的payload毁坏了这段校验数据,程序会伤心的拒绝执行接下来的指令。(当然你也可以小心一点,抬抬脚绕开这段校验数据)

4.NX(想在我这里执行指令,这又不是Text Segment段!)

主要体现在shellcode手法上,指在栈溢出后直接在栈上写入接下来要执行的指令,然后通过第二次溢出再跳转到这里执行指令,相当于把栈区当Text Segment在用。当NX开启时,这种行为会被制止。

5.PIE(看rand就图一乐,真随机还得是我)

当PIE开启时,我就知道我大部分手法就要失效了,美国的麦克阿瑟夫将军曾经说过:“当我碰到开启PIE的Pwn题,我就知道,这题的分不再属于我了。”

PIE会使得地址随机,这就意味着通过gdb调试和ida得到的地址可能会不准确,想要确定system等函数的地址更加困难。

(工作量比我想的大,先肝到这里吧)

下周学习计划

|应该要做的事情

继续学习Pwn基础视频

刷题刷题刷题刷题刷题刷题刷题刷题刷题刷题刷题刷题刷题刷题

学习感受

对计算机底层有了更多的了解,还有一些程序运行时具体怎么操作的知识,了解了在构造程序时应该注意的一些安全问题。(其实就是学了Pwn以后腰不疼了,腿不酸了,就是头上有点凉)


23-10-23
https://zlsf-zl.github.io/2023/10/23/10-23-10-29/
作者
ZLSF
发布于
2023年10月23日
许可协议