Showing posts with label SQL. Show all posts
Showing posts with label SQL. Show all posts

Apr 15, 2009

毕业设计(新增加密解密工具)




1:配置文件里新增了加密的相关配置信息。主要就是两个,一个是加密的关键字符串key,另外一个是一个0~1之间的小数(最好只到小数点后两位)。它们共同构成了加密的算法。算法是我从网上搜刮来的,我改了一下。原先我将算法直接用来加密memo里的所有内容,结果发现会发生一些小概率的加密未完成便中止的现象。后来我把它改成逐行读取逐行加密就没有问题了。这个BUG真的很奇怪,我已经发给老大了,希望他有时间帮我看一下。我数学不行啊。哎~~~~~



2:该图是输入明文后的效果。这个加密工具可以独立使用也可以嵌入到任何一个工具里。只要包含了加密工具的单元,调用它的一个hasIniOriginalText或者hasIniDecodedText将其设置为true,就可以讲你要加密的内容从你的工具里导入到加密工具的明文或密文输入栏了。


3:上图为加密后的脚本内容,看不懂吧,呵呵。这个密文可以放到明文显示栏里再加密,每次加密可以更换不同的key和percent,只要记住顺序,一次次的用正确的key和percent解密就行了,很方便很强大吧。。。真是要感谢贴了这个加密算法的蝈蝈。之前上网搜了,论坛问了,有人让我用MD5加密,这个加密虽然破解还是有一定难度,可是解密也很麻烦的。当然,我对MD5甚至对密码学根本是门外汉,上述完全是凭直觉哈。

下边贴出我稍微改动后的加密算法代码:

var
frmCryptograph: TfrmCryptograph;

key: string;
percent1: Double;

implementation

{$R *.dfm}

function TfrmCryptograph.DeCode(aCryptograph, aKey: string): string;
var
i,keylen,codelen:integer;
begin
keylen :=Length(akey);
codelen:=Length(aCryptograph);
SetLength(Result, Length(aCryptograph));
for i:=1 to codelen do
begin
Result[i]:=Chr(Ord(aCryptograph[i])-Ord(aKey[(i mod KeyLen)+1]));
end;
end;


function TfrmCryptograph.EnCode(aCryptograph, aKey: string): string;
var
i,keylen,codelen:integer;
begin
keylen:=Length(akey);
codelen:=Length(aCryptograph);
SetLength(Result, Length(aCryptograph));
for i:=1 to codelen do
begin
Result[i]:=Chr(Ord(aCryptograph[i])+Ord(aKey[(i mod KeyLen)+1]));
end;
end;

function TfrmCryptograph.GetKey(aKey: string; aPercent: Double): string;
var
i:integer;
begin
SetLength(Result,Length(aKey));
for i:=1 to Length(aKey) do
begin
Result[i]:=Chr(Round(Ord(aKey[i])*aPercent));
end;
end;

procedure TfrmCryptograph.btnEncodeClick(Sender: TObject);
var
i: Integer;
str: string;
begin
mmoOutput.Clear;
pgcCryptograph.ActivePageIndex := 1;
for i := 0 to mmoInput.Lines.Count do
begin
str := EnCode(mmoInput.Lines.Strings[i],GetKey(Key,Percent1));
mmoOutput.Lines.Append(str);
end;
end;

procedure TfrmCryptograph.btnDecodeClick(Sender: TObject);
var
i: Integer;
str: string;
begin
mmoInput.Clear;
pgcCryptograph.ActivePageIndex := 0;
for i := 0 to mmoOutput.Lines.Count do
begin
str := DeCode(mmoOutput.Lines.Strings[i],GetKey(Key,Percent1)) ;
mmoInput.Lines.Append(str)
end;
end;

procedure TfrmCryptograph.FormCreate(Sender: TObject);
begin
initEnDeCodeForm(Sender);
pgcCryptograph.ActivePageIndex := 0;
end;

procedure TfrmCryptograph.initEnDeCodeForm(Sender: TObject);
var
iniFileName: string;
begin
{如果没有初始化的明文输入,则清空}
if not withIniOriginalText then
mmoInput.Clear;
if not withIniDecodedText then
mmoOutput.Clear;
iniFileName := 'config\config.ini';
with TInifile.Create(iniFileName) do
begin
percent1 := ReadFloat('CRYPTOGRAPHY','PERCENT',0);
key := ReadString('CRYPTOGRAPHY','KEY','');
Free;
end;
end;

procedure TfrmCryptograph.FormShow(Sender: TObject);
begin
initEnDeCodeForm(Sender);
end;

procedure TfrmCryptograph.btnImportEncodeClick(Sender: TObject);
begin
dlgOpenCryptograph.Execute;
mmoInput.Clear;
try
mmoInput.Lines.LoadFromFile(dlgOpenCryptograph.FileName);
except
Exit;
// MessageBox(Handle, '读取文件出错,请重试', '提示', MB_OK);
end;
end;

procedure TfrmCryptograph.btnExportEncodeClick(Sender: TObject);
begin
dlgSaveCryptograph.Execute;
try
mmoInput.Lines.SaveToFile(dlgSaveCryptograph.FileName + '.sql');
except
Exit;
// MessageBox(Handle, '保存文件出错,请重试', '提示', MB_OK);
end;
end;

procedure TfrmCryptograph.btnImportDecodeClick(Sender: TObject);
begin
dlgOpenCryptograph.Execute;
mmoInput.Clear;
try
mmoInput.Lines.LoadFromFile(dlgOpenCryptograph.FileName);
except
Exit;
// MessageBox(Handle, '读取文件出错,请重试', '提示', MB_OK);
end;
end;

procedure TfrmCryptograph.btnExportDecodeClick(Sender: TObject);
begin
dlgSaveCryptograph.Execute;
try
mmoOutput.Lines.SaveToFile(dlgSaveCryptograph.FileName + '.sql');
except
Exit;
// MessageBox(Handle, '保存文件出错,请重试', '提示', MB_OK);
end;
end;

procedure TfrmCryptograph.FormClose(Sender: TObject;
var Action: TCloseAction);
begin
mmoInput.Clear;
mmoOutput.Clear;
withIniOriginalText := False;
withIniDecodedText := False;
end;

总结:这个小工具花了我一天多的时间。其中大部分用来找加密未完成却中断的原因了,结果还是没有找到,可见要成为IT中的牛人,数学是多么重要。虽然实际开发中未必用得了算法,未必要那么考虑效率,可是,对于一个程序员来说,追求完美是一种天性,应该坚持下去。

Apr 14, 2009

查看SQL SERVER 加密存储过程,函数,触发器,视图

原作出处:http://dev.csdn.net/develop/article/23/23218.shtm
作者:CSDN某大虾
读后感:目前难以理解透

create PROCEDURE sp_decrypt(@objectname varchar(50))
AS
begin
set nocount on
--CSDN:j9988 copyright:2004.07.15
--V3.2
--破解字节不受限制,适用于SQLSERVER2000存储过程,函数,视图,触发器
--修正上一版"视图触发器"不能正确解密错误
--发现有错,请E_MAIL:CSDNj9988@tom.com
begin tran
declare @objectname1 varchar(100),@orgvarbin varbinary(8000)
declare @sql1 nvarchar(4000),@sql2 varchar(8000),@sql3 nvarchar(4000),@sql4 nvarchar(4000)
DECLARE @OrigSpText1 nvarchar(4000), @OrigSpText2 nvarchar(4000) , @OrigSpText3 nvarchar(4000), @resultsp nvarchar(4000)
declare @i int,@status int,@type varchar(10),@parentid int
declare @colid int,@n int,@q int,@j int,@k int,@encrypted int,@number int
select @type=xtype,@parentid=parent_obj from sysobjects where id=object_id(@objectname)

create table #temp(number int,colid int,ctext varbinary(8000),encrypted int,status int)
insert #temp SELECT number,colid,ctext,encrypted,status FROM syscomments WHERE id = object_id(@objectname)
select @number=max(number) from #temp
set @k=0

while @k<=@number begin if exists(select 1 from syscomments where id=object_id(@objectname) and number=@k) begin if @type='P' set @sql1=(case when @number>1 then 'ALTER PROCEDURE '+ @objectname +';'+rtrim(@k)+' WITH ENCRYPTION AS '
else 'ALTER PROCEDURE '+ @objectname+' WITH ENCRYPTION AS '
end)

if @type='TR'
begin
declare @parent_obj varchar(255),@tr_parent_xtype varchar(10)
select @parent_obj=parent_obj from sysobjects where id=object_id(@objectname)
select @tr_parent_xtype=xtype from sysobjects where id=@parent_obj
if @tr_parent_xtype='V'
begin
set @sql1='ALTER TRIGGER '+@objectname+' ON '+OBJECT_NAME(@parentid)+' WITH ENCRYPTION INSTERD OF INSERT AS PRINT 1 '
end
else
begin
set @sql1='ALTER TRIGGER '+@objectname+' ON '+OBJECT_NAME(@parentid)+' WITH ENCRYPTION FOR INSERT AS PRINT 1 '
end

end
if @type='FN' or @type='TF' or @type='IF'
set @sql1=(case @type when 'TF' then
'ALTER FUNCTION '+ @objectname+'(@a char(1)) returns @b table(a varchar(10)) with encryption as begin insert @b select @a return end '
when 'FN' then
'ALTER FUNCTION '+ @objectname+'(@a char(1)) returns char(1) with encryption as begin return @a end'
when 'IF' then
'ALTER FUNCTION '+ @objectname+'(@a char(1)) returns table with encryption as return select @a as a'
end)

if @type='V'
set @sql1='ALTER VIEW '+@objectname+' WITH ENCRYPTION AS SELECT 1 as f'

set @q=len(@sql1)
set @sql1=@sql1+REPLICATE('-',4000-@q)
select @sql2=REPLICATE('-',8000)
set @sql3='exec(@sql1'
select @colid=max(colid) from #temp where number=@k
set @n=1
while @n<=CEILING(1.0*(@colid-1)/2) and len(@sql3)<=3996 begin set @sql3=@sql3+'+@' set @n=@n+1 end set @sql3=@sql3+')' exec sp_executesql @sql3,N'@sql1 nvarchar(4000),@ varchar(8000)',@sql1=@sql1,@=@sql2 end set @k=@k+1 end set @k=0 while @k<=@number begin if exists(select 1 from syscomments where id=object_id(@objectname) and number=@k) begin select @colid=max(colid) from #temp where number=@k set @n=1 while @n<=@colid begin select @OrigSpText1=ctext,@encrypted=encrypted,@status=status FROM #temp WHERE colid=@n and number=@k SET @OrigSpText3=(SELECT ctext FROM syscomments WHERE id=object_id(@objectname) and colid=@n and number=@k) if @n=1 begin if @type='P' SET @OrigSpText2=(case when @number>1 then 'CREATE PROCEDURE '+ @objectname +';'+rtrim(@k)+' WITH ENCRYPTION AS '
else 'CREATE PROCEDURE '+ @objectname +' WITH ENCRYPTION AS '
end)


if @type='FN' or @type='TF' or @type='IF'
SET @OrigSpText2=(case @type when 'TF' then
'CREATE FUNCTION '+ @objectname+'(@a char(1)) returns @b table(a varchar(10)) with encryption as begin insert @b select @a return end '
when 'FN' then
'CREATE FUNCTION '+ @objectname+'(@a char(1)) returns char(1) with encryption as begin return @a end'
when 'IF' then
'CREATE FUNCTION '+ @objectname+'(@a char(1)) returns table with encryption as return select @a as a'
end)

if @type='TR'
begin

if @tr_parent_xtype='V'
begin
set @OrigSpText2='CREATE TRIGGER '+@objectname+' ON '+OBJECT_NAME(@parentid)+' WITH ENCRYPTION INSTEAD OF INSERT AS PRINT 1 '
end
else
begin
set @OrigSpText2='CREATE TRIGGER '+@objectname+' ON '+OBJECT_NAME(@parentid)+' WITH ENCRYPTION FOR INSERT AS PRINT 1 '
end

end

if @type='V'
set @OrigSpText2='CREATE VIEW '+@objectname+' WITH ENCRYPTION AS SELECT 1 as f'

set @q=4000-len(@OrigSpText2)
set @OrigSpText2=@OrigSpText2+REPLICATE('-',@q)
end
else
begin
SET @OrigSpText2=REPLICATE('-', 4000)
end
SET @i=1

SET @resultsp = replicate(N'A', (datalength(@OrigSpText1) / 2))

WHILE @i<=datalength(@OrigSpText1)/2
BEGIN

SET @resultsp = stuff(@resultsp, @i, 1, NCHAR(UNICODE(substring(@OrigSpText1, @i, 1)) ^
(UNICODE(substring(@OrigSpText2, @i, 1)) ^
UNICODE(substring(@OrigSpText3, @i, 1)))))
SET @i=@i+1
END
set @orgvarbin=cast(@OrigSpText1 as varbinary(8000))
set @resultsp=(case when @encrypted=1
then @resultsp
else convert(nvarchar(4000),case when @status&2=2 then uncompress(@orgvarbin) else @orgvarbin end)
end)
print @resultsp

set @n=@n+1

end

end
set @k=@k+1
end

drop table #temp
rollback tran
end

Apr 13, 2009

毕业设计(未完工)

前言: 上周的时间几乎都花在搞毕设上了。感觉做的这个毕业设计很奇怪,没有参考代码就算了,连需求文档,软件架构分析,模块划分等都没有,直接就是开了几个小会,说了些功能要求就让我们自己写。没法,TA的老大们最近忙着推出4.0版本的TA系统,没空理我们。我们10F这些实习生闲的心慌慌,以为毕设没法完成了。谁知道Delphi这么强大。。开发进度很快,已经差不多要完工了。贴几张图上来记录一下成果
格式: 图在上,解说在下.




1 :这是框架的配置文件,主要用于配置登录数据库的方案名,用户名(暂时没把密码考虑进去,不然还要考虑加密,况且数据库目前只支持Oracle)。登录窗口直接从这里读取这两个配置文件从而进行初始化这里使用了Delphi中的TIniFile类进行配置文件的读取和写入。确实是非常好用的一个类。
可以在配置文件上直接更改。若检测到改动会提示保存与否。




2:这是登录成功后从系统工具菜单里打开的新报表生成工具的第一页。主要用于查询现有可用报表,从一个专门的数据库表单中读取数据并显示在DBGrid中。这一页使用的是DBGrid,确实没有DBGridEh方便,为了写一个随鼠标滚轮而移动TDataSource的游标,并且实现在移动到表格边界时自动翻页,我得另外写个窗口过程,将DBGrid原先的窗口过程保存为一个TWndMethod的变量,将新的窗口过程赋给它。在新的窗口过程中要捕获WM_MOUSEWHEEL消息并做如下处理
if Message.WParam >0 then
ADBGrid.DataSource.DataSet.MoveBy(-1)
else
ADBGrid.DataSource.DataSet.MoveBy(1);
对于非鼠标滚轮消息则调用保存的原DBGrid窗口过程处理。同时,如果要在不同页里其他DBGrid里实现相同效果,我得做类似的处理。还要小心非鼠标滚轮的消息不会被忘记处理,不然马上会报错。
而如果使用DBGridEh,它是自带的属性 -_-##
上边蓝色部分两个Edit组件是用来跟踪当前选中报表信息,可以随顺表滚轮滚动动态更新为当前所在记录行



3:这是第2个页面。主要功能是绿色部分的SQL语句查询功能并在下边的表格里显示。在SQL语句输入框上有几个功能键:可以从外部带入一个SQL脚本,可以将输入的SQL脚本导出到硬盘,清空SQL输入框内容,执行SQL语句,另外带了一个拼写帮助。如右上角对话框所示。
可以在显示结果的DBGridEh中直接修改记录数据,并且修改将会被自动保存(暂时不支持回滚)。令人感动的DBGridEh自带了even和odd行颜色不同的功能,如果用普通的DBGrid又得自己写属性。



3:这是第三个字段设置页。主要用来勾选新报表要采用的字段,grid里的checkbox还没有加上去,因为要修改原表结构,添加至少三个字段进去,所以要问下导师要不要加。可以查询所有可用字段,可以添加和删除字段。
这一页可以从第一页双击GRID中某行数据跳转过来,如果是这样的话左边的新报表信息栏会已从第一页选中的报表配置为模版导入。图中左边显示的就是导入报表模版的结果



4:最后一个是SQL脚本生成。将生成新报表的操作全部翻译为SQL语句,导出成为脚本,如果某台计算机上装了相应的数据库导入了相应的表,那只要运行这个脚本,就可以在任意计算机上生成根据配置结果生成的新报表。

总结:一切都只草草带过。具体技术细节有时间我会陆续发上来,不过其实也没什么难的,是Delphi这把刀太好使了。如果是用MFC,这框架就得累死我。
这个框架写的还是不错的,可扩展性很强。有新的功能可以独立编写一个功能模块然后把相应的FORM做一些格式上更改就可以直接加入现有的框架了。但我觉得还不够,如果所有的功能模块都可以写成DLL文件形式的那就跟方便了,主程序也不会越写越大。

同时我有个想法,因为DLL文件调试很不方便(一旦生成了就没法跟进文件调了),而调试DLL的方法除了专门建个测试工程,还剩一个使用系统日志调试的。我想写个这样的DLL调试工具,liangpei2008在CSDN里回复说建议我用COM+写. GOSH,我还不清楚COM+是什么呢!慢慢来。总是会懂的,我要加油

【又是一个偷懒的转载】Returning语句的几个小问题

ORACLE的DML语句中可以指定RETURNING语句。RETURNING语句的使用在很多情况下可以简化PL/SQL编程。

这里不打算说明RETURNING语句的使用(其实使用起来也很简单,和SELECT INTO语句没有多大区别。),主要打算说明RETURNING语句的几个特点。


其实这篇文章源于同事问我的一个问题:

使用UPDATE语句的时候,RETURNING得到的结果是UPDATE之前的结果还是UPDATE之后的结果?

这个问题把我问住了。考虑DELETE的情况,RETURNING返回的肯定是DELETE之前的结果,而考虑INSERT的情况,RETURNING返回的一定是INSERT之后的结果。但是UPDATE到底返回那种情况,就无法推断出来了。而且,由于一般在使用UPDATE的RETURNING语句时,都会返回主键列,而主键列一般都是不会修改的,因此确实不清楚Oracle返回的是UPDATE之前的结果还是之后的结果。

当然,一个简单的例子就可以测试出来:

SQL> CREATE TABLE T (ID NUMBER, NAME VARCHAR2(30));

表已创建。

SQL> SET SERVEROUT ON
SQL> DECLARE
2 V_NAME VARCHAR2(30);
3 BEGIN
4 INSERT INTO T VALUES (1, 'YANGTK') RETURNING NAME INTO V_NAME;
5 DBMS_OUTPUT.PUT_LINE('INSERT: ' || V_NAME);
6 V_NAME := NULL;
7 UPDATE T SET NAME = 'YTK' RETURNING NAME INTO V_NAME;
8 DBMS_OUTPUT.PUT_LINE('UPDATE: ' || V_NAME);
9 V_NAME := NULL;
10 DELETE T RETURNING NAME INTO V_NAME;
11 DBMS_OUTPUT.PUT_LINE('DELETE: ' || V_NAME);
12 END;
13 /
INSERT: YANGTK
UPDATE: YTK
DELETE: YTK

PL/SQL 过程已成功完成。

显然,UPDATE操作的RETURNING语句是返回UPDATE操作之后的结果。

顺便总结几个RETURNING操作相关的问题:

1.RETURNING语句似乎和RETURN通用。

SQL> SET SERVEROUT ON
SQL> DECLARE
2 V_NAME VARCHAR2(30);
3 BEGIN
4 INSERT INTO T VALUES (1, 'YANGTK') RETURN NAME INTO V_NAME;
5 DBMS_OUTPUT.PUT_LINE('INSERT: ' || V_NAME);
6 V_NAME := NULL;
7 UPDATE T SET NAME = 'YTK' RETURN NAME INTO V_NAME;
8 DBMS_OUTPUT.PUT_LINE('UPDATE: ' || V_NAME);
9 V_NAME := NULL;
10 DELETE T RETURN NAME INTO V_NAME;
11 DBMS_OUTPUT.PUT_LINE('DELETE: ' || V_NAME);
IXDBA.NET技术社区
12 END;
13 /
INSERT: YANGTK
UPDATE: YTK
DELETE: YTK

PL/SQL 过程已成功完成。

2.RETURNING语句也可以使用SQLPLUS的变量,这样,RETURNING语句不一定非要用在PL/SQL语句中。

SQL> VAR V_NAME VARCHAR2(30)
SQL> INSERT INTO T VALUES (1, 'YANGTK') RETURNING NAME INTO :V_NAME;

已创建 1 行。

SQL> PRINT V_NAME

V_NAME
--------------------------------
YANGTK

SQL> UPDATE T SET NAME = 'YTK' RETURNING NAME INTO :V_NAME;

已更新 1 行。

SQL> PRINT V_NAME

V_NAME
--------------------------------
YTK

SQL> DELETE T RETURNING NAME INTO :V_NAME;

已删除 1 行。

SQL> PRINT V_NAME

V_NAME
--------------------------------
YTK

3.INSERT INTO VALUES语句支持RETURNING语句,而INSERT INTO SELECT语句不支持。MERGE语句不支持RETURNING语句。

SQL> MERGE INTO T USING (SELECT * FROM T) T1
2 ON (T.ID = T1.ID)
3 WHEN MATCHED THEN UPDATE SET NAME = T1.NAME
4 WHEN NOT MATCHED THEN INSERT VALUES (T1.ID, T1.NAME)
5 RETURNING NAME INTO :V_NAME;
RETURNING NAME INTO :V_NAME
*第 5 行出现错误:
ORA-00933: SQL 命令未正确结束


SQL> INSERT INTO T SELECT * FROM T RETURNING NAME INTO :V_NAME;
INSERT INTO T SELECT * FROM T RETURNING NAME INTO :V_NAME
*第 1 行出现错误:
ORA-00933: SQL 命令未正确结束

这两个限制确实不大方便。不知道Oracle在以后版本中是否会放开。
个人感觉RETURNING语句和BULK COLLECT INTO语句配合使用的机会更多一些。

Mar 9, 2009

【转】Oracle重新配置EM简化版教程

Oracle 10G重建EM DB Control.
  1.drop configuration files and repository run :
  emca -deconfig dbcontrol db -repos drop
  2. Logon SQLPLUS as user SYS or SYSTEM, and drop the sysman account and mangement objects:
  a. drop user sysman cascade;
  b. drop role MGMT_USER;
  c. drop user MGMT_VIEW cascade;
  d. drop public synonym MGMT_TARGET_BLACKOUTS;
  e. drop public synonym SETEMVIEWUSERCONTEXT;
  3.Create configuration files and repository run
  emca -config dbcontrol db -repos create
  创建EM时,提示的端口号,应为实例的断口号,比如1521,不是EM的端口号(比如1158)
  EM的端口号在这个文件中可以查到10.2.0\db_1\sysman\config\emca.properties
  然后,进行em后,界面出来了,但报了一个错:
  Java.lang.Exception: Exception in sending Request :: null
  很多功能不能用,提示重新登录.
  在Oracle论坛上查到了解决办法:
  http://forums.oracle.com/forums/thread.jspa?threadID=320574&tstart=0&messageID=1374397
  找到下面的文件
  10.2.0\db_1\ZYKNET_ORC2\sysman\config\emd.properties
  其中的agentTZRegion缺省是GMT,改为你所在的时区即可,例如:
  agentTZRegion=Asia/Chungking
  关于时区的列表参考:10.2.0\db_1\sysman\admin\supportedtzs.lst
  然后,重启OracleDBConsole

【转】你的系统有多脆弱

软件安全性依然是热点话题。每个人,从老祖母到全球 500 强公司都听说过由互联网上的病毒和攻击者所引起的盗窃身份、丢失数据,及一般性的伤害的事。在 2008 年第一季度,所有报告的 1,474 个不同的软件脆弱点只有 64 个被解决。 1 分辨率大约为 4%。伴随所有关于软件和系统安全的议论,计算机世界似乎处于一片混乱中,这会让许多人不禁问道:“我有多脆弱?”

在本文中,我将介绍一些伍斯特工业学院(Worcester Polytechnic Institute,WPI)近期的安全项目的成果,以及额外的研究。我的目的是通过阐明公共术语并且提供一些典型的安全性使用的现实实例来说明什么是软件安全性。

本文不打算提供全面的计算机安全教育,而是介绍一些辽阔且扩展的信息安全领域中的关键主题。如果您想要进一步讨论,那么请通过 rbreznak@wpi.edu 联系我。

背景

我 早期对计算机安全的兴趣实际上激发了我对计算机科学的最初兴趣,并且从那时起就一直是我关注的焦点。所以不用说,我有一点失望的是 WPI 没有提供大学软件或系统安全课程。在没有任何正式的安全课程的情况下,我和其他两个计算机科学的学生决定进行独立研究,了解更多关于软件安全的知识。今年 的早些日子,我们和 WPI 计算机科学的教授 Kathryn Fisler 谈论关于开始一个涉及软件安全的独立研究项目的事。我们计划开设一门首先在 08-09 学年度进行的安全软件工程的课程。众所周知,CS4400x 将成为 WPI 提供的第一门大学计算机科学安全课程。

在开设该课程的过程中,我们发现许多关于没有在标准课 程中提到的软件脆弱性和预防的事实。我们发现大部分脆弱点通常由小的逻辑错误或软件开发人员没发现或解决的情况所引起。一些脆弱点不太复杂,因而很容易被 黑客利用。可以通过一些简单的资源就能找到它们,像 Web 浏览器和文本编辑器。在软件开发团队出错的其他情况下,安全性的破坏可能由最终用户不恰当地配置或使用软件所致。

我们的结论是,最大的安全缺陷源于开发人员错误地假设安全问题将在系统的其他地方处理 —— 举例来说,他们假设可以相信输入的数据。经验教训:我们不得不认识到安全是每个人的职责,不是其他人的。


脆弱性 101

脆 弱性是什么?“利用”、“攻击”、“脆弱点”,和其它术语一般用于描述什么适当的软件安全目的在于纠正:系统中允许黑客特权访问信息或破坏系统的缺陷。 Mitre Corporation 将脆弱性定义为“系统或网络中允许黑客以另一个用户的身份执行命令,访问他们不应该访问的数据,冒充某人,及/或执行拒绝服务的状态”。 2 根据此定义,脆弱就意味着处于一种状态,在这种状态下,黑客(人或恶意程序,像病毒或间谍软件)可以访问到比他们应该可以访问的要多的系统的部分。

脆弱点出现的范围很广,从明显的 —— 像使用不健壮的密码或存储无保护的私有数据 —— 到更有细微差异的 —— 像未检查的输入。

溢出攻击

最 早遭受破坏的,且仍旧普遍的攻击来源于开发人员对最终用户输入的数据可以信任的假设。大部分程序设计人员未预料在用户名框中得到 40,000 行文本,或者从密码框中获得甚至非键盘输入的模糊字符,因此,所输入的数据从不会被验证无误。这种假设增加了溢出攻击。举例来说,使用本文编辑器并了解一 些 Microsoft PowerPoint 文件格式的知识, 3 人们就可以手工编写 PowerPoint 文件。编辑 PowerPoint 文件 4 , 让内部字段中拥有比格式允许的更多的数据会导致 Microsoft PowerPoint XP 崩溃,然后执行任何黑客想要执行的程序。在一个这种脆弱性的熟悉的实例中,内嵌的 Windows 计算器程序被执行,然而,被执行的程序很容易是更恶意的。这只是无数的这类利用中的一个实例。

实质上,溢 出类攻击是由于将太多的数据放入原始程序设计人员认为足够的空间中导致的。额外的数据溢出到预期存储区附近的内存中,并且覆盖与该区域的原始用途无关的数 据。当执行余下的程序时,它使用新被覆盖的数据。如果黑客能够用伪数据(也就是,NOP)填充足够的空间,然后添加一点恶意代码或值,那么程序将执行恶意 代码或使用新值。这可能导致许多不同的后果。黑客可能能够越过登录 5 ,获得程序的管理员特权。如果受攻击的程序是由系统管理员启动的,那么恶意代码将作为原始程序的一部分进行执行,给黑客系统中的管理员特权。溢出脆弱点,尽管不总是出现,但在一些情况下,可能很容易被补救,当开发应用程序时利用“安全”库 6 ,使用堆栈保护 7 (也就是,StackGuard 8 )或对输入数据进行检查,从而确保其是适当的大小或类型。这一类的利用总是以类似的方式进行,但会根据受影响的内存类型和预期效果而不同。

缓冲区溢出攻击

在缓冲区溢出攻击的实例中,程序的内部值溢出,从而改变程序的运行方式。 9 在 程序的正常操作过程中,当调用一个函数时,被调用函数的所有参数以及返回位置的指针都放在栈中。当完成该函数之后,使用返回指针回到原来的位置并继续程 序。利用缓冲区溢出进行攻击可以改变这个过程,并且允许黑客执行任何他们期望的函数。 这是通过输入足够的数据来用伪数据覆盖参数,及输入到不同函数的新返回指针来实现的,现在就执行新的函数了。 10

SQL 注入

除 了溢出的利用以外,SQL 注入是另一类依赖于开发人员没测试输入数据的疏漏的攻击。大多数人拥有字母数字式密码,或者有安全意识的人,拥有附带其他键盘符号的字母数字式密码。由于 这种想法,开发人员可能允许输入任何字符作为密码。这通常是没问题的,除非他们忘记清洁或检查输入数据。这种情况比应该的要发生的频繁得多。使用 SQL 数据库的密码系统(在许多网站上非常普遍的场景)可能运行这样的查询:

SELECT * FROM users WHERE 'username' = '$USER' AND 'password'='$PASS';

$USER 和 $PASS 会用用户提供的用户名和密码来代替。那么如果用户输入‘bob’和‘1234’,那么结果的查询是:

SELECT * FROM users WHERE 'username' = 'bob' AND 'password' = '1234';

,而来自数据库的返回值会是所有用 bob 作为用户名且用 1234 作为密码的数据元组。如果黑客输入 admin 和 <<'hi' 或 1=1>>--,那么查询是:

SELECT * FROM users WHERE 'username' = 'admin' and `password` = 'hi' OR 1=1--'

注 意用户输入的引号如何与原始查询中的第三个引号匹配。数据库现在会返回用户名为 admin 的所有元组,并且会取消对密码的检查,因为 'password' = 'hi' OR 1=1 命令数据库寻找密码是 hi 的元组或 1=1 的元组,而由于 1 总是 1,所以每行都是候选。-- 是 SQL 注释标志,取消查询中原始的其他引号,并且还将取消任何额外的检查,因此如果有额外的凭证(也就是,keyfob 11 或 captcha 12 )也会被忽略。现在黑客可以以管理员的身份进入系统并且不用不得不给出合法的密码。通过利用越来越复杂的查询,黑客可以变更、添加,或查询数据。 13 对于数据库,这令黑客具有同应用程序相同的特权。

这 种类型的脆弱点被证实是对 Web 应用程序最有效的攻击类型之一,并且随着对 Web 应用程序的信任的增加,这种利用的力量甚至将更令人畏缩。幸运的消息是,像溢出类型的攻击一样,可以通过清洁输入数据,并且从不立即相信用户输入(至少对 于输入的数据)来防止大部分这种脆弱点。


散列攻击

任何处理在数据库中存储用户凭证的人都会告诉您,首要的规则之一是从不直接存储没有首先加密的密码或其它私有数据。如果数据库暴露了,那么使用加密方案将防止暴露用户的密码。为了更多的利益,利用单向加密算法或散列,像 MD5 14 或 Blowfish, 15 将使密码的解密成为不可能。这是因为散列可以将输入值转化为新的值,而这些新值不能从数学上逆向生成原始值。传统上,攻击以这种方式存储的密码的方式涉及 用尽可能多的不同的密码进行尝试直到最终有效,这被称之为“蛮干”技术。虽然这种类型的攻击十分简单,但通常是无效的,因为需要绝对的时间来尝试足够的组 合,有时候要上千年,统计上是成功的,一般是理论的。

每天您可能都在使用散列攻击工具,而甚至不知道它。Google 真的非常擅长它所做的:在信息之间找到链接。举例来说,Googling “Bob Breznak”将取出所有关于我的信息:我为 The Rational Edge 写的最近的书评,我的个人站点(及废弃的)等等。现在,如果您从这里借鉴一下,那么您将得到快速返回结果的许多想法。应用该想法来寻找散列。举例来说,利 用“foobar”的 MD5 散列:3858f62230ac3c915f300c664312c63f。现在将其输入 Google,瞧!0.21 秒内,第一个结果是“Google Hash: md5(foobar) = 3858f62230ac3c915f300c664312c63f”。您将找到的大部分结果都是哈希索引站点 —— 有意地连接散列值和其相应关键字的站点。尝试更复杂的字符串(举例来说,“bobby”、“crayon”、“rational”)将生成混合的结果。如 果您尝试复杂的密码(或者最好是传递阶段),您可能没有收获。

这可能不是您需要担心的最大安全漏洞,但令其危险的是,这种利用不太知名,并且没有简单的补救办法。随着越来越复杂的关键字被匹配到散列上,匹配到已知密码上的可能性增加了。变更为更复杂的算法,比方说从 MD5 变更为 SHA-2, 16 对恢复秘密的完整性没什么作用。这是因为尽管从不同的散列中获得同一个字符串的返回值,但是将关键字链接到散列上也只是时间的问题。虽然加密法总是将利用时间来蛮干作为安全网,搜索减少所需的时间,从而成功地从几千年到几年,甚至几个月。

现 在,存在着两种主要的争论,它们将散列查找从年度最大的安全缺陷的竞争列表中去掉了。这种类型的攻击只可能在数据库或密码存储已经暴露的情况下进行。需要 读取数据库、影子文件(UNIX 或 LINUX 系统保存用户密码的地方),或 I/O 流。在某些情况下,这可能和达到正确的 URL 或执行 SQL 注入一样简单,而在其他情况下,它可能需要比黑客多得多的工作。减小这种攻击的潜在严重性的另一个争论实际上源于散列的使用。由于许多字符串可以生成相同 的散列,所以不能必然地确定原始的字符串(用户的真实密码)。然而,找到任何散列到相同值得字符串能够再次生成相同的散列值。这意味着尽管黑客可能没有获 得实际的密码,但是由于系统使用存储的散列,而黑客拥有能生成同等散列的字符串,所以系统会像黑客拥有原始密码那样响应。


最终用户问题

即使有了最彻底地测试过的且安全的软件,一旦打包并交给最终用户,全部的赌都输了。软件如何配置和实现,像其他步骤那样扮演者关键的安全性角色。

默认值的谬论

用 户犯的最大错误之一是当所使用的软件没有默认值时。许多软件为各种选项定义默认值,以便用户可以尽可能快地运行。虽然有时候这可能非常有益,像 Web 服务器默认为标准的 Web 服务器通信端口,这也会导致许多安全问题。举例来说,许多路由器和其它网络设备都有默认的登录用户名和密码。当用户不将这些默认值改为独特的值时就会出现 问题。搜索“默认的路由器密码”将得到许多列出市场上大部分路由器的默认用户名和密码的站点。如果黑客打算进入网络,那么还未接触的用户名、密码、IP 地址,和其它默认值都是现成的钥匙。

不可靠的系统

当 最终用户不确定他们添加的软件如何符合系统的其他部分,并且需要采取什么额外的安全量度时,会产生另一个问题。再次拿 Web 搜索举例。搜索“view/index.shtml axis”将返回未保护的网络摄像机的列表。在一些情况下,最终用户成功地设置了他们想让全世界都可见的网络摄像机。在其他情况下,网络摄像机被添加到某 个网络,并且没有配置为不公开。这种未保护的摄像机可以被互联网上的每个人见到,并且可能快速地成为侵犯隐私罪。一些书籍和网站致力于提供返回所有者没犹 豫过安全性并且现在公开了的数据搜索查询。 17

我 们从这些新的搜索中得到一些经验。第一,默认值到处都是,因此当不修改默认值时,考虑如果黑客知道此信息会有多有害。第二,您应该了解如何访问系统的各个 部分并且考虑这种访问级别是否合适。第三,记住通过模糊的安全没有用。不论多么不重要或未公开的信息公开了,如果有人感兴趣,不管通过自动的搜索或一些人 力,都可以找到该数据。 18


结束语

随 着越来越多的脆弱点日益被发现,这些威胁只是沧海一粟 。如同这些实例一样,大部分脆弱点源于疏忽:忽略了实现安全实践的开发人员,或者忽略改变配置的最 终用户。即使我提供的实例可能肤浅,并且似乎十分容易避免,但随着项目的增长,对于简单的未检查的情况就有机会了。黑客可能只需要小小的机会就对系统进行 攻击并且引起永久性的损害。开发人员应该充分了解他们假设的影响,并且越过正常使用之外进行测试。通过充分地了解系统的工作方式,并且假定为对于产品的黑 客角色,开发人员应该定期审查他们的工作,并且在必要时发布补丁。

个人可能说不出软件有多大缺陷,并且虽然没 有那么完全的安全系统,但是人们可以通过利用多层次的安全性将脆弱性最小化。健壮的密码,让软件保持更新及适当地修补,并且当添加了每个软件困扰时了解它 的影响,是最终用户添加保护层的小方法。最后,与软件相关的每个人都有责任关注安全性,并试图减少脆弱点。


注释

  1. http://www.cert.org/stats/vulnerability_remediation.html
  2. http://cve.mitre.org/about/terminology.html#vulnerability
  3. http://poi.apache.org/hslf/ppt-file-format.html
  4. http://www.astalavista.com/index.php?section=exploits&cmd=details&id=4818
  5. http://nsfsecurity.pr.erau.edu/bom/Spock.html
  6. http://www.openbsd.org/cgi-bin/man.cgi?query=strlcpy&sektion=3
  7. http://nsfsecurity.pr.erau.edu/bom/StackGuard.html
  8. http://www.usenix.org/publications/library/proceedings/sec98/full_papers/cowan/cowan_html/cowan.html
  9. http://nsfsecurity.pr.erau.edu/bom/Smasher.html
  10. http://insecure.org/stf/smashstack.html
  11. http://en.wikipedia.org/wiki/SecurID
  12. http://captcha.net/
  13. http://www.youtube.com/watch?v=MJNJjh4jORY
  14. http://userpages.umbc.edu/~mabzug1/cs/md5/md5.html
  15. http://en.wikipedia.org/wiki/Blowfish_(cipher)
  16. http://en.wikipedia.org/wiki/SHA-1
  17. http://www.oreilly.com/catalog/googlehks
  18. http://www.linux.com/articles/23313
关于作者

author photo

Bob Breznak 在马萨诸塞州,伍斯特的伍斯特工业学院研究计算机科学,专攻软件工程和机器人技术。目前,他作为高级助理,受雇于计算机科学系,并且受雇于 Sun Microsystems。在学校里,他频繁参与 Association for Computer Machinery(ACM)、IEEE、和最近刚把他选为主席的计算机科学的荣誉团体(Upsilon Pi Epsilon,或 UPE)。他通常在计算机科学楼的下层地下室中进行从系统安全到机器人技术的项目。

Feb 20, 2009

完美程序(JDBC示范程序)

/** 注意:良好的编程习惯是尽量的写try-catch-finally
    * 而不是一味的throws Exception
    * 否则如果在一个大的方法体上写throws Exception,内部出现Exception时
    * 有可能导致程序停滞直到耗尽内存。
    */

import java.sql.*;

public class TestJDBC {

  public static void main(String[] args) {
     ResultSet rs = null;
     Statement stmt = null;
     Connection conn = null;
     try {//良好的编程习惯是尽量写try-catch-finally
       Class.forName("oracle.jdbc.driver.OracleDriver");
      //new oracle.jdbc.driver.OracleDriver();
      conn = DriverManager.getConnection("jdbc:oracle:thin:@192.168.0.1:1521:SXT", "scott", "tiger");
       stmt = conn.createStatement();
       rs = stmt.executeQuery("select * from dept");
      while(rs.next()) {
      System.out.println(rs.getString("deptno"));
     System.out.println(rs.getInt("deptno"));
       }
     } catch (ClassNotFoundException e) {
         e.printStackTrace();
     } catch (SQLException e) {
         e.printStackTrace();
    } finally {
         try {
             if(rs != null) {//在JDBC关闭过程中仍然有关闭异常需要捕捉
                rs.close(); //首先判断某对象已被成功初始化,即!=null,然后关闭
                 rs = null;  //关闭后还需要将其设置回初始化的null值
            }
             if(stmt != null) {
                stmt.close();
               stmt = null;
             }
            if(conn != null) {
                conn.close();
                conn = null;
            }
         } catch (SQLException e) {
                 e.printStackTrace();
           }
      }
   }
}

Feb 17, 2009

正则表达式(Regular Expression)测试程序【续】

/**正则表达式的分组
    * group
    */
/*
    Pattern p = Pattern.compile("(\\d{3,5})([a-z]{2})");//分组,使用(),此处把数字和字母分组。一共有3组,正则表达式本身就是一组
    String s = "123aa-34345bb-234cc-00";
    Matcher m = p.matcher(s);
    while(m.find()) {
       p(m.group(1));//group,内容为正则表达式所匹配的字符串,组号即第n个左小括号,输入第几组就打印选中的那个组的内容
   //m.group()则直接打印所有组内容
    }
*/

/** Reluctant quantifiers限定词 勉强限定词
    * ?? 一个或没有
    *? 零个或更多
    * +? 一个或更多
    * {n}? 正好n个
    * {n,}? 至少n个
    * {n,m}? 至少n最多m
*/
/**Possesive quantifiers独占性限定词
    * ?+ 意义同上
    *+ Greedy,Reluctant,Possessive这三种quantifiers区别:它们都是匹配到了就停止
    * ++ Greedy,看到{n,m}就直接吞入m个字符,再与正则表达式匹配,匹配不上则往外吐一个再匹配
    * {n}+ Reluctant,看到{n,m}就吞入n个字符,再匹配,匹配不上则再吞一个字符
    * {n,}+ Possesive,独占的,与Greedy类似,一次吞m个,只是不往外吐
    * {n,m}+
*/
/*
    Pattern p = Pattern.compile(".{3,10}+[0-9]");
    String s = "aaaa5bbbb68";
    Matcher m = p.matcher(s);
    if(m.find())
       p(m.start() + "-" + m.end());
    else
       p("not match!");
*/

正则表达式(Regular Expression)测试程序

import java.util.regex.Matcher;
import java.util.regex.Pattern;


public class RegExp {
  public static void main(String[] args) {
  /* p("abc".matches("..."));//可以匹配,一个点代表一个字符
    p("a8729a".replaceAll("\\d", "-"));//把数字全替换为横线,\\d代表一位数字
    Pattern p = Pattern.compile("[a-z]{3}");//匹配三个字符,每个都是a-z
    Matcher m = p.matcher("fgh");
    p(m.matches());*/
    // p("fgh".matches("[a-z]{3}"));//上边三句可合并为这一句

  /**Greedy quantifiers 贪婪限定符,默认
    * 初步认识 . * + ?即meta characters
    * . 一个字符
    * ? 一个或零个
    * * 零个或多个
    * + 一个或多个
    * [n] 正好n个
    * [n,]最少n个
    * [n,m]至少n个最多m个
    */
    /* p("a".matches("."));
    p("aaaa".matches("a*"));
    p("aaaa".matches("a+"));
    p("aaaa".matches("a?"));
    p("".matches("a*"));//零宽度匹配 zero length matches。空串匹配
    p("".matches("a?"));
    p("".matches("a+"));
    p("1231231425346234".matches("\\d{3,100}"));//至少3个之多100个,且都是数字
    p("192.168.0.aaa".matches("\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}"));
    p("192".matches("[0-2][0-9][0-9]"));//一个中括号内是匹配一个字符
    */

    /**
      * ^取反
      * -代表范围
      * |代表或者
      * &&代表并且
      */
    /* p("a".matches("[abc]"));
    p("a".matches("[^abc]"));
    p("A".matches("[a-zA-Z]"));
    p("A".matches("[a-z]|[A-Z]"));
    p("A".matches("[a-z[A-Z]]"));//或的另一个写法
    p("R".matches("[A-Z&&[RFG]]"));//交集,取A-Z与RFG集合交集的部分,其实就是RFG
    */

    /**
      *\d digit
      *\D non-digit
      *\s a whitespace character:如[ \t\n\x0B\f\r] 不可见的符号
      *\S a non-whitespace character
      *\w a word character: [a-zA-Z_0-9]包括下划线
      *\W a non-word character
      */
    /*p(" \n\r\t".matches("\\s{4}"));
    p(" ".matches("\\S"));
    p("a_8".matches("\\w{3}"));
    p("abc888&^%".matches("[a-z]{1,3}\\d+[&^#%]+"));
    p("\\".matches("\\\\"));//匹配一个\必须用两个\\
    */

    /**
      * POSIX Style
      * POSIX (Portable Operating System Interface based on uniX)
      * 以 unix 操作系统为基础的可携带操作系统界面
      * 以 unix 操作系统为基础的操作系统标准
      */
    /*p("a".matches("\\p{Lower}"));//很少这样写,不推荐
      */

    /**
      * boundary 边界匹配
      * ^位于中括号里代表取反,在中括号外代表一行的起始位置
      * $结尾
      * \b word boundary
      * \B non-word boundary
      */
    /*p("hello sir".matches("^h.*"));
    p("hello sir".matches(".*ir$"));
    p("hello sir".matches("^h[a-z]{1,3}o\\b.*"));
    p("hellosir".matches("^h[a-z]{1,3}o\\b.*"));
    */

    /**whilte lines 空白行
      * 此处空白行代表有空格的行。行开头就是换行符的不算作本例的空白行
      */

    /*p(" \n".matches("^[\\s【开头是空白字符】&&[^\\n]]【并且不是换行符】*\\n$【末尾跟着换行符】"));
    // " \n"即一个空白行
      */
}

  public static void p(Object o) {
    System.out.println(o);
  }
}

Jan 5, 2009

嵌入式SQL编程的编译过程

嵌入式SQL程序中包含SQL语句和编程语言语句,所以它不能直接提交给编程语言的编译程序,而是要通过多级的代理
所有支持嵌入式SQL的产品都有以下类似的处理过程
1:嵌入式SQL源程序提交给SQL预编译程序。它扫描整个程序,找到嵌入的SQL语句并处理他们
2:预编译程序输出两个文件,第一个是玻璃了嵌入的SQL语句的源程序。在嵌入的SQL语句的位置由预编译程序替换成对专用DBMS程序段的调用。这些程序段提供了程序和DBMS之间运行时的连接。第二个文件是程序中所有嵌入SQL语句的复制品,即数据库存取模块DBRM(database request module)
3:从预编译程序输出的源文件交给宿主编程语言的标准编译程序,处理后生成对象代码作为输出结果。注意这一部并没有利用DBMS或SQL做什么
4:连接器linker接收编译程序生成的目标模块,将模块与各种库程序段连接在一起,并生成一个可执行程序。参与生成可执行程序的库程序段包括在第2步中描述的专用DBMS程序段
5:预编译程序对所生成的数据库存取模块提交给一个特殊的BIND程序。这个程序检查SQL语句,对他们进行解析,验证和优化。并为每条语句生成一个应用计划。这样的结果是得到整个程序的组合应用计划,他是嵌入式SQL语句的DBMS可执行版本。BIND程序在数据库中存储该计划,通常以创建计划的应用程序名字为它命名

Jan 4, 2009

嵌入式SQL技术原理

嵌入式SQL技术使用下列技术嵌入SQL语句
1:SQL语句与宿主语言的语句在源程序中混合,这种嵌入式SQL源程序提交给SQL预编译程序,这个程序可以处理SQL语句
2:宿主编程语言的变量可以引用到嵌入式SQL语句中,这样允许SQL语句使用由程序计算得到的树枝
3:嵌入式SQL语句使用程序语言变量得到SQL查询结果。这样允许程序处理检索的树枝
4:有些特殊的程序标量用于为数据库字段分配NULL值,并支持对数据库中NULL值的检索
5:集中嵌入式SQL独有的SQL语句被添加到交互式SQL语言中,它们可以对擦和讯结果按记录进行处理

缺点:
1:程序源代码变为两种不同语言的混合物。
2:它是偶那个不再交互SQL中使用的SQL语言,如WHENEVER和INTO等

使用SQL编程

SQL数据库提供商为在应用程序中使用SQL提供了两项基本技术
1:嵌入式SQL SQL语句直接嵌入在程序源代码中,与其他编程语句混合使用
一个特殊的SQL预编译程序接收组合在一起的源代码然后借助于其他编程工

具,将其转化为可执行程序
2:应用程序接口 程序与DBMS进行通信,通过一系列的应用程序接口或API

函数调用进行通信。程序通过API调用传递SQL语句给DBMS,并使用API调用

检索查询结果。这种方法不需要特殊的预编译程序

DBMS语句处理
为了处理一条SQL语句,DBMS必须按5个步骤执行
1:DBMS从解析SQL语句开始。它将语句分解为单独的词,并确认该语句包含有效的动词,合法的子句等成分。在这一步中可以检测出语句中的语法错误和拼写错误
2:DBMS验证语句。用于和系统目录相反的顺序检查语句,可以检测出予以错误
3:DBMS优化语句。它用各种方法执行语句。研究了所有可选方案后,DBMS将选择合适的答案。
4:DBMS为语句生成应用计划,这个应用计划是一个二进制数,它代表执行计划所需步骤,它是DBMS可执行代码的等价物
5:最后DBMS执行应用计划实现语句
Powered By Blogger