ELF obfuscator written in Rust
Note
日本語版のREADMEもあります(README in Japanese is also available)
$ cattleya -h
A CLI application to obfuscate ELF file(s)
Usage: cattleya [OPTIONS]
Options:
-i, --input <INPUT> input file name [default: ]
-o, --output <OUTPUT> output file name [default: ]
-c, --class change architecture class in the ELF
-e, --endian change endian in the ELF
-s, --sechdr nullify section header in the ELF
--symbol nullify symbols in the ELF
--comment nullify comment section in the ELF
--section <SECTION> nullify section in the ELF [default: ]
-r, --recursive <RECURSIVE> recursive [default: ]
-g, --got perform GOT overwrite
--got-l <GOT_L> GOT overwrite target library function name [default: ]
--got-f <GOT_F> GOT overwrite target function name [default: ]
--encrypt encrypt function name with the given key
--encrypt-f <ENCRYPT_F> encryption target function name [default: ]
--encrypt-key <ENCRYPT_KEY> encryption key [default: ]
--swap-symbol swap two symbol names in the .symtab
--swap-symbol-a <SWAP_SYMBOL_A> first symbol name to swap [default: ]
--swap-symbol-b <SWAP_SYMBOL_B> second symbol name to swap [default: ]
-h, --help Print help
-V, --version Print version
Both input and recursive options cannot be empty.
Obfuscates by changing the part of the ELF file that indicates endianness
$ cattleya -i input -e
start obfuscating input...
obfuscation done!
$ readelf -h input
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
...
$ readelf -h obfuscated
ELF Header:
Magic: 7f 45 4c 46 02 02 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, big endian
...
$ objdump -d obfuscated
objdump: obfuscated: file format not recognized
Obfuscates by changing the part of the ELF file that indicates the architecture (32bit or 64bit)
$ cattleya -i input -c
start obfuscating input...
obfuscation done!
$ file input
input: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=287e5058b7070a849e4153fb8f072381f780541b, for GNU/Linux 3.2.0, not stripped
$ file obfuscated
obfuscated: ELF 32-bit LSB shared object, x86-64, version 1 (SYSV), no program header, no section header
$ objdump -d obfuscated
objdump: obfuscated: file format not recognized
Obfuscates by keeping section header information confidential
$ cattleya -i input -s
start obfuscating input...
obfuscation done!
$ readelf -S input > /dev/null
$ readelf -S obfuscated > /dev/null
readelf: Error: no .dynamic section in the dynamic segment
Erases symbol names in the target
$ cattleya -i input --symbol
start obfuscating input...
obfuscation done!
$ readelf -p .strtab input
String dump of section '.strtab':
[ 1] Scrt1.o
[ 9] __abi_tag
[ 13] crtstuff.c
[ 1e] deregister_tm_clones
[ 33] __do_global_dtors_aux
[ 49] completed.0
[ 55] __do_global_dtors_aux_fini_array_entry
[ 7c] frame_dummy
[ 88] __frame_dummy_init_array_entry
[ a7] main.c
[ ae] __FRAME_END__
[ bc] _DYNAMIC
...
$ readelf -p .strtab obfuscated
String dump of section '.strtab':
No strings found in this section.
Erases comments in the target
$ cattleya -i input --comment
start obfuscating input...
obfuscation done!
$ readelf -p .comment input
String dump of section '.comment':
[ 0] GCC: (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
$ readelf -p .comment obfuscated
String dump of section '.comment':
No strings found in this section.
Encrypts the name of a specific function with AES 256bit using the given key:
$ cattleya -i bin/test_64bit --encrypt --encrypt-f fac --encrypt-key foo -o bin/res_enc
start obfuscating bin/test_64bit...
obfuscation done!
$ ./bin/res_enc
fac(1)=1
fib(1)=1
fac(5)=120
fib(5)=5
fac(10)=3628800
fib(10)=55
$ objdump -d bin/res_enc
...
000000000000120c <main>:
120c: f3 0f 1e fa endbr64
1210: 55 push %rbp
1211: 48 89 e5 mov %rsp,%rbp
1214: 48 83 ec 10 sub $0x10,%rsp
1218: 89 7d fc mov %edi,-0x4(%rbp)
121b: 48 89 75 f0 mov %rsi,-0x10(%rbp)
121f: bf 01 00 00 00 mov $0x1,%edi
1224: e8 20 ff ff ff call 1149 <�0,>
1229: bf 01 00 00 00 mov $0x1,%edi
122e: e8 6a ff ff ff call 119d <fib>
1233: bf 05 00 00 00 mov $0x5,%edi
1238: e8 0c ff ff ff call 1149 <�0,>
123d: bf 05 00 00 00 mov $0x5,%edi
1242: e8 56 ff ff ff call 119d <fib>
1247: bf 0a 00 00 00 mov $0xa,%edi
124c: e8 f8 fe ff ff call 1149 <�0,>
1251: bf 0a 00 00 00 mov $0xa,%edi
1256: e8 42 ff ff ff call 119d <fib>
125b: b8 00 00 00 00 mov $0x0,%eax
1260: c9 leave
1261: c3 ret
...
Function name "fac" is encrypted.
Swaps the st_name field of two .symtab entries. The string table itself is left untouched, so both names are still present in the binary, but each one now points at the other function's body. Disassemblers and tools such as nm / objdump will faithfully render the wrong name at the wrong address, sending static-analysis readers down the wrong call chain.
When the input binary contains DWARF debug information, this method also rewrites .debug_info (and .debug_aranges). Specifically, it locates the two DW_TAG_subprogram DIEs by DW_AT_name and swaps their DW_AT_low_pc / DW_AT_high_pc values.
$ cattleya -i bin/test_64bit --swap-symbol --swap-symbol-a fac --swap-symbol-b fib -o bin/res_swap
start obfuscating bin/test_64bit...
swap symbol names "fac" <-> "fib" success
$ nm bin/test_64bit | grep -E ' T (fac|fib)'
0000000000001149 T fac
000000000000119d T fib
$ nm bin/res_swap_symbol | grep -E ' T (fac|fib)'
000000000000119d T fac
0000000000001149 T fib
$ ./bin/test_64bit
fac(1)=1
fib(1)=1
fac(5)=120
fib(5)=5
fac(10)=3628800
fib(10)=55
$ ./bin/res_swap_symbol
fac(1)=1
fib(1)=1
fac(5)=120
fib(5)=5
fac(10)=3628800
fib(10)=55
Note that this method requires a non-stripped binary (it operates on .symtab).
Overwrites the GOT section with a specified value
$ cat bin/got.c
// gcc got.c -no-pie -o got
#include <stdio.h>
#include <stdlib.h>
int secret(char* s) {
if (s[0] == 's' && s[1] == 'e' && s[2] == 'c' && s[3] == 'r' && s[4] == 'e' && s[5] == 't' && s[6] == '?') {
printf("secret function called\n");
}
return 0;
}
int main() {
system("secret?\n");
}
$ cattleya -i bin/got --got --got-l system --got-f secret -o bin/res_got
start obfuscating bin/got...
obfuscation done!
$ ./bin/res_got
secret function called
As shown below, only the system function is called in the main function as far as disassembly of main is concerned:
$ objdump -d bin/res_got
...
00000000004011e1 <main>:
4011e1: f3 0f 1e fa endbr64
4011e5: 55 push %rbp
4011e6: 48 89 e5 mov %rsp,%rbp
4011e9: 48 8d 05 2b 0e 00 00 lea 0xe2b(%rip),%rax # 40201b <_IO_stdin_used+0x1b>
4011f0: 48 89 c7 mov %rax,%rdi
4011f3: e8 68 fe ff ff call 401060 <system@plt>
4011f8: b8 00 00 00 00 mov $0x0,%eax
4011fd: 5d pop %rbp
4011fe: c3 ret
...
By specifying the directory name in the recursive option, the same obfuscation can be applied to all ELF files in that directory:
$ tree recursive_sample
recursive_sample
├── bar
└── foo
0 directories, 2 file
$ cattleya -r recursive_sample --symbol
...
$ tree obfuscated_dir
tree obfuscated_dir
obfuscated_dir
└── recursive_sample
├── bar
└── foo
1 directory, 2 files
$ cargo test
By running this command, examples of binaries obfuscated using each obfuscation methods will be created in the bin directory.
Note that all tests are defined in src/main.rs. Some tests require external tools such as readelf and nm, and some tests need an environment capable of executing ELF files.