자세한 취약점 코드 분석
먼저 해당 취약점이 왜 발생했는지 소스코드를 좀 더 자세히 분석했다.
해당 취약점은 tls1_process_heartbeat 함수에서 발생하였으며, 인자로 들어온 s는 연결 하나? 정도로 보면 될 것 같다.
s -> s3 -> rrec 안에 패킷이 들어있으며, 구조는
SSL
└── s3
└── rrec
├── data ← 패킷 데이터
└── length ← 패킷 길이
와 같다. 함수 시작점을 보자.
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;
위 코드를 통해 데이터 첫 바이트 값을 읽어와 p 변수에 저장한 것을 볼 수 있고, 이 때 첫 바이트는 responce인지, request인지를 나타낸다. 그리고 hbtype에 *p값을 저장하고 다음 바이트로 ++해 주었으며, n2s(network to short, 2바이트) 함수를 통해 Length 2바이트를 payload 변수에 넣어주었다. pl은 payload 시작 주소를 가리킨다.
이후 if 분기가 나오게 되는데, 분기는 hbtype이 request인지, responce인지에 따라 분기가 발생한다. 우리는 Request에서 해당 문제가 발생한 것임을 알고 있으므로 Request를 보자.
unsigned char *buffer, *bp;
int r;
buffer = OPENSSL_malloc(1 + 2 + payload + padding);
bp = buffer;
*bp++ = TLS1_HB_RESPONSE;
s2n(payload, bp);
memcpy(bp, pl, payload);
bp += payload;
먼저 응답을 위한 버퍼를 만들어준다. 버퍼 크기는 1바이트(타입) + 2바이트(페이로드 길이) + 페이로드 + 패딩(16바이트)만큼 할당이 이뤄지고, bp 변수에 버퍼 시작점 주소를 둔다. 이후 1바이트에 Responce 값을 주고, 그 다음 2바이트는 s2n 함수를 통해 페이로드 길이를 2바이트만큼 채운다.
그 다음 memcpy가 문제인데, bp에 payload 길이만큼 받은 데이터인 pl을 채우는 memcpy까지는 좋다. 이 때 buffer는 payload 크기에 맞게 할당까지는 했지만, 그래서 저 실제 페이로드인 pl의 길이가 페이로드의 길이와 일치하는지 확인 없이 그대로 집어넣는 것을 볼 수 있다.
예를 들어 실제로 받은 데이터인 pl이 가리키는 실제 데이터의 크기가 1이지만, 패킷에 적힌 payload가 1000이라면, memcpy(bp, pl, payload)를 통해 값을 읽어올 때 memcpy(bp, pl(크기 1), 1000), 즉 pl이 가리키는 데이터 1 크기만큼을 읽고, 그 뒤에 999만큼 데이터를 더 읽어서 bp에 복사되는 것이다.
탐지 계획 수립
우선 Source와 Sink를 구분해보자. 해당 취약점은 공격자가 payload 길이를 조작하여 문제를 일으키는 만큼 먼저 Source는 프로그램 변수로 바꾸는 지점인 n2s(p, payload), 변수는 payload가 될 것이다. 또한 Sink는 memcpy가 될 것이다. 여기서 Sink 지점 위험 연산의 세번째 인자인 payload가 진짜 Source의 payload가 맞는지를 확인을 위한 Use-Def Chain을 구축한다. 그리고 구축한 Chain 내 P-Code 연산에 LESSEQUAL과 같은 연산이 있는지를 확인해보고자 한다.