PicoCTF 2019 Crypto Writeups
Preface
PicoCTF was my first introduction to the world of CTF when I played PicoCTF 2021. PicoCTF 2019 is the only CTF available on the PicoGym that I did not participate in. As such, I decided to go back and solve the challenges and write up my solutions. Below are the solutions to all the cryptography challenges from PicoCTF 2019. Each writeup is a brief (informal) explanation of the solution written while solving the challenge, sorted by difficulty.
Challenge Writeups
The Numbers (50 points)
We’re given an image file. Opening it up, we see the numbers 16 9 3 15 3 20 6 { 20 8 5 14 21 13 2 5 18 19 13 1 19 15 14 }
.
It’s pretty obvious that each number corresponds to an alphabet character, which we know because due to flag format, flags must start with picoCTF{
. The hint also tells us that the flag is in the format PICOCTF{}
, which is in all caps, so let us assume all letters are uppercase.
We can see that the numbers map 1 -> A
, 2 -> B
, 3 -> C
, … Z -> 26
. So it’s quite trivial to reverse, and we get the flag: PICOCTF{THENUMBERSMASON}
Easy1 (100 points)
We’re told that we’re solving onetimepad, and that we’re given flag of UFJKXQZQUNB
and key SOLVECRYPTO
. We could solve this manually with the table they provide us, or we could just use an online decoder. More information on onetimepad is available from Cryptohack this is your first time hearing about it.
Decrypting, we get the message CRYPTOISFUN
. Because the hint tells us to wrap our flag with picoCTF{}
. we get our flag! picoCTF{CRYPTOISFUN}
13 (100 points)
We’re given the following text cvpbPGS{abg_gbb_onq_bs_n_ceboyrz}
, that also hints us to ROT13. This message looks suspiciously like the flag format.
ROT13 is a form of a ceaser cipher, which basically shifts each individual letter by a set amount. Wikipedia has a nice article about it.
Plugging it into an online solver such as cyberchef, we get our flag! picoCTF{not_too_bad_of_a_problem}
caesar (100 points)
For this problem, we’re given the following ciphertext: picoCTF{gvswwmrkxlivyfmgsrhnrisegl}
. Note how the picoCTF{}
already seems to be in plaintext.
Given the caeser cipher hint, we once again plug the inside into cyberchef and get our flag. picoCTF{crossingtherubicondjneoach}
la cifra de (200 points)
This challenge gives us a netcat port to connect to. Upon doing so, it gives us a lot of encrypted text.
Encrypted message:
Ne iy nytkwpsznyg nth it mtsztcy vjzprj zfzjy rkhpibj nrkitt ltc tnnygy ysee itd tte cxjltk
Ifrosr tnj noawde uk siyyzre, yse Bnretèwp Cousex mls hjpn xjtnbjytki xatd eisjd
Iz bls lfwskqj azycihzeej yz Brftsk ip Volpnèxj ls oy hay tcimnyarqj dkxnrogpd os 1553 my Mnzvgs Mazytszf Merqlsu ny hox moup Wa inqrg ipl. Ynr. Gotgat Gltzndtg Gplrfdo
Ltc tnj tmvqpmkseaznzn uk ehox nivmpr g ylbrj ts ltcmki my yqtdosr tnj wocjc hgqq ol fy oxitngwj arusahje fuw ln guaaxjytrd catizm tzxbkw zf vqlckx hizm ceyupcz yz tnj fpvjc hgqqpohzCZK{m311a50_0x_a1rn3x3_h1ah3x6kp60egf}
Ehk ktryy herq-ooizxetypd jjdcxnatoty ol f aordllvmlbkytc inahkw socjgex, bls sfoe gwzuti 1467 my Rjzn Hfetoxea Gqmexyt.
Tnj Gimjyèrk Htpnjc iy ysexjqoxj dosjeisjd cgqwej yse Gqmexyt Doxn ox Fwbkwei Inahkw.
Tn 1508, Ptsatsps Zwttnjxiax tnbjytki ehk xz-cgqwej ylbaql rkhea (g rltxni ol xsilypd gqahggpty) ysaz bzuri wazjc bk f nroytcgq nosuznkse ol yse Bnretèwp Cousex.
Gplrfdo’y xpcuso butvlky lpvjlrki tn 1555 gx l cuseitzltoty ol yse lncsz. Yse rthex mllbjd ol yse gqahggpty fce tth snnqtki cemzwaxqj, bay ehk fwpnfmezx lnj yse osoed qptzjcs gwp mocpd hd xegsd ol f xnkrznoh vee usrgxp, wnnnh ify bk itfljcety hizm paim noxwpsvtydkse.
Looking at the challenge name, we can note that La Cifra is a book written by Bellaso, which described a precursor of the vigenere cipher.
After several attempts with online cipher decoders, I stumbled across guballa. Inputting our text there, we get some readable plaintext alongside our flag: picoCTF{b311a50_0r_v1gn3r3_c1ph3r6fe60eaa}
Tapping (200 points)
For this challenge, when we netcat to the server, we get a text that looks like the following.
.--. .. -.-. --- -.-. - ..-. { -- ----- .-. ... ...-- -.-. ----- -.. ...-- .---- ... ..-. ..- -. .---- ..--- -.... .---- ....- ...-- ---.. .---- ---.. .---- }
We get a lot of dots and dashes, which is indicative of morse code (Wikipedia).
Plugging it into any online decoder gives us our flag: PICOCTF{M0RS3C0D31SFUN1261438181}
Flags (200 points)
We are given an image, that looks sort of like a flag but the characters have been replaced with images of actual flags.
After some research, we find that these are maritime signal flags.
Substituting them in, we get our flag: PICOCTF{F1AG5AND5TUFF}
Mr-Worldwide (200 points)
We’re given the following string with what appears to be coordinates: picoCTF{(35.028309, 135.753082)(46.469391, 30.740883)(39.758949, -84.191605)(41.015137, 28.979530)(24.466667, 54.366669)(3.140853, 101.693207)_(9.005401, 38.763611)(-3.989038, -79.203560)(52.377956, 4.897070)(41.085651, -73.858467)(57.790001, -152.407227)(31.205753, 29.924526)}
.
Putting this into a table with their locations, we get
(K)yoto, Japan (35.028309, 135.753082)
(O)desa, Ukraine (46.469391, 30.740883)
(D)ayton, Ohio (39.758949, -84.191605)
(I)stanbul, Turkey (41.015137, 28.979530)
(A)bu Dhabi, UAE (24.466667, 54.366669)
(K)uala Lumpur, Malaysia (3.140853, 101.693207)
_
(A)ddis Ababa, Ethiopia (9.005401, 38.763611)
(L)oja, Ecuador (-3.989038, -79.203560)
(A)msterdam, Netherlands (52.377956, 4.897070)
(S)leepy Hollow, New York (41.085651, -73.858467)
(K)odiak, Alaska (57.790001, -152.407227)
(A)lexandria, Egypt (31.205753, 29.924526)
Putting them together, we get our flag! picoCTF{KODIAK_ALASKA}
rsa-pop-quiz (200 points)
We’re given a netcat, and this netcat service just seems to be a quiz on RSA. They a good Wikipedia link to learn about it which is nice.
The below is my rather long solve script. I’m too lazy to do a proper writeup for this challenge besides that you should know RSA and to read up on it.
# add pwntools
from pwn import *
r = remote('jupiter.challenges.picoctf.org', 1981)
# recv until
r.recvuntil('IS THIS POSSIBLE and FEASIBLE? (Y/N):')
# sendline
r.sendline('Y')
r.recvuntil('n:')
def find_n(p, q):
return p * q
r.sendline(str(find_n(60413,76753)))
r. recvuntil('IS THIS POSSIBLE and FEASIBLE? (Y/N):')
r.sendline('Y')
r.recvuntil('q:')
def find_q(n, p):
return n // p
r.sendline(str(find_q(5051846941, 54269)))
r.recvuntil('IS THIS POSSIBLE and FEASIBLE? (Y/N):')
r.sendline('N')
r.recvuntil('IS THIS POSSIBLE and FEASIBLE? (Y/N):')
r.sendline('Y')
r.recvuntil('totient(n):')
def find_totient(p, q):
return (p - 1) * (q - 1)
r.sendline(str(find_totient(66347, 12611)))
r.recvuntil('IS THIS POSSIBLE and FEASIBLE? (Y/N):')
r.sendline('Y')
r.recvuntil('ciphertext:')
def find_ciphertext(n, e, m):
return pow(m, e, n)
m = 6357294171489311547190987615544575133581967886499484091352661406414044440475205342882841236357665973431462491355089413710392273380203038793241564304774271529108729717
e = 3
n = 29129463609326322559521123136222078780585451208149138547799121083622333250646678767769126248182207478527881025116332742616201890576280859777513414460842754045651093593251726785499360828237897586278068419875517543013545369871704159718105354690802726645710699029936754265654381929650494383622583174075805797766685192325859982797796060391271817578087472948205626257717479858369754502615173773514087437504532994142632207906501079835037052797306690891600559321673928943158514646572885986881016569647357891598545880304236145548059520898133142087545369179876065657214225826997676844000054327141666320553082128424707948750331
r.sendline(str(find_ciphertext(n, e, m)))
r.recvuntil('IS THIS POSSIBLE and FEASIBLE? (Y/N):')
r.sendline('N')
r.recvuntil('IS THIS POSSIBLE and FEASIBLE? (Y/N):')
r.sendline('Y')
r.recvuntil('d:')
q = 92092076805892533739724722602668675840671093008520241548191914215399824020372076186460768206814914423802230398410980218741906960527104568970225804374404612617736579286959865287226538692911376507934256844456333236362669879347073756238894784951597211105734179388300051579994253565459304743059533646753003894559
p = 97846775312392801037224396977012615848433199640105786119757047098757998273009741128821931277074555731813289423891389911801250326299324018557072727051765547115514791337578758859803890173153277252326496062476389498019821358465433398338364421624871010292162533041884897182597065662521825095949253625730631876637
e = 65537
# implement extended euclidean algorithm to find d, given p, q and e
def egcd(a, b):
if a == 0:
return (b, 0, 1)
else:
g, y, x = egcd(b % a, a)
return (g, x - (b // a) * y, y)
_, d, _ = egcd(e, find_totient(p, q))
r.sendline(str(d))
r.recvuntil('IS THIS POSSIBLE and FEASIBLE? (Y/N):')
r.sendline('Y')
r.recvuntil('plaintext:')
p = 153143042272527868798412612417204434156935146874282990942386694020462861918068684561281763577034706600608387699148071015194725533394126069826857182428660427818277378724977554365910231524827258160904493774748749088477328204812171935987088715261127321911849092207070653272176072509933245978935455542420691737433
c = 18031488536864379496089550017272599246134435121343229164236671388038630752847645738968455413067773166115234039247540029174331743781203512108626594601293283737392240326020888417252388602914051828980913478927759934805755030493894728974208520271926698905550119698686762813722190657005740866343113838228101687566611695952746931293926696289378849403873881699852860519784750763227733530168282209363348322874740823803639617797763626570478847423136936562441423318948695084910283653593619962163665200322516949205854709192890808315604698217238383629613355109164122397545332736734824591444665706810731112586202816816647839648399
e = 65537
n = 23952937352643527451379227516428377705004894508566304313177880191662177061878993798938496818120987817049538365206671401938265663712351239785237507341311858383628932183083145614696585411921662992078376103990806989257289472590902167457302888198293135333083734504191910953238278860923153746261500759411620299864395158783509535039259714359526738924736952759753503357614939203434092075676169179112452620687731670534906069845965633455748606649062394293289967059348143206600765820021392608270528856238306849191113241355842396325210132358046616312901337987464473799040762271876389031455051640937681745409057246190498795697239
q = find_q(n, p)
_, d, _ = egcd(e, find_totient(p, q))
def find_m(c, d, n):
return pow(c, d, n)
m = find_m(c, d, n)
r.sendline(str(m))
# convert m to hex
m = hex(m)[2:]
# convert hex to ascii
m = bytearray.fromhex(m).decode()
print(m)
r.interactive()
Running it, we get our flag: picoCTF{wA8_th4t$_ill3aGal..ode01e4bb}
waves over lambda (300 points)
When we run the netcat, we get a huge blob of text. Wavelength / lambda (in the problem name) is equal to frequency, so we can guess that this is a frequency attack.
Running it into decode, we get our flag (which is hinted to be nonstandard flag format): FREQUENCY_IS_C_OVER_LAMBDA_AGFLCGTYUE
.
miniRSA (300 points)
For this challenge, we’re given n, e and c. The objective is to find m, or the plaintext. The trick to this challenge is that our e value is very small (3).
As such, we’re able to brute force the value of m because we know part of our plaintext, as well as due to the fact that cube root is relatively trivial to do.
n = 29331922499794985782735976045591164936683059380558950386560160105740343201513369939006307531165922708949619162698623675349030430859547825708994708321803705309459438099340427770580064400911431856656901982789948285309956111848686906152664473350940486507451771223435835260168971210087470894448460745593956840586530527915802541450092946574694809584880896601317519794442862977471129319781313161842056501715040555964011899589002863730868679527184420789010551475067862907739054966183120621407246398518098981106431219207697870293412176440482900183550467375190239898455201170831410460483829448603477361305838743852756938687673
c = 2205316413931134031074603746928247799030155221252519872650080519263755075355825243327515211479747536697517688468095325517209911688684309894900992899707504087647575997847717180766377832435022794675332132906451858990782325436498952049751141
e = 3
# know that m^e = c mod n
# aka m ^ 3 = c + k * n
# we want to find m (the message)
from gmpy2 import iroot
from tqdm import tqdm
for k in tqdm(range(10**4)):
m = iroot(c + k * n, e)
# convert m to hex
if m[1] == False:
continue
m = hex(int(m[0]))[2:]
# convert hex to ascii
m = bytearray.fromhex(m).decode()
if "picoCTF" in m:
print(k)
print(m)
break
As a side note, while my solution does work, I wrote a lot of unnecessary code. It’s possible to see that c << n (this notation means that c is significantly smaller than n), so we know that K = 0
, so we only need to outright run a cuberoot to get our flag.
Running our solve script, we find the flag: picoCTF{n33d_a_lArg3r_e_d0cd6eae}
b00tl3gRSA2 (400 points)
Explanation is in the solve script below, TLDR; is that e and d are swapped and we’re able to decrypt.
c = 26793577203328495455993279452412488771370833130513294340749912952395025734496908759270670584418133802698542587036345156417940686718862357504472926533581535512125784150450670524192348653214055378493847619077101300211792024394706307785493817561779771950533836852727498000943350283564318244766129798054429547019
n = 106523765577864440391590423093002616209023119431647373133104338283139118541890678665628298040695986943959563554522267672773902052389800377441249485730826465786092280958861087915537991755177840486805331556011383500790252344974243035147020713186411725436845104773204336895019334551708092180345920428480035650337
e = 29114144514254663110825452163539113196149077853116067985415336486680621501172556514010926263102469111192207491777207662156127585370189425221289665203023685582153927795935711231851351742620669553649673634693783103020506016313338913243942735265042462278188017911145400724269290447314047915756435262576944878433
# We're given large values of e. We're told that e and d are swapped, so let's assume that d is 65537.
# to find m given d and e, we can use the following formula:
# m = (c^d) % n
# We can use the following python code to find m:
m = pow(c, 65537, n)
# We can then convert m to hex and then to ascii to get the flag:
print(bytes.fromhex(hex(m)[2:]).decode())
Running script gives us the flag: picoCTF{bad_1d3a5_2152720}
AES-ABC (400 points)
We’re given a script that encrypts a .ppm file (an image file) containing the flag. The description says its using a custom cipher block chaining method, and that this is because AES-ECB is bad. We are also given the encrypted file, which just appears as a bunch of noise.
#!/usr/bin/env python
from Crypto.Cipher import AES
from key import KEY
import os
import math
BLOCK_SIZE = 16
UMAX = int(math.pow(256, BLOCK_SIZE))
def to_bytes(n):
s = hex(n)
s_n = s[2:]
if 'L' in s_n:
s_n = s_n.replace('L', '')
if len(s_n) % 2 != 0:
s_n = '0' + s_n
decoded = s_n.decode('hex')
pad = (len(decoded) % BLOCK_SIZE)
if pad != 0:
decoded = "\0" * (BLOCK_SIZE - pad) + decoded
return decoded
def remove_line(s):
# returns the header line, and the rest of the file
return s[:s.index('\n') + 1], s[s.index('\n')+1:]
def parse_header_ppm(f):
data = f.read()
header = ""
for i in range(3):
header_i, data = remove_line(data)
header += header_i
return header, data
def pad(pt):
padding = BLOCK_SIZE - len(pt) % BLOCK_SIZE
return pt + (chr(padding) * padding)
def aes_abc_encrypt(pt):
cipher = AES.new(KEY, AES.MODE_ECB)
ct = cipher.encrypt(pad(pt))
blocks = [ct[i * BLOCK_SIZE:(i+1) * BLOCK_SIZE] for i in range(len(ct) / BLOCK_SIZE)]
iv = os.urandom(16)
blocks.insert(0, iv)
for i in range(len(blocks) - 1):
prev_blk = int(blocks[i].encode('hex'), 16)
curr_blk = int(blocks[i+1].encode('hex'), 16)
n_curr_blk = (prev_blk + curr_blk) % UMAX
blocks[i+1] = to_bytes(n_curr_blk)
ct_abc = "".join(blocks)
return iv, ct_abc, ct
if __name__=="__main__":
with open('flag.ppm', 'rb') as f:
header, data = parse_header_ppm(f)
iv, c_img, ct = aes_abc_encrypt(data)
with open('body.enc.ppm', 'wb') as fw:
fw.write(header)
fw.write(c_img)
Let’s examine what the encryption does. Most of the code is irrelevant, except for the aes_abc_encrypt
function. We see that that is the function being called to encrypt the data in the image (besides the header). Let us supply the code with comments to explain what it does.
def aes_abc_encrypt(pt):
# set up and complete an AES-ECB encryption
cipher = AES.new(KEY, AES.MODE_ECB)
ct = cipher.encrypt(pad(pt))
# splits the ciphertext into blocks of size 16, and adds a (useless?) IV to the start.
blocks = [ct[i * BLOCK_SIZE:(i+1) * BLOCK_SIZE] for i in range(len(ct) / BLOCK_SIZE)]
iv = os.urandom(16)
blocks.insert(0, iv)
# The ABC "encryption"
for i in range(len(blocks) - 1):
prev_blk = int(blocks[i].encode('hex'), 16)
curr_blk = int(blocks[i+1].encode('hex'), 16)
n_curr_blk = (prev_blk + curr_blk) % UMAX
blocks[i+1] = to_bytes(n_curr_blk)
ct_abc = "".join(blocks)
return iv, ct_abc, ct
We see that the first two lines set up and complete an ECB encryption. This ECB encryption is not very good for encrypting images, which you can learn more about from the picoCTF primer. Essentially, you can still make out the important features of the image.
Furthermore, the ACB layer of “encryption” is trivial to cryptographically reverse, we can see that there is no actual encryption of any kind happening here. So all we need to do is reverse this layer, and we should be able to find an image that resembles the original.
As such, we can write a decryption function that does the opposite, and it should look like this.
def aes_abc_decrypt(data):
blocks = [data[i * BLOCK_SIZE:(i+1) * BLOCK_SIZE] for i in range(len(data) / BLOCK_SIZE)]
for i in range(len(blocks) - 1, 0, -1):
prev_blk = int(blocks[i-1].encode('hex'), 16)
curr_blk = int(blocks[i].encode('hex'), 16)
n_curr_blk = (curr_blk - prev_blk) % UMAX
blocks[i] = to_bytes(n_curr_blk)
pt = "".join(blocks)
return pt
Putting it together in the full script (note that this is python2 and not python3), we get our final decryption script.
#!/usr/bin/env python
import math
BLOCK_SIZE = 16
UMAX = int(math.pow(256, BLOCK_SIZE))
def to_bytes(n):
s = hex(n)
s_n = s[2:]
if 'L' in s_n:
s_n = s_n.replace('L', '')
if len(s_n) % 2 != 0:
s_n = '0' + s_n
decoded = s_n.decode('hex')
pad = (len(decoded) % BLOCK_SIZE)
if pad != 0:
decoded = "\0" * (BLOCK_SIZE - pad) + decoded
return decoded
def remove_line(s):
# returns the header line, and the rest of the file
return s[:s.index('\n') + 1], s[s.index('\n')+1:]
def parse_header_ppm(f):
data = f.read()
header = ""
for i in range(3):
header_i, data = remove_line(data)
header += header_i
return header, data
def pad(pt):
padding = BLOCK_SIZE - len(pt) % BLOCK_SIZE
return pt + (chr(padding) * padding)
def aes_abc_decrypt(data):
blocks = [data[i * BLOCK_SIZE:(i+1) * BLOCK_SIZE] for i in range(len(data) / BLOCK_SIZE)]
for i in range(len(blocks) - 1, 0, -1):
prev_blk = int(blocks[i-1].encode('hex'), 16)
curr_blk = int(blocks[i].encode('hex'), 16)
n_curr_blk = (curr_blk - prev_blk) % UMAX
blocks[i] = to_bytes(n_curr_blk)
pt = "".join(blocks)
return pt
if __name__=="__main__":
with open('body.enc.ppm', 'rb') as f:
header, data = parse_header_ppm(f)
img = aes_abc_decrypt(data)
with open('flag.ppm', 'wb') as fw:
fw.write(header)
fw.write(img)
Running it and opening flag.ppm, we see an image where we can make out our flag. picoCTF{d0nt_r0ll_yoUr_0wN_aES}
b00tl3gRSA3 (450 points)
For this problem, we’re given c, n and e. Normally, this would be difficult to crack. However, we’re told that there are more factors than p and q being used to find n.
As such, this should be easily factorable, as n being hard to factor is dependent on their being only two large primes. An easy way to do this quickly is with alpertron. Alternatively sagemath also makes a lot of the math easy.
c = 42704754052735406804599985162404550917208505149045367410514359889058041377588916230459179154134316215285535894591956591636303593626829643033450913508025377058061916259634563566803741837811274856099177573292396791481075818726357416990649116941645386589122686689674034438375073095576094364461242579481292719315656249094287285530714691674476822040
n = 66514601264420483448802036616153156565076145394756869653310238211595345386209769600392616376651799901273346797246858254587419300955024232786584769372812454825885579789821058159744530956192144856684092850278840586192994348957159701251769136525669117390246082324407952481854534492778237141561829170007424129988838557513621905228559175240710262121
e = 65537
phi = euler_phi(n)
d = inverse_mod(e, phi)
m = pow(c, d, n)
m = hex(m)[2:]
m = bytearray.fromhex(m).decode()
print(m)
Running this gets us our flag: picoCTF{too_many_fact0rs_8606199}
john_pollard (500 points)
This challenge gives us an RSA certificate. The hint tells us the flag is formatted like picoCTF{p,q}
, so it’s obvious that we need to factor the certificate.
To do this, we can import the RSA certificate, and then run the smpy primefactors function, and hope that the modulus n is small.
from Crypto.PublicKey import RSA
from sympy import primefactors
key_encoded='''-----BEGIN CERTIFICATE-----
MIIB6zCB1AICMDkwDQYJKoZIhvcNAQECBQAwEjEQMA4GA1UEAxMHUGljb0NURjAe
Fw0xOTA3MDgwNzIxMThaFw0xOTA2MjYxNzM0MzhaMGcxEDAOBgNVBAsTB1BpY29D
VEYxEDAOBgNVBAoTB1BpY29DVEYxEDAOBgNVBAcTB1BpY29DVEYxEDAOBgNVBAgT
B1BpY29DVEYxCzAJBgNVBAYTAlVTMRAwDgYDVQQDEwdQaWNvQ1RGMCIwDQYJKoZI
hvcNAQEBBQADEQAwDgIHEaTUUhKxfwIDAQABMA0GCSqGSIb3DQEBAgUAA4IBAQAH
al1hMsGeBb3rd/Oq+7uDguueopOvDC864hrpdGubgtjv/hrIsph7FtxM2B4rkkyA
eIV708y31HIplCLruxFdspqvfGvLsCynkYfsY70i6I/dOA6l4Qq/NdmkPDx7edqO
T/zK4jhnRafebqJucXFH8Ak+G6ASNRWhKfFZJTWj5CoyTMIutLU9lDiTXng3rDU1
BhXg04ei1jvAf0UrtpeOA6jUyeCLaKDFRbrOm35xI79r28yO8ng1UAzTRclvkORt
b8LMxw7e+vdIntBGqf7T25PLn/MycGPPvNXyIsTzvvY/MXXJHnAqpI5DlqwzbRHz
q16/S1WLvzg4PsElmv1f
-----END CERTIFICATE-----'''
pubkey = RSA.importKey(key_encoded)
print(pubkey.n)
print(primefactors(pubkey.n))
Running this program and putting our factors together (we did need to swap p and q), we get our flag! picoCTF{73176001,67867967}