230 lines
10 KiB
Markdown
230 lines
10 KiB
Markdown
# 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
|
|
|
|
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 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
|
|
```
|
|
|
|
Additional local repeat testing against the release build reached the control-flow marker in consecutive runs:
|
|
|
|
```text
|
|
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:
|
|
|
|
```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 environments.
|