430 lines
15 KiB
Plaintext
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
|