We got 4th overall and also qualified for the finals as the 3rd verified local team! My teammates were absolute beasts even though they were quite busy :) Challenge names with 🍭 are supposed to be “easier” while those with 🩸 come with first blood prizes (I managed to get 2!).
You have entered a top secret robot production facilities and your clumsy friend tripped the alarm. You and your friends are about to be “decontaminated”.
Luckily, you have unpacked the authentication firmware during previous reconaissance. Can you use them to override the decontamination process?
In the provided archive, there are 100 stripped 64-bit ELF binaries.
1
2
3
4
5
6
7
8
a@a:/grey/authentication$ ls -1
03e73f2bd4c480b209caa321eb3e2eb974b680e2ec7cbada8cfd457375eaa3c0
04480e6a6ad345bf50f4f428f802656264d4738a000270d8967734cecd0ba2a9
08d4a97677fd1c8d14b069b9922518e5c7a680b67417990d998c41357d4c7c14
...
...
a@a:/grey/authentication$ ls -1 | wc -l
100
1
2
a@a:/grey/authentication$ file 03e73f2bd4c480b209caa321eb3e2eb974b680e2ec7cbada8cfd457375eaa3c0
03e73f2bd4c480b209caa321eb3e2eb974b680e2ec7cbada8cfd457375eaa3c0: ELF 64-bit LSB executable, x86-64, version 1(SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=3f33add36e8e79282d0d0ed3aaa826744ddc5a39, for GNU/Linux 3.2.0, stripped
If we open a few of them in IDA Pro, we’ll see that all of these binaries basically do the same thing: fgets is used to read an input string from stdin and then each character is checked against 2 hardcoded strings using some arithmetic operations. What differs across the binaries, though, are the input length, the 2 hardcoded strings, the constant used for the arithmetic operations, and the addresses of the main function and some locations.
Below is the pseudocode for the 03e73f2bd4c480b209caa321eb3e2eb974b680e2ec7cbada8cfd457375eaa3c0 binary with variables and function names renamed by me:
If we connect to the server and port provided in the challenge description, we’ll see that we’re supposed to provide the correct input strings (passwords) for specified binaries (challenge codes) within 10 minutes.
1
2
3
4
5
Unauthorized access detected.
Decontamination in 10 minutes
Do you wish to override? (y/n)y
Password required for challenge code: ffbb5c9d6d32299cc53c90d410282b42f7431c60ee40540f19603f0b4c1cfc90
Technically there’s enough time to do this manually since the server turned out to only request input strings for 5 binaries, but I didn’t know this and went ahead to learn and use angr for automated reverse engineering which I suppose is what’s hinted by the challenge name. It went surprisingly well for my first time!
Using angr
We’ll be using angr’s SimulationManager to help us find a program state with the correct input string by means of symbolic execution, so there’s a few things we need to identify in each binary we process:
Addresses we want to find/reach
Addresses we want to avoid
Input length
Addresses
The is_success = 0; basic block in check_input with address 0x401305 is what we’ll tell angr to avoid.
And in main we want check_input(input) == 0; to fail meaning we don’t want the jz after check_input to be taken, so the location we should tell angr to find for is 0x401395.
But of course as mentioned earlier, I noticed these address locations and the address of main change across the binaries provided, so I first located fgets using the ELF binary’s Procedure Linkage Table (PLT) and then retrieved the first and only callgraph predecessor of fgets which is main.
To get to the basic block in check_input that we want angr to avoid, I retrieved the second last call site in main which is call check_input. I then retrieved the return site of the last function call in check_input which is a call check_chr, and added the offset of 0xF to reach the mov [rbp+is_success], 0 that we want to avoid.
To get to the basic block in main that we want angr to find, I sorted the basic blocks in main by address and took the address of the 5th last basic block.
Input
I retrieved the first basic block in main and searched through the instructions for mov esi, xx where xx is the length argument given to fgets to retrieve user input. xx - 1 will then represent the actual input length because fgets will also take in a newline character.
I also constrained the input to only be printable characters because the code in check_chr appears to try to check for that.
importangrimportclaripyimportcapstoneimportloggingfrompwnimport*logging.getLogger("claripy.ast.bv").setLevel("ERROR")logging.getLogger("angr.storage").setLevel("ERROR")logging.getLogger("angr.simos").setLevel("ERROR")logging.getLogger("angr.analyses.cfg").setLevel("ERROR")defchar(state,c):returnstate.solver.And(c<="~",c>=" ")defsolve_file(file):print(file)p=angr.Project("authentication/{}".format(file),auto_load_libs=False)fgets_addr=p.loader.main_object.plt["fgets"]cfg=p.analyses.CFGFast()# find the basic block to FINDfgets_callers=cfg.functions.callgraph.predecessors(fgets_addr)# get the first and only calling predecessor which is mainmain_func=[cfg.functions[caller]forcallerinfgets_callers][0]print("main: {}".format(hex(main_func.addr)))main_blocks=sorted(main_func.blocks,key=lambdab:b.addr)# we want to reach the 5th last block where the jz is not takenFIND=main_blocks[-5].addrprint("FIND: {}".format(hex(FIND)))# find length of input required via# mov esi, 1Ehfirst_block=main_blocks[0]forinsninfirst_block.capstone.insns:if(len(insn.operands)==2andinsn.mnemonic=="mov"andinsn.operands[0].reg==capstone.x86.X86_REG_ESIandinsn.operands[1].type==capstone.x86.X86_OP_IMM):INPUT_LENGTH=insn.operands[1].imm-1print("input length: {}".format(INPUT_LENGTH))# find the basic block to AVOIDfunctions=cfg.kb.functions[main_func.addr]main_calls=functions.get_call_sites()# second last call in main is check_inputcheck_input_addr=[functions.get_call_target(call)forcallinmain_calls][-2]functions=cfg.kb.functions[check_input_addr]check_input_calls=functions.get_call_sites()# get the last return site of the function calls in check_inputAVOID=max(filter(None,[functions.get_call_return(call)forcallincheck_input_calls],))OFFSET=0xFAVOID+=OFFSETprint("AVOID: {}".format(hex(AVOID)))flag=claripy.BVS("flag",INPUT_LENGTH*8)state=p.factory.entry_state(stdin=flag)state.options.add(angr.options.LAZY_SOLVES)forcinflag.chop(8):state.solver.add(char(state,c))sm=p.factory.simulation_manager(state)sm.explore(find=FIND,avoid=AVOID)print("finding...")ifsm.found:found=sm.found[0]solution=found.solver.eval(flag,cast_to=bytes)print("found: {}".format(solution))else:print("not found")solution=Noneprint("-"*64)returnsolutionif__name__=="__main__":conn=remote("challs.nusgreyhats.org",10523)conn.recvuntil(b")")conn.recvline()conn.sendline(b"y")response=b""whileTrue:response=conn.recvline()ifb": "inresponse:challenge_code=response.split(b": ")[-1].rstrip(b"\n").decode()password=solve_file(challenge_code)conn.sendline(password)else:breakprint(response)
a@a:/grey/gogogo$ file binary
binary: ELF 64-bit LSB executable, x86-64, version 1(SYSV), statically linked, Go BuildID=OHBJFJh5S4MEkda8Q683/cMydJq6y9QbVjBCjK1KP/8R1f9ddSl9EfpM8KP2Dy/3G9-Ju3BW7WUsgoGNyvl, not stripped
a@a:/grey/gogogo$ cat challenge.txt
GvVf+fHWz1tlOkHXUk3kz3bqh4UcFFwgDJmUDWxdDTTGzklgIJ+fXfHUh739+BUEbrmMzGoQOyDIFIz4GvTw+j--
In the given archive there’s a 64-bit ELF Go binary and a challenge.txt containing encoded text that looks like some variant of Base64 encoding.
From blackbox analysis, the encoding scheme also seems similar to Base64 in that 3-byte input blocks (aaa) are independently encoded to 4-byte output blocks (CGTx), and padding (-) is used to keep the output length a multiple of 4 when the input length is not a multiple of 3:
A string is read from stdin, converted into a Go slice of bytes, and then passed into main_Encode. The encoded slice of bytes is then converted back into a string and printed.
What does main_Encode do? At the top, we see a string b64str containing 64 unique characters. This is used to form the majority of output in the first while loop where input_ptr is iterated over in chunks of 3 while output is iterated over in chunks of 4.
At the bottom, we see that for remaining input bytes not covered by the chunks of 3, the character - is padded to output. All these observations match what we found from our blackbox analysis and it really does seem like Base64 encoding. For additional confirmation, I also Googled the >> 18 & 0x3f seen in the code and the results all show code for Base64 encoding.
The difference here is that NaRvJT1B/m6AOXL9VDFIbUGkC+sSnzh5jxQ273d4lHPg0wcEpYqruWyfZoM8itKe is used as a custom character set with - as the padding instead of the standard ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/ with = as the padding.
At this point I just tried decoding the contents of challenge.txt in CyberChef with the custom Base64 character set and it turns out 4 rounds of decoding are needed.
The drawback though is that 5 unused arguments are added to some of the anonymous functions thus cluttering up the output. In the cleaned-up output with renamed variables below, I’ve just renamed these arguments to {5argsthing} so that it’s less of an eyesore.
Main_main_closure=>>=$fMonadIO(fmap$fFunctorIOunpackgetLine)(\input->>>=$fMonadIOgetCurrentTime(\currentTimeUTC->>>=$fMonadIO($(\{5argsthing}->return$fMonadIO)(${- for each of the random numbers in the generate list, mask it with & 0xff -}(map(\{5argsthing}->.(\{5argsthing}->fromIntegral$fIntegralInt32$fNumInteger)(\loc_4231256_arg_0->.&.$fBitsInt32loc_4231256_arg_0(\{5argsthing}->fromInteger$fNumInt32(IS255))))){- generate list of random numbers based on current floored time in seconds -}($(\flooredtime_4230968->rng_4229392$fIntegralIntflooredtime_4230968){- floor . nominalDiffTimeToSeconds . utcTimeToPOSIXSeconds -}(.(case$fRealFracFixed$fHasResolutionTYPEE12ofloc_4228312_case_tag_DEFAULT_arg_0@_DEFAULT->floor<index0inloc_4228312_case_tag_DEFAULT>$fIntegralInt)(.nominalDiffTimeToSecondsutcTimeToPOSIXSeconds)currentTimeUTC))))(\randnumbers_4230768->>>=$fMonadIO($(\{5argsthing}->return$fMonadIO)(zipWith(\{5argsthing}->xor$fBitsInt32)(map(\{5argsthing}->fromIntegral$fIntegralWord8$fNumInt32)input)(map(fromIntegral$fIntegralInteger$fNumInt32)randnumbers_4230768)))(\output_4229920->$putStr($pack(map(\{5argsthing}->.chr(fromIntegral$fIntegralInt32$fNumInt))output_4229920)))))){- i think this recursively creates a list of random numbers and returns it -}rng_4229392=\IntegralIntargtimearg->:a4_4229128(rng_4229392$fIntegralInt32a4_4229128)a4_4229128=xor$fBitsInt32a3_4228928(shiftL$fBitsInt32a3_4228928(I#5))a3_4228928=xor$fBitsInt32a2_4228728(shiftR$fBitsInt32a2_4228728(I#17))a2_4228728=xor$fBitsInt32a1_4228568(shiftL$fBitsInt32a1_4228568(I#13))a1_4228568=fromIntegralIntegralIntarg$fNumInt32timearg{-
xorshift32?:
a1 = from timearg
a2 = a1 ^ (a1 << 13)
a3 = a2 ^ (a2 >> 17)
a4 = a3 ^ (a3 << 5)
return a4?
-}
Now to be frank I can’t explain in-depth how the Haskell code flow exactly works, but this is my general interpretation:
Input string is taken from stdin
Current UTC time is retrieved, converted to POSIX time, and floored to seconds
POSIX time in seconds is used to seed a Xorshift32 generator function
Each byte in the input string is XOR’d with a 0xFF-masked integer generated from the Xorshift32 function
Final output is converted to characters, packed together and printed
The possible UTC time used to produce the contents in challenge.bin could range from the time binary was made to the time challenge.bin was made. I replicated everything in Python and bruteforced for the time used although it turns out to be just the exact last modified time of challenge.bin.
importbinascii# # alternative from https://stackoverflow.com/questions/1375897/how-to-get-the-signed-integer-value-of-a-long-in-python# def toSigned32(n):# n = n & 0xffffffff# return (n ^ 0x80000000) - 0x80000000deftoSigned(n,bits):n=n&(2**bits-1)returnn|(-(n&(1<<(bits-1))))if__name__=="__main__":challenge_bin=binascii.unhexlify("7cabe72416c36067139149c04e20c4a248d0318c704a0ce2c57c98af03986577")max_time=1654353866# last modified time of challenge.bin: Saturday, June 4, 2022 10:44:26 PM GMT+08:00min_time=1654352191# last modified time of binary: Saturday, June 4, 2022 10:16:31 PM GMT+08:00fortimeinrange(max_time,min_time,-1):# reset seed stateseed=timedefxorshift32():globalseedseed^=toSigned(seed<<13,32)seed^=toSigned(seed>>17,32)seed^=toSigned(seed<<5,32)returnseedoutput=""forcinchallenge_bin:o=c^(xorshift32()&0xFF)output+=chr(o)# print(time, output)ifoutput[:5]=="grey{":print("found: {}{}".format(time,output))
From the Javascript snippet above where “flag” is found, it seems that we just need to call genFlag() so that’s what I did right in my Firefox Browser Console and out came the flag “5uch_4_pr0_g4m3r”.
Flag: greyctf{5uch_4_pr0_g4m3r}
flappy-o
I know you cheated in flappy-js. This time the game is written in C, I don’t think you can cheat so easily. Or can you?
Show me your skills by getting a score of at least 64.
flappybird: ELF 64-bit LSB shared object, x86-64, version 1(SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=7b93a7bc7eee1ab668de0cad3ed2fb3927d0ec45, for GNU/Linux 3.2.0, with debug_info, not stripped
This time the Flappy Bird game runs in an unstripped 64-bit ELF and you’re hinted to patch it, but I found it easier to just lift out the flag decoding functions and run them separately.
In main, gameLoop() is called which calls updateAndDrawFlag().
The arrays flag1 and flag2 are populated by genFlag1() and genFlag2() respectively based on your score and actualScore. Linear-feedback shift register (LFSR) functions lsfr1() and lsfr2() are used to achieve this along with hardcoded arrays key1 and key2 as well as a hardcoded lsfr2_seed.
Once we know these key components of the flag generation, we can copy the relevant pseudocode out, compile it and run it to print both flags. We just have to increment score in a loop to simulate how it is incremented by checkAndHandleCollision() in gameLoop(). More specifically, it should be incremented until 10000 * 8 * 25 because actualScore = score / 8; and the condition (actualScore / 10000) <= 25 is used to generate the 25 DWORDs of the 2nd flag.
We’re supposed to look for some image asset in the APK, so I used apktool to decode the APK with apktool d memory-game.apk and searched with ripgrep in the res/drawable* folders because that’s where image resources are supposed to be stored according to the Android Developer Documentation.
Turns out that there’s a flag.png in res/drawable-hdpi/ with the flag text in the image.
1
2
a@a:/apktool/memory-game/res$ rg --files res/drawable* | rg flag
res/drawable-hdpi/flag.png
Flag: grey{th1s_1s_dr4w4bl3_bu7_e4s13r_t0_7yp3}
🩸 Memory Game (Part 2)
Can you finish MASTER difficulty in 20 seconds? If you can, the flag will be given to you through logcat with the tag FLAG.
I opened the APK in JADX and did a text search for “flag” and found results in com.snatik.matches.engine.Engine.onEvent(FlipCardEvent). This should be what we’re looking for as the code Log.i("FLAG", ...) matches the challenge’s description of the flag being printed “through logcat with the tag FLAG”.
Now that we know which part of the code to look at, I switched to Bytecode Viewer to decompile the APK because I prefer the built-in Procyon Decompiler’s output.
Below is the relevant segment of the code in com.snatik.matches.engine.Engine.onEvent(FlipCardEvent) where the flag is logged if MASTER difficulty was chosen and the game was finished within the 20 seconds time limit:
I was too lazy to set up emulators and Android Debug Bridge (adb) so I decided to just copy and port everything above over to Java including com.snatik.matches.rng.MTRandom.class and com.snatik.matches.rng.Rnd.class in order to print out the flag.
importjavax.crypto.spec.*;importjava.security.spec.*;importjavax.crypto.*;importjava.security.*;importjava.util.*;importjava.nio.charset.*;importjava.io.*;classMain{publicstaticvoidmain(Stringargs[]){SecretKeyFactoryinstance=null;Cipherinstance2=null;try{instance=SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");instance2=Cipher.getInstance("AES/CBC/PKCS5Padding");}catch(finalNoSuchAlgorithmExceptionex9){}catch(finalNoSuchPaddingExceptionex10){}Rnd.reSeed();finalbyte[]array=newbyte[16];for(inti=0;i<16;++i){array[i]=(byte)Rnd.get(256);}finalPBEKeySpecpbeKeySpec=newPBEKeySpec("1.01.001007".toCharArray(),array,65536,256);Keykey2=null;try{key2=newSecretKeySpec(((SecretKeyFactory)instance).generateSecret(pbeKeySpec).getEncoded(),"AES");}catch(finalInvalidKeySpecExceptionex3){}finalbyte[]array2=newbyte[16];for(intj=0;j<16;++j){array2[j]=(byte)Rnd.get(256);}finalIvParameterSpecivParameterSpec=newIvParameterSpec(array2);try{instance2.init(2,key2,ivParameterSpec);}catch(finalInvalidKeyExceptionex4){}catch(finalInvalidAlgorithmParameterExceptionex5){}try{System.out.println(newString(instance2.doFinal(Base64.getDecoder().decode(newString("diDrBf4+uZMtDV+0k/3BCGM4xyTpEyGEuUFYegIaSjQyQcgfIfZRbvGQ9hHMqnuflNCKv4HW/NXq93j4QqLc/Q==").getBytes("UTF-8"))),StandardCharsets.UTF_8));}catch(finalIllegalBlockSizeExceptionex6){}catch(finalBadPaddingExceptionex7){}catch(finalUnsupportedEncodingExceptionex8){}}}classRnd{privatestaticMTRandomrnd;static{Rnd.rnd=newMTRandom((long)(-1966625086));}publicRnd(){super();}publicstaticfloatget(){returnRnd.rnd.nextFloat();}publicstaticintget(finalintn){finaldoublenextDouble=Rnd.rnd.nextDouble();finaldoublen2=n;Double.isNaN(n2);return(int)Math.floor(nextDouble*n2);}publicstaticintget(finalintn,finalintn2){finaldoublenextDouble=Rnd.rnd.nextDouble();finaldoublen3=n2-n+1;Double.isNaN(n3);returnn+(int)Math.floor(nextDouble*n3);}publicstaticbooleannextBoolean(){returnRnd.rnd.nextBoolean();}publicstaticdoublenextDouble(){returnRnd.rnd.nextDouble();}publicstaticdoublenextGaussian(){returnRnd.rnd.nextGaussian();}publicstaticintnextInt(){returnRnd.rnd.nextInt();}publicstaticintnextInt(finalintn){finaldoublenextDouble=Rnd.rnd.nextDouble();finaldoublen2=n;Double.isNaN(n2);return(int)Math.floor(nextDouble*n2);}publicstaticvoidreSeed(){Rnd.rnd=newMTRandom((long)(-1966625086));}}classMTRandomextendsRandom{privatestaticfinallongDEFAULT_SEED=5489L;privatestaticfinalintLOWER_MASK=Integer.MAX_VALUE;privatestaticfinalintM=397;privatestaticfinalint[]MAGIC;privatestaticfinalintMAGIC_FACTOR1=1812433253;privatestaticfinalintMAGIC_FACTOR2=1664525;privatestaticfinalintMAGIC_FACTOR3=1566083941;privatestaticfinalintMAGIC_MASK1=-1658038656;privatestaticfinalintMAGIC_MASK2=-272236544;privatestaticfinalintMAGIC_SEED=19650218;privatestaticfinalintN=624;privatestaticfinalintUPPER_MASK=Integer.MIN_VALUE;privatestaticfinallongserialVersionUID=-515082678588212038L;privatetransientbooleancompat;privatetransientint[]ibuf;privatetransientint[]mt;privatetransientintmti;static{MAGIC=newint[]{0,-1727483681};}publicMTRandom(){super();this.compat=false;}publicMTRandom(finallongn){super(n);this.compat=false;}publicMTRandom(finalbooleancompat){super(0L);this.compat=compat;longcurrentTimeMillis;if(compat){currentTimeMillis=5489L;}else{currentTimeMillis=System.currentTimeMillis();}this.setSeed(currentTimeMillis);}publicMTRandom(finalbyte[]seed){super(0L);this.compat=false;this.setSeed(seed);}publicMTRandom(finalint[]seed){super(0L);this.compat=false;this.setSeed(seed);}publicstaticint[]pack(finalbyte[]array){finalintlength=array.length;finalintn=array.length+3>>>2;finalint[]array2=newint[n];intn2;for(inti=0;i<n;i=n2){n2=i+1;intn3;if((n3=n2<<2)>length){n3=length;}intn4;intn5;for(n4=n3-1,n5=(array[n4]&0xFF);(n4&0x3)!=0x0;--n4,n5=(n5<<8|(array[n4]&0xFF))){}array2[i]=n5;}returnarray2;}privatevoidsetSeed(intmti){if(this.mt==null){this.mt=newint[624];}this.mt[0]=mti;mti=1;while(true){this.mti=mti;mti=this.mti;if(mti>=624){break;}finalint[]mt=this.mt;mt[mti]=(mt[mti-1]^mt[mti-1]>>>30)*1812433253+mti;++mti;}}@Overrideprotectedfinalintnext(finalintn){try{if(this.mti>=624){intn2=0;inti;while(true){i=n2;if(n2>=227){break;}finalint[]mt=this.mt;finalintn3=mt[n2];finalintn4=n2+1;finalintn5=(Integer.MAX_VALUE&mt[n4])|(Integer.MIN_VALUE&n3);mt[n2]=(MTRandom.MAGIC[n5&0x1]^(mt[n2+397]^n5>>>1));n2=n4;}while(i<623){finalint[]mt2=this.mt;finalintn6=mt2[i];finalintn7=i+1;finalintn8=(n6&Integer.MIN_VALUE)|(mt2[n7]&Integer.MAX_VALUE);mt2[i]=(MTRandom.MAGIC[n8&0x1]^(mt2[i-227]^n8>>>1));i=n7;}finalint[]mt3=this.mt;finalintn9=(Integer.MAX_VALUE&mt3[0])|(Integer.MIN_VALUE&mt3[623]);mt3[623]=(MTRandom.MAGIC[n9&0x1]^(mt3[396]^n9>>>1));this.mti=0;}finalintn10=this.mt[this.mti++];finalintn11=n10^n10>>>11;finalintn12=n11^(n11<<7&0x9D2C5680);finalintn13=n12^(n12<<15&0xEFC60000);return(n13^n13>>>18)>>>32-n;}finally{}}@OverridepublicfinalvoidsetSeed(finallongn){synchronized(this){if(this.compat){this.setSeed((int)n);}else{if(this.ibuf==null){this.ibuf=newint[2];}finalint[]ibuf=this.ibuf;ibuf[0]=(int)n;ibuf[1]=(int)(n>>>32);this.setSeed(ibuf);}}}publicfinalvoidsetSeed(finalbyte[]array){this.setSeed(pack(array));}publicfinalvoidsetSeed(int[]mt){try{finalintlength=mt.length;if(length!=0){intn;if(624>length){n=624;}else{n=length;}this.setSeed(19650218);intn2=1;intn3=0;intn7;for(inti=n;i>0;--i,n3=n7){finalint[]mt2=this.mt;finalintn4=mt2[n2];finalintn5=n2-1;mt2[n2]=(n4^(mt2[n5]>>>30^mt2[n5])*1664525)+mt[n3]+n3;finalintn6=n2+1;++n3;if((n2=n6)>=624){mt2[0]=mt2[623];n2=1;}if((n7=n3)>=length){n7=0;}}for(intj=623;j>0;--j){mt=this.mt;finalintn8=mt[n2];finalintn9=n2-1;mt[n2]=(n8^(mt[n9]>>>30^mt[n9])*1566083941)-n2;if(++n2>=624){mt[0]=mt[623];n2=1;}}this.mt[0]=Integer.MIN_VALUE;return;}thrownewIllegalArgumentException("Seed buffer may not be empty");}finally{}}}
You can run it here in OnlineGDB and it’ll give you the flag as the output.
This means that the ELF is likely packed with UPX. If vanilla UPX is used, such binaries can simply be unpacked with the official upx binary itself by adding the -d flag:
1
2
3
4
5
6
7
8
9
10
C:\grey>upx -d parcel -o unpackedparcel Ultimate Packer for eXecutables
Copyright (C) 1996 - 2020
UPX 3.96w Markus Oberhumer, Laszlo Molnar & John Reiser Jan 23rd 2020
File size Ratio Format Name
-------------------- ------ ----------- -----------
428392 <- 105224 24.56% linux/amd64 unpackedparcel
Unpacked 1 file.
1
2
a@a:/grey$ file unpackedparcel
unpackedparcel: ELF 64-bit LSB executable, x86-64, version 1(SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=f616d88f62c910d6dddddd732eb503f9c61fa88a, for GNU/Linux 3.2.0, not stripped
Ok now where’s the flag? If we run parcel or unpackedparcel, it’ll ask us for the address of certain functions like h12. Now that we have the unpacked unstripped binary unpackedparcel, we can get those addresses easily using nm -t d unpackedparcel | grep <function_name>.
1
2
3
4
5
6
7
8
9
10
11
a@a:/grey$ nm -t d unpackedparcel | grep h12
0000000004207125 T h12
a@a:/grey$ nm -t d unpackedparcel | grep t80
0000000004221073 T t80
a@a:/grey$ nm -t d unpackedparcel | grep g20
0000000004206113 T g20
a@a:/grey$ ./parcel
Tell me the address of the function h12 (in decimal): 4207125Tell me the address of the function t80 (in decimal): 4221073Tell me the address of the function g20 (in decimal): 4206113Congrats! grey{d1d_y0u_us3_nm_0r_objdump_0r_gdb_0r_ghidra_0r_rizin_0r_ida_0r_binja?}
a@a:/grey$ cat ghost
I got message for you:
NTQgNjggNjkgNzMgMjAgNjkgNzMgMjAgNmUgNmYgNzQgMjAgNjkgNzQgMjAgNmQgNjEgNmUgMmMgMjAgNzQgNzIgNzkgMjAgNjggNjEgNzIgNjQgNjUgNzIgMjE=++++++++++[>+>+++>+++++++>++++++++++<<<<-]>>>++++++++++++++.>++++.+.++++++++++.<<++.>>----------.++++++++++.<<.>>----------.+++++++++++.<---------------------.<.>>------.-------------.+++++++.
pi pi pi pi pi pi pi pi pi pi pika pipi pi pipi pi pi pi pipi pi pi pi pi pi pi pi pipi pi pi pi pi pi pi pi pi pi pi pichu pichu pichu pichu ka chu pipi pipi pipi pipi pi pi pi pi pi pi pi pi pikachu pi pi pi pikachu ka ka ka pikachu pichu pichu pi pi pikachu pipi pipi pi pi pikachu pi pikachu
KRUGS4ZANFZSA3TPOQQHI2DFEBTGYYLHEBWWC3RAIQ5A====synt{abgGungFvzcyr:C}Me: Message received.
The text file given contains a few different kinds of encoded messages, but the huge chunk of empty spaces/lines in the middle are the most suspicious. Let’s take a closer look at the hex values used.
The empty space chunk consists of only 2 values — 0x20s and 0x09s, so my first thought was that it represented either binary encoding or morse code. Brief experimentation in CyberChef will reveal that it is indeed binary encoding where the 0x20s represent 0s and the 0x09s represent 1s. Here is the decoding done in CyberChef.
Flag: grey{gh0s7_byt3$_n0t_1nvisIbl3}
Calculator
Calculator is a nice invention
mechfrog88
US Instance: nc 35.184.68.167 15521
EU Instance: nc 34.77.124.65 15521
nc challs.nusgreyhats.org 15521
a@a:~$ nc challs.nusgreyhats.org 15521 __________
.'----------`.
| .--------. |
| |########| | __________
| |########| | /__________\
.--------| `--------'|------| --=-- |-------------.
|`----,-.-----'|o======||| ______|_|_______ |__________||| / %%%%%%%%%%%% \ || / %%%%%%%%%%%%%% \ || ^^^^^^^^^^^^^^^^^^^^ |+-----------------------------------------------------+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
How fast are you on calculating basic math expression?
Answer 100 math questions in 30 seconds to the flag
Operations include:
1. add x y -> returns x + y
2. mul x y -> returns x * y
3. sub x y -> returns x - y
4. neg x -> returns -x
5. inc x -> returns x + 1Example1: mul add 12 sub 51Ans1: (1 + 2) * (5 - 1)=12Example2: add mul sub 32 inc 53Ans2: (3 - 2) * (5 + 1) + 3=9Send START when you are ready!
Prefix notation is used to evaluate these math expressions, so I just took a Python prefix calculator script from https://stackoverflow.com/a/5307565 and edited the tokens to match those required in this challenge:
fromcollectionsimportdequefrompwnimport*defparse(tokens):token=tokens.popleft()iftoken==b'add':returnparse(tokens)+parse(tokens)eliftoken==b'sub':returnparse(tokens)-parse(tokens)eliftoken==b'mul':returnparse(tokens)*parse(tokens)eliftoken==b'neg':returnparse(tokens)*(-1)eliftoken==b"inc":returnparse(tokens)+1else:# must be just a numberreturnint(token)if__name__=='__main__':conn=remote('challs.nusgreyhats.org',15521)conn.recvuntil(b'!')conn.recvline()conn.sendline(b'START')foriinrange(100):problem=conn.recvline()problem=problem.rstrip(b'\n')answer=(parse(deque(problem.split())))conn.sendline(bytes(str(answer),'ascii'))conn.recvline()result=conn.recvline()print(result)flag=conn.recvline()print(flag)
Script output:
1
2
3
4
5
a@a:/grey$ python3 prefix_calculator.py
[+] Opening connection to challs.nusgreyhats.org on port 15521: Done
b'You done it in 21.74351406097412 seconds!\n'b'grey{prefix_operation_is_easy_to_evaluate_right_W2MQshAYVpGVJPcw}\n'[*] Closed connection to challs.nusgreyhats.org port 15521
I ran binwalk -e on firmware.img to extract the Squashfs filesystem from the image so that we can find the file we are told to look for.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
a@a:/grey$ binwalk -e firmware.img
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
1105744 0x10DF50 uImage header, header size: 64 bytes, header CRC: 0x39150100, created: 1978-08-22 13:45:13, image size: 204509413 bytes, Data Address: 0x33FF2FE1, Entry Point: 0x2099E5, data CRC: 0x52E3, OS: 4.4BSD, image name: ""1164816 0x11C610 CRC32 polynomial table, little endian
1187240 0x121DA8 device tree image (dtb)1189888 0x122800 uImage header, header size: 64 bytes, header CRC: 0x2E68CDF5, created: 2021-10-24 09:01:35, image size: 596535 bytes, Data Address: 0x4000000, Entry Point: 0x4000000, data CRC: 0x3982880E, OS: Firmware, CPU: ARM, image type: Firmware Image, compression type: none, image name: "U-Boot 2019.07 for zynq board"1191944 0x123008 CRC32 polynomial table, little endian
1524744 0x174408 device tree image (dtb)1549184 0x17A380 SHA256 hash constants, little endian
1772908 0x1B0D6C device tree image (dtb)1789952 0x1B5000 device tree image (dtb)1790180 0x1B50E4 gzip compressed data, maximum compression, from Unix, last modified: 1970-01-01 00:00:00 (null date)5248724 0x5016D4 MySQL MISAM index file Version 106232576 0x5F1A00 device tree image (dtb)18874368 0x1200000 Squashfs filesystem, little endian, version 4.0, compression:xz, size: 2285698 bytes, 840 inodes, blocksize: 262144 bytes, created: 2021-10-24 09:01:35
After that, I just searched for the flag fragment grey in the extracted filesystem using ripgrep and the flag was found in /etc/inittab.
1
2
3
a@a:/grey/_firmware.img.extracted/squashfs-root$ rg grey
etc/inittab
5:# you found me! grey{inittab_1s_4n_1mp0rt4nt_pl4c3_t0_l00k_4t_wh3n_r3v3rs1ng_f1rmw4r3}
Open the given PCAP in Wireshark and use File -> Export Objects -> HTTP to view HTTP objects transmitted.
There’s a ctf.png in the multipart/form-data HTTP object with the flag string in a PNG tEXt chunk labelled “Author”.
Flag: grey{wireshark_exiftool_are_good}
Web
🍭 Too Fast
Something went by too quickly!
Dragonym
hxxp://challs.nusgreyhats.org:14004/
I analyzed the given website but nothing stood out in the HTML source or the HTTP request/response headers, so I ran a “Discover content” scan with Burp Suite and it quickly found /admin.php with the flag in the response body.
Request:
1
2
3
4
5
6
7
8
GET/admin.phpHTTP/1.1Host:challs.nusgreyhats.org:14004Accept-Encoding:gzip, deflateAccept:*/*Accept-Language:en-US;q=0.9,en;q=0.8User-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.67 Safari/537.36Connection:closeCache-Control:max-age=0
Response:
1
2
3
4
5
6
7
8
9
HTTP/1.1302FoundHost:challs.nusgreyhats.org:14004Date:Fri, 10 Jun 2022 13:27:20 GMTConnection:closeX-Powered-By:PHP/7.4.29Location:./index.phpContent-type:text/html; charset=UTF-8<p><spanstyle='color: #ffffff;'>grey{why_15_17_571LL_ruNn1n_4Pn39Mq3CQ7VyGrP}</span></p>