XCTF联赛小秘       2018-05-04 10:20:37 12602  6

New Document

*ctf offical writeup

all files related to *ctf2018 are published at https://github.com/sixstars/starctf2018


I feel sorry that an old version of binary which has no entry is offered at the begining.I upload the corret version after we found it.

Source code: https://github.com/sixstars/starctf2018/tree/master/re-wasm

You can load the wasm module into chrome and debug it. The code does something like TEA encrypt and you can get the flag by decrypting the cipher.

PPC-Chess Master

The AI of my program is very stupid. You can use any popular chess engine(such as stockfish) to beat it.

A reference tool: https://github.com/iamjarret/pystockfish

A possible solution to win:

    if "win" in s:
    fen=getfen(s)  #deal with the receive string to generate a fen string

babystack 2018


  • Make a baby stack overflow task in 2018 (no need to ret2dlresolv)
  • Keep it fun


  • Simple stack overflow



  • Just like in the 90s
  • Smashing the Stack for Fun and Profit by Aleph One
  • Details in solve.py



  • open('/dev/tty') fails in docker container.


  • Yet another stack overflow challenge.


  • Ignore the home made canary and smash the stack as usual.


the heap challenge is boring, so I write a new method for malloc and free. =,=
you can find the chunk and bins_ptr(a simple version of main_arena) struct at https://github.com/sixstars/starctf2018/tree/master/pwn-young_heap

chunk struct
| Last Chunk Size    |  last bit 0:free, 1:use
|--------------------|  last two bit: 1:mmaped chunk
| Chunk Size         |
| Content or Next ptr|
|                    |

there is no bug in libheap.so (for now), but in program, when you edit chunk, you can overwrite the next chunk's last chunk size after you feed the full of the chunk you using. Finally you can use off by one bug to change the top chunk size.

At initiation, program call mmap to allocate two memory chunks, one for top chunk, one for bins_ptr. The distance between two memory chunks is a const value (but different from different machine). Therefore, you can change the top chunk size to a big value to overwrite bins_ptr, then you can malloc a fake chunk at got table, control the execution flow.


there is a bug when parsing input, it doesn't check whether it's a legal expression.
so we can input like "++" to leak stack data, in this way we can get canary value, libc address and stack address.

Then we use some illeagal input to modify stack data to construct ROP, but the input size is limited so you should make full use of the stack space.

The most difficult things that we can't get the shell because of seccomp, when program return, it will close 0 1 2, which means we can't get any output and send any payload again!

we can use the following steps to read the flag

  • call mprotect to make stack excuateable
  • build a socket fd using syscall
  • connect your own remote server using syscall
  • read shellcode to stack from your socket fd
  • jump to shellcode, open,read,write flag to your socket fd


a bad syscall will set rip to rcx, so here is steps:

  • gets rip
  • use read syscall to overwrite opcode
  • send real shellcode

a prime code example like this

    dec edx
    mov esi,ecx
    jmp start


scanf("%256s",buffer) can read no more than 256 bytes, and set the next byte to "\x00", which means it can change 257 bytes!

in sub_400D92, we send the payload with the length of 256, exactly change the rbp of main.

char *sub_400D92()
  char s; // [rsp+0h] [rbp-100h]

  _isoc99_scanf("%256s", &s);
  return strdup(&s);

when it return to main, the variables has been disrupted. Using a meticulously constructed payload, we can control the scanf's first argv, and set it to "%s" or whatever you like, we can overflow the stack and then use ret to csu to get the shell.



the idea is from garzon

(3) [1, 12, 3]


Since you know the sort is in alphabetic order, it's not difficult to find the solution such as 15, 20, 31, 4, 47.



The original idea is, even for a trivial stack overflow, if you cannot input some special chars like \x00, the exploit would be much harder.


Since chars below \x20 are not allowed, it's quite natural to consider partial overwrite. For the given libc we have one_gadget with printable low-bits but we need to leak the libc_base first. After partial the stored rbp in stack, we can add an offset towards the local variables. And the message OVERFLOW! and no overflow can be used as side channel to leak some value in stack byte by byte. Partial write of rbp require the knowledge of lowest byte of current stack address but you can just guess and have around 1/16 chance to win.

(Sadly at least one unintended solution existed during competition)



The original idea of this challenge is from KRACK. If you are familiar with WPA2 you can see the imitates the process of WPA2 handshake although the protocol fields are designed (quite arbitrarily) by myself.

By the way, the secret in is quite short. But due to the KDF used, I think it's impossible to bruteforce it.


Both two service use AES in CTR mode incorrectly, which leads to reuse of xor key. If you get pairs of long plaintext and ciphertext you can decrypt some of the future messages.

For ssss, just store a long string and read it, then you would have enough xor key to recover the flag.

For ssss2, the stored string is truncated and read the stored flag is much harder. But with the idea of KRACK, you can reenter the final step of handshake so that the key used will be misaligned by one. You can make two connections and misalign one of them. The key for received message in one connection will be the key for sent message in another connection, so you can keep sending valid messages. After collecting enough xor keys you can recover the flag correctly.



When yafu delete factors, it fails to update the factor type correctly. So it's possible to fool yafu if you use a smooth number which consists of many small primes. Follow the link in solve.py for more details.

I expect someone would find it when feeding some special cases (like the smooth number as mentioned above) towards yafu. But maybe most players instead focused on the prime test. I released a hint (yafu can be wrong) but seems (as xdd suggested) they just turned to prime test inside yafu lol.



The ADD ROTATE XOR are three basic operations for many modern block ciphers. And it's possible to build any sbox using ADD and ROTATE only. For example, operations (0,2),(1,7),(0,255),(1,1) gives a construction for transposition of adjacent elements 254 and 255. However, for arbitrary pair a,b the direct construction with ADD ROTATE can take too much steps while we have a limit of 2000. But with XOR operation, we can change any pair a,b to 0,1 with steps in proportion to element's bit number. Since we can swap any pair now, it would be trivial to build a proper cipher.

More details of my construction can be found in solve.py


  1. First, use crass and you will get many files from misc.xp3. I hope you can find there is a hint in g01.ks: the “Flag” is out there ―― there is a secret in the and pictures.
  2. Then, you may find 504B0304 is at the end of g01.ks, and other .ks files also have some strange text like this. If you are familiar with zip format, you will understand there is a zip file in s.
  3. It's easy to write a python to get the zip file from g01.ks ~ g55.ks. You will find it is encrypted and you need a key.
  4. You will find the pic25.tlg is 237KB while others are 50KB
  5. Use tlg2png and make .tlg = .png. You only need to get pic25.tlg.png and one of others such as pic2.tlg.png.
  6. Useing BlindWaterMark Tool and you will find the key Key: NoGalgameNoLife
  7. With the key, you can get flag from zip: *ctf{Hope_Every0ne_Has_A_Happy_End2333}

import codecs
from pwn import *
with open('./out.zip', 'w') as f1:
    for i in range(1, 56):
        ks = './g%02d.ks' % (i)
        f2 = codecs.open(ks, 'r', 'utf16')
        line = f2.readlines()[-1]
        data = line[3::]
        f1.write(p32(int(data, 16))[::-1])


  1. Get the binary welcome
  2. Download simplesim-3v0e.tgz from http://www.simplescalar.com
  3. Install the simplesim by following steps:
    tar xvf simplesim-3v0e.tgz
    cd simplesim-3.0
    make config-pisa
    make sim-fast #(others is also ok)
    ./sim-fast welcome
  4. And you will find the flag is printed to the screen: *ctf{we1_t0_*ctf}


# -*- coding: utf8 -*-
import hashlib
import base58

def check_printable(c):
    return c = 0x20 and c = 0x7e

def part1():
    after = [0xb8, 0x34, 0x2e, 0x58, 0x96, 0x6c]
    before = []
    possible = []

    for v in after:
        v = v4|v4&0xff
        v = (v&0xcc)2|(v&0x33)2
        v = (v&0xaa)1|(v&0x55)1
    for i in range(0x20, 0x7f):
        for j in range(0x20, 0x7f):
            a2 = i ^ before[4]
            a3 = j ^ before[5]            
            a4 = i ^ before[2]
            a5 = j ^ before[3]
            if check_printable(a2) and check_printable(a3) and check_printable(a4) and check_printable(a5):
                possible.append(''.join([chr(c) for c in [i, j, a2, a3, a4, a5]]))
    return possible

# you need to find the special character
def part2():
    possible = []
    pT = ['T', 't']
    pH = ['H', 'h']
    pN = ['N', 'n']
    for t in pT:
        for h in pH:
            for n in pN:
                possible.append(''.join([t, h, 'İ', n, 'K']))
    return possible

# this is a simple base58 encode and you can decode easily
def part3():
     return bytes.decode(base58.b58decode('DQuf7TsLf6W'))

# a tree
def part4():
    return 'Interest1n9??!'

def sha1_check(possible):
    true_sha1 = 'da117a29dd40fd69768ee49e35f82113f9868bba'
    for flag in possible:
        sha1 = hashlib.sha1()
        if sha1.hexdigest() == true_sha1:
            return flag

def main():
    f1 = part1()
    f2 = part2()
    f3 = part3()
    f4 = part4()

    possible = []

    for p in f1:
        for q in f2:
            possible.append("*ctf{%s}" % ('_'.join([p, q, f3, f4])))





The code is really a mess, I don't have time to rearrange it.
And I'm not familiar to Android development as most of time, I only do reversing work on Android.
If you'd like to read it, keep calm.

I'm sorry that the app is not well tested on different phone which cause most of you guys failed to run it.
My test phone is really old, Honor 4 Play G621-TL00 with Android 4.4.4 and kernel 3.10.28-gc883740.


I'd like to reproduce some protections I'v ever met.
But it's really a hard work to realize it.
I need to protect the app from both static and dynamic analysis to keep it more fair for all the competitors.
So I made some tradeoff.

Key point

  1. I hide the library load operation for b.so in JNI_Onload of a.so.
    Though you can guess that b.so must have been loaded in this challenge.

  2. a.so ptrace itself when loaded to prevent debugger.

  3. a.so bind stringFromJNI to supply checksum for a.so, which will be called by b.so.

  4. b.so checked app name in .init function to prevent reuse the so in another app.

  5. b.so decrypt the JNI_Onload and key function in .init.

  6. b.so decrypt the key function again in JNI_Onload.

  7. b.so assert the checksum for a.so and b.so is the same to prevent binary patch.

  8. b.so bind the key check method, and the strings for binding are encrypted to prevent string search.

  9. b.so checked the trace id to prevent debugging.

  10. b.so called stringFromJNI to get the final key from a.so.

  11. Most of keys are related to the checksum to make the patched binary unable to decrypt.

  12. There is an snag in the checksum function to assert interval between some operations is less than 2s which prevent you guys to modify some value with debugger manually.
    If interval is too long, the checksum would be wrong and the decryption would fail.
    And I guess, on most Android ARM emulator, the interval would be too long.

  13. Indeed, the checksum is crc32 but removed a reverse_bits operation before function return.
    I don't remember where I download the for crc32 calculation and modification.
    I'm sorry that half of you guys realized the checksum function because of the failure on running app.
    And I'd like to pay great respect to the guys who inferred the checksum by the first four bytes of encrypted JNI_Onload. It's really amazing!
    By the way, maybe you can bypass the crash point with debugger to get the checksum dynamically.
    Or even, maybe you can write an binary to call the function directly.

I only remember the above key points...

PS: There is something interesting if you dig into the code. You can consider why I use Runtime.getRuntime().load(LIB_B, Thread.currentThread().getContextClassLoader()) instead of calling System.load(LIB_B) directly in JNI_Onload of liba.so.



Really terrible for this challenge, all guys submitted writeup used the unintended solution.


The challenge is inspired by nonamestill from Code Blue CTF 2017.
Thanks binja for sharing the source code.

In fact, there should be only one urlparse challenge in design.
However, when I write the exploit for urlparse, I think an info leak is needed.
So I comment the check for small size to allow fgets read nothing, thus avoid terminate null bytes.
However, 0 size not only cause info leak, but can also lead to heap mess.
That's why I can forgive myself because I use the same unintended solution to pwn nonamestill.
And then, I decided to think about a revenge, urlparse2 is born.

Unfortunately, most of you guys used another unintended solution, which haven't been fixed in urlparse2. Regretful for not checking the captured payload carefully.


When created url, the program fgets the input to a global buffer, decode it, and copy to heap.
Normally, assume input size n, the string length is at most n - 1.
However, if the input is ended with %, it will eat the terminate null byte and use the residual chars in global buf which would cause the decoded string longer than original string.
So, n bytes is copied to heap without terminate null byte, thus, info leak and control flow hijacking.

Another key point is how to arrange heap layout when \0 is not allowed with strncpy.
In urlparse1, we can easily decode %00 to generate \0.
However, in urlparse2, all chars after \0 is cleared after decoding.

0ops told me a great bypass for \0 in urlparse2, thanks.
After leaking libc address, they first point the linked list to libc to leak program base address.
Then they point the linked list to the global buffer, construct the fake chunk in global buffer.
Input string starts with \0, the decode will not change the input.
So arbitrary chars is allowed now.

+1 已点过赞