Skip to content

Add a VMProtect-packed ELF whose version tables are not at their file offsets - #230

Open
zardus wants to merge 1 commit into
masterfrom
cle383-verneed-fixture
Open

Add a VMProtect-packed ELF whose version tables are not at their file offsets#230
zardus wants to merge 1 commit into
masterfrom
cle383-verneed-fixture

Conversation

@zardus

@zardus zardus commented Sep 10, 2026

Copy link
Copy Markdown
Member

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Problem

Nothing in this repository is a test input for an ELF whose symbol-version tables sit at a
virtual address that is not also a valid file offset, so cle's ELF loader cannot be tested
against one. Measured over the 940 tracked paths whose content is an ELF at 0166109e: 578
DT_VERNEED entries and 40 DT_VERDEF entries, and in every one of the 618 the table's RVA and
its file offset are the same number.

Root cause

An RVA is a usable file offset wherever a segment still sits at the image base plus its own file
offset. The count above is the measurement: at 0166109e nothing tracked here puts a version
table anywhere else.

Fix

Add tests/x86_64/vmprotect_sample1.vmp.bin, a VMProtect-packed x86-64 ELF, 3048571 bytes,
sha256 b7b577bd569a10cdd1c660c28c8e78e8ae35af8add43f0f57766d899dbd03564.

Its seven PT_LOAD segments carry five different p_vaddr - p_offset deltas, up to
0xA00000. DT_VERNEED is 0xCE3808 and the image base is 0x400000, so the RVA is
0x8E38080x5FB38D bytes past the end of a 0x2E847B-byte file — while the table really
begins at file offset 0x2E3808, 0x600000 lower.

It is a published third-party sample: vmp_binaries/binaries/sample1.vmp.bin in
JonathanSalwan/VMProtect-devirtualization
at 971be45920b504586a9c94fc7df941f8f57261b1, the last commit to touch that path, and
byte-identical to the sample the reporter attached to angr/cle#383 in 2023. All three copies
hash to the sha256 above. GitHub reports no licence for that repository ("license": null) and
it has no top-level LICENSE file; every licence text in it sits under pin/licensing/ and
belongs to the Intel Pin distribution it vendors.

Testing

Loading it under cle master raises ELFParseError: expected 2, found 0. The consumer is a
pull request against angr/cle adding tests/test_symbol_versions.py, which fixes that and
asserts on this file; it references this one, so GitHub cross-links the two. This repository
runs no test suite of its own, so nothing here reads the file.

Expected CI: no check runs and no commit statuses, which is what this repository's three most
recent pull requests each carry.

Validation: #230 (comment)

🤖 Generated with Claude Code

session: sharpen

… offsets

cle reads DT_VERNEED and DT_VERDEF through pyelftools, which wants a file
offset. Every ELF tracked here puts those tables at an RVA that is also a
valid file offset, so nothing covers the case where the two differ: over the
940 tracked paths whose content is an ELF at 0166109 there are 578
DT_VERNEED and 40 DT_VERDEF entries, and all 618 have RVA == file offset.

tests/x86_64/vmprotect_sample1.vmp.bin is
vmp_binaries/binaries/sample1.vmp.bin from
JonathanSalwan/VMProtect-devirtualization at
971be45920b504586a9c94fc7df941f8f57261b1, byte-identical to the sample
attached to angr/cle#383 in 2023. sha256
b7b577bd569a10cdd1c660c28c8e78e8ae35af8add43f0f57766d899dbd03564

Its DT_VERNEED is 0xCE3808 against an image base of 0x400000, so the RVA is
0x8E3808 in a 0x2E847B-byte file, while the table really begins at file
offset 0x2E3808.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@zardus

zardus commented Sep 10, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Validation record for head 47f3e68de3e3c57687f5b492af278adca0f6c7c7 against baseline
0166109eb1fa0aec5baefb12403a487268f0e9ca.

The file

Expected CI: no check. angr/binaries runs no workflow on its default branch, and its three
most recent pull requests each carry zero check runs and zero commit statuses.

One added path, tests/x86_64/vmprotect_sample1.vmp.bin, 3048571 bytes, sha256
b7b577bd569a10cdd1c660c28c8e78e8ae35af8add43f0f57766d899dbd03564. Three copies hash to that
value: the committed file, the raw file in the public source repository, and the sample inside
the reproduce.zip attached to angr/cle#383 in April 2023.

The source repository, checked anonymously: HTTP/2 200, "private": false, and
x-ratelimit-limit: 60 in the response headers, which is the unauthenticated limit and so
proves the call carried no token. Its full tree (2659 entries, truncated: false) holds no
top-level LICENSE, and every licence text in it sits under pin/licensing/, belonging to the
Intel Pin distribution it vendors. The sample is cited at 971be45920b504586a9c94fc7df941f8f57261b1, the
last commit to touch that path.

Why it is needed

Over the 940 tracked paths whose content is an ELF at 0166109e: 578 DT_VERNEED entries and
40 DT_VERDEF entries, and all 618 place the table at an RVA that is also its file offset. The
same instrument at 47f3e68d reports 941/579/40 and exactly one mismatch, this fixture, which
calibrates the zero.

The consumer

angr/cle at 19f21250e972d1f6d0329785077df9e2e1ec6675 adds
tests/test_symbol_versions.py, which loads this file. That test fails on cle master
0e77ade3 with ELFParseError: expected 2, found 0 and passes on the cle branch. Gate run
2788910, ./feature.sh test cle383 with this repository at 47f3e68d, exited 0:
cle, angr, angr-rust, pysoot, workspace, test-inputs, test-packages,
pre-commit, feature-build and mono ran and passed; worktree-cleanliness passed with no
checkout changed.

A green gate that skipped a suite is green over less than it appears to be: archinfo,
pypcode, pyvex and angr-management did not run, because the feature had not adopted them,
and the angr suite ran at 23b470d9f, twelve commits behind angr's own master.

This repository runs no test suite of its own, so nothing here executes against the file.

session: sharpen

@zardus

zardus commented Sep 10, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

What this file is for: versions.py, run against cle master and against the consumer branch
that adds tests/test_symbol_versions.py. DT_VERNEED is 0xCE3808, the
image base is 0x400000, the file is 0x2E847B bytes long, and the table's real file offset is
0x2E3808.

import cle

ld = cle.Loader("binaries/tests/x86_64/vmprotect_sample1.vmp.bin", auto_load_libs=False)
obj = ld.main_object
print(obj)
print(sorted({s.version for s in obj.symbols if s.version}))
print(len([s for s in obj.symbols if s.version not in (None, "*local*", "*global*")]))
for name in ("printf", "__gxx_personality_v0", "_ZNSs6appendEPKcm"):
    print(name, sorted({s.version for s in obj.symbols if s.name == name}))

Before — the RVA 0x8E3808 is seeked in the file stream, 0x5FB38D past its end, and the
object does not load at all:

cle master 0e77ade3
Traceback (most recent call last):
  File "elftools/construct/core.py", line 351, in _parse
    return self.packer.unpack(_read_stream(stream, self.length))[0]
                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "elftools/construct/core.py", line 293, in _read_stream
    raise FieldError("expected %d, found %d" % (length, len(data)))
elftools.construct.core.FieldError: expected 2, found 0

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "elftools/common/utils.py", line 43, in struct_parse
    return struct.parse_stream(stream)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "elftools/construct/core.py", line 190, in parse_stream
    return self._parse(stream, Container())
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "elftools/construct/core.py", line 647, in _parse
    subobj = sc._parse(stream, context)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "elftools/construct/core.py", line 353, in _parse
    raise FieldError(ex)
elftools.construct.core.FieldError: expected 2, found 0

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "versions.py", line 3, in <module>
    ld = cle.Loader("binaries/tests/x86_64/vmprotect_sample1.vmp.bin", auto_load_libs=False)
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "cle/loader.py", line 187, in __init__
    self.initial_load_objects = self._internal_load(
                                ^^^^^^^^^^^^^^^^^^^^
  File "cle/loader.py", line 805, in _internal_load
    obj = self._load_object_isolated(main_spec)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "cle/loader.py", line 1017, in _load_object_isolated
    result = backend_cls(binary, binary_stream, is_main_bin=self._main_object is None, loader=self, **options)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "cle/backends/elf/elf.py", line 208, in __init__
    self.__register_segments()
  File "cle/backends/elf/elf.py", line 1036, in __register_segments
    self.__register_dyn(seg)
  File "cle/backends/elf/elf.py", line 1137, in __register_dyn
    for _, aux in readelf_verneed.iter_versions():
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "elftools/elf/gnuversions.py", line 154, in iter_versions
    for verneed, vernaux in super(GNUVerNeedSection, self).iter_versions():
                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "elftools/elf/gnuversions.py", line 108, in iter_versions
    entry = struct_parse(
            ^^^^^^^^^^^^^
  File "elftools/common/utils.py", line 45, in struct_parse
    raise ELFParseError(str(e))
elftools.common.exceptions.ELFParseError: expected 2, found 0

After — with the consumer's one-line-each fix to __register_dyn, the version tables are
read from the image, 112 symbols get a library version back,
and the two entries sorted(...) shows beside the six real ones are the *local* and
*global* pseudo-versions cle assigns to everything else:

with the consumer's change
Symbol imported without a known size; emulation may fail if it is used non-opaqely: _ZNSs4_Rep20_S_empty_rep_storageE, _ZTVN10__cxxabiv117__class_type_infoE, _ZTVN10__cxxabiv120__si_class_type_infoE. See https://docs.angr.io/extending-angr/environment#simdata
<ELF Object vmprotect_sample1.vmp.bin, maps [0x400000:0xce77b7]>
['*global*', '*local*', 'CXXABI_1.3', 'GLIBCXX_3.4', 'GLIBC_2.2.5', 'GLIBC_2.3', 'GLIBC_2.3.4', 'GLIBC_2.4']
112
printf ['GLIBC_2.2.5']
__gxx_personality_v0 ['CXXABI_1.3']
_ZNSs6appendEPKcm ['GLIBCXX_3.4']

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant