Decrypting SHell Compiled (SHC) ELF files

In its recent blog post AhnLab described a campaign that relies on SHell Compiled (SHC) ELF files. I wanted to see if I can replicate their reverse engineering work and decrypt actual shell commands they had shared in their post. This turned out to be a bit more complicated than I thought, hence this post aiming at making it a bit easier for you.

Before I go into nitty-gritty details – when I try to crack new stuff I usually look for an existing body of work first. A really quick google search helped me to discover a tool called UnSHC that helps to decrypt SHC ELF files in a generic way. And I must digress here for a moment – it’s an amazing hack of a tool really – a shell script engaging lots of CLI tools trying to discover the inner working of the encryption via automated code analysis, find the decryption routine and then actually produce the decrypted script as an output. While it didn’t work for me, I feel kudos to the author are in order. Studying the inner working of UnShc was a pure pleasure. Thumbs up!

Coming back to the AhnLab post. I was intrigued by the Alleged RC4 encryption used by SHC and thought — okay, I just need to load it into a debugger, step through it, maybe instrument it here and there, and then look at the decrypted buffers. So, I did that, then I realized that despite walking through the code, I could only decrypt part of the encrypted data. I could decrypt the ‘internal’ strings of SHC, but not the final shell code. I did correctly guess where the encrypted shell script is, I did instrument the debugger to go there, but while trying to decrypt the encrypted blob… I was getting another binary blob that looked like garbage.

Hmm something was really fishy there.

After staring at the code of the sample in IDA, I realized there is a routine where the executable is trying to retrieve a value of a particular environment variable. After studying it a bit more, I realized that the SHC author engaged a clever trick to make reversers’ life a bit harder. When the program is executed a tuple of an environment variable and its value derived from a process ID, number of arguments (argc), the actual routine itself (probably to detect ‘int 3’ opcodes in it, if debugged) are added to the process environment. The program then calls execve on its own file. This makes the program restart with the same pid (the ELF image is overwritten in memory and restarted). And this finally leads to the execution of the aforementioned routine again, and this time the required tuple of environment variable and its value are present. Only then the decryption of the actual embedded shell script is possible. From a debugging/instrumenting perspective it’s unpleasant, so I had to quickly devise a way to bypass it.

It turned out that it’s easier than expected.

The solution: the very same ‘environment-operating’ routine can be called twice. The first time it will look for the environment variable, and it won’t find it there, so it will add it. The second time we execute it via instrumentation, the environment variable will be there. So, it will read the value of the environment variable, set appropriate inner variables, and with that in place we can decrypt the main shell script within a single process instance.

Let’s have a look at the example: 256ab7aa7b94c47ae6ad6ca8ebad7e2734ebaa21542934604eb7230143137342.

We load it into edb first, and then make a breakpoint on 0x400FDD — this is prior to executing aforementioned ‘environment variable’ tinkering procedure. Then we run the program (F9). We should get a break here:

We step over it F8, F8 and now we end up in 0x400FE5.

We then re-run the code above to make it look like we execute it as if we were in the new instance of the process. So. we go back to 0x400FDD and set the RIP to ‘here’ — right click and ‘Set RIP to this instruction’, or CTRL+*. We do F8, F8. And we are set.

All we have to do now is F8 many times until we reach 0x4011A7, at this stage point your dump window to the location rdi points to. Then execute decryption routine and you will see the decrypted shell script in the data dump window:

Update

Additionally, in 2023-12 NtSuspendProcess(NtCurrentProcess()) contacted me to let me know of his script that uses a clever trick to achieve the same thing w/o using debugger.

See the script here.

ELF sections stats

If you follow my blog you may know that I have dedicated a lot of time building a very comprehensive list of PE Sections, Today I realized that I never looked at ELF section the same way. With this post I took a first stab at it. The below are nothing but quick & dirty stats from a reasonably sized sampleset of ELF files:

47165 .shstrtab
44289 .bss
33390 .comment
31664 .strtab
31651 .symtab
23516 .data
20756 .got
12634 .debug_aranges
12628 .debug_line
12628 .debug_info
12628 .debug_abbrev
12181 .debug_frame
11408 .sbss
10339 .mdebug.abi32
9359 .ARM.attributes
8239 .jcr
6703 .dynamic
6547 .rodata
6432 .debug_str
6386 .ctors
6343 .dtors
6035 .debug_pubnames
5846 .debug_ranges
5834 .debug_loc
5101 .fini_array
4915 .data.rel.ro
4858 .pdr
4133 .eh_frame
3056 .fini
2919 .text
2877 .plt
2515 .init
2444 .sdata
1858 .got.plt
1778 .note
1542 .init_array
1335 .stabstr
1335 .stab
1140 .rel.plt
1003 __libc_freeres_ptrs
862 .tbss
839 .tdata
820 .note.gnu.gold-version
812 .gcc_except_table
791 __libc_thread_subfreeres
739 .ARM.exidx
484 .ARM.extab
423 .data.rel.ro.local
414 .eh_frame_hdr
283 __libc_atexit
245 __libc_subfreeres
239 .note.ABI-tag
172 .preinit_array
140 .note.stapsdt
138 .stapsdt.base
117 .bmp
114 .mips
113 .compiler
110 .dynstr
96 .rld_map
76 .gnu.attributes
75 .noptrbss
73 .context
71 .note.go.buildid
49 .rel.dyn
45 .gnu_debuglink
38 .gnu.prelink_undo
36 .debug_pubtypes
33 .gnu_extab
30 .stab.indexstr
30 .stab.index
29 .note.GNU-stack
29 .engine
20 .xt.prop
19 .xtensa.info
19 .xt.lit
19 .debug_gdb_scripts
19 .bep
18 .rel.gnu.linkonce.this_module
18 .gnu.warning.llseek
17 .interp
17 .gnu.linkonce.this_module
16 .rodata.str1.1
15 .gnu.conflict
14 .rel.debug_aranges
14 .rel.data
13 .rel__ex_table
13 .rel.debug_pubnames
13 .redata
13 .jgd
12 __ex_table
12 .rodata.str1.4
12 .rel.eh_frame
12 .dynbss
11 __versions
11 .rel.rodata
11 .modinfo
10 __mcount_loc
10 .rel__mcount_loc
10 .rel.debug_line
10 .data1
8 __ksymtab
8 .plt.got
8 .exception_ranges
8 .ex_shared
8 .debug_macinfo
8 .data.rel.local
7 COFF
7 .mdebug
6 .rodata1
6 .rel.text
6 .rel.fixup
6 .rel.debug_info
6 .MIPS.stubs
5 __param
5 PROGRAM
5 IBC_2.0
5 ABI
5 .xzrodata
5 .rel__param
5 .rel.debug_loc
5 .rel.debug_frame
4 .note.android.ident
4 .got2
4 .gnu.version_r
4 .cpp_finidata
4 .arm_vfe_header
3 Input file:
3 .upx.1
3 .smp_locks
3 .rel.smp_locks
3 .rdata
3 .ident
2 text_env
2 ta
2 odata
2 elink
2 __verbose
2 __ksymtab_strings
2 ___ksymtab_gpl+fb_mode_option
2 ___ksymtab_gpl+fb_destroy_modelist
2 ___ksymtab+vesa_modes
2 ___ksymtab+fb_videomode_to_var
2 ___ksymtab+fb_var_to_videomode
2 ___ksymtab+fb_mode_is_equal
2 ___ksymtab+fb_match_mode
2 ___ksymtab+fb_find_nearest_mode
2 ___ksymtab+fb_find_mode_cvt
2 ___ksymtab+fb_find_mode
2 ___ksymtab+fb_find_best_mode
2 ___ksymtab+fb_find_best_display
2 Import
2 C_2.0
2 .vmp
2 .tptext
2 .tm_clone_table
2 .rodata.cst4
2 .rela.plt
2 .rel__verbose
2 .rel___ksymtab_gpl+fb_mode_option
2 .rel___ksymtab_gpl+fb_destroy_modelist
2 .rel___ksymtab+vesa_modes
2 .rel___ksymtab+fb_videomode_to_var
2 .rel___ksymtab+fb_var_to_videomode
2 .rel___ksymtab+fb_mode_is_equal
2 .rel___ksymtab+fb_match_mode
2 .rel___ksymtab+fb_find_nearest_mode
2 .rel___ksymtab+fb_find_mode_cvt
2 .rel___ksymtab+fb_find_mode
2 .rel___ksymtab+fb_find_best_mode
2 .rel___ksymtab+fb_find_best_display
2 .rel.debug_pubtypes
2 .null
2 .msym
2 .fixup
2 .conststring
2 .constdata
2 .compact_rel
2 .comment.SUSE.OPTs
2 .PPC.EMB.apuinfo