Files
2026-10-17 21:57:25 +08:00

430 lines
15 KiB
Plaintext

from stdint import *
import stdlib
import string
import viperlib
import sys
import lib.VFs as VFs
import lib.VLog as VLog
import lib.VDirs as VDirs
import lib.VProc as VProc
import lib.VCli as VCli
import win32.win32file as wf
import win32.win32base as wb
# VPe - PE/COFF parsing plus the ver subcommand flow.
# Entry points: TryReadPeInfo/ParsePeHead/Phase1VersionSection used by VScan and ver.
# PE/COFF constants
define DOS_SIGNATURE = 0x5A4D # "MZ" little-endian
define NT_SIGNATURE = 0x00004550 # "PE\0\0" little-endian
define OPT_MAGIC_PE32 = 0x10B
define OPT_MAGIC_PE32P = 0x20B
define SEC_ENTRY_SIZE = 40
define E_LFANEW_OFF = 0x3C # DOS header e_lfanew
define COFF_MACHINE_OFF = 4 # PE sig(4)+COFF: Machine @ COFF+0
define COFF_NUMSEC_OFF = 6 # PE sig(4)+COFF: NumberOfSections @ COFF+2
define COFF_OPTSIZE_OFF = 20 # PE sig(4)+COFF: SizeOfOptionalHeader @ COFF+16
define OPT_MAGIC_OFF = 24 # after PE sig(4)+COFF(20) comes OptionalHeader Magic
define SEC_NAME_OFF = 0
define SEC_RAWSIZE_OFF = 16
define SEC_RAWPTR_OFF = 20
define VERSION_SECTION = ".PRVERSN"
# COFF Machine values (winnt.h) -> arch string
define MACHINE_X86_64 = 0x8664
define MACHINE_X86 = 0x14C
define MACHINE_ARM64 = 0xAA64
define MACHINE_ARM = 0x1C0
define MACHINE_ARMNT = 0x1C4
# PE read cap for scanning
define PE_HEAD_READ = 8192 # single-read head window (DOS+COFF+optional header+section table)
define VER_CAP = 128 # .PRVERSN version string buffer
define ARCH_CAP = 32 # arch string buffer ("unknown(0xFFFF)" is 14 bytes)
# ParsePeHead outputs (module globals, not array outparams)
Pe_RawPtr: uint32 = 0
Pe_RawSize: uint32 = 0
# GetLastError when TryReadPeInfo open fails (rc=1); the check channel maps it to
# NOT_FOUND/NO_PERMISSION. Reset on entry, written right after CreateFileA (any call clobbers it).
Pe_OpenErr: ULONG = 0
# Failure-path diagnostics for the [pe] line: Pe_DbgGot (bytes parsed), Pe_DbgSecOff (section table
# offset), Pe_DbgLastRc (last rc). Reset on entry to TryReadPeInfo and Phase1VersionSection.
Pe_DbgLfanew: uint32 = 0
Pe_DbgNsec: uint32 = 0
Pe_DbgOptSize: uint32 = 0
Pe_DbgSecOff: uint32 = 0
Pe_DbgGot: sizet = 0
Pe_DbgLastRc: int = 0
# ---- Little-endian reads (Buf is an unsigned byte base; Buf[i] is u8) ----
def ReadU16(Buf: BYTEPTR, Off: int) -> uint32:
Val: uint32 = uint32(Buf[Off])
Val = Val | (uint32(Buf[Off + 1]) << 8)
return Val
def ReadU32(Buf: BYTEPTR, Off: int) -> uint32:
Val: uint32 = uint32(Buf[Off])
Val = Val | (uint32(Buf[Off + 1]) << 8)
Val = Val | (uint32(Buf[Off + 2]) << 16)
Val = Val | (uint32(Buf[Off + 3]) << 24)
return Val
# copy an 8-byte section name and NUL-terminate into Out (caller guarantees >=9 bytes)
def CopySectionName(Buf: BYTEPTR, Off: int, Out: bytes):
for k in range(8):
Out[k] = Buf[Off + k]
Out[8] = 0
# ---- PE parsing (lightweight, no diagnostics; scan and ver share the same constants/offsets) ----
# COFF Machine -> arch string; returns 1 when written
def FillArch(OutArch: bytes, Cap: int, Machine: int) -> int:
if Cap < 24:
return 0
if Machine == MACHINE_X86_64:
VFs.CopyRaw(OutArch, Cap, "x86_64")
elif Machine == MACHINE_X86:
VFs.CopyRaw(OutArch, Cap, "x86")
elif Machine == MACHINE_ARM64:
VFs.CopyRaw(OutArch, Cap, "arm64")
elif Machine == MACHINE_ARM or Machine == MACHINE_ARMNT:
VFs.CopyRaw(OutArch, Cap, "arm")
else:
viperlib.snprintf(OutArch, sizet(Cap), "unknown(0x%X)", uint32(Machine))
return 1
# [diag] print one PE parse intermediate line (assembled from fixed-arity calls). Rc = failure
# code, Fsz = file size, RawPtr/RawSize = .PRVERSN position; process detail, --verbose only.
def PeDbgPrint(Rc: int, Fsz: sizet, RawPtr: uint32, RawSize: uint32):
global Pe_DbgLastRc
Pe_DbgLastRc = Rc
SecTabEnd: sizet = sizet(Pe_DbgSecOff) + sizet(Pe_DbgNsec) * sizet(SEC_ENTRY_SIZE)
VLog.VerboseFmt4(" [pe] rc=%u lfanew=%u nsec=%u optsize=%u",
sizet(Rc), sizet(Pe_DbgLfanew), sizet(Pe_DbgNsec), sizet(Pe_DbgOptSize))
VLog.VerboseFmt4(" sec_off=%u sectab_end=%u got=%u fsz=%u",
sizet(Pe_DbgSecOff), SecTabEnd, Pe_DbgGot, Fsz)
VLog.VerboseFmt2(" rawptr=%u rawsize=%u\n", sizet(RawPtr), sizet(RawSize))
# Get the last CreateFileA failure code (meaningful only when rc=1, else 0). The check channel
# maps it: 2/3 (NOT_FOUND) -> NOT_FOUND; 5/32 and others -> NO_PERMISSION.
def PeOpenErr() -> ULONG:
return Pe_OpenErr
# Pure parse: from the head-window Buf (Got bytes) find .PRVERSN raw_ptr/raw_size (globals
# Pe_RawPtr/Pe_RawSize) and fill OutArch. Returns 0 = found; 1..7 = failure kinds.
def ParsePeHead(Buf: bytes, Got: sizet, OutArch: bytes, ArchCap: int) -> int:
global Pe_RawPtr, Pe_RawSize
global Pe_DbgLfanew, Pe_DbgNsec, Pe_DbgOptSize, Pe_DbgSecOff, Pe_DbgGot, Pe_DbgLastRc
Pe_RawPtr = 0
Pe_RawSize = 0
Pe_DbgLfanew = 0
Pe_DbgNsec = 0
Pe_DbgOptSize = 0
Pe_DbgSecOff = 0
Pe_DbgGot = Got
Pe_DbgLastRc = 0
if Got < 0x40:
Pe_DbgLastRc = 1
return 1
ub: BYTEPTR = BYTEPTR(Buf)
if ReadU16(ub, 0) != DOS_SIGNATURE:
Pe_DbgLastRc = 2
return 2
lfanew: uint32 = ReadU32(ub, E_LFANEW_OFF)
Pe_DbgLfanew = lfanew
if sizet(lfanew) + 24 > Got:
Pe_DbgLastRc = 3
return 3
if ReadU32(ub, int(lfanew)) != NT_SIGNATURE:
Pe_DbgLastRc = 4
return 4
nsec: uint32 = ReadU16(ub, int(lfanew) + COFF_NUMSEC_OFF)
optsize: uint32 = ReadU16(ub, int(lfanew) + COFF_OPTSIZE_OFF)
Pe_DbgNsec = nsec
Pe_DbgOptSize = optsize
magic: uint32 = ReadU16(ub, int(lfanew) + OPT_MAGIC_OFF)
if magic != OPT_MAGIC_PE32 and magic != OPT_MAGIC_PE32P:
Pe_DbgLastRc = 5
return 5
mach: uint32 = ReadU16(ub, int(lfanew) + COFF_MACHINE_OFF)
FillArch(OutArch, ArchCap, int(mach))
sec_off: int = int(lfanew) + 24 + int(optsize)
Pe_DbgSecOff = uint32(sec_off)
if sizet(sec_off) + sizet(nsec) * sizet(SEC_ENTRY_SIZE) > Got:
Pe_DbgLastRc = 6
return 6 # section table beyond head window (extreme many-section PE; skip)
for i in range(int(nsec)):
entry: int = sec_off + i * SEC_ENTRY_SIZE
if string.strncmp(str(Buf + entry + SEC_NAME_OFF), VERSION_SECTION, 8) == 0:
Pe_RawPtr = ReadU32(ub, entry + SEC_RAWPTR_OFF)
Pe_RawSize = ReadU32(ub, entry + SEC_RAWSIZE_OFF)
return 0
Pe_DbgLastRc = 7
return 7
# copy the version string in Src[Off..Off+Len) (to first NUL or VerCap-1) into OutVer
def CopyVersion(Src: bytes, Off: int, Len: int, OutVer: bytes, VerCap: int) -> int:
n: int = 0
while n < Len and n < VerCap - 1:
if Src[Off + n] == 0:
break
OutVer[n] = Src[Off + n]
n += 1
OutVer[n] = 0
return n
# Lightweight PE probe for scanning: open once, read the head window to parse the section table,
# fall back to a whole-file read if needed. Returns 0 with OutVer/OutArch, else non-zero skip.
def TryReadPeInfo(Target: str, OutVer: bytes, VerCap: int, OutArch: bytes, ArchCap: int) -> int:
global Pe_DbgLfanew, Pe_DbgNsec, Pe_DbgOptSize, Pe_DbgSecOff, Pe_DbgGot, Pe_DbgLastRc, Pe_OpenErr
OutVer[0] = 0
OutArch[0] = 0
# reset diagnostics on entry so this [pe] line reflects only this parse
Pe_DbgLfanew = 0
Pe_DbgNsec = 0
Pe_DbgOptSize = 0
Pe_DbgSecOff = 0
Pe_DbgGot = 0
Pe_DbgLastRc = 0
Pe_OpenErr = 0
h: wb.HANDLE = wf.CreateFileA(
Target, wf.GENERIC_READ,
wf.FILE_SHARE_READ | wf.FILE_SHARE_WRITE | wf.FILE_SHARE_DELETE,
None, wf.OPEN_EXISTING, wf.FILE_ATTRIBUTE_NORMAL, None)
if h == wb.INVALID_HANDLE_VALUE:
Pe_OpenErr = wb.GetLastError() # read right after CreateFileA (PeDbgPrint's log would clobber it)
PeDbgPrint(1, sizet(0), 0, 0)
return 1
li: wb.LARGE_INTEGER = wb.LARGE_INTEGER()
if wf.GetFileSizeEx(h, &li) == 0:
wb.CloseHandle(h)
PeDbgPrint(2, sizet(0), 0, 0)
return 2
fsz: sizet = sizet(li.QuadPart)
if fsz < 0x40:
wb.CloseHandle(h)
PeDbgPrint(3, fsz, 0, 0)
return 3
head_cap: sizet = sizet(PE_HEAD_READ)
if fsz < head_cap:
head_cap = fsz
head: bytes = stdlib.malloc(head_cap + 1)
if head is None:
wb.CloseHandle(h)
PeDbgPrint(4, fsz, 0, 0)
return 4
got: sizet = VFs.ReadFull(h, head, head_cap)
head[got] = 0
Pe_DbgGot = got
if got < 0x40:
stdlib.free(rawptr(head))
wb.CloseHandle(h)
PeDbgPrint(5, fsz, 0, 0)
return 5
prc: int = ParsePeHead(head, got, OutArch, ArchCap)
if prc != 0:
stdlib.free(rawptr(head))
wb.CloseHandle(h)
PeDbgPrint(100 + prc, fsz, Pe_RawPtr, Pe_RawSize)
return 100 + prc
raw_ptr: uint32 = Pe_RawPtr
raw_size: uint32 = Pe_RawSize
if raw_size == 0 or sizet(raw_ptr) + sizet(raw_size) > fsz:
stdlib.free(rawptr(head))
wb.CloseHandle(h)
PeDbgPrint(6, fsz, raw_ptr, raw_size)
return 6
if sizet(raw_ptr) + sizet(raw_size) <= got:
# section content is within the head window
CopyVersion(head, int(raw_ptr), int(raw_size), OutVer, VerCap)
else:
# Section content is outside the head window: read the whole file (same API as ver [1/3]).
# Do not seek: wf.SetFilePointerEx passes liDistanceToMove by value, so an address breaks it.
stdlib.free(rawptr(head))
wb.CloseHandle(h)
whole: bytes = VFs.ReadWholeFile(
Target, 0,
wf.FILE_SHARE_READ | wf.FILE_SHARE_WRITE | wf.FILE_SHARE_DELETE)
if whole is None:
PeDbgPrint(7, fsz, raw_ptr, raw_size)
return 7
CopyVersion(whole, int(raw_ptr), int(raw_size), OutVer, VerCap)
stdlib.free(rawptr(whole))
return 0
stdlib.free(rawptr(head))
wb.CloseHandle(h)
return 0
# ver [1/3]: open the target exe, parse the section table and read .PRVERSN. Returns 0 = found
# and passed, 1 = failure. Independent parse; [pe] rc encoded like ParsePeHead (1..7).
def Phase1VersionSection(Target: str) -> int:
global Pe_DbgLfanew, Pe_DbgNsec, Pe_DbgOptSize, Pe_DbgSecOff, Pe_DbgGot, Pe_DbgLastRc
# reset diagnostics on entry so this [pe] line reflects only this parse
Pe_DbgLfanew = 0
Pe_DbgNsec = 0
Pe_DbgOptSize = 0
Pe_DbgSecOff = 0
Pe_DbgGot = 0
Pe_DbgLastRc = 0
buf: bytes = VFs.ReadWholeFile(Target, 0,
wf.FILE_SHARE_READ | wf.FILE_SHARE_WRITE | wf.FILE_SHARE_DELETE)
if buf is None:
rc_err: int = VFs.LastReadErr
if rc_err == 1:
VLog.LogFmt1(" open file failed (win32 err=%u)\n", sizet(VFs.LastReadWinErr))
elif rc_err == 2:
VLog.LogStr(" read file size failed\n")
elif rc_err == 3:
VLog.LogStr(" file is empty\n")
else:
VLog.LogStr(" buffer allocation failed\n")
return 1
total: sizet = VFs.LastReadSize
Pe_DbgGot = total
if total < 0x40:
VLog.LogStr(" not a valid PE file (file too short)\n")
PeDbgPrint(1, total, 0, 0)
stdlib.free(rawptr(buf))
return 1
ub: BYTEPTR = BYTEPTR(buf)
# DOS header
if ReadU16(ub, 0) != DOS_SIGNATURE:
VLog.LogStr(" not a valid PE file (missing MZ header)\n")
PeDbgPrint(2, total, 0, 0)
stdlib.free(rawptr(buf))
return 1
# NT header
lfanew: uint32 = ReadU32(ub, E_LFANEW_OFF)
Pe_DbgLfanew = lfanew
if sizet(lfanew) + 24 > total:
VLog.LogStr(" not a valid PE file (e_lfanew out of range)\n")
PeDbgPrint(3, total, 0, 0)
stdlib.free(rawptr(buf))
return 1
if ReadU32(ub, int(lfanew)) != NT_SIGNATURE:
VLog.LogStr(" not a valid PE file (missing PE signature)\n")
PeDbgPrint(4, total, 0, 0)
stdlib.free(rawptr(buf))
return 1
nsec: uint32 = ReadU16(ub, int(lfanew) + COFF_NUMSEC_OFF)
optsize: uint32 = ReadU16(ub, int(lfanew) + COFF_OPTSIZE_OFF)
Pe_DbgNsec = nsec
Pe_DbgOptSize = optsize
magic: uint32 = ReadU16(ub, int(lfanew) + OPT_MAGIC_OFF)
if magic != OPT_MAGIC_PE32 and magic != OPT_MAGIC_PE32P:
VLog.LogFmt1(" unknown optional-header Magic: 0x%X\n", magic)
PeDbgPrint(5, total, 0, 0)
stdlib.free(rawptr(buf))
return 1
sec_off: int = int(lfanew) + 24 + int(optsize)
Pe_DbgSecOff = uint32(sec_off)
if sizet(sec_off) + sizet(nsec) * SEC_ENTRY_SIZE > total:
VLog.LogFmt1(" section table out of range (section count=%u)\n", nsec)
PeDbgPrint(6, total, 0, 0)
stdlib.free(rawptr(buf))
return 1
# find .PRVERSN
found: int = 0
raw_ptr: uint32 = 0
raw_size: uint32 = 0
for i in range(int(nsec)):
entry: int = sec_off + i * SEC_ENTRY_SIZE
if string.strncmp(str(buf + entry + SEC_NAME_OFF), VERSION_SECTION, 8) == 0:
found = 1
raw_size = ReadU32(ub, entry + SEC_RAWSIZE_OFF)
raw_ptr = ReadU32(ub, entry + SEC_RAWPTR_OFF)
break
if found == 0:
VLog.LogFmtS1(" section %s not found\n", VERSION_SECTION)
PeDbgPrint(7, total, 0, 0)
VLog.LogStr(" actual section names:\n")
name8: bytes = stdlib.malloc(9)
if name8 is not None:
for j in range(int(nsec)):
CopySectionName(ub, sec_off + j * SEC_ENTRY_SIZE, name8)
VLog.LogFmtS1(" %s\n", str(name8))
stdlib.free(rawptr(name8))
stdlib.free(rawptr(buf))
return 1
# up to the first NUL
if sizet(raw_ptr) + sizet(raw_size) > total:
VLog.LogFmt2(" section content out of range (offset=%u size=%u)\n", raw_ptr, raw_size)
PeDbgPrint(6, total, raw_ptr, raw_size)
stdlib.free(rawptr(buf))
return 1
vlen: uint32 = 0
while vlen < raw_size and ub[int(raw_ptr) + int(vlen)] != 0:
vlen += 1
VLog.LogFmt1(" raw length: %u bytes\n", vlen)
VLog.LogFmtS1(" version string: %s\n", str(buf + int(raw_ptr)))
stdlib.free(rawptr(buf))
return 0
# ver subcommand: [0/3] -> [1/3] -> [2/3] checks and exit codes (logs to stderr)
def RunVer() -> int:
if sys._argc < 3 or sys._argv[2] is None:
VLog.LogStr("Usage: VET.exe ver <target exe path>\n")
return 1
target: str = sys._argv[2]
# banner (no path) is kept; the target path is process detail, --verbose only
VLog.LogFmtS1("VET - Viper Environment Tool %s\n\n", VCli.Version())
VLog.VerboseFmtS1("target: %s\n\n", target)
VLog.LogStr("[0/3] common directory table\n")
rc0: int = VDirs.BuildTable()
if rc0 == 0:
VLog.LogStr(" ✓ pass\n\n")
else:
VLog.LogStr(" ✗ fail (table empty)\n\n")
VLog.LogFmtS1("[1/3] embedded version section (%s)\n", VERSION_SECTION)
rc1: int = Phase1VersionSection(target)
if rc1 == 0:
VLog.LogStr(" ✓ pass\n\n")
else:
VLog.LogStr(" ✗ fail\n\n")
VLog.LogStr("[2/3] --version prefix\n")
rc2: int = VProc.RunVersionPrefixCheck(target)
if rc2 == 0:
VLog.LogStr(" ✓ pass\n")
else:
VLog.LogStr(" ✗ fail\n")
if rc0 == 0 and rc1 == 0 and rc2 == 0:
return 0
return 1