~/posts/binary/excel-embedded-png-dll-analysis.md

Two DLL architectures inside an Excel document's embedded PNG

Trace an Excel document from OLE streams to two adjacent PE payloads. Check extraction boundaries and overlay hashes, then separate VBA architecture-selection declarations from evidence of actual execution.

date[31:24]
read[23:16]
6 min
cat[15:8]
Binary
Contents
  1. 0x00Find the payload container in the object streams
  2. 0x01A PNG header and embedded PEs are separate evidence
  3. 0x02Overlay changes the extraction's identity
  4. 0x03Verify the boundary arithmetic
  5. 0x04VBA declarations connect bitness to filenames
  6. 0x05Specimen identifiers and detection points
  7. 0x06References

A PNG signature says how a file starts, not what every byte contains. This 2019 Excel specimen embeds two DLLs with different bitness in an object, while its VBA declarations reference exchange1.dll and exchange2.dll. Matching those declarations to the container layout is more useful than treating each suspicious string in isolation.

The evidence here covers the workbook's OLE structure, embedded object, and visible VBA declarations. All offsets are specimen-specific. The complete macro bodies and runtime logs are outside this evidence set, so successful loading, export execution, and subsequent network activity remain unproven.

Find the payload container in the object streams

oledump.py lists 22 streams. Stream 4 carries the object flag O; streams 12, 13, 14, 20, and 22 carry VBA flags. Follow the object and code paths separately instead of treating the workbook as one undifferentiated byte string.

Key OLE streams6 rows
Index Flag Bytes Stream path
4 O 160402 MBD00DF435B/\x01Ole10Native
12 M 4824 _VBA_PROJECT_CUR/VBA/Module1
13 M 1738 _VBA_PROJECT_CUR/VBA/Module2
14 M 1365 _VBA_PROJECT_CUR/VBA/UserForm1
20 m 973 _VBA_PROJECT_CUR/VBA/bb
22 M 1409 VBA module stream

The uppercase M and lowercase m flags are preserved as reported; they are distinct flags. Commands below use the normalized filename SAMPLE.xls. Their stream numbers and values describe this specimen, not a universal workbook layout.

Object metadata and PE rule hit (excerpt)
$ oledump.py -s 4 -i SAMPLE.xls
String 1: 2AB07F92.png
Size embedded file: 159931
MD5 embedded file: b9fd4ec14b72f944160ebafa7f45b818
MAGIC: 89504e47
$ oledump.py -y contains_pe_file.yara SAMPLE.xls
  4: O 160402 'MBD00DF435B/\x01Ole10Native'
       YARA rule: Contains_PE_File

The object's metadata also contains user temporary-directory and Content.MSO paths. These are packaging records, not evidence of DLL output paths at runtime. The rule file contains_pe_file.yara was an external input to this inspection. Its match narrows the search; it does not establish PE boundaries.

A PNG header and embedded PEs are separate evidence

First 16 bytes of the embedded object
0000000089504E470D0A1A0A0000000D49484452
  1. PNG signature0x00–0x07
  2. IHDR length0x08–0x0B
  3. IHDR0x0C–0x0F

The header supports "starts with PNG," not "contains only an image." It also does not establish that an image parser accepted the entire object. After -e extracts the embedded content, pecheck.py -l P reports two DLLs with their start offsets, structural end offsets, and the enclosing file's end:

Locating PEs inside the object
$ oledump.py -s 4 -e SAMPLE.xls | pecheck.py -l P
1: 0x00002ebb DLL 32-bit 0x00016eba 0x000270ba (EOF)
2: 0x00016ebb DLL 64-bit 0x000270ba 0x000270ba (EOF)
Object-relative offsets, with inclusive ends2 rows
Payload Start Structural end Length
DLL32 0x2ebb 0x16eba 0x14000
DLL64 0x16ebb 0x270ba 0x10200

The ranges are adjacent: the first end plus one equals the second start. The second end plus one equals the object's 159931-byte length. The 160402-byte OLE stream includes an additional object wrapper; its size belongs to a different layer.

Overlay changes the extraction's identity

Extracting from the first PE through the object EOF yields 0x24200 bytes, not the first PE's 0x14000 bytes. The tool reports the remaining 0x10200 bytes as overlay, and their hash matches the second DLL.

First PE overlay (excerpt)
$ oledump.py -s 4 -e SAMPLE.xls | pecheck.py -l 1 | headtail.py
Overlay:
 Start offset: 0x00014000
 Size:         0x00010200 64.5 KB 44.64%
 MD5:          6eede113112f85b0ae99a2210e07cdd0
 SHA-256:      141d71d86cd25b210b67fe8e49d2abf63324b7ce36736b95b51c9258c4b1ddbb
 MAGIC:        4d5a9000
PE file without overlay:
 MD5:          3bd4fcbee95711392260549669df7236
 SHA-256:      5f66744cef565f0be87c84011293a89931373a34be3eea7c247d2d61f7c499d2

The overlay offset 0x14000 is relative to the first extracted PE. The earlier 0x16ebb is relative to the whole object. They agree because 0x2ebb + 0x14000 = 0x16ebb; mixing these coordinate systems would place the payload incorrectly.

The second extraction has no overlay. Its inspection also gives an entry-point RVA and file offset, which should remain distinct:

Second extracted payloadPE32+
Type
DLL
Entry point
0x10001907
MD5
6eede113112f85b0ae99a2210e07cdd0
SHA256
141d71d86cd25b210b67fe8e49d2abf63324b7ce36736b95b51c9258c4b1ddbb
Header fields
AddressOfEntryPoint
0x1907
EntryPointFileOffset
0x0D07
Overlay
None
Sections4
NameEntropy
.text5.25
.rdata2.89
.data6.38
.pdata2.15

Entropy (bits/byte)< 1 sparse< 6.8 typical6.8–7.2 high≥ 7.2 packed or encrypted

The entry value is the address calculated by the tool from the preferred image base, not an observed runtime address. PE has no universal end marker. A boundary assessment must account for raw section ranges and possible out-of-section data such as the certificate table. Here, the adjacent starts, structural ends, and object EOF agree. Microsoft PE format

Verify the boundary arithmetic

This check validates the listed ranges. It neither parses nor executes a malicious file, and it does not recompute specimen hashes. Python slices use an exclusive stop, so an inclusive end needs one added to it.

verify_regions.pypython
object_size = 159931
regions = [(0x2ebb, 0x16eba), (0x16ebb, 0x270ba)]
lengths = [end - start + 1 for start, end in regions]
assert lengths == [0x14000, 0x10200]
assert regions[0][1] + 1 == regions[1][0]
assert regions[1][1] + 1 == object_size
assert regions[0][0] + sum(lengths) == object_size
assert object_size - regions[0][0] == 0x24200
print("PASS: contiguous PE ranges; object EOF; overlay = DLL64")
Boundary-check result
$ python verify_regions.py
PASS: contiguous PE ranges; object EOF; overlay = DLL64

A hash identifies a byte range. The first PE without overlay and the extraction from its start through EOF are different objects. Treating their different hashes as proof of a new variant loses that distinction.

VBA declarations connect bitness to filenames

Stream 13 contains these Module2 declarations. The return types are retained for specimen analysis, not presented as a correct Win64 API template.

Module2 declarationsvb
Attribute VB_Name = "Module2"
#If Win64 Then
    Public Declare PtrSafe Function Amway Lib _
        "exchange2.dll" () As Integer
    Public Declare PtrSafe Function k32LL Lib "kernel32" Alias "LoadLibraryW" (ByVal lpLibFileName As String) As Long
#Else
    Public Declare Function Amway Lib _
        "exchange1.dll" () As Integer
    Public Declare Function k32LL Lib "kernel32" Alias "LoadLibraryW" (ByVal lpLibFileName As String) As Long
#End If

Win64 reflects the VBA host environment: the 64-bit Office branch declares exchange2.dll, and the other branch declares exchange1.dll. A 32-bit Office installation on 64-bit Windows still takes the latter branch. The operating system's bitness alone does not select the payload. Microsoft VBA compiler constants

LoadLibraryW loads a module into the calling process and returns a module handle. Yet the 64-bit declaration above still uses As Long. PtrSafe does not automatically widen a pointer or handle: declarations that retain a 64-bit handle need a pointer-sized type. This is another reason to distinguish an API declaration from a proven successful call. Microsoft LoadLibraryW, Microsoft PtrSafe

The paired payloads and conditional declarations are consistent with selecting a DLL for Office's bitness, loading it inside Excel, and calling Amway. Completing that execution chain requires the object-read, file-write, load, and export-call bodies, plus runtime evidence. The available declarations do not show Amway internals, so attribution, networking, and persistence remain undetermined.

Specimen identifiers and detection points

These hashes identify the workbook, embedded object, and two DLLs with explicit boundaries. DLL32 and DLL64 label byte ranges. The filenames come from the macro declarations; actual dropped files still need to be correlated with hashes.

Specimen and extracted payloads
SHA-256 3
  • 86a07beee7a5d10a9e36eb8cb95abc0254a7e468930197f94094b2611dcd2b16
    SAMPLE.xls
  • 5f66744cef565f0be87c84011293a89931373a34be3eea7c247d2d61f7c499d2
    DLL32 [0x2ebb, 0x16eba]
  • 141d71d86cd25b210b67fe8e49d2abf63324b7ce36736b95b51c9258c4b1ddbb
    DLL64 [0x16ebb, 0x270ba]
MD5 1
  • b9fd4ec14b72f944160ebafa7f45b818
    2AB07F92.png
File names 2
  • exchange1.dll
  • exchange2.dll
6 indicators · 3 types · defanged

Detection should extend beyond suspicious Office child processes. Correlate document opening, macro execution, DLL writes, and Excel module loads. A filename or PNG magic value alone is insufficient. Application-control checks should also establish whether the deployed policy covers DLLs as well as EXEs; the answer depends on the active rules.

Keep extraction ranges, PE boundaries, range-qualified hashes, and macro call relationships together. They give dynamic analysis a traceable starting point and preserve the distinction between "contains two DLLs" and "executed two DLLs."

References

NORMAL~/posts/binary/excel-embedded-png-dll-analysis.md§--
0%en