窥探SQL预编译内幕

Author : kj021320
TEAM : I.S.T.O
Author_blog: http://blog.csdn.net/kj021320

前言套话:

本来文章打算昨天就写出来,环境没搭好… 迟来的祝福~Mickey 生日快乐!

首先感谢PT007竭力帮我搭环境,可惜最后还是没能用上,还有ISTO团队的幕后成员,AMXSA,SUMMER等…还有我几位好朋友axis,suddy,larry 春节提前快乐!哈!

现在部分大企业 都采用DOTNET或者J2EE,迟点应该都是RubyOnRails,之后的技术越来越成熟,在SQL操作无疑都使用绑定变量,有些人叫预编译,也有些人叫同构SQL,参数追加…

像dotnet j2ee 上这样的使用几乎成了习惯,规范!

早期的ADODB中也一样有参数绑定的方式,只是少人使用Adodb.Command。

PHP后期也推出了MYSQL扩展了mysqli_prepare函数,而对Oracle OCI早早就有提供了

那么到底他们是如何进行操作,到底还会不会存在SQL注射的呢?

首先从数据库技术角度去看这个,几乎是百利而无一害的!(排除某些特殊情况)

OK先来思想教育一下~~

每个数据库中 都有自己的SQL引擎,为什么PostGreSQL MSSQL SYBASE可以执行多语句,而MYSQL ORACLE DB2就不行呢?就是因为他们引擎都有自己的实现方式,各有各的特点优势。

首先把SQL语句读入引擎,然后语法分析,有些是解析 有些是编译(预编译就是这里来)

解析我就不多说了!例如JET的,SQLite的…

那么编译呢?SQL引擎会把整个 语句的结构取出来,然后如果发现有参数的地方就会拿变量代替!整个结构编译为 该数据库能识别的执行指令,存储在SQL缓存池里面

例子

select * from ISTOMEMBER where membername=’kj021320’

这样的语句就好比 一般的C语言语句

if(ISTOMember.membername==”kj021320”)printf(“*”);

以上C代码非常不灵活,如果我换一个判断把kj021320改为amxsa,那你得从新编译生成EXE

现在用预编译,以变量的方式使用

select * from ISTOMEMBERN where membername=?

转为我们熟悉的C

if(ISTOMember.membername==name)println(“*”);

现在应该很好理解了吧?用预编译之后每次再使用同一个语句,只需要换一下条件就OK了,就是上述C语言代码里面的name变量。所以免去了语法分析,优化,编译,这些操作,使则数据库执行非常快…

那么到底我们提交SQL语句中,驱动是做了哪些手脚呢?

现在我来揭晓这个迷!本来打算DB2 PostGreSQL Informix那些数据库都拉上来分析的!后来因为机器环境问题,没下文了!……..

我对MYSQL进行分析,第一个拿它开刀是因为他的驱动包开源,而且MYSQL在它们当中比较小

现在我先稿写一个简单调用… 然后跟踪进去

import java.io.*;

import java.sql.*;

public class SQLtrack {

public static void main(String[] args) throws Exception{

Class.forName("com.mysql.jdbc.Driver");

Connection con=DriverManager.getConnection("jdbc:mysql://localhost:3306/mysql", "kj021320", "I.S.T.O");

PreparedStatement ps=null ;

ps=con.prepareStatement("select * from user where username=?");

ps.setString(1, "hello kj"); //加入断点 跟踪进去

ps.executeQuery();

ps.close();

con.close();

}

}

package com.mysql.jdbc;

public class PreparedStatement extends com.mysql.jdbc.Statement implements

java.sql.PreparedStatement {

public void setString(int parameterIndex, String x) throws SQLException {

// if the passed string is null, then set this column to null

if (x == null) {

setNull(parameterIndex, Types.CHAR);

} else {

checkClosed();

int stringLength = x.length();

if (this.connection.isNoBackslashEscapesSet()) {

// Scan for any nasty chars

boolean needsHexEscape = false;

for (int i = 0; i < stringLength; ++i) {

char c = x.charAt(i);

switch (c) {

case 0: /* Must be escaped for 'mysql' */

needsHexEscape = true;

break;

case '\n': /* Must be escaped for logs */

needsHexEscape = true;

break;

case '\r':

needsHexEscape = true;

break;

case '\\':

needsHexEscape = true;

break;

case '\'':

needsHexEscape = true;

break;

case '"': /* Better safe than sorry */

needsHexEscape = true;

break;

case '\032': /* This gives problems on Win32 */

needsHexEscape = true;

break;

}

if (needsHexEscape) {

break; // no need to scan more

}

}

if (!needsHexEscape) {

byte[] parameterAsBytes = null;

StringBuffer quotedString = new StringBuffer(x.length() + 2);

quotedString.append('\'');

quotedString.append(x);

quotedString.append('\'');

if (!this.isLoadDataQuery) {

parameterAsBytes = StringUtils.getBytes(quotedString.toString(),

this.charConverter, this.charEncoding,

this.connection.getServerCharacterEncoding(),

this.connection.parserKnowsUnicode());

} else {

// Send with platform character encoding

parameterAsBytes = quotedString.toString().getBytes();

}

setInternal(parameterIndex, parameterAsBytes);

} else {

byte[] parameterAsBytes = null;

if (!this.isLoadDataQuery) {

parameterAsBytes = StringUtils.getBytes(x,

this.charConverter, this.charEncoding,

this.connection.getServerCharacterEncoding(),

this.connection.parserKnowsUnicode());

} else {

// Send with platform character encoding

parameterAsBytes = x.getBytes();

}

setBytes(parameterIndex, parameterAsBytes);

}

return;

}

StringBuffer buf = new StringBuffer((int) (x.length() * 1.1));

buf.append('\'');

//

// Note: buf.append(char) is _faster_ than

// appending in blocks, because the block

// append requires a System.arraycopy()….

// go figure…

//

for (int i = 0; i < stringLength; ++i) {

char c = x.charAt(i);

switch (c) {

case 0: /* Must be escaped for 'mysql' */

buf.append('\\');

buf.append('0');

break;

case '\n': /* Must be escaped for logs */

buf.append('\\');

buf.append('n');

break;

case '\r':

buf.append('\\');

buf.append('r');

break;

case '\\':

buf.append('\\');

buf.append('\\');

break;

case '\'':

buf.append('\\');

buf.append('\'');

break;

case '"': /* Better safe than sorry */

if (this.usingAnsiMode) {

buf.append('\\');

}

buf.append('"');

break;

case '\032': /* This gives problems on Win32 */

buf.append('\\');

buf.append('Z');

break;

default:

buf.append(c);

}

}

buf.append('\'');

String parameterAsString = buf.toString();

byte[] parameterAsBytes = null;

if (!this.isLoadDataQuery) {

parameterAsBytes = StringUtils.getBytes(parameterAsString,

this.charConverter, this.charEncoding, this.connection.getServerCharacterEncoding(), this.connection.parserKnowsUnicode());

} else {

// Send with platform character encoding

parameterAsBytes = parameterAsString.getBytes();

}

setInternal(parameterIndex, parameterAsBytes);

}

}

}

以上 有颜色的代码块就是 进行替换的操作,很明显MYSQL没有SQL缓存池,每提交1条语句数据库服务器就得从新编译,然后执行!哈!所以MYSQL慢哈!…这个 预编译的类有点欺骗的感觉…总的来说!替换交给数据库!大家注意到在这个for语句前后 都有这个东西吗?

buf.append('\''); 就是如果你是字符串 他会帮你加入这个',

那么如果是PreparedStatement的 setInt方法呢?

这个不用说了吧!直接类型转换为数值!如果有注射那就给卡住了

OK~ 继续往下一个数据库的分析 MSSQL

再写了一个差不多的方法贴出来

import java.sql.*;

public class SQLtrack {

public static void main(String[] args) throws Exception{

Class.forName("com.microsoft.jdbc.sqlserver.SQLServerDriver");

Connection con=DriverManager.getConnection("jdbc:microsoft:sqlserver://127.0.0.1:1433;DatabaseName=master", "isto-team", "kj021320");

PreparedStatement ps=null ;

ps=con.prepareStatement("select * from sysobjects where name=?");

ps.setString(1, "aaa");

ps.executeQuery();

ps.close();

con.close();

}

}

这里MSSQL JDBC驱动是没有开源的,所以我就不跟踪代码了!换一个方式!我们跟踪数据库的SQL,MSSQL提供了profiler工具可以直接跟踪 这样省事了!哈!如果变量为字符串的,MSSQL-JDBC驱动还会像MYSQL那样负责把escape字符转换掉的。那到底MSSQL驱动会转换为什么样的预编译语句呢?看下图

我把语句复制出来了给看不到图片的朋友…

Exec sp_executesql N’select * from sysobjects where name=@P1’,N’@P1 nvarchar(4000)’,N’aaa’

好了!到了最后的ORACLE了!这个分析起来比较复杂

看下面代码

import java.sql.*;

public class SQLtrack {

public static void main(String[] args) throws Exception{

Class.forName("oracle.jdbc.driver.OracleDriver");

Connection con=DriverManager.getConnection("jdbc:oracle:thin:@127.0.0.1:1521:orcl","isto-team","kj021320");

PreparedStatement ps=con.prepareStatement("select * from all_tables where table_name=?");

ps.setString(1, "table_name");

ps.executeQuery();

ps.close();

con.close();

}

}

//再写一个测试类同样的方式 , 有颜色的地方就是需要跟踪进去的

/*

这里一个小插曲

ORACLE的JDBC驱动class12.jar 是编译好的class文件!一般看不到原代码的

需要用jad反编译,然后需要修改好些部分才可以从新编译运行

经过我一翻修改终于 能跑起来…待会我会提供整个驱动java文件下载,大家有兴趣可以分析一下人家是怎么实现SOCKET连接数据库提交SQL的

*/

//言归正转 现在来继续跟踪下面的函数调用

private PreparedStatement privatePrepareStatement(String s, String s1, int i, int j)throws SQLException{

if(s1 == null && s == null || s == "")DBError.throwSqlException(104);

checkPhyiscalStatus();

if(closed)DBError.throwSqlException(8);

Object obj = null;

if(logicalHandle && m_opc.isStatementCacheInitialized()){

obj = m_opc_oc.privatePrepareStatement(s, s1, i, j);

} else {

int k = 0;//OracleStatement.DEFAULT_RSET_TYPE;

if(i != -1 || j != -1)k = ResultSetUtil.getRsetTypeCode(i, j);

if(statementCache != null)

if(s1 != null)obj = (OraclePreparedStatement)statementCache.searchExplicitCache(s1);

else obj = (OraclePreparedStatement)statementCache.searchImplicitCache(s, 1, k);

if((statementCache == null || s1 == null) && obj == null)

if(i != -1 || j != -1)obj = new OraclePreparedStatement(this, s, default_batch, default_row_prefetch, i, j);

else obj = new OraclePreparedStatement(this, s, default_batch, default_row_prefetch);

}

return ((PreparedStatement) (obj));

}

//着色的地方是 需要跟踪进去的地方,继续

public OraclePreparedStatement(OracleConnection oracleconnection, String s, int i, int j, int k, int l)

throws SQLException

{

super(oracleconnection, i, j, k, l);

check_bind_types = true;

has_ref_cursors = false;

m_batchStyle = 0;

super.statementType = 1;

super.need_to_parse = true;

has_ref_cursors = false;

prepare_for_new_result(true);

super.sql_query = s;

super.m_originalSql = s;

super.clear_params = true;

m_binds = null;

m_scrollRsetTypeSolved = false;

premature_batch_count = 0;

super.binds_in = super.connection.db_access.createDBDataSet(oracleconnection, this, i, 1);

parseSqlKind();

if(oracleconnection.db_access.getVersionNumber() >= 8000)

{

min_binary_stream_size = 2000;

min_ascii_stream_size = 4000;

} else

{

min_binary_stream_size = 255;

min_ascii_stream_size = 2000;

}

}

//一大陀, 郁闷啊!下面还有好多调用!呢!我就不贴出来了!哈!

//不然会浪费收藏本文章人事的硬盘

//直接给出最终调用吧!

oracle.jdbc.driver.OracleSql

这个类 大家有兴趣可以分析一下~,最后他在内部把 ? 替换为单个单个的变量

上述语句 他替换为 ORACLE独有的变量绑定

替换前

select * from all_tables where table_name=?

替换后

select * from all_tables where table_name=:1

不过这里有个败北的就是不知道它怎么对这个 :1 的变量进行追加参数,以下是我跟踪ORACLE SQL缓存池的语句

select a.address address,s.hash_value hash_value,s.piece piece,s.sql_text sql_text,u.username

parsing_user_id,c.username parsing_schema_id from v$sqlarea a,v$sqltext_with_newlines

s,dba_users u,dba_users c where a.address=s.address and

a.hash_value=s.hash_value and a.parsing_user_id=u.user_id and

a.parsing_schema_id=c.user_id and exists (select 'x' from v$sqltext_with_newlines

x where x.address=a.address and x.hash_value=a.hash_value and upper(x.sql_text) like

'%TABLE_NAME%')order by 1,2,3

执行之后可以看到我的预编译语句结构,但是同样也没有发现追加进去的参数写到哪里!

汗!看来我的技术还没有修炼到家,闭关去…

哦对了~ ORACLE-JDBC驱动的源代码 可编译的 我会上传到

http://blog.csdn.net/I_S_T_O/ 下载资源区

全文完

相关日志

发表评论