现在的位置: 首页 WinDbg >正文

用WinDbg监视mysql查询

代码审计时总是少不了sql监控:sqlserver有SQL Profiler来监控语句执行;mysql可以写到文件或表中,但缺点就是日志文件看起来太不直观,记录到表里面的数据又要配合其他工具去轮询。

懒得装第三方软件,干脆直接把windbg的控制台当成实时输出来用。测试基于mysql 5.5.24-log,其他版本会略有出入。


mysql所有查询都要经过mysqld导出的mysql_parse函数,这个函数第一个参数是THD类实例的指针。

THD类继承自Statement类,Statement类有一个CSET_STRING类型的query_string成员保存了查询语句,CSET_STRING类的第一个成员string是LEX_STRING类型,LEX_STRING类的第一个成员str就是查询语句的字符串指针,第二个成员length就是字符串长度。于是可以认为query_string相当于一个LEX_STRING(当然之后还有一些不相关的数据)。

在query_string后面还有几个成员,db字段保存了指向数据库名称的字符串指针,之后的字段为这个字符串的长度db_length,其实也是相当于一个LEX_STRING。

字段都是嵌入的类,所以这两个字符串指针必然保存在第一个参数指向的内存块之后的某个位置。在各种编译选项和版本不同的情况下,不同机器偏移不一定相同(例如mysql5.5中query_string后面紧跟着的就是db,mysql5.6中间又多了一个类成员rewritten_query占了不少的空间)。不过找起来也不困难,由于字符串指针包含在LEX_STRING类中,其后面必然跟随着字符串长度,只要找到保存长度的地址就行了。例如在mysql数据库执行select 1的结果为:

0:020> dd poi(@esp+4)
02b7cb18  0076e15c 008f4834 008f4838 0076e14c
02b7cb28  00000000 02b7e580 00000003 00000000
02b7cb38  00000001 00000000 00000000 02b7daa0
02b7cb48  02b23018 00000008 00a9d100 015dfff8
02b7cb58  00000005 00000000 00000000 00000000
02b7cb68  00000000 00000000 00000000 00000000
02b7cb78  00000000 00000000 0017ccf8 ffffffff
02b7cb88  00000000 00000000 00000000 00000000
0:020> da 02b23018 
02b23018  "select 1"
0:020> da 015dfff8
015dfff8  "mysql"

找到地址后算出偏移,下个条件断点就行了。例如这里query_string的偏移是30h,db的偏移是3ch,db_length的偏移是40h:

bp mysqld!mysql_parse ".if(poi(poi(@esp+4)+40h)!=0){.printf \"%ma:\",poi(poi(@esp+4)+3ch);};.printf \"%ma\\n\",poi(poi(@esp+4)+30h);g;"

db字段不一定有数据,为防止出错先进行判断再打印。其实一般也不需要这个字段,绝大多数cms都只用一个数据库。

为优雅起见还可以使用伪寄存器保存THD的指针:

bp mysqld!mysql_parse "r $t0=poi(@esp+4);.if(poi($t0+40h)!=0){.printf \"%ma:\",poi($t0+3ch);};.printf \"%ma\\n\",poi($t0+30h);g;"

但会严重拖慢执行速度,只能作罢。


其实最好的办法是改源码自己编译,想干什么就能干什么。只可惜A/M/P这类的跨平台开源项目在windows上编译不是一般的费劲,还是从官网下载对应的符号,结合源码找到关键点,最后用调试器更方便点。

最后附一张结果图,前面几条是console执行的语句,后面的是刷新了一次phpmyadmin主页:


(图中的断点是mysql_parse的下层函数,在不命中查询缓存时就会调用这个函数。懒得重新截图了,其断点效果基本是一样的。)