窥探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
执行之后可以看到我的预编译语句结构,但是同样也没有发现追加进去的参数写到哪里!
汗!看来我的技术还没有修炼到家,闭关去...
全文完
 
                    
                     
                    
                 
                    
                 
 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号