Deploy site
Lonami Exo totufals@hotmail.com
Sun, 07 Mar 2021 12:44:03 +0100
4 files changed,
13 insertions(+),
9 deletions(-)
M
blog/atom.xml
→
blog/atom.xml
@@ -508,7 +508,7 @@ <p>This totally does work. Step 5: complete 🎉</p>
<h2 id="properly-patching-instructions">Properly patching instructions</h2> <p>You may not be satisfied at all with our solution. Not only are we hardcoding some magic constants to set hardware watchpoints, we're also relying on knowledge specific to the Cheat Engine tutorial (insofar that we're replacing two bytes worth of instruction with NOPs).</p> <p>Properly supporting more than one hardware breakpoint, along with supporting different types of breakpoints, is definitely doable. The meaning of the bits for the debug registers is well defined, and you can definitely study that to come up with <a href="https://github.com/mmorearty/hardware-breakpoints">something more sophisticated</a> and support multiple different breakpoints. But for now, that's out of the scope of this series. The tutorial only wants us to use an on-write watchpoint, and our solution is fine and portable for that use case.</p> -<p>However, relying on the size of the instructions is pretty bad. The instructions x86 executes are of variable length, so we can't possibly just look back until we find the previous instruction, or even naively determine its length. A lot of unrelated sequences of bytes are very likely instructions themselves. We need a disassembler. No, we're not writing our own.</p> +<p>However, relying on the size of the instructions is pretty bad. The instructions x86 executes are of variable length, so we can't possibly just look back until we find the previous instruction, or even naively determine its length. A lot of unrelated sequences of bytes are very likely instructions themselves. We need a disassembler. No, we're not writing our own<sup class="footnote-reference"><a href="#4">4</a></sup>.</p> <p>Searching on <a href="https://crates.io">crates.io</a> for &quot;disassembler&quot; yields a few results, and the first one I've found is <a href="https://crates.io/crates/iced-x86">iced-x86</a>. I like the name, it has a decent amount of GitHub stars, and it was last updated less than a month ago. I don't know about you, but I think we've just hit a jackpot!</p> <p>It's quite heavy though, so I will add it behind a feature gate, and users that want it may opt into it:</p> <pre><code class="language-toml" data-lang="toml">[features]@@ -559,7 +559,8 @@ ))
} </code></pre> <p>Pretty straightforward! We can set the &quot;instruction pointer&quot; of the decoder so that it matches with the address we're reading from. The <code>next_ip</code> method comes in really handy. Overall, it's a bit inefficient, because we could reuse the regions retrieved previously, but other than that, there is not much room for improvement.</p> -<p>With this, we are no longer hardcoding the instruction size or guessing which instruction is doing what. You may wonder, what if the region does not start with valid executable code? It could be possible that the instructions are in some memory region with garbage except for a very specific location with real code. I don't know how Cheat Engine handles this, but I think it's reasonable to assume that the region starts with valid code. If you can think of any more reliable way to figure out the instruction right before a given address, please let me know!</p> +<p>With this, we are no longer hardcoding the instruction size or guessing which instruction is doing what. You may wonder, what if the region does not start with valid executable code? It could be possible that the instructions are in some memory region with garbage except for a very specific location with real code. I don't know how Cheat Engine handles this, but I think it's reasonable to assume that the region starts with valid code.</p> +<p>As far as I can tell (after having asked a bit around), the encoding is usually self synchronizing (similar to UTF-8), so eventually we should end up with correct instructions. But someone can still intentionally write real code between garbage data which we would then disassemble incorrectly. This is a problem on all variable-length ISAs. Half a solution is to <a href="https://stackoverflow.com/q/3983735/">start at the entry point</a>, decode all instructions, and follow the jumps. The other half would be correctly identifying jumps created just to trip a disassembler up, and jumps pointing to dynamically-calculated addresses!</p> <h2 id="finale">Finale</h2> <p>That was quite a deep dive! We have learnt about the existence of the various breakpoint types (software, hardware, and even behaviour, such as watchpoints), how to debug a separate process, and how to correctly update the code other process is running on-the-fly. The <a href="https://github.com/lonami/memo">code for this post</a> is available over at my GitHub. You can run <code>git checkout step5</code> after cloning the repository to get the right version of the code.</p> <p>Although we've only talked about <em>setting</em> breakpoints, there are of course <a href="https://reverseengineering.stackexchange.com/a/16547">ways of detecting them</a>. There's <a href="https://www.codeproject.com/Articles/30815/An-Anti-Reverse-Engineering-Guide">entire guides about it</a>. Again, we currently hardcode the fact we want to add a single watchpoint using the first debug register. A proper solution here would be to actually calculate the needs that need to be set, as well as keeping track of how many breakpoints have been added so far.</p>@@ -575,17 +576,20 @@ <div class="footnote-definition" id="1"><sup class="footnote-definition-label">1</sup>
<p>I'm not super happy about the design of it all, but we won't actually need anything beyond scanning for integers for the rest of the steps so it doesn't really matter.</p> </div> <div class="footnote-definition" id="2"><sup class="footnote-definition-label">2</sup> -<p>There seems to be a way to pause the entire process in one go, with the [undocumented <code>NtSuspendProcess</code>] function!</p> +<p>There seems to be a way to pause the entire process in one go, with the <a href="https://stackoverflow.com/a/4062698/">undocumented <code>NtSuspendProcess</code></a> function!</p> </div> <div class="footnote-definition" id="3"><sup class="footnote-definition-label">3</sup> <p>It really is called that. The naming went from &quot;IP&quot; (instruction pointer, 16 bits), to &quot;EIP&quot; (extended instruction pointer, 32 bits) and currently &quot;RIP&quot; (64 bits). The naming convention for upgraded registers is the same (RAX, RBX, RCX, and so on). The <a href="https://wiki.osdev.org/CPU_Registers_x86_64">OS Dev wiki</a> is a great resource for this kind of stuff.</p> </div> +<div class="footnote-definition" id="4"><sup class="footnote-definition-label">4</sup> +<p>Well, we don't need an entire disassembler. Knowing the length of each instruction is enough, but that on its own is also a lot of work.</p> +</div> </content> </entry> <entry xml:lang="en"> <title>Writing our own Cheat Engine: Floating points</title> - <published>2021-02-22T00:00:00+00:00</published> - <updated>2021-02-22T00:00:00+00:00</updated> + <published>2021-02-28T00:00:00+00:00</published> + <updated>2021-02-28T00:00:00+00:00</updated> <link href="https://lonami.dev/blog/woce-4/" type="text/html"/> <id>https://lonami.dev/blog/woce-4/</id> <content type="html"><p>This is part 4 on the <em>Writing our own Cheat Engine</em> series:</p>
M
blog/woce-4/index.html
→
blog/woce-4/index.html
@@ -1,4 +1,4 @@
-<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=description content="Official Lonami's website"><meta name=viewport content="width=device-width, initial-scale=1.0, user-scalable=yes"><title> Writing our own Cheat Engine: Floating points | Lonami's Blog </title><link rel=stylesheet href=/style.css><body><article><nav class=sections><ul class=left><li><a href=/>lonami's site</a><li><a href=/blog class=selected>blog</a><li><a href=/golb>golb</a></ul><div class=right><a href=https://github.com/LonamiWebs><img src=/img/github.svg alt=github></a><a href=/blog/atom.xml><img src=/img/rss.svg alt=rss></a></div></nav><main><h1 class=title>Writing our own Cheat Engine: Floating points</h1><div class=time><p>2021-02-22</div><p>This is part 4 on the <em>Writing our own Cheat Engine</em> series:<ul><li><a href=/blog/woce-1>Part 1: Introduction</a> (start here if you're new to the series!)<li><a href=/blog/woce-2>Part 2: Exact Value scanning</a><li><a href=/blog/woce-3>Part 3: Unknown initial value</a><li>Part 4: Floating points<li><a href=/blog/woce-5>Part 5: Code finder</a></ul><p>In part 3 we did a fair amount of plumbing in order to support scan modes beyond the trivial "exact value scan". As a result, we have abstracted away the <code>Scan</code>, <code>CandidateLocations</code> and <code>Value</code> types as a separate <code>enum</code> each. Scanning for changed memory regions in an opened process can now be achieved with three lines of code:<pre><code class=language-rust data-lang=rust>let regions = process.memory_regions(); +<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=description content="Official Lonami's website"><meta name=viewport content="width=device-width, initial-scale=1.0, user-scalable=yes"><title> Writing our own Cheat Engine: Floating points | Lonami's Blog </title><link rel=stylesheet href=/style.css><body><article><nav class=sections><ul class=left><li><a href=/>lonami's site</a><li><a href=/blog class=selected>blog</a><li><a href=/golb>golb</a></ul><div class=right><a href=https://github.com/LonamiWebs><img src=/img/github.svg alt=github></a><a href=/blog/atom.xml><img src=/img/rss.svg alt=rss></a></div></nav><main><h1 class=title>Writing our own Cheat Engine: Floating points</h1><div class=time><p>2021-02-28</div><p>This is part 4 on the <em>Writing our own Cheat Engine</em> series:<ul><li><a href=/blog/woce-1>Part 1: Introduction</a> (start here if you're new to the series!)<li><a href=/blog/woce-2>Part 2: Exact Value scanning</a><li><a href=/blog/woce-3>Part 3: Unknown initial value</a><li>Part 4: Floating points<li><a href=/blog/woce-5>Part 5: Code finder</a></ul><p>In part 3 we did a fair amount of plumbing in order to support scan modes beyond the trivial "exact value scan". As a result, we have abstracted away the <code>Scan</code>, <code>CandidateLocations</code> and <code>Value</code> types as a separate <code>enum</code> each. Scanning for changed memory regions in an opened process can now be achieved with three lines of code:<pre><code class=language-rust data-lang=rust>let regions = process.memory_regions(); let first_scan = process.scan_regions(&regions, Scan::InRange(0, 500)); let second_scan = process.rescan_regions(&first_scan, Scan::DecreasedBy(7)); </code></pre><p>How's that for programmability? No need to fire up Cheat Engine's GUI anymore!<p>The <code>first_scan</code> in the example above remembers all the found <code>Value</code> within the range specified by <code>Scan</code>. Up until now, we have only worked with <code>i32</code>, so that's the type the scans expect and what they work with.<p>Now it's time to introduce support for different types, like <code>f32</code>, <code>i64</code>, or even more atypical ones, like arbitrary sequences of bytes (think of strings) or even numbers in big-endian.<p>Tighten your belt, because this post is quite the ride. Let's get right into it!<h2 id=floating-points>Floating points</h2><details open><summary>Cheat Engine Tutorial: Step 4</summary> <blockquote><p>In the previous tutorial we used bytes to scan, but some games store information in so called 'floating point' notations. (probably to prevent simple memory scanners from finding it the easy way). A floating point is a value with some digits behind the point. (like 5.12 or 11321.1)<p>Below you see your health and ammo. Both are stored as Floating point notations, but health is stored as a float and ammo is stored as a double. Click on hit me to lose some health, and on shoot to decrease your ammo with 0.5<p>You have to set BOTH values to 5000 or higher to proceed.<p>Exact value scan will work fine here, but you may want to experiment with other types too.<p>Hint: It is recommended to disable "Fast Scan" for type double</blockquote></details><h2 id=generic-values>Generic values</h2><p>The <code>Value</code> enumeration holds scanned values, and is currently hardcoded to store <code>i32</code>. The <code>Scan</code> type also holds a value, the value we want to scan for. Changing it to support other types is trivial:<pre><code class=language-rust data-lang=rust>pub enum Scan<T> {
M
blog/woce-5/index.html
→
blog/woce-5/index.html
@@ -335,7 +335,7 @@ </code></pre><p>Although it seems to work:<pre><code>Watching writes to 15103f0 for 10s
Patched [10002c5ba] with NOP </code></pre><p>It really doesn't:<blockquote><p><strong>Tutorial-x86_64</strong><p>Access violation.<p>Press OK to ignore and risk data corruption.<br> Press Abort to kill the program.<p><kbd>OK</kbd> <kbd>Abort</kbd></blockquote><p>Did we write memory somewhere we shouldn't? The documentation does mention "segment-relative" and "linear virtual addresses":<blockquote><p><code>GetThreadSelectorEntry</code> returns the descriptor table entry for a specified selector and thread. Debuggers use the descriptor table entry to convert a segment-relative address to a linear virtual address. The <code>ReadProcessMemory</code> and <code>WriteProcessMemory</code> functions require linear virtual addresses.</blockquote><p>But nope! This isn't the problem. The problem is that the <code>ExceptionRecord.ExceptionAddress</code> is <em>after</em> the execution happened, so it's already 2 bytes beyond where it should be. We were accidentally writing out the first half of the next instruction, which, yeah, could not end good.<p>So does it work if I do this instead?:<pre><code class=language-rust data-lang=rust>process.write_memory(addr - 2, &[0x90, 0x90]) // ^^^ new -</code></pre><p>This totally does work. Step 5: complete 🎉<h2 id=properly-patching-instructions>Properly patching instructions</h2><p>You may not be satisfied at all with our solution. Not only are we hardcoding some magic constants to set hardware watchpoints, we're also relying on knowledge specific to the Cheat Engine tutorial (insofar that we're replacing two bytes worth of instruction with NOPs).<p>Properly supporting more than one hardware breakpoint, along with supporting different types of breakpoints, is definitely doable. The meaning of the bits for the debug registers is well defined, and you can definitely study that to come up with <a href=https://github.com/mmorearty/hardware-breakpoints>something more sophisticated</a> and support multiple different breakpoints. But for now, that's out of the scope of this series. The tutorial only wants us to use an on-write watchpoint, and our solution is fine and portable for that use case.<p>However, relying on the size of the instructions is pretty bad. The instructions x86 executes are of variable length, so we can't possibly just look back until we find the previous instruction, or even naively determine its length. A lot of unrelated sequences of bytes are very likely instructions themselves. We need a disassembler. No, we're not writing our own.<p>Searching on <a href=https://crates.io>crates.io</a> for "disassembler" yields a few results, and the first one I've found is <a href=https://crates.io/crates/iced-x86>iced-x86</a>. I like the name, it has a decent amount of GitHub stars, and it was last updated less than a month ago. I don't know about you, but I think we've just hit a jackpot!<p>It's quite heavy though, so I will add it behind a feature gate, and users that want it may opt into it:<pre><code class=language-toml data-lang=toml>[features] +</code></pre><p>This totally does work. Step 5: complete 🎉<h2 id=properly-patching-instructions>Properly patching instructions</h2><p>You may not be satisfied at all with our solution. Not only are we hardcoding some magic constants to set hardware watchpoints, we're also relying on knowledge specific to the Cheat Engine tutorial (insofar that we're replacing two bytes worth of instruction with NOPs).<p>Properly supporting more than one hardware breakpoint, along with supporting different types of breakpoints, is definitely doable. The meaning of the bits for the debug registers is well defined, and you can definitely study that to come up with <a href=https://github.com/mmorearty/hardware-breakpoints>something more sophisticated</a> and support multiple different breakpoints. But for now, that's out of the scope of this series. The tutorial only wants us to use an on-write watchpoint, and our solution is fine and portable for that use case.<p>However, relying on the size of the instructions is pretty bad. The instructions x86 executes are of variable length, so we can't possibly just look back until we find the previous instruction, or even naively determine its length. A lot of unrelated sequences of bytes are very likely instructions themselves. We need a disassembler. No, we're not writing our own<sup class=footnote-reference><a href=#4>4</a></sup>.<p>Searching on <a href=https://crates.io>crates.io</a> for "disassembler" yields a few results, and the first one I've found is <a href=https://crates.io/crates/iced-x86>iced-x86</a>. I like the name, it has a decent amount of GitHub stars, and it was last updated less than a month ago. I don't know about you, but I think we've just hit a jackpot!<p>It's quite heavy though, so I will add it behind a feature gate, and users that want it may opt into it:<pre><code class=language-toml data-lang=toml>[features] patch-nops = ["iced-x86"] [dependencies]@@ -373,4 +373,4 @@ io::ErrorKind::Other,
"no matching instruction found", )) } -</code></pre><p>Pretty straightforward! We can set the "instruction pointer" of the decoder so that it matches with the address we're reading from. The <code>next_ip</code> method comes in really handy. Overall, it's a bit inefficient, because we could reuse the regions retrieved previously, but other than that, there is not much room for improvement.<p>With this, we are no longer hardcoding the instruction size or guessing which instruction is doing what. You may wonder, what if the region does not start with valid executable code? It could be possible that the instructions are in some memory region with garbage except for a very specific location with real code. I don't know how Cheat Engine handles this, but I think it's reasonable to assume that the region starts with valid code. If you can think of any more reliable way to figure out the instruction right before a given address, please let me know!<h2 id=finale>Finale</h2><p>That was quite a deep dive! We have learnt about the existence of the various breakpoint types (software, hardware, and even behaviour, such as watchpoints), how to debug a separate process, and how to correctly update the code other process is running on-the-fly. The <a href=https://github.com/lonami/memo>code for this post</a> is available over at my GitHub. You can run <code>git checkout step5</code> after cloning the repository to get the right version of the code.<p>Although we've only talked about <em>setting</em> breakpoints, there are of course <a href=https://reverseengineering.stackexchange.com/a/16547>ways of detecting them</a>. There's <a href=https://www.codeproject.com/Articles/30815/An-Anti-Reverse-Engineering-Guide>entire guides about it</a>. Again, we currently hardcode the fact we want to add a single watchpoint using the first debug register. A proper solution here would be to actually calculate the needs that need to be set, as well as keeping track of how many breakpoints have been added so far.<p>Hardware breakpoints are also limited, since they're simply a bunch of registers, and our machine does not have infinite registers. How are other debuggers like <code>gdb</code> able to create a seemingly unlimited amount of breakpoints? Well, the GDB wiki actually has a page on <a href=https://sourceware.org/gdb/wiki/Internals%20Watchpoints>Internals Watchpoints</a>, and it's really interesting! <code>gdb</code> essentially single-steps through the entire program and tests the expressions after every instruction:<blockquote><p>Software watchpoints are very slow, since GDB needs to single-step the program being debugged and test the value of the watched expression(s) after each instruction.</blockquote><p>However, that's not the only way. One could <a href=https://stackoverflow.com/a/7805842/>change the protection level</a> of the region of interest (for example, remove the write permission), and when the program tries to write there, it will fail! In any case, the GDB wiki is actually a pretty nice resource. It also has a section on <a href=https://sourceware.org/gdb/wiki/Internals/Breakpoint%20Handling>Breakpoint Handling</a>, which contains some additional insight.<p>With regards to code improvements, <code>DebugToken::wait_event</code> could definitely be both nicer and safer to use, with a custom <code>enum</code>, so the user does not need to rely on magic constants or having to resort to <code>unsafe</code> access to get the right <code>union</code> variant.<p>In the next post, we'll tackle the sixth step of the tutorial: Pointers. It reuses the debugging techniques presented here to backtrack where the pointer for our desired value is coming from, so here we will need to actually <em>understand</em> what the instructions are doing, not just patching them out!<h3 id=footnotes>Footnotes</h3><div class=footnote-definition id=1><sup class=footnote-definition-label>1</sup><p>I'm not super happy about the design of it all, but we won't actually need anything beyond scanning for integers for the rest of the steps so it doesn't really matter.</div><div class=footnote-definition id=2><sup class=footnote-definition-label>2</sup><p>There seems to be a way to pause the entire process in one go, with the [undocumented <code>NtSuspendProcess</code>] function!</div><div class=footnote-definition id=3><sup class=footnote-definition-label>3</sup><p>It really is called that. The naming went from "IP" (instruction pointer, 16 bits), to "EIP" (extended instruction pointer, 32 bits) and currently "RIP" (64 bits). The naming convention for upgraded registers is the same (RAX, RBX, RCX, and so on). The <a href=https://wiki.osdev.org/CPU_Registers_x86_64>OS Dev wiki</a> is a great resource for this kind of stuff.</div></main><footer><div><p>Share your thoughts, or simply come hang with me <a href=https://t.me/LonamiWebs><img src=/img/telegram.svg alt=Telegram></a> <a href=mailto:totufals@hotmail.com><img src=/img/mail.svg alt=Mail></a></div></footer></article><p class=abyss>Glaze into the abyss… Oh hi there!+</code></pre><p>Pretty straightforward! We can set the "instruction pointer" of the decoder so that it matches with the address we're reading from. The <code>next_ip</code> method comes in really handy. Overall, it's a bit inefficient, because we could reuse the regions retrieved previously, but other than that, there is not much room for improvement.<p>With this, we are no longer hardcoding the instruction size or guessing which instruction is doing what. You may wonder, what if the region does not start with valid executable code? It could be possible that the instructions are in some memory region with garbage except for a very specific location with real code. I don't know how Cheat Engine handles this, but I think it's reasonable to assume that the region starts with valid code.<p>As far as I can tell (after having asked a bit around), the encoding is usually self synchronizing (similar to UTF-8), so eventually we should end up with correct instructions. But someone can still intentionally write real code between garbage data which we would then disassemble incorrectly. This is a problem on all variable-length ISAs. Half a solution is to <a href=https://stackoverflow.com/q/3983735/>start at the entry point</a>, decode all instructions, and follow the jumps. The other half would be correctly identifying jumps created just to trip a disassembler up, and jumps pointing to dynamically-calculated addresses!<h2 id=finale>Finale</h2><p>That was quite a deep dive! We have learnt about the existence of the various breakpoint types (software, hardware, and even behaviour, such as watchpoints), how to debug a separate process, and how to correctly update the code other process is running on-the-fly. The <a href=https://github.com/lonami/memo>code for this post</a> is available over at my GitHub. You can run <code>git checkout step5</code> after cloning the repository to get the right version of the code.<p>Although we've only talked about <em>setting</em> breakpoints, there are of course <a href=https://reverseengineering.stackexchange.com/a/16547>ways of detecting them</a>. There's <a href=https://www.codeproject.com/Articles/30815/An-Anti-Reverse-Engineering-Guide>entire guides about it</a>. Again, we currently hardcode the fact we want to add a single watchpoint using the first debug register. A proper solution here would be to actually calculate the needs that need to be set, as well as keeping track of how many breakpoints have been added so far.<p>Hardware breakpoints are also limited, since they're simply a bunch of registers, and our machine does not have infinite registers. How are other debuggers like <code>gdb</code> able to create a seemingly unlimited amount of breakpoints? Well, the GDB wiki actually has a page on <a href=https://sourceware.org/gdb/wiki/Internals%20Watchpoints>Internals Watchpoints</a>, and it's really interesting! <code>gdb</code> essentially single-steps through the entire program and tests the expressions after every instruction:<blockquote><p>Software watchpoints are very slow, since GDB needs to single-step the program being debugged and test the value of the watched expression(s) after each instruction.</blockquote><p>However, that's not the only way. One could <a href=https://stackoverflow.com/a/7805842/>change the protection level</a> of the region of interest (for example, remove the write permission), and when the program tries to write there, it will fail! In any case, the GDB wiki is actually a pretty nice resource. It also has a section on <a href=https://sourceware.org/gdb/wiki/Internals/Breakpoint%20Handling>Breakpoint Handling</a>, which contains some additional insight.<p>With regards to code improvements, <code>DebugToken::wait_event</code> could definitely be both nicer and safer to use, with a custom <code>enum</code>, so the user does not need to rely on magic constants or having to resort to <code>unsafe</code> access to get the right <code>union</code> variant.<p>In the next post, we'll tackle the sixth step of the tutorial: Pointers. It reuses the debugging techniques presented here to backtrack where the pointer for our desired value is coming from, so here we will need to actually <em>understand</em> what the instructions are doing, not just patching them out!<h3 id=footnotes>Footnotes</h3><div class=footnote-definition id=1><sup class=footnote-definition-label>1</sup><p>I'm not super happy about the design of it all, but we won't actually need anything beyond scanning for integers for the rest of the steps so it doesn't really matter.</div><div class=footnote-definition id=2><sup class=footnote-definition-label>2</sup><p>There seems to be a way to pause the entire process in one go, with the <a href=https://stackoverflow.com/a/4062698/>undocumented <code>NtSuspendProcess</code></a> function!</div><div class=footnote-definition id=3><sup class=footnote-definition-label>3</sup><p>It really is called that. The naming went from "IP" (instruction pointer, 16 bits), to "EIP" (extended instruction pointer, 32 bits) and currently "RIP" (64 bits). The naming convention for upgraded registers is the same (RAX, RBX, RCX, and so on). The <a href=https://wiki.osdev.org/CPU_Registers_x86_64>OS Dev wiki</a> is a great resource for this kind of stuff.</div><div class=footnote-definition id=4><sup class=footnote-definition-label>4</sup><p>Well, we don't need an entire disassembler. Knowing the length of each instruction is enough, but that on its own is also a lot of work.</div></main><footer><div><p>Share your thoughts, or simply come hang with me <a href=https://t.me/LonamiWebs><img src=/img/telegram.svg alt=Telegram></a> <a href=mailto:totufals@hotmail.com><img src=/img/mail.svg alt=Mail></a></div></footer></article><p class=abyss>Glaze into the abyss… Oh hi there!
M
sitemap.xml
→
sitemap.xml
@@ -206,7 +206,7 @@ <lastmod>2021-02-19</lastmod>
</url> <url> <loc>https://lonami.dev/blog/woce-4/</loc> - <lastmod>2021-02-22</lastmod> + <lastmod>2021-02-28</lastmod> </url> <url> <loc>https://lonami.dev/blog/woce-5/</loc>