窥探SQL预编译内幕


Author : kj021320

TEAM : I.S.T.O

Author_blog: http://blog.csdn.net/kj021320

前言套话:

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

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

现在部分大企业 都采用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驱动是没有开源的,所以我就不跟踪代码了!换一个方式!我们跟踪数据库的SQLMSSQL提供了profiler工具可以直接跟踪 这样省事了!哈!如果变量为字符串的,MSSQL-JDBC驱动还会像MYSQL那样负责把escape字符转换掉的。那到底MSSQL驱动会转换为什么样的预编译语句呢?看下图  


窥探SQL预编译内幕_第1张图片

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

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();

    }

}

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

/*

这里一个小插曲

ORACLEJDBC驱动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/    下载资源区

全文完

你可能感兴趣的:(sql)