Como ocultar o TracerPID de um processo?

5

Estou supondo SQL Server no Linux está verificando /proc/self/status para TracerPID e depois morrendo se não for 0 . Eu quero testar isso. Brincando, aqui está o strace,

... lots of stuff
openat(AT_FDCWD, "/proc/self/status", O_RDONLY) = 5
fstat(5, {st_mode=S_IFREG|0444, st_size=0, ...}) = 0
read(5, "Name:\tsqlservr\nUmask:\t0022\nState"..., 1024) = 1024
close(5)                                = 0
rt_sigprocmask(SIG_UNBLOCK, [ABRT], NULL, 8) = 0
rt_sigprocmask(SIG_BLOCK, ~[RTMIN RT_1], [], 8) = 0
getpid()                                = 28046
gettid()                                = 28046
tgkill(28046, 28046, SIGABRT)           = 0
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
--- SIGABRT {si_signo=SIGABRT, si_code=SI_TKILL, si_pid=28046, si_uid=999} ---
gettid()                                = 28046
write(2, "Dump collecting thread [28046] h"..., 59Dump collecting thread [28046] hit exception [6]. Exiting.
) = 59
exit_group(-1)                          = ?

O ltrace é ainda mais contundente, mas felizmente eles estão usando strstr , o que faz com que pareça realmente provável que minha teoria esteja correta.

strstr("PPid:\t28515\n", "TracerPid:")                                                                  = nil
__getdelim(0x7ffc0b7d2330, 0x7ffc0b7d2328, 10, 0x7f12f5811980)                                          = 17
strstr("TracerPid:\t28515\n", "TracerPid:")                                                             = "TracerPid:\t28515\n"
strtol(0x7f12f581840b, 0x7ffc0b7d2320, 10, 0)                                                           = 0x6f63
free(0x7f12f5818400)                                                                                    = <void>
fclose(0x7f12f5811980)                                                                                  = 0
abort( <no return ...>
--- SIGABRT (Aborted) ---
syscall(186, 6, 0, 0)                                                                                   = 0x6f64
fprintf(0x7f12f6ec4640, "Dump collecting thread [%d] hit "..., 28516, 6Dump collecting thread [28516] hit exception [6]. Exiting.
)                                = 59
fflush(0x7f12f6ec4640)                                                                                  = 0
exit(-1 <unfinished ...>

A última linha no arquivo que eles verificam (com strstr ) antes de abort() é a linha com TracerPid: , no entanto, com meu /proc/self/status , há muitas linhas depois dela.

Em ordem de preferência, gostaria que /proc/self/status denunciasse

...stuff...
TracerPid:  0
...stuff...

para este processo. Se isso não puder ser alcançado, gostaria de informar 0 para todos os processos.

É possível criar um wrapper que altere o valor de TracerPID para /proc/self/status e, em seguida, exec do argumento dado a ele, resultando em não ter acesso a TracerPID ?

    
por Evan Carroll 29.12.2017 / 19:48

2 respostas

2

Na verdade, o patch do kernel pode ser uma solução mais interessante se você quiser depurar outros programas protegidos da mesma maneira. Por exemplo, gdb usa o mesmo truque para detectar se está sendo depurado.

No entanto, com base na sua pergunta, eu investiguei como modificar o comportamento do servidor mssql quando TracerPID está mostrando um PID diferente de 0; e acredito que descobri uma solução mais limpa.

Eu usei o Hopper para desmontar / descompilar o arquivo binário do servidor MS SQL sqlservr e localizei a sub-rotina problemática que verifica o TracerPID para evitar a depuração.

Na saída do Hopper, descompilada, a função problemática é:

int sub_2d6d0() {
    r14 = fopen(0xa9b4e, 0xb6444);
    rbx = 0x0;
    if (r14 == 0x0) goto loc_2d791;

loc_2d702:
    var_30 = 0x0;
    var_38 = 0x0;
    r15 = &var_30;
    r12 = &var_38;
    goto loc_2d730;

loc_2d730:
    rbx = 0x0;
    if (__getdelim(r15, r12, 0xa, r14) < 0x0) goto loc_2d77b;

loc_2d74a:
    rax = strstr(var_30, "TracerPid:");
    if (rax == 0x0) goto loc_2d730;

loc_2d75b:
    var_40 = 0x0;
    rbx = strtol(rax + 0xb, &var_40, 0xa);
    goto loc_2d77b;

loc_2d77b:
    rdi = var_30;
    if (rdi != 0x0) {
            free(rdi);
    }
    fclose(r14);
    goto loc_2d791;

loc_2d791:
    rax = rbx;
    return rax;
}

Na interpretação humana (muito editada), o pseudo-código C da função é:

int  IsMonitorProcess() {                          ; sub_2d6d0
    FILE * f = fopen("/proc/self/", "r" );
    int pid = 0;                                   ; rbx
    char *s = NULL;

    if (f != NULL ) 
    {             
        while (__getdelim(s, 0, 0xa, f) >= 0x0) 
        {
            char *temp;

            temp = strstr(s, "TracerPid:");
            pid = 0;
            if (temp != NULL)
                pid = strtol(temp + 0xb, NULL, 10);
        }

        if (s != NULL) {
               free(s);
        }

        fclose(f);
    }

    return pid;
}

Como pode ser visto, se strstr encontrar a string "TracerPid:", temp / rax será diferente de 0 (NULL).

O strtol será então invocado para converter o restante da string em um inteiro (longo). O rbx foi então carregado com o valor retornado por strtol (que, na lista de desmontagem, está em rax ).

Portanto, há mais duas soluções para desabilitar a detecção de rastreamento, além de corrigir o kernel como você mencionou:

  • A solução de limpeza: você escreve uma biblioteca para ser carregada com LD_PRELOAD ao invocar sqlservr .

O que eu aconselho como a solução mais simples é interceptar strstr e strtol , no qual você escreve código em strstr que quando ele encontrar "TracerPid:", ativará um sinalizador que fará o próximo strtol retorno de invocação 0.

(Eu já verifiquei o binário e, de fato, strstr e strtol são carregados dinamicamente)

Outra opção é interceptar fopen , mas o código pode ser um pouco mais complicado.

  • o binário sqlservr está corrigido, você substitui rax = rbx para rax = 0 , pois rbx contém a conversão strtol / string para número inteiro longo após "TracerPid:".

A desvantagem desta solução é que cada nova versão terá que ser corrigida novamente.

Na verdade, na própria montagem, o carregamento do registrador rbx vem logo após chamar strtol . O binário pode ser corrigido de mov rbx, rax para xor rbx,rbx ou mov rbx,0 , qual deles é menor.

000000000002d75b         mov        qword [rbp+var_40], 0x0
000000000002d763         add        rax, 0xb
000000000002d767         lea        rsi, qword [rbp+var_40]                     ; argument "__endptr" for method j_strtol
000000000002d76b         mov        edx, 0xa                                    ; argument "__base" for method j_strtol
000000000002d770         mov        rdi, rax                                    ; argument "__nptr" for method j_strtol
000000000002d773         call       j_strtol                                    ; strtol
000000000002d778         mov        rbx, rax   <----------- xor rbx,rbx

                     loc_2d77b:
000000000002d77b         mov        rdi, qword [rbp+var_30]                     ; CODE XREF=sub_2d6d0+120
000000000002d77f         test       rdi, rdi
000000000002d782         je         loc_2d789

000000000002d784         call       j_free                                      ; free

                     loc_2d789:
000000000002d789         mov        rdi, r14                                    ; argument "__stream" for method j_fclose, CODE XREF=sub_2d6d0+178
000000000002d78c         call       j_fclose 

Obviamente, aconselho usar a solução LD_PRELOAD em vez de tentar corrigir o kernel ou o próprio binário.

É uma solução muito mais limpa, e não depende de ter que ser feita novamente toda vez que você tiver um MSSQL ou uma atualização do kernel.

Nota: eu baixei o mssql-server_14.0.3008.27-1_amd64.deb e o descomprimi em um Mac.

Quanto ao código-fonte da biblioteca LD_PRELOAD, a ideia geral é aproximadamente:

int flag = 0;

char * strstr (const char *s1, const char *s2)
{
    if(!strcmp(s2, "TracerPid:"))
    {
        flag = 1;
    }
    .... rest of usual code
}

long strtol(const char *nptr, char **endptr, register int base)
{
    if(flag)
    {
        flag = 0;
        return 0;
    }
    .... rest of usual code
}

Comentando sobre fopen apontando apenas para "/proc/self/" : não é um erro.

Sim, acho estranho o fopen ser feito apenas para "/proc/self/" . Muito provavelmente, o par de variáveis inteiras depois que elas estão lá apenas para preencher um espaço, que será usado para completar o resto da string em tempo de execução, e é um truque barato para enganar qualquer um que esteja tentando ver o binário. / p>     

por 31.01.2018 / 12:33
1

A única maneira que eu encontrei para fazer isso por remendando o kernel . Embora eu ache que também seja possível hackear essa coisa com LD_PRELOAD , eu verificarei mais tarde.

    
por 31.01.2018 / 00:52