Oracle Database 11g Wrap加密解密分析

Catalogue
  1. 1. 1. Oracle Wrap的文件格式
  2. 2. 2. Oracle Wrap的加密机制
  3. 3. 3. Unwrap反向解密
    1. 3.1. 3.1 base64解码得到Oracle双字符转换后字符串
    2. 3.2. 3.2 计算Oracle双字符转换表
      1. 3.2.1. 3.2.1 创建LZ压缩包
      2. 3.2.2. 3.2.2 比较LZ压缩串和base64解码串
      3. 3.2.3. 3.2.3 计算替换表
      4. 3.2.4. 3.2.4 编写Unwrap PL/SQL程序
  4. 4. 4. Wrap和Unwrap过程

[TOC]

​ Oracle为PL/SQL开发者提供的一种对他们所写的代码(oracle下的对象Package、Procedure、Function、Java Source等)进行加密的工具。当PL/SQL代码被加密以后,它就被描述为被“包装过”。使wrap工具对代码进行包装,wrap工具会取出含有要包装的代码的文件名并输出下面的文件:

1
wrap iname=plain.sql oname=encrypted.plb

​ 因为代码被加密了,所以其细节被隐藏了,而且Oracle不提供解包装设备。但是我们可以编写我们自己的unwrap程序。

1. Oracle Wrap的文件格式

​ Oracle为了防止自己的对象程序源码泄露,也对其做了wrap处理。因为许多PACKAGE、PROCEDURE等程序本身可能存在SQL注入漏洞,但是如果得不到源码,对其进行审计是很困难的。这样wrap在一定程度上也起到了保护作用。

​ 这里随便找一个Oracle的PACKAGE为实例,这里我们找到kupd$data包程序,其代码如图所示:

​ 可以将所有被warp加密的*.plb文件内容,分为header和body(BASE64)。header包含有关被加密包的数据库版本,对象类型以及加密和未加密文本的长度信息。

​ body包含使用BASE64编码的实际被加密的代码。

​ header格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1. CREATE OR REPLACE PACKAGE kupd$data wrapped 
2. a000000
3. 1
4. abcd
5. abcd
6. abcd
7. abcd
8. abcd
9. abcd
10. abcd
11. abcd
12. abcd
13. abcd
14. abcd
15. abcd
16. abcd
17. abcd
18. abcd
19. 9
20. abb 42e

​ 展开.plb文件时,可以忽略前20行。包装的代码的header提供有关PL/SQL代码类型的一些信息,下面是header的一些字段(不过,实际上,不需要它们来解密PL/SQL)

​ 第3行:
​ 看起来像是数据库服务器相关的十六进制值,但是在大多数情况下,它们等于1。因此,使用此标头记录没有实际值。

​ 第19行:
​ 一个十六进制值,指定PL / SQL对象的类型(所有PL / SQL对象的类型):

十六进制 对象类型
7 过程
8 函数
9
b 包体
d 类型
e 类型体

​ 第20行:

​ header的最后一行包含两个十六进制值,中间用空格分隔。这些值包含长度信息。第一个值包含未加密文本的长度(不包含CREATE OR REPLACE部分)。第二个值包含不带标题且不带结尾LF(0x0A)和“ /”符号的包体的长度。

​ body

​ 如前所述,加密的PL / SQL文本是BASE64编码的,需要解码后才能真正开始解包(解密)。BASE64解码主体的前20个字节包含包装(加密)主体的SHA1哈希值。主体的其余部分是一个编码的(使用编码表)压缩的字节流,其中包含源文本。

2. Oracle Wrap的加密机制

​ Oracle加密的原理就是先对源码进行lz压缩,得到lz压缩串,然后对压缩串进行SHA-1运算得到40位的加密串,然后将加密串与压缩串拼接得到拼接字符串,然后对拼接字符串进行Oracle双字符转换(转换表)。最后将转换后的字符串进行base64编码,最终得到wrap的加密串。

3. Unwrap反向解密

​ 通过上面的加密机制,反向操作达到解密目的。

3.1 base64解码得到Oracle双字符转换后字符串

​ 首先,我们以一小段PL/SQL代码来测试分先加密字符串的结构,这里我先讲程序加密处理。如图所示,这里将create procedure a代码wrap之后变成了如下代码

1
select dbms_ddl_wrap('create procedure a') from dual;

​ 为了进一步分析其加密字符串中的结构,我们利用如下print_warpped_str.sql代码:

1
2
3
4
with src as
( select 'procedure a' txt from dual ), wrap as
( select src.txt, dbms_ddl.wrap( 'create ' || src.txt ) wrap from src )
select rtrim( substr( wrap.wrap, instr(wrap.wrap, chr(10), 1, 20 ) + 1),chr(10)) from wrap;

1
8BgMHdmA3Qg9IbJmntlZoZQoHwcwg5nnm7+fMr2ywFxakaamb40d1Q==

​ ps:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
oracle中trim,ltrim,rtrim函数用法

oracle中trim,ltrim,rtrim函数用法
该函数共有两种作用:
第一种,即去除空格。
例子:
--TRIM去除指定字符的前后空格
SQL> SELECT TRIM(' dd df ') FROM dual;
TRIM('DDDF')
\------------
dd df

--LTRIM去除指定字符的前面空格
SQL> SELECT LTRIM(' dd df ') FROM dual;
LTRIM('DDDF')
\-------------
dd df

--RTRIM去除指定字符后面后空格
SQL> SELECT RTRIM(' dd df ') FROM dual;
RTRIM('DDDF')
\-------------
dd df

第二种,去除指定的字符。trim只能去除单个字符,而ltrim和rtrim可以去除多个字符。
trim去除字符的写法:
--表示字符串string2去除前面|后面|前后面(leading|trailing|both)的字符string1,默认去除方式为both
SELECT TRIM(leading|trailing|both string1 FROM string2) FROM dual;
例子:
SQL> SELECT trim(leading 'd' from 'dfssa') FROM dual;
TRIM(LEADING'D'FROM'DFSSA')
\---------------------------
fssa

SQL> SELECT trim(both '1' from '123sfd111') FROM dual;
TRIM(BOTH'1'FROM'123SFD111')
\----------------------------
23sfd

SQL> SELECT trim(trailing '2' from '213dsq12') FROM dual;
TRIM(TRAILING'2'FROM'213DSQ12')
\------------------------------
213dsq1

​ 进一步对这段代码做base64解码,获得经过Oracle双字符转换后的字符串,如图所示:

1
2
3
4
with src as
( select 'procedure a' txt from dual ), wrap as
( select src.txt, dbms_ddl.wrap( 'create ' || src.txt ) wrap from src )
select utl_encode.base64_decode( utl_raw.cast_to_raw( rtrim( substr( wrap.wrap, instr(wrap.wrap, chr(10), 1, 20 ) + 1),chr(10)))) from wrap;

1
F0180C1DD980DD083D21B2669ED959A194281F07308399E79BBF9F32BDB2C05C5A91A6A66F8D1DD5

因为字符串是由两部分组成,先经过LZ压缩,然后经过SHA-1处理,然后再将SHA-1字符串和压缩字符串拼接得到,因为SHA-1总共40位,所以40位以后的全为经过Oracle双字符转换的压缩串,也就是

1
308399E79BBF9F32BDB2C05C5A91A6A66F8D1DD5

3.2 计算Oracle双字符转换表

​ 当wrap完成sha-1字符串和压缩字符串的拼接之后,会对照一个字符代替表进行Oracle双字符转换。这个表可能是Oracle的商业机密,所以官方没有给出该表的信息。

​ 但是,我们已经得到了经过字符转换之后的LZ压缩串,同时我们可以通过LZ算法得到的LZ压缩串,通过对比这两个字符串,我们便可以推算出这个转换表。

3.2.1 创建LZ压缩包

​ 关于LZ压缩,这里用老外提供的一个JAVA包,为了爆破转换表,我们先把JAVA包创建好,用以进行LZ压缩与解压,LZ_java.sql,如下所示(用SYS用户):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
create or replace java source named UNWRAPPER
as
import java.io.*;
import java.util.zip.*;

public class UNWRAPPER
{
public static String Inflate( byte[] src )
{
try
{
ByteArrayInputStream bis = new ByteArrayInputStream( src );
InflaterInputStream iis = new InflaterInputStream( bis );
StringBuffer sb = new StringBuffer();
for( int c = iis.read(); c != -1; c = iis.read() )
{
sb.append( (char) c );
}
return sb.toString();
} catch ( Exception e )
{
}
return null;
}
public static byte[] Deflate( String src, int quality )
{
try
{
byte[] tmp = new byte[ src.length() + 100 ];
Deflater defl = new Deflater( quality );
defl.setInput( src.getBytes( "UTF-8" ) );
defl.finish();
int cnt = defl.deflate( tmp );
byte[] res = new byte[ cnt ];
for( int i = 0; i < cnt; i++ )
res = tmp;
return res;
} catch ( Exception e )
{
}
return null;
}
}
/

alter java source UNWRAPPER compile
/

​ 这里deflate函数实现LZ压缩,而inflate则实现解压。

​ 然后用包把JAVA声明进来,create_amosunwrapper.sql:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
create or replace package amosunwrapper
is
function deflate( src in varchar2 )
return raw;
function deflate( src in varchar2, quality in number )
return raw;
function inflate( src in raw )
return varchar2;
end;
/

create or replace package body amosunwrapper
is
function deflate( src in varchar2 )
return raw
is
begin
return deflate( src, 6 );
end;

function deflate( src in varchar2, quality in number )
return raw
as language java
name 'UNWRAPPER.Deflate( java.lang.String, int ) return byte[]';

function inflate( src in raw )
return varchar2
as language java
name 'UNWRAPPER.Inflate( byte[] ) return java.lang.String';

end;
/

3.2.2 比较LZ压缩串和base64解码串

​ 创建好LZ压缩包,来看一下,LZ串和base64解码串的对比,

​ 首先,使用JAVA包,需要设置压缩级别参数,这个等级参数不一样,压缩得到的字符串完全不一样。我们可以从0等级开始一个一个进行测试,经过测试发现,Oracle用的是9等级。

​ 所以,我们用以下代码对比两个字符串:comp_lz_and_base64.sql:

1
2
3
4
5
6
7
8
9
10
with src AS 
( select 'procedure a' txt from dual ),
wrap as
( select src.txt , dbms_ddl.wrap( 'create ' || src.txt ) wrap from src ),
subst as
(select substr( utl_encode.base64_decode( utl_raw.cast_to_raw(rtrim( substr( wrap.wrap, instr( wrap.wrap, chr( 10 ), 1, 20 ) + 1 ), chr(10) ) ) ), 41 ) x,
amosunwrapper.deflate( wrap.txt || chr(0), 9 ) d from wrap )
select substr( x, r * 2 - 1, 2 ) c_base64,
substr( d, r * 2 - 1, 2 ) c_translatecode from subst ,
( select rownum r from dual connect by rownum <= ( select length( x ) / 2 from subst ) );

​ 结果如下:

​ 通过对结果的排序,没有出现同一个base64编码对应不同的十六进制的情况,因此我们知道了可以用这个SQL为基础,通过用不同的串来产生替换表的内容。

3.2.3 计算替换表

​ 首先建一个IDLTRANSLATE表来存储替换表的内容,create_idltranslate.sql:

1
2
3
4
5
6
CREATE TABLE SYS.IDLTRANSLATE
(
C_BASE64DECODE VARCHAR2(2) NOT NULL,
C_LZDEFLATECODE VARCHAR2(2) NULL
)
/

​ 然后写一段PL/SQL块来生成替换表的内容,储存到IDLTRANSLATE表,generate_idltranslate.sql:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
declare
nCnt integer;
nLoop integer;
nSLoop integer;
nCharmax integer;
nCharmin integer;
vChar Varchar2(3);
cursor getchar is
with src AS ( select 'procedure '||vChar txt from dual ),
wrap as ( select src.txt , dbms_ddl.wrap( 'create ' || src.txt ) wrap from src ),
subst as (select substr( utl_encode.base64_decode( utl_raw.cast_to_raw(rtrim( substr( wrap.wrap, instr( wrap.wrap, chr( 10 ), 1, 20 ) + 1 ), chr(10) ) ) ), 41 ) x,amosunwrapper.deflate( wrap.txt || chr(0), 9 ) d from wrap )
select substr( x, r*2 - 1, 2 ) xr ,substr( d, r*2 - 1, 2 ) dr from subst , ( select rownum r from dual connect by rownum <= ( select length( x ) / 2 from subst ) );
begin
nCharmax:=97;
nCharmin:=122;
For nLoop In 97..122 Loop
For nSloop In 0..99 Loop
vChar := chr(nLoop)||to_char(nSloop);
For abc In getchar Loop
Select Count(*) Into nCnt From sys.idltranslate WHERE c_base64decode = abc.xr;
If nCnt < 1 Then
Insert INTO sys.idltranslate VALUES (abc.xr,abc.dr);
Commit;
Else
Select Count(*) Into nCnt From sys.idltranslate WHERE c_base64decode = abc.xr AND c_lzdeflatecode=abc.dr;
If nCnt < 1 Then
DBMS_OUTPUT.PUT_LINE('wrong orginal char:'||vchar||' hex base64:'||abc.xr);
End If;
End If;
End Loop;
End Loop;
End Loop;
end;
/

​ 等待运行一段时间

​ 筛选出sys.idltranslate表前10条数据:

1
select * from sys.idltranslate where rownum<=10;

​ 运行上面这段SQL大概会产生200多条记录:

1
select count(*) c_base64decode from sys.idltranslate;

​ 还未达到00-FF总共256条记录,建议替换

1
select 'procedure '||vChar txt from dual

​ 中的procedure关健字为package或者function类似的,继续运行直到替换表中有不重复的256条记录为止。有了替换表的内容,就可以unwrap出明文。

替换表的值

1
3D6585B318DBE287F152AB634BB5A05F7D687B9B24C228678ADEA4261E03EB176F343E7A3FD2A96A0FE935561FB14D1078D975F6BC4104816106F9ADD6D5297E869E79E505BA84CC6E278EB05DA8F39FD0A271B858DD2C38994C480755E4538C46B62DA5AF322240DC50C3A1258B9C16605CCFFD0C981CD4376D3C3A30E86C3147F533DA43C8E35E1994ECE6A39514E09D64FA5915C52FCABB0BDFF297BF0A76B449445A1DF0009621807F1A82394FC1A7D70DD1D8FF139370EE5BEFBE09B97772E7B254B72AC7739066200E51EDF87C8F2EF412C62B83CDACCB3BC44EC069366202AE88FCAA4208A64557D39ABDE1238D924A1189746B91FBFEC901EA1BF7CE

3.2.4 编写Unwrap PL/SQL程序

​ 写unwrap.sql程序,思路反向操作,得到密文的替换串,截取20字节后的替换串,然后查表得到LZ压缩串,再通过LZ_java包解压得到明文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
set serveroutput on;
create directory FILEPATH as 'D:\sql_files'; --创建一个路径FILEPATH
grant read,write on directory FILEPATH to sys; --给用户sys授予路径读写的权限
create or replace procedure unwrap(o in varchar,n in varchar, t in varchar)
as
vWrappedtext Varchar2(32767);
vtrimtext Varchar2(32767);
vChar Varchar2(2);
vRepchar Varchar2(2);
vLZinflatestr Varchar2(32767);
nLen Integer;
nLoop Integer;
nCnt Integer;
l_file utl_file.file_type;

type vartab is table of varchar2(2) index by varchar2(2);
mytbl vartab;
cursor getchar is select C_BASE64DECODE xr,C_LZDEFLATECODE dr from sys.idltranslate;
Begin
for i in getchar loop --sys.idltranslate表内容存到字符数组
mytbl(i.xr):=i.dr;
end loop;
vtrimtext:='';
select count(*) into nCnt from DBA_SOURCE
Where owner=o
And Name = n
And Type = t ;
if nCnt >0 and nCnt <5 then
for i in 1..nCnt loop
if i=1 then
select rtrim( substr( TEXT, instr( TEXT, chr( 10 ), 1, 20 ) + 1 ), chr(10) ) --保存去掉换行的BASE64码正文
into vLZinflatestr
from DBA_SOURCE
Where owner = o
And Name = n
And Type = t and line=i;
else
select text into vLZinflatestr
from DBA_SOURCE
Where owner = o
And Name = n
And Type=t and line=i;
end if;
vtrimtext:=vtrimtext||vLZinflatestr;
end loop;
end if;
vtrimtext:=replace(vtrimtext,chr(10),'');
nLen := Length(vtrimtext)/64 ;
vWrappedtext :='';
for i in 0..nLen loop
if i< nLen then
vWrappedtext:=vWrappedtext||utl_encode.base64_decode( utl_raw.cast_to_raw(substrb(vtrimtext,64*i+1 , 64 ))) ;
else
vWrappedtext:=vWrappedtext||utl_encode.base64_decode( utl_raw.cast_to_raw(substrb(vtrimtext,64*i+1 ))) ;
end if;
--DBMS_OUTPUT.PUT_LINE(vWrappedtext);
End Loop;
--vWrappedtext:=substr(vWrappedtext,41);
nLen := Length(vWrappedtext)/2 - 1;
vLZinflatestr :='';

For nLoop In 20..nLen Loop --从第21字节开始
vChar := Substrb(vWrappedtext,nLoop*2+1,2);
/*
Select Count(*) Into nCnt From SYS.IDLTRANSLATE Where C_BASE64DECODE=vChar;
If nCnt <> 1 Then
DBMS_OUTPUT.PUT_LINE('SUBSTATION TABLE WARNING: Count not find following char--'||vChar);
Return;
Else
Select C_LZDEFLATECODE Into vRepchar From SYS.IDLTRANSLATE Where C_BASE64DECODE=vChar;
End If;
*/
vLZinflatestr := vLZinflatestr || mytbl(vChar); --从字符数组匹配
--DBMS_OUTPUT.PUT_LINE(vLZinflatestr);
End Loop;
--DBMS_OUTPUT.PUT_LINE(vLZinflatestr);
l_file := utl_file.fopen('FILEPATH', 'unwrap_text.sql', 'W'); --给文件变量赋予一个初值unwrap_text.sql,最后一个参数W表示写入
utl_file.put_line(l_file, amosunwrapper.inflate(vLZinflatestr));
utl_file.fclose(l_file);--关闭文件
DBMS_OUTPUT.PUT_LINE(amosunwrapper.inflate(vLZinflatestr));
End;
/

4. Wrap和Unwrap过程

1.先编一个需要wrap的sql文件,test.sql:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
create or replace function test_f(hy in varchar)
return varchar
as
x varchar(2);
begin
select
case
when hy >='01' and hy<= '05' then 1
when hy >='06' and hy<= '11' then 2
when hy >='13' and hy<= '43' then 3
when hy >='44' and hy<= '46' then 4
when hy >='47' and hy<= '50' then 5
when hy >='51' and hy<= '59' then 6
when hy >='60' and hy<= '62' then 7
when hy ='63' or hy = '65' then 8
when hy >='66' and hy<= '67' then 9
when hy >='68' and hy<= '71' then 10
when hy >='72' and hy<= '72' then 11
when hy >='73' and hy<= '74' then 12
when hy >='75' and hy<= '78' then 13
when hy >='79' and hy<= '81' then 14
when hy >='82' and hy<= '83' then 15
when hy >='84' and hy<= '84' then 16
when hy >='85' and hy<= '87' then 17
when hy >='88' and hy<= '92' then 18
when hy >='93' and hy<= '98' then 19
else null
end
into x from dual;
return x;
end;
/

2.用wrap工具加密test.sql文件,在cmd里运行wrap工具

1
2
3
C:\Users\Tahir> wrap iname=C:\Users\Tahir\Desktop\instantclient_12_1\test.sql oname=C:\Users\Tahir\Desktop\instantclient_12_1\test.plb

wrap iname=C:\Users\Tahir\Desktop\instantclient_12_1\test2.sql oname=C:\Users\Tahir\Desktop\instantclient_12_1\test2.plb

3.用plb文件创建函数

4.测试函数

1
select sys.test_f('45') from dual;

5.加密后的内容

6.利用unwrap.sql进行解密

首先执行@unwrap.sql,生成unwrap储存过程

调用unwrap存储过程,对test_f进行解密

1
exec sys.unwrap('SYS','TEST_F','FUNCTION');

测试的时候,发现解压LZ会报错

1
java.util.zip.ZipException: oversubscribed dynamic bit lengths tree

java jdk版本过低,更换版本后解决,执行成功。