Tech Talk: Behind the curtain Obfuscating Linux Symbols
Obfuscating Linux Symbols: a novel approach to evade static analysis in Linux malware. Original security research from the Bulletproof.
Introduction
Obfuscating Linux Symbols: a novel approach to evade static analysis in Linux malware.
This is a Bulletproof Tech Talk article: original research from our red team covering issues, news, and tech that interests them. It’s more technical and in-depth that our usual blog content, but no less interesting.
This blog looks at obfuscating Linux Symbols using dl_iterate_phdr
with callbacks. It represents original security research from the Bulletproof Red Team.
API hashing has long been used throughout Windows malware development. There are many publicly available proof-of-concepts (PoCs) for this technique, which helps evade static analysis by hiding entries from the Import Address Table on Windows. This technique is required to prevent defensive software from easily identifying the functionality of a potentially malicious application. For example the presence of known API calls used for process injection would be a big red flag of malicious intent. The same detection logic is now being applied on Linux based systems as EDRs continue to enhance detection capabilities on these platforms. This blog post and PoC aims to achieve that same result of obscuring used symbols that may indicate the payloads malicious functionality, by hiding entries from the Symbol Table, with the aim of bypassing static detection mechanisms used by Linux based EDR's.
Share this Article
Contents
- Windows vs. Linux
- What is dl_iterate_phdr?
- Using dl_iterate_phdr
- Parsing Section Headers to locate the Symbol Table
- Parsing and printing all symbols from the Symbol Table
- Generating a hash for the system function
- Implementing the hashing function
- Executing the resolved function
- Caveats
- Conclusion
- References
Related Service
Red TeamingWindows vs. Linux
A few weeks ago, I started exploring different methods to hide from the dynamic symbol table (.dynsym) within an ELF binary. It turns out the approach is quite similar to what you'd do on Windows, with a slight twist.
On Windows the high-level process is as follows:
- Get the base address of the library where our function of interest is, e.g, kernel32.dll.
- Locate the kernel32 Export Address Table (EAT).
- Iterate through each exported function name by the kernel32 module.
- For each exported function name, calculate its hash value and compare it against our own, then calculate the function's virtual address if there's a match.
On Linux the high-level approach is similar to Windows:
- Use the dl_iterate_phdr function to iterate over all shared objects loaded in the current process.
- For each shared object (or just libc), locate its symbol table (e.g, the dynamic symbol table (dynsym) section in the ELF file).
- Iterate through each symbol name in the symbol table of the shared object.
- For each exported symbol name, calculate its hash value using our hash function.
- If the calculated hash equals the target hash, then retrieve the symbol's address.
As you can see, the process is quite similar across both Windows and Linux.
What is dl_iterate_phdr?
The dl_iterate_phdr
function is apart of the GNU C Library (glibc) and it is used to walk over the list of shared objects loaded in a process. It calls the callback
function once for each object until all objects have been processed or if the callback
returns a non zero value. Using this function allows us to inspect each shared object and view their internal structures, such as the dynamic symbol table, which is exactly what we want to do here.
This function takes the following two parameters:
- A
callback
function that it will call for each shared object. - A pointer to predefined user data
(void *data)
.
Here is a snippet taken from the man page of dl_iterate_phdr which demonstrates how this function is defined:
int dl_iterate_phdr(int (*callback)(struct dl_phdr_info *info,
size_t size, void *data),
void *data);
The callback function receives a dl_phdr_info
struct, which contains information about the shared object. The struct looks like this, which is also a snippet taken from the man page:
struct dl_phdr_info {
ElfW(Addr) dlpi_addr; /* Base address of object */
const char *dlpi_name; /* (Null-terminated) name of object */
const ElfW(Phdr) *dlpi_phdr; /* Pointer to array of ELF program headers for this object */
ElfW(Half) dlpi_phnum; /* Number of items in dlpi_phdr */
/* The following fields were added in glibc 2.4, after the first
version of this structure was available. Check the size
argument passed to the dl_iterate_phdr callback to determine
whether or not each later member is available. */
unsigned long long dlpi_adds; /* Incremented when a new object may have been added */
unsigned long long dlpi_subs; /* Incremented when an object may have been removed */
size_t dlpi_tls_modid; /* If there is a PT_TLS segment, its module ID as used in TLS relocations, else zero */
void *dlpi_tls_data; /* The address of the calling thread's instance of this module's PT_TLS segment, if it has one and it has been allocated in the calling thread, otherwise a null pointer */
};
As we see from the above structure, it contains the following information:
- The base address of the shared object
- The name of the shared object
- A pointer to an array of ELF program headers in the object
- The number of program headers
Also, as stated in the man page the ElfW() macro turns its argument into the name of an ELF data type suitable for the hosts hardware architecture. For example, on a 32-bit platform, ElfW(Addr) resolves to Elf32_Addr and on a 64-bit platform it resolves to Elf64_Addr. This just ensures that the dl_phdr_info struct correctly represents the underlying ELF format of the loaded shared objects, regardless of the hosts architecture.
Using dl_iterate_phdr
As I mentioned previously, we can use this function to loop through all the shared objects loaded in our current process. This allows us to get the names and base addresses of these shared objects. Here is the code that will achieve this for us:
// callback function to be called for each shared object
int callback(struct dl_phdr_info *info, size_t size, void *data) {
if (info->dlpi_name && info->dlpi_name[0]) {
printf("Shared object: %s @ %p\n", info->dlpi_name, (void *)info->dlpi_addr);
}
else {
printf("Shared object: (null)\n");
}
return 0;
}
int main() {
// iterate over the shared objects in the current process
dl_iterate_phdr(callback, NULL);
exit(EXIT_SUCCESS);
}
The dl_iterate_phdr
function goes through all shared objects and calls the callback function for each one as stated previously, this can be observed below from running the program:
$ ./main
Shared object: (null)
Shared object: linux-vdso.so.1 @ 0x7ffe6d5cc000
Shared object: /usr/lib/libc.so.6 @ 0x7f4c3cfe8000
Shared object: /lib64/ld-linux-x86-64.so.2 @ 0x7f4c3d1f8000
The output above shows the shared objects and their corresponding base addresses. The null shared object is just our own program, this is because its always the the first entry in this list. However, we are currently only interested in libc.so.6 which provides us with the essential functions for our program.
Now that we have this information, we can continue to parse the section headers and locate the dynamic symbol table within the libc shared object.
Parsing Section Headers to locate the Symbol Table
To locate the symbol table, we will:
- Use
dl_iterate_phdr
to iterate over all shared objects loaded in the current process - Identify the
libc.so.6
shared object and map it to memory - Parse the ELF headers and section headers of
lib.so.6
to locate .dynsym
We will start by using the dl_iterate_phdr
function, which we have already explained, so I will not go into detail here. Below is the code that initiates the iteration and identifies the libc.so.6
shared object:
void locate_symtable(const char *obj_path);
// callback function to be called for each shared object
int callback(struct dl_phdr_info *info, size_t size, void *data) {
if (info->dlpi_name && info->dlpi_name[0]) {
printf("Shared object: %s @ %p\n", info->dlpi_name, (void *)info->dlpi_addr);
if (strstr(info->dlpi_name, "libc.so")) {
locate_symtable(info->dlpi_name);
}
}
else {
printf("Shared object: (null)\n");
}
return 0;
}
We added the locate_symtable
function, which is called from the callback
function when the libc.so
shared object is identified. This function will map the ELF file into memory and read its headers to locate the symbol tables.
Here is the locate_symtable
function:
// function to locate the symbol table in the shared object
void locate_symtable(const char *obj_path) {
int fd = open(obj_path, O_RDONLY);
if (fd < 0) {
perror("open");
return;
}
// map the ELF file into memory
off_t file_size = lseek(fd, 0, SEEK_END);
void *elf_base = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (elf_base == MAP_FAILED) {
perror("mmap");
close(fd);
return;
}
// read ELF header
ElfW(Ehdr) *ehdr = (ElfW(Ehdr) *)elf_base;
// read section headers
ElfW(Shdr) *shdrs = (ElfW(Shdr) *)((char *)elf_base + ehdr->e_shoff);
const char *shstrtab = (char *)elf_base + shdrs[ehdr->e_shstrndx].sh_offset;
// iterate over section headers to find the dynamic symbol table
for (int i = 0; i < ehdr->e_shnum; i++) {
if (shdrs[i].sh_type == SHT_DYNSYM) {
const char *section_name = shstrtab + shdrs[i].sh_name;
printf(" Found symbol table: %s\n", section_name);
}
}
munmap(elf_base, file_size);
close(fd);
}
Our
- Open the shared object file in read-only mode
- Map the entire file into memory
- Read the ELF header to get information such as offsets to the section headers
- Reads the section headers and the section header string table to access section names and their attributes
- Iterates over all section headers to find the SHT_DYNSYM section
All in all, this will allow us to identify the dynamic symbol table in the libc.so.6
, this can be seen below once compiled and run:
$ ./main
Shared object: (null)
Shared object: linux-vdso.so.1 @ 0x7ffe6d5cc000
Shared object: /usr/lib/libc.so.6 @ 0x7f4c3cfe8000
Found symbol table: .dynsym <--- found dynamic symbol table
Shared object: /lib64/ld-linux-x86-64.so.2 @ 0x7f4c3d1f8000
Parsing and printing all symbols from the Symbol Table
Next, we'll expand upon our locate_symtable
function to print all symbols found in the dynsym of the libc.so.6
shared object.
We will have to make a few changes for this to work:
- Read the symbol table and its corresponding string table.
- Determine the number of symbols in the table.
- Iterate the entries of the symbol table and print every symbol name.
void locate_symtable(const char *obj_path) {
int fd = open(obj_path, O_RDONLY);
if (fd < 0) {
perror("open");
return;
}
// map the ELF file into memory
off_t file_size = lseek(fd, 0, SEEK_END);
void *elf_base = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (elf_base == MAP_FAILED) {
perror("mmap");
close(fd);
return;
}
// read ELF header
ElfW(Ehdr) *ehdr = (ElfW(Ehdr) *)elf_base;
// read section headers
ElfW(Shdr) *shdrs = (ElfW(Shdr) *)((char *)elf_base + ehdr->e_shoff);
const char *shstrtab = (char *)elf_base + shdrs[ehdr->e_shstrndx].sh_offset;
// iterate over section headers to find the dynamic symbol table
for (int i = 0; i < ehdr->e_shnum; i++) {
if (shdrs[i].sh_type == SHT_DYNSYM) {
const char *section_name = shstrtab + shdrs[i].sh_name;
printf(" Found symbol table: %s\n", section_name);
// read the symbol table
ElfW(Sym) *symtab = (ElfW(Sym) *)((char *)elf_base + shdrs[i].sh_offset);
const char *strtab = (const char *)elf_base + shdrs[shdrs[i].sh_link].sh_offset;
int num_symbols = shdrs[i].sh_size / shdrs[i].sh_entsize;
for (int j = 0; j < num_symbols; j++) {
printf(" Symbol: %s\n", strtab + symtab[j].st_name);
}
}
}
munmap(elf_base, file_size);
close(fd);
}
After identifying the symbol table section as we have done previously, we:
- Read the symbol table at
shdrs[i].sh_offset
and its corresponding string table atshdrs[shdrs[i].sh_link].sh_offset
. - Determine the number of symbols by dividing the section size by the size of each entry using
shdrs[i].sh_size / shdrs[i].sh_entsize
. - Iterate through the symbol table entries and print the name of each symbol using the string table, as seen below:
$ ./main
Shared object: (null)
Shared object: linux-vdso.so.1 @ 0x7ffe6d5cc000
Shared object: /usr/lib/libc.so.6 @ 0x7f4c3cfe8000
Found symbol table: .dynsym
Symbol:
Symbol: _dl_argv
Symbol: _dl_find_dso_for_object
Symbol: __libc_enable_secure
Symbol: _dl_deallocate_tls
Symbol: __tls_get_addr
Symbol: __libc_stack_end
Symbol: _rtld_global_ro
Symbol: _dl_signal_error
Symbol: _dl_signal_exception
Symbol: _dl_audit_symbind_alt
Symbol: __tunable_is_initialized
Symbol: _dl_rtld_di_serinfo
Symbol: _dl_allocate_tls
Symbol: __tunable_get_val
Symbol: _dl_catch_exception
Symbol: _dl_allocate_tls_init
Symbol: _rtld_global
Symbol: __nptl_change_stack_perm
Symbol: _dl_audit_preinit
Symbol: fgetc
Symbol: pthread_attr_setscope
Symbol: pthread_attr_getstacksize
Symbol: envz_strip
Symbol: pthread_attr_getstacksize
....[snip]....
Now that we are able to view all the symbols, this brings us closer to the objective of implementing symbol hashing for dynamic function resolution.
Generating a hash for the system function
To proceed, we need to generate a hash for the system
function using a basic Djb2 hashing function:
unsigned long HASH(unsigned char *str) {
unsigned long hash = 6543;
int c;
while ((c = *str++)) {
hash = ((hash << 5) + hash) + c;
}
return hash;
}
int main() {
unsigned char *function_name = (unsigned char *)"system";
unsigned long hash = HASH(function_name);
printf("hash of %s: %u\n", function_name, hash);
return 0;
}
This will give us the hash of the string system
which corresponds to: 2227611796
Implementing the hashing function
We can now implement this hashing function into our code and resolve the function if it matches the hash, we will do this by pre-defining our system hash which we obtained previously via the HASH
hashing function, then compare all symbols to this hash, and if it matches, we know it is the system symbol.
Here is our updated code to reflect this:
// djb2 hash function
uint32_t HASH(const char *str) {
uint32_t hash = 6543;
int c;
while ((c = *str++)) {
hash = ((hash << 5) + hash) + c;
}
return hash;
}
// function to locate the symbol table in the shared object and resolve function addresses
void locate_symtable(const char *obj_path, void *base_addr) {
int fd = open(obj_path, O_RDONLY);
if (fd < 0) {
perror("open");
return;
}
// map the ELF file into memory
off_t file_size = lseek(fd, 0, SEEK_END);
void *elf_base = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (elf_base == MAP_FAILED) {
perror("mmap");
close(fd);
return;
}
// read ELF header
ElfW(Ehdr) *ehdr = (ElfW(Ehdr) *)elf_base;
// read section headers
ElfW(Shdr) *shdrs = (ElfW(Shdr) *)((char *)elf_base + ehdr->e_shoff);
const char *shstrtab = (char *)elf_base + shdrs[ehdr->e_shstrndx].sh_offset;
// system hash calculated from other snippet
uint32_t target_hash = 2227611796;
// iterate over section headers to find the dynamic symbol table
for (int i = 0; i < ehdr->e_shnum; i++) {
if (shdrs[i].sh_type == SHT_DYNSYM) {
const char *section_name = shstrtab + shdrs[i].sh_name;
printf(" Found symbol table: %s\n", section_name);
// read symbol table
ElfW(Sym) *symtab = (ElfW(Sym) *)((char *)elf_base + shdrs[i].sh_offset);
const char *strtab = (const char *)elf_base + shdrs[shdrs[i].sh_link].sh_offset;
int num_symbols = shdrs[i].sh_size / shdrs[i].sh_entsize;
// iterate each symbol to check for a matching hash with system
for (int j = 0; j < num_symbols; j++) {
const char *sym_name = strtab + symtab[j].st_name;
uint32_t sym_hash = HASH(sym_name);
if (sym_hash == target_hash) {
printf(" Found target symbol: %s\n", sym_name);
// calculate the functions address (need to calculate this by also adding the base addr)
void *func_addr = (void *)((char *)base_addr + symtab[j].st_value);
printf(" Address of system: %p\n", func_addr);
}
}
}
}
munmap(elf_base, file_size);
close(fd);
}
// callback function to be called for each shared object
int callback(struct dl_phdr_info *info, size_t size, void *data) {
if (info->dlpi_name && info->dlpi_name[0]) {
printf("Shared object: %s @ %p\n", info->dlpi_name, (void *)info->dlpi_addr);
if (strstr(info->dlpi_name, "libc.so")) {
locate_symtable(info->dlpi_name, (void *)info->dlpi_addr);
}
} else {
printf("Shared object: (null)\n");
}
return 0;
}
int main() {
// iterate over the shared objects in the current process
dl_iterate_phdr(callback, NULL);
return 0;
}
What we have done at this point is:
- Added the Djb2 hash function to calculate the hash of each symbol
- Added the pre-defined system hash calculated from our Djb2 hashing calculator
- Modified our Symbol Table parsing functionality to compare hashes to the system hash, and resolve the function if the hash matches.
Executing the resolved function
Once we run the updated code, we can see that it finds the system address. This can be confirm via GDB as well:
gef➤ b *main+30
Breakpoint 1 at 0x15ef: file main.c, line 98.
gef➤ r
....</snip>....
Shared object: (null)
Shared object: linux-vdso.so.1 @ 0x7ffdf0fe9000
Shared object: /usr/lib/libc.so.6 @ 0x75d6d33a3000
Found symbol table: .dynsym
Found target symbol: system
Address of system: 0x75d6d33f3f10 <--- system addr
Shared object: /lib64/ld-linux-x86-64.so.2 @ 0x75d6d35b3000
Breakpoint 1, 0x00005555555555ef in main () at main.c:98
gef➤ x/x 0x75d6d33f3f10
0x75d6d33f3f10 <system>: 0xfa1e0ff3
As the output from GDB shows, we have the address of system()!! Now, at this point, we really have everything set up in order to use this resolved function to execute a command, we can simply edit the code to do this. We will execute the uname command via system, it should be noted that this string will appear in the binary, but there are ways to get around this.
First, we define a function pointer that has the same function signature as system:typedef int (*system_func)(const char *);
Then inside of the locate_symtable
function, once we have found the address of system
, we cast system_func
:system_func _system = (system_func)func_addr;
Finally, we call the system function using the function pointer to execute the uname command:_system("uname");
Overall, it looks something like this:
if (sym_hash == target_hash) {
printf(" Found target symbol: %s\n", sym_name);
// calculate the functions address (need to calculate this by also adding the base addr)
void *func_addr = (void *)((char *)base_addr + symtab[j].st_value);
printf(" Address of system: %p\n", func_addr);
// define a function pointer type for system
typedef int (*system_func)(const char *);
system_func _system = (system_func)func_addr;
// execute uname using the resolved system function
_system("uname");
}
Once, executed we should have it output the value from uname
:
$ ./main
Shared object: (null)
Shared object: linux-vdso.so.1 @ 0x7ffe1f9d3000
Shared object: /usr/lib/libc.so.6 @ 0x7028ce28e000
Found symbol table: .dynsym
Found target symbol: system
Address of system: 0x7028ce2def10
Linux <--- uname executed
Shared object: /lib64/ld-linux-x86-64.so.2 @ 0x7028ce49e000
We can now also check the symbols of this binary to show that system() is in fact not there. This binary was also compiled with symbols:
$ readelf -Ws ./main
Symbol table '.dynsym' contains 16 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND dl_iterate_phdr@GLIBC_2.2.5 (2)
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.34 (3)
3: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTable
4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 (2)
5: 0000000000000000 0 FUNC GLOBAL DEFAULT UND mmap@GLIBC_2.2.5 (2)
6: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.2.5 (2)
7: 0000000000000000 0 FUNC GLOBAL DEFAULT UND lseek@GLIBC_2.2.5 (2)
8: 0000000000000000 0 FUNC GLOBAL DEFAULT UND close@GLIBC_2.2.5 (2)
9: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
10: 0000000000000000 0 FUNC GLOBAL DEFAULT UND munmap@GLIBC_2.2.5 (2)
11: 0000000000000000 0 FUNC GLOBAL DEFAULT UND open@GLIBC_2.2.5 (2)
12: 0000000000000000 0 FUNC GLOBAL DEFAULT UND perror@GLIBC_2.2.5 (2)
13: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
14: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5 (2)
15: 0000000000000000 0 FUNC GLOBAL DEFAULT UND strstr@GLIBC_2.2.5 (2)
Symbol table '.symtab' contains 36 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS main.c
2: 0000000000000000 0 FILE LOCAL DEFAULT ABS
3: 0000000000003de0 0 OBJECT LOCAL DEFAULT 21 _DYNAMIC
4: 00000000000020ac 0 NOTYPE LOCAL DEFAULT 17 __GNU_EH_FRAME_HDR
5: 0000000000003fe8 0 OBJECT LOCAL DEFAULT 23 _GLOBAL_OFFSET_TABLE_
6: 0000000000000000 0 FUNC GLOBAL DEFAULT UND dl_iterate_phdr@GLIBC_2.2.5
7: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.34
8: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTable
9: 0000000000004050 0 NOTYPE WEAK DEFAULT 24 data_start
10: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5
11: 0000000000004060 0 NOTYPE GLOBAL DEFAULT 24 _edata
12: 0000000000001608 0 FUNC GLOBAL HIDDEN 15 _fini
13: 0000000000000000 0 FUNC GLOBAL DEFAULT UND mmap@GLIBC_2.2.5
14: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.2.5
15: 0000000000000000 0 FUNC GLOBAL DEFAULT UND lseek@GLIBC_2.2.5
16: 0000000000000000 0 FUNC GLOBAL DEFAULT UND close@GLIBC_2.2.5
17: 0000000000004050 0 NOTYPE GLOBAL DEFAULT 24 __data_start
18: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
19: 0000000000004058 0 OBJECT GLOBAL HIDDEN 24 __dso_handle
20: 0000000000002000 4 OBJECT GLOBAL DEFAULT 16 _IO_stdin_used
21: 0000000000004068 0 NOTYPE GLOBAL DEFAULT 25 _end
22: 00000000000010d0 38 FUNC GLOBAL DEFAULT 14 _start
23: 000000000000120f 793 FUNC GLOBAL DEFAULT 14 locate_symtable
24: 0000000000004060 0 NOTYPE GLOBAL DEFAULT 25 __bss_start
25: 0000000000000000 0 FUNC GLOBAL DEFAULT UND munmap@GLIBC_2.2.5
26: 00000000000015e9 31 FUNC GLOBAL DEFAULT 14 main
27: 0000000000000000 0 FUNC GLOBAL DEFAULT UND open@GLIBC_2.2.5
28: 0000000000000000 0 FUNC GLOBAL DEFAULT UND perror@GLIBC_2.2.5
29: 0000000000004060 0 OBJECT GLOBAL HIDDEN 24 __TMC_END__
30: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
31: 0000000000001528 193 FUNC GLOBAL DEFAULT 14 callback
32: 00000000000011c9 70 FUNC GLOBAL DEFAULT 14 HASH
33: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5
34: 0000000000001000 0 FUNC GLOBAL HIDDEN 12 _init
35: 0000000000000000 0 FUNC GLOBAL DEFAULT UND strstr@GLIBC_2.2.5
As the output above from readelf suggests, there is no system() symbol present. This can also be seen when running strings against the binary. The only entry found relates to the printf() statement.
$ strings ./main | grep -i sys
Address of system: %p
Caveats
This will not work if the binary was compiled statically. The dl_iterate_phdr function is used to walk through a list of shared objects that are dynamically loaded into an executable, and since statically linked programs do not load the libc
shared object, there is nothing for dl_iterate_phdr
to iterate over.
Conclusion
By following the code snippets above this blog should provide you with the understanding to implement simple symbol hashing for your Linux based payloads. The code that was used within this blog is for demonstrative purposes to help understand the concepts alongside the walk through. A more complete proof of concept to accompany this blog for this can be found here. Hopefully this blog has proved useful and highlighted a potentially familiar approach to symbol hashing for Linux. As EDRs and prevention systems continue to advance on additional platforms such as Linux and MacOS we feel its important to spend time developing solutions to existing problems around payload development on different platforms.
Keep the hackers out with red team
Don’t let your business be breached. Find your security flaws with a red team assessment from Bulletproof.
Get started todayRelated resources
Trusted cyber security & compliance services from a certified provider
Get a quote today
If you are interested in our services, get a free, no obligation quote today by filling out the form below.