여담
Copy Fail을 정리하면서 AF_ALG가 계속 등장했는데, 해당 interface 자체를 따로 정리하지 않으면 algif_aead, splice, scatterlist 흐름이 계속 애매하게 느껴질 것 같았다.
그래서 이번 글에서는 특정 CVE exploit을 보기 전에, userspace가 어떤 syscall 흐름으로 kernel crypto API까지 들어가는지 위주로 정리하겠다.
해당 글은 AF_ALG의 사용법을 추천하는 글이 아니라, Linux kernel에서 userspace crypto socket interface가 어떤 구조로 동작하는지 정리하는 글입니다.
AF_ALG란?
AF_ALG는 Linux Kernel Crypto API를 userspace에서 사용할 수 있게 만든 socket interface이다.
일반적인 TCP/UDP socket처럼 네트워크 통신을 하는 것이 아니라, socket(), bind(), setsockopt(), accept(), sendmsg(), recvmsg() 같은 socket syscall을 이용해서 kernel 내부 crypto algorithm을 호출하는 구조이다.
간단하게 말하면 아래와 같다.
userspace program
-> socket(AF_ALG, SOCK_SEQPACKET, 0)
-> bind(type, algorithm name)
-> setsockopt(key, authsize, ...)
-> accept()
-> sendmsg()/write()
-> recvmsg()/read()
-> kernel crypto API result
Plain Text
복사
여기서 중요한 점은 AF_ALG가 privileged interface가 아니라는 점이다. 기본적으로 unprivileged process도 socket(AF_ALG, SOCK_SEQPACKET, 0)를 호출할 수 있다.
그래서 AF_ALG는 단순한 crypto helper가 아니라, userspace에서 kernel crypto subsystem으로 들어가는 공격 표면이 될 수 있다.
기본 정보
•
Interface : AF_ALG
•
Socket family : PF_ALG
•
Socket type : SOCK_SEQPACKET
•
Socket option level : SOL_ALG
•
Main header : include/uapi/linux/if_alg.h
•
Main implementation : crypto/af_alg.c
•
Type implementation : crypto/algif_hash.c, crypto/algif_skcipher.c, crypto/algif_aead.c, crypto/algif_rng.c
•
Main syscall : socket, bind, setsockopt, accept, sendmsg, recvmsg, read, write
공식 문서 기준으로 접근 가능한 타입은 크게 아래와 같다.
type | 의미 | 예시 |
hash | Message digest / keyed digest | sha256, hmac(sha256) |
skcipher | Symmetric key cipher | cbc(aes), ctr(aes) |
aead | Authenticated encryption | gcm(aes), authenc(...) |
rng | Random number generator | stdrng |
즉, AF_ALG에서 bind()할 때 salg_type으로 어떤 wrapper를 쓸지 고르고, salg_name으로 실제 algorithm 이름을 넘긴다.
Userspace API 흐름
가장 기본적인 사용 흐름은 아래 코드처럼 생겼다.
#include <linux/if_alg.h>
#include <sys/socket.h>
#include <unistd.h>
int tfmfd = socket(AF_ALG, SOCK_SEQPACKET, 0);
struct sockaddr_alg sa = {
.salg_family = AF_ALG,
.salg_type = "hash",
.salg_name = "sha256",
};
bind(tfmfd, (struct sockaddr *)&sa, sizeof(sa));
int opfd = accept(tfmfd, NULL, 0);
write(opfd, "AAAA", 4);
read(opfd, out, 32);
C
복사
여기서 tfmfd와 opfd를 구분해서 봐야 한다.
tfmfd는 algorithm을 선택하고 key 같은 공통 state를 들고 있는 parent socket에 가깝고, opfd는 실제 crypto operation을 수행하는 accepted socket이다.
AF_ALG에서는 accept()가 네트워크 연결을 받는 의미라기보다는, 선택된 crypto transform을 실제로 사용할 operation fd를 만드는 역할에 가깝다.
sockaddr_alg
bind()에 넘기는 구조체는 struct sockaddr_alg이다.
struct sockaddr_alg {
__u16 salg_family;
__u8 salg_type[14];
__u32 salg_feat;
__u32 salg_mask;
__u8 salg_name[64];
};
C
복사
여기서 핵심은 salg_type과 salg_name이다.
•
salg_type : hash, skcipher, aead, rng 같은 AF_ALG wrapper type
•
salg_name : sha256, cbc(aes), gcm(aes) 같은 kernel crypto algorithm name
예를 들어 salg_type = "aead", salg_name = "gcm(aes)"라면 userspace는 algif_aead wrapper를 통해 kernel의 gcm(aes) 구현을 사용하게 된다.
setsockopt
key나 AEAD auth tag size 같은 값은 setsockopt()로 설정한다.
setsockopt(tfmfd, SOL_ALG, ALG_SET_KEY, key, keylen);
setsockopt(tfmfd, SOL_ALG, ALG_SET_AEAD_AUTHSIZE, NULL, authsize);
C
복사
SOL_ALG level에서 자주 나오는 option은 아래와 같다.
•
ALG_SET_KEY : key 설정
•
ALG_SET_KEY_BY_KEY_SERIAL : keyring에 있는 key serial로 key 설정
•
ALG_SET_AEAD_AUTHSIZE : AEAD authentication tag size 설정
•
ALG_SET_DRBG_ENTROPY : RNG entropy 설정
ALG_SET_IV, ALG_SET_OP, ALG_SET_AEAD_ASSOCLEN도 있지만 이 값들은 보통 setsockopt()가 아니라 sendmsg()의 control message로 전달된다.
sendmsg control message
skcipher나 aead는 단순히 data만 보내면 부족하다. encrypt인지 decrypt인지, IV가 무엇인지, AAD 길이가 얼마인지 같은 meta data가 필요하다.
그래서 sendmsg()의 cmsg를 통해 아래 값들이 들어간다.
SOL_ALG + ALG_SET_OP -> ALG_OP_ENCRYPT / ALG_OP_DECRYPT
SOL_ALG + ALG_SET_IV -> struct af_alg_iv
SOL_ALG + ALG_SET_AEAD_ASSOCLEN -> AAD length
Plain Text
복사
kernel의 af_alg_cmsg_send()는 이 control message를 파싱해서 struct af_alg_control에 저장한다.
정리하면 userspace가 넘기는 data는 두 종류로 나뉜다.
1.
실제 처리할 data : write(), send(), sendmsg() payload
2.
crypto operation metadata : sendmsg()의 cmsg
Kernel 내부 흐름
이제 kernel code 기준으로 흐름을 따라가보자.
AF_ALG의 공통 진입점은 crypto/af_alg.c에 있고, 타입별 실제 crypto operation은 algif_hash.c, algif_skcipher.c, algif_aead.c 같은 파일로 나뉜다.
socket(AF_ALG)
userspace에서 아래 syscall을 호출하면,
socket(AF_ALG, SOCK_SEQPACKET, 0);
C
복사
kernel에서는 alg_create()가 호출된다.
socket(AF_ALG, SOCK_SEQPACKET, 0)
-> alg_create()
-> sk_alloc(PF_ALG, ...)
-> sock->ops = &alg_proto_ops
-> sock_init_data()
Plain Text
복사
여기서 alg_proto_ops에는 bind, setsockopt, accept, release 정도만 의미 있게 연결되어 있다.
아직 hash나 aes 같은 algorithm이 선택된 상태는 아니다. 그냥 PF_ALG socket 하나가 만들어진 상태이다.
bind()
그 다음 bind()를 호출하면 alg_bind()로 들어간다.
bind(tfmfd, sockaddr_alg)
-> alg_bind()
-> alg_get_type(sa->salg_type)
-> 없으면 request_module("algif-%s", salg_type)
-> type->bind(sa->salg_name)
-> ask->type = type
-> ask->private = crypto transform
Plain Text
복사
예를 들어 salg_type = "hash"이면 algif_hash type을 찾고, 없다면 algif-hash module load를 시도한다.
이후 type->bind(salg_name)이 호출된다.
hash type에서는 대략 아래처럼 실제 crypto transform을 잡는다.
hash_bind("sha256")
-> crypto_alloc_ahash("sha256", ...)
Plain Text
복사
즉, bind()는 단순히 이름만 저장하는 것이 아니라, kernel crypto API에서 사용할 transform object를 실제로 준비하는 단계이다.
accept()
bind()가 끝난 뒤 accept()를 호출하면 실제 operation fd가 만들어진다.
accept(tfmfd)
-> af_alg_accept()
-> 새 PF_ALG sock 할당
-> newsock->ops = type->ops
-> type->accept(parent_private, child_sock)
-> child socket에 operation context 연결
-> newsock->state = SS_CONNECTED
Plain Text
복사
여기서 중요한 점은 accepted socket의 ops가 type별 operation으로 바뀐다는 점이다.
예를 들어 hash면 sendmsg = hash_sendmsg, recvmsg = hash_recvmsg가 연결되고, skcipher면 skcipher_sendmsg, skcipher_recvmsg 흐름을 타게 된다.
그래서 사용자는 같은 sendmsg()/recvmsg()를 호출하지만, 실제 kernel 내부에서는 선택한 type에 따라 전혀 다른 코드가 실행된다.
sendmsg()
skcipher와 aead 계열에서는 공통 helper인 af_alg_sendmsg()가 자주 등장한다.
이 함수의 역할은 crypto operation을 바로 실행하는 것이 아니라, userspace에서 넘어온 data와 control message를 kernel 내부 context에 쌓아두는 쪽에 가깝다.
sendmsg(opfd)
-> type별 sendmsg()
-> af_alg_sendmsg()
-> af_alg_cmsg_send()로 cmsg 파싱
-> ALG_SET_OP / ALG_SET_IV / ALG_SET_AEAD_ASSOCLEN 저장
-> userspace data를 TX SGL에 복사
-> ctx->more = MSG_MORE 여부 저장
Plain Text
복사
여기서 TX SGL은 transmit scatter-gather list이다.
즉, userspace가 보낸 data가 kernel에서 하나의 연속 buffer로만 관리되는 것이 아니라, page 단위의 scatterlist로 관리된다.
recvmsg()
실제 crypto operation은 보통 recvmsg() 시점에 수행된다.
recvmsg(opfd)
-> type별 recvmsg()
-> RX SGL 구성
-> TX SGL에서 필요한 data 추출
-> crypto request 구성
-> crypto API 호출
-> 결과를 userspace buffer로 copy
Plain Text
복사
algif_skcipher.c 주석도 이 구조를 설명한다.
TX SGL은 sendmsg()로 채워지고, recvmsg()에서 사용자가 제공한 output buffer를 RX SGL로 잡은 뒤 crypto request를 준비한다.
정리하면 AF_ALG에서 sendmsg()는 input을 밀어 넣는 단계이고, recvmsg()는 output buffer를 제공하면서 실제 연산을 트리거하는 단계라고 보면 된다.
타입별 동작
AF_ALG는 하나의 socket family지만, salg_type에 따라 내부 의미가 꽤 달라진다.
hash
hash type은 가장 단순하다.
struct sockaddr_alg sa = {
.salg_family = AF_ALG,
.salg_type = "hash",
.salg_name = "sha256",
};
C
복사
사용자는 data를 send()/write()로 넣고, recv()/read()로 digest를 읽는다.
send("AAAA")
-> hash update/digest
recv(out)
-> sha256 digest 반환
Plain Text
복사
MSG_MORE를 쓰면 여러 번 나눠서 update하고, 마지막 send에서 final digest를 만들 수 있다.
HMAC처럼 key가 필요한 hash는 ALG_SET_KEY로 key를 먼저 넣는다.
skcipher
skcipher는 AES-CBC, AES-CTR 같은 symmetric cipher를 userspace에서 호출하는 wrapper이다.
struct sockaddr_alg sa = {
.salg_family = AF_ALG,
.salg_type = "skcipher",
.salg_name = "cbc(aes)",
};
C
복사
이 경우 key는 setsockopt(ALG_SET_KEY)로 넣고, IV와 encrypt/decrypt 방향은 sendmsg() cmsg로 넣는다.
setsockopt(ALG_SET_KEY)
sendmsg(ALG_SET_OP + ALG_SET_IV + plaintext/ciphertext)
recvmsg(output buffer)
Plain Text
복사
여기서 block size, IV size, input length alignment 같은 제약은 선택한 algorithm의 /proc/crypto 정보와 맞아야 한다.
aead
aead는 gcm(aes)처럼 encryption과 authentication을 같이 처리하는 타입이다.
struct sockaddr_alg sa = {
.salg_family = AF_ALG,
.salg_type = "aead",
.salg_name = "gcm(aes)",
};
C
복사
AEAD에서는 AAD와 authentication tag 때문에 layout을 정확히 맞춰야 한다.
공식 문서 기준 input/output 구조는 아래와 같다.
Encryption input : AAD || plaintext
Encryption output : ciphertext || authentication tag
Decryption input : AAD || ciphertext || authentication tag
Decryption output : plaintext
Plain Text
복사
ALG_SET_AEAD_ASSOCLEN으로 AAD 길이를 알려주고, ALG_SET_AEAD_AUTHSIZE로 tag size를 설정한다.
Copy Fail에서 문제가 되었던 algif_aead도 바로 이 aead type의 userspace wrapper이다.
rng
rng는 kernel RNG interface를 userspace에서 읽기 위한 타입이다.
struct sockaddr_alg sa = {
.salg_family = AF_ALG,
.salg_type = "rng",
.salg_name = "stdrng",
};
C
복사
이 경우에는 read()/recvmsg()로 random data를 얻는다.
다만 일반적인 userspace 프로그램에서 random byte가 필요하다면 보통 getrandom()이나 libc/OpenSSL 쪽 API를 쓰는 것이 더 일반적이다.
splice와 zero-copy
AF_ALG를 system hacking 관점에서 볼 때 중요한 부분 중 하나가 splice() 기반 zero-copy input이다.
공식 kernel 문서도 현재 AF_ALG의 zero-copy interface로 vmsplice()와 splice() 사용법을 설명한다. 따라서 이 기능이 제거되었거나 AF_ALG 전체가 deprecated되었다고 보는 것은 맞지 않다.
Copy Fail에서 사용하는 실제 file-backed 경로는 다음과 같다.
target file page cache
-> splice(file -> pipe)
pipe_buffer.page = target page
-> splice(pipe -> AF_ALG socket)
splice_to_socket()
-> bio_vec
-> kernel이 MSG_SPLICE_PAGES 설정
-> sock_sendmsg()
-> af_alg_sendmsg()
-> extract_iter_to_sg()
-> TX scatterlist가 같은 page를 참조
Plain Text
복사
이 경로에서 userspace가 sendmsg(..., MSG_SPLICE_PAGES)를 직접 호출하는 것이 아니다. 두 번째 splice()의 kernel 구현인 splice_to_socket()이 ITER_BVEC iterator와 MSG_SPLICE_PAGES를 만든다.
zero-copy라는 표현도 범위를 좁혀서 봐야 한다. 모든 splice() 조합이 반드시 byte copy 없이 끝난다는 뜻은 아니다. file type과 file_operations에 따라 fallback copy가 존재할 수 있다. 다만 일반 page-cache-backed file에서 Copy Fail이 사용하는 경로는 struct page * reference를 pipe, bio_vec, scatterlist로 전달한다.
pipe_buffer.page == bio_vec.bv_page == sg_page(sg)
Plain Text
복사
성능을 위한 page sharing 자체는 정상 기능이다. 취약점은 이 source page가 이후 algif_aead의 destination SGL 안으로 섞였다는 데 있다.
splice와 pipe 내부 구현은 다음 글에서 별도로 본다.
왜 공격 표면이 되는가
AF_ALG는 암호 연산 interface라서 처음 보면 위험하지 않아 보인다.
하지만 kernel exploitation 관점에서는 꽤 넓은 공격 표면이다.
이유는 아래와 같다.
1.
unprivileged userspace에서 접근 가능하다.
2.
kernel crypto API의 여러 algorithm과 template 조합에 닿는다.
3.
sendmsg() cmsg, keyring, AEAD layout, scatterlist, async request 같은 복잡한 구조가 같이 동작한다.
4.
zero-copy path에서는 pipe, page cache, bio_vec, scatterlist까지 한 syscall 흐름에 엮인다.
5.
type별 구현이 hash, skcipher, aead, rng로 나뉘어서 검증해야 할 코드가 넓다.
공식 kernel 문서는 현재도 AF_ALG와 zero-copy interface를 문서화하고 있다. 실무에서 userspace crypto library가 더 일반적인 경우가 많다는 사실과 kernel interface의 지원 상태는 구분해야 한다.
공격 표면 관점에서 중요한 점은 AF_ALG가 kernel 내부의 software algorithm과 template system을 unprivileged socket API로 노출한다는 것이다. 단순히 AES 함수 하나를 호출하는 interface보다 request layout과 memory ownership 경계가 넓다.
AF_ALG에서 위험한 부분은 암호 알고리즘 자체가 아니라, unprivileged user가 복잡한 kernel crypto path를 socket syscall만으로 직접 실행시킬 수 있다는 점이다.
비활성화 관점
사용하지 않는 시스템이라면 kernel config에서 아래 계열을 끄는 것이 attack surface 감소에 도움이 된다.
CONFIG_CRYPTO_USER_API
CONFIG_CRYPTO_USER_API_HASH
CONFIG_CRYPTO_USER_API_SKCIPHER
CONFIG_CRYPTO_USER_API_AEAD
CONFIG_CRYPTO_USER_API_RNG
Plain Text
복사
모듈 기반 환경에서는 배포판과 kernel 구성에 따라 algif_hash, algif_skcipher, algif_aead, algif_rng 같은 module load를 제한하는 방식도 고려할 수 있다.
다만 어떤 프로그램이 실제로 AF_ALG를 사용하고 있을 수 있으므로 운영 환경에서는 무작정 막기 전에 의존성을 확인해야 한다.
Copy Fail과 연결해서 보면
Copy Fail 글에서 AF_ALG가 등장한 위치를 다시 보면 아래 흐름이다.
socket(AF_ALG, SOCK_SEQPACKET, 0)
-> bind(type="aead", name="authencesn(...)")
-> setsockopt(ALG_SET_KEY, ...)
-> setsockopt(ALG_SET_AEAD_AUTHSIZE, ...)
-> accept()
-> sendmsg(AAD + control message, MSG_MORE)
-> splice(file -> pipe)
-> splice(pipe -> AF_ALG operation socket)
// kernel 내부에서 MSG_SPLICE_PAGES 설정
-> recvmsg()
-> algif_aead decrypt path
Plain Text
복사
여기서 AF_ALG는 userspace가 algif_aead에 도달하기 위한 문이다.
취약점의 직접 원인은 AF_ALG라는 socket family 자체가 아니라, algif_aead 내부에서 source와 destination scatterlist를 구성하는 방식이었다.
하지만 AF_ALG가 unprivileged로 열려 있었기 때문에 그 취약한 crypto path가 일반 user에게도 노출되었다.
따라서 Copy Fail을 이해할 때 질문을 이렇게 나누면 좋다.
1.
userspace가 kernel crypto API에 어떻게 들어가는가? → AF_ALG
2.
어떤 type 구현으로 들어가는가? → algif_aead
3.
input page가 어떻게 들어가는가? → splice()와 scatterlist
4.
page cache가 왜 write 대상이 되는가? → in-place / destination SGL 구성 문제
5.
왜 LPE가 되는가? → page cache corruption이 setuid binary 실행에 영향
이번 글은 이 중 1번과 2번 앞부분을 정리한 것이다. AF_ALG 내부 구조체와 request 구성은 다음 글에서 code by code로 이어진다.
정리
AF_ALG를 한 줄로 정리하면 아래와 같다.
AF_ALG는 userspace가 socket syscall을 통해 Linux kernel crypto API의 hash, skcipher, AEAD, RNG algorithm을 호출할 수 있게 만든 interface이다.
흐름은 아래처럼 기억하면 된다.
socket(AF_ALG)
-> alg_create()
bind(sockaddr_alg)
-> alg_bind()
-> algif_* type 선택
-> crypto transform allocation
setsockopt(SOL_ALG)
-> key/authsize/entropy 설정
accept()
-> operation fd 생성
sendmsg()/write()
-> input data와 cmsg 저장
recvmsg()/read()
-> crypto request 실행 후 결과 반환
Plain Text
복사
system hacking 관점에서 중요한 점은 AF_ALG가 단순한 library call이 아니라 kernel 내부 crypto subsystem으로 들어가는 unprivileged syscall interface라는 것이다.
그래서 algif_aead, scatterlist, splice, page cache 같은 kernel 내부 구조와 엮이면 Copy Fail 같은 취약점의 진입점이 될 수 있다.
공식 kernel 문서는 현재도 AF_ALG와 zero-copy 사용법을 제공한다. 다만 사용하지 않는 환경에서는 의존성을 확인한 뒤 CONFIG_CRYPTO_USER_API_* 비활성화나 module/policy 기반 제한을 attack-surface reduction 관점에서 검토할 수 있다.
일단 이번 글에서는 AF_ALG의 전체 흐름과 kernel 내부 경계까지만 정리하고 마치겠다.