[GH-ISSUE #4627] [Feature] add support for VB3 (visual basic) #2407

Open
opened 2026-10-17 21:04:15 +00:00 by bsakaguchi · 5 comments
Owner

Originally created by @dfl on GitHub (Oct 17, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/4627

Before submitting

  • I searched the open and closed issues and pull requests for a related proposal.
  • I understand the contribution scope must be reviewed before integration; open questions are welcome here.
  • This request does not include credentials, private samples, or sensitive target data.

Area

Native analysis or decompilation

Current limitation

REA rejects 16-bit Windows (NE) executables during target resolution, and has no analysis for Visual Basic 3 programs, which compile to p-code interpreted by VBRUN300.DLL. A native disassembler only shows the interpreter, not the program's logic.

Use cases

  • Recovering the logic of legacy Windows 3.x / VB3 applications whose source is lost (the motivating case is Polaris, a 1990s astrology program).
  • Static triage of VB3 programs: which DLL functions they declare and call, which forms and event handlers they have, and which Win16/DOS services their statements reach.
  • Inspecting any NE image (Windows 3.x programs, DLLs, VBX controls) without a session provider.

Desired outcome and success criteria

  1. REA classifies NE images and inspects them with a provider-free inspect_ne_executable (draft PR #4626).
  2. REA can analyze VB3 p-code through ghidra-vb3 as a bring-your-own tool, the way it uses other external tools: procedures with file offsets, resolved calls (local Subs and Declared DLL functions), form/event names, variables with typed evidence, and reachable Win16/DOS calls, returned as Evidence with provenance.

Success: on a VB3 EXE, REA reports these facts with their file locations, and distinguishes recorded facts from inferences and unknowns.

Expected interaction with targets

Read existing files or captured data

Target and scope details (optional)

Static, read-only analysis of local files; nothing is executed, injected or modified. ghidra-vb3 is a Python tool (Ghidra script and standalone CLI with --json, schema ghidra-vb3/disassembly-v1). It reads VBRUN300.DLL and .VBX files that the user supplies; REA would bundle neither.

Open questions for maintainers:

  • Whether VB3 analysis belongs in REA as a bring-your-own provider adapter (like the existing external tools) or should stay a separate tool that REA only points to.
  • Token tables: ghidra-vb3 decodes p-code with DoDi's VBDIS3 opcode tables (vbdis3i.dat, vbdis3x.dat), included in that repository with attribution; their author stated no license. REA would not distribute them.
  • Dependencies: Python 3, and optionally capstone (for runtime-call mapping) and Ghidra's Win16 ordinal tables.
  • Test samples: the ground-truth programs below are original code and can be shared, but REA's rules do not allow committing binaries, so provider tests would need an external fixture path.

Verification available so far

I have a prototype or observed results

Verification details (optional)

Executed on macOS (arm64) unless noted.

NE inspection (#4626): inspect_ne_executable parses 110 real NE files (the VB3 IDE and runtime, 29 VBX controls, Polaris and its DLLs, two compiled test programs); segment counts and RCDATA offsets agree with ghidra-vb3's independent parser. The PR's validation section has the full check:pr results.

ghidra-vb3, against programs compiled with Visual Basic 3.0 from original source:

  • tests/vb3/types: all 57 declared variables (every scalar type as local, ByRef/ByVal parameter, Static, module-level and global, arrays, an object, event parameters) recover the declared storage class and type; 53 from types the opcodes record, 4 inferred.
  • tests/vb3/byref: argument passing. Constants passed ByRef go through a temporary whose token names the parameter type (all 7 types identified); 38 parameters typed only by argument passing (pass-through, chains, typed callers, ChDrive/Kill operands, ByVal String DLL arguments) are all recovered.
  • tests/vb3/usage: 40 variables whose type only their use shows (For counters, array elements in each storage class, array elements passed ByRef, comparisons), all recovered.
  • tests/vb3/stmts: 114 statements and functions. Their emitted tokens confirm the VBDIS3 keyword of 95 of 106 labelled cases; the other 11 are explained (conversions compile to generic conversion tokens, Print # and LoadPicture to generic tokens). For 32 statements with a known Win16/DOS behaviour, 31 reach it statically in VBRUN300.DLL 3.0.5.38 (e.g. Kill -> DOS delete, Shell -> WinExec, MsgBox -> MessageBox, InputBox$ -> DialogBoxParam, Open -> DOS open/create); Load is an open gap (window creation is 13 calls deep, where about half of all statements also reach it, so it cannot be attributed to Load).

On Polaris (a real VB3 application): 613 procedures, 3,483 resolved calls, 292 event procedures (266 named from the control models; 26 belong to a control with no model available), and 4,137 variables: 3,842 with recorded types, 145 inferred, 150 unknown. On the locals and parameters both tools type, recorded types agree with current vb3dec on 1,735 of 1,750 (99%). Of the 38 Declares whose parameter sizes are known, all match the bytes their calls push.

On the VB3 setup kit's SETUP1 as shipped with Polaris, compared with the setup kit source that comes with VB3 (not redistributable, so not committed): all 4 forms, their controls, events and Declares match exactly, and every parameter and Dim type matches (one other local stays unknown rather than guessed). Library signatures built from a stock compile of that source (hashes of token sequences, strings and DLL calls; no code or text) name 27 of the 29 procedures in Polaris's copy, 21 as exact matches and 6 as modified copies; the 2 unnamed are Polaris's own. They name nothing in Polaris's main program or the ground-truth programs.

Runtime-call mapping is static reachability ("may reach"), not proof of calls on every execution. ghidra-vb3 has run headless in Ghidra 12.1.4 with PyGhidra; it has not been tested on VB1/VB2 p-code or VBGuard-protected programs.

Alternatives considered

  • vb3dec (Go, MIT): a full VB3 decompiler that reconstructs .MAK/.FRM/.bas source. Its output has no file offsets, so it fits REA's provenance model less well. It could still serve as an optional source-level provider.
  • Porting VB3 decoding into REA itself: would mean distributing or re-deriving the VBDIS3 tables inside REA, which the bring-your-own approach avoids.

Possible direction

  1. Land NE inspection (#4626).
  2. With maintainer agreement on scope, add a ghidra-vb3 provider adapter that runs its CLI on an explicit target and maps its JSON to Evidence, with procedure/call/variable facts keyed by file offset and inferences marked as such.

References

Originally created by @dfl on GitHub (Oct 17, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/4627 ### Before submitting - [x] I searched the open and closed issues and pull requests for a related proposal. - [x] I understand the [contribution scope](https://github.com/morluto/rea/blob/main/CONTRIBUTING.md#contribution-scope) must be reviewed before integration; open questions are welcome here. - [x] This request does not include credentials, private samples, or sensitive target data. ### Area Native analysis or decompilation ### Current limitation REA rejects 16-bit Windows (NE) executables during target resolution, and has no analysis for Visual Basic 3 programs, which compile to p-code interpreted by `VBRUN300.DLL`. A native disassembler only shows the interpreter, not the program's logic. ### Use cases - Recovering the logic of legacy Windows 3.x / VB3 applications whose source is lost (the motivating case is Polaris, a 1990s astrology program). - Static triage of VB3 programs: which DLL functions they declare and call, which forms and event handlers they have, and which Win16/DOS services their statements reach. - Inspecting any NE image (Windows 3.x programs, DLLs, VBX controls) without a session provider. ### Desired outcome and success criteria 1. REA classifies NE images and inspects them with a provider-free `inspect_ne_executable` (draft PR #4626). 2. REA can analyze VB3 p-code through [ghidra-vb3](https://github.com/dfl/ghidra-vb3) as a bring-your-own tool, the way it uses other external tools: procedures with file offsets, resolved calls (local Subs and `Declare`d DLL functions), form/event names, variables with typed evidence, and reachable Win16/DOS calls, returned as Evidence with provenance. Success: on a VB3 EXE, REA reports these facts with their file locations, and distinguishes recorded facts from inferences and unknowns. ### Expected interaction with targets Read existing files or captured data ### Target and scope details (optional) Static, read-only analysis of local files; nothing is executed, injected or modified. ghidra-vb3 is a Python tool (Ghidra script and standalone CLI with `--json`, schema `ghidra-vb3/disassembly-v1`). It reads `VBRUN300.DLL` and `.VBX` files that the user supplies; REA would bundle neither. Open questions for maintainers: - Whether VB3 analysis belongs in REA as a bring-your-own provider adapter (like the existing external tools) or should stay a separate tool that REA only points to. - Token tables: ghidra-vb3 decodes p-code with DoDi's VBDIS3 opcode tables (`vbdis3i.dat`, `vbdis3x.dat`), included in that repository with attribution; their author stated no license. REA would not distribute them. - Dependencies: Python 3, and optionally capstone (for runtime-call mapping) and Ghidra's Win16 ordinal tables. - Test samples: the ground-truth programs below are original code and can be shared, but REA's rules do not allow committing binaries, so provider tests would need an external fixture path. ### Verification available so far I have a prototype or observed results ### Verification details (optional) Executed on macOS (arm64) unless noted. NE inspection (#4626): `inspect_ne_executable` parses 110 real NE files (the VB3 IDE and runtime, 29 VBX controls, Polaris and its DLLs, two compiled test programs); segment counts and RCDATA offsets agree with ghidra-vb3's independent parser. The PR's validation section has the full `check:pr` results. ghidra-vb3, against programs compiled with Visual Basic 3.0 from original source: - [`tests/vb3/types`](https://github.com/dfl/ghidra-vb3/tree/main/tests/vb3/types): all 57 declared variables (every scalar type as local, ByRef/ByVal parameter, Static, module-level and global, arrays, an object, event parameters) recover the declared storage class and type; 53 from types the opcodes record, 4 inferred. - [`tests/vb3/byref`](https://github.com/dfl/ghidra-vb3/tree/main/tests/vb3/byref): argument passing. Constants passed ByRef go through a temporary whose token names the parameter type (all 7 types identified); 38 parameters typed only by argument passing (pass-through, chains, typed callers, `ChDrive`/`Kill` operands, `ByVal` String DLL arguments) are all recovered. - [`tests/vb3/usage`](https://github.com/dfl/ghidra-vb3/tree/main/tests/vb3/usage): 40 variables whose type only their use shows (`For` counters, array elements in each storage class, array elements passed ByRef, comparisons), all recovered. - [`tests/vb3/stmts`](https://github.com/dfl/ghidra-vb3/tree/main/tests/vb3/stmts): 114 statements and functions. Their emitted tokens confirm the VBDIS3 keyword of 95 of 106 labelled cases; the other 11 are explained (conversions compile to generic conversion tokens, `Print #` and `LoadPicture` to generic tokens). For 32 statements with a known Win16/DOS behaviour, 31 reach it statically in `VBRUN300.DLL` 3.0.5.38 (e.g. `Kill` -> DOS delete, `Shell` -> `WinExec`, `MsgBox` -> `MessageBox`, `InputBox$` -> `DialogBoxParam`, `Open` -> DOS open/create); `Load` is an open gap (window creation is 13 calls deep, where about half of all statements also reach it, so it cannot be attributed to `Load`). On Polaris (a real VB3 application): 613 procedures, 3,483 resolved calls, 292 event procedures (266 named from the control models; 26 belong to a control with no model available), and 4,137 variables: 3,842 with recorded types, 145 inferred, 150 unknown. On the locals and parameters both tools type, recorded types agree with current vb3dec on 1,735 of 1,750 (99%). Of the 38 `Declare`s whose parameter sizes are known, all match the bytes their calls push. On the VB3 setup kit's `SETUP1` as shipped with Polaris, compared with the setup kit source that comes with VB3 (not redistributable, so not committed): all 4 forms, their controls, events and `Declare`s match exactly, and every parameter and `Dim` type matches (one other local stays unknown rather than guessed). Library signatures built from a stock compile of that source (hashes of token sequences, strings and DLL calls; no code or text) name 27 of the 29 procedures in Polaris's copy, 21 as exact matches and 6 as modified copies; the 2 unnamed are Polaris's own. They name nothing in Polaris's main program or the ground-truth programs. Runtime-call mapping is static reachability ("may reach"), not proof of calls on every execution. ghidra-vb3 has run headless in Ghidra 12.1.4 with PyGhidra; it has not been tested on VB1/VB2 p-code or VBGuard-protected programs. ### Alternatives considered - [vb3dec](https://github.com/jrmcpeek/vb3dec) (Go, MIT): a full VB3 decompiler that reconstructs `.MAK`/`.FRM`/`.bas` source. Its output has no file offsets, so it fits REA's provenance model less well. It could still serve as an optional source-level provider. - Porting VB3 decoding into REA itself: would mean distributing or re-deriving the VBDIS3 tables inside REA, which the bring-your-own approach avoids. ### Possible direction 1. Land NE inspection (#4626). 2. With maintainer agreement on scope, add a ghidra-vb3 provider adapter that runs its CLI on an explicit target and maps its JSON to Evidence, with procedure/call/variable facts keyed by file offset and inferences marked as such. ### References - ghidra-vb3: https://github.com/dfl/ghidra-vb3 - vb3dec and its VBDIS3 audit: https://github.com/jrmcpeek/vb3dec - VBDIS3 by DoDi (Hans-Peter Diettrich), the original VB3 p-code disassembler - NE inspection PR: #4626
Author
Owner

@XinwangTao commented on GitHub (Oct 17, 2026):

VB3 (16-bit) P-code decompilation would be a genuinely useful addition — 16-bit Windows binaries (.NE / VB runtime v1/v2) are still common in legacy malware and forensic samples, and tooling for them is scarce. The hardest part is usually mapping the P-code opcode stream back to high-level constructs (event handlers, form layout, Variant coercion), so having a prototype that already walks the .NE file format is a strong starting point. I'd suggest also looking at how existing VB decompilers handle the translation of P-code calls into Win16 API references, since that is what trips up most implementations. Happy to help test against real samples if you share them in a follow-up.

<!-- gh-comment-id:6227978571 --> @XinwangTao commented on GitHub (Oct 17, 2026): VB3 (16-bit) P-code decompilation would be a genuinely useful addition — 16-bit Windows binaries (.NE / VB runtime v1/v2) are still common in legacy malware and forensic samples, and tooling for them is scarce. The hardest part is usually mapping the P-code opcode stream back to high-level constructs (event handlers, form layout, Variant coercion), so having a prototype that already walks the .NE file format is a strong starting point. I'd suggest also looking at how existing VB decompilers handle the translation of P-code calls into Win16 API references, since that is what trips up most implementations. Happy to help test against real samples if you share them in a follow-up.
Author
Owner

@dfl commented on GitHub (Oct 17, 2026):

Thanks! The P-code side already exists as a separate tool, and I should have linked it here: dfl/ghidra-vb3. It works as a Ghidra script and also standalone with --json. #4626 is only the NE format inspection REA needs first. I've updated the issue body with the details.

On your points:

  • Event handlers and forms: event procedures are named from each form's event tables, using the control models in VBRUN300.DLL and the project's .VBX files, e.g. Form10.Command1_Click (Index As Integer). Module slots are mapped to forms and controls.
  • Variables and Variant coercion: variables are typed from the types the opcodes record. The Variant conversion tokens are mapped by source type. Anything inferred from usage is labelled as inferred, with the rule that produced it.
  • P-code to Win16 calls: two layers.
    • Calls to Declared functions resolve to library and entry point, with parameter lists inferred from call sites.
    • For the runtime's own statements, tools/vbrun_map.py walks the handlers in VBRUN300.DLL and records the Win16 and DOS functions they can reach: Kill -> DOS delete, Shell -> WinExec, MsgBox -> MessageBox, InputBox$ -> DialogBoxParam. Tokens turn out to be the handlers' own offsets in the runtime (threaded code), and most statements go through an inline runtime-function dispatch the tool decodes. It's static reachability, so it's labelled "may reach".

Verification uses programs compiled with VB3 from original source, so they can be shared:

  • tests/vb3/types: 57 declared variables, and all 57 storage classes and types are recovered.
  • tests/vb3/stmts: 114 statements. Their tokens confirm the VBDIS3 keyword tables, and 30 of 32 statements with a known Win16/DOS behaviour reach it. Open and Load are open gaps.

It's VB3 only; VB1/VB2 p-code is untested. Samples would help, especially VB1/2 or VBGuard-protected ones, as long as you can share them. For anything sensitive, results from running the tool are just as useful as the binaries.

<!-- gh-comment-id:6229167517 --> @dfl commented on GitHub (Oct 17, 2026): Thanks! The P-code side already exists as a separate tool, and I should have linked it here: **[dfl/ghidra-vb3](https://github.com/dfl/ghidra-vb3)**. It works as a Ghidra script and also standalone with `--json`. #4626 is only the NE format inspection REA needs first. I've updated the issue body with the details. On your points: - **Event handlers and forms:** event procedures are named from each form's event tables, using the control models in `VBRUN300.DLL` and the project's `.VBX` files, e.g. `Form10.Command1_Click (Index As Integer)`. Module slots are mapped to forms and controls. - **Variables and Variant coercion:** variables are typed from the types the opcodes record. The Variant conversion tokens are mapped by source type. Anything inferred from usage is labelled as inferred, with the rule that produced it. - **P-code to Win16 calls:** two layers. - Calls to `Declare`d functions resolve to library and entry point, with parameter lists inferred from call sites. - For the runtime's own statements, `tools/vbrun_map.py` walks the handlers in `VBRUN300.DLL` and records the Win16 and DOS functions they can reach: `Kill` -> DOS delete, `Shell` -> `WinExec`, `MsgBox` -> `MessageBox`, `InputBox$` -> `DialogBoxParam`. Tokens turn out to be the handlers' own offsets in the runtime (threaded code), and most statements go through an inline runtime-function dispatch the tool decodes. It's static reachability, so it's labelled "may reach". Verification uses programs compiled with VB3 from original source, so they can be shared: - [`tests/vb3/types`](https://github.com/dfl/ghidra-vb3/tree/main/tests/vb3/types): 57 declared variables, and all 57 storage classes and types are recovered. - [`tests/vb3/stmts`](https://github.com/dfl/ghidra-vb3/tree/main/tests/vb3/stmts): 114 statements. Their tokens confirm the VBDIS3 keyword tables, and 30 of 32 statements with a known Win16/DOS behaviour reach it. `Open` and `Load` are open gaps. It's VB3 only; VB1/VB2 p-code is untested. Samples would help, especially VB1/2 or VBGuard-protected ones, as long as you can share them. For anything sensitive, results from running the tool are just as useful as the binaries.
Author
Owner

@XinwangTao commented on GitHub (Oct 17, 2026):

Thanks for the detailed breakdown — the two-layer mapping (Declared functions vs runtime statement handlers via threaded-code offsets) is a much cleaner design than trying to decode every call statically, and labeling reachability as "may reach" with provenance is the right call for runtime dispatch. The Open/Load gaps are notable; if useful, I can try to dig up VB1/VB2-era samples from public archives and run the tool against them — happy to share results rather than binaries if licensing is a concern.

<!-- gh-comment-id:6229391609 --> @XinwangTao commented on GitHub (Oct 17, 2026): Thanks for the detailed breakdown — the two-layer mapping (Declared functions vs runtime statement handlers via threaded-code offsets) is a much cleaner design than trying to decode every call statically, and labeling reachability as "may reach" with provenance is the right call for runtime dispatch. The Open/Load gaps are notable; if useful, I can try to dig up VB1/VB2-era samples from public archives and run the tool against them — happy to share results rather than binaries if licensing is a concern.
Author
Owner

@dfl commented on GitHub (Oct 17, 2026):

sure, please go ahead and test it out. thanks!

<!-- gh-comment-id:6229415022 --> @dfl commented on GitHub (Oct 17, 2026): sure, please go ahead and test it out. thanks!
Author
Owner

@XinwangTao commented on GitHub (Oct 17, 2026):

Will do. I'll look for VB1/VB2-era samples in public archives (and VBGuard-protected ones if any surface) and run ghidra-vb3 against them. I'll report back with the test results — sharing output rather than binaries where licensing is unclear.

<!-- gh-comment-id:6229486290 --> @XinwangTao commented on GitHub (Oct 17, 2026): Will do. I'll look for VB1/VB2-era samples in public archives (and VBGuard-protected ones if any surface) and run ghidra-vb3 against them. I'll report back with the test results — sharing output rather than binaries where licensing is unclear.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#2407
No description provided.