이번엔 이전 문제에서 배웠던 mod연산과 register에 대해 인지하고 있다는 전제가 필요하며 추가적인 정보를 알려준다.
요런식으로 나와있는데 한번 읽어보고 뭔소리인가 싶었다 처음엔...
해석해보자면, 만약 y가 2의 거듭제곱이라면 (y = 2^n), x % y의 결과는 x의 하위 n 비트와 같다는 말이다. 즉, x % 256이면 결과는 x의 하위 8비트와 같으며, x % 65536이면 결과는 x의 하위 16비트와 같다.
더 자세히 설명해보자면, 이는 2의 거듭제곱으로 나눌 때 발생하는 특이한 상황인데, 예를 들어 "256(2^8)로 나누는 경우, 나머지는 항상 8비트 이하의 값이 된다"는 것은 나머지는 항상 나누는 수보다 크지 않다는 개념을 적용하여 생각해보면 매우 쉽다.
상기의 연산 방법이 일반적인 나머지 연산에 비해 빠르다고 한다.
일반적으로 나머지 연산은 단순한 나눗셈보다 더 많은 하드웨어 리소스를 소비한다고 하는데... 아마 이전에 Assembly crash에서 볼 수 있듯 레지스터의 사용이나 명령어의 수가 늘어나기에 나오는 말 같다. 하지만, 상기의 방법은 간단한 명령만으로 처리되기에 효율적이며 성능상의 이점을 가지고 있다.
위의 내용을 인지하고, 문제를 접근해보자.
mov연산만을 사용하라고 한다.
처음엔 대체 어떻게 mov 만을 사용할지 감이 안왔는데, 우선 아래의 사진을 봐야한다.
좀 더 세부적인 레지스터의 사진이다.
상기에서 언급한대로 자신의 하위비트보다 클 수 없으므로 하위비트가 나머지가 된다. 그렇기에 그 하위 비트의 값을 세팅해주면된다.
n_hdlc라는 kernel내의 driver에서 race condition이 트리거되며 모든 배포판에 있어 제공되고 load가능한 module이기에 모든 메이저 배포판들이 영향을 받았다(CONFIG_N_HDLC=m).
HDLC(High-Level Data Link Control) driver는 현재는 device간의 연결을 지원을 위해 주로 사용된다. 해당 드라이버는 data link계층의 프로토콜이며, 각 frame들을 통해 serial line들을 전송한다.
본 취약점은 2009.06.22에 처음 소개되었으며 2017.02.01에 발제자의 syzkaller에서 detect되었고, 2017.02.03에 race condition repro가 생성되었다. 최종적으로 2017.02.28에 exploit PoC를 구현하였으며 patch로 적용되었다.
(이후에 나오는 일정은 해당 취약점에 대해 처리되는 일정...)
본래 HDLC 드라이버는 selt-made singly linked lists(단일연결리스트) 로 구현되어 data를 저장하기 위한 buffer로 구현되었다. 이 때의 데이터는 line(?)을 의미한다.
debuff 변수를 통해 buffer를 가리키는 pointer를 저장하고, 해당 pointer는 이미 전송된 위치에 대해 재전송(resend)하는 경우에 사용된다고 한다.
이때까지는 괜찮았으나, 이후에 be10eb75893 commit에서 buffer flushing이 추가되었다. 그리고 이로 인하여 race condition이 발생할 여지가 생겼다.
buffer를 flushing하는 function과 sending하는 function이 특정 buffer에 대해 free할 list로 두번 지정하게 된다. 이 상황으로 인하여 사용자가 terminal을 닫을 시에 insanely wrong locking(비정상 lock)이 발생하고, n_hdlc_release()에 의해 double-free가 발생된다.
이후에 exploit 환경에 대해 나오는데 SMAP는 적용되어 있지 않고, SMEP만 적용된 linux mint 4.4.0-53 generic환경에서 수행되었다.
소개된 exploit순서는 아래와 같다.
1. Prepare N_HDLC line discipline
2. Hitting the race condition to get double-free
3. Heap spraying for turning double-free into use-after-free
4. Another heap spraying to exploit use after free
5. Heap stabilization
6. SMEP bypass (without ROP)
우선 1번 과정에 대한 diagram이다.
main logic의 line discipline에서 수행되며, master slave로 각각 생성된 terminal이 있다.
( 아마 추측하건데, tty로 전환해서 쓰는 터미널이 master terminal이고, usermode에서 직접 켜서 사용하는 terminal이 salve terminal인 듯하다..? 맞게 이해한건지 모르겠다.)
일단 GPT로 찾아보니,
'pty master'와 'pty slave'는 일반적으로 이러한 가상 터미널을 만들 때 사용되는 두 가지 연결된 파일 디스크립터를 나타냅니다. 'pty master'는 터미널 세션의 마스터 엔드(master end)를 나타내며, 실제 사용자 입력 및 출력과 연결되어 있습니다. 'pty slave'는 터미널 세션의 슬레이브 엔드(slave end)를 나타내며, 응용 프로그램이 이를 통해 실제 터미널과 상호 작
뭐 이래 말해주긴하는데... 느낌상 내가 이해한 표현이 맞는 듯하다..! 아시는 분 댓글 좀요..
line discipline은 kernel code조각이며, backspace를 수행하거나 input에 대해 echo를 하는 로직( 즉 PIO(programmed input output)에 대해 terminal에서 xterm으로 돌아가는 것을 예시로 할 수 있다)이 존재한다.
cf. terminal vs xterminal
gnome에서는 기본적으로 xterminal이 깔려있지 않지만, 다른 OS에서는 xterminal이 깔려있는 것을 볼 수 있다.
일반적으로 그냥 terminal은 text 기반 환경에서 작동하며, xterminal은x window와 함께 사용되는emulator를 의미한다.
해당 부분에 대해서는 필자는 사용해 본 적이 없어서 자세한 사항은 따로 조사해봐야겠다.
여하튼 이후에 vulnerable driver와 hdlc driver hdlc discipline을 제공하고 있으며, 이를 활용하여 exploit을 수행한다.
exploit 구현을 위해 단일 CPU를 사용하여 모든 buffer의 할당이 하나의 할당된 cache를 활용하도록 하였다.
(추측이지만, 일부러 하나의 cache에서 사용하여 racy 환경을 더 쉽게 구축하기 위해서 이렇게 구성한 듯 하다.)
이후에 pseudotermninal master/ slave 쌍을 생성한다.
ptmd = open("/dev/ptmx", O_RDWR);
pseudoterminal(이하 ptmd)이란, 가상 터미널(?) 디바이스의 한 형태로써, 프로세스간 통신, 터미널 세션 모의화, 디버깅 등의 역할을 수행한다고 한다. 아마 해당 터미널은 생성된 pty master 와 pty slave라는 두가지 연결된 fd를 사용하여 가상 터미널을 제어하고 데이터를 전송하는데 사용하기 위해 생성하는 것 같다.
이렇게 세팅하면, vulnerable module(n_hdlc.ko)는 자동으로 생성된다고 한다.
const int ldisc = N_HDLC;
ioctl(ptmd, TIOCSETD, &ldisc);
이로써 exploit을 위한 환경 구성이 대략 끝난 것 같다.
이제 위에서 언급하였듯 race condition은 sending과 flushing이 이전에 보낸 영역(?)에 대해 수행되는 경우 발생된다. 그리고 이러한 상태를 만들어주기 위하여 terminal에서 현재 출력을 일시 적으로 중단하거나 일시 정지시켰다(일반적으로 ctrl+S로 출력 중단, ctrl + Q로 출력 재시작).
ioctl(ptmd, TCXONC, TCOOFF); //suspend the pty output
write(ptmd, buf, size); //write one data buffer(saved in n_hdlc.tbuf)
전송에 실패한 buffer에 대해 재전송하기 위해 사용되는 pointer(buffer를 가리키는) 를 tbuf에 변수에 저장한다.
이제 진짜 끝난거 같다 ㅋㅋ. 그리고 exploit이 수행되는 스레드들을 모든 CPU에서 수행한다.
현재 두 스레드의 상태이다. 하나는 flush하고, 하나는 suspend(일시 출력 중지)되었다.
첫 스레드는 flush하며 iocontrol(ioctl)을 수행한다. 다른 스레드는 저장된 debuff라는 racy변수를 재전송할 것이다.
다음의 상황이다.
위의 상황은 lag(지연)을 발생시켜 exploit이 더 빨리 수행되고, race condition을 빨리 구현되게 한 모습이다.
사진처럼 flushing thread가 loop에서 spinning중일때, 즉 대기하면서 지속적으로 CPU시간을 소모할 때, 이미 다른 thread는 vulnerable driver와 통신을 이미 시작한 상태이다.
그리고, 사진은 error지점에서 둘이 동시에 tbuf 변수(n_hdlc.tbuf)를 사용하게 되며 이 때, race condition이 발생한다.
해당 영상에서 발제자가 말하길 race condition은 위와 같은 상황에서 시스템에서 발생되는 상황이며 어떠한 동작의 순서에 의존하고 비결정론적인 상황(결과를 예측하기 어려운 상황)이다. 또한, 위 상황에서 tbuf변수에 대한 결과는 어떻게 thread collision이 발생하는지에 의존한다고 한다.
(어찌보면 당연한 말이다, 충돌이 적절한 시점에 이루어져야 하니까..)
본 취약점 구현 중 지연에 대한 부분은 아래와 같다고 한다.
그리고 최대 지연시간은 50microseconds라고 한다.
이제 double free에 대한 부분이다.
아래의 도식을 따른다.
1. 객체 A를 할당한다.
2. 할당한 객체 A를 해제한다. 이 A를 두번 해제하게되어 double free 에러가 발생한다.
3. A와 같은 크기를 가진 객체 B를 할당한다. 이때, B는 A가 해제되었던 주소와 같은 위치에 들어간다. kernel allocator는
방금 해제된 영역에 대해 우선적으로 할당하려 시도한다. 가장 빠르게 access할 수 있기 때문이다.
4. 하지만 해당 위치는 double free가 발생하는 위치이고, B가 실질적으로 해제된다.
5. 이제 2번째 heap spray를 수행한다. 객체 X를 할당하는데, 이는 payload를 가지고 있으며 역시 사이즈는 B와 같기에 이전에 B가 해제되었던 위치에 들어간다.
6. 마지막으로 code가 B를 사용하려 할때, X에 담긴 paylaod가 수행된다.
정리하면 첫번째heap spray를 통해 double free취약점을 use after free취약점으로 전환하는 것이다. B를 활용함으로써 use after free취약점을 이용하는 것이다.
흠... 뭔가 두루뭉슬한데 어렴풋이 감은 온다.
이제 다시 우리가 원래 보던 버퍼쪽으로 돌아간다.
n_hdlc_buf는 kmalloc-8192 slab cache로 할당되어 있다. 그리고 해당 cache를 대상으로 double free취약점을 이용하려한다. 이제 우리는 두가지 kernel 객체를 필요로 한다. 첫번째 heap spray에 활용될 function pointer와 이 function pointer를 덮어쓰고 shell code를 수행할 payload가 필요하다.
이때, 사용할 버퍼로는 sk_buff(socket buffer)가 가장 적당하다고 한다. 우리가 exploit에 활용하고, 덮어쓸 수 있 첫번째 function pointer를 가지고 있으며, cache를 할당받을 수 있고, network frame들을 저장할 수 있는 객체로 linux kernel에서 사용된다. sk_buff는 그리고 meta정보를 저장하고 있게된다.
하지만 첫번쨰 heap spray과정이 쉬운 과정은 아니다. double free되는 부분에 있어서 n_hdlc_release()는 13개의 n_hdlc_buf 를 바로 한번에 쉬지 않고 해제한다. hdlc driver내의 free list 내에는 13개의buffer들이 존재하고, 모든 buffer는 allocator에 의해 관리 된다. 우리가 double free에 사용할 요소는 free list에 앞부분에 존재하고 원하는 요소를 바로 찾아내서 사용하기에 쉽지 않다. (하긴 어떻게 알고 바로 띡하고 찾아내냐 ㅋㅋ)
발제자는 buffer를 구분하기 위해 free되는 요소들에 대해 data를 사이사이에 추가해주려는 것 같았으나 실패했다고 한다.
슬라이드는 요로코롬..
실패 이유에 대해서는 single CPU에서는 interrupt과정이 전혀 없으므로 사이사이에 추가할 수 없다고 한다....!
결국, 해당 부분은 아마 interrupt가 사이사이에 발생될 때, data를 추가해서 구분지어야하는데, interrupt가 없으므로 추가를 못한다는 말로 정리할 수 있을 것 같다.
다시 ppt로 돌아가서, buffer에 대한 free()가 포함된 n_hdlc_release()는 kernel에서 crash를 유발하지 않는다고 한다.
이는 즉 SLUB allocator가 buffer(same address)에 대한 연속적인 free()동작에 있어서 허용을 해준다는 말이다.
(이전 ppt에서 13번 n_hdlc_buf에 대해 해제 하는 내용 상기)
그리하여 발제자는 n_hdlc_release이후에 heap spray를 수행해 두면... free list로 부터 buffer를 하나씩 가져올 수 있고,
같은 데이터를 가리키고 있는 두개의 socker buffer를 가지고 있는 경우, 둘중에 하나의 buffer를 받게 된 이후에, 남은 buffer를 user-after-free취약점을 위해 사용할 수 있기 때문이다.
double-free취약점에서 user-after-free취약점으로 이어지게 하기 위해 8KB의 사이즈를 가진 UDP packet들을 race이후에 생성하였고, kernel memory에 저장하게 하였다.
하지만 이 과정은 network packet들을 위한 socket queue의 사이즈가 제한적이기에 쉽지 않은 과정이라고 한다.
이 제한적 상황으로 인해 overflow가 발생하지 않도록 많은 socket queue들을 만들어서 저장하였다고 한다. 그리고 한 쌍의 socket buffer를 받아 use-after-free취약점을 유발하였다.
heap spray의 수행은 아래의 사진과 같다.
우선, heap spray를 위한 200개의 server socket들을 생성한 후에 전송한다. 이때, 어떤 패킷이 동일한 데이터를 가리킬 가능성이 있는지 경험적으로 안다고 한다.....(이걸 어케앎;;;)
암튼 전용 server socket으로 전송하여 use-after-free error가 발생하고, 전용 서버로부터 몇몇 패킷들을 받은 뒤, allocator의 상태를 초기의 위치(?)로 다시 돌려둔다고 한다. 아마 위의 사진처럼 한번에 너무 많은 데이터를 받아 처리할떄, slab allocator가 고갈되어 kernel crush가 발생할 수 있으므로, 이를 방지한다는 얘기인듯 싶다.
이로써 첫번째 heap spray과정이 끝났다.
이젠 한쌍의 패킷들을 받아 LPE(Local Privilege Escalation을 위한 overwriting수행한다.
heap spraying #2(사진 5 참고)는 socker buffer내의 destructor_arg를 덮어쓰기 위한 과정이다. 하지만, 다른 socket buffer를 대상으로는 할 수 없다고 한다.
그이유는 우리가 data로 덮어써야할 구조체 이후에 사진 7을 참고하여보면, skb_shared_info 구조체가 바로 나오며, skb_shared_info구조체의 descturctor_arg와 우리가 활용하는 sk_buff구조체의 .head가 둘다 시작으로부터 같은 offset에 위치하므로, 조작하지 않는다고 한다.
그니까 결국 offset이 겹쳐서 조작하기 힘드니까 sk_buff로 destructor_arg까지는 overwrite하지 않고, 다른 kernel 객체로 destructor_arg를 overwrite한다는 말이다.
이때 활용되는 것이 add_key syscall이다.
add_key syscall을 통해 payload를 커널 메모리에 저장할 수 있고, cache에 할당할 수 있음을 알게 되었다고 한다.
(상기의 heap spray 에 대한 reference 중 inflearn강의에서도 add_key syscall을 사용하시는 것이 나왔던 것으로 기억한다.)
// linux/net/core/skbuff.c in skb_release_data()
if (shinfo->tx_flags & SKBTX_DEV_ZEROCOPY) {
struct ubuf_info *uarg;
uarg = shinfo->destructor_arg;
if (uarg->callback)
uarg->callback(uarg, true);
}
위의 코드는 데이터 버퍼를 해제할 때 호출되며, 특정 조건이 충족되면 SKBTX_DEV_ZEROCOPY 플래그를 사용하여Zero-Copy 모드로 전환되어 있는 경우 호출된다. shinfo 구조체 에서 destructor_arg field를 통해 uarg 구조체를 얻는다.
이후에 callback이 수행된다.
즉, 네트워크 패킷 전송에 있어서 zero-copy 모드에서 데이터를 해제할때의 작업 수행에 대한 부분이다.
고로 두번째 heap spray는 data를 overwrite하고, 상기의 특수한 플래그를 넣어주고, destructor를 user space에 할당된 payload로 덮어 씌우는 과정이다. shell code역시 user space에 존재한다. 하단의 사진을 참고.
상기의 사진에서 destructor_arg 포인터가 ubuf_info구조체를 역참조할 경우, SMAP에 의해 막힌다.
kernel의 포인터가 user - level에 접근못하게 막는 것이 SMAP이기 때문이다.
우선 bypass이 수월하지 않다고 한다. key data의 할당은 root에 의해 조작되며 우리에게 주어진 payload의 size는 20000byte이기 때문이다.
root에 의해 조작해야하는데 어떻게 할까...? 싶다가 내용이 추후에 나오게 된다.
이 20000byte는 key syscall 수행을 고작 두번밖에 못하기에 spraying하기에는 턱없이 부족한 상태이다. 여기서 왜 두번 밖에 수행 못하냐면, 8KB 페이로드를 동시에 저장해야하기 때문이라고 한다.
(8KB에 대한 내용은 아마 이전에 다른 CVE분석에 언급되어 있을텐데.. 아마 우선 페이지가 주로 4KB이나 8KB이고, OS내에서 편의적으로 고려되는 사이즈이기에 그랬던것 같다.)
다음 슬라이드에서 좀 신기한 이야기를 한다.
heap spray의 성공은 add_key syscall의 호출여부에 의존하지 않는다고 한다. 그리하여 현재 root권한이 아닌 발제자가 add_key syscall을 수행하는 것이 실패하겠지만, 실질적으로는 payload가 kernel memory에 존재하며, socket buffer를 덮어쓸 수 있었다. 고로 호출을 실패해도 그냥 둔다.
이제 최종적인 spraying은 아래와 같다.
server socket을 대상으로 뿌릴 20개의 패킷을 만들고, 경험적으로 미루어 보았을때, 12, 13, 14, 15번째 패킷들은 각각 같은 데이터를 가리키는 상태가 될 것이라고 하였다.
12~15의 4개의 패킷들을 각각 하나씩 받아가서 overwrite을 위해 add_key syscall을 수행하게 한다. 최종적으로 recv() (==free()) 이후에 다시 초기 상태의 allocator를 저장하기 위해 추가적으로 15개의 패킷을 다른 서버로 보낸다. 이 수행은 exploit이 안정적으로 수행되게 도와주며, 방금의 15개의 패킷을 보내는 과정이 없을 시 krenel crach가 발생할 수 있다. 이유는 slab이 완전히 free되었을때, double free를 감지하기 때문에 이를 방지하고자 보내는 것이다. 이러한 기법을 "Slab exhaustion" 이라고 부른다.
정확히는 "Slab이나 다른 메모리 할당 기법에서 할당된 메모리가 과도하게 소비되어 더 이상 사용 가능한 메모리가 없는 상태" 를 가리킨다. 그리고, 이러한 상황이 발생 시, Slab allocator는 새로운 page를 메모리에 할당해준다. 그렇기에 Slab Exhaustion은 새로운 페이지에 할당되는 구조 즉, 메모리 할당이 서로 다른 페이지에서 이루어지게 되어 메모리가 인접하지 않는 구조를 유지, 를 통해, 메모리가 완전히 해제된 후에 해당 메모리를 다시 할당하는 시도를 막는다. 이는 곳, double free의 발생을 막아주는 것이다.
한마디로, 안전한 exploit 수행을 위해 메모리 할당을 통제하고, 메모리가 완전히 해제되면서도 안정적인 상태를 유지하는 데에 사용된다는 것이다.
추가적으로 더 알아보고 싶은 부분은 아래의 논문을 보면 될 듯하다. 필자는 일부분만을 보았으며 추후에 한번 읽어볼 듯 싶다. heap layout에 대한 내용이 많이 있는 듯하다.
발제자의 machine에서는 cr4레지스터의 SMEP에 해당하는 21번을 unset하는 데에 사용할 native_write_cr4()의 주소가 0x406e0이므로 해당 주소를 사용한 것이고, 결론적으로 ROP를 사용하지 않고도 SMEP를 bypass하게 된 것이다.
주솟값은 CPU에 의한 userspace와 x86의 CPU OID instruction를 통해 결정된다.
이로써 kernel에서의 exploit을 위한 환경은 완료되었고, SMEP가 disabled된 이후엔 shell code를 실행할 수 있는 두번째 race를 수행한다.
이후부턴 patch에 대한 내용으로 이어진다.
본래에 vulnerable한 buffer는 자체적으로 개발한 구조체가 아닌 standard란 linked list를 사용하고 있었고, 발제자는 race condition을 유발하는 n_hdlc.tbuf를 제거하도록 하였다. 그리고 전송에 실패하는 경우, tx_buf_list를 현재에 있는 data buffer앞에 위치하게 하여 추후에 전송하도록 만들었다고 한다.
그리하여 현재의 n_hdlc구조체가 아래와 같은 형태를 가지게 되는 것 같고, 주석을 참고하여보면 보류 중인 전송 프레임 buffer list가 구조상 앞쪽에 있다.
/**
* struct n_hdlc - per device instance data structure
* @tbusy: reentrancy flag for tx wakeup code
* @woke_up: tx wakeup needs to be run again as it was called while @tbusy
* @tx_buf_list: list of pending transmit frame buffers
* @rx_buf_list: list of received frame buffers
* @tx_free_buf_list: list unused transmit frame buffers
* @rx_free_buf_list: list unused received frame buffers
*/
struct n_hdlc {
bool tbusy;
bool woke_up;
struct n_hdlc_buf_list tx_buf_list;
struct n_hdlc_buf_list rx_buf_list;
struct n_hdlc_buf_list tx_free_buf_list;
struct n_hdlc_buf_list rx_free_buf_list;
struct work_struct write_work;
struct tty_struct *tty_for_write_work;
};
이젠 SLUB에 대한 patch이다.
이전에 잠시 언급되었듯 glibc에 의한 'double free or corruption(fasttop)' security check을 피하기위해 같은 객체에 대해 진행되는 두 free 사이에 다른 청크가 free된다.
a = malloc(10); // 0xa04010
b = malloc(10); // 0xa04030
c = malloc(10); // 0xa04050
free(a);
free(b); // To bypass "double free or corruption (fasttop)" check
free(a); // Double Free
d = malloc(10); // 0xa04010
e = malloc(10); // 0xa04030
f = malloc(10); // 0xa04010 - Same as 'd'
Q&A(특히나 번역도 빡세고, 여기서 나온 키워드들을 기반으로 더 searching이 이루어져야겠다.)
Q: 해당 컨퍼런스가 발표되는 시점에서 좀전에 언급된 보호 기법이 mainline에 적용되지 않아있는데 이유는?
A: 이미 slub debug의 특징에는 비슷한 보호기법이 적용되어 있다고 한다. 그렇기에 중복되는 보호기법을 적용하고 싶지 않아하는 상황이지만, default kernel은 이러한 보호기법이 걸려있지 않다고 한다. 그렇기에 allocator는 double free에 대해 용한다.
또한, 다른 솔루션으로 제시된 것은 SLAB_FREELIST_HARDENED라는 kernel config option이다. 이 옵션은 freelist내부의 item들의 주소를 랜덤하게 바꿔주는 옵션이다. 이는 공격자가 heapoverflow exploit을 통해 kernel내의 포인터를 덮어서 다음 freelist의 포인터를 조작하려는 것을 어렵게 만든다. 공격자는 이 때, 고유한 장소에 저장되어 있는 쿠키를 통하여 유추해 내야하며, 발제자가 제시한 보호기법은 이 옵션에 포함될 것이라고 한다.
(글을 작성하는 현시점에는 slub allocator가 채택되어 default allocator로 사용되고 있다.)
다음질문은 architecture 자체에 대한 내용인데, 말이 상당히 빨랐어서 간단히만 얘기하면 user space와 kernel space가 mapping되어 있지 않아도 본 exploit이 수행될 수 있는지 여부를 묻는 것 같다. 답변으로는 발제자의 exploit은 userspace단에서 동작하는 프로그램이며 몇몇 systemcall을 수행하기에 kernel은 현재 실행되는 프로세스에 대한 정보를 알아야한다고는 하는데....
아직 더 알아봐야하는 부분인 듯하다.
나머지 질문들은 약간 여담의 느낌이라 한번 스윽 들어보는 정도여도 괜찮을 듯 싶다.
위양성에 대한 얘기도 나오는 것 같고...thread sanitizer에 대해서도 언급하고.. 그외 등등...
그리고 중간에 segmentation이라는 말이 있을텐데, 이는 메모리 접근 방식에서의 segmant 방식을 의미할 것이다.
회사다니면 거진 2주간 짬짬 열심히 작성하였고 많은 공부가 된 컨퍼런스였다.
띄엄띄엄 써서 글이 자유분방하지만... 여기서 얻은 키워드들을 바탕으로 더 공부해 나아가야겠다는 생각이 든다
Syzkaller를 활용한 fuzzing에 대한 내용은 언젠가 따로 해둘 것이며 본 내용은 code coverage에 대해 아래의 reference들을 통해 정리 및 번역해둔 것이다.
[+] 아직 지식이 많이 부족하기에 글의 오류가 있다면 지적에 주시면 감사하겠습니다( _ _ ) .
[+] 더욱 알아가며 내용을 보충해 보겠습니다.
Kernel fuzzing을 수행해보면, dashboard에서 아래와 같이 많은데 coverage에 대해서 본 글에서는 정리할 것이다.
기본적인 상태에서는 filtered coverage가 안뜨고
아래와 같이 customizing했을때, filtered coverage가 뜬다. 그리고 filtered coverage의 파일쪽으로 control flow graph(이하 cfg) 가 형성되는 것을 볼 수 있다.
본인의 경우 특정 1-day보고서를 작성하며 fuzzing을 통해 생성되는 report를 직접 눈으로 확인해보고 싶어서 위와 같이 구성하여 시도해보는 중이었다.
하지만, 이러한 목적성 외에도 linux kernel내의 cfg에 대한 이해도가 높다거나, 본인처럼 관심있는 특정 서브시스템 같은 것이 있다면 그 쪽에 대한 contributing을 목적으로 fuzzing을 수행하기 위해 사용하기에도 용이할 것 같다는 생각이 든다.
자 이제 저 많은 dashboard 중 coverage가 뭐냐! 라고하면 주로, 정적 분석 도구에서 사용되며 코드를 어디어디를 얼마나 순회했는지 체크할 수 있는 지표(?)라고 표현할 수 있을 것 같다.
이제보니 위키피디아에도 다음과 같이 나와있다.
In computer science, test coverage is a measure used to describe the degree to which the source code of a program is executed when a particular test suite runs. A program with high test coverage, measured as a percentage, has had more of its source code executed during testing, which suggests it has a lower chance of containing undetected software bugs compared to a program with low test coverage. - wikipedia
뭐.. 내가 한말이랑 같은 말이다 ㅋㅋ
소프트웨어의 코드가 얼마나 실행되었는지 수치화시킨것이다.
그렇다면 Code Coverage는 대체 무엇을 기준으로 지표를 생성할까?
구문(statement), 조건(condition), 결정(decision)의 구조로 코드가 존재할 때, 이를 얼마나 순회(커버(?))했는가에 기반하여 측정된다.
구문(statement)
- 코드 한줄이 한번이상 실행된다면 충족된다.( linux kernel의 pipe.c파일을 예시로 설명)
왜냐하면 조건 커버리지를 만족해도, 구문 커버리지 혹은 이후에 설명할 결정 커버리지를 만족하지 못하는 경우가 존재하기에 이에 대한 예시로는 본인이 사용한 code가 적절하지 않아서이다. 이를 위한 예시로는 사용하신 x>0 && y<0이러한 것처럼, 조건이 두개 이상 존재하는 code가 더 적절해 보인다.
위의 코드와 위에서 언급했던 (1), (2)예시의 경우 조건식에 대해 true혹은 false가 존재하므로, 결정 coverage를 충족하는 code라고 볼 수 있다.
언급한 3가지 code coverage구문 중에서는 구문 커버리지가 가장 대표적으로 많이 사용되며, 조건문이 존재하지 않는 경우, 결정/조건 coverage 의 대상에서 제외된다고 한다. 또한, 라인 coverage가 가장 많이 사용되는 이유는 모든 code에 대해 실행된다는 의미이며 물론, 모든 시나리오를 순회한다는 보장은 없지만, 어떠한 시나리오에도 문제없음은 보장될 수 있기 때문이다.
syzkaller에서는 이러한 coverage를 표현하기 위해 KCOV를 활용한다.
해당 옵션을 사용하기 위해서는 커널을 구성할 때, 아래의 문장을 넣어주어야 한다.
CONFIG_KCOV=y
또한, syzkaller은 sanitizer coverage도 같이 활용하는데, 해당 부분에 있어서는 아직 더 공부가 필요하여, 참고용 링크를 남겨둔다.
이제 다시 돌아와서 얘기해보면 syzkaller는 우선, compiler에 의해 object code(compiler나 assembler에 의해 컴파일 이후에 생성된 결과물) 룰 기반으로 coverage point들을 넣어 tracing한다. 이 coverage point는 basic block혹은 cfg의 edge(cfg의 마지막 부분)를 기반으로 생성된다.
[+] basic block은 본인의 이해 방식으로는 entry point외에는 들어오는 분기(branch)가 없으며, exit(return)외에는 분기가 없는 쭈우우욱 실행되는 코드 문자열이라고 보면된다.
[+] gcc의 경우 default가 basic block, clang/Linux의 경우 defualt가 cfg edge이다.
여하튼, 이 coverage point들은 compiler에 의해 많은 변환 및 최적화 과정을 거친 후 삽입된다. 그리고 이는 당연하게도 소스코드와 많은 연관성을 가진다. 그리고 아래와 같은 모습으로 나타난 것을 볼 수 있다.
횟수야 뭐.. 아래의 글을 참고하여 code를 보면 어느정도 알 수 있겠지만, 사실 색 표시에 대한 정리를 위해 이 글을 작성하게 되었다.
해당 라인이 coverage 를 만족한다는 의미(?)인 듯 하다. 왼쪽에 해당 줄과 관련된 pc값의 실행을 trigger한 프로그램이 몇개 있는지 보여준다. 그리고, 번호를 누를 시에 마지막으로 실행된 프로그램이 열린다.
2. 주황(Uncover && Cover)
몇몇의 PC값이 해당 라인과 연관있으며 모든 경우에 대해 커버리지를 만족하지는 못한다는 의미인 듯 하다. 즉, coverage를 만족하는 것과, 만족하지 못하는 것이 둘다 있는 경우를 뜻한다고 보면된다. 해당 색 역시 번호를 누를 경우 마지막으로 trigger할때 수행된 PC값들을 볼 수 있다. (block단위의 coveragepoint기준)
3. 크림슨 레드(빨강이지만 볼드체가 아닌것) (Weak- uncovered)
이 색의 경우 함수(symbol)가 어떠한 coverage를 만족하지 못한다는 의미이다. 즉 , 함수가 실행되지 않는다는 의미이다.
하지만, 해당 라인은 컴파일 시 활용되는 부분이라고 볼 수 있다.
4. 빨강(볼드체) (Uncovered)
해당 라인이 coverage를 어떠한 것(라인에 대해/ 함수에 대해)도 전혀 만족하지 못한다.
5. 회색 ( Not instrumented)
해당 라인과 연결된 PC값을 전혀 확인할 수 없거나, code를 생성하지 않을 때 나타난다고 하는데, 이는 본인 생각에 전혀 callback(다른 함수의 실행 이후 실행되는 함수)이 이루어지지 않는 code들을 의미하는 것 같다.