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
Evidence 01 / segment 5 : 0D5F
The defender rolled 2, then 3.
But the original compares against:
That 6 belongs to the attacker.
One wrong memory operand. Real casualties.
An explanatory reconstruction, not a screenshot. The changed byte is at file offset 0x6360. The rest of the repair involves other bytes too.
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
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 bytesDS:07ECPlayers. Three 24-byte records, including names, controller flags and card counts.72 bytesDS:0834Territories. 42 records of 22 bytes. Ownership and army count are the first two words.924 bytesDS:0BD2Turn state. State, active player, selections and flags coordinate the input paths.sharedDS:0BF0Attacker dice. Three word-sized slots, ranked high to low.6 bytesDS:0BF6Defender dice. Two word-sized slots immediately after the attacker’s array.4 bytesThese 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.
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
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
Enumerate all ordered face combinations for each legal dice configuration and compare both explanatory models with a separate, straightforward sort-and-compare reference.
| Dice | Cases | Original wrong | Repaired wrong |
|---|
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
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
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
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
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
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
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.
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
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.
| Layer | Established | Not established |
|---|---|---|
| File inspection | Segments, resources, imports, relocations, byte changes, fingerprints | A successfully launched Windows program |
| Instruction harness | Exercised routine behaviour with deterministic inputs and explicit API/runtime doubles | All GDI, dialog, timing and operating-system behaviour |
| This browser exhibit | Interactive explanations, extracted geometry, independently tested JavaScript models | Execution of the original EXE or a replacement game |
| Native integration | Still required: a complete graphical game in the intended Win16 environment | Not 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
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.