Skip to content

Instantly share code, notes, and snippets.

@liath
Last active November 30, 2020 19:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save liath/f0cddb7945f1b2c7af397cbe9d022c02 to your computer and use it in GitHub Desktop.
Save liath/f0cddb7945f1b2c7af397cbe9d022c02 to your computer and use it in GitHub Desktop.
Extracts FileVersion and other fun fields as seen in the Properties dialog for dll and exe files Like https://gist.github.com/Liath/c148ce9f72a64457150e16f2a880e7c4, but this time using only bash, sed, tac, tr, and xxd (which afaik are pretty standard) so hopefully this is portable :)
#!/usr/bin/env bash
FILE=$1
BUF_SIZE=64
function getBytes {
xxd -seek "$1" -len "$2" -p "$FILE" | tr -d '\n'
}
function getIntBytesLE {
getBytes "$1" "$2" | sed -r 's|(..)|\1\n|g' | tac | tr -d '\n'
}
function read32 { # uint32le
printf "%d" "0x$(getIntBytesLE "$1" 4)"
}
function read16 { # uint16le
printf "%d" "0x$(getIntBytesLE "$1" 2)"
}
function readString {
local BUF
local CHAR
local RESULT
BUF=$(getBytes "$1" "$BUF_SIZE")
local OFFSET=0
local RESULT=""
while [ $OFFSET -lt $BUF_SIZE ]; do
CHAR=$(xxd -r -p <<<"$BUF" | xxd -p -seek "$OFFSET" -len 1)
[ "$CHAR" == "00" ] && break
RESULT="$RESULT$CHAR"
OFFSET=$((OFFSET + 1))
done
RESULT=$(xxd -r -p <<<"$BUF" | xxd -p -seek 0 -len "$OFFSET" | xxd -r -p)
if [ $OFFSET -eq $BUF_SIZE ]; then
RESULT="$RESULT"$(readString $(($1 + BUF_SIZE)))
fi
echo -n "$RESULT"
}
function readStringUTF16 { # probably hacky, Idk enough about encodings
local BUF
local CHAR
local RESULT
BUF=$(getBytes "$1" "$BUF_SIZE")
local OFFSET=0
local RESULT=""
while [ $OFFSET -lt $BUF_SIZE ]; do
CHAR=$(xxd -r -p <<<"$BUF" | xxd -p -seek "$OFFSET" -len 2)
[ "$CHAR" == "0000" ] && break
RESULT="$RESULT$CHAR"
OFFSET=$((OFFSET + 1))
done
RESULT=$(xxd -r -p <<<"$BUF" | xxd -p -seek 0 -len "$OFFSET" | \
xxd -r -p | sed -r 's|^(.+)\x0\x0.*|\1|;s|\x0||g')
if [ $OFFSET -eq $BUF_SIZE ]; then
RESULT="$RESULT$(readStringUTF16 $(($1 + BUF_SIZE)))"
fi
echo -n "$RESULT"
}
function utf16len {
echo $(( (${#1} + 1) * 2 ))
}
function dwordAlign {
echo $(((($1 + $2 + 3) & 0xfffffffc) - ($2 & 0xfffffffc)))
}
# move to start of COFF Header
AT=$(read32 $((0x3c)))
if [ "$(getBytes $((AT)) 4)" != "50450000" ]; then
echo "Did not see PE magic constant"
exit 1
fi
NOOF_SECTIONS=$(read16 $((AT + 0x6)))
LEN_OPTHEADER=$(read16 $((AT + 0x14)))
# move to start of section table
AT=$((AT + 0x18 + LEN_OPTHEADER))
SECTIONS_END=$((AT + (NOOF_SECTIONS * 0x28)))
RESOURCE_TABLE=0
RESOURCE_TABLE_RVA=0
while [ $AT -lt $SECTIONS_END ]; do
NAME="$(readString $AT)"
if [ "$NAME" == ".rsrc" ]; then
RESOURCE_TABLE=$(read32 $((AT + 0x14)))
RESOURCE_TABLE_RVA=$(read32 $((AT + 0xc)))
break
fi
AT=$((AT + 0x28))
done
if [ "$RESOURCE_TABLE" -eq 0 ]; then
echo "Could not find resource table"
exit 1
fi
# move to start of root resource table
AT=$RESOURCE_TABLE
# named + id entries
RESOURCE_TABLE_ENTRIES=$(($(read16 $((AT + 0xc))) + $(read16 $((AT + 0xe)))))
# move to start of root resource table entries
AT=$((AT + 0x10))
# scan for version resource child dir
RESOURCE_ENTRIES_END=$((AT + (RESOURCE_TABLE_ENTRIES * 0x8)))
NEXT=0
while [ $AT -lt $RESOURCE_ENTRIES_END ]; do
ID=$(read32 $AT)
DATA=$(read32 $((AT + 0x4)))
# if `is version id` and `is directory`
if [ "$ID" -eq 16 ] && [ $((DATA >> 31)) -eq 1 ]; then
NEXT=$((DATA & 0x7fffffff))
break
fi
AT=$((AT + 0x8))
done
if [ $NEXT -eq 0 ]; then
echo "Could not find version resource child dir"
exit 1
fi
# move to child dir and do it all again
AT=$((RESOURCE_TABLE + NEXT))
RESOURCE_TABLE_ENTRIES=$(($(read16 $((AT + 0xc))) + $(read16 $((AT + 0xe)))))
AT=$((AT + 0x10))
RESOURCE_ENTRIES_END=$((AT + (RESOURCE_TABLE_ENTRIES * 0x8)))
NEXT=0
while [ $AT -lt $RESOURCE_ENTRIES_END ]; do
DATA=$(read32 $((AT + 0x4)))
if [ $((DATA >> 31)) -eq 1 ]; then
NEXT=$((DATA & 0x7fffffff))
break
fi
AT=$((AT + 0x8))
done
if [ $NEXT -eq 0 ]; then
echo "Could not find version resource grandchild dir"
exit 1
fi
# move to grandchild dir and do it all again
AT=$((RESOURCE_TABLE + NEXT))
RESOURCE_TABLE_ENTRIES=$(($(read16 $((AT + 0xc))) + $(read16 $((AT + 0xe)))))
AT=$((AT + 0x10))
RESOURCE_ENTRIES_END=$((AT + (RESOURCE_TABLE_ENTRIES * 0x8)))
NEXT=0
while [ $AT -lt $RESOURCE_ENTRIES_END ]; do
DATA=$(read32 $((AT + 0x4)))
if [ $((DATA >> 31)) -eq 0 ]; then
NEXT=$DATA
break
fi
AT=$((AT + 0x8))
done
if [ "$NEXT" -eq 0 ]; then
echo "Could not find version resource grandchild"
exit 1
fi
# move to version data info
AT=$((RESOURCE_TABLE + NEXT))
VERSION_DATA_RVA=$(read32 $AT)
VERSION_DATA_SIZE=$(read32 $((AT + 0x4)))
# move to version data (adjusted from RVA)
AT=$((VERSION_DATA_RVA - RESOURCE_TABLE_RVA + RESOURCE_TABLE))
if [ "$(readStringUTF16 $((AT + 0x6)))" != "VS_VERSION_INFO" ]; then
echo "Could not find version data"
exit 1
fi
VDE_AT=$AT
SFI_END=$((AT + VERSION_DATA_SIZE))
# move to start of string file info list
AT=$(dwordAlign $((AT + 0x5a)) $AT)
while [ "$AT" -lt $SFI_END ]; do
SFI_ENTRY_AT=$AT
SFI_ENTRY_TOTAL_SIZE=$(read16 "$AT")
SFI_ENTRY_NAME=$(readStringUTF16 $((AT + 0x6)))
if [ "$SFI_ENTRY_NAME" == "StringFileInfo" ]; then
AT=$(dwordAlign $((AT + 0x6 + $(utf16len "$SFI_ENTRY_NAME"))) $VDE_AT)
while [ "$AT" -lt $((SFI_ENTRY_AT + SFI_ENTRY_TOTAL_SIZE)) ]; do
STI_ENTRY_AT=$AT
STI_ENTRY_TOTAL_SIZE=$(read16 "$AT")
STI_ENTRY_NAME=$(readStringUTF16 $((AT + 0x6)))
AT=$(dwordAlign $((AT + 0x6 + $(utf16len "$STI_ENTRY_NAME"))) $VDE_AT)
while [ "$AT" -lt $((STI_ENTRY_AT + STI_ENTRY_TOTAL_SIZE)) ]; do
SEI_ENTRY_TOTAL_SIZE=$(read16 "$AT")
SEI_ENTRY_NAME=$(readStringUTF16 $((AT + 0x6)))
SEI_ENTRY_VALUE_SIZE=$(read16 $((AT + 0x2)))
SEI_ENTRY_VALUE=""
if [ "$SEI_ENTRY_VALUE_SIZE" -gt 0 ]; then
SEI_ENTRY_VALUE_AT=$(dwordAlign $((AT + 0x6 + $(utf16len "$SEI_ENTRY_NAME"))) $VDE_AT)
SEI_ENTRY_VALUE=$(readStringUTF16 $SEI_ENTRY_VALUE_AT)
fi
echo "$SEI_ENTRY_NAME: $SEI_ENTRY_VALUE"
AT=$(dwordAlign $((AT + SEI_ENTRY_TOTAL_SIZE)) $VDE_AT)
done
AT=$(dwordAlign $((STI_ENTRY_AT + STI_ENTRY_TOTAL_SIZE)) $VDE_AT)
done
fi
AT=$(dwordAlign $((SFI_ENTRY_AT + SFI_ENTRY_TOTAL_SIZE)) $VDE_AT)
done