Descompilando binários gerados pelo SHC
Você conhece o SHC (https://github.com/neurobin/shc) ? Até a alguns dias
atrás eu nunca tinha ouvido falar disso, mas ai uma pessoa, em um grupo de
shell-script no telegram, apareceu pedindo ajuda para descompilar um binário
que foi gerado usando essa tool, e eu, como bom desempregado samaritano que
sou, fui ver qual era a desse SHC.
Como o SHC funciona ?
O programa funciona de maneira bem simples, ele encripta o script usando RC4, e gera um arquivo .c com o conteúdo encriptado hardcoded e algumas técnicas anti-debugging. Esse arquivo depois é compilado, o que gera o binário, que ao ser executado desencripta o script, que por fim, é passado via linha de comando para o bash (bash -c decoded-script).
A tool tem varias opções, mas as que realmente importam são apenas duas,
-H (hardening) e -U (untraceble), que são responsáveis por ativar as proteções
anti-debugging. Apesar de algumas operações serem redundates, optei por criar o
binário, que será usado como exemplo, usando ambas: shc -UHf script.sh
.
Mecanismos anti-debugging
Inicialmente, o programa usa a função prctl duas vezes, a primeira chamando
prctl(PR_SET_DUMPABLE, 0)
, que tem como um dos efeitos impedir que o
processo seja debugado (PTRACE_ATTACH). E a segunda chamando
prctl(PR_SET_PTRACER, -1)
, que ignora possíveis restrições em
/proc/sys/kernel/yama/ptrace_scope, e permite que uma child desse processo
possa controlá-lo.
Continuando a execução, é checado se o ppid (parent pid), isto é, o pid que
criou o pid em que o binário está rodando, está em uma whitelist de programas
permitidos, que contém bash, sh, sudo, entre outros. Isto é feito obtendo o
ppid através da função getppid()
, e depois lendo o arquivo
/proc/[ppid]/cmdline.
Depois disso, o programa cria uma child e através dela verifica se pode controlar o pid principal (PTRACE_ATTACH).
Passando essa etapa, ele começa o processo de desencriptação, e após a conclusão dessa fase vem as duas últimas técnicas anti-debugging.
Um arquivo .c é criado em /tmp/shc_x.c, esse arquivo é compilado, gerando um DSO em /tmp/shc_x.so. A função dele é hookar a função main e sobrescrever os parâmetros que foram passados via linha de comando, evitando assim que eles sejam acessíveis via /proc/[pid]/cmdline. Esse DSO é injetado usando LD_PRELOAD.
E para finalizar, PTRACE_TRACEME é usado para checar se o processo está sendo debugado ou não.
Descompilando
Sabendo o funcionamento do programa, uma maneira fácil de se obter o script original seria interceptar a system call que executa programas (execve) e printar os argumentos, já que o script inteiro é passado via linha de comando. Isso pode ser feito usando o gdb, radare2, ou algum outro debugger, mas eu vi que era a oportunidade perfeita para brincar um pouco com a libspyderhook, que foi feita justamente para manipular system calls. Mas primeiro é necessário burlar a whitelist de programas.
Fakeproc
Criar um fakeproc é bastante simples, pelo menos se /proc/[pid]/cmdline estiver sendo usado para obter o nome do processo, que é o caso, você só precisa alterar o primeiro argumento para o nome desejado:
int main(int argc, char **argv, char **envp){
/* re-executa o programa se o primeiro argumento
* for diferente de /bin/bash */
if(strcmp(argv[0], "/bin/bash")){
char *exe = argv[0];
argv[0] = "/bin/bash";
execve(exe, argv, envp);
/* encerra o programa, se execve falhar */
return 1;
}
// ...
E... Voilà! Problema resolvido.
Manipulando as system calls
Agora vem a parte que realmente importa, depois de toda essa enrolação...
A libspyderhook funciona de uma maneira bem simples, ela intercepta as system calls em userland usando ptrace, e passa o controle para uma callback em duas situações distintas, a primeira antes da system call ser de fato executada, e a segunda quando a system call já foi executada. Através do status code retornado pela callback a lib decide o que fazer, continuar a execução, não executar a syscall, matar o pid, etc. Além disso, ela conta com algumas funções para facilitar a manipulação dos parâmetros, sendo possível alterar completamente a syscall que será executada, ou o seu retorno.
Só existem duas system calls que realmente serão um problema na hora de debuggar o programa, prctl e ptrace. A prctl não pode ser executada, e a ptrace precisa ter o resultado alterado para 0 (Success). Sendo assim duas callbacks serão necessárias.
Dando bypass no prctl:
int syscall_enter(pidinfo_t *info, unsigned long nr, void *data){
/* faz a lib 'pular' a syscall */
if(nr == SYS_prctl){
return SH_SKIP_SYSCALL;
}
/* printa os parametros da syscall execve */
if(nr == SYS_execve){
print_execve(info->pid);
}
return SH_CONTINUE;
}
Alterando o resultado da syscall ptrace:
int syscall_result(pidinfo_t *info, unsigned long nr, void *data){
if(nr == SYS_ptrace){
/* altera o resultado da syscall ptrace
* que no SHC é usada para detectar se
* o processo está sendo debugado */
sh_setreg(info->pid, SH_SYSCALL_RESULT, 0);
}
return SH_CONTINUE;
}
A configuração da lib fica da seguinte forma:
#include <spyderhook.h>
// ...
int main(int argc,char **argv, char **envp){
spyderhook_t *sh;
// ...
sh = sh_init();
if(sh == NULL){
die("sh_init()");
return 1;
}
sh_setopt(sh, SHOPT_FILENAME, argv[1]);
sh_setopt(sh, SHOPT_ARGV, argv+1);
sh_setopt(sh, SHOPT_ENVP, envp);
sh_setopt(sh, SHOPT_ENTER_CALLBACK, syscall_enter);
sh_setopt(sh, SHOPT_RESULT_CALLBACK, syscall_result);
sh_setopt(sh, SHOPT_FOLLOW_ALL, 1);
if((err = sh_mainloop(sh)) != SH_SUCCESS){
printf("error => %s\n", sh_strerror(err));
}
sh_free(sh);
// ...
Bem autoexplicativo, apesar da lib ainda não ter documentação, com esses exemplos, e lendo os comentários no header, fica fácil usar.
Por último, mas não menos importante, na função print_execve, eu uso a libignotum, para facilitar a leitura da memória:
#include <ignotum.h>
// ...
void print_execve(pid_t pid){
long exe, args, addr;
size_t len;
int i = 0;
char *string;
/* pega os endereços de memória */
exe = sh_getreg(pid, SH_FIRST_ARG);
/* read_until_zero usa a libignotum */
string = read_until_zero(pid, exe, &len);
printf("\n[*] cmd = %s\n", string);
free(string);
if(wantcontinue()){
return;
}
args = sh_getreg(pid, SH_SECOND_ARG);
while(1){
addr = 0;
/* addr = *remote_argv */
ignotum_mem_read(pid, &addr, sizeof(long), args);
if(addr == 0)
break;
printf("ARG[%d]:\n", i++);
/* printf("%s\n", addr); */
string = read_until_zero(pid, addr, &len);
hexdump(string, len);
free(string);
/* remote_argv++ */
args += sizeof(long);
}
}
O código completo está nesse link.
Compilando o descompilador
Esses são os comandos para compilar o código:
git clone https://gist.github.com/hc0d3r/318b0aa0ae4697372688b624cecbd610 unshc
cd unshc
git clone https://github.com/hc0d3r/spyderhook
git clone https://github.com/hc0d3r/ignotum
make -C spyderhook
make -C ignotum
gcc unshc.c -o unshc -Iignotum/src -Ispyderhook/src \
ignotum/lib/libignotum.a spyderhook/lib/libspyderhook.a
Testando
$ shc -UHf script.sh
$ ./script.sh.x
./script.sh.x: Operation not permitted
Morto
$ sudo ./script.sh.x
nothing to view here
$ ./unshc ./script.sh.x
$ ./unshc ./script.sh.x
[*] cmd = ./script.sh.x
>> printar os parametros (s/n) ? s
ARG[0]:
00000000 2e 2f 73 63 72 69 70 74 2e 73 68 2e 78 00 |./script.sh.x.|
0000000e
[*] cmd = /bin/bash
>> printar os parametros (s/n) ? s
ARG[0]:
00000000 2e 2f 73 63 72 69 70 74 2e 73 68 2e 78 00 |./script.sh.x.|
0000000e
ARG[1]:
00000000 2d 63 00 |-c.|
00000003
ARG[2]:
00000000 65 78 65 63 20 27 2e 2f 73 63 72 69 70 74 2e 73 |exec './script.s|
00000010 68 2e 78 27 20 22 24 40 22 00 |h.x' "$@".|
0000001a
ARG[3]:
00000000 2e 2f 73 63 72 69 70 74 2e 73 68 2e 78 00 |./script.sh.x.|
0000000e
[*] cmd = /home/matheus/Desktop/pwn-shc/script.sh.x
>> printar os parametros (s/n) ? s
ARG[0]:
00000000 2e 2f 73 63 72 69 70 74 2e 73 68 2e 78 00 |./script.sh.x.|
0000000e
#### muitos programas depois ...
[*] cmd = /bin/sh
>> printar os parametros (s/n) ? s
ARG[0]:
00000000 73 68 00 |sh.|
00000003
ARG[1]:
00000000 2d 63 00 |-c.|
00000003
ARG[2]:
00000000 23 21 2f 62 69 6e 2f 62 61 73 68 0a 23 20 6c 61 |#!/bin/bash.# la|
00000010 20 63 75 63 61 72 61 63 68 61 0a 0a 65 63 68 6f | cucaracha..echo|
00000020 20 22 6e 6f 74 68 69 6e 67 20 74 6f 20 76 69 65 | "nothing to vie|
00000030 77 20 68 65 72 65 22 0a 00 |w here"..|
00000039
nothing to view here
Could not start seccomp:: Function not implemented
$ cat script.sh
#!/bin/bash
# la cucaracha
echo "nothing to view here"
Conclusão
O código para descompilação não é uma sandbox, e mesmo se fosse, tenha muito cuidado com os binários que você roda, ou pode acabar conhecendo o /dev/null.
Se for usar o SHC, não deixe nada de comprometedor nos seus scripts, e.g, xingamentos, segredos escusos, que você usa java...