应用程序中难免会遇到数字的处理,针对数字的处理如果不当也会造成严重的问题,著名的Heartbleed漏洞也是没有验证数字的有效性导致的。
数值处理不好的,轻则产生异常,重则影响整个程序的正常运行,因此,针对由数值导致的问题也不能掉以轻心。主要问题总结为以下几点:
1) 异常
当从请求中获取参数的值需要转成数字时,这是就可能产生异常,产生异常的原因:一、内容符合数值的要求,含有异常字符;二、数值超出了数值类型的范围。如一下代码:
public static void main(String[] args) {
int value = Integer.parseInt("9147483647");
System.out.println("the int value is"+value);
}
当数值超过类型的最大值时,就会抛出异常信息如下:
同样,当数值含有不合法的字符时,抛出的异常也是一样的:
因此再接受数值的数据时,需要注意捕捉NumberFormatException这个异常,以防,程序在出异常后,直接跳出程序。
2) DOS
当数值被用于创建或者申请资源时,这时就需要注意验证数值的有效范围,例如:当一个数值是从客户端的搜索页面传递而来的,这个数值是用于指示搜索的一个页面显示多少条记录,通常这个数值的是几个常用的数字【10,50,100】;,但是当这个数值被修改为10000或者更大的数字而且符合条件的数据又恰好很多时,服务器如果直接根据这个数值去数据库查询所有记录,并组装成响应消息返回,就会消耗很多资源,特别是内存资源。曾经在渗透的测试过程中,遇到过一个页面通过查询返回了近7000条数据,而且花了很长时间页面才显示正常。
在使用C或者C++时,如果使用攻击者可以控制的数字来分配内存,就会直接导致更严重的问题,例如:内存溢出、分配内存失败或者读内存失败导致的程序运行异常,直接可以导致程序中断运行直接退出。
3) 信息泄露
Heartbleed漏洞就是典型的信息泄露的最好的例子,由于在读取内存时,没有判断数值的有效范围,导致在复制内存时,复制了超越实际范围的内存,使得攻击者可以通过此漏洞读取服务器端内存的内容,如果内存中含有用户登录相关的信息,攻击者就可以直接获取登录的会话信息劫持会话。HeartBleed的实例代码如下:
int dtls1_process_heartbeat(SSL *s)
{
unsigned char *p = &s->s3->rrec.data[0], *pl;
unsigned short hbtype;
unsigned int payload;
unsigned int padding = 16; /* Use minimum padding */
/* Read type and payload length first */
hbtype = *p++;
n2s(p, payload);
pl = p;
//do something with the payload
if (s->msg_callback)
s->msg_callback(0, s->version, TLS1_RT_HEARTBEAT,
&s->s3->rrec.data[0], s->s3->rrec.length,
s, s->msg_callback_arg);
if (hbtype == TLS1_HB_REQUEST)
{
unsigned char *buffer, *bp;
int r;
/* Allocate memory for the response, size is 1 byte
* message type, plus 2 bytes payload length, plus
* payload, plus padding
*/
buffer = OPENSSL_malloc(1 + 2 + payload + padding);
//allocate all that memory without any checks
bp = buffer;
/* Enter response type, length and copy payload */
*bp++ = TLS1_HB_RESPONSE;
s2n(payload, bp);
memcpy(bp, pl, payload);
bp += payload;
/* Random padding */
RAND_pseudo_bytes(bp, padding);
r = dtls1_write_bytes(s, TLS1_RT_HEARTBEAT, buffer, 3 + payload + padding);
//send the response back, even the stuff the attacker wasn't supposed to see
if (r >= 0 && s->msg_callback)
s->msg_callback(1, s->version, TLS1_RT_HEARTBEAT,
buffer, 3 + payload + padding,
s, s->msg_callback_arg);
OPENSSL_free(buffer);
if (r tlsext_hb_seq)
{
dtls1_stop_timer(s);
s->tlsext_hb_seq++;
s->tlsext_hb_pending = 0;
}
}
return 0;
}
从代码中可以看到从SSL中获取payload之后,没有做任何验证,直接用于:buffer = OPENSSL_malloc(1 + 2 + payload + padding)分配内存,并且用于复制内存。修改之后的代码如下:
int dtls1_process_heartbeat(SSL *s)
{
unsigned char *p = &s->s3->rrec.data[0], *pl;
unsigned short hbtype;
unsigned int payload;
unsigned int padding = 16; /* Use minimum padding */
if (s->msg_callback)
s->msg_callback(0, s->version, TLS1_RT_HEARTBEAT,
&s->s3->rrec.data[0], s->s3->rrec.length,
s, s->msg_callback_arg);
/* Read type and payload length first */
if (1 + 2 + 16 > s->s3->rrec.length)
return 0; /* silently discard */
hbtype = *p++;
n2s(p, payload);
if (1 + 2 + payload + 16 > s->s3->rrec.length)
return 0; /* silently discard per RFC 6520 sec. 4 */
pl = p;
if (hbtype == TLS1_HB_REQUEST)
{
unsigned char *buffer, *bp;
unsigned int write_length = 1 /* heartbeat type */ +
2 /* heartbeat length */ +
payload + padding;
int r;
if (write_length > SSL3_RT_MAX_PLAIN_LENGTH)
return 0;
/* Allocate memory for the response, size is 1 byte
* message type, plus 2 bytes payload length, plus
* payload, plus padding
*/
buffer = OPENSSL_malloc(write_length);
bp = buffer;
/* Enter response type, length and copy payload */
*bp++ = TLS1_HB_RESPONSE;
s2n(payload, bp);
memcpy(bp, pl, payload);
bp += payload;
/* Random padding */
RAND_pseudo_bytes(bp, padding);
r = dtls1_write_bytes(s, TLS1_RT_HEARTBEAT, buffer, write_length);
if (r >= 0 && s->msg_callback)
s->msg_callback(1, s->version, TLS1_RT_HEARTBEAT,
buffer, write_length,
s, s->msg_callback_arg);
OPENSSL_free(buffer);
if (r tlsext_hb_seq)
{
dtls1_stop_timer(s);
s->tlsext_hb_seq++;
s->tlsext_hb_pending = 0;
}
}
return 0;
}
在if
(1 + 2 + payload + 16 > s->s3->rrec.length)队payload增加了检查。
当应用程序因为数字处理不当时导致异常被是直接回显到页面时,也会导致信息泄露【可以参考应用安全系列之三十一:信息泄露_jimmyleeee的博客-CSDN博客】。
4) 除零操作
如果进行除法或者取余操作时,没有判断数值的有效性,会导致除零异常。在Java如果有除零发生时,抛出的异常如下:
有的语言也可能会导致程序崩溃,进而也导致DOS攻击的发生。
5) 溢出
数值操作最容易犯的错误还是溢出的问题,无论使用任何语言,都会有堆数值类型的加减乘除操作,在操作时,如果没有类型使用不当可能会导致数值溢出的问题发生。如下Java代码:
public static void main(String[] args) {
int overflow = Integer.MAX_VALUE + 10;
System.out.println("the overflow value is"+overflow);
}
此程序运行的结果是:
可以看出显示的结果是一个负的值。在C、C++语言中,因为有unsigned int无符号整型,结果值就会是一个很大的值,针对无符号整型,还会产生环绕问题,就是溢出之后,变成0或者一个很小的数值,这个值一旦用于用于分配资源操作或者作为业务逻辑的参数,就会导致结果严重偏离实际的操作。
提别是进行减法运算时,如果没有检查减数的合法性,更容易导致溢出问题的发生,可以根据需求转成加法操作,例如,需要判断a-b是否大于c,可以改成a是否大于b+c等。
对于Java语言而言每种类型的定义里都有一个最大值和最小值,可以使用此最大值来判断数值是否可能会产生溢出:
在数值的实际操作时,也需要根据类型操作的结果的可能范围,将结果的类型设置为一个更大的范围的类型,就会避免溢出的发生。例如:两个int相加的结果,类型设置为long型,操作的结果可能超出int型的最大值,但是,绝对不可能超出long型的范围。
6) 截断
当一个高精度的数值存入到低精度的数值时可能会导致数值被截断,截断之后的值可能会变小,也可能会变成负值,这需要根据被赋值变量的类型而定。示例如下:
public static void main(String[] args) {
long value = Long.MAX_VALUE;
System.out.println("before cut off value is"+value);
int cutoff = (int) value;
System.out.println("After cut off value is"+cutoff);
}
运行结果显示如下:
由上可见,数值如果处理不好对系统影响也非常大,因此,在数值处理时需要记住以下几点,以防数值操作导致结果不符合预期:
- 在使用数值之前判断其是否在预期的合法范围之内;
- 避免将高精度类型转换成低精度类型,大范围的数值类型转成小范围的数值类型;
- 定义变量时,根据可能的取值范围选择合适的类型;【也不是越大越好,越大占用的内存就大,计算时消耗的CPU资源也大】;
- 在进行计算时,尝试使用不容易出错的运算符,例如:将减法转换成加法;