*ctf offical writeup
all files related to *ctf2018 are published at https://github.com/sixstars/starctf2018
Re-wasm
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:
while(True):
s=con.recv()
if "win" in s:
break
fen=getfen(s) #deal with the receive string to generate a fen string
engine.setfenposition(fen)
move=engine.bestmove()["move"]
con.sendline(move)
babystack 2018
Idea
- Make a baby stack overflow task in 2018 (no need to ret2dlresolv)
- Keep it fun
Vulnerability
- Simple stack overflow
Mitigations
- NX/RELRO/CANARY are enabled
- Only disable PIE
- But we have thread !
- http://www.openwall.com/lists/oss-security/2018/02/27/5
- Thread stack address and thread fs register value is close
Exploit
- Just like in the 90s
- Smashing the Stack for Fun and Profit by Aleph One
- Details in
solve.py
warmup
Idea
open('/dev/tty') fails in docker container
.
Vulnerability
- Yet another stack overflow challenge.
Exploit
- Ignore the home made canary and smash the stack as usual.
young_heap
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.
calc
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
primepwn
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
start:
syscall
dec edx
mov esi,ecx
jmp start
note
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.
simpleweb
Idea
the idea is from garzon
[1,3,12].sort()
(3) [1, 12, 3]
Solve
Since you know the sort is in alphabetic order, it's not difficult to find the solution such as 15, 20, 31, 4, 47
.
stackoverflow
Idea
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.
Solve
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)
ssss+ssss2
Idea
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.
Solve
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.
yafu
Solve
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.
primitive
Solve
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
xp3
- First, use
crass
and you will get many files frommisc.xp3
. I hope you can find there is a hint ing01.ks
:the “Flag” is out there ―― there is a secret in the and pictures
. - Then, you may find
504B0304
is at the end ofg01.ks
, and other.ks
files also have some strange text like this. If you are familiar withzip
format, you will understand there is a zip file in s. - 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. - You will find the
pic25.tlg
is237KB
while others are50KB
- Use
tlg2png
and make.tlg
=.png
. You only need to getpic25.tlg.png
and one of others such aspic2.tlg.png
. - Useing BlindWaterMark Tool and you will find the key
Key: NoGalgameNoLife
- 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])
f2.close()
welcome
- Get the binary
welcome
- Download
simplesim-3v0e.tgz
from http://www.simplescalar.com - 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
- And you will find the flag is printed to the screen:
*ctf{we1_t0_*ctf}
ez-js
# -*- 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
before.append(v)
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()
sha1.update(flag.encode('utf8'))
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])))
print(sha1_check(possible))
main()
baby-droid
Explain
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
.
Motivation
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
I hide the library load operation for
b.so
inJNI_Onload
ofa.so
.
Though you can guess thatb.so
must have been loaded in this challenge.a.so
ptrace itself when loaded to prevent debugger.a.so
bindstringFromJNI
to supply checksum fora.so
, which will be called byb.so
.b.so
checked app name in.init
function to prevent reuse theso
in another app.b.so
decrypt theJNI_Onload
and key function in.init
.b.so
decrypt the key function again inJNI_Onload
.b.so
assert the checksum fora.so
andb.so
is the same to prevent binary patch.b.so
bind the keycheck
method, and the strings for binding are encrypted to prevent string search.b.so
checked the trace id to prevent debugging.b.so
calledstringFromJNI
to get the final key froma.so
.Most of keys are related to the checksum to make the patched binary unable to decrypt.
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.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 encryptedJNI_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
.
urlparse+urlparse2
Preface
Really terrible for this challenge, all guys submitted writeup used the unintended solution.
Main
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.
Unintended
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.