diff --git a/html/assets/style.css b/html/assets/style.css deleted file mode 100644 index 352e2b2..0000000 --- a/html/assets/style.css +++ /dev/null @@ -1,133 +0,0 @@ -* { - margin: 0; - padding: 0; -} - -body, html { - height: 100%; -} - -.sr-only { - position:absolute; - left:-10000px; - top:auto; - width:1px; - height:1px; - overflow:hidden; -} - -::-moz-selection { - color: #000b13; - background: #c42337; -} - -::selection { - color: #040404; - background: #d7d9ce; -} - -body { - background-color: #040404; - color: #119da4; - font-family: monospace; - font-size: 1.0em; - - display: flex; - justify-content: center; - align-items: center; - flex-direction: column; - - max-width: 1500px; - margin: auto; -} - -header { - text-align: center; - font-size: 1.2em; -} - -header h1 { - font-size: 4em; -} - -header h2 { - font-size: 2em; -} - -header img { - border-radius: 100%; - width: 220px; - margin: 10px; -} - -header .socials { - margin-top: 5px; - list-style-type: none; - font-size: 1.4em; -} - -header .socials li { - display: inline-block; -} - -header .socials a { - color: inherit; - text-decoration: none; -} - -header .socials a:hover { - color: #d7d9ce; -} - -.articles { - margin-top: 50px; - font-size: 1.3em; -} - -.articles h3 { - font-size: 1em; - font-weight: 800; - color: #f4b65c; -} - -.articles ul { - list-style-type: none; -} - -.articles li { - margin-top: 0.4em; - margin-bottom: 0.4em; -} - -.articles a { - color: #7f8589; - text-decoration: none; -} - -.articles a:hover { - color: #c42337; -} - -article h1 { - color: #f4b65c; - margin-bottom: 0.7em; -} - -article { - font-size: 1.3em; -} - -article p { - padding-top: 3px; - padding-bottom: 3px; - text-align: justify; -} - -article a { - color: #119da4; - text-decoration: none; -} - -article a:hover { - color: #d7d9ce; -} diff --git a/html/blog/article.html b/html/blog/article.html deleted file mode 100644 index 9d72661..0000000 --- a/html/blog/article.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - Juju - - - - - - -
-

Article

-

Test

-
- - - diff --git a/html/blog/conf_exploit/binary_exploitation.pdf b/html/blog/conf_exploit/binary_exploitation.pdf deleted file mode 100644 index ed13ffa..0000000 Binary files a/html/blog/conf_exploit/binary_exploitation.pdf and /dev/null differ diff --git a/html/blog/conf_exploit/index.html b/html/blog/conf_exploit/index.html deleted file mode 100644 index 81e2afd..0000000 --- a/html/blog/conf_exploit/index.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - Juju - - - - - - -
-

Introduction à la rétro-ingénierie et à l'exploitation logicielle

-

- - - - - - - -

-
- - - diff --git a/html/blog/index.html b/html/blog/index.html deleted file mode 100644 index efefbbb..0000000 --- a/html/blog/index.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - Juju - - - - - - -
-

Posts

-
  • -

    Introduction à la rétro-ingénierie et à l'exploitation logicielle [FR]

    -
  • -
    - - - diff --git a/html/favicon.ico b/html/favicon.ico deleted file mode 100644 index 77703d7..0000000 Binary files a/html/favicon.ico and /dev/null differ diff --git a/html/index.html b/html/index.html deleted file mode 100644 index fead681..0000000 --- a/html/index.html +++ /dev/null @@ -1,60 +0,0 @@ - - - - - Juju - - - - - - -
    - Photo of myself -

    Juju

    - - -
    - - diff --git a/html/juju.jpg b/html/juju.jpg deleted file mode 100644 index fc17b5a..0000000 Binary files a/html/juju.jpg and /dev/null differ diff --git a/jujure/content/writeups/fcsc_2024/svartalfheim.md b/jujure/content/writeups/fcsc_2024/svartalfheim.md new file mode 100644 index 0000000..c5b0e40 --- /dev/null +++ b/jujure/content/writeups/fcsc_2024/svartalfheim.md @@ -0,0 +1,429 @@ +--- +title: "Lifting a reloc based VM | Svartalfheim @ FCSC 2024" +date: "2024-04-09 18:00:00" +author: "Juju" +tags: ["Reverse", "Writeup", "fcsc"] +toc: true +--- + +# Intro + +Svartalfheim is a weird reversing challenge. It seems like a simple x64 ELF with +only a few bytes of machine code, but after playing with it, you might notice +some quantum behaviours. The program might be patching itself when you are +not looking at it, so stay alert :eyes:. + +{{< image src="/brachiosaure/meme.jpg" style="border-radius: 8px;" >}} + +## Challenge description +`reverse` | `479 pts` `9 solves` `:star::star::star:` + +``` +Trouvez le flag accepté par le binaire. +``` + +Author: `Quanthor_ic` + +## Given files + +[svartalfheim](/svartalfheim/svartalfheim) + + +# Writeup + + +## Overview + +Things are already weird without opening up any disassembler: +`file` tells us the the binary is dynamically linked but ldd says otherwise. + +```console +$ file svartalfheim +svartalfheim: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), +dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, no +section header + +$ ldd svartalfheim + not a dynamic executable +``` + +Here is the decompiled code of the entrypoint. + +{{< code file="/static/svartalfheim/main.c" language="c" >}} + +Basically, it simply deletes a file named `_` from the current directory, then +re-create it and opening it write mode. + +The process then dumps itself into the opened file, close it and execve the +dumped file. + +Cool so this should do absolutely nothing except calling itself endlessly. + +But what if we give it a try: + +```console +./svartalfheim +Welcome to Svartalfheim +FCSC{test} +Nope +``` + +WTF is going on ? + + +## Quantum binary + +So first I wanted a way to see what was happening between each execution. + +I patched the `execve` with a breakpoint in the binary. + +It will now crash instead of starting itself again and I can inspect the +`_` file before the next instance deletes it. + +Here is the diff of the hexdump of the patched binary (breakpoint instead +of execve) with the hexdump of the `_` file after a single execution: + +```console +$ diff svartalfheim_breakpointed.hex first_run.hex +517c517 +< 00002040: 0700 0000 0000 0000 e821 0400 0000 0000 .........!...... +--- +> 00002040: 0700 0000 0000 0000 3037 0400 0000 0000 ........07...... +519c519 +< 00002060: 0800 0000 0000 0000 3000 0000 0000 0000 ........0....... +--- +> 00002060: 0800 0000 0000 0000 6000 0000 0000 0000 ........`....... +``` + +A total of 3 bytes have changed, so I go check the corresponding addresses +in my decompiler: + +{{< image src="/svartalfheim/rela_patched.png" style="border-radius: 8px;" >}} + +So the first two bytes that are patched are the relation table addr inside +the dynamic table. + +The last byte patched is the size of said relocation table. + +This means that at the next execution, the program will have different relocations. + +Maybe let's take a look at the original relocation table: + +{{< image src="/svartalfheim/original_relocs.png" style="border-radius: 8px;" >}} + +Unusual relocations indeed. So the first one points to the relocation table +addresse inside the dynamic table and the second one to the relocation table +size, also in the dynamic table, the two values that were patched in the next +binary. We can already guess that the relocation table will be patched at +every execution, running new relocations every time, just like a processor +run instructions and increments its program counter. This might be a +relocation based virtual machine. + +## Reloc based virtual machine + +### Figuring out the instruction set + +So since the relocation table have changed in the next binary, let's open this +one and check the new relocation table: + +{{< image src="/svartalfheim/second_relocs.png" style="border-radius: 8px;" >}} + +Once again we can see relocs pointing to DT_RELA and DT_RELASZ values, but there are also two other addresses that are patched. + +When looking them up, we can see that these two addresses are located inside +the symbol table. To be precise, the values of symbol `1` and `2` are patched. + +{{< image src="/svartalfheim/second_symtab.png" style="border-radius: 8px;" >}} + +Great so now let's run the binary a second time and inspect the third relocation table. + +I will stop on this one a bit longer because it actually contains the entire +instruction set. + +But first, what is *really* happening ? + +The relocation table holds ... relocations indeed. + +Relocations are applied by `ldso` when a process is loaded into memory +(so at `execve`). + +Applying a relocation has different effects depending on the relocation type +(denoted in the lower 32 bits of the `info` field of the reloc). + +However, most relocation types imply dereferencing the `value` of a symbol (see the above screenshot for an example of the symbol table) and storing it in the relocation address. + +The said symbol is denoted by the higher 32 bits of the `info` field of the reloc. + +Now let's really look at the third relocation table and run it in our mind: + +{{< image src="/svartalfheim/third_relocs.png" style="border-radius: 8px;" >}} + +The first relocation is of type `0x8`, and has symbol `0x0` (which mean no symbol) + +It points to the address of the `value`` of symbol `0x5`. + +Relocation type `0x8` will simply put its `addend` value at the address pointed by its `addr` field. Thus storing `0xff` in the `0x5` symbol `value` + +Basically this relocation is a `mov mem, imm` instruction. + +Second relocation is of type `0x1` and symbol `0x1`. + +This relocation will take the `value` of symbol `0x1`, add the reloc +`addend` value, and store the result at the relocation `addr` + +So it looks like some sort of `add mem, reg, imm` instruction, considering +symbols as registers. + +I'll do the third relocation and we will have the whole instruction set: + +The type is `0x5`, symbol `0x1`. It will take the value of the corresponding symbol, dereference it, and store it in the reloc `addr`. + +The assembly for this might look like `mov mem, [reg]` + +Here we go, that's it, an instruction set of 3 instruction, cannot even +branch or add 2 registers. + +Let's write an interpreter for the VM so we can debug it. + +### Writing the interpreter + +Basically the interpreter will have multiple role in the analysis: + +* Get an execution trace and disassembly of the virtual machine +* Set breakpoints during execution +* Dump process to inspect the patched ELF at any stage easily and fast + +{{< code file="/static/svartalfheim/interpreter.py" language="py" >}} + + +This interpreter stops every time that the VM patches the native code section +of the binary, this way I can stop whenever IO is performed, dump the binary +and analyse it. + +The VM patches the native code a total of 7 times: + +* Setup a syscall to write to prompt on stdout +* Immediately after, reset the native code the its original content +* Setup a syscall to read the flag from stdin +* Immediately after, reset the native code the its original content +* Setup a syscall to write the flag validation +* Immediately after, reset the native code the its original content +* Setup a syscall to exit the program instead of the `execve` it again + +Investigating the third dumped binary will show us the flag address given to `read`, which will allow us to inject it in our interpreter: + +{{< image src="/svartalfheim/radare_read.png" style="border-radius: 8px;" >}} + +The interpreter also builds a disassembled execution trace: + +I tried to make it readable as if it was intel assembly. + +I added some comments for easier analysis: +* NATIVE CODE LOADING means tha this block (a complete run of a single relocation table) has patched the native code section +* The comment hexstring is the data that is being outputed in the destination operand +* PATCHING CODE means that this instruction has a destination address pointing to the next instruction, meaning it is trying to patch its own code +* PATCHING FAR is the same but on an instruction of the same block but not the next one + +These were really helpful during analysis to have a reminder to check for code patching. + +You might be saying that the shown assembly doesn't correspond to the +instruction defined above as there was no such instruction as +`add reg, reg, imm`, it is indeed true, but the trick is that every registers +are memory mapped (since they are simply symbols in the symtab of the ELF), so a memory deref can actually be a register and my disassembler lifts this. + +```console +0x48080: mov [0x480f0], $0xffffffffffffffa0 # a0ffffffffffffff PATCHING FAR +0x48098: add [0x480b0], r4, $0x480d8 # f080040000000000 PATCHING CODE +0x480b0: mov [0x480f0], $0x0 # 0000000000000000 PATCHING FAR +0x480c8: add r1, r1, $0x0 # 1036040000000000 +0x480e0: add r1, r1, $0x0 # 1036040000000000 +0x480f8: add r1, r1, $0x10 # 2036040000000000 +0x48110: mov DT_RELA, [r1].8 # 5881040000000000 +0x48128: add r2, r1, $0x8 # 2836040000000000 +0x48140: mov DT_RELASZ, [r2].8 # 7800000000000000 +#End of block +0x48158: mov r7, $0x5 # 0500000000000000 +0x48170: add r1, r1, $0x10 # 3036040000000000 +0x48188: mov DT_RELA, [r1].8 # d081040000000000 +0x481a0: add r2, r1, $0x8 # 3836040000000000 +0x481b8: mov DT_RELASZ, [r2].8 # f000000000000000 +#End of block +# NATIVE CODE LOADING: +0x481d0: mov [0x4100e], $0xba48 # 48ba000000000000 +0x481e8: mov [0x41016], $0xbe480000 # 000048be00000000 +0x48200: mov [0x4101e], $0x6a5f016a00000000 # 000000006a015f6a +0x48218: mov [0x41026], $0x90909090050f5801 # 01580f0590909090 +0x48230: add [0x41010], r7, $0x0 # 0500000000000000 +0x48248: add [0x4101a], r8, $0x0 # 23a2050000000000 +0x48260: add r1, r1, $0x10 # 4036040000000000 +0x48278: mov DT_RELA, [r1].8 # c082040000000000 +0x48290: add r2, r1, $0x8 # 4836040000000000 +0x482a8: mov DT_RELASZ, [r2].8 # c000000000000000 +#End of block +``` + +### Side channel attempt + +My first attempt at a solver was really simple, I though that maybe the +VM would check bytes one by one. + +So I added a method to inject the flag into my interpreter's memory and tried +to bruteforce the first char, watching for execution trace length every time. + +But all 256 possible bytes gave the same number of instructions + +At this points I was thinking about lifting the code a little more to reduce +the trace size before reading it, but I was in the mood to read 65000+ lines of the same 3 instructions. + +## Analysing the execution trace + +Just kidding I did not actually read the whole execution trace. + +I knew that the flag start with `FCSC{`, so I dumped 2 execution trace: + +* One with the flag as `FCSC{test` +* The other one with `HCSC{test` + +I put them side by side (`FCSC{` on the left, `HCSC{` on the right), jumped right after the `read` call, and started comparing them, and lifting the code on paper. + +{{< image src="/svartalfheim/trace_read.png" style="border-radius: 8px;" >}} + +I will not show you the whole execution trace to keep your eyes safe, but +the VM starts by a bunch of `mov reg, imm` instructions to initialize some +variables. + +And then a fun pattern appears: + +### `add mem, reg, reg` + +{{< image src="/svartalfheim/add_reg_reg.png" style="border-radius: 8px;" >}} + +These three instructions together can actually be lifted to `add r13.b, r6, r13` + +It is crucial to understand this simple pattern before we continue to how +branching are handled in this VM. + +Start by looking the first instruction: it takes the value of `r13`, add 0, +and store it at `0x45a50`. The first thing to notice is that `add mem, reg, +$0x0` is actually equivalent to a `mov mem, reg`, but there is no such +instruction in our instruction set (`mov mem, [reg]` will deref the reg) thus the add instruction trick. + +Then if you look the destination address, it points to the the next instruction `addend`. Looking at the comment, we know that the output value is `0x0` on 8 bytes. + +So now when we look at the second instruction, it is indeed `add r13, r6, +$0x0`, but said immediate `$0x0` was patched by previous instruction, with the +value of `r13`, even if the instruction add an immediate, in this context, +the immediate was patched with a register. Thus performing a `add mem, reg, reg` + +The third instruction simply zeros out the 7 higher bytes of the `r13` +register, my disassembler did not lift the addresses of the higher bytes of +regs but trust me on this one. + +The comment on the second instruction shows us that the addition had a result of `0x1` (64 bits little endian) (`r6` had value 1), so these 3 instructions simply increments `r13`. + + +### Lookup tables + +I skipped a few instructions, all you need to know is that `r3` points +to the first byte of the flag, and that `r12` was initialized with a byte +coming from an array indexed with the same counter as the flag. + +{{< image src="/svartalfheim/lut_lookup.png" style="border-radius: 8px;" >}} + +The first block simply loads the current flag byte in `r11` (`0x46` = `'F'`) +and the second one basically substitute the byte from the flag based on a lookup table. + +The LuT is indexed based on the flag byte and `r12`, which I assume is some +sort of nonce to add the information of the position of the byte in the +flag during the substitution. + +Here is a c equivalent I lifted on paper: + +```c +uint64_t r12 = nonce[i]; +uint64_t r11 = flag[i]; +uint64_t r3 = (r11 << 32) + r12; +uint64_t *LuT = 0x49000; +r11 = *(LuT + r3); +``` + +The next few blocks are not that important, store the LuTed byte in memory, +basically increment string iterators, decrement size counters + +But then comes the one most important code pattern of this VM: + +### Branching + +{{< image src="/svartalfheim/for.png" style="border-radius: 8px;" >}} + +These two blocks perform a branch + +It essentially is a `test r7; jne mem` heres how it works after lifting: + +```c +// First block +jump_table = 0x59000; +r4 = jump_table[r7]; // r7 is remaining flag len +// Second block +r1 = r1 + 0x10 - 0xf0 + r4; +// DT_RELA = r1, r1 is the program counter +``` + +So `0x59000` contains a jump table indexed on the remaining flag size. + +Here you can see at instruction `0x46628`, the jump offset is in the comment. + +I noticed it changed when `r7` reached 0. + +It is essentialy the first `for` loop iterating on the flag, applying lookup +tables on each byte. + +### Flag checking + +After that there is a really similar block of code, also performing lookups +of some sort I did not really bother to understand (as the ones of the +previous step) because I found a really interesting branch which was not a loop. + +I notice a similar pattern than the for loop above, sligthly differentm but +still some kind of jump table. + +What stroke me is that as you can see on the screenshot bellow, it was +the first time in the whole execution trace, that my purposely wrong flag, ran +to a different branch than the one starting with `FCSC{` + +{{< image src="/svartalfheim/check.png" style="border-radius: 8px;" >}} + +What I did not notice at first is that there are two different branch in +the screenshot (with the jump offsets marked in red). I noticed it quickly and +backported it to my solver. +I do not actually know what is the meaning of these 2 checks regarding the +previous look up tables but all I know is that I needed to hit jump +offset `0x18` twice for a flag byte to be valid. + +So I modified my interpreter to add a breakpoint at the addresses marked in red, +check the value moved in `r4` is `0x18`. + +And then bruteforced byte by byte: + +While bruteforcing the `n`th byte, I need to hit the breakpoint succesfully (with `0x14` in `r4`) `2*n` times. If the breakpoint check fails once then the byte is fucked up. + +## Solver + +Here is the complete solver code, with the interpreter, correct breakpoints and +bruteforcing + +{{< code file="/static/svartalfheim/solve.py" language="python" >}} + +```console +$ ./solve.py +bytearray(b'F\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') +bytearray(b'FC\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') +bytearray(b'FCS\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') +... +bytearray(b'FCSC{162756828312aad562394d47c854134803a092d7f5b9eb795528f4a0f16f7c65}') + +$ ./svartalfheim +Welcome to Svartalfheim +FCSC{162756828312aad562394d47c854134803a092d7f5b9eb795528f4a0f16f7c65} +Well done! +``` diff --git a/jujure/static/svartalfheim/add_reg_reg.png b/jujure/static/svartalfheim/add_reg_reg.png new file mode 100644 index 0000000..34904cf Binary files /dev/null and b/jujure/static/svartalfheim/add_reg_reg.png differ diff --git a/jujure/static/svartalfheim/check.png b/jujure/static/svartalfheim/check.png new file mode 100644 index 0000000..53be6d2 Binary files /dev/null and b/jujure/static/svartalfheim/check.png differ diff --git a/jujure/static/svartalfheim/for.png b/jujure/static/svartalfheim/for.png new file mode 100644 index 0000000..d01d3ba Binary files /dev/null and b/jujure/static/svartalfheim/for.png differ diff --git a/jujure/static/svartalfheim/interpreter.py b/jujure/static/svartalfheim/interpreter.py new file mode 100755 index 0000000..28506c8 --- /dev/null +++ b/jujure/static/svartalfheim/interpreter.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python3 + +import struct +from typing import Optional +import copy + +regs = { + 0x42048: "DT_RELA", + 0x42068: "DT_RELASZ", + 0x42088: "r0", + 0x420a0: "r1", + 0x420b8: "r2", + 0x420d0: "r3", + 0x420e8: "r4", + 0x42100: "r5", + 0x42118: "r6", + 0x42130: "r7", + 0x42148: "r8", + 0x42160: "r9", + 0x42178: "r10", + 0x42190: "r11", + 0x421a8: "r12", + 0x421c0: "r13", + 0x421d8: "r14", + 0x421f0: "r15", + + 0x42090: "r0.size", + 0x420a8: "r1.size", + 0x420c0: "r2.size", + 0x420d8: "r3.size", + 0x420f0: "r4.size", + 0x42108: "r5.size", + 0x42120: "r6.size", + 0x42138: "r7.size", + 0x42150: "r8.size", + 0x42168: "r9.size", + 0x42180: "r10.size", + 0x42198: "r11.size", + 0x421b0: "r12.size", + 0x421c8: "r13.size", + 0x421e0: "r14.size", + 0x421f8: "r15.size" +} + +class Stream: + i: int + buf: bytes + + def __init__(self, buf) -> None: + self.pos = 0 + self.buf = buf + + def read_u8(self) -> Optional[int]: + try: + val = struct.unpack(" Optional[int]: + try: + val = struct.unpack(" Optional[int]: + try: + val = struct.unpack(" Optional[int]: + try: + val = struct.unpack("= len(self.buf) + + +class RelaEnt: + def __init__(self, stream): + self.addr = stream.read_u64() + self.type = stream.read_u32() + self.symbol = stream.read_u32() + self.addend = stream.read_u64() + +class Rela: + def __init__(self, rela_addr, rela_size, elf): + self.rela_addr = rela_addr + self.rela_size = rela_size + self.elf = elf + + def pop(self): + if self.rela_size <= 0: + return None + stream = Stream(self.elf.get_data(self.rela_addr, 0x18)) + entry = RelaEnt(stream) + entry.offset = self.rela_addr + self.rela_addr += 0x18 + self.rela_size -= 0x18 + return entry + + def peek(self): + if self.rela_size <= 0: + return None + stream = Stream(self.elf.get_data(self.rela_addr, 0x18)) + entry = RelaEnt(stream) + entry.offset = self.rela_addr + return entry + +class Symbol: + def __init__(self, data): + stream = Stream(data) + self.name = stream.read_u16() + self.info = stream.read_u8() + self.other = stream.read_u8() + self.shndx = stream.read_u32() + self.value = stream.read_u64() + self.size = stream.read_u64() + +class Elf: + def __init__(self, path): + with open(path, 'rb') as f: + self._data = bytearray(f.read()) + + self.dynt_rela = 0x42048 + self.dynt_relasz = 0x42068 + self.symtab = 0x42080 + self.breakpoints = {} + + def dump(self, path): + with open(path, 'wb') as f: + f.write(self._data) + + def get_data(self, addr, size): + addr -= 0x40000 + return self._data[addr:addr + size] + + def set_data(self, addr, data): + addr -= 0x40000 + self._data[addr:addr + len(data)] = data + + def set_flag(self, flag): + addr = 0x5a100 + addr -= 0x40000 + self._data[addr:addr + len(flag)] = flag + + def get_rela(self): + rela_addr = Stream(self.get_data(self.dynt_rela, 8)).read_u64() + rela_size = Stream(self.get_data(self.dynt_relasz, 8)).read_u64() + return Rela(rela_addr, rela_size, self) + + def get_symbol(self, i): + sym_data = self.get_data(self.symtab + 0x18 * i, 0x18) + return Symbol(sym_data) + + def apply_relocs(self): + rela = self.get_rela() + patched_code = False + asm = "" + while True: + entry = rela.pop() + if not entry: + break + symbol = self.get_symbol(entry.symbol) + dst_name = regs[entry.addr] if entry.addr in regs else f'[{hex(entry.addr)}]' + line = f'{hex(entry.offset)}: ' + if entry.type == 1: + data_int = (symbol.value + entry.addend) % 2**64 + data = struct.pack(' entry.offset and entry.addr < 0x49000: + next_instr = rela.peek() + if not (entry.addr >= next_instr.offset and entry.addr < next_instr.offset + 0x18): + line += " PATCHING FAR" + else: + line += ' PATCHING CODE' + asm += line + '\n' + + self.set_data(entry.addr, data) + if entry.addr >= 0x41000 and entry.addr < 0x41083: + patched_code = True + asm += '#End of block\n' + return asm, patched_code + + +def main(): + elf = Elf('./svartalfheim') + asm = "" + n = 0 + while n <= 7: + step_asm, patched_code = elf.apply_relocs() + if patched_code: + asm += '# NATIVE CODE LOADING:\n' + n += 1 + elf.dump(f'./dumps/pydumped{n}') + asm += step_asm + print(asm) + +if __name__ == "__main__": + main() diff --git a/jujure/static/svartalfheim/lut_lookup.png b/jujure/static/svartalfheim/lut_lookup.png new file mode 100644 index 0000000..9408bd0 Binary files /dev/null and b/jujure/static/svartalfheim/lut_lookup.png differ diff --git a/jujure/static/svartalfheim/main.c b/jujure/static/svartalfheim/main.c new file mode 100644 index 0000000..9c7ef0b --- /dev/null +++ b/jujure/static/svartalfheim/main.c @@ -0,0 +1,9 @@ +int64_t _start() +{ + int64_t path = '_'; + syscall(sys_unlink {0x57}, &path); + int32_t fd = syscall(sys_open {2}, &path, O_CREAT | O_WRONLY); + syscall(sys_write {1}, fd, &__elf_header, 0x1a228); + syscall(sys_close {3}, fd); + syscall(sys_execve {0x3b}, &path, nullptr, nullptr); +} \ No newline at end of file diff --git a/jujure/static/svartalfheim/original_relocs.png b/jujure/static/svartalfheim/original_relocs.png new file mode 100644 index 0000000..ea70a6f Binary files /dev/null and b/jujure/static/svartalfheim/original_relocs.png differ diff --git a/jujure/static/svartalfheim/radare_read.png b/jujure/static/svartalfheim/radare_read.png new file mode 100644 index 0000000..64cf63b Binary files /dev/null and b/jujure/static/svartalfheim/radare_read.png differ diff --git a/jujure/static/svartalfheim/rela_patched.png b/jujure/static/svartalfheim/rela_patched.png new file mode 100644 index 0000000..bfabfa2 Binary files /dev/null and b/jujure/static/svartalfheim/rela_patched.png differ diff --git a/jujure/static/svartalfheim/second_relocs.png b/jujure/static/svartalfheim/second_relocs.png new file mode 100644 index 0000000..a42caa6 Binary files /dev/null and b/jujure/static/svartalfheim/second_relocs.png differ diff --git a/jujure/static/svartalfheim/second_symtab.png b/jujure/static/svartalfheim/second_symtab.png new file mode 100644 index 0000000..cad1cf6 Binary files /dev/null and b/jujure/static/svartalfheim/second_symtab.png differ diff --git a/jujure/static/svartalfheim/solve.py b/jujure/static/svartalfheim/solve.py new file mode 100755 index 0000000..9a9455d --- /dev/null +++ b/jujure/static/svartalfheim/solve.py @@ -0,0 +1,273 @@ +#!/usr/bin/env python3 + +import struct +from typing import Optional +import copy + +regs = { + 0x42048: "DT_RELA", + 0x42068: "DT_RELASZ", + 0x42088: "r0", + 0x420a0: "r1", + 0x420b8: "r2", + 0x420d0: "r3", + 0x420e8: "r4", + 0x42100: "r5", + 0x42118: "r6", + 0x42130: "r7", + 0x42148: "r8", + 0x42160: "r9", + 0x42178: "r10", + 0x42190: "r11", + 0x421a8: "r12", + 0x421c0: "r13", + 0x421d8: "r14", + 0x421f0: "r15", + + 0x42090: "r0.size", + 0x420a8: "r1.size", + 0x420c0: "r2.size", + 0x420d8: "r3.size", + 0x420f0: "r4.size", + 0x42108: "r5.size", + 0x42120: "r6.size", + 0x42138: "r7.size", + 0x42150: "r8.size", + 0x42168: "r9.size", + 0x42180: "r10.size", + 0x42198: "r11.size", + 0x421b0: "r12.size", + 0x421c8: "r13.size", + 0x421e0: "r14.size", + 0x421f8: "r15.size" +} + +class Stream: + i: int + buf: bytes + + def __init__(self, buf) -> None: + self.pos = 0 + self.buf = buf + + def read_u8(self) -> Optional[int]: + try: + val = struct.unpack(" Optional[int]: + try: + val = struct.unpack(" Optional[int]: + try: + val = struct.unpack(" Optional[int]: + try: + val = struct.unpack("= len(self.buf) + + +class RelaEnt: + def __init__(self, stream): + self.addr = stream.read_u64() + self.type = stream.read_u32() + self.symbol = stream.read_u32() + self.addend = stream.read_u64() + +class Rela: + def __init__(self, rela_addr, rela_size, elf): + self.rela_addr = rela_addr + self.rela_size = rela_size + self.elf = elf + + def pop(self): + if self.rela_size <= 0: + return None + stream = Stream(self.elf.get_data(self.rela_addr, 0x18)) + entry = RelaEnt(stream) + entry.offset = self.rela_addr + self.rela_addr += 0x18 + self.rela_size -= 0x18 + return entry + + def peek(self): + if self.rela_size <= 0: + return None + stream = Stream(self.elf.get_data(self.rela_addr, 0x18)) + entry = RelaEnt(stream) + entry.offset = self.rela_addr + return entry + +class Symbol: + def __init__(self, data): + stream = Stream(data) + self.name = stream.read_u16() + self.info = stream.read_u8() + self.other = stream.read_u8() + self.shndx = stream.read_u32() + self.value = stream.read_u64() + self.size = stream.read_u64() + +class Elf: + def __init__(self, path): + with open(path, 'rb') as f: + self._data = bytearray(f.read()) + + self.dynt_rela = 0x42048 + self.dynt_relasz = 0x42068 + self.symtab = 0x42080 + self.breakpoints = {} + + def dump(self, path): + with open(path, 'wb') as f: + f.write(self._data) + + def get_data(self, addr, size): + addr -= 0x40000 + return self._data[addr:addr + size] + + def set_data(self, addr, data): + addr -= 0x40000 + self._data[addr:addr + len(data)] = data + + def set_flag(self, flag): + addr = 0x5a100 + addr -= 0x40000 + self._data[addr:addr + len(flag)] = flag + + def get_rela(self): + rela_addr = Stream(self.get_data(self.dynt_rela, 8)).read_u64() + rela_size = Stream(self.get_data(self.dynt_relasz, 8)).read_u64() + return Rela(rela_addr, rela_size, self) + + def get_symbol(self, i): + sym_data = self.get_data(self.symtab + 0x18 * i, 0x18) + return Symbol(sym_data) + + def set_breakpoint(self, addr, func, res): + self.breakpoints[addr] = (func, res) + + def apply_relocs(self): + rela = self.get_rela() + patched_code = False + asm = "" + while True: + entry = rela.pop() + if not entry: + break + symbol = self.get_symbol(entry.symbol) + dst_name = regs[entry.addr] if entry.addr in regs else f'[{hex(entry.addr)}]' + line = f'{hex(entry.offset)}: ' + if entry.type == 1: + data_int = (symbol.value + entry.addend) % 2**64 + data = struct.pack(' entry.offset and entry.addr < 0x49000: + next_instr = rela.peek() + if not (entry.addr >= next_instr.offset and entry.addr < next_instr.offset + 0x18): + line += " PATCHING FAR" + else: + line += ' PATCHING CODE' + asm += line + '\n' + + if entry.offset in self.breakpoints: + stop = not self.breakpoints[entry.offset][0](self, entry, line, data, self.breakpoints[entry.offset][1]) + if stop: + return asm, stop + + self.set_data(entry.addr, data) + if entry.addr >= 0x41000 and entry.addr < 0x41083: + patched_code = True + + asm += '#End of block\n' + return asm, patched_code + + +def fetch_r4(elf, entry, asm, data, res): + if data != b'\x00': + res.append(True) + return True + else: + return False + +def check(elf, entry, asm, data): + if data == b'\x00': + print(asm) + + +def main(): + elf = Elf('./svartalfheim') + asm = "" + n = 0 + while n <= 3: + #print('---') + step_asm, patched_code = elf.apply_relocs() + if patched_code: + asm += '# NATIVE CODE LOADING:\n' + n += 1 + elf.dump(f'./dumps/pydumped{n}') + asm += step_asm + flag = bytearray(0x46) + for i in range(0x46): + for b in range(0x30, 127): + n = 4 + flag[i] = b + before_read = copy.deepcopy(elf) + res = [] + before_read.set_breakpoint(0x47000, fetch_r4, res) + before_read.set_breakpoint(0x47270, fetch_r4, res) + before_read.set_flag(flag) + while n < 7: + step_asm, patched_code = before_read.apply_relocs() + if patched_code: + asm += '# NATIVE CODE LOADING:\n' + n += 1 + elf.dump(f'./dumps/pydumped{n}') + break + asm += step_asm + if len(res) == 2 * (i + 1): + print(flag) + break + + + #print(asm) + +if __name__ == "__main__": + main() diff --git a/jujure/static/svartalfheim/svartalfheim b/jujure/static/svartalfheim/svartalfheim new file mode 100755 index 0000000..5760061 Binary files /dev/null and b/jujure/static/svartalfheim/svartalfheim differ diff --git a/jujure/static/svartalfheim/third_relocs.png b/jujure/static/svartalfheim/third_relocs.png new file mode 100644 index 0000000..e63a1ce Binary files /dev/null and b/jujure/static/svartalfheim/third_relocs.png differ diff --git a/jujure/static/svartalfheim/trace_read.png b/jujure/static/svartalfheim/trace_read.png new file mode 100644 index 0000000..7a33499 Binary files /dev/null and b/jujure/static/svartalfheim/trace_read.png differ