바이너리 패치
개요
바이너리 패치의 의미와 방법을 살펴보고, 코드 수정과 삽입을 통해 취약점을 패치해 보겠습니다.
바이너리 패치
바이너리 패치(binary patching)란 바이너리의 내용을 수정하여 다르게 동작하도록 하는 과정을 의미합니다. 바이너리 패치는 소스 코드의 확보가 제한되는 바이너리를 대상으로 분석이나 연구, 또는 취약점을 보완할 필요가 있는 상황에서 유용합니다. 예를 들어 사물인터넷(IoT) 장치의 펌웨어에서 제조사가 패치하지 못한 취약점을 자체적으로 보완하여 사용하거나, 디버거를 탐지하면 종료하는 등 분석을 방해하는 악성 코드의 기능을 무력화하기 위해 바이너리 패치 기술을 사용할 수 있습니다.
방법론
바이너리 패치는 바이너리의 동작을 얼마나 변형할 것인지에 따라 다양한 방법을 사용할 수 있습니다. 아주 사소한 버그를 수정하거나, 일부 코드를 무력화하는 등 기존의 동작을 조금 변형하는 정도라면 헥스 에디터를 이용하여 해당하는 인스트럭션을 수정하는 것으로도 충분합니다. 그러나 특정 함수의 동작 자체를 수정하거나, 원하는 코드를 추가적으로 실행하는 등 변형의 정도가 큰 경우에는 코드를 삽입하기 위한 다양한 방법을 사용해야 합니다.
이 글에서는 예제 바이너리에 존재하는 두 가지 취약점을 패치하는 상황을 가정하고, 헥스 에디터를 이용한 간단한 수정과 코드를 삽입하여 패치하는 방법을 모두 연습해 보겠습니다. 실습에 사용할 바이너리는 다음 링크에서 내려받을 수 있습니다.
example.zip
간단한 수정의 경우
바이너리의 동작을 조금만 변형하는 간단한 수정의 경우, 헥스 에디터를 사용하는 방법이 직관적이고 간편합니다. 헥스 에디터를 사용하는 방법은 기초적인 도구만으로도 간편하게 패치를 할 수 있다는 장점이 있습니다. 패치의 과정은 먼저 디스어셈블러를 이용해 패치할 코드와 위치를 확인한 후, 헥스 에디터로 바이너리를 열고 해당 위치의 인스트럭션을 원하는 인스트럭션으로 덮어씁니다. 아래 그림은 헥스 에디터를 사용하여 특정 함수의 호출을 무력화하는 간단한 수정의 예시를 나타내고 있습니다.
다만 이 방법은 패치할 부분을 제외한 나머지 바이트들이 제자리에 위치한 상태에서만 수정이 가능하다는 분명한 한계가 있습니다. 예를 들어 원래의 인스트럭션보다 수정할 인스트럭션의 길이가 길다면, 인스트럭션을 덮어쓸 때 이후의 인스트럭션 내용까지 덮어쓰게 되어 바이너리가 정상 동작하지 않을 수 있습니다. 코드 섹션의 사용하지 않는 여유 공간에 새로운 코드를 추가할 수도 있겠지만, 대부분의 바이너리는 코드를 추가할 만큼 충분한 여유 공간을 가지고 있지 않습니다.
off-by-one 버그 패치하기
바이너리에 존재하는 off-by-one 버그를 헥스 에디터를 사용한 방법으로 패치해 보겠습니다. 압축 파일에서 example1/main
파일이 패치할 바이너리입니다. 바이너리를 실행하면 add, delete, show 등의 메뉴를 확인할 수 있고, 각각의 메뉴를 선택해 보면 사용자로부터 문자열을 입력받아 관리하고 보여주는 프로그램임을 짐작할 수 있습니다.
1 | $ ./main |
add 메뉴에 해당하는 add_data
함수를 디컴파일하면 다음과 같습니다. 11행에서 길이를 입력받고, 16보다 작거나 같으면 14행에서 20바이트 크기의 구조체를 할당한 후 오프셋 16에 길이를 저장합니다. 17행의 반복문에서는 입력받은 길이만큼 1바이트씩 문자를 입력받아 구조체의 오프셋 0에서부터 저장하는 것처럼 보입니다. 20행의 조건문은 입력받은 문자가 개행 문자면 널(null) 문자로 바꾼 후 반복문을 탈출하도록 합니다.
1 | _DWORD *add_data() |
그런데 반복문의 탈출 조건을 잘 보면 i < v2
가 아닌 i <= v2
로, 길이만큼 입력받도록 한 개발자의 의도와는 달리 실제로는 한 바이트를 더 입력할 수 있습니다. 만약 허용되는 최대 길이인 16을 입력한 후 17바이트를 입력한다면, 마지막 바이트는 구조체의 오프셋 16에 저장되면서 해당 위치에 있는 길이 값을 아래 그림과 같이 덮어쓸 것입니다.
show 메뉴에 해당하는 show_data
함수를 보면 오프셋 16의 5행에서 반복문의 탈출 조건에 사용되고 있습니다. 반복문은 구조체의 내용을 1바이트씩 출력하므로, 길이 값이 모두 몇 바이트를 출력할지 결정하고 있는 것입니다. 위의 그림과 같이 길이 값이 16보다 큰 값으로 오염된 상태라면, 힙 메모리에 대한 out-of-bounds 읽기가 발생하여 메모리 주소가 노출되는 취약점으로까지 연계될 수 있습니다.
1 | int __fastcall show_data(__int64 a1) |
실제로 길이 값이 오염된 구조체 이후에 해제된 힙 메모리가 위치하도록 한 후, show 메뉴를 이용해 출력해보면 힙 영역의 메모리 주소가 노출되는 것을 확인할 수 있습니다.
1 | 1. add data |
이와 같이 add_data
함수에 존재하는 off-by-one 버그를 헥스 에디터를 사용해 패치하여 취약점을 보완해 보겠습니다. 우선 디스어셈블러를 이용해 버그의 원인이 된 반복문 탈출 조건이 add_data
함수에서 어떤 인스트럭션에 해당하는지 조사해야 합니다. 다음은 GDB로 add_data
함수를 디스어셈블한 결과의 일부입니다. 반복문의 카운터 역할을 하는 [rbp-0x14]
에 1을 더하고 지역 변수 [rbp-0x18]
과 비교하는 부분에서 탈출 조건에 해당하는 코드임을 알 수 있습니다.
1 | pwndbg> disass add_data |
여기서 정확히 off-by-one 버그의 정확한 원인이 되는 인스트럭션은 main+233
의 jbe
인스트럭션입니다. jbe
인스트럭션은 "jump if below or equal"을 의미하는 조건부 점프 인스트럭션으로, 버그를 패치하기 위해서는 "jump below"에 해당하는 jb
인스트럭션으로 바꿔야 합니다.
그런데 GDB의 내장 디스어셈블러가 편의상 jbe 0x13eb
와 같이 출력하는 것과 달리, 조건부 점프 인스트럭션은 실제로는 점프할 주소까지의 상대적인 오프셋을 인자로 인코딩합니다. 따라서 먼저 바꿔야 하는 인스트럭션의 정확한 opcode를 확인한 후, 인자를 정확하게 표현하는 디스어셈블러로 인스트럭션의 원래 형태를 파악하는 것이 좋습니다. GDB를 사용하여 확인한 jbe
인스트럭션의 opcode는 \x76\xb5
입니다.
1 | pwndbg> x/2bx 0x1434 |
인스트럭션의 디스어셈블과 어셈블에는 shell-storm.org에서 운영하는 웹페이지를 사용하는 방법이 가장 간단합니다. 아래와 같이 웹페이지 하단에서 opcode를 입력하고 x86 (64)를 선택한 다음 Disassemble 버튼을 클릭하면, 인스트럭션의 원래 형태는 jbe 0xffffffffffffffb7
임을 파악할 수 있습니다.
다음으로 웹페이지 상단에서 jb 0xffffffffffffffb7
를 입력한 후 동일하게 Assemble 버튼을 클릭하면, 새로 바꿀 opcode \x72\xb5
를 얻을 수 있습니다.
이제 헥스 에디터를 이용하여 바이너리를 수정할 차례입니다. 헥스 에디터는 무료 소프트웨어인 HxD, 상용 소프트웨어인 010 Editor 등 잘 알려진 소프트웨어가 많으므로 자신이 사용하기 편한 것을 사용하면 됩니다. 이 글에서는 편의상 커맨드라인에서 편집이 가능한 hexedit을 사용하겠습니다. hexedit은 APT를 이용하여 설치할 수 있습니다.
1 | sudo apt update && sudo apt install hexedit -y |
hexedit은 인자로 바이너리의 경로를 전달하여 실행합니다. 주요 단축키는 다음과 같습니다.
단축키 | 기능 |
---|---|
Ctrl+G | 지정한 파일 오프셋으로 이동 |
F2 | 저장 |
Ctrl+X | 저장하고 종료 |
Ctrl+C | 저장하지 않고 종료 |
main
바이너리의 복사본 main.patched
를 만들고 hexedit으로 열어 오프셋 0x1434
로 이동하면 아래와 같이 원래의 인스트럭션에 해당하는 76 b5
가 있습니다.
새로 바꿀 opcode인 72 b5
를 입력하고, 저장한 후 종료합니다. 패치된 바이너리의 add_data
함수를 GDB로 디스어셈블해보면 jbe
인스트럭션이 jb
로 바뀐 것을 확인할 수 있습니다.
1 | pwndbg> disass add_data |
패치된 바이너리는 앞서 힙 메모리가 노출되도록 하였던 입력을 전달하여도 더 이상 구조체의 길이 필드가 오염되지 않아 트리거에 실패합니다. 따라서 off-by-one 버그로 인해 발생하였던 취약점이 잘 보완되었음을 알 수 있습니다.
1 | $ ./main.patched |
코드를 삽입해야 하는 경우
이전 문단에서는 간단한 수정이 필요한 경우 헥스 에디터를 사용하여 바이너리를 패치하는 방법을 살펴보았습니다. 그러나 이 방법은 앞서 언급하였던 대로 수정해야 할 내용이 조금만 늘어나도 바꿀 코드를 위치시킬 공간이 부족하여 적용하기 어렵습니다. 예를 들어 패치할 함수가 입력값에 대한 검증을 충분히 하고 있지 않아 검증하는 코드를 직접 추가해야 하는 경우에는 직접 바이너리에 새로운 코드를 삽입하여 메모리에 로드한 후 분기나 호출 부분을 수정하여 실행 흐름을 옮겨야 합니다.
코드를 삽입해야 하는 경우는 새로운 코드를 메모리상에서 로드할 위치와, 기존의 실행 흐름을 돌려 새로운 코드를 실행할 방법을 모두 고려하여야 합니다. 이 글에서는 바이너리에서 실제 동작과 무관한 부분에 코드를 삽입하고, 실행 가능한 영역으로 변경하여 로드하는 방법을 소개합니다. 이 방법의 이해를 위해서는 바이너리의 섹션과 세그먼트 개념에 대한 기초적인 숙지가 필요합니다.
섹션과 세그먼트
바이너리의 코드와 데이터들은 논리적으로 섹션(sections)이라는 부분들로 구분되어 있습니다. 모든 섹션에 대해 섹션의 성질이나 섹션에 해당하는 바이트들의 위치를 나타내는 섹션 헤더(section headers)가 존재하며, 모든 섹션 헤더는 섹션 헤더 테이블에 위치하고 있습니다. 유의해야 할 내용은 섹션의 구분은 링커(linker)의 편의를 위한 것으로, 프로세스를 실행하기 위해 모든 섹션이 필요한 것은 아니라는 점입니다.
다음은 /usr/include/elf.h
파일에 정의된 섹션 헤더의 구조입니다.
1 | typedef struct |
섹션 헤더에서 눈여겨볼 필드들은 다음과 같습니다.
sh_type
필드는 섹션의 타입을 나타내며, 타입은 섹션의 내용 및 구조에 대한 정보를 알려줍니다.SHT_PROGBITS
타입은 섹션이 인스트럭션이나 상수와 같은 프로그램 데이터로 이루어져 있음을 나타냅니다.
sh_flags
필드는 섹션 플래그들이며, 섹션에 대한 추가적인 정보를 나타냅니다.SHF_WRITE
플래그는 섹션이 런타임에 쓰기 가능함을 나타냅니다.SHF_ALLOC
플래그는 섹션의 내용물이 바이너리를 실행할 때 가상 메모리로 로드됨을 나타냅니다.SHF_EXECINSTR
플래그는 섹션이 실행 가능한 인스트럭션들을 포함하고 있음을 나타냅니다.
sh_addr
,sh_offset
,sh_size
는 각각 섹션의 가상 주소, 파일 오프셋과 크기를 나타냅니다.
1 |
readelf 도구를 이용하여 ELF 바이너리의 섹션에 대한 정보를 확인할 수 있습니다. 다음은 아래의 실습 문단에서 사용할 example2/main
바이너리의 섹션 정보를 확인한 예시입니다.
1 | $ readelf --sections --wide main |
섹션의 구분은 링커의 편의를 위한 것입니다. 반면 바이너리를 로드할 때 관련있는 코드나 데이터를 함께 취급하거나, 특정 영역의 데이터를 로드할지 여부를 결정하기 위해서 바이너리를 세그먼트(segments) 관점으로도 구분합니다. 세그먼트는 단순히 여러 섹션을 하나로 합쳐 놓은 것입니다. 섹션과 마찬가지로, 모든 세그먼트에 대해 세그먼트의 정보를 나타내는 프로그램 헤더(program headers)가 존재합니다. 모든 프로그램 헤더는 바이너리에서 프로그램 헤더 테이블에 위치하고 있습니다.
다음은 /usr/include/elf.h
파일에 정의된 프로그램 헤더의 구조입니다.
1 | typedef struct |
프로그램 헤더에서 눈여겨볼 필드들은 다음과 같습니다.
p_type
필드는 세그먼트의 타입을 나타냅니다.PT_LOAD
타입은 프로세스를 실행할 때 메모리에 로드되는 세그먼트를 나타냅니다.
p_flags
필드는 세그먼트 플래그로, 세그먼트에 대한 런타임에서의 권한을 나타냅니다.PF_X
,PF_W
,PF_R
플래그는 각각 런타임에서 실행 가능, 쓰기 가능, 읽기 가능함을 나타냅니다.
p_offset
,p_vaddr
,p_filesz
필드는 각각 세그먼트의 파일 오프셋, 가상 주소와 크기를 나타냅니다.
1 |
세그먼트에 대한 정보도 readelf를 사용하여 확인할 수 있습니다. 특히 어떤 섹션들이 어떤 세그먼트에 속하는지 대응 관계를 보면, 세그먼트는 단지 여러 섹션이 합쳐진 것임을 분명히 알 수 있습니다.
1 | $ readelf --segments --wide main |
PT_NOTE
세그먼트를 이용한 코드 삽입
바이너리를 실행하기 위해 모든 섹션이 필요한 것은 아닙니다. 다시 말해 바이너리에서 실행에 필요한 코드나 데이터와는 무관한 공간들이 존재한다는 것입니다. 만약 이 공간에 추가할 코드를 삽입하고, 섹션 헤더와 프로그램 헤더를 변경하여 메모리에 로드해야 하는 코드인 것처럼 꾸미면 우리는 삽입한 코드를 바이너리의 일부였던 것처럼 실행할 수 있습니다.
바이너리의 PT_NOTE
세그먼트와, 이 세그먼트를 구성하는 .note.*
섹션들은 특히 코드를 삽입하기 좋은 공간입니다. 원래 .note.*
섹션들은 아래 file
커맨드의 실행 결과에서 “BuildID[sha1]~” 부분과 같이 바이너리의 빌드 id나 제조사와 같은 추가적인 정보를 담기 위해 존재하는 공간입니다. 내용이 없어지거나 변형되어도 실행에 전혀 문제가 되지 않으며, 애초에 프로세스의 실행 도중에는 참조조차 되지 않는 데이터들입니다.
1 | $ file main |
앞서 readelf로 세그먼트를 확인한 예시를 보면 오프셋 0x368
에 존재하는 PT_NOTE
타입의 8번 세그먼트는 .note.gnu.build-id
, .note.ABI-tag
의 두 개 섹션으로 이루어져 있고, 68바이트의 공간을 차지하는 읽기 전용 세그먼트입니다. 바이너리의 특정 함수에 인자를 검증하는 로직을 추가해야 한다고 가정해 보겠습니다. 추가할 코드를 해당 세그먼트에 삽입하고 기존 코드를 패치해 분기하도록 한 후, 아래와 같이 섹션 헤더와 프로그램 헤더를 변경하면 실행 흐름을 옮길 수 있습니다.
이 방법을 조금 응용하면 코드의 크기가 68바이트보다 크다고 하여도 새로운 섹션 자체를 추가하여 삽입할 수 있습니다. 코드를 바이너리의 끝부분 이후에 삽입하여 새로운 공간을 만든 후, 섹션 헤더와 프로그램 헤더를 변경하여 새로운 공간의 오프셋을 가리키도록 하면 됩니다. 다만 이 경우에는 가상 주소와 오프셋 필드뿐만 아니라 크기 필드까지 새로운 공간의 크기에 해당하는 값으로 변경해야 합니다. 또한 바이너리의 크기나 섹션의 위치 등을 변경할 수 없다는 제약이 있다면 적용할 수 없습니다.
double free 버그 패치하기
이번에는 바이너리에 존재하는 double free 버그를 코드를 삽입하는 방법으로 패치해 보겠습니다. example2/main
파일이 패치할 바이너리이며, 핵심적인 로직은 이전의 example1/main
바이너리와 거의 차이가 없습니다.
main
함수를 보면 add 메뉴의 경우 22행에서 add_data
함수가 반환한 구조체 포인터를 s[i]
에 저장합니다. s
는 스택에 존재하는 배열이며, i
는 16행의 반복문에서 s[i]
가 NULL
인 인덱스를 자동으로 선택합니다. delete 메뉴에 해당하는 33행은 인덱스 i
를 입력받고, 16보다 작다면 s[i]
가 가리키는 구조체를 해제하기 위해 delete_data
함수를 호출합니다.
1 | int __cdecl __noreturn main(int argc, const char **argv, const char **envp) |
그런데 delete_data
함수는 단순히 free
함수를 호출하는 것 이외에 별다른 기능을 하지 않습니다. 또한 main
함수에서 s[i]
를 NULL
로 초기화하는 등의 조치를 하지 않고 있어, 해제한 힙 메모리를 다시 해제할 수 있는 double free 버그가 발생합니다. double free 버그를 악용하면 동적 메모리 할당자가 할당 시 임의 주소를 반환하도록 하는 프리미티브를 구성할 수 있습니다. 또한 경우에 따라 임의 읽기 및 쓰기나 코드 실행 취약점으로 연계할 수 있습니다.
다음은 바이너리에서 delete 메뉴를 두 번 선택하고 동일한 인덱스를 입력한 결과입니다. 동적 메모리 할당자 내부에서 double free 버그의 발생을 탐지하여 실행이 강제로 종료되었음을 확인할 수 있습니다.
1 | $ ./main |
double free 버그를 패치하기 위해서는 free
함수의 호출 이후 s[i]
가 해제된 힙 포인터를 저장하지 않도록 NULL
로 초기화하는 코드를 추가해야 합니다. 원칙적으로는 free
함수를 호출하기 이전에 s[i]
가 NULL
인지 검사하는 조건문 또한 필요하겠으나, 사실 glibc의 free
함수 구현체인 __libc_free
는 다음과 같이 인자로 주어진 포인터가 NULL
인 경우 아무 동작도 하지 않기 때문에 문제가 없습니다. 추가할 코드 길이도 단축할 겸 조건문은 생략하겠습니다.
1 | void |
패치를 위해 main
함수에서 delete 메뉴를 처리하는 부분을 살펴보겠습니다. main+366
에서 s[i]
의 값을 레지스터 rax
에 대입한 후, main+374
에서 레지스터 rdi
로 옮겨 main+377
에서 호출하는 delete_data
함수의 인자가 되도록 하고 있습니다. 패치에는 여러 방법이 있겠으나, 여기서는 main+366
에서 새로 삽입한 코드를 호출하여 s[i]
의 초기화와 free
함수 호출을 처리하도록 하겠습니다. 호출 이후 main+382
의 분기까지는 nop
로 덮어 기존의 인스트럭션을 무시하도록 합니다.
1 | pwndbg> disass main |
새로운 코드를 삽입하기 전 먼저 섹션 헤더와 세그먼트 헤더를 수정하여 코드를 삽입할 공간이 실행 가능한 영역으로 로드되도록 해야 합니다. 섹션 헤더와 세그먼트 헤더를 수정하기 위해서는 바이너리의 섹션 헤더 테이블과 프로그램 헤더 테이블에서 해당하는 부분을 직접 고쳐야 합니다. 상용 프로그램인 010 Editor의 템플릿 기능을 사용하는 방법이 가장 빠르긴 하나, 바이너리를 파싱하여 편집할 수 있도록 도와주는 무료 웹페이지를 사용하는 방법도 못지 않게 간편합니다.
페이지 좌상단의 Open 버튼을 클릭하여 바이너리를 업로드한 후, 왼쪽의 Section headers 메뉴를 클릭하면 섹션 헤더들이 나열됩니다. 이들 중 Elf_Shdr3 , Elf_Shdr4 섹션 헤더가 코드를 삽입할 .note.gnu.build-id
, .note.ABI-tag
섹션에 해당합니다. 각각의 필드를 클릭하면 아래와 같이 페이지 우측에 헥스 에디터와 같은 인터페이스가 표시됩니다.
인터페이스의 edit 버튼을 클릭하여 바이트 단위로 편집하고, commit 버튼을 클릭하면 바이너리에 반영할 수 있습니다. 이전 문단의 그림과 같이 .note.gnu.build-id
, .note.ABI-tag
섹션 헤더의 sh_type
과 sh_flags
필드를 각각 SHT_PROGBITS
, SHF_ALLOC | SHF_EXECINSTR
로 변경합니다. 변경해야 할 값은 섹션 헤더와 프로그램 헤더를 소개한 문단에서 /usr/include/elf.h
파일에 정의된 내용을 참고하면 됩니다.
동일한 방법으로 Elf_Phdr8 프로그램 헤더의 p_type
필드와 p_flags
필드를 PT_LOAD
, PF_R | PX_X
로 변경합니다. 변경을 마친 후 페이지 좌상단의 Save 버튼을 클릭하면 수정사항이 반영된 바이너리를 내려받을 수 있습니다. readelf로 섹션과 세그먼트를 조사해 보면 잘 변경되었음을 확인할 수 있습니다.
1 | $ readelf --sections --wide .\main.patched.1 |
다음으로 삽입할 코드를 작성해 보겠습니다. 삽입할 코드는 delete_data
함수를 대신하여 호출되며, 이 코드에서 수행해야 할 작업을 의사코드로 나타내면 다음과 같습니다.
1 | free(s[i]); |
따라서 다음과 같이 어셈블리 코드로 옮길 수 있습니다. call
인스트럭션은 이후 opcode로 옮길 때 인스트럭션의 위치와 free@plt
의 상대적인 오프셋을 인자로 전달해야 하므로, call 0xd86
이 되어야 함에 유의합니다.
1 | lea rax, qword ptr [rbp + rax*8 - 0x90] |
어셈블리 코드를 위에서 사용한 온라인 어셈블러 웹페이지에서 opcode로 옮긴 후, 헥스 에디터를 이용해 .note.gnu.build-id
섹션의 위치인 오프셋 0x368
에 덮어씁니다. 또한 main+366
부터 main+382
까지는 삽입한 코드를 호출하는 call
인스트럭션과 nop
인스트럭션으로 덮어씁니다. 패치된 바이너리를 디스어셈블하면 최종적으로 다음과 같아야 합니다.
1 | pwndbg> disass main |
패치된 바이너리는 동일한 구조체를 연속으로 해제하려 시도하여도 동적 메모리 할당자에 의해 강제 종료되지 않습니다. 따라서 double free 버그로 인해 발생한 취약점이 잘 보완되었음을 확인할 수 있습니다.
1 | $ ./main.patched |
결론
바이너리 패치는 소스 코드가 없는 바이너리의 동작을 변형해야 하는 상황에서 유용합니다. 패치를 위해서는 디스어셈블러로 패치할 인스트럭션의 위치와 정확한 형태를 먼저 파악한 후, 헥스 에디터로 직접 편집하거나 코드를 삽입한 후 삽입된 코드를 호출하도록 해야 합니다. 코드 삽입에는 섹션 헤더와 프로그램 헤더를 수정하여 실행에 직접적으로 필요하지 않는 섹션을 사용하였으며, 바이너리의 취약점을 패치로 보완하는 실습을 통해 실제로 코드를 삽입하고 실행 흐름을 변형하는 것이 가능함을 확인하였습니다.
참고문헌
[1] D. Andriesse, “Chapter 2: The ELF Format,” in Practical Binary Analysis. San Francisco, CA: No Starch Press, 2019, pp. 31-55.
[2] D. Andriesse, “Chapter 7: Simple Code Injection Techniques,” in Practical Binary Analysis. San Francisco, CA: No Starch Press, 2019, pp. 155-187.