首页
社区
课程
招聘
2
[原创]pwn—栈的学习
发表于: 2024-7-18 16:46 9139

[原创]pwn—栈的学习

2024-7-18 16:46
9139

pwn—栈的学习

文章基于台科大LJP的pwn课程学习
https://www.youtube.com/watch?v=8zO47WDUdIk&t=1087s

1.前卫知识

1.x86 Assembly

与c语言比较

rax和rbx是x86架构下的一种通用寄存器

image-20240317193730112

image-20240317193903917

jmp这里是跳过的意思,这里指直接跳到begin

cmp是比较的意思

jle是conditional jump指条件跳转(即如果前一个比较指令结果表明第一个操作数小于或等于第二个操作数,则执行跳转)

LOOP这里指的是直接跳出去(这里是跳到循环开头)

如果要搜索直接在谷歌或者edge输入==x86 要搜索的指令==

2.save bp

在 x86 架构中,特别是在函数调用过程中,通常会使用基址指针 bp(也称为帧指针)来访问函数的局部变量和参数

当一个函数被调用时,它的栈帧(stack frame)被压入栈中,bp 指针会指向栈帧的基址,这样就可以通过 bp 指针来访问函数的局部变量和参数

3.read函数的调用规则

1
ssize_t read(int fd, void *buf, size_t count);
  • fd:表示要读取数据的文件描述符。通常情况下,0 表示标准输入(stdin)、1 表示标准输出(stdout)、2 表示标准错误输出(stderr)。其他文件描述符则是由打开文件时返回的整数。
  • buf:表示存放读取数据的缓冲区的地址。
  • count:表示要读取的字节数。

read() 函数会尝试从文件描述符 fd 中读取 count 个字节的数据,并将数据存储到 buf 所指向的缓冲区中

它会返回实际读取的字节数:

  1. 如果返回值为 0,则表示已经读取到文件的末尾
  2. 如果返回值为 -1,则表示出现了错误,此时可以通过查看 errno 变量获取具体的错误信息

read() 函数是一个阻塞函数,如果没有数据可读,它会一直等待

2.栈帧(Stake Frame)

1.前卫知识

不同的区域存在不同的栈帧,里面存放不同的局部变量,每个function都有头部和尾部

头部:prologue

尾部:epilogue

前面是头部后面是尾部

image-20240317202312587

image-20240317202753822

0x0007fffffffe5c8是rsp的具体的位置

2.main函数头部的流程

1
2
3
4
5
6
7
push rbp
mov rbp,rsp
sub rsp,20h
```
call function1
leave
ret

1.push:将rbp的值push进rsp的位置,在stack的位置向上移动了8个bit

这个地方做一个说明这里的8bit是一个举例,在x86 架构下,这里一般是4个字节(32个bit)

image-20240317203837044

2.将rbp的值存到rsp

image-20240317204200395

3.rsp减掉0x20,这样main函数就会放在rsp和rbp之间的位置

image-20240317204809250

4.然后呼叫function1,进入function1执行,这里x86 对于function1 的返回后的的地址问题给出处理方法是在call之后,立刻将返回的地址push进入main的stack位置

image-20240317205603743

2.function1头部的流程

1
2
3
4
5
6
push rbp
mov rbp,rsp
sub rsp,30h
```
leave
ret

1.将rbp的值再次压入rsp中

image-20240317210011735

2.将rbp的值赋给了rsp的值

image-20240317210148972

3.将rsp减去0x30,中间的就是function的区域

image-20240317210254407

4.尾部流程(本质就是头部的反操作)

1.leave可以理解为的等效的

1
2
mov rsp rbp
pop rbp

1.mov rsp,rbp,就是将rbp的值赋给rsp,这样rsp就会飞到rbp(这里相当于是丢弃函数体中的局部变量)

image-20240317210906662

2.pop rbp

1
用于将数据从栈中弹出,在 x86 架构的汇编语言中,“pop” 指令通常与 “push” 指令配对使用,用于栈操作。它从栈顶弹出数据,并将栈指针减小相应的字节数,以指向下一个数据。 “pop” 指令通常用于恢复之前压入栈的寄存器值或局部变量,以及其他需要的数据

这里不太好理解,

我认为的是这里的pop指令的关键就是上面的铺设和mov的操作的一次返还来保证这里的栈的稳定性,不发生偏移

1.取消掉rsp前面被push进入栈的8个bit也就是rbp原本的值

2.然后取消掉==这里rbp被mov到rsp经过push压栈后的值的操作==

结果如下(若果不理解可以再看看上面栈头部的操作)

image-20240317214938037

2.function1中的ret的操作就是呼应了上面call之后的x86的函数调用的返回的处理方法

这里的执行方法就是说将这里rsp中的0x401234的返回值pop给一个另外的寄存器rip(它的作用是不断更新执行那个下一条将要执行的地址)这样rip就会将rsp的pop回到原来的值(也就是e5a0对应的值,也就是main函数那里leave对应的地址)

image-20240317221717108

这里可以发现stack的值已经返回了原来的函数调用前的值

3.main函数里面的leave的操作

取消掉rsp前面被push进入栈的8个bit也就是rbp原本的值

然后取消掉==这里rbp被mov到rsp经过push压栈后的值的操作==

4.然后就return退出这个stack

3.GDB

1.基本配置

这里推荐大佬的文章配置pwn的ubuntu的pwn环境

ubuntu20.04 PWN(含x86、ARM、MIPS)环境搭建_ubuntu powerpc编译环境-CSDN博客

但是个人觉得kali的好看一点

推荐的套件:

  1. gef套件(美化gdb)
  2. pwngdb套件

2.常用的指令

1.b*[中断的地址]:(break point)

设定中断点

2.c:(continue)

继续执行

3.ni:(==不步入==)

执行一个命令

4.si:(==步入==)

执行一个命令

5.x/格式如下:(address ecxpression)

image-20240318201347580

显示记忆内容

3.演示

1.disassemble+函数名

disassemble 命令用于反汇编指定的代码区域,将其转换为汇编语言的形式并显示出来

可以指定要反汇编的代码区域

  1. 函数名
  2. 地址范围
  3. 代码地址

使用指令示例:

  • disassemble

    在不指定具体代码区域的情况下,会显示当前程序执行点所在的代码的汇编

  • disassemble function_name

    指定函数名,显示该函数的汇编代码

  • disassemble start_address, end_address

    指定地址范围,显示该范围内的代码的汇编

  • disassemble *address

    指定具体的地址,显示该地址处的代码的汇编

2.x实例

这里是gef的显示的界面

image-20240319194213981

可以看见这里再b下断点之后发现,这里是展示了00到08的记忆体的数据

我么想要查看后面8位的数据

就使用指令

1
x/1xg 0x00007fffffffe538

这里1个g就表示8个bit

就可以看到这个界面

image-20240319194611681

4.pwntools(一个基于python的攻击模组)

1.常用函数functions

image-20240319194819296

1.process

用于创建本地进程。通过该函数,你可以启动一个本地的程序,并与其进行交互

1
p = process("/path/to/program")

其中 p 是一个 process 对象

/path/to/program 是要执行的程序的路径

你可以使用 p.send() 方法发送数据给该进程,使用 p.recv() 方法接收进程的输出

示例代码

1
2
3
4
5
6
7
8
9
10
from pwn import *
 
# 创建一个本地进程
p = process("/bin/bash")
 
# 发送数据给进程
p.sendline("ls")
 
# 接收进程的输出
print(p.recvall().decode())

2.remote

用于创建网络连接并在远程主机上执行程序。通过该函数,你可以连接到远程主机的指定端口,并与其进行交互

1
r = remote("host", port)

r 是一个 remote 对象

host 是远程主机的 IP 地址或域名

port 是要连接的端口号

可以使用 r.send() 方法发送数据给远程主机,使用 r.recv() 方法接收远程主机的输出

示例代码

1
2
3
4
5
6
7
8
9
10
from pwn import *
 
# 连接到远程主机的 1337 端口
r = remote("example.com", 1337)
 
# 发送数据给远程主机
r.sendline("ls")
 
# 接收远程主机的输出
print(r.recvall().decode())

3.send

用于向进程或远程主机发送数据。无论是本地进程还是远程主机,你都可以使用 send 函数发送数据

1
p.send("data")

其中 p 可以是 process 对象或 remote 对象,data 是要发送的数据

示例代码

1
2
3
4
5
6
7
8
9
from pwn import *
 
p = process("/bin/bash")
 
# 向进程发送数据
p.sendline("echo hello")
 
# 接收并打印进程的输出
print(p.recv().decode())

4.sendline

发送带有换行符的数据。当需要发送一行数据并在末尾添加换行符时,可以使用 sendline 函数

1
p.sendline("data")

其中 p 可以是 process 对象或 remote 对象

data 是要发送的数据,在末尾会自动添加换行符 \n

示例代码

1
2
3
4
5
6
from pwn import *
 
p = process("/bin/bash")
 
# 使用 sendline 发送数据,自动添加换行符
p.sendline("echo hello")

5.sendafter

在指定字符串出现后发送数据。当需要等待某个特定字符串出现后再发送数据时,可以使用 sendafter 函数

1
p.sendafter("search", "data")

其中 p 可以是 process 对象或 remote 对象,search 是要搜索的字符串,一旦找到该字符串就会发送数据 "data"

示例代码

1
2
3
4
5
6
from pwn import *
 
p = process("/bin/bash")
 
# 在字符串 "prompt: " 出现后发送数据
p.sendafter("prompt: ", "ls")

6.sendlineafter

在指定字符串出现后发送带有换行符的数据。结合了 sendlinesendafter 的功能,在指定字符串出现后发送带有换行符的数据

1
p.sendlineafter("search", "data")

其中 p 可以是 process 对象或 remote 对象,search 是要搜索的字符串,一旦找到该字符串就会发送带有换行符的数据 "data\n"

1
2
3
4
5
6
from pwn import *
 
p = process("/bin/bash")
 
# 在字符串 "prompt: " 出现后发送带有换行符的数据
p.sendlineafter("prompt: ", "echo hello")

sendline 用于发送带有换行符的数

sendafter 用于在指定字符串出现后发送数据

sendlineafter 则是在指定字符串出现后发送带有换行符的数据

7.recv

接收指定长度的数据。该函数用于接收指定长度的数据并返回

1
p.recv(n)

其中 p 可以是 process 对象或 remote 对象,n 是要接收的数据的长度。如果不指定 n,则会接收尽可能多的数据

1
2
3
4
5
6
7
from pwn import *
 
p = process("/bin/bash")
 
# 接收长度为 10 的数据
data = p.recv(10)
print(data.decode())

8.recvline

接收一行数据。该函数用于接收一行数据并返回,一行数据以换行符 \n 结尾

示例代码

1
2
3
4
5
6
7
from pwn import *
 
p = process("/bin/bash")
 
# 接收一行数据
line = p.recvline()
print(line.decode())

9.recvuntil

接收直到指定字符串出现为止的所有数据。该函数用于接收数据,直到指定的字符串出现为止,然后返回所有接收到的数据(包括指定的字符串)

1
p.recvuntil("pattern")

其中 p 可以是 process 对象或 remote 对象,pattern 是要等待的字符串

1
2
3
4
5
6
7
from pwn import *
 
p = process("/bin/bash")
 
# 接收直到出现 "shell" 为止的所有数据
data = p.recvuntil("shell")
print(data.decode())

2.利用手段

后期慢慢说,现在知道概念就行

5.栈溢出漏洞

1.简述

由在局部变量上面越界输入导致

  1. 导致其他局部变量被更改
  2. 导致return address被更改(用于指示函数执行完成后程序应该返回到哪个位置继续执行)

2.示例详解

依旧利用前面的栈帧来进行模拟

1.栈的情况

image-20240325175019692

假设这个蓝色的框里面存在使用者进行传参的窗口

2.传入n个A到rsp的位置

如果传入的A没有限制

1.假如传入的A在560到590之间,则没有影响

image-20240325175401795

2.加入传入的A过多将后面的数据盖掉(就有可能出现bug)

image-20240325175436405

到leave的时候程序还能执行

但是到ret的时候,return回去8个A,就是8个4141的地址(假如它存在),如果不存在,程序就会出错

3.堆栈保护机制

一种用于防范缓冲区溢出攻击的安全机制

会在程序的栈帧中插入一个称为"Stack Canary"的特殊值。这个值被设计为一个随机生成的数字,会在系统运行时动态地插入到程序栈的关键位置之间,如函数返回地址之前

当程序运行时,栈上的保护值会被监视,如果发生缓冲区溢出攻击,导致该值被修改,程序会检测到这种异常情况并采取相应的安全措施,例如终止程序执行,防止进一步的恶意操作

1.利用cannary之后的示例分析

image-20240325194322222

1.mov rax,fs:28h

fs是一个暂存器,这里fs:28h相当于将存储在 FS 段寄存器偏移地址为 0x28 的位置)加载到 RAX 寄存器

2.mov [rbp-8],rax

将rax的值放在rbp-8的位置

image-20240325195643959

3.模拟中间的传参(过多传参)

image-20240325195757228

这里cannary的值被改掉了

4.尝试获取前面存放cannary的值

mov rcx,[rbp-8]就是指尝试获取前面rbp+8存放的cannary的值

image-20240325200529067

5.将获取到的新的rbp上的cannry的值与原来fs:28 处存放的cannary的值进行比较

image-20240325200513516

一样的输出0 ,如果不一样则结果不是0

6.如果是0的话就jump

image-20240325200728724

jump到ok的位置

image-20240325200900536

正常跳到ok,执行ok的后面正常退出

7.如果不相同则继续执行

call __stack_chk_fail

呼叫这个检查函数,程序不会正常退出,程序检查到了栈溢出的问题

2.绕过cannary的技术

基本的想法:

泄露cannary的值

在传入参数的时候将cannnary放到正确的位置,其他还是一样的传入A

image-20240325202012354

就可以绕过检查

3.pwn攻击示例

image-20240325202442198

不太清晰这个东西以后再分享类似的

6.shellcode简易原理以及操作

一段用于利用计算机系统漏洞或实施攻击的机器码,通常是用来注入恶意代码并获得对系统控制权的一种技术。Shellcode 通常以二进制形式编写,用于利用特定的漏洞或弱点,例如缓冲区溢出漏洞。一旦成功执行,Shellcode 可能会启动一个 shell 或者执行其他恶意操作

1.将这里的数据写入,从全是A改成shellcode

image-20240325211219098

2.将这里的从main函数call的func1返回的地址数据改成上面shellcode执行的开始的地址

==这样本来的func1函数执行结束后应该返回位于main函数的该函数返回地址继续执行后面的操作,但是这里将地址更改后就会执行shellcode的程序==

image-20240325211624576

1.这里leave执行以后,就变成了这样

image-20240325212224010

2.在执行ret之后程序就会来到shellcode的地方

image-20240325212347536

7.NX(NO-eXecute)技术

这里针对的就是刚刚的关键性步骤在栈上的数据,能够被当做指令执行的问题

栈上的记忆体有三重权限(rwx三重权限)

==r(read) w(write) x(eXcute)==

  • “r” 表示读取权限,允许用户查看文件的内容或目录的列表
  • “w” 表示写入权限,允许用户修改文件的内容或在目录中创建、删除文件
  • “x” 表示执行权限,对于文件来说,允许用户执行文件的程序;对于目录来说,允许用户进入该目录

但是在设定nx之后将不会存在rwx区段

image-20240325214551615

在stack这个区段上面fde000-fff000存在rwx的权限

1.查看记忆体区段

b main

之后run运行

到断点的位置

1
vnmap

将会展示详细的记忆体区段信息

image-20240325215116165

这里可以发现这里的的字段存在了很多rwx的三重权限

一般来说这里都要可读r(否则无法读取的话执行和写入都无法进行)

2.nx的原理

将数据所在内存页(用户栈中)标识为不可执行

当程序溢出成功转入shellcode时,程序会尝试在数据页面上执行指令,此时CPU就会抛出异常,而不是去执行恶意指令

就像这样:

微信截图_20221014115435.png

3.shellcode demo理解

1.运行shellcode,断点位于main,run进入调试,vnmap查看结构体(跟上面一样)

image-20240327105141966

2.先看源码配合调试理解

image-20240327105504550

1.stack-protecter

1
2
3
gcc-fno-stack-protecter -o 文件
 
即是说不存在canary保护
1
2
3
gcc-stack-protecter -o 文件
 
即是存在cannary的保护
1
2
gcc-fno-stack-protecter -z execstack -o 文件
即使指这里的stack是rwx

2.设置缓冲区

image-20240327110140096

stevbuff的官方解释:

https://www.runoob.com/cprogramming/c-function-setvbuf.html

声明:

1
int setvbuf(FILE *stream, char *buffer, int mode, size_t size)

参数:

  • stream -- 这是指向 FILE 对象的指针,该 FILE 对象标识了一个打开的流

  • buffer -- 这是分配给用户的缓冲。如果设置为 NULL,该函数会自动分配一个指定大小的缓冲

  • mode -- 这指定了文件缓冲的模式:

    模式 描述
    _IOFBF 全缓冲:对于输出,数据在缓冲填满时被一次性写入。对于输入,缓冲会在请求输入且缓冲为空时被填充。
    _IOLBF 行缓冲:对于输出,数据在遇到换行符或者在缓冲填满时被写入,具体视情况而定。对于输入,缓冲会在请求输入且缓冲为空时被填充,直到遇到下一个换行符。
    _IONBF 无缓冲:不使用缓冲。每个 I/O 操作都被即时写入。buffer 和 size 参数被忽略。
  • size --这是缓冲的大小,以字节为单位

返回值:

如果成功,则该函数返回 0,否则返回非零值

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include
#include
#include
int main()
{
  
   char buff[1024];
  
   memset( buff, '\0', sizeof( buff ));
  
   fprintf(stdout, "启用全缓冲\n");
   setvbuf(stdout, buff, _IOFBF, 1024);
  
   fprintf(stdout, "这里是 runoob.com\n");
   fprintf(stdout, "该输出将保存到 buff\n");
   fflush( stdout );
  
   fprintf(stdout, "这将在编程时出现\n");
   fprintf(stdout, "最后休眠五秒钟\n");
  
   sleep(5);
  
   return(0);
}
1
在这里,程序把缓冲输出保存到 buff,直到首次调用 fflush() 为止,然后开始缓冲输出,最后休眠 5 秒钟。它会在程序结束之前,发送剩余的输出到 STDOUT

3.进入gdb inint

image-20240327114438698

概念详解:

  1. 当 GDB(即 GNU Project Debugger)启动时,它在当前用户的主目录中寻找一个名为 .gdbinit 的文件;如果该文件存在,则 GDB 就执行该文件中的所有命令
  2. 该文件用于简单的配置命令
  3. 可以读取宏编码语言,从而允许实现更强大的自定义

基本格式:

1
2
3
4
5
6
define
end
document
<help text>
end

关键指令

image-20240327115120803

tty识别linux设备号

image-20240327115246671

在将这里的tty识别出的linux设备号设置回去

image-20240327115405621

设置的gdb init的内容


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2024-7-18 19:18 被gir@ffe编辑 ,原因:
收藏
免费 2
支持
分享
赞赏记录
参与人
雪币
留言
时间
PLEBFE
为你点赞!
2024-8-30 01:21
jmpcall
+1
期待更多优质内容的分享,论坛有你更精彩!
2024-7-19 09:22
最新回复 (1)
雪    币: 35662
活跃值: (64621)
能力值: (RANK:135 )
在线值:
发帖
回帖
粉丝
2
将标题补充一下,感谢分享!
2024-7-18 18:28
0
游客
登录 | 注册 方可回帖
返回

账号登录
验证码登录

忘记密码?
没有账号?立即免费注册