redis6.0.5之ziplist一段代码的理解--重现一个fix的bug

在阅读ziplist的函数unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) 时候,
其中有如下的代码段
。。。
if (nextdiff == -4 && reqlen < 4) {
nextdiff = 0;
forcelarge = 1; 所以需要强制使用大编码空间,保证新的列表总体长度不缩小
}
。。。
初始不能理解,后来查了相关资料
https://github.com/redis/redis/commit/c495d095ae495ea5253443ee4562aaa30681a854#diff-3cee7e9c2084403adfa8843617f87099444c7ef8aa2ad69f397013b11794589c
原文如下:

Ziplist: insertion bug under particular conditions fixed.
压缩列表: 修复了一个 在特定条件下插入的bug

Ziplists had a bug that was discovered while investigating a different
issue, resulting in a corrupted ziplist representation, and a likely
segmentation foult and/or data corruption of the last element of the
ziplist, once the ziplist is accessed again.
在调查压缩列表一个bug的时候发现了另外一个不同的问题,会导致压缩列表的奔溃,
当再次访问压缩列表的时候,压缩列表最后一个实体会出现段错误或者数据奔溃。

The bug happens when a specific set of insertions / deletions is
performed so that an entry is encoded to have a "prevlen" field (the
length of the previous entry) of 5 bytes but with a count that could be
encoded in a "prevlen" field of a since byte. This could happen when the
"cascading update" process called by ziplistInsert()/ziplistDelete() in
certain contitious forces the prevlen to be bigger than necessary in
order to avoid too much data moving around.
这个bug发生的原因在于 一些列的插入和删除导致了一个实体中的prevlen字段,
本来可以用一个字节存储的,但是实际上用了5个字节。
这种情况实际发生在函数ziplistInsert()/ziplistDelete()调用级联更新的时候,
在特定的条件下导致prevlen字段变的比需要的空间大,这个是为了避免移动更多的数据(提高性能)

Once such an entry is generated, inserting a very small entry
immediately before it will result in a resizing of the ziplist for a
count smaller than the current ziplist length (which is a violation,
inserting code expects the ziplist to get bigger actually). So an FF
byte is inserted in a misplaced position. Moreover a realloc() is
performed with a count smaller than the ziplist current length so the
final bytes could be trashed as well.
当这样一个实体产生了,立即插入一个非常小的实体在它之前将导致压缩列表的空间重新分配,
这个时候总的空间反而笔当前的压缩列表小了(这是一种不允许的操作,插入操作期望压缩列表实际变得更大)。
所以导致了结尾符号FF被插入到错误的地方。此外,执行函数realloc导致了比现在压缩列必报更小的空间,
以至于最后的字节被当垃圾丢弃。

SECURITY IMPLICATIONS: 对安全的影响
Currently it looks like an attacker can only crash a Redis server by
providing specifically choosen commands. However a FF byte is written
and there are other memory operations that depend on a wrong count, so
even if it is not immediately apparent how to mount an attack in order
to execute code remotely, it is not impossible at all that this could be
done. Attacks always get better... and we did not spent enough time in
order to think how to exploit this issue, but security researchers
or malicious attackers could.
目前看来一个攻击者通过提供特定选择过的命令只能导致redis服务器奔溃。
但是一个FF字节被写入,还有另外基于错误计数的内存操作.
所以即使它不是立刻可以看出如何利用远程代码开始攻击,也并非完全不可能做到这一点,
因为攻击者总是做得更好。。。我们没有花费更多的时间去思考如何利用这个漏洞,
但是安全研究者或者黑客可以
***************************************************************************

明白了其中的原委,昨天就想着今天抽时间来重现这个bug,构思重现这个bug如下

第一步
我们构建这样一个压缩列表
先插入5个实体 分别是 字符串c3 "3" 一个长900的字符串c 一个长249的字符串cbug 字符串c2 "2" 字符串c1 "1" ,如下所示
"3"->一个长900的字符串c->一个长249的字符串cbug ->"2" -> "1"
这个时候 一个长249的字符串cbug 的实体长度为 5 + 2 + 249 = 256 大于254
所以 接下来的 实体字符串c2 "2" 所在的表示前一个实体长度的编码为 5个字节

第二步
删除 一个长900的字符串c 压缩列表变为
"3"->一个长249的字符串cbug ->"2" -> "1"
这个时候 一个长249的字符串cbug 的长度变为 1+2+249 = 252,
本来252这个长度是可以用一个字节表示的,但是为了性能,
保持了实体字符串c2中前一个实体长度编码原来的长度, 所以仍然为5个字节

第三步
在 实体字符串c2 "2" 前面 插入一个实体 字符串c4 "4"
压缩列表变为
"3"->一个长249的字符串cbug ->"4" -> "2" -> "1"
在这个过程中,因为 实体字符串c2 会收缩4个字节,
如果这个时候 新插入的实体 长度小于4,我们这里为2,
那么整个压缩列表的长度反而是缩小,导致结尾符号错乱。
就在如下的代码中函数ziplistResize
/* Store offset because a realloc may change the address of zl. */
offset = p-zl;
zl = ziplistResize(zl,curlen+reqlen+nextdiff); 这里
p = zl+offset;
。。。。
/* Resize the ziplist. */
unsigned char *ziplistResize(unsigned char *zl, unsigned int len) {
zl = zrealloc(zl,len);
ZIPLIST_BYTES(zl) = intrev32ifbe(len);
zl[len-1] = ZIP_END; 这里位置错误了,丢弃了需要的字节
return zl;
}

至此已经fix的bug重现完成。产生的主要原因在于为了性能,保留了多余的空间,
插入的时候恰好又需要去除, 新的实体长度过小导致总长度缩小

下面是具体的测试代码

****************************************************************************
...
    unsigned char *zl = ziplistNew();  //新建一个压缩列表
    unsigned char *p;

    int where = 1; //从尾部开始插入实体
    p = (where == ZIPLIST_HEAD) ? ZIPLIST_ENTRY_HEAD(zl) : ZIPLIST_ENTRY_END(zl);
    printf("length p %p\n",p);
    
    char *c1 = "1"; //字符1可以当做整数处理,编码后为 f1 +1 = f2
    __ziplistInsert(zl,p,c1,strlen(c1));
    
    char *c2 = "2"; //字符2可以当做整数处理,编码后为 f1 +2 = f3
    __ziplistInsert(zl,p,c2,strlen(c2));
    
    char *cbug = "Ziplists had a bug that was discovered while investigating a different \
issue, resulting in a corrupted ziplist representation, and a likely \
segmentation foult &/| data corruption of the last element of the \
ziplist, once the ziplist is accessed again"; 
//长度249
    __ziplistInsert(zl,p,cbug,strlen(cbug));

    char *c = "The bug happens when a specific set of insertions / deletions is \
performed so that an entry is encoded to have a prevlen field (the \
length of the previous entry) of 5 bytes but with a count that could be \
encoded in a prevlen field of a since byte. This could happen when the \
cascading update process called by ziplistInsert()/ziplistDelete() in \
certain contitious forces the prevlen to be bigger than necessary in \
order to avoid too much data moving around. \
Once such an entry is generated, inserting a very small entry \
immediately before it will result in a resizing of the ziplist for a \
count smaller than the current ziplist length (which is a violation, \
inserting code expects the ziplist to get bigger actually). So an FF \
byte is inserted in a misplaced position. Moreover a realloc() is \
performed with a count smaller than the ziplist current length so the \
final bytes could be trashed as well.";     
//长度900
     __ziplistInsert(zl,p,c,strlen(c));
    
     char *c3 = "3"; //字符3可以当做整数处理,编码后为 f1 +3 = f4
    __ziplistInsert(zl,p,c3,strlen(c3));
    
    int totallegnth = ZIPLIST_BYTES(zl);
     for(int i=0; i<totallegnth; i++ ){
        printf("p+%d %x ",i,*(zl+i));  
     }
     printf("\n");
//到这里为止 我们总共插入了5个实体  第一个是字符串1 第二个字符串2 第三个引发bug的字符串 第四个是这个bug的说明字符串  第五个是字符串3
//因为是尾部插入,所以注意顺序相反
//p+0 9c p+1 4 p+2 0 p+3 0 p+4 99 p+5 4 p+6 0 p+7 0 p+8 5 p+9 0 p+10 0 p+11 f4 p+12 2 p+13 43 p+14 84 p+15 54 p+16 68 p+17 65 p+18 20... //这里的 p+0 9c p+1 4 p+2 0 p+3 0 表示为压缩列表总长度 为 4*256+9*16+12=1024+144+12=1180 共计1180个字节长度 //p+4 99 p+5 4 p+6 0 p+7 0 为 对结尾最后一个实体开始位置的偏移量 4*256 + 9*16 + 9 = 1117 //p+8 5 p+9 0 总共5个实体 // p+10 0 p+11 f4 第一实体是字符3,可以转化为数字保存 结果为 f1 +3 = f4 //p+12 2 表示前一个实体的长度为2 ,用一个字节表示 //第二个实体是 是解释这个bug的长字符串,共900个字符 .900 转二进制为 0000 0011 1000 0100 ->03 84 //p+13 43 p+14 84 表示长度的编码 因为900小于16383 可以用两个字节表示 |01pppppp|qqqqqqqq|,加上上面的值就是 43 84 //p+15 54 p+16 68 p+17 65 p+18 20 ... 这里开始就是具体的字符串 The 。。了 // ...p+911 65 p+912 6c p+913 6c p+914 2e p+915 fe p+916 87 p+917 3 p+918 0 p+919 0 p+920 40 p+921 f9 p+922 5a p+923 69 p+924 70... //p+911 65 p+912 6c p+913 6c p+914 2e 表示第二个实体的结尾ell. // p+915 fe p+916 87 p+917 3 p+918 0 p+919 0 表表示前一个实体的长度 3*256 + 8*16+ 7 = 768 + 128 +7 = 903 //前一个实体的长度 为 1 + 2 + 900 = 903 得到验证 //第三个实体是cbug字符串 长度为252,因为252小于16383 可以用两个字节表示 |01pppppp|qqqqqqqq| //p+920 40 p+921 f9 所以编码为 0100 0000 1111 1001 即为 40 f9 //p+922 5a p+923 69 p+924 70 。。。 就是cbug字符串的开始 Zip。。。 //。。。p+1168 61 p+1169 69 p+1170 6e p+1171 fe p+1172 0 p+1173 1 p+1174 0 p+1175 0 p+1176 f3 p+1177 6 p+1178 f2 p+1179 ff //。。。p+1171 61 p+1172 69 p+1173 6e 这里是cbug字符串结尾 。。。ain //第四个实体 // p+1174 fe p+1175 3 p+1176 1 p+1177 0 p+1178 0 前一个实体的长度 fe表示是大长度 1*256+0=256 //前一个实体的长度 为 5 + 2 + 249 = 256 得到验证 // p+920 f2 第三(最后一个)实体是字符1,可以转化为数字保存 结果为 f1 +2 = f3 // 第五个实体即最后一个实体 p+1177 6 p+1178 f2 长度为 5+1 =6 ,编码和值在一起 f2= f1 + 1 //接下来我们把中间的元素删除 p = zl+12; //把指向中间的实体开始位置 zl = __ziplistDelete(zl,p,1); totallegnth = ZIPLIST_BYTES(zl); for(int i=0; i<totallegnth; i++ ){ printf("p+%d %x ",i,*(zl+i)); } printf("\n"); //p+0 11 p+1 1 p+2 0 p+3 0|p+4 e p+5 1 p+6 0 p+7 0|p+8 4 p+9 0 |p+10 0 p+11 f4 |p+12 2 p+13 40 p+14 f9 p+15 5a p+16 69 p+17 70... //...p+261 61 p+262 69 p+263 6e p+264 fe p+265 fc p+266 0 p+267 0 p+268 0 p+269 f3 p+270 6 p+271 f2 p+272 ff //总长1*256+16+1=273 | 最后一个实体位置的偏移量1*256+14=270 | 总共4个实体 | 第一个实体3 |第二个实体是cbug ... //...p+264 fe p+265 fc p+266 0 p+267 0 p+268 0 p+269 f3 // 第三个实体 15*16+12 = 252 = 1+ 2+ 249 这里本来可以缩小,但是为了性能,保持原样大小5个字节 //现在我们插入一个实体,让这个实体的总长度小于4个字节, 不妨插入字符串"4" char *c4 = "4"; //字符4可以当做整数处理,编码后为 f1 +4 = f5 p = zl+264; //把指向这个本可以缩小但是为了性能保留原来空间的实体 __ziplistInsert(zl,p,c4,strlen(c4)); //在这个实体前面插入一个长度小于4的实体 //这个时候满足条件 nextdiff == -4 && reqlen = 4 ,就触发了这个bug return 0; } *********************************************************************************

下面是具体调试的过程中进入了这个bug的fix,证明我们重现了bug

Breakpoint 1, __ziplistInsert (zl=0x40e010 "\021\001", p=0x40e118 "\376", <incomplete sequence \374>, s=0x40a478 "4", slen=1) at ziplist.c:784
784 nextdiff = 0;
(gdb) l
779 * make sure that the next entry can hold this entry's length in
780 * its prevlen field. */
781 int forcelarge = 0;
782 nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : 0;
783 if (nextdiff == -4 && reqlen < 4) {
784 nextdiff = 0;
785 forcelarge = 1;
786 }
787
788 /* Store offset because a realloc may change the address of zl. */
(gdb) p nextdiff
$3 = -4
(gdb) n
785 forcelarge = 1;
(gdb) p reqlen
$4 = 2

*********************************************************************************

posted on 2021-01-21 17:06  子虚乌有  阅读(202)  评论(0)    收藏  举报