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.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.