여담
처음 AF_ALG를 보면 그냥 userspace에서 kernel crypto API를 socket처럼 쓰는 인터페이스라고 생각하기 쉽다.
근데 Copy Fail을 이해할 때는 그 정도 설명으로는 부족하다. 핵심은 AF_ALG가 어떤 socket object를 만들고, sendmsg()로 들어온 입력을 어떤 struct scatterlist에 저장하고, recvmsg() 시점에 그 SGL을 AEAD request의 src/dst로 어떻게 넘기느냐이다.
이번 글은 그래서 API 사용법보다 커널 코드 흐름을 기준으로 본다. 기준 소스는 로컬 랩의 두 트리다.
vulnerable tree : E:\N4C\kernel-lab\src\linux-copyfail-vuln
patched tree : E:\N4C\kernel-lab\src\linux-copyfail-patched
Plain Text
복사
linux-copyfail-vuln과 linux-copyfail-patched는 git repo가 아니라 snapshot이라서 diff는 git diff --no-index로 봤다. 패치가 걸린 파일은 크게 다음 네 개다.
crypto/af_alg.c 10 insertions, 39 deletions
crypto/algif_aead.c 19 insertions, 81 deletions
crypto/algif_skcipher.c 3 insertions, 3 deletions
include/crypto/if_alg.h 2 insertions, 3 deletions
Plain Text
복사
scatterlist.c, scatterlist.h, scatterwalk.c, authencesn.c는 vuln/patched 사이에서 바뀐 파일은 아니다. 대신 이 파일들은 왜 패치가 필요한지 이해하기 위한 background code다.
이 글은 AF_ALG socket이 page-backed input을 받은 시점부터 집중한다. file page가 pipe와 ITER_BVEC를 거쳐 여기까지 오는 과정은 Background (3)에서 분리해서 본다.
큰 흐름
AF_ALG AEAD 경로를 syscall 기준으로 줄이면 다음과 같다.
userspace
socket(AF_ALG, SOCK_SEQPACKET, 0)
-> crypto/af_alg.c:alg_create()
bind(tfmfd, { salg_type="aead", salg_name="authencesn(...)" })
-> crypto/af_alg.c:alg_bind()
-> crypto/af_alg.c:alg_get_type("aead")
-> crypto/algif_aead.c:aead_bind()
-> crypto_alloc_aead("authencesn(...)")
setsockopt(tfmfd, SOL_ALG, ALG_SET_KEY, ...)
-> crypto/af_alg.c:alg_setsockopt()
-> crypto/af_alg.c:alg_setkey()
-> crypto/algif_aead.c:aead_setkey()
setsockopt(tfmfd, SOL_ALG, ALG_SET_AEAD_AUTHSIZE, ...)
-> crypto/af_alg.c:alg_setsockopt()
-> crypto/algif_aead.c:aead_setauthsize()
accept(tfmfd, ...)
-> crypto/af_alg.c:af_alg_accept()
-> crypto/algif_aead.c:aead_accept_parent()
-> child socket gets struct af_alg_ctx
sendmsg(opfd, AAD + cmsg, MSG_MORE)
-> crypto/algif_aead.c:aead_sendmsg()
-> crypto/af_alg.c:af_alg_sendmsg()
-> AAD is copied into an AF_ALG private TX page
splice(target file -> pipe)
-> pipe_buffer.page = target page-cache page
splice(pipe -> opfd)
-> fs/splice.c:splice_to_socket()
-> kernel sets MSG_SPLICE_PAGES + ITER_BVEC
-> crypto/af_alg.c:af_alg_sendmsg()
-> target page is stored in ctx->tsgl_list
recvmsg(opfd, output_iovec, ...)
-> crypto/algif_aead.c:aead_recvmsg()
-> crypto/algif_aead.c:_aead_recvmsg()
-> AF_ALG builds aead_request src/dst SGL
-> crypto_aead_encrypt() / crypto_aead_decrypt()
Plain Text
복사
중요한 점은 sendmsg()에서 crypto operation이 바로 실행되지 않는다는 점이다. sendmsg()는 입력을 AF_ALG child socket의 TX SGL에 쌓는다. 실제 crypto request는 recvmsg()에서 output buffer를 받은 뒤 만들어진다.
AF_ALG에서 중요한 구조체
코드는 include/crypto/if_alg.h부터 보는 게 제일 빠르다.
struct alg_sock
위치: include/crypto/if_alg.h:24-35
struct alg_sock {
struct sock sk;
struct sock *parent;
atomic_t refcnt;
atomic_t nokey_refcnt;
const struct af_alg_type *type;
void *private;
};
C
복사
alg_sock은 AF_ALG socket의 kernel-side object다. 여기서 private의 의미는 parent socket과 child socket에서 다르다.
parent socket 기준:
ask->type = &algif_type_aead
ask->private = struct crypto_aead *tfm
Plain Text
복사
child socket 기준:
ask->parent = parent sock
ask->type = &algif_type_aead
ask->private = struct af_alg_ctx *ctx
Plain Text
복사
즉 parent는 algorithm transform을 들고 있고, child는 한 operation의 입력/상태를 들고 있다.
struct af_alg_type
위치: include/crypto/if_alg.h:43-56
struct af_alg_type {
void *(*bind)(const char *name, u32 type, u32 mask);
void (*release)(void *private);
int (*setkey)(void *private, const u8 *key, unsigned int keylen);
int (*accept)(void *private, struct sock *sk);
int (*accept_nokey)(void *private, struct sock *sk);
int (*setauthsize)(void *private, unsigned int authsize);
struct proto_ops *ops;
struct proto_ops *ops_nokey;
char name[14];
};
C
복사
이 구조체는 AF_ALG의 dispatch table이다.
bind()에서 salg_type="aead"가 들어오면 AF_ALG core는 name == "aead"인 af_alg_type을 찾는다. 그 결과가 algif_aead.c의 algif_type_aead다.
위치: crypto/algif_aead.c:500-510
algif_type_aead
.bind = aead_bind
.release = aead_release
.setkey = aead_setkey
.setauthsize = aead_setauthsize
.accept = aead_accept_parent
.ops = &algif_aead_ops
.name = "aead"
Plain Text
복사
그래서 AF_ALG core는 aead를 직접 모른다. core는 af_alg_type callback만 호출하고, 실제 AEAD transform 처리는 algif_aead.c가 한다.
struct af_alg_ctx
위치: include/crypto/if_alg.h:143-164
struct af_alg_ctx {
struct list_head tsgl_list;
void *iv;
void *state;
size_t aead_assoclen;
struct crypto_wait wait;
size_t used;
atomic_t rcvused;
bool more, merge, enc, write, init;
unsigned int len;
unsigned int inflight;
};
C
복사
ctx는 child socket에 붙는 per-operation state다.
•
tsgl_list: sendmsg()로 들어온 입력을 저장하는 TX SGL list
•
iv: CMSG로 받은 IV
•
aead_assoclen: AEAD associated data length
•
used: TX SGL에 아직 소비되지 않고 남아 있는 byte 수
•
enc: encrypt/decrypt 여부
•
more: MSG_MORE가 켜져 있는지
•
merge: 새 데이터가 이전 page 뒤에 붙을 수 있는지
Copy Fail을 볼 때는 ctx->tsgl_list와 ctx->used가 가장 중요하다.
TX SGL과 RX SGL
위치: include/crypto/if_alg.h:58-79, 94-111
AF_ALG는 SGL을 크게 두 종류로 쓴다.
TX SGL
userspace -> kernel 방향
sendmsg()가 채움
ctx->tsgl_list에 매달림
RX SGL
kernel -> userspace 방향
recvmsg()가 받은 output iovec 기반으로 만듦
areq->rsgl_list / areq->first_rsgl에 매달림
Plain Text
복사
struct af_alg_async_req 안에는 per-request TX SGL도 있다.
areq->tsgl
recvmsg()에서 이번 crypto request에 쓸 TX fragment만 따로 떼어온 SGL
Plain Text
복사
patched 버전에서는 이 areq->tsgl이 AEAD에서도 중요해진다. vulnerable 버전 AEAD decrypt는 이걸 tag 부분에만 이상하게 썼고, patched 버전은 요청 전체 입력을 areq->tsgl로 만든다.
socket(AF_ALG): alg_create()
위치: crypto/af_alg.c:505-529
socket(AF_ALG, SOCK_SEQPACKET, 0)이 들어오면 alg_create()가 호출된다.
alg_create()
if sock->type != SOCK_SEQPACKET: reject
if protocol != 0: reject
sk = sk_alloc(..., PF_ALG, ..., &alg_proto, ...)
sock->ops = &alg_proto_ops
sock_init_data(sock, sk)
sk->sk_destruct = alg_sock_destruct
Plain Text
복사
이 시점의 socket은 아직 어떤 crypto algorithm에도 bind되지 않았다.
parent alg_sock after socket()
ask->type = NULL
ask->private = NULL
sock->ops = alg_proto_ops
Plain Text
복사
alg_proto_ops에는 .bind = alg_bind, .setsockopt = alg_setsockopt, .accept = alg_accept가 들어간다. 즉 parent socket은 sendmsg()/recvmsg()를 하지 않고, bind/setkey/accept용 control socket처럼 동작한다.
type registration: algif_aead_init()
AF_ALG core가 aead라는 type을 알 수 있는 이유는 algif_aead module이 자기 type을 등록하기 때문이다.
위치: crypto/algif_aead.c:513-515
algif_aead_init()
-> af_alg_register_type(&algif_type_aead)
Plain Text
복사
위치: crypto/af_alg.c:62-90
af_alg_register_type(type)
down_write(&alg_types_sem)
if same name exists: -EEXIST
node = kmalloc(...)
type->ops->owner = THIS_MODULE
node->type = type
list_add(&node->list, &alg_types)
up_write(&alg_types_sem)
Plain Text
복사
alg_types는 AF_ALG type registry다.
alg_types list
"hash" -> algif_hash
"skcipher" -> algif_skcipher
"aead" -> algif_aead
"rng" -> algif_rng
Plain Text
복사
bind()에서 salg_type 문자열로 이 list를 조회한다.
bind(): salg_type과 salg_name이 분리된다
위치: crypto/af_alg.c:148-206
userspace는 보통 이런 식으로 bind한다.
struct sockaddr_alg sa = {0};
sa.salg_family = AF_ALG;
strncpy((char *)sa.salg_type, "aead", sizeof(sa.salg_type));
strncpy((char *)sa.salg_name, "authencesn(hmac(sha256),cbc(aes))", sizeof(sa.salg_name));
bind(tfm, (struct sockaddr *)&sa, sizeof(sa));
C
복사
로컬 probe 기준 위치: payloads/copyfail_probe.c:98-135
여기서 두 문자열은 의미가 다르다.
salg_type = "aead"
AF_ALG transport type
algif_aead.c를 고른다.
salg_name = "authencesn(hmac(sha256),cbc(aes))"
실제 Crypto API algorithm/template 이름
crypto_alloc_aead()에 들어간다.
Plain Text
복사
alg_bind()의 흐름은 다음과 같다.
alg_bind(sock, sockaddr_alg)
reject connected socket
validate addr_len
validate salg_feat / salg_mask
force NUL termination on salg_type/salg_name
type = alg_get_type(sa->salg_type)
if type == -ENOENT:
request_module("algif-%s", sa->salg_type)
type = alg_get_type(sa->salg_type)
private = type->bind(sa->salg_name, feat, mask)
lock parent socket
if child refs exist: -EBUSY
swap(ask->type, type)
swap(ask->private, private)
Plain Text
복사
alg_get_type()은 alg_types list에서 node->type->name과 salg_type을 비교한다.
위치: crypto/af_alg.c:43-60
aead의 type->bind()는 aead_bind()다.
위치: crypto/algif_aead.c:424-427
aead_bind(name, type, mask)
-> crypto_alloc_aead(name, type, mask)
Plain Text
복사
따라서 bind 이후 parent socket 상태는 이렇게 된다.
parent socket
ask->type = algif_type_aead
ask->private = crypto_aead object for authencesn(hmac(sha256),cbc(aes))
Plain Text
복사
setsockopt(): key와 authsize 설정
위치: crypto/af_alg.c:357-408
AF_ALG에서 key와 AEAD authsize는 parent socket에 설정한다.
setsockopt(SOL_ALG, ALG_SET_KEY)
-> alg_setsockopt()
-> alg_setkey()
-> type->setkey(ask->private, key, keylen)
-> aead_setkey()
-> crypto_aead_setkey()
setsockopt(SOL_ALG, ALG_SET_AEAD_AUTHSIZE)
-> alg_setsockopt()
-> type->setauthsize(ask->private, optlen)
-> aead_setauthsize()
-> crypto_aead_setauthsize()
Plain Text
복사
alg_setsockopt()에서 눈여겨볼 조건은 이 부분이다.
if (atomic_read(&ask->refcnt) != atomic_read(&ask->nokey_refcnt))
return -EBUSY
Plain Text
복사
이미 key가 필요한 operation child가 붙어 있으면 parent 설정을 바꾸지 못하게 막는다. 즉 key/authsize는 operation이 아니라 transform object 쪽 state다.
CMSG로 들어가는 값은 다르다.
ALG_SET_KEY -> setsockopt()
ALG_SET_AEAD_AUTHSIZE -> setsockopt()
ALG_SET_OP -> sendmsg() CMSG
ALG_SET_IV -> sendmsg() CMSG
ALG_SET_AEAD_ASSOCLEN -> sendmsg() CMSG
Plain Text
복사
accept(): parent와 child가 갈라진다
위치: crypto/af_alg.c:410-470
AF_ALG는 parent socket에서 바로 crypto operation을 하지 않는다. accept()로 child socket을 만들고, 그 child에서 sendmsg()/recvmsg()를 한다.
af_alg_accept(parent_sk, newsock)
type = parent->type
sk2 = sk_alloc(PF_ALG)
sock_init_data(newsock, sk2)
newsock->ops = type->ops
err = type->accept(parent->private, sk2)
if err == -ENOKEY and type->accept_nokey exists:
err = type->accept_nokey(parent->private, sk2)
alg_sk(sk2)->parent = parent_sk
alg_sk(sk2)->type = type
newsock->state = SS_CONNECTED
Plain Text
복사
AEAD에서는 type->accept()가 aead_accept_parent()다.
위치: crypto/algif_aead.c:459-498
aead_accept_parent(private=tfm, child_sk)
if tfm still needs key: return -ENOKEY
return aead_accept_parent_nokey(private, child_sk)
aead_accept_parent_nokey(private=tfm, child_sk)
ctx = sock_kmalloc(sizeof(*ctx))
ctx->iv = sock_kmalloc(ivlen)
INIT_LIST_HEAD(&ctx->tsgl_list)
crypto_init_wait(&ctx->wait)
child->private = ctx
child->sk_destruct = aead_sock_destruct
Plain Text
복사
결과적으로 child socket은 이렇게 된다.
child socket
ask->parent = parent socket
ask->type = algif_type_aead
ask->private = af_alg_ctx
sock->ops = algif_aead_ops
Plain Text
복사
그리고 algif_aead_ops에는 다음 operation이 들어간다.
.sendmsg = aead_sendmsg
.recvmsg = aead_recvmsg
.poll = af_alg_poll
Plain Text
복사
sendmsg(): CMSG parsing
위치: crypto/af_alg.c:559-597, 940-999
AEAD child socket에 sendmsg()를 호출하면 먼저 aead_sendmsg()를 거친다.
위치: crypto/algif_aead.c:54-63
aead_sendmsg(sock, msg, size)
tfm = parent->private
ivsize = crypto_aead_ivsize(tfm)
return af_alg_sendmsg(sock, msg, size, ivsize)
Plain Text
복사
공통 구현은 af_alg_sendmsg()다. 이 함수는 먼저 CMSG를 파싱한다.
af_alg_cmsg_send(msg, &con)
for each cmsghdr:
if cmsg_level != SOL_ALG: continue
ALG_SET_IV:
con.iv = CMSG_DATA(cmsg)
validate ivlen
ALG_SET_OP:
con.op = encrypt/decrypt
ALG_SET_AEAD_ASSOCLEN:
con.aead_assoclen = value
Plain Text
복사
그 다음 af_alg_sendmsg()는 child ctx에 metadata를 저장한다.
if CMSG exists:
ctx->enc = (ALG_OP_ENCRYPT ? true : false)
if IV exists: memcpy(ctx->iv, con.iv->iv, ivsize)
ctx->aead_assoclen = con.aead_assoclen
ctx->init = true
Plain Text
복사
로컬 copyfail_probe.c는 CMSG parsing과 MSG_SPLICE_PAGES branch를 직접 확인하기 위한 분석용 probe다. 실제 page-cache exploit의 userspace syscall은 이 probe와 다르다.
실제 exploit
accept(tfm) -> opfd
sendmsg(opfd, AAD + CMSG, MSG_MORE)
pipe(pipefd)
splice(target file -> pipe)
splice(pipe -> opfd)
-> kernel 내부에서 MSG_SPLICE_PAGES 설정
recv(opfd, output)
Plain Text
복사
즉 아래에서 말하는 MSG_SPLICE_PAGES path는 userspace가 해당 flag를 직접 넘기는 호출이 아니라, splice_to_socket()이 AF_ALG의 sendmsg operation을 호출한 뒤의 kernel 내부 경로다.
sendmsg(): TX SGL에 입력 page를 쌓는다
위치: crypto/af_alg.c:1001-1121
af_alg_sendmsg()의 본체는 while (size) loop다. 이 loop가 userspace input을 ctx->tsgl_list에 저장한다.
일반 sendmsg 경로와 MSG_SPLICE_PAGES 경로가 다르다.
normal sendmsg
alloc_page()
memcpy_from_msg(page_address(page), msg, plen)
sg_assign_page(sg, page)
sg[i].length = plen
kernel-internal MSG_SPLICE_PAGES path
// splice_to_socket()이 만든 ITER_BVEC
extract_iter_to_sg(&msg->msg_iter, len, &sgtable, ...)
for each new sg:
get_page(sg_page(&sg[i]))
Plain Text
복사
일반 경로는 새 kernel page를 할당하고 데이터를 복사한다. 그래서 TX SGL의 page는 AF_ALG가 새로 만든 private page다.
반대로 두 번째 splice가 만든 kernel-internal MSG_SPLICE_PAGES 경로는 ITER_BVEC에서 page fragment를 뽑아 SGL에 넣는다. 핵심은 이쪽이 “데이터 복사”가 아니라 “page 참조”라는 점이다.
source file page cache
-> pipe_buffer.page
-> bio_vec.bv_page
-> ITER_BVEC
-> extract_iter_to_sg()
-> struct scatterlist entries
-> ctx->tsgl_list
Plain Text
복사
extract_iter_to_sg()는 lib/scatterlist.c:1408-1438에 있다. iterator type에 따라 user page, bvec page, kvec page, xarray page 등을 SG entry로 바꾼다.
extract_iter_to_sg(iter, maxsize, sgtable, sg_max, flags)
switch iov_iter_type(iter):
ITER_UBUF / ITER_IOVEC -> extract_user_to_sg()
ITER_BVEC -> extract_bvec_to_sg()
ITER_KVEC -> extract_kvec_to_sg()
ITER_FOLIOQ -> extract_folioq_to_sg()
ITER_XARRAY -> extract_xarray_to_sg()
Plain Text
복사
여기서 Copy Fail 관점의 중요한 상태는 다음이다.
ctx->tsgl_list
sg[0] -> page cache page P, offset X, length L
sg[1] -> page cache page Q, offset Y, length M
...
ctx->used = total input bytes
Plain Text
복사
즉 AF_ALG TX SGL이 source file의 page cache page를 참조할 수 있다.
스캐터리스트란 무엇인가
위치: include/linux/scatterlist.h:11-22
struct scatterlist는 데이터를 담는 buffer가 아니다. 데이터를 가리키는 descriptor다.
struct scatterlist {
unsigned long page_link;
unsigned int offset;
unsigned int length;
dma_addr_t dma_address;
};
C
복사
실제 데이터는 page 안에 있고, SGL entry는 “어느 page의 어느 offset부터 length byte를 보라”는 메타데이터만 가진다.
scatterlist entry
page_link -> struct page *
offset -> page 안에서 시작 offset
length -> 이 entry가 나타내는 byte 길이
Plain Text
복사
page_link에 page pointer가 그대로 들어가는 것처럼 보이지만, lower bit에는 flag도 같이 encode된다.
위치: include/linux/scatterlist.h:51-75
SG_CHAIN = bit 0
SG_END = bit 1
SG_PAGE_LINK_MASK = SG_CHAIN | SG_END
Plain Text
복사
그래서 page를 직접 대입하면 안 되고 sg_set_page()나 sg_assign_page()를 써야 한다.
위치: include/linux/scatterlist.h:129-165
sg_set_page(sg, page, len, offset)
sg_assign_page(sg, page)
sg->offset = offset
sg->length = len
sg_page(sg)
return page_link & ~SG_PAGE_LINK_MASK
Plain Text
복사
이 구조 때문에 다음 두 SGL은 완전히 다르다.
private copied SGL
sg[0] -> AF_ALG가 alloc_page()로 만든 page
page-cache backed SGL
sg[0] -> file mapping의 page cache page
Plain Text
복사
둘 다 type은 struct scatterlist지만, 실제 write가 발생했을 때 영향 범위가 다르다.
SGL chaining
위치: include/linux/scatterlist.h:235-265
SGL은 하나의 연속 배열만으로 끝나지 않을 수 있다. sg_chain()은 한 SGL 배열의 마지막 entry를 다음 SGL 배열로 이어 붙인다.
sg_chain(prv, prv_nents, next_sgl)
-> __sg_chain(&prv[prv_nents - 1], next_sgl)
-> last entry page_link = next_sgl | SG_CHAIN
Plain Text
복사
그리고 sg_next()는 chain entry를 만나면 다음 SGL 배열로 점프한다.
위치: include/linux/scatterlist.h:107-117
sg_next(sg)
if sg_is_last(sg): return NULL
sg++
if sg_is_chain(sg): sg = sg_chain_ptr(sg)
return sg
Plain Text
복사
이게 Copy Fail에서 중요하다. vulnerable AEAD decrypt path는 RX SGL 뒤에 TX tag SGL을 chain한다.
RX SGL: [ user output buffer page(s) ] --chain--> [ TX tag source page ]
Plain Text
복사
이렇게 되면 crypto code가 req->dst를 따라 걷다가 원래는 source였던 page를 destination처럼 볼 수 있다.
recvmsg(): RX SGL 생성
위치: crypto/af_alg.c:1203-1300
recvmsg()는 output buffer를 같이 제공한다. AF_ALG는 이 output iovec도 SGL로 바꾼다.
af_alg_get_rsgl(sk, msg, flags, areq, maxsize, &outlen)
while output buffer remains:
rsgl = first_rsgl or new af_alg_rsgl
sg_init_table(rsgl->sgl.sgt.sgl, ALG_MAX_PAGES)
extract_iter_to_sg(&msg->msg_iter, seglen, &rsgl->sgl.sgt, ...)
sg_mark_end(last_sg)
if previous RX SGL exists:
af_alg_link_sg(previous, current)
areq->last_rsgl = rsgl
len += extracted
Plain Text
복사
즉 recvmsg()의 output buffer도 page 단위로 SG entry가 된다.
areq->first_rsgl.sgl.sgt.sgl
-> user output buffer page(s)
Plain Text
복사
정상적으로는 crypto request의 dst가 이 RX SGL을 가리키고, 결과만 output buffer에 써야 한다.
af_alg_pull_tsgl(): TX SGL을 소비한다
Copy Fail 패치의 중심 함수 중 하나가 af_alg_pull_tsgl()이다.
vulnerable 위치: crypto/af_alg.c:704-765
vulnerable signature:
void af_alg_pull_tsgl(struct sock *sk, size_t used,
struct scatterlist *dst, size_t dst_offset)
C
복사
이 함수는 ctx->tsgl_list 앞에서부터 used byte를 소비한다. 그리고 dst != NULL이면 소비하는 TX page를 dst SGL에 다시 설정한다.
취약 버전에는 dst_offset이 있었다.
if dst_offset >= plen:
discard this TX page fragment
dst_offset -= plen
else:
get_page(page)
sg_set_page(dst + j,
page,
plen - dst_offset,
sg[i].offset + dst_offset)
Plain Text
복사
이 말은 “TX SGL을 앞에서부터 used만큼 소비하되, 그중 offset 이전은 버리고 offset 이후만 dst SGL로 재지정할 수 있다”는 뜻이다.
AEAD decrypt path는 이 기능을 tag 분리에 사용했다.
auth tag starts at processed - authsize
-> af_alg_pull_tsgl(sk, processed, areq->tsgl, processed - as)
Plain Text
복사
patched 버전에서는 signature가 바뀐다.
patched 위치: crypto/af_alg.c:646-736, include/crypto/if_alg.h:233-247
unsigned int af_alg_count_tsgl(struct sock *sk, size_t bytes);
void af_alg_pull_tsgl(struct sock *sk, size_t used, struct scatterlist *dst);
C
복사
dst_offset이 사라졌다. 이제 af_alg_pull_tsgl()은 앞에서부터 소비한 fragment를 그대로 dst에 옮길 수만 있다.
if dst:
get_page(page)
sg_set_page(dst + j, page, plen, sg[i].offset)
Plain Text
복사
즉 “중간 offset 이후만 골라서 dst SGL로 빼내기”가 API 차원에서 사라졌다.
AEAD recv: vulnerable code
vulnerable 위치: crypto/algif_aead.c:66-290
_aead_recvmsg()는 AEAD request를 실제로 만드는 함수다.
먼저 길이를 계산한다.
used = ctx->used
as = crypto_aead_authsize(tfm)
if encrypt:
outlen = used + as
else:
outlen = used - as
used -= ctx->aead_assoclen
processed = used + ctx->aead_assoclen
Plain Text
복사
decrypt 기준으로 보면 input은 다음 layout이다.
TX input = AAD || CT || TAG
ctx->used = len(AAD) + len(CT) + len(TAG)
outlen = len(AAD) + len(CT)
used = len(CT) + len(TAG) // crypto API cryptlen
processed = len(AAD) + len(CT) + len(TAG)
Plain Text
복사
vulnerable code는 먼저 global TX SGL에서 첫 non-empty SG entry를 찾는다.
위치: crypto/algif_aead.c:157-173
for each tsgl in ctx->tsgl_list:
for each sg in tsgl:
if sg has length and page:
tsgl_src = sg
break
Plain Text
복사
그 다음 encrypt와 decrypt가 갈린다.
vulnerable encrypt
위치: crypto/algif_aead.c:191-204
rsgl_src = RX SGL
memcpy_sglist(RX SGL, tsgl_src, processed)
af_alg_pull_tsgl(sk, processed, NULL, 0)
aead_request_set_crypt(src = RX SGL,
dst = RX SGL,
cryptlen = used,
iv = ctx->iv)
Plain Text
복사
encrypt에서는 TX를 RX로 복사하고, source와 destination을 둘 다 RX로 둔다. in-place operation을 만들기 위한 최적화다.
vulnerable decrypt
위치: crypto/algif_aead.c:205-248
rsgl_src = RX SGL
memcpy_sglist(RX SGL, tsgl_src, outlen)
areq->tsgl_entries = af_alg_count_tsgl(sk, processed, processed - as)
areq->tsgl = sock_kmalloc(...)
sg_init_table(areq->tsgl, areq->tsgl_entries)
af_alg_pull_tsgl(sk, processed, areq->tsgl, processed - as)
if RX SGL exists:
sg_unmark_end(last_rx_sg)
sg_chain(RX SGL, ..., areq->tsgl)
else:
rsgl_src = areq->tsgl
aead_request_set_crypt(src = rsgl_src,
dst = RX SGL,
cryptlen = used,
iv = ctx->iv)
Plain Text
복사
이걸 메모리 상태로 보면 더 잘 보인다.
1. 두 번째 splice가 page-backed input을 AF_ALG TX SGL에 넣은 뒤
ctx->tsgl_list
TX[0] -> page-cache page: AAD || CT || TAG
2. recvmsg()에서 RX SGL 생성
RX SGL
RX[0] -> user output buffer page
3. vulnerable decrypt가 AAD || CT를 RX로 복사
RX[0] -> AAD || CT
TX[0] -> original page-cache page: AAD || CT || TAG
4. vulnerable decrypt가 TAG 부분만 TX에서 떼어 areq->tsgl로 재지정
areq->tsgl
TAG_SG -> same original page-cache page, offset = TAG offset, length = authsize
5. vulnerable decrypt가 RX 뒤에 TAG_SG를 chain
req->src / req->dst base
RX[0] -> user output AAD || CT
--chain-->
TAG_SG -> original page-cache page TAG area
Plain Text
복사
여기서 문제는 tag source page가 req->dst walk 안으로 들어온다는 점이다.
AEAD recv: patched code
patched 위치: crypto/algif_aead.c:65-230
patched 버전은 AEAD recv logic을 out-of-place 방식으로 바꾼다.
핵심 변경은 이 부분이다.
processed = used + ctx->aead_assoclen
areq->tsgl_entries = af_alg_count_tsgl(sk, processed)
areq->tsgl = sock_kmalloc(...)
sg_init_table(areq->tsgl, areq->tsgl_entries)
af_alg_pull_tsgl(sk, processed, areq->tsgl)
tsgl_src = areq->tsgl
rsgl_src = RX SGL
memcpy_sglist(rsgl_src, tsgl_src, ctx->aead_assoclen)
aead_request_set_crypt(src = tsgl_src,
dst = RX SGL,
cryptlen = used,
iv = ctx->iv)
Plain Text
복사
바뀐 점은 세 가지다.
1.
processed 전체를 per-request TX SGL인 areq->tsgl로 옮긴다.
2.
aead_request_set_crypt()의 source가 RX SGL이 아니라 tsgl_src가 된다.
3.
destination은 RX SGL로 유지되지만, RX 뒤에 tag source page를 chain하지 않는다.
patched decrypt의 메모리 상태는 다음이다.
1. 두 번째 splice가 page-backed input을 AF_ALG TX SGL에 넣은 뒤
ctx->tsgl_list
TX[0] -> page-cache page: AAD || CT || TAG
2. recvmsg()에서 per-request TX SGL 생성
areq->tsgl
TXREQ[0] -> same source page-cache page: AAD || CT || TAG
ctx->tsgl_list
consumed / advanced
3. RX SGL 생성
RX[0] -> user output buffer page
4. AAD만 RX로 복사
RX[0] -> AAD
5. crypto request
req->src = TXREQ[0] // source page-cache page, read side
req->dst = RX[0] // user output buffer, write side
Plain Text
복사
source가 page-cache backed일 수는 있다. 하지만 destination walk는 RX SGL만 따라간다. 그러므로 crypto code가 req->dst에 write해도 page cache source page가 destination으로 등장하지 않는다.
왜 authencesn에서 4-byte write가 문제가 되는가
Copy Fail은 algif_aead만 봐서는 마지막 조각이 빠진다. 실제 write는 crypto/authencesn.c에서 발생한다.
중요 경로는 decrypt다.
위치: crypto/authencesn.c:247-294
crypto_authenc_esn_decrypt(req)
authsize = crypto_aead_authsize(...)
assoclen = req->assoclen
cryptlen = req->cryptlen
dst = req->dst
cryptlen -= authsize
if (req->src != dst)
memcpy_sglist(dst, req->src, assoclen + cryptlen)
scatterwalk_map_and_copy(ihash, req->src,
assoclen + cryptlen, authsize, 0)
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)
dst = scatterwalk_ffwd(..., dst, 4)
crypto_ahash_digest(...)
crypto_authenc_esn_decrypt_tail(...)
Plain Text
복사
scatterwalk_map_and_copy(..., out=1)은 write다.
위치: include/crypto/scatterwalk.h:241-250
if out:
memcpy_to_sglist(sg, start, buf, nbytes)
else:
memcpy_from_sglist(buf, sg, start, nbytes)
Plain Text
복사
그리고 memcpy_to_sglist()는 scatterwalk.c에서 SGL을 따라가며 실제 page에 memcpy한다.
위치: crypto/scatterwalk.c:48-61, 76-87
그래서 dst가 어떤 SGL을 가리키는지가 중요하다.
vulnerable AF_ALG decrypt에서는 req->src == req->dst == RX SGL이고, 그 RX SGL 뒤에 source tag page가 chain되어 있다.
vulnerable req->dst walk
RX output page
--chain-->
source TAG page-cache page
Plain Text
복사
따라서 authencesn의 다음 write가 destination walk를 따라가다가 source page-cache page에 닿을 수 있다.
scatterwalk_map_and_copy(tmp + 1,
dst,
assoclen + cryptlen,
4,
1)
Plain Text
복사
이 write는 ESN 처리 과정의 scratch write다. authentication이 실패하더라도 이 write는 이미 일어난 뒤일 수 있다. 그래서 결과적으로 readable file의 page cache page가 4 byte 변조되는 primitive가 된다.
patched AF_ALG decrypt에서는 req->src != req->dst다.
patched req->src walk
source page-cache page: AAD || CT || TAG
patched req->dst walk
RX output page only
Plain Text
복사
그래서 같은 authencesn write가 발생해도 destination은 RX output buffer다. source page cache는 read side에만 남는다.
패치 diff를 한 줄로 요약하면
패치 전:
AEAD decrypt tries to build an in-place request.
RX SGL is used as src and dst.
TAG source SGL is chained behind RX SGL.
Some crypto code writes to req->dst.
If chained TAG SGL points to page cache, page cache is written.
Plain Text
복사
패치 후:
AEAD uses out-of-place request.
TX per-request SGL is src.
RX SGL is dst.
No source TAG SGL is chained into dst.
Crypto writes stay in RX output buffer.
Plain Text
복사
af_alg_pull_tsgl()의 dst_offset 제거도 같은 방향이다. offset을 줘서 TX SGL의 뒷부분만 새 destination SGL처럼 재사용하는 기능을 없앤 것이다.
skcipher 변경은 왜 같이 있나
crypto/algif_skcipher.c도 diff가 있다.
vulnerable:
af_alg_count_tsgl(sk, len, 0)
af_alg_pull_tsgl(sk, len, areq->tsgl, 0)
Plain Text
복사
patched:
af_alg_count_tsgl(sk, len)
af_alg_pull_tsgl(sk, len, areq->tsgl)
Plain Text
복사
위치: crypto/algif_skcipher.c:137-157
이건 root cause fix라기보다는 af_alg_count_tsgl() / af_alg_pull_tsgl() signature가 바뀌면서 caller를 맞춘 것이다. skcipher는 이미 per-request TX SGL을 source로 쓰고 RX SGL을 destination으로 쓰는 구조다.
skcipher_request_set_crypt(src = areq->tsgl,
dst = RX SGL,
len = len,
iv = ctx->iv)
Plain Text
복사
즉 Copy Fail의 핵심 패치는 AEAD 쪽 in-place decrypt 구조 제거다.
page cache 관점에서 다시 보기
Copy Fail에서 중요한 page 상태는 다음처럼 나눌 수 있다.
[normal sendmsg]
userspace buffer
-> memcpy_from_msg()
-> AF_ALG private kernel page
-> TX SGL points to private page
Plain Text
복사
이 경우 crypto code가 실수로 TX page를 destination으로 써도, 보통 AF_ALG가 할당한 private page가 깨진다.
[splice-backed path: kernel-internal MSG_SPLICE_PAGES]
file page cache page
-> iov_iter
-> extract_iter_to_sg()
-> TX SGL points to original page
Plain Text
복사
이 경우 TX SGL이 file page cache page를 가리킬 수 있다.
그런데 vulnerable AEAD decrypt가 TX tag page를 RX SGL 뒤에 chain했다.
RX output page -> chain -> TX source page-cache page
Plain Text
복사
그리고 authencesn은 req->dst에 4 byte를 썼다.
req->dst walk reaches TX source page-cache page
-> page cache write
Plain Text
복사
그래서 이 취약점은 단순히 “AF_ALG에 splice가 있다”가 아니다. 조건을 더 정확히 쓰면 다음이다.
1. AF_ALG send path can place source page references into TX SGL.
2. vulnerable algif_aead decrypt path chains source TAG SGL into RX/dst SGL.
3. authencesn decrypt writes ESN scratch data through req->dst.
4. req->dst walk can reach source page-cache-backed TAG page.
Plain Text
복사
이 네 개가 같이 맞물려야 Copy Fail primitive가 나온다.
함수별로 외울 포인트
crypto/af_alg.c:505-529 alg_create()
AF_ALG parent socket 생성. 아직 algorithm 없음.
Plain Text
복사
crypto/af_alg.c:148-206 alg_bind()
salg_type으로 algif module 선택.
salg_name으로 crypto algorithm/template 선택.
Plain Text
복사
crypto/algif_aead.c:424-441 aead_bind() / aead_setkey() / aead_setauthsize()
parent socket의 crypto_aead transform 설정.
Plain Text
복사
crypto/af_alg.c:410-470 af_alg_accept()
operation child socket 생성.
child->private = af_alg_ctx.
Plain Text
복사
crypto/af_alg.c:940-1121 af_alg_sendmsg()
CMSG로 op/iv/aadlen 저장.
input을 ctx->tsgl_list에 저장.
MSG_SPLICE_PAGES면 extract_iter_to_sg()로 page reference를 SGL에 넣음.
Plain Text
복사
include/linux/scatterlist.h:11-22 struct scatterlist
data buffer가 아니라 page + offset + length descriptor.
Plain Text
복사
include/linux/scatterlist.h:235-265 sg_chain()
두 SGL 배열을 이어 붙인다.
sg_next()가 chain을 따라간다.
Plain Text
복사
crypto/af_alg.c:704-765 vulnerable af_alg_pull_tsgl()
dst_offset으로 TX SGL 중간 이후만 dst SGL에 재지정 가능.
Plain Text
복사
crypto/af_alg.c:684-736 patched af_alg_pull_tsgl()
offset 제거. 앞에서부터 소비한 fragment만 그대로 dst로 옮김.
Plain Text
복사
crypto/algif_aead.c:205-248 vulnerable decrypt
RX SGL 뒤에 TAG source SGL을 chain.
req->src와 req->dst가 RX base를 공유.
Plain Text
복사
crypto/algif_aead.c:155-190 patched decrypt
per-request TX SGL을 source로 사용.
RX SGL을 destination으로 사용.
source TAG page를 dst chain에 넣지 않음.
Plain Text
복사
crypto/authencesn.c:247-294 crypto_authenc_esn_decrypt()
req->dst에 ESN scratch 4-byte write 발생.
취약 버전에서는 dst walk가 source page cache까지 이어질 수 있음.
Plain Text
복사
정리
AF_ALG 자체는 userspace crypto socket interface다. 하지만 Copy Fail에서 AF_ALG가 중요한 이유는 socket API 때문이 아니라 메모리 전달 방식 때문이다.
pipe-to-AF_ALG splice()는 kernel 내부에서 MSG_SPLICE_PAGES와 ITER_BVEC를 만들고, 입력 page를 복사하지 않은 채 reference 형태로 TX SGL에 넣을 수 있다. struct scatterlist는 실제 데이터를 담지 않고 page, offset, length만 가리킨다. 그래서 TX SGL이 page cache page를 가리키면, 그 SG entry가 나중에 destination path에 섞이는 순간 문제가 된다.
취약한 algif_aead decrypt path는 in-place 최적화를 위해 RX SGL 뒤에 TX tag SGL을 chain했다. 이 설계 때문에 req->dst가 user output buffer를 넘어 source tag page까지 따라갈 수 있었다. 그리고 authencesn은 ESN 처리를 위해 req->dst에 4 byte write를 수행한다.
패치는 AEAD를 out-of-place request로 되돌린다. source는 per-request TX SGL, destination은 RX SGL로 분리한다. 동시에 af_alg_pull_tsgl()에서 offset 기반 partial reassignment를 제거해서 TX SGL 뒷부분만 destination chain에 재사용하는 구조를 없앴다.
결국 핵심은 이것이다.
vulnerable:
source page-cache page can become part of req->dst
patched:
source page-cache page remains only req->src
req->dst is RX output buffer only
Plain Text
복사
이 request가 authencesn과 scatterwalk를 거쳐 실제 page-cache memcpy()로 이어지는 지점은 본편에서 계속 본다.