String Buffer Overflow
20byte의 buf를 할당하고 std::cin 함수를 통해 문자열을 입력받는다. 하지만 여기서도 입력한 문자열의 길이를 검사하는 부분이 없어서 20byte 이상의 문자열을 입력한다면 버퍼오버플로우가 발생할 수 있다.
#include <iostream>
using namespace std;
int main()
{
char buf[20];
cin >> buf;
// string 제외 입력에 따라 자동 할당됨.
}
Container Overflow
f 함수 부분이다.
vector v를 src 매개변수로 받는다.
std::vector<int> dest(5)
7행에서 기본값(0)으로 초기화 된 5개의 원소를 가지는 vector dest를 생성한다.
std::copy(src.begin(), src.end(), dest.begin());
8행에서는 copy함수를 이용하여 src.begin() 부터 src.end() 전 까지의 모든 원소들을 dest.begin() 부터 시작하는 곳에 복사한다. src가 dest(5) 보다 크다면 오버플로우가 발생하기 때문에 7행을 std::vector<int> dest(src); 로 바꾸어주면 오버플로우 취약점을 막을 수 있다.
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
void F(const vector<int>& src)
{
vector<int> dest(5);
copy(src.begin(), src.end(), dest.begin());
}
int main()
{
int size = 0;
cin >> size;
vector<int> v(size);
v.assign(size, 0x41414141);
F(v);
}
int형 vector v를 선언한 후 std::fill_n 함수를 통해 v.begin() 부터 10개의 배열을 0x42로 초기화 하는 코드다. 하지만 백터의 크기를 따로 지정해주지 않았기 때문에 7행의 코드가 실행되는 순간 버퍼오버플로우가 발생한다. 이러한 취약점을 막기 위해서는 아래와 같이 벡터를 선언할 때 크기를 지정해 주면 된다.
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
int main()
{
vector<int> v; // v(10) 해서 초기세팅 해줘야함
fill_n(v.begin(), 10, 0x42);
}
C++에서 반복자(iterator)는 컨테이너에 저장된 원소를 순회하고 접근할 수 있도록 한다. 반복자를 사용할 때 크기 계산을 정확하게 하지 않는다면 오버플로우가 발생할 수 있다.
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
void F(const vector<int>& c)
{
for (auto i = c.begin(), e = i + 20; i != e; i++)
cout << *i << endl;
}
int main()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
F(v);
}
위 코드는 int형 vector c를 20번 순회한다. 그러나 f 함수의 매개변수로 전달된 c의 크기가 20을 넘지 않는다면 e의 위치는 c의 바깥을 가리키게 되어 Out-of-Bound Access가 발생한다.
이 취약점은 for문의 반복 횟수를 벡터의 크기로 지정해주면 된다.
void F(const vector<int>& c)
{
for (auto i = c.begin(), e = i + c.size(); i != e; i++)
cout << *i << endl;
}
아래의 예제는 long long형인 idx 변수를 insert_table 함수의 매개변수 pos로 넘겨주고 pos 값이 table.size() 보다 크거나 같다면 overflow! 문구를 출력하고 종료하고, pos값이 table.size() 보다 작다면 table[pos]에 value를 대입하는 코드다.
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
void InsertTable(vector<int>& table, long long pos, int value)
{
if (pos >= (long long)table.size())
cout << "overflow!" << endl;
else
table[pos] = value;
}
int main()
{
long long idx;
vector<int> v(5);
cin >> idx;
InsertTable(v, idx, 0x41414141);
}
long long 자료형의 범위는 아래와 같다.
-9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807
따라서 idx 값으로 음수를 넘겨준다면 if문을 항상 만족하지 않으므로 std::vector 객체의 범위를 벗어난 쓰기가 발생하므로 배열의 인덱스로 사용되는 정수형 변수는 unsigned 자료형으로 선언해 사용해야 한다.
void InsertTable(vector<int>& table, unsigned long long pos, int value)
C++에서도 C와 마찬가지로 변수를 사용할 때 초기화하지 않으면 쓰레기값이 출력되기 때문에 취약점으로 이어질 수 있다.
#include <iostream>
using namespace std;
int main()
{
int i;
cout << i;
}
C++에서 초기화되지 않은 변수로 인한 문제가 가장 많이 발생하는 부분은 생성자다.
클래스 S는 함수 f에서 인자로 전달된 i와 c를 더한 후 그 값을 리턴한다. 생성자가 없기 때문에 디폴트 생성자가 생성되고 멤버 변수를 별도로 초기화 하지 않는다. 따라서 멤버변수 c에는 스택 메모리에 있는 잔여값이 저장되고, s.f(10)이 호출될 때 참조되고 이 결과로 인해 메모리 유출로 이어진다.
#include <iostream>
using namespace std;
class S
{
int c;
public:
int F(int i) const { return i + c; }
};
int F()
{
S s;
return s.F(10);
}
int main()
{
int val = F();
cout << val << endl;
}
S의 생성자는 mem_size가 인자로 전달되면 그 값만큼 메모리를 할당한다. 하지만 mem_size값에 음수를 전달하면 mem은 초기화가 이루어지지 않으므로 스택 잔여 메모리가 쓰여진다. 이후 mem 포인터를 참조하는 연산이 있고 공격자가 스택 값을 조작할 수 있다면 이는 임의 주소 쓰기 취약점으로 이어질 수 있다.
#include <iostream>
using namespace std;
class S
{
public:
char* mem;
int size;
public:
S() : mem(0),size(0){ }
S(int size)
{
this->size = size;
if (size > 0)
mem = new char[size];
}
};
int main()
{
S s(-1);
cout << s.mem << endl;
}
UAF (Use-After-Free)
C++ 언어에서도 UAF 취약점이 발생할 수 있다.
UAF 취약점은 Heap 영역에서 할당된 메모리를 해제(Free)하고 다시사용(re-use)할 때 발생하는 취약점이다.
4행에서 구조체 S를 정의하고 f() 함수를 호출한다.
main 함수에서 객체 s를 동적 할당하고 11행에서 해제한다.
13행에서 해제된 s를 참고하여 f 함수를 호출하려고 시도한다.
이러한 형태는 가장 기본적인 UAF 취약점이다. 공격자가 해제된 객체 s의 메모리에 원하는 값을 쓰게 된다면 치명적일 수 있다.
#include <iostream>
using namespace std;
struct S
{
void F();
};
int main()
{
S* s = new S;
delete s;
s->F();
}
str_func 함수에서 리턴받은 "aaaa" 문자열을 char형으로 str 변수에 저장하고, display_string 함수의 인자로 넘겨주어서 출력하는 코드다.
str_func 함수에서 생성되는 string 객체 a는 7행에서 반환되는 즉시 해제되지만 str 변수는 해당 객체의 문자열 포인터를 계속 참고하고 있어서 15행 이후에는 해제된 메모리를 가리키게 된다. 이후 새로 생성되는 객체 b가 해당 메모리를 사용하게 된다면 str 변수로 인해 UAF가 발생한다.
#include <iostream>
#include <string>
using namespace std;
string StrFn()
{
string a;
return a = "aaaa";
}
void Display(const char* buf)
{
cout << buf << endl;
}
int main()
{
const char* str = StrFn().c_str();
Display(str);
string b = "bbbb";
Display(str);
}
스마트 포인터 (Smart Pointers)
스마트 포인터를 올바르게 사용하지 않는다면 UAF가 발생할 수 있다. 스마트 포인터를 사용할 때 주의할 점은, 같은 메모리를 서로 다른 두 개의 스마트 포인터가 가리키게 해서는 안된다.
이 코드의 경우에는 main 함수가 종료될 때 p1, p2가 사라지게 되고, p1, p2가 관리하고 있던 i를 두번 해제해버리는 Double Free 버그가 발생한다.
#include <memory>
using namespace std;
int main()
{
int* i = new int;
shared_ptr<int> p1(i);
shared_ptr<int> p2(i);
}
객체 B가 5행에 정의되어 있고, 이를 상속하는 객체 D가 9행에 정의되어 있다.
함수 g는 인자로 std::shared_ptr<D> 형의 값을 받는다.
main 함수에서는 D의 인스턴스를 생성하고 이를 std::shared_ptr<B> 형의 스마트 포인터 poly에 저장한다.
그 다음 17행에서 함수 g를 호출하기 위해 B형으로 선언된 poly를 형 변환한다.
poly와 함수 g의 인자로 전달하기 위해 만든 새로운 스마트 포인터는 서로 다른 레퍼런스 카운터를 가지고 있고, 15행에서 new D를 통해 만든 인스턴스를 각각 가리킨다. 따라서 함수 g의 호출이 끝날 때 poly가 가지고 있는 포인터는 해제된다.
#include <iostream>
#include <memory>
using namespace std;
struct B
{
virtual ~B() = default;
};
struct D : B
{
};
void G(shared_ptr<D> derived)
{
cout << "Hi im G!" << endl;
}
int main()
{
shared_ptr<B> poly(new D);
G(shared_ptr<D>(dynamic_cast<D*>(poly.get()));
}
Type Confusion
Type Confusion은 프로그램에서 사용하는 변수나 객체를 선언 혹은 초기화되었을 때와 다른 타입으로 사용할 때 발생하는 취약점이다.
다음 코드는 C언어에서 정수를 입력받아 출력하는 예제다.
puts 함수의 원형은 아래와 같다.
int puts(const char *s)
정수로 값을 입력받지만 puts 함수는 char* 형 포인터를 인자로 받기 때문에 Type Confusion이 발생해 메모리에 존재하지 않는 주소를 입력한다면 다음과 같이 프로그램이 비정상 종료된다.
#include <stdio.h>
int main()
{
int val;
scanf("%d", &val);
puts(val);
}
Type Casting
dynamic_cast를 이용해 클래스 A를 클래스 B로 형 변환하는 예제다.
프로그램이 실행되는 도중 형 변환 과정에서 객체의 형태를 검사하는 dynamic_cast와 달리 static_cast와 reinterpret_cast에는 이러한 검증이 존재하지 않아 Type Confusion 취약점이 발생할 수 있다.
#include <iostream>
using namespace std;
class A
{
public:
virtual void F()
{
cout << "Class A::F()" << endl;
}
};
class B
{
public:
void F()
{
cout << "Class B::F()" << endl;
}
};
int main()
{
A* classA = new A;
classA->F();
B* classB = dynamic_cast<B*>(classA);
classB->F();
}
#include <iostream>
#include <string>
using namespace std;
class Parent
{
public:
};
class Print :public Parent
{
public:
virtual void PrintStr(char* str)
{
cout << str << endl;
}
~Print()
{
}
};
class Read :public Print
{
public:
virtual void ReadStr(char* str)
{
cout << "Input: " << str << endl;
cin >> str;
cout << "Data : " << str << endl;
}
~Read()
{
}
};
int main()
{
Parent* p1 = new Print, * p2 = new Read;
Print* b1;
char buf[256];
strcpy(buf, "I'm PrintStr");
b1 = static_cast<Print*>(p1);
b1->PrintStr(buf);
b1 = static_cast<Print*>(p2);
b1->PrintStr(buf);
delete p1,p2;
return 0;
}
https://sh1r0hacker.tistory.com/151
https://sh1r0hacker.tistory.com/152
https://sh1r0hacker.tistory.com/154
https://sh1r0hacker.tistory.com/155
'프로그래밍 언어 > C++' 카테고리의 다른 글
[C++] 문자열 (string) 함수 모음 (0) | 2023.10.02 |
---|---|
[C] 복합 대입 / 증감 연산자 (0) | 2023.10.02 |
[C] 버퍼 오버플로 (buffer overflow) (0) | 2023.09.27 |
[C++] 디버그 모드에서 변수의 메모리 차지 공간 (0) | 2023.09.27 |
[C/C++] 메모리 오류에 대하여 (0) | 2023.09.27 |