// Name: uaf_overwrite.c
// Compile: gcc -o uaf_overwrite uaf_overwrite.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
struct Human {
char name[16];
int weight;
long age;
};
struct Robot {
char name[16];
int weight;
void (*fptr)();
};
struct Human *human;
struct Robot *robot;
char *custom[10];
int c_idx;
void print_name() { printf("Name: %s\n", robot->name); }
void menu() {
printf("1. Human\n");
printf("2. Robot\n");
printf("3. Custom\n");
printf("> ");
}
void human_func() {
int sel;
human = (struct Human *)malloc(sizeof(struct Human));
strcpy(human->name, "Human");
printf("Human Weight: ");
scanf("%d", &human->weight);
printf("Human Age: ");
scanf("%ld", &human->age);
free(human);
}
void robot_func() {
int sel;
robot = (struct Robot *)malloc(sizeof(struct Robot));
strcpy(robot->name, "Robot");
printf("Robot Weight: ");
scanf("%d", &robot->weight);
if (robot->fptr)
robot->fptr();
else
robot->fptr = print_name;
robot->fptr(robot);
free(robot);
}
int custom_func() {
unsigned int size;
unsigned int idx;
if (c_idx > 9) {
printf("Custom FULL!!\n");
return 0;
}
printf("Size: ");
scanf("%d", &size);
if (size >= 0x100) {
custom[c_idx] = malloc(size);
printf("Data: ");
read(0, custom[c_idx], size - 1);
printf("Data: %s\n", custom[c_idx]);
printf("Free idx: ");
scanf("%d", &idx);
if (idx < 10 && custom[idx]) {
free(custom[idx]);
custom[idx] = NULL;
}
}
c_idx++;
}
int main() {
int idx;
char *ptr;
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
while (1) {
menu();
scanf("%d", &idx);
switch (idx) {
case 1:
human_func();
break;
case 2:
robot_func();
break;
case 3:
custom_func();
break;
}
}
}
전역 변수
struct Human {
char name[16];
int weight;
long age;
};
struct Robot {
char name[16];
int weight;
void (*fptr)();
};
struct Human *human;
struct Robot *robot;
char *custom[10];
int c_idx;
두 구조체의 자료형을 보면 동일한 메모리 크기를 사용하는 것을 알 수 있다.
UAF(Use After Free 문제인 만큼 Human→age에 주소 값을 입력하고 해제하고 Robot 구조체로 재 할당하면, 재 할당 받은 Robot→fptr로 원하는 코드 흐름을 실행시킬 수 있을 것 같다. 또한, 주소를 담고 있는 custom 배열과 이를 참조하는 c_idx가 선언된다.
human_func()
void human_func() {
int sel;
human = (struct Human *)malloc(sizeof(struct Human));
strcpy(human->name, "Human");
printf("Human Weight: ");
scanf("%d", &human->weight);
printf("Human Age: ");
scanf("%ld", &human->age);
free(human);
}
Robot(2번) 메뉴를 선택하면 robot_function함수가 실행된다. Robot 구조체 1개를 동적할당 하고, weight를 입력받아 데이터를 저장한다. fptr에 대한 입력은 없지만, robot→fptr의 값이 NULL이 아니라면 fptr의 주소로 점프한다.
위의 두개의 함수를 통해 Human→age에 원하는 주소를 입력해두고, robot_func()을 실행하면 robot→fptr에 앞서 저장해준 Human→age값으로 robot→fptr()이 실행된다. 그렇다면 Human→age에 원샷 가젯의 주소를 넣으면 셸을 얻을 수 있을 것이다. 하지만 우리는 libc를 모르기 때문에, 메모리 릭을 통해 libc를 먼저 구해야 한다.
데이터를 입력받은 후, custom[c_idx]에 값을 출력해준다. 이 Data 부분에는 Unsorted bin을 free 했을 때 남아있던 fd 값이 출력된다. unsorted bin에 들어간 청크의 fd와 bk은 main 의 주소이고, 이 오프셋을 통해서 libc를 구할 수 있다.
unsigned int idx;
...
if (idx < 10 && custom[idx]){
free(custom[idx]);
custom[idx] = NULL;
}
...
idx가 unsigned int자료형으로 선언되었다. 만약 idx에 -1(음수)를 입력하게 되면 0xffffffff와 같은 형식으로 메모리에 입력된다. 그러면 할당된 메모리를 free하지 않는다.
해당 풀이를 참고하여 리마인드차 UAF 문제를 풀어보고자 하였는데 이용이 불가하네 일단 개념만 다시보고 드림핵 문제를 풀어봐야겠다.
나중에 다시 접근을 해봐야겠다..
① 힙영역이란?
- 메모리를 동적으로 할당하여, 사용하는 공간을 의미한다.
- 필요에 의해, 메모리를 할당하고 해제한다.
② heap overflow란?
- 할당된 메모리보다 많은 값을 넣어서, 다른 메모리 주소 값까지 침범하여 발생한다.
- 만약, 함수 포인터가 존재하는 부분까지 침범해서 조작한다면, 프로그램의 실행 흐름을 바꿀 수 있는 문제가 발생한다.
③ UAF(Use After Free)
- 힙 영역에서 메모리 해제 후, 해제한 메모리 영역을 재사용할 때 발생하는 취약점이다.
#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언어에서는 malloc과 free로 동적할당하지만, C++에서는 new과 delete를 사용한다.
위 코드 분석을 통해 알 수 있는 점은 아래와 같다.
⇨ class Human에 선언된 private권한의 give_shell()함수에 접근해, 쉘을 획득할 수 있다. ⇨ Human을 상속받은 man과 woman의 객체를 생성한다. ⇨ case 1은 생성한 객체 m과 w를 통해, introduce()함수를 불러온다. ⇨ case 2는 사용자에게 입력받아온 값을 바탕으로 동적할당한다. ( argv[1] : len / argv[2] : file ) ⇨ case 3은 객체 m과 w 메모리를 해제한다.
시나리오를 만들어보자면, 아래와 같다.
할당되어 있는 m과 w 메모리를 해제하고, 해제한 메모리와 같은 크기를 할당한다. 이는 같은 크기이기 때문에, m과 w가 가르키고 있는 공간을 할당하게 된다. 새로 할당받은 공간을 악의적인 값으로 변경하고 메모리부분을 실행시키면, 해제했던 메모리를 재사용하게 된다. 이는 uaf취약점이 발생한 것으로, 접근이 불가능한 부분에 접근하여 shell을 획득할 수 있다.
시나리오를 바탕으로, 우리는 uaf 취약점을 활용해서 아래 순서대로 문제를 clear 할 수 있다.
① 메뉴 1번을 눌러서, 동적할당한 m과 w가 introduce( )함수의 주소값을 갖게 한다. ② 메뉴 3번을 눌러서, m과 w 메모리를 해제한다. ③ 메뉴 2번을 눌러서, 위에서 해제했던 메모리와 같은 크기를 동적할당하고, 값은 give_shell주소값으로 바꿔준다. ( 0x401570-0x8 = 0x401568 ) ④ 메뉴 1번을 눌러서, 2번에서 해제했던 메모리를 재사용함으로써 uaf가 발생하는 취약점이다.
import struct
import socket
import time
def until(s, string):
data = b''
while string not in data:
data += s.recv(1)
return data
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('192.168.249.139', 2997))
l = list()
l.append(int(struct.unpack('<I',s.recv(4))[0]))
l.append(int(struct.unpack('<I',s.recv(4))[0]))
l.append(int(struct.unpack('<I',s.recv(4))[0]))
l.append(int(struct.unpack('<I',s.recv(4))[0]))
result = int(sum(l))
print(l)
print('sum : '+str(result))
s.send(struct.pack('<I', result))
print(str(s.recv(1024)))
s.close()
import socket
import struct
s = socket.socket()
s.connect(("192.168.56.101",2997))
sum = 0
for i in range(4):
data = s.recv(4)
data = "%d\n" % (struct.unpack('<i', data))
sum += int(data)
s.send(struct.pack("<I", sum))
print s.recv(1024)
s.close()
#include "../common/common.c"
#define NAME "net0"
#define UID 999
#define GID 999
#define PORT 2999
void run()
{
unsigned int i;
unsigned int wanted;
wanted = random();
printf("Please send '%d' as a little endian 32bit int \n", wanted);
if(fread(&i, sizeof(i), 1, stdin) == NULL) {
errx(1, ":(\n");
}
if(i == wnated) {
printf("Thank you sir/madam \n");
} else {
printf("I'm sorry, you sent %d instead \n", i);
}
}
int main(int argc, char **argv, char **envp)
{
int fd;
char *username;
/* Run the process as a daemon */
background_process(NAME, UID, GID);
/* Wait or socket activity and return */
fd = serve_forever(PORT);
/* Set the client socket to STDIN, STDOUT, and STDERR */
set_io(fd);
/* Don't do this */
srandom(time(NULL));
run();
}
import socket
import struct
def until(s, string):
data = b''
while string not in data:
data += s.recv(1)
return data
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('192.168.249.139', 2999))
data = until(s, b'\n')
recvstring = str(data)
print(recvstring)
start = str(data).find("'")+1
end = str(data).find("'", start)
quiz = int(recvstring[start:end])
convlittle = struct.pack('<I',quiz)
s.send(convlittle)
print(until(s, b'\n'))
s.close()
참고
# using pwntool : https://github.com/Gallopsled/pwntools
from pwn import *
s = remote('10.211.55.9', 2999)
data = int(s.recvline()[13:22])
print "[+] RECV " + str(data)
print "[+] SEND"
s.send(p32(data)) # packing
print "[+] " + str(s.recvline())
import socket
import struct
s = socket.socket()
s.connect(("192.168.xxx.xxx",2999))
data = s.recv(1024)
print data
data = data.split("'")
s.send(struct.pack('<i', int(data[1])))
print s.recv(1024)
s.close()