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.
| 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.
$ 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_FileThe 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
PNG signature0x00–0x07IHDR length0x08–0x0BIHDR0x0C–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:
$ 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)| 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.
$ 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: 5f66744cef565f0be87c84011293a89931373a34be3eea7c247d2d61f7c499d2The 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:
- Type
- DLL
- Entry point
- 0x10001907
- MD5
- 6eede113112f85b0ae99a2210e07cdd0
- SHA256
- 141d71d86cd25b210b67fe8e49d2abf63324b7ce36736b95b51c9258c4b1ddbb
- AddressOfEntryPoint
- 0x1907
- EntryPointFileOffset
- 0x0D07
- Overlay
- None
| Name | Entropy |
|---|---|
| .text | 5.25 |
| .rdata | 2.89 |
| .data | 6.38 |
| .pdata | 2.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.
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")$ python verify_regions.py
PASS: contiguous PE ranges; object EOF; overlay = DLL64A 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.
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 IfWin64 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.
86a07beee7a5d10a9e36eb8cb95abc0254a7e468930197f94094b2611dcd2b16SAMPLE.xls5f66744cef565f0be87c84011293a89931373a34be3eea7c247d2d61f7c499d2DLL32 [0x2ebb, 0x16eba]141d71d86cd25b210b67fe8e49d2abf63324b7ce36736b95b51c9258c4b1ddbbDLL64 [0x16ebb, 0x270ba]
b9fd4ec14b72f944160ebafa7f45b8182AB07F92.png
exchange1.dllexchange2.dll
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."