Linux Kernel runtime unpacker and binary signature
by @Jonathan Salwan - 2013-03-25Mach-OS and iOS have implemented a runtime unpacker and signature verification when binaries are exectued. This allows to check if the binary is approved by Apple and make it more complicated to reverse (We need to dump the memory). In this short note, we will see what can be done on Linux with this same implementation. I have implemented a runtime unpacker for the ndh2k13 CTF (crackme300) when a binary is loaded in memory. When reversing the binary, you can see that the TEXT section is packed, as shown below:
; [12] va=0x00400610 pa=0x00000610 sz=1048 vsz=1048 rwx=-r-x .text / function: section..text (155) | ; -------- section..text: | 0x00400610 23ae7deca991 and ebp, [rsi-0x6e561383] | 0x00400616 94 xchg esp, eax | 0x00400617 437ad8 jp loc.004005f2 | 0x0040061a e6d5 out 0xd5, al | 0x0040061c d106 rol dword [rsi], 1 | 0x0040061e d7 xlatb | 0x0040061f b30a mov bl, 0xa | 0x00400621 f01d1bc498ff lock sbb eax, 0xff98c41b | 0x00400627 93 xchg ebx, eax | 0x00400628 643268b9 xor ch, [fs:rax-0x47] | 0x0040062c e341 jrcxz 0x40066f | 0x0040062e 0f53e6 rcpps xmm4, xmm6 | 0x00400631 e0c7 loopnz 0x4005fa | 0x00400633 12ab939a8730 adc ch, [rbx+0x30879a93] | 0x00400639 285a08 sub [rdx+0x8], bl | 0x0040063c d8e6 fsub st0, st6 | 0x0040063e dd invalid | 0x0040063f 291e sub [rsi], ebx | 0x00400641 08ff or bh, bh | 0x00400643 54 push rsp | 0x00400644 39dd cmp ebp, ebx | 0x00400646 12cc adc cl, ah | 0x00400648 1d7720a79d sbb eax, 0x9da72077 | 0x0040064d b1b1 mov cl, 0xb1 | 0x0040064f 60 invalid | 0x00400650 cdc0 int 0xc0 | 0x00400652 57 push rdi | 0x00400653 82 invalid | 0x00400654 7617 jbe 0x40066d | 0x00400656 82 invalid | 0x00400657 d3a4f5e85f4c5a shl dword [rbp+rsi*8+0x5a4c5fe8], cl | 0x0040065e 0800 or [rax], al
And the entrypoint is not modified.
$ readelf -h ./crackme.packed | grep "Entry point address" Entry point address: 0x400610 $
Now, to unpack the binary when it is executed on the Operating System, we need to patch the kernel source. When a binary
is executed, the load_elf_binary
function is called. This function setups all VMA and is the one that needs to be changed.
First of all, we need to know if the binary is packed or not. To do that I decided to put a flag in the ELF header.
$ readelf -h ./crackme.packed | grep "Flags" Flags: 0x20 $
When the ELFHeader.flags values 0x20
the binary is packed, otherwise it is not. In the load_elf_binary
function we check if
the binary has this flag.
#define PACKED_MASK 0x20 [...] static int load_elf_binary(struct linux_binprm *bprm, struct pt_regs *regs) { [...] if (loc->elf_ex.e_flags & PACKED_MASK){ packer_flag = 1; } [...] }
Currently, only the TEXT section is packed, therefore we need to know what is the size of TEXT section.
elf_ppnt = elf_phdata; for (i = 0; i < loc->elf_ex.e_phnum; i++, elf_ppnt++){ if (elf_ppnt->p_type == PT_LOAD){ if (packer_flag && (elf_ppnt->p_flags & 0x2)) upk_binary(start_code, loc->elf_ex.e_entry, elf_ppnt->p_filesz); break; } }
The upk_binary
is the unpack function, for the context of the NDH2k13 this function uses a simple xor-based algorithm.
static int upk_binary(long unsigned int start_code, unsigned long e_entry, unsigned long p_filesz) { unsigned long base, size, x, i; unsigned char *p; unsigned char key[] = { 0x12, 0x43, 0x34, 0x65, 0x78, 0xcf, 0xdc, 0xca, 0x98, 0x90, 0x65, 0x31, 0x21, 0x56, 0x83, 0xfa, 0xcd, 0x30, 0xfd, 0x12, 0x84, 0x98, 0xb7, 0x54, 0xa5, 0x62, 0x61, 0xf9, 0xe3, 0x09, 0xc8, 0x94, 0x12, 0xe6, 0x87 }; base = e_entry - start_code; /* base phy */ size = p_filesz - base; p = (unsigned char *)e_entry; for (i = 0, x = 0 ; i < size ; i++, x++){ if (x == 35) x = 0; p[i] ^= key[x]; } return 0; }
You can found the complete patch to Linux 3.7.10 here. Currently, this implementation is not really ideal because the packing algorithm is not satisfying. And we cannot verify that the binary is signed. Recently RedHat commited some tools in Linux crypto API to manipulate RSA (patch) and their works on modsign feature. You can found his work on his repository.
Let's take a look at that.
$ cp /lib64/modules/3.6.11-gentoo/misc/vmmon.ko . $ ./modsign.sh ./vmmon.ko 2048R/328EABC4 $ ls -l ./vmmon.ko* -rw-r--r-- 1 jonathan users 98K Mar 25 15:47 ./vmmon.ko -rw-r--r-- 1 jonathan users 98K Mar 25 15:47 ./vmmon.ko.signed $
2048R/328EABC4
is my gpg key.
$ gpg --list-key /home/jonathan/.gnupg/pubring.gpg --------------------------------- pub 2048R/328EABC4 2013-02-01 uid Jonathan Salwan (shell-storm key) <jonathan.salwan at shell storm org> sub 2048R/604B9EDF 2013-02-01 $
The modsign.sh
script adds a section which contains the binary signature.
[...] [+] section 28: .comment [+] section 29: .note.GNU-stack [+] section 30: .module_sig name string index 0000013f type 00000001 (progbits) flags 0000000000000003 details address 0000000000000000 offset 000000000000b8dc size 0000000000000048 link 0000000000000000 info 0000000000000001 alignment 00000000 entsize 00000000 [+] section 31: .shstrtab [+] section 32: .symtab [...]
Currently, this patch just signs Linux modules but that would be nice if the system could check the signature and decrypt the binary. In this context, the Kernel embeds the public key and when the load_elf_binary function is called, it check the signature and decrpyts the binary. This mechanism prevents anyone, except the developpers, from compiling and signing the binary. Maybe it will be useful in embedded system. I will try to implement it.