Last active
August 30, 2026 20:17
-
-
Save PeterGabaldon/2ff25fa1a13aeab0adae0d40c72ae84e to your computer and use it in GitHub Desktop.
UPX-packed ELF64 HexPat
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #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