5

I'm conjecturing SQL Server on Linux is checking /proc/self/status for TracerPID and then dying if it's not 0. I want to test that. Playing around, here is the 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)                          = ?

The ltrace is even more damning, thankfully they're using strstr which makes it seem really likely my theory is correct.

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 ...>

The last line on the file they check (with strstr) before they abort() is the line with TracerPid:, however with my /proc/self/status there are many lines after it.

In order of preference, I'd like /proc/self/status to report

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

for this process. If that can't be achieved, I'd like it to report 0 for all processes.

Is it possible to create a wrapper that changes the value of TracerPID for /proc/self/status and then exec the argument given to it resulting in it not having access to TracerPID?

0

2 Answers 2

3

The only way I found to do this by patching the kernel. Though I think it may also be possible to hack this of thing with LD_PRELOAD, I'll check into later.

0
2

Actually the kernel patch may be a more interesting solution if you want to debug other programs protected in the same way. For instance, gdb uses the same trick to detect if is being debugged.

However, based on your question, I investigated how to modify the mssql server behaviour when TracerPID is showing a PID different than 0 ; and I believe I found out a cleaner solution.

I used Hopper for disassembling/decompiling the MS SQL server binary file sqlservr and found the offending subroutine that checks TracerPID for preventing debugging.

In Hopper output, decompiled, the offending function is:

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;
}

In (heavily edited) human interpretation, the C pseudo-code of the function is:

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;
}

As it can be seen, if strstr finds the string "TracerPid:", temp/rax will be different than 0(NULL).

The strtol will then be invoked to convert the remaining of the string to a (long) integer. rbx with then been loaded with the value returned by strtol (which actually in the disassembly listing, is in rax).

So there are two more solutions for disabling the tracing detection, besides patching the kernel as you mention:

  • The cleaner solution: You write a library to be loaded with LD_PRELOAD when invoking sqlservr.

What I advise as the simplest solution is intercepting strstr and strtol, in which you write code in strstr that when it founds "TracerPid:", it will activate a flag that makes the next strtol invocation return 0.

(I already double checked the binary, and indeed, strstr and strtol are dynamically loaded)

Another option is intercepting fopen, however the code might be a bit more complicated.

  • the sqlservr binary is patched, you replace rax = rbx for rax = 0, as rbx holds the strtol/string to long integer conversion of the value after "TracerPid:".

The disadvantage of this solution is that each new version will have to be patched again.

Actually in the assembly itself, the rbx register loading comes right after calling strtol. The binary can be patched from mov rbx, rax to xor rbx,rbx or mov rbx,0, which one is shorter.

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 

Obviously I do advise using the LD_PRELOAD solution instead of either trying to patch the kernel or the binary itself.

It is a much cleaner solution, and it is not dependent on having to be done again each time you got a MSSQL or a kernel upgrade.

Note: I downloaded mssql-server_14.0.3008.27-1_amd64.deb and decompressed it on a Mac.

As for the source code for the LD_PRELOAD library, the general idea is roughly:

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
}

Commenting about fopen pointing only to "/proc/self/": It is not a mistake.

Yes, I find it odd the fopen being done to "/proc/self/" only. Most probably, the couple of integer variables after it are just there for filling a space, that will be used completing the rest of the string at runtime, and it is a cheap trick for deceiving anyone who is trying to look at the binary.

0

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .