Skip to content

Instantly share code, notes, and snippets.

@PeterGabaldon
Last active August 30, 2026 20:17
Show Gist options
  • Select an option

  • Save PeterGabaldon/2ff25fa1a13aeab0adae0d40c72ae84e to your computer and use it in GitHub Desktop.

Select an option

Save PeterGabaldon/2ff25fa1a13aeab0adae0d40c72ae84e to your computer and use it in GitHub Desktop.
UPX-packed ELF64 HexPat
#pragma description UPX-packed ELF64 (tolerant of stripped UPX! magics)
#pragma author Peter Gabaldon
#pragma endian little
/*
* upx_elf.hexpat
* -----------------------------------------------------------------------
* Parses the UPX metadata that lives *after* the ELF program header table:
*
* Elf64_Ehdr @ 0x00
* Elf64_Phdr[e_phnum] @ e_phoff
* l_info @ e_phoff + e_phentsize * e_phnum
* p_info immediately after l_info
* b_info + data repeated until the loader stub
* ...loader stub...
* PackHeader (32 B) near EOF
*
* Written for samples whose UPX! magics have been zeroed (THC gsocket
* beta-channel binaries do this), which makes `upx -d` refuse the file while
* leaving every other field valid. Nothing here depends on the magic.
*
* NOTE: UPX may place further b_info blocks *after* the loader stub
* (inter-segment padding, section headers, .shstrtab). Those are not
* contiguous with the main chain, so place `BInfo` manually at their offset
* if the block sizes do not add up to p_info.p_filesize.
*
* License: MIT
*/
#include <std/mem.pat>
// ---------------------------------------------------------------- ELF header
enum ElfClass : u8 {
ELFCLASS32 = 1,
ELFCLASS64 = 2
};
enum ElfType : u16 {
ET_NONE = 0,
ET_REL = 1,
ET_EXEC = 2,
ET_DYN = 3,
ET_CORE = 4
};
enum ElfMachine : u16 {
EM_386 = 0x03,
EM_MIPS = 0x08,
EM_ARM = 0x28,
EM_X86_64 = 0x3E,
EM_AARCH64 = 0xB7,
EM_RISCV = 0xF3
};
struct Elf64_Ehdr {
char ei_mag[4] [[color("E06C75"), comment("\\x7fELF")]];
ElfClass ei_class;
u8 ei_data;
u8 ei_version;
u8 ei_osabi;
u8 ei_abiversion;
u8 ei_pad[7];
ElfType e_type;
ElfMachine e_machine;
u32 e_version;
u64 e_entry [[color("D19A66"), comment("entry = UPX loader stub")]];
u64 e_phoff;
u64 e_shoff;
u32 e_flags;
u16 e_ehsize;
u16 e_phentsize;
u16 e_phnum;
u16 e_shentsize;
u16 e_shnum;
u16 e_shstrndx;
};
enum PhdrType : u32 {
PT_NULL = 0,
PT_LOAD = 1,
PT_DYNAMIC = 2,
PT_INTERP = 3,
PT_NOTE = 4,
PT_PHDR = 6,
PT_TLS = 7,
PT_GNU_EH_FRAME = 0x6474e550,
PT_GNU_STACK = 0x6474e551,
PT_GNU_RELRO = 0x6474e552
};
bitfield PhdrFlags {
X : 1;
W : 1;
R : 1;
padding : 29;
};
struct Elf64_Phdr {
PhdrType p_type;
PhdrFlags p_flags;
u64 p_offset;
u64 p_vaddr;
u64 p_paddr;
u64 p_filesz;
u64 p_memsz;
u64 p_align;
};
// --------------------------------------------------------------- UPX structs
enum UpxFormat : u8 {
UPX_F_LINUX_i386 = 11,
UPX_F_LINUX_ELF_i386 = 12,
UPX_F_LINUX_ELF64_AMD = 22,
UPX_F_LINUX_ELFPPC32 = 23,
UPX_F_LINUX_ELF64_ARM = 26,
UPX_F_LINUX_ELF32_ARM = 25
};
enum UpxMethod : u8 {
M_NRV2B_LE32 = 2,
M_NRV2B_8 = 3,
M_NRV2D_LE32 = 5,
M_NRV2E_LE32 = 8,
M_NRV2E_8 = 9,
M_LZMA = 14,
M_DEFLATE = 15
};
/* Zeroed on anti-unpacking samples. Compare against "UPX!" (55 50 58 21). */
struct LInfo {
u32 l_checksum [[color("61AFEF")]];
char l_magic[4] [[color("E5C07B"), comment("\"UPX!\" -- all-zero means stripped")]];
u16 l_lsize [[comment("loader size; also zeroed on stripped samples")]];
u8 l_version;
u8 l_format;
};
struct PInfo {
u32 p_progid;
u32 p_filesize [[color("98C379"), comment("size of the ORIGINAL, unpacked file")]];
u32 p_blocksize;
};
/* One compressed block: header + payload. */
struct BInfo {
u32 sz_unc [[color("98C379")]];
u32 sz_cpr [[color("C678DD")]];
UpxMethod b_method;
u8 b_ftid [[comment("0 = no filter; 0x49 = ctojr x86 call/jmp")]];
u8 b_cto8;
u8 b_unused;
u8 data[sz_cpr < sz_unc ? sz_cpr : sz_unc] [[color("3E4451")]];
};
/* 32-byte trailer UPX normally writes at EOF. */
struct PackHeader {
char magic[4] [[color("E5C07B"), comment("\"UPX!\" -- all-zero means stripped")]];
u8 version;
UpxFormat format;
UpxMethod method;
u8 level;
u32 u_adler;
u32 c_adler;
u32 u_len;
u32 c_len;
u32 u_file_size [[color("98C379")]];
u8 filter;
u8 filter_cto;
u8 n_mru;
u8 header_checksum;
};
// ------------------------------------------------------------------ helpers
/* Keep consuming b_info blocks while the next header still looks sane.
The chain ends where the loader stub begins. */
fn more_blocks() {
u64 pos = $;
if (pos + 12 >= std::mem::size())
return false;
u32 unc = std::mem::read_unsigned(pos, 4);
u32 cpr = std::mem::read_unsigned(pos + 4, 4);
u8 met = std::mem::read_unsigned(pos + 8, 1);
u8 ftid = std::mem::read_unsigned(pos + 9, 1);
u8 unused = std::mem::read_unsigned(pos + 11, 1);
if (unc == 0 || unc > 0x08000000)
return false;
if (cpr == 0 || cpr > unc)
return false;
if (unused != 0 || ftid > 0x50)
return false;
if (met != 2 && met != 3 && met != 5 && met != 8 && met != 9 && met != 14 && met != 15)
return false;
if (pos + 12 + cpr > std::mem::size())
return false;
return true;
};
/* Locate the trailing PackHeader without relying on the magic: look for a
32-byte window whose u_file_size matches p_info.p_filesize and whose format
byte is a known UPX format. Falls back to UPX's usual EOF-32 slot. */
fn find_packheader(u32 orig_size) {
u64 end = std::mem::size();
if (end < 64)
return 0;
u64 lo = end > 65536 ? end - 65536 : 0;
u64 i = end - 32;
while (i > lo) {
if (std::mem::read_unsigned(i + 24, 4) == orig_size) {
u8 fmt = std::mem::read_unsigned(i + 5, 1);
if (fmt == 22 || fmt == 12 || fmt == 25 || fmt == 26 || fmt == 23)
return i;
}
i = i - 1;
}
return end - 32;
};
// ------------------------------------------------------------------- layout
Elf64_Ehdr ehdr @ 0x00;
Elf64_Phdr phdrs[ehdr.e_phnum] @ ehdr.e_phoff;
LInfo l_info @ ehdr.e_phoff + ehdr.e_phentsize * ehdr.e_phnum;
PInfo p_info @ addressof(l_info) + sizeof(l_info);
BInfo blocks[while(more_blocks())] @ addressof(p_info) + sizeof(p_info);
PackHeader pack_header @ find_packheader(p_info.p_filesize);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment