文章基于台科大LJP的pwn课程学习
https://www.youtube.com/watch?v=8zO47WDUdIk&t=1087s
与c语言比较
rax和rbx是x86架构下的一种通用寄存器


jmp这里是跳过的意思,这里指直接跳到begin
cmp是比较的意思
jle是conditional jump指条件跳转(即如果前一个比较指令结果表明第一个操作数小于或等于第二个操作数,则执行跳转)
LOOP这里指的是直接跳出去(这里是跳到循环开头)
如果要搜索直接在谷歌或者edge输入==x86 要搜索的指令==
在 x86 架构中,特别是在函数调用过程中,通常会使用基址指针 bp
(也称为帧指针)来访问函数的局部变量和参数
当一个函数被调用时,它的栈帧(stack frame)被压入栈中,bp
指针会指向栈帧的基址,这样就可以通过 bp
指针来访问函数的局部变量和参数
1 |
ssize_t read( int fd, void *buf, size_t count);
|
-
fd
:表示要读取数据的文件描述符。通常情况下,0 表示标准输入(stdin)、1 表示标准输出(stdout)、2 表示标准错误输出(stderr)。其他文件描述符则是由打开文件时返回的整数。
-
buf
:表示存放读取数据的缓冲区的地址。
-
count
:表示要读取的字节数。
read()
函数会尝试从文件描述符 fd
中读取 count
个字节的数据,并将数据存储到 buf
所指向的缓冲区中
它会返回实际读取的字节数:
- 如果返回值为 0,则表示已经读取到文件的末尾
- 如果返回值为 -1,则表示出现了错误,此时可以通过查看
errno
变量获取具体的错误信息
read()
函数是一个阻塞函数,如果没有数据可读,它会一直等待
不同的区域存在不同的栈帧,里面存放不同的局部变量,每个function都有头部和尾部
头部:prologue
尾部:epilogue
前面是头部后面是尾部


0x0007fffffffe5c8是rsp的具体的位置
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)

2.将rbp的值存到rsp

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

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

1
2
3
4
5
6
|
push rbp
mov rbp,rsp
sub rsp, 30h
```
leave
ret
|
1.将rbp的值再次压入rsp中

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

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

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

2.pop rbp
1 |
用于将数据从栈中弹出,在 x86 架构的汇编语言中,“pop” 指令通常与 “push” 指令配对使用,用于栈操作。它从栈顶弹出数据,并将栈指针减小相应的字节数,以指向下一个数据。 “pop” 指令通常用于恢复之前压入栈的寄存器值或局部变量,以及其他需要的数据
|
这里不太好理解,
我认为的是这里的pop指令的关键就是上面的铺设和mov的操作的一次返还来保证这里的栈的稳定性,不发生偏移
1.取消掉rsp前面被push进入栈的8个bit也就是rbp原本的值
2.然后取消掉==这里rbp被mov到rsp经过push压栈后的值的操作==
结果如下(若果不理解可以再看看上面栈头部的操作)

2.function1中的ret的操作就是呼应了上面call之后的x86的函数调用的返回的处理方法
这里的执行方法就是说将这里rsp中的0x401234的返回值pop给一个另外的寄存器rip(它的作用是不断更新执行那个下一条将要执行的地址)这样rip就会将rsp的pop回到原来的值(也就是e5a0对应的值,也就是main函数那里leave对应的地址)

这里可以发现stack的值已经返回了原来的函数调用前的值
3.main函数里面的leave的操作
取消掉rsp前面被push进入栈的8个bit也就是rbp原本的值
然后取消
掉==这里rbp被mov到rsp经过push压栈后的值
的操作==
4.然后就return退出这个stack
这里推荐大佬的文章配置pwn的ubuntu的pwn环境
ubuntu20.04 PWN(含x86、ARM、MIPS)环境搭建_ubuntu powerpc编译环境-CSDN博客
但是个人觉得kali的好看一点
推荐的套件:
- gef套件(美化gdb)
- pwngdb套件
1.b*[中断的地址]:(break point)
设定中断点
2.c:(continue)
继续执行
3.ni:(==不步入==)
执行一个命令
4.si:(==步入==)
执行一个命令
5.x/格式如下:(address ecxpression)

显示记忆内容
1.disassemble+函数名
disassemble
命令用于反汇编指定的代码区域,将其转换为汇编语言的形式并显示出来
可以指定要反汇编的代码区域
- 函数名
- 地址范围
- 代码地址
使用指令示例:
-
disassemble
:
在不指定具体代码区域的情况下,会显示当前程序执行点所在的代码的汇编
-
disassemble function_name
:
指定函数名,显示该函数的汇编代码
-
disassemble start_address, end_address
:
指定地址范围,显示该范围内的代码的汇编
-
disassemble *address
:
指定具体的地址,显示该地址处的代码的汇编
2.x实例
这里是gef的显示的界面

可以看见这里再b下断点之后发现,这里是展示了00到08的记忆体的数据
我么想要查看后面8位的数据
就使用指令
这里1个g就表示8个bit
就可以看到这个界面


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
用于创建网络连接并在远程主机上执行程序。通过该函数,你可以连接到远程主机的指定端口,并与其进行交互
r
是一个 remote
对象
host
是远程主机的 IP 地址或域名
port
是要连接的端口号
可以使用 r.send()
方法发送数据给远程主机,使用 r.recv()
方法接收远程主机的输出
示例代码
1
2
3
4
5
6
7
8
9
10
|
from pwn import *
r = remote( "example.com" , 1337 )
r.sendline( "ls" )
print (r.recvall().decode())
|
3.send
用于向进程或远程主机发送数据。无论是本地进程还是远程主机,你都可以使用 send
函数发送数据
其中 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
函数
其中 p
可以是 process
对象或 remote
对象
data
是要发送的数据,在末尾会自动添加换行符 \n
示例代码
1
2
3
4
5
6
|
from pwn import *
p = process( "/bin/bash" )
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" )
p.sendafter( "prompt: " , "ls" )
|
6.sendlineafter
在指定字符串出现后发送带有换行符的数据。结合了 sendline
和 sendafter
的功能,在指定字符串出现后发送带有换行符的数据
1 |
p.sendlineafter( "search" , "data" )
|
其中 p
可以是 process
对象或 remote
对象,search
是要搜索的字符串,一旦找到该字符串就会发送带有换行符的数据 "data\n"
1
2
3
4
5
6
|
from pwn import *
p = process( "/bin/bash" )
p.sendlineafter( "prompt: " , "echo hello" )
|
sendline
用于发送带有换行符的数
sendafter
用于在指定字符串出现后发送数据
sendlineafter
则是在指定字符串出现后发送带有换行符的数据
7.recv
接收指定长度的数据。该函数用于接收指定长度的数据并返回
其中 p
可以是 process
对象或 remote
对象,n
是要接收的数据的长度。如果不指定 n
,则会接收尽可能多的数据
1
2
3
4
5
6
7
|
from pwn import *
p = process( "/bin/bash" )
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
接收直到指定字符串出现为止的所有数据。该函数用于接收数据,直到指定的字符串出现为止,然后返回所有接收到的数据(包括指定的字符串)
其中 p
可以是 process
对象或 remote
对象,pattern
是要等待的字符串
1
2
3
4
5
6
7
|
from pwn import *
p = process( "/bin/bash" )
data = p.recvuntil( "shell" )
print (data.decode())
|
后期慢慢说,现在知道概念就行
由在局部变量上面越界输入导致
- 导致其他局部变量被更改
- 导致return address被更改(用于指示函数执行完成后程序应该返回到哪个位置继续执行)
依旧利用前面的栈帧来进行模拟
1.栈的情况

假设这个蓝色的框里面存在使用者进行传参的窗口
2.传入n个A到rsp的位置
如果传入的A没有限制
1.假如传入的A在560到590之间,则没有影响

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

到leave的时候程序还能执行
但是到ret的时候,return回去8个A,就是8个4141的地址(假如它存在),如果不存在,程序就会出错
一种用于防范缓冲区溢出攻击的安全机制
会在程序的栈帧中插入一个称为"Stack Canary"的特殊值。这个值被设计为一个随机生成的数字,会在系统运行时动态地插入到程序栈的关键位置之间,如函数返回地址之前
当程序运行时,栈上的保护值会被监视,如果发生缓冲区溢出攻击,导致该值被修改,程序会检测到这种异常情况并采取相应的安全措施,例如终止程序执行,防止进一步的恶意操作
1.利用cannary之后的示例分析

1.mov rax,fs:28h
fs是一个暂存器,这里fs:28h相当于将存储在 FS 段寄存器偏移地址为 0x28 的位置)加载到 RAX 寄存器
2.mov [rbp-8],rax
将rax的值放在rbp-8的位置

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

这里cannary的值被改掉了
4.尝试获取前面存放cannary的值
mov rcx,[rbp-8]就是指尝试获取前面rbp+8存放的cannary的值

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

一样的输出0 ,如果不一样则结果不是0
6.如果是0的话就jump

jump到ok的位置

正常跳到ok,执行ok的后面正常退出
7.如果不相同则继续执行
call __stack_chk_fail
呼叫这个检查函数,程序不会正常退出,程序检查到了栈溢出的问题
2.绕过cannary的技术
基本的想法:
泄露cannary的值
在传入参数的时候将cannnary放到正确的位置,其他还是一样的传入A

就可以绕过检查
3.pwn攻击示例

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

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

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

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

这里针对的就是刚刚的关键性步骤在栈上的数据,能够被当做指令执行的问题
栈上的记忆体有三重权限(rwx三重权限)
==r(read) w(write) x(eXcute)==
- “r” 表示读取权限,允许用户查看文件的内容或目录的列表
- “w” 表示写入权限,允许用户修改文件的内容或在目录中创建、删除文件
- “x” 表示执行权限,对于文件来说,允许用户执行文件的程序;对于目录来说,允许用户进入该目录
但是在设定nx之后将不会存在rwx区段

在stack这个区段上面fde000-fff000存在rwx的权限
b main
之后run运行
到断点的位置
将会展示详细的记忆体区段信息

这里可以发现这里的的字段存在了很多rwx的三重权限
一般来说这里都要可读r(否则无法读取的话执行和写入都无法进行)
将数据所在内存页(用户栈中)标识为不可执行
当程序溢出成功转入shellcode时,程序会尝试在数据页面上执行指令,此时CPU就会抛出异常,而不是去执行恶意指令
就像这样:

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

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

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.设置缓冲区

stevbuff的官方解释:
https://www.runoob.com/cprogramming/c-function-setvbuf.html
声明:
1 |
int setvbuf( FILE * stream, char * buffer , int mode, size_t 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
|
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

概念详解:
- 当 GDB(即 GNU Project Debugger)启动时,它在当前用户的主目录中寻找一个名为 .gdbinit 的文件;如果该文件存在,则 GDB 就执行该文件中的所有命令
- 该文件用于简单的配置命令
- 可以读取宏编码语言,从而允许实现更强大的自定义
基本格式:
1
2
3
4
5
6
|
define
end
document
< help text>
end
|
关键指令

tty识别linux设备号

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

设置的gdb init的内容
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
最后于 2024-7-18 19:18
被gir@ffe编辑
,原因: