[GH-ISSUE #2825] [Feature] Recognize Delphi AnsiString literals (short and cp1252) in string inventory and referenced_strings #1008

Open
opened 2026-10-15 18:30:18 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @matbrgz on GitHub (Oct 13, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/2825

Type: Feature Domain: Reverse Engineering Status: Proposed Ecosystem: REA

Before submitting

  • I searched open and closed issues/PRs (string, Pascal, charset, referenced_strings, Delphi). Closest precedent: #2030 (Swift String literals in native function results). Related: #2810, #2811.
  • I understand the contribution scope must be reviewed before integration.
  • No credentials, private samples or sensitive data.

Area

Native analysis or decompilation

Current limitation

Delphi (2–2007) stores string constants as AnsiString literals: FF FF FF FF (refcount −1), a 32-bit length, the bytes in the ANSI code page (cp1252 here), and a NUL. With the Ghidra provider, REA's search_strings, trace_feature and analyze_function.referenced_strings lose two classes of them:

  1. Short literals (1–3 bytes) are not defined as strings at all. In this target the APAC "fifth digit" rule compares a field with "6", "7", "0", "9" before choosing a SQL filter. analyze_function on 0x65caac lists 40 referenced strings but none of these four, so the per-digit branching is invisible. read_bytes 0x65d528 16 shows the literal: ffffffff 01000000 37000000 ("7"); inspect_address_context 0x65d530 reports DAT_0065d530.
  2. Literals with non-ASCII cp1252 bytes are truncated at the first accented byte. search_strings "Localizado" returns 0x65d632 "o Localizado" (the literal is "Não Localizado" at 0x65d630), and search_strings "faixa dispon" returns nothing, although "Não existe faixa disponível !" is referenced by four event handlers. Most user-facing messages in Portuguese, Spanish, French or German Delphi apps are affected.

The string addresses are referenced by immediate operands (mov edx, imm32 / push imm32), so the references exist; only the defined-data inventory is missing or split. In the pseudocode the pushed pointers sometimes appear as doubles (local_58 = 9.713838074096593e-307).

Environment: macOS 27.0.1 arm64, Node 24.13.0, rea-agents@6.4.0 (local install, MCP over stdio, no agent setup), Ghidra 12.1.4 (portable, native decompiler built for mac_arm_64), JDK 21.0.12. REA_GHIDRA_SEED_FILE with 1,610 Delphi method names; same result with REA_GHIDRA_COMPILER_SPEC_ID=borlanddelphi.

Proposed outcome

  • A provider-independent recognizer for Delphi AnsiString literals (and ShortString len + bytes where unambiguous) in initialized code/data sections, validated by refcount −1, length, terminating NUL and an exact immediate reference to the payload address.
  • Decode with an explicit, recorded code page (default cp1252 for Win32 Delphi ANSI builds, overridable), keeping the raw bytes.
  • Merge the results into search_strings, trace_feature and referenced_strings with a distinct provenance (delphi-ansistring), so short and accented literals are visible with their source addresses.
  • Keep it static; no execution, no provider database writes unless the caller asks for labels.

Use cases

Recovering business rules from Delphi line-of-business software, where SQL fragments and validation messages are AnsiString constants and branch selectors are 1-character literals.

Minimal public repro

  • ftp://arpoador.datasus.gov.br/siasus/autorizador/AUTORIZADOR_VER6630.exe (sha256 0c79af43ce52a9317591e081ebfc9f29f70d5575033b2eb9e177d0828a634c44) → innoextract -e → app/AUTORIZADOR.exe (sha256 1398d55a71a9e4ac04f7a12f5f1bec9d0b4343fbd1c0f8635938d261f18fa363).
  • open_binary (ghidra) → analyze_function {procedure: "0x65caac"} → compare referenced_strings with the immediates at 0x65cd10 ("6"), 0x65ce3c ("7"), 0x65cec8 ("0"), 0x65cf6a ("9"), 0x65d0d1 ("Procedimento não foi encontrado. …").
  • search_strings {pattern: "faixa dispon"} → []; the literal is referenced from TFCadAPAC.bitsalvarClick (0x658cf0).

Suggested approach

A small post-analysis pass (TypeScript over the PE snapshot, or a Ghidra script in the bridge) scanning for the FF FF FF FF + length + NUL pattern whose payload address is the target of an immediate operand; the external reference scanner in our research repo (tools/disasm_delphi.py, dstr()) does this in ~15 lines.

Originally created by @matbrgz on GitHub (Oct 13, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/2825 [![Type: Feature](https://img.shields.io/badge/type-feature-blue.svg?style=flat-square)](https://github.com/morluto/rea) [![Domain: Reverse Engineering](https://img.shields.io/badge/domain-reverse_engineering-blueviolet.svg?style=flat-square)](https://github.com/morluto/rea) [![Status: Proposed](https://img.shields.io/badge/status-proposed-blue.svg?style=flat-square)](https://github.com/morluto/rea) [![Ecosystem: REA](https://img.shields.io/badge/ecosystem-morluto%2Frea-8A2BE2.svg?style=flat-square)](https://github.com/morluto/rea) ### Before submitting - [x] I searched open and closed issues/PRs (`string`, `Pascal`, `charset`, `referenced_strings`, `Delphi`). Closest precedent: #2030 (Swift String literals in native function results). Related: #2810, #2811. - [x] I understand the contribution scope must be reviewed before integration. - [x] No credentials, private samples or sensitive data. ### Area Native analysis or decompilation ### Current limitation Delphi (2–2007) stores string constants as **AnsiString literals**: `FF FF FF FF` (refcount −1), a 32-bit length, the bytes in the ANSI code page (cp1252 here), and a NUL. With the Ghidra provider, REA's `search_strings`, `trace_feature` and `analyze_function.referenced_strings` lose two classes of them: 1. **Short literals (1–3 bytes) are not defined as strings at all.** In this target the APAC "fifth digit" rule compares a field with `"6"`, `"7"`, `"0"`, `"9"` before choosing a SQL filter. `analyze_function` on `0x65caac` lists 40 referenced strings but none of these four, so the per-digit branching is invisible. `read_bytes 0x65d528 16` shows the literal: `ffffffff 01000000 37000000` (`"7"`); `inspect_address_context 0x65d530` reports `DAT_0065d530`. 2. **Literals with non-ASCII cp1252 bytes are truncated at the first accented byte.** `search_strings "Localizado"` returns `0x65d632 "o Localizado"` (the literal is `"Não Localizado"` at `0x65d630`), and `search_strings "faixa dispon"` returns nothing, although `"Não existe faixa disponível !"` is referenced by four event handlers. Most user-facing messages in Portuguese, Spanish, French or German Delphi apps are affected. The string *addresses* are referenced by immediate operands (`mov edx, imm32` / `push imm32`), so the references exist; only the defined-data inventory is missing or split. In the pseudocode the pushed pointers sometimes appear as doubles (`local_58 = 9.713838074096593e-307`). Environment: macOS 27.0.1 arm64, Node 24.13.0, `rea-agents@6.4.0` (local install, MCP over stdio, no agent setup), Ghidra 12.1.4 (portable, native decompiler built for mac_arm_64), JDK 21.0.12. `REA_GHIDRA_SEED_FILE` with 1,610 Delphi method names; same result with `REA_GHIDRA_COMPILER_SPEC_ID=borlanddelphi`. ### Proposed outcome - A provider-independent recognizer for Delphi AnsiString literals (and ShortString `len + bytes` where unambiguous) in initialized code/data sections, validated by refcount −1, length, terminating NUL and an exact immediate reference to the payload address. - Decode with an explicit, recorded code page (default cp1252 for Win32 Delphi ANSI builds, overridable), keeping the raw bytes. - Merge the results into `search_strings`, `trace_feature` and `referenced_strings` with a distinct provenance (`delphi-ansistring`), so short and accented literals are visible with their source addresses. - Keep it static; no execution, no provider database writes unless the caller asks for labels. ### Use cases Recovering business rules from Delphi line-of-business software, where SQL fragments and validation messages are AnsiString constants and branch selectors are 1-character literals. ### Minimal public repro - `ftp://arpoador.datasus.gov.br/siasus/autorizador/AUTORIZADOR_VER6630.exe` (sha256 `0c79af43ce52a9317591e081ebfc9f29f70d5575033b2eb9e177d0828a634c44`) → `innoextract -e` → `app/AUTORIZADOR.exe` (sha256 `1398d55a71a9e4ac04f7a12f5f1bec9d0b4343fbd1c0f8635938d261f18fa363`). - `open_binary` (ghidra) → `analyze_function {procedure: "0x65caac"}` → compare `referenced_strings` with the immediates at `0x65cd10` (`"6"`), `0x65ce3c` (`"7"`), `0x65cec8` (`"0"`), `0x65cf6a` (`"9"`), `0x65d0d1` (`"Procedimento não foi encontrado. …"`). - `search_strings {pattern: "faixa dispon"}` → `[]`; the literal is referenced from `TFCadAPAC.bitsalvarClick` (`0x658cf0`). ### Suggested approach A small post-analysis pass (TypeScript over the PE snapshot, or a Ghidra script in the bridge) scanning for the `FF FF FF FF` + length + NUL pattern whose payload address is the target of an immediate operand; the external reference scanner in our research repo (`tools/disasm_delphi.py`, `dstr()`) does this in ~15 lines.
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#1008
No description provided.