19 KiB
Reverse-engineering procedure
This is the working method used to replace VDDK’s NBD path with
OpenVixDiskLib. Protocol details live in docs/nfc_auth.md,
docs/nfc_open.md, docs/nfc_read.md, and docs/nfc_write.md.
The capture tool is described in docs/ssl_hook.md.
This file is the sequence of steps, including dead ends, so later
NFC work can follow the same loop instead of rediscovering it.
Scope so far: VixDiskLib_ConnectEx + VixDiskLib_Open +
VixDiskLib_Read + VixDiskLib_Write against lab vCenter 8.0.1 /
ESXi 8, transports nbd and nbdssl. Validation method:
tests/integration/ (the session-scoped lab fixture creates a temporary
empty VM with a 10 GiB disk and destroys it when the pytest session ends).
Rule from AGENTS.md: reuse pyVmomi for every public VIM operation.
Only reimplement what pyVmomi does not expose.
Loop
Each unknown stage (ticket SOAP, authd, NFC binary) went through:
- Name it from VDDK logs and
stringson the bundled libraries. - See it on the wire (or prove that tcpdump cannot).
- Replay the smallest working subset in Python against the lab.
- Write findings into a protocol doc and keep the hook out of the library path.
Do not skip (2). Log lines such as SESSIONID or useSSL=0 named the
wrong wire command until the intercept existed.
Lab and artifacts
| Item | Where / value |
|---|---|
| VDDK 8.0.2 | .vddk/ (libvixDiskLib, libvddkVimAccess, libvim-types) |
| pyVmomi | .venv |
| Known-good VDDK client | tests/integration/test_vddk.py / tests/integration/vixdisklib.py |
| Verbose NFC logs | vixDiskLib.nfc.LogLevel=4 in a temp VDDK config |
| ctypes capture drivers | docs/probing_samples/ (not library code) |
SSL / write hook |
/tmp/sslhook.c → /tmp/sslhook.so |
Pickled LabEnv |
/tmp/vddk-write-wire-lab.pkl during hooked captures only |
Always set LD_LIBRARY_PATH to .vddk/ so VDDK uses its own
libssl.so.3. Unset LD_PRELOAD before running OpenVixDiskLib;
a leftover write hook will crash pyVmomi’s TLS.
Tools
tcpdump was the first capture attempt and is the wrong tool for TLS stages (Step 3). Everything that actually produced protocol bytes or names is in this table.
| Tool | What it was used for | Limitation |
|---|---|---|
| tcpdump on 443 / 902 | Prove VDDK talks to vCenter then ESXi:902; see TLS record sizes | Ciphertext only: no SOAP, authd lines, or NFC headers |
strings -a on .vddk/*.so |
Candidate tokens (SESSION, NfcGetVmFiles, NFC_AIO_MSG_*) |
Not command order, spacing, or replies |
nm -D / objdump -T |
Which library imports SSL_write vs write; exported APIs |
Not wire layout |
VDDK vixDiskLib.nfc.LogLevel=4 |
Function names and AIO opId / type / size to label a frame |
Not magic numbers, path placement, or BANNER \r\n |
LD_PRELOAD SSL / write hook |
Plaintext of SOAP, authd, and (after PROXY) NFC on fd 902 | Must not stay on the OpenVixDiskLib process; docs/ssl_hook.md |
strace -f -x on write / send* |
First writable NFC capture without rebuilding the hook (Step 10) | Noisy; TLS still opaque; -s truncates large extras |
pickle of LabEnv |
Create the temp VM unhooked, then load it under the hook | /tmp only; never commit pickles (lab host and credentials) |
ctypes drivers in docs/probing_samples/ |
Repeatable ConnectEx / Open / Read / Write under capture |
Not library code |
ltrace was considered for OpenSSL and libc write. It was not used:
VDDK is stripped enough that strace on syscalls plus the LD_PRELOAD
hook were enough. An ESXi impersonator (AGENTS.md) was also not
needed; the lab already answered VDDK.
Step 1 — Map the public VDDK calls
tests/integration/test_vddk.py is the specification of what
“success” looks like: login, open the temporary VM’s VMDK, write a
known pattern, read it back.
Turn on VDDK verbose logging around InitEx / ConnectEx / Open. The
logs split the work that the Python API hides:
ConnectEx→ VIM login only.Open→ NFC ticket, authd, NFC handshake, AIO open, then I/O.transport_modes=nbd→ URL formvpxa-nfc://[ds] path.vmdk@esxi:902.snapshot_refdoes not appear on the ticket SOAP call.
That mapping is the table at the top of docs/nfc_auth.md. It tells you
which stage to reverse next and which arguments belong there (VM moref
on the ticket, VMDK path on NFC OPEN_FILE).
Step 2 — Strings and pyVmomi before any capture
strings -a on .vddk/*.so produced candidate tokens before a single
packet was decoded:
- SOAP:
NfcService,NfcGetVmFiles,nfcService,ha-nfc,HostServiceTicket. - authd:
SESSION,BANNER,THUMBPRINT_SHA2,PROXY,USER,PASS,SSL Required. - NFC:
NFC_HANDSHAKE,NFC_CONNECTION_DATA,NFC_AIO_MSG_*,NFC_DISK.
Then check whether pyVmomi already has the type:
from pyVmomi import vim
hasattr(vim, "NfcService") # False
hasattr(vim, "HostServiceTicket") # True
ServiceManager.QueryServiceList on the live vCenter does not list
NFC. GET /sdk/nfcServiceVersions.xml does (urn:nfc 7.0.3.2). The
moref is hardcoded in VDDK (nfcService on vCenter, ha-nfc on ESXi).
Anything public (SmartConnect, vim.VirtualMachine,
HostServiceTicket) stays in pyVmomi. Missing managed types are
registered with CreateManagedType on the same SOAP stub so cookies
and serialization are not reimplemented.
Step 3 — Confirm tcpdump is the wrong tool for TLS stages
tcpdump on 443 and 902 shows TLS records only. That is enough to prove “something talks to vCenter then to ESXi:902”, and not enough for SOAP bodies, authd lines, or NFC headers.
VDDK logs name functions and AIO opId / type / size. They do not
give magic numbers, path placement, or command spacing (BANNER \r\n).
An ESXi-impersonating service was considered (AGENTS.md) and not
needed: the lab answers VDDK, so capturing the real client is simpler
than simulating the server.
Step 4 — Interpose OpenSSL (authd and SOAP)
VDDK 8.0.2 still calls SSL_write / SSL_read. A small LD_PRELOAD
library logs those buffers as hex, tagged with the SSL * pointer.
Run a minimal ctypes program (InitEx, ConnectEx, Open, Read)
under:
export LD_LIBRARY_PATH=…/.vddk
export LD_PRELOAD=/tmp/sslhook.so
export SSLHOOK_LOG=/tmp/sslhook-open.log
python /tmp/vddk_open_trace.py
Parse offline:
- Concatenate adjacent same-direction records (authd
SSL_readis often one byte). - Split by
SSL *. vCenter HTTPS containsPOST /sdkand SOAP. ESXi:902 containsSESSION/PROXY. - An early hook without the pointer mixed both streams; always tag.
The vCenter stream identified the ticket as NfcGetVmFiles with moref
nfcService and xmlns="urn:vim25" (not a guess from strings alone).
The ESXi stream gave the authd command order, including the trailing
space on BANNER and THUMBPRINT_SHA2 PlainText.
Hook implementation notes: docs/ssl_hook.md.
Step 5 — Probe SOAP, then replay only what VDDK sends
With a SmartConnect session, raw SOAP posts were used to learn parameter names and which moref vCenter accepts:
ha-nfcon vCenter →ManagedObjectNotFound.NfcGetServerNfcLibVersionwithouthostForAccess→ invalid argument; with a host moref →11.NfcGetVmFiles(vm)→HostServiceTicket(VDDK read-only Open).NfcRandomAccessOpenReadonly(vm, diskDeviceKey, host)→ same ticket type, disk-scoped read.NfcRandomAccessOpenDisk(vm, diskDeviceKey, host)→ writable ticket.GetVmFilesplusOPEN_FILEflags0x1afails withVIX_E_FILE_READ_ONLY(0x0b).
OpenVixDiskLib registers those methods and calls them through pyVmomi. It does not ship a hand-rolled SOAP client for login or tickets.
Step 6 — Probe authd; record dead ends
Plaintext banner (220 … SSL Required), then ssl.wrap_socket.
Commands before TLS drop the connection.
vCenter UID/password are not sent to port 902. Attempts that failed and must not be retried for this ticket type:
| Attempt | Result |
|---|---|
USER / PASS (vCenter account) |
530 Login incorrect |
USER / PASS with sessionId |
530 Login incorrect |
SESSIONID <sessionId> |
530 Please login with USER and PASS |
CONNECT_VPXA after TLS |
530 Please login with USER and PASS |
Wait for a reply after SESSION |
Hang until BANNER / PROXY follow |
THUMBPRINT_SHA2 with SHA-1 digest |
501 Invalid arguments |
The working sequence is in docs/nfc_auth.md. useSSL=0 in the VDDK
log means skip a second NFCSSL wrap, not skip TLS on 902.
Replay: openvixdisklib/nfc_auth.py /
tests/integration/test_nfc_auth.py. Stop at 200 Connect.
Step 7 — NFC binary: extend the hook to write / read
After PROXY, SSL_write on the ESXi SSL * goes silent. VDDK logs
useSSL=0 / “plain-text connection is deprecated” and then NFC
function names. The bytes are write(SSL_get_fd(ssl), …) / read on
peer port 902.
The hook was extended to those syscalls, filtered with getpeername
port 902, and mutex-locked (VDDK is multi-threaded). Skip TLS records
(16 03 / 17 03) left over from the authd phase.
Correlate each frame with the verbose log line that has the same
type and size (NfcAioSendMessage: opId = … type = … size = …).
Name the types from the consecutive NFC_AIO_MSG_* string table in
libvixDiskLib.so. Lengths 4 and 7 on the connection-data message are
the ASCII strings vddk and nbdmode sent in the next two writes.
Classic NFC uses a 264-byte padded struct; AIO uses a 16-byte header
(magic 0xA100DA7A) plus payload; path / DDB key / sector data are
extra writes not included in size.
strace is a usable second view of this same plaintext NFC path when
the hook is not loaded. It cannot replace the hook for TLS (authd and
SOAP). How it was run, and why pickle sits between VM create and the
hooked VDDK process, is in Step 10.
Step 8 — Replay the smallest subset, then compare to VDDK
Python must dup the authd fd and send NFC as raw TCP.
SSLSocket.send would encrypt; unwrap() would SSL_shutdown. VDDK
does neither.
OpenVixDiskLib (openvixdisklib/nfc_open.py) replays handshake + AIO
OPEN_SESSION / sockopts / resource pool / OPEN_FILE. VDDK’s extra
DDB_GET keys were omitted once a file handle was enough to read. Proof
of open: tests/integration/test_nfc_open.py.
Do not copy every VDDK message. Copy what the server requires for the Python API you are replacing.
Step 9 — Vary VixDiskLib_Read until IO fields stop moving
A single-sector read is not enough to decode NFC_AIO_MSG_IO. Drive
VDDK with several (startSector, numSectors) pairs in one Open
(including n=128 = 64 KiB and n=129) under the write/read hook.
What that comparison showed:
- Wire units are bytes (
offset = start * 512,length = n * 512). - Request size stays 44; data is extra after the payload.
- VDDK sends one request even when
length > 65536. The server replies with several type-7 messages that shareopId, each with a chunk length at payload offset 32 (max 65536). - Treating offset 36 as
NFC_DISK(2) was a 1-sector coincidence; VDDK repeats the byte length there. - Zeros on the wire are real transferred zeros, not a sparse skip.
Replay: NfcDisk.read places fragments at the byte offset in the
reply (they may arrive out of order) until length bytes are filled.
Proof: tests/integration/test_nfc_read_write.py writes a known pattern
(including a 129-sector read that must assemble two fragments) and
checks the bytes that came back.
Step 10 — Writes: strace, then the same IO message
The first writable Open was captured with strace, not the SSL
hook. After PROXY, NBD NFC is ordinary write / read on the authd
fd (useSSL=0). strace dumps those buffers as hex without compiling
sslhook.so. TLS to vCenter and the authd handshake stay ciphertext
in the same log, so this is only useful once Step 7 has already shown
that NFC is plaintext.
unset LD_PRELOAD
export LD_LIBRARY_PATH=…/.vddk
strace -f -x -s 2048 \
-e trace=write,writev,send,sendto,sendmsg \
-o /tmp/vddk_write.strace \
python docs/probing_samples/vddk_write_trace.py
| Flag | Why |
|---|---|
-f |
VDDK I/O runs on worker threads; without it the NFC write is missing |
-x |
Hex, so AIO magic and binary payloads are searchable |
-s 2048 |
Fits a 264-byte classic frame plus a 44-byte IO header and one 512-byte sector. Truncates 64 KiB extras; use the hook for those |
-e trace=write,… |
Drop open/mmap noise. Still includes Python logging writes |
Parse offline: search for AIO magic 7a da 00 a1 (little-endian
0xA100DA7A), then keep the fd that also sent 264-byte frames or
PROXY. That stream showed:
OPEN_FILEflags0x1a(read-write), not the read-only0x1e- IO direction
0at payload offset 8 (read is1) - 44-byte IO header and the sector extra in one
write
Later write captures (64 KiB fragments, FastLZ) used the port-902
write/read hook instead, because -s would clip the extra. To keep
pyVmomi’s TLS off that hook, the temp VM was created in a separate
process and the LabEnv was pickled:
# Process A: no LD_PRELOAD (VIM login + CreateVM)
lab = create_lab_vm()
with open("/tmp/vddk-write-wire-lab.pkl", "wb") as f:
pickle.dump(lab, f)
# Process B: LD_PRELOAD=/tmp/sslhook.so, SSLHOOK_LOG=…
with open("/tmp/vddk-write-wire-lab.pkl", "rb") as f:
lab = pickle.load(f)
# VixDiskLib_ConnectEx / Open / Write on lab.disk_path
Pickles lived under /tmp only. They contain lab host, credentials,
and the VM moref; do not commit them. Destroy the VM in an unhooked
process after the capture (destroy_lab_vm).
VixDiskLib_Write uses the same 44-byte NFC_AIO_MSG_IO layout as
read. The direction field at offset 8 is 0 instead of 1, and the
sector bytes are sent after the payload (like the path on
OPEN_FILE). Writable OPEN_FILE flags are 0x1a (the captured
read-only flags 0x1e with bit 0x04 cleared, matching
VIXDISKLIB_FLAG_OPEN_READ_ONLY in .vddk/vixDiskLib.h).
Writable OPEN_FILE still failed with VIX_E_FILE_READ_ONLY until
the ticket switched from NfcGetVmFiles to NfcRandomAccessOpenDisk
(libvim-types.so: vmodl randomAccessOpen ↔ WSDL
NfcRandomAccessOpenDisk). Integration tests create a temporary empty
10 GiB VM for the run so writes cannot land on other lab disks.
A write larger than 64 KiB is one AIO opId with several type-7
request fragments (same layout as read replies: total length, then
fragment offset / length) and a single 44-byte ACK. Separate
VixDiskLib_Write calls are not coalesced. Header and extra go in one
sendall, with TCP_NODELAY. Details: docs/nfc_write.md. Proof:
write then read in tests/integration/test_nfc_read_write.py and the
VDDK cross-check in tests/integration/test_crosscheck.py.
Step 11 — NBDSSL: second TLS after PROXY vpxa-nfcssl
VDDK strings name nbdssl, vpxa-nfcssl://, and ha-nfcssl. The NFC
ticket SOAP call is unchanged (service stays vpxa-nfc). Transport is
an authd/client choice:
- Same
SESSION/BANNER/THUMBPRINT_SHA2 PlainTextas NBD. PROXY vpxa-nfcssl→200 Connect ha-nfcssl.- A new TLS handshake on the same TCP connection (not TLS-in-TLS
and not
THUMBPRINT_SHA2 <sha256>). The colon thumbprint is still501 Invalid arguments. - Classic NFC handshake type 43 still sends ASCII
PlainText. I/O framing is unchanged.
Replay: connect_authd(..., nfc_ssl=True) plus
nfc_open.wrap_nfcssl_socket. Sending NFC on the first authd
SSLSocket after ha-nfcssl fails (BAD_RECORD_TYPE); sending
plaintext NFC on the dup'd fd gets EOF. Dup + wrap_socket is the
working subset. Proof: tests/integration/test_nfc_open.py (nbdssl)
and test_openvixdisklib.py with transport_modes="nbdssl".
Native VixDiskLib_ConnectEx(..., transport_modes="nbdssl") through
the old VixDiskLibConnectParams ctypes struct can still log nbdssl
and then fall back to vpxa-nfc / useSSL=0. Do not treat that log
line as a wire capture of NFCSSL.
Step 12 — FASTLZ NBD compression
VIXDISKLIB_FLAG_OPEN_COMPRESSION_FASTLZ (1 << 5) is an IO codec,
not an OPEN_FILE bit. Capture VDDK with that flag (NBD + the port-902
write/read hook):
- Handshake stays
PlainText.OPEN_FILEflags stay0x1a/0x1e. - VDDK’s URL is
FASTLZ-vpxa-nfc://…;PROXYis stillvpxa-nfc. - IO opcode
uint64= direction in the low half, compression type in the high half (2= FastLZ). Offset 32 is uncompressed length; offset 36 is compressed extra size when type is 2. - Incompressible chunks fall back to type
0and raw extra. - 64 KiB chunks use FastLZ level 2; smaller chunks use level 1.
Replay: pip pyfastlz via openvixdisklib/fastlz.py (NFC extra is
raw FastLZ, without the wrapper's 4-byte length prefix) plus NfcDisk
compression on each IO. Proof:
tests/integration/test_nfc_read_write.py (fastlz) and
tests/perf/test_compare.py.
What to write down
After a stage works:
| Document | Contents |
|---|---|
docs/nfc_auth.md |
Ticket SOAP + authd wire format |
docs/nfc_open.md |
Classic NFC + AIO open |
docs/nfc_read.md |
AIO IO / VixDiskLib_Read |
docs/nfc_write.md |
AIO IO / VixDiskLib_Write |
docs/ssl_hook.md |
Capture tool only |
docs/reverse_engineering_procedure.md |
This procedure (update when the method changes) |
Keep the hook and ctypes driver under /tmp. They are not part of
OpenVixDiskLib.
Next stages (same procedure)
Not yet reversed, same loop as above:
DDB_GET/ disk geometry, zlib/skipz compression, encrypted disksNFC_DELTA_DISK, CBT /QueryAllocatedBlocksVixDiskLib_GetInfocapacity- Host-switch AIO messages
- Direct ESXi
ha-nfcwithout vCentervpxa-nfc