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.