Reconstruction of des.exe/undes.exe

This commit is contained in:
2026-09-09 23:28:46 +02:00
parent b90136f137
commit 5aa37bf374
64 changed files with 13344 additions and 2 deletions
+162
View File
@@ -0,0 +1,162 @@
# Reconstruction notes — DES/UNDES V1.1.0
## Confidence
The main program logic is now reconstructed with high confidence. The supplied
`cryptl99.zip` is especially useful because it contains the exact CRYPTLIB 0.99
API and libdes 3.14 source that was linked into the executables.
This is **not** the original source text. Original variable names, comments,
whitespace and file decomposition were not retained in the executable. Names
such as `firstTime`, `passwordBlock`, `negativeMarker`, and the split into
`des_common.c`/`.h` are descriptive reconstruction choices.
## Compiler and memory model
Both programs are 16-bit MS-DOS MZ executables and contain the runtime string:
`Turbo-C - Copyright (c) 1988 Borland Intl.`
The generated code uses far data pointers extensively, and `malloc()` returns a
segment:offset pointer. That is consistent with a Turbo C large-memory-model
build.
## Exact crypto dependency
The executables contain:
`libdes v 3.14 - eay`
The supplied CRYPTLIB archive contains the same `libdes/version.h` string, plus
`crypt.h`, `crypt.c`, `lib_3des.c` and the relevant libdes sources. This is the
actual dependency family used by the executables rather than merely a later
compatible library.
## Important correction: it is 3DES
The machine code assigns value 3 to both the algorithm and mode variables.
According to the supplied `crypt.h`:
- `CRYPT_ALGO_3DES` = 3
- `CRYPT_MODE_CBC` = 3
Therefore the encryption is **two-key 3DES-CBC**, despite the executable being
called `des.exe`.
`crypt.c` defines the recommended 3DES-CBC parameters as:
- key size: 112 bits = 14 bytes
- IV size: 32 bits = 4 bytes
- DES block size: 64 bits = 8 bytes
The main routine obtains both sizes through `queryAlgoModeInformation()`.
## Password handling
The command-line password is copied directly into a 14-byte key buffer and is
zero-padded if shorter. Characters after byte 14 do not influence encryption.
There is no password hash or KDF in the DES/UNDES layer.
CRYPTLIB's `des3InitKey()` then constructs the two DES key schedules from that
user-key buffer. `des_check_key` in the bundled libdes defaults to zero, so
parity/weak-key checking is not enabled by default.
## File layout
For a non-empty encrypted input the file is:
```text
Offset Size Meaning
0 4 file version: 0x00002775
4 4 CRYPTLIB-recommended CBC IV
8 256 encrypted password-check block
264 16384 encrypted data block 1
16648 16384 encrypted data block 2
... ...
```
For an empty input, `des.exe` writes only the four-byte version. Because no
`encryptBuffer()` call with data occurs, no IV or password-check block is
written.
## Data-block layout before encryption
Each 16384-byte encrypted data block consists of:
```text
0..3 signed 32-bit length marker
4..7 signed 32-bit secondary marker
8..16383 at most 16376 bytes input data
```
For a partial final block, the first field is the positive plaintext byte count.
For a full block it is a negative value; `undes.exe` interprets any negative
value as exactly 16376 plaintext bytes.
The full-block negative value is formed from the absolute value of the first
signed 16-bit word of the plaintext and then negated. If that produces zero,
`-1` is stored instead. The second field is also a negative marker derived from
a signed 16-bit plaintext word. `undes.exe` does not use this second field.
The exact purpose of the secondary marker cannot be established from the
executables alone; its computation is established, its design intent is not.
## CBC ordering on the first block
An unusual but confirmed detail is the first-block ordering.
`des.exe` performs the cryptographic operations in this order:
1. encrypt first 16 KiB data block
2. retrieve the now-generated IV
3. encrypt the 256-byte password-check block
but writes:
1. IV
2. encrypted password-check block
3. encrypted first data block
`undes.exe` reads the stored password block and first data block, then decrypts
in cryptographic order:
1. decrypt first data block
2. decrypt password-check block
This preserves the CBC state exactly. The apparent storage/processing order
mismatch is therefore intentional in the program logic, not a decompiler error.
## Password-check block
Before encryption, the first 14 bytes of the 256-byte password-check block are
the derived 14-byte key. The executable does not initialise the remaining 242
bytes before encrypting the block. During decryption only the first 14 bytes
are compared.
## Uninitialised bytes in the final data block
`des.exe` always encrypts the complete 16384-byte buffer. If the final input
chunk is shorter than 16376 bytes, the unused tail is not explicitly cleared
first. This is also visible in the machine code. UNDES writes only the number
of plaintext bytes stored in the first 32-bit field.
## Error handling and robustness
Read errors are detected only when `read()` returns `-1`; short reads are not
rejected. `undes.exe` does not authenticate ciphertext and does not validate a
positive decrypted length against 16376 before passing it to `fwrite()`. These
are normal limitations for small DOS-era utility code, but they matter if old
encrypted files are corrupted or attacker-controlled.
## Remaining uncertainty
The following cannot be recovered exactly without original source/debug
symbols:
- original variable/function names for the DES/UNDES-specific code;
- original comments and formatting;
- exact source-file decomposition;
- the design rationale for the secondary 32-bit marker;
- the symbolic name/formula originally used for file version `0x2775`.
The actual program behaviour no longer depends on guessed CRYPTLIB API names.