我猜测Linux 上的 SQL Server 正在检查/proc/self/status
,TracerPID
如果没有则停止运行0
。我想测试一下。玩玩,这是 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) = ?
ltrace
更可恶的是,谢天谢地,他们正在使用这strstr
使得它看起来真的可能我的理论是正确的。
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 ...>
他们检查的文件的最后一行(用strstr
)在它们之前abort()
是用 的行TracerPid:
,但是用 my/proc/self/status
后面有很多行。
按照优先顺序,我想/proc/self/status
报告
...stuff...
TracerPid: 0
...stuff...
对于这个过程。如果无法实现这一点,我希望它报告0
所有流程。
是否可以创建一个包装器来更改TracerPID
for的值/proc/self/status
,然后exec
更改赋予它的参数,导致它无法访问TracerPID
?
答案1
我发现做到这一点的唯一方法是给内核打补丁。虽然我认为也可以用 来破解这个东西LD_PRELOAD
,但我稍后会检查。
答案2
实际上,如果您想调试以相同方式保护的其他程序,内核补丁可能是一个更有趣的解决方案。例如,gdb
使用相同的技巧来检测是否正在调试。
但是,根据您的问题,我研究了如何在TracerPID
显示 PID 不同于 0 时修改 mssql 服务器行为;我相信我找到了一个更干净的解决方案。
我使用 Hopper 反汇编/反编译 MS SQL 服务器二进制文件sqlservr
,并发现了有问题的子例程,该子例程检查 TracerPID 以防止调试。
在 Hopper 反编译输出中,有问题的函数是:
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;
}
在(经过大量编辑的)人类解释中,该函数的 C 伪代码是:
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;
}
可以看出,如果strstr
找到字符串“TracerPid:”,temp/拉克斯将不同于 0(NULL)。
然后strtol
调用将字符串的其余部分转换为(长)整数。RBX然后加载了返回的值strtol
(实际上在反汇编列表中,位于拉克斯)。
因此,除了您提到的修补内核之外,还有两种禁用跟踪检测的解决方案:
- 更干净的解决方案:您编写一个库,在调用时使用 LD_PRELOAD 加载
sqlservr
。
我建议最简单的解决方案是拦截strstr
and strtol
,在其中编写代码,strstr
当它找到“TracerPid:”时,它将激活一个标志,使下一次strtol
调用返回 0。
(我已经仔细检查了二进制文件,并且确实strstr
是strtol
动态加载的)
另一种选择是拦截fopen
,但代码可能会更复杂一些。
- 二进制文件
sqlservr
已修补,您将 替换rax = rbx
为rax = 0
, 作为RBX保存strtol
“TracerPid:”之后的值的/字符串到长整数的转换。
这种解决方案的缺点是每个新版本都必须重新打补丁。
实际上在大会本身中,RBX调用后立即进行寄存器加载strtol
。二进制文件可以从mov rbx, rax
到xor rbx,rbx
或进行修补mov rbx,0
,哪一个更短。
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
显然,我确实建议使用该LD_PRELOAD
解决方案,而不是尝试修补内核或二进制文件本身。
这是一个更加简洁的解决方案,并且不需要每次进行 MSSQL 或内核升级时都必须再次执行此操作。
注:我是mssql-server_14.0.3008.27-1_amd64.deb
在Mac上下载并解压的。
至于LD_PRELOAD库的源码,大致思路是:
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
}
fopen
关于仅指向 的评论"/proc/self/"
:这不是一个错误。
是的,我觉得这样fopen
做很奇怪"/proc/self/"
。最有可能的是,它后面的几个整数变量只是用来填充一个空格,它将在运行时用来完成字符串的其余部分,这是一个欺骗任何试图查看二进制文件的人的廉价技巧。