0x4D44 / Software archaeologyCase file · WINRISK.EXE · 1992The evidence ↗

Inside WINRISK / An interactive binary autopsy

A tiny byte.
A different war.

An old Windows game. No source code. A defender who quietly borrows the attacker’s best die. This is how we took WINRISK apart, proved what was wrong, and repaired it without making the executable a single byte larger.

DECEMBER 1992 SOFTWARE · SEPTEMBER 2026 INVESTIGATION

WINRISK.EXE — the wrong die

Evidence 01 / segment 5 : 0D5F

The defender rolled 2, then 3.

But the original compares against:

36

That 6 belongs to the attacker.
One wrong memory operand. Real casualties.

A1 F0 0B
A1 F6 0B

An explanatory reconstruction, not a screenshot. The changed byte is at file offset 0x6360. The rest of the repair involves other bytes too.

220,672bytes, before and after
10executable segments
10,836combat cases per build
2,317 → 0combat disagreements

What was actually verified: selected routines ran in a custom 16-bit x86 instruction harness, with Windows APIs replaced by test doubles. No native Windows GUI session or complete played game was validated. The browser experiments below explain the findings; they are not the binary harness.

01 / The specimen

A whole world in 215½ KiB.

WINRISK is a three-player territory-conquest game for 16-bit Windows. Its executable contains the rules, computer opponents, dialogs, map artwork and the invisible geometry that decides where you clicked.

The About dialog identifies Freeware Version 2.11, Steve Stancliff, December 1992. A second label in the executable says Windows Risk 2.01. Both are original evidence, not a discrepancy to conceal: the repair leaves both untouched. We describe the exact supplied file by its SHA-256 fingerprint, rather than assume that every download bearing the same title contains these bugs. [Audit: identification]

The brief was unusually constrained: find real bugs, fix the executable and preserve the game. There was no original C project to rebuild. The investigation therefore worked in three layers: map the file’s structures, follow the machine instructions, and construct small experiments that force a suspected error to become observable.

The game you see

Claim territories, place armies, choose an attack or movement, roll up to three attacker dice against up to two defender dice, occupy conquered territory, and receive cards after a turn with a conquest. Eliminate the other players to win.

The machine underneath

Windows delivers messages. A window procedure forwards actions to game-state code. That code consults fixed-size records and updates the board; drawing routines turn the resulting state back into pixels. Several defects live at these boundaries, not in the main rules.

Not every surprising behaviour warranted a patch. The 42-node adjacency graph had valid reciprocal edges and no self-neighbours. The AI was examined but not rewritten: a weak strategic choice is not automatically a correctness defect. Unexplained pre-existing NOP runs were preserved rather than “cleaned up” on intuition.

02 / Inside the machine

An executable is a small city.

The file is a Windows New Executable (NE), not a modern PE image. Code, shared data, imported services and resources have distinct addresses and jobs.

The DOS header points to the NE header at 0x0250. Its segment table describes nine code segments and one data segment. Calls into USER, GDI, KERNEL and WIN87EM are resolved through imports and relocations. The audit counted 530 relocation records and 59 resources. Those are part of the loader’s contract, so a repair cannot treat apparently unused bytes as free space without checking what follows them.

Instrument 01

Choose a segment

EXTRACTED FILE STRUCTURE

Segment details load with the interactive exhibit.

A composition chart, not file order. The bitmap resource occupies about 70% of the file; all nine original code segments together occupy 22,126 bytes. Resource lengths include their allocated padding.

The shared data is the real board

Most game operations do not act on pictures. They index small records in the data segment. The following names describe roles recovered during the audit; they are not recovered source-code identifiers.

DS:0784Deck. 44 two-byte entries; a cursor tracks the next draw.88 bytes
DS:07ECPlayers. Three 24-byte records, including names, controller flags and card counts.72 bytes
DS:0834Territories. 42 records of 22 bytes. Ownership and army count are the first two words.924 bytes
DS:0BD2Turn state. State, active player, selections and flags coordinate the input paths.shared
DS:0BF0Attacker dice. Three word-sized slots, ranked high to low.6 bytes
DS:0BF6Defender dice. Two word-sized slots immediately after the attacker’s array.4 bytes

These are data-segment offsets, not positions in the EXE. For example, code segment 5 starts at file offset 0x5600; instruction offset 0x0D5F therefore maps to file offset 0x635F. Its operand begins one byte later.

Windows message→window procedure→hit test→state transition→record update→redraw

The same click has different meanings in different states. Clicking the sea during territory selection should not select an imaginary country. Clicking the sea after a battle may legitimately dismiss the result. A blanket “reject every background click” patch would stop the corruption but break the interface.

03 / A borrowed die

The dice were fair.
The comparison wasn’t.

The defender’s insertion-sort code reads from the wrong array. When a higher defender roll arrives, it preserves the attacker’s maximum instead of the defender’s previous maximum.

With attacker rolls 6, 5, 1 and defender rolls 2, then 3, the correct comparisons are 6 against 3 and 5 against 2. Both go to the attacker. The original instead stores 3 and 6 for the defender, so its second comparison costs the attacker an army. Roll order matters: reverse the defender’s rolls and that particular error disappears.

Instrument 02

Put the two builds on the same rolls

BROWSER MODEL
Attacker · click a die to change it
Defender · preserve roll order

Original

Attacker loses 1; defender loses 1.

Repaired

Attacker loses 0; defender loses 2.

Equal dice cost the attacker an army. Only the top min(attacker dice, defender dice) comparisons count. The model reproduces the documented sorting operation; it does not execute x86 instructions or reproduce the original random generator.

; segment 5 : 0D5F
A1 F0 0B   mov ax, [0BF0h]
; highest ATTACKER die
; segment 5 : 0D5F
A1 F6 0B   mov ax, [0BF6h]
; previous DEFENDER maximum

This is a one-byte repair: F0 → F6 at 0x6360. The following instruction still writes AX to the defender’s second slot at 0x0BF8. We change the source, not the rules, number of dice or random generator. [Patch manifest]

Instrument 03

Don’t trust a single example

EXHAUSTIVE BROWSER CHECK

Enumerate all ordered face combinations for each legal dice configuration and compare both explanatory models with a separate, straightforward sort-and-compare reference.

Not run in this browser yet.

The binary audit goes further than this browser demonstration: it executes the decoded combat instructions, supplies deterministic random results, and checks army counts and return-state codes. It finds 2,317 disagreements in the original and none in the repair across the same 10,836 configurations. That fraction is not an estimate of the percentage of real battles affected: different dice configurations do not occur equally often in a played game. [Recorded regressions]

04 / Between the turns

The bugs in the gaps.

A battle can be calculated correctly and still lead to the wrong game. Messages, cancelled dialogs, card cursors and continuation paths all have their own contracts.

One physical click, two queued actions

After a combat result, the old mouse handler posts a dismissal message but carries on through normal hit testing. It posts another action from the same click. By the time that second action is handled, the first may already have advanced the state. The repair returns after the first post; ordinary input continues through the normal path.

Instrument 04

Watch the message queue

EXPLANATORY TRACE

Original queue

Empty

Repaired queue

Empty

Each press shows one click in the selected context, not a full emulated message loop. The audit tested actual PostMessage boundaries: 2 → 1 posts for result dismissal, one for normal human clicks, none for a computer turn.

Off-map is a sentinel, not a country

A failed hit test returns -1. Without a range check, the dispatcher calculates territories + (-1 × 22), which lands in the preceding player storage. In a crafted test state, two preceding words change from (3, 5) to (0, 6). This proves an out-of-range write; it does not say that every sea click corrupts an ordinary game.

Instrument 05

Follow the index into memory

ADDRESS CALCULATION

The guard accepts only 0–41 for territory-dependent actions. It separately preserves background dismiss, cancel, movement-end and middle-button pass behaviour. The actual regression covered 399 invalid index/state/button combinations and all 42 valid initial-claim targets.

An elimination that misses its handover

Conquering a player’s final territory sets a pending-elimination flag. The optional troop-movement continuation processes it and transfers the defeated player’s cards. But when compulsory movement leaves only one army behind, execution follows the ordinary result-dismissal path instead. That path used to return directly to attack selection, bypassing the transfer.

Instrument 06

Inspect a recorded conquest sequence

BINARY TEST FIXTURE

This reads the supplied instruction-harness results, not a new conquest simulation. Each case used a one-army final defending territory and a third player still alive. The fixture gives the defeated player two cards. Starting stacks 2, 3 and 4 expose the original skip; 5–9 take the working path.

The fix checks the pending flag during result dismissal, routes through elimination processing when necessary, then clears the flag. A second continuation must not award the same cards twice; the audit checked that too.

The missing forty-fourth card

The shuffle creates 44 entries, numbered 0–43. The old draw routine reshuffles when the cursor is greater than 42, before slot 43 can be used. Changing the comparison constant from 0x2A to 0x2B makes the check agree with the deck’s actual length.

Instrument 07

Walk across the deck boundary

CONTROLLED DECK MODEL

For visibility, this deck holds 42 ordinary cards then two wildcards, and a reshuffle restarts that same ordering. In a genuinely shuffled game, the skipped final card need not be a wildcard. The patch makes no claim of a stronger shuffle algorithm.

Cancel must mean cancel

The Setup dialog originally edited live player records. Cancel closed the window but did not undo a changed name or human/computer flag. The wrapper also accepted any nonzero return value, inadvertently treating dialog-creation failure (-1) as success.

Instrument 08

Edit a name, then choose the outcome

TRANSACTION MODEL

Each experiment begins with “Player1”. A creation failure occurs before edits can happen. The repaired wrapper stages three records in a 72-byte stack buffer and commits only names and controller flags on an explicit OK. It does not copy stale army/card fields over live gameplay data.

05 / Invisible borders

The map you click is not the map you see.

The artwork and the hit regions are separate resources. Eight malformed coordinates distort seven countries’ clickable outlines while the printed borders look perfectly normal.

Eastern Australia contains an x-coordinate of 346 instead of 546. Indonesia contains a y-coordinate of 476 instead of 276. Other slips affect Middle East, Taymyr, Buryat, China and India. These corrections were inferred from neighbouring vertices, shared borders and the original map bitmap; no artwork was altered. [Audit: polygon repairs]

Instrument 09

Inspect the actual polygon data

EXTRACTED VERTICES
Original: dashed redRepaired: solid green
Original and repaired territory polygonsChoose a territory to compare its outlines. Use the coordinate fields below for keyboard-controlled hit testing. These are region shapes, not the game’s original bitmap.

Move the pointer or tap the diagram to test a point, or edit x/y above. The dots mark corrected vertices. The witness comes from the binary audit’s geometry tests. Browser classifications use nonzero winding fill; native GDI’s precise pixel-edge behaviour has not been validated. The grey base outlines are the repaired resource geometry, not the unchanged bitmap artwork.

A marker mistaken for a vertex

Each point list ends with (−100, −100). That is a terminator, not a distant corner of the world. Both polygon consumers passed it to CreatePolygonRgn: the hit-test path in segment 4 and the bounds path in segment 3. Both loops needed repair.

Switch off “Include original sentinel” above to separate that error from the malformed coordinates. With a correctly closed polygon, the extra trip out to the sentinel and back can enclose zero area. So it would be wrong to claim that this bug visibly corrupts every territory. Buryat’s malformed closing point is a particularly revealing combination: the bad endpoint and the sentinel can create an unintended filled wedge.

The audit exercised all 42 resources through both actual polygon-construction routines: 84 repaired API-boundary counts, with no terminator included. The geometric witness points demonstrate changed classifications; they are not screenshots of a native Windows session.

06 / Binary surgery

Make room without moving the world.

Some repairs fit in one operand. Others need new control flow. The challenge is to add that logic without invalidating the executable’s layout.

The card threshold and combat operand are in-place changes. The input guard, elimination continuation and transactional Setup wrapper need more instructions. The repair adds 94 bytes to segment 5 and 58 bytes to segment 9, using existing file padding. Near jumps divert the old paths into these helpers, which then return to the existing code.

existing routine→near-jump hook→helper in padding→existing continuation

Padding is not automatically safe to overwrite. Each affected segment’s relocation table originally followed its code. The patch moves that unchanged table forward within available padding and updates the segment’s length and minimum allocation. Other code and resource offsets remain fixed. The whole file still occupies 220,672 bytes.

Changed deliberately

Selected operands and branches; segment 5 and 9 lengths; their helper bodies and relocation-table positions; eight coordinates in seven polygon resources.

Preserved deliberately

Exported entry points, imported modules, all relocation targets and sites, the bitmap, menus, dialogs, strings, icons and original credits. There are no new runtime dependencies.

Instrument 10

Read the patch ledger

EXACT MANIFEST

This displays the supplied patcher’s operation manifest. Helper entries describe relocation-table moves rather than listing every moved byte as an independent operation. The manifest should not be treated as a flat “write these offsets” patch format.

A hash-locked patcher accepts only the exact original fingerprint and reproduces the repaired fingerprint. That prevents a plausible-looking patch from being applied to a different revision where the same file offsets could mean something else. The original was kept for rollback.

07 / The repair ledger

Nine defects.
One bounded repair set.

The source audit groups these findings into eight sections, with the two Setup defects together. They are separated here so each contract failure can be inspected on its own. Open an entry for the location, witness and repair.

01The defender borrows an attacker die

SEG 5:0D5F · FILE 0x6360 · F0 → F6

Witness: attacker 6,5,1; defender 2,3. Original: one loss each. Repaired: defender loses two.

Repair: read the defender’s previous maximum at DS:0BF6 rather than attacker maximum DS:0BF0 when inserting a new defender maximum.

Validation: 10,836 ordered dice cases per build; casualties and result states checked by the instruction harness. Original 2,317 disagreements; repaired zero. Operate the dice lab →

02An invalid index reaches player memory

SEG 5:02E1 · FILE 0x58E1 · GUARD HELPER

Witness: “no territory” is −1, but the original can still multiply it by the 22-byte record size. A crafted case changes preceding words from (3,5) to (0,6).

Repair: accept 0–41 before territory-dependent actions. Preserve the states in which background clicks intentionally dismiss, cancel or pass.

Validation: 399 invalid-index/state/button combinations preserve the protected records; 42 valid initial-claim cases match the original. This does not establish every possible re-entrant message sequence.

03A dismissal click falls through

SEG 4:03BC–03D1 · FILE 0x53BD / 0x53CE

Witness: one click on a combat result produces two posts: dismissal, then normal action.

Repair: return after the dismissal post. Ordinary input goes directly to its normal hit-test path, also bypassing an obsolete instruction-count delay loop.

Validation: one result-dismissal message after repair. Normal left, right and middle clicks still post once; computer-controlled turns do not post a human action.

04An eliminated player keeps their cards

SEG 5:0996 · FILE 0x5F96 · CONTINUATION HELPER

Witness: final conquest with starting attacker stacks of 2, 3 or 4 bypasses card transfer on the tested continuation path; a later attack can clear the still-pending flag.

Repair: result dismissal honours pending elimination, processes the transfer and clears the flag.

Validation: complete routine-level conquest/continuation sequences for stacks 2–9; cards transfer in all repaired cases and do not transfer twice on later processing.

05The final deck entry is never drawn

SEG 5:0E4A · FILE 0x644E · 2A → 2B

Witness: the old cursor limit skips slot 43 in a 44-entry deck. An ordered fixture yields card-type totals [15,14,14,1] in its first 44 awards rather than [14,14,14,2].

Repair: reshuffle only when the cursor is greater than 43. Slot 43 is draw 44; the next draw triggers the shuffle.

Validation: the 44th/45th boundary, no-conquest/no-card behaviour and deterministic complete shuffles. An arbitrary shuffled deck may lose an ordinary card, not a wildcard.

06Setup edits survive Cancel

SEG 9:02C2 WRAPPER · HOOKS AT 02D1 / 0331

Witness: changing a name or controller flag modifies live player records before the dialog returns. Cancel has no rollback.

Repair: pass a staged 72-byte copy to the dialog. On OK, copy only configuration fields back. Army and card fields must not be overwritten by that temporary snapshot.

Validation: the actual wrapper and temporary pointers are exercised at a simulated DialogBoxParam boundary. Cancel leaves configuration unchanged; OK commits; unrelated live changes survive.

07Dialog failure is treated as acceptance

SEG 9:0331 · FILE 0x8931 · EXPLICIT OK TEST

Witness: creation failure returns −1 (0xFFFF as a 16-bit word). It is nonzero, so the old wrapper treats it as success.

Repair: commit only when the return value equals 1, not when it merely differs from zero.

Validation: simulated return values 0, 1 and −1. A failed dialog leaves configuration unchanged and the repaired wrapper returns failure. A native resource-exhaustion scenario was not run.

08Eight coordinates pull seven borders apart

CUSTOM RESOURCE TYPE 28685 · IDS 27, 30, 33, 36, 37, 39, 42

Witness: local polygon outlines contain large coordinate outliers. Each repaired region has a point whose old and new hit classifications differ.

Repair: correct Middle East, Taymyr, Buryat, China, India (two vertices), Indonesia and Eastern Australia. Inference is grounded in adjacent vertices, shared borders and the embedded bitmap, not a redesign of the map.

Validation: eight changed vertices across seven resources; geometric witness tests. The visible artwork remains byte-identical. Explore the outlines →

09Two callers draw the end-of-list marker

SEG 3:04A4 / 04AB · SEG 4:0243 / 024A

Witness: both callers pass the terminating (−100,−100) to CreatePolygonRgn as another vertex.

Repair: count only real vertices in both the bounding-region and point-hit-test consumers.

Validation: all 42 polygons through both routines, for 84 correct boundary counts. The sentinel may add zero area to a correctly closed outline; not every affected call implies a visible bug.

Exact byte operations are in the patch manifest; the full audit retains the original finding groups and qualifications.

08 / Proof & limits

Evidence, not a victory lap.

The strongest part of a repair is not how convincing the explanation sounds. It is whether the bad case fails before the patch, passes after it, and leaves the surrounding contracts intact.

What each layer does—and does not—establish
LayerEstablishedNot established
File inspectionSegments, resources, imports, relocations, byte changes, fingerprintsA successfully launched Windows program
Instruction harnessExercised routine behaviour with deterministic inputs and explicit API/runtime doublesAll GDI, dialog, timing and operating-system behaviour
This browser exhibitInteractive explanations, extracted geometry, independently tested JavaScript modelsExecution of the original EXE or a replacement game
Native integrationStill required: a complete graphical game in the intended Win16 environmentNot claimed by this audit

The Python harness deliberately fails on unsupported instructions rather than pretending to execute them. Linear disassembly can mistake embedded jump-table data for instructions, so decoding is restarted at function boundaries. Imported API and calling-convention identification was cross-checked against Wine’s Win16 specification files. These precautions reduce specific sources of error; they do not turn a purpose-built harness into a complete Windows machine. [Audit: method and confidence]

Which executable does this describe?

Original SHA-256

b829f76340d6848a697b7a4b499c60c27bb109921a00b96fd771eb5bff894ae3

Repaired SHA-256

9fcdf5e021d089c135eb6b39f609f495b955e71a4d7798fbe41844c1284fcc76

Instrument 11

Identify your own copy

LOCAL-ONLY HASH CHECK

Select an EXE to compare its size and SHA-256. The file stays in this tab: no upload, no execution, no modification. A recognised hash is identity evidence, not a safety certificate.

No file selected. The site does not distribute either executable.

What remains to be tested

Play a complete graphical game in the intended Windows or emulator environment. Exercise Setup Cancel and OK; click the corrected territories and the sea; dismiss multiple combat results; eliminate a player and check the card handover. Observe actual GDI regions, resource loading and message behaviour. General low-resource failures, every possible re-entrant message sequence and exhaustive AI validation remain outside the repair set.

The absence of another justified patch is not proof that no other bug exists. Likewise, preserving the binary’s structure is necessary but not sufficient for compatibility. This is a bounded, tested repair with a remaining integration check—not a certification that WINRISK is bug-free.

Continue through the Machine Room.

See how Win16 high-level emulation rebuilds a Windows API environment, explore the evolution of x86, or follow the earlier machines in A World in 640K.

Original game credited to Steve Stancliff. This is an unofficial analysis and repair exhibit. The displayed polygon drawings are generated from resource coordinates; they do not reproduce the original bitmap. The original audit is dated 14 September 2026; this exhibit was prepared on 15 September 2026.