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:
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
Unpatched
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.
The current main head and latest official release tag were both verified through the same resolver I/O path. The PoC is not an offline packet parser: it starts a loopback DNS-over-TCP server, lets c-ares issue real TCP DNS queries, sends the two-response EDNS retry sequence, and then resets the connection before cleanup consumes the stale state.
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 upstreammain.evidence/v1.34.6-gdb.txt- sanitized GDB stack forv1.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
FORMERRwithout 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:
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:
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:
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
gcccmakegit- POSIX sockets and pthreads
The build script links a static PoC binary against the static libcares.a produced from the supplied checkout.
Run
chmod +x ./cares_tcp_uaf_calc_poc
./scripts/run_until_hit.sh ./cares_tcp_uaf_calc_poc
Expected success:
CARES_RCE_PAYLOAD_TRIGGERED
run=N rc=77
c-ares control-flow payload reached pid=...
The proof writes:
/tmp/cares_rce_proof_latest
Calculator launch order:
- WSL Windows Calculator through
/mnt/c/Windows/System32/calc.exe - WSL Windows Calculator through
cmd.exe /c start "" calc.exe xcalcgnome-calculatorkcalc
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:
upstream main c93e50f3: run=1 rc=77
v1.34.6 release: run=1 rc=77
Additional local repeat testing against the release build reached the control-flow marker in consecutive runs:
run=1 rc=77 hit=true
run=2 rc=77 hit=true
run=3 rc=77 hit=true
run=4 rc=77 hit=true
run=5 rc=77 hit=true
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:
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/caresand 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
aresDNS 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.caresresolveras a resolver using c-ares throughpycares. Other Python packages such aspycaresandaiodnsmay bring c-ares into applications. - Rust wrappers - crates such as
c-aresand 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:
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:
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 environments.