VERE - Rev-4 - 13bit
Published at Mar 15, 2025

I wrote a program that should give you the flag… if you can run it! I changed exactly 13 bits in this program. You’d think that such a small change shouldn’t make it SO hard to fix, right?? How well do you know the ELF file format?
Overview
This challenge comes with a corrupted ELF executable 13bit
that, according to the description, had exactly 13 bits flipped from its original functional state. The goal is to identify and correct these bit flips to restore the program, which, when executed, gives us our flag.
Fixed Bits So Far: 0
Running file 13bit
initially shows nothing helpful, linux doesn’t recognize it as an ELF file. Unsurprisingly, attempting to execute it fails as well.

The first indication of something identifiably wrong comes when I run the binary through xxd
:

I can’t remember exactly what the first digit of the ELF header looks like, but the empty }
looks strange to me. Fortunately, I have another ELF file that I can compare:

Do you see that? The first four bytes (The Magic Number, which tells the computer what type of file this is) of both files are different, even though they are both ELFs! 7D 45 4C 46
is close to the working executable’s 7F 45 4C 46
, I wonder how different 7D
and 7F
are when expressed as bits. Writing them out we can see this:
>>> bin(int('7D',16))
'0b1111101'
>>> bin(int('7F',16))
'0b1111111'
Well lookey here, one bit different (the second-last one). I think we’ve found our first corrupted bit! Let’s fix that with the following short python script and see if it does the trick:
with open("13bit", "rb") as f:
data = bytearray(f.read())
data[0] = 0x7f # 7D -> 7F
with open("13bit_fixed", "wb") as f:
f.write(data)
This script just isolates that byte in the file and writes the one that I want it to change to (7F
). Running the same identification commands on the new 13bit_fixed
file shows:

I’ve never been so happy to see a seg fault
. That means our file is now recognized as an executable! …an executable that doesn’t work still, but hey progress is progress.
Fixed Bits So Far: 1
Now that we have a recognizable executable, there’s another command that can help identify the file: readelf
. Here is the output of that on my fixed file:
readelf -l 13bit_fixed
Elf file type is DYN (Shared object file)
Entry point 0x10c0
There are 13 program headers, starting at offset 48
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
NULL 0x001e001f0040000d 0x0000000400000006 0x0000000000000040
0x0000000000000040 0x0000000000000040 0x2d8
<unknown>: 2d8 0x0000000000000008 0x0000000400000003 0x0000000000000318
0x0000000000000318 0x0000000000000318 0x1c
<unknown>: 1c 0x0000000000000001 0x0000000400000001 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x7c8
<unknown>: 7c8 0x0000000000001000 0x0000000500000001 0x0000000000001000
0x0000000000001000 0x0000000000001000 0x351
<unknown>: 351 0x0000000000001000 0x0000000400000001 0x0000000000002000
0x0000000000002000 0x0000000000002000 0x134
<unknown>: 134 0x0000000000001000 0x0000000600000001 0x0000000000002dc8
0x0000000000003dc8 0x0000000000003dc8 0x3458
<unknown>: 348 0x0000000000001000 0x0000000600000002 0x0000000000002de0
0x0000000000003de0 0x0000000000003de0 0x1e0
<unknown>: 1e0 0x0000000000000008 0x0000000400000004 0x0000000000000338
0x0000000000000338 0x0000000000000338 0x20
<unknown>: 20 0x0000000000000008 0x0000000400000004 0x0000000000000358
0x0000000000000358 0x0000000000000358 0x44
<unknown>: 44 0x0000000000000004 0x000000046474e553 0x0000000000000338
0x0000000000000338 0x0000000000000338 0x20
<unknown>: 20 0x0000000000000008 0x000000046474e550 0x0000000000002008
0x0000000000002008 0x0000000000002008 0x3c
<unknown>: 3c 0x0000000000000004 0x000000066474e551 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0
NULL 0x0000000000000010 0x000000046474e552 0x0000000000002dc8
0x0000000000003dc8 0x0000000000003dc8 0x238
...
As you can see, all of the headers are <unknown>
. This isn’t what this is supposed to look like, so there is clearly something still wrong with this header.
Realizing that my knowledge of the ELF header ended at Magic Bytes, and considering that the challenge description indicates that I should know the ELF header in detail, I did some research (This blog summarized it well, but I had to use a couple extra resources to really understand it). To summarize what I found:
ELF Header Requirements:
The ELF header is 64 bytes (in a 64-bit environment) and defines the file’s structure. Here is how it is laid out:
- Bytes 0-3: Magic number (
7F 45 4C 46
). - Byte 4: Class (
02
= 64-bit). - Byte 5: Endianness (
01
= little-endian). - Byte 16-17: Type (
02
=ET_DYN
,03
=ET_EXEC
). - Byte 24-31: Entry point (
e_entry
, virtual address of first instruction). - Byte 32-39: Program header offset (
e_phoff
, offset to program header table). - Byte 56-57: Number of program headers (
e_phnum
).
With that information now at the forefront of my mind, I looked back at the readelf
output, this part in particular:
Entry point 0x10c0
There are 13 program headers, starting at offset 48
and I realized that the program headers start at offset 48
. Considering that I just learned that the file headers are 64 bytes long, this seems incorrect. As a less-concrete but still valid point, from the challenge author’s perspective, it makes a lot of sense to alter the entry point — both from an obfuscation standpoint and as a breadcrumb for the solver to follow. The third thing I noticed is that this change is exactly 16 numbers off, which is a binary value, indicating that some binary change happened to cause the difference. Doing the same binary comparison as before on 0x30
(Hex for 48
) vs my expected 0x40
(Hex for 64
) value, the binary representation is:
>>> bin(int('30',16))
'0b0110000'
>>> bin(int('40',16))
'0b1000000'
Just like I expected, these binary values are remarkably similar, although these have 3 bits differing at positions 2, 3, and 4 (the leading 0 is cut off). I don’t know if the challenge would flip multiple bits for one fix, but I definitely prefer flipping more than one bit each time, that makes for less steps overall. Let’s see if this is the case.
Looking into the xxd
:
xxd -l 64 13bit_fixed
00000000: 7f45 4c46 0201 0100 0000 0000 0000 0000 .ELF............
00000010: 0300 3e00 0100 0000 c010 0000 0000 0000 ..>.............
00000020: 3000 0000 0000 0000 386a 0000 0000 0000 0.......8j......
00000030: 0000 0000 4000 3800 0d00 4000 1f00 1e00 ....@.8...@.....
Referring back to my research on the ELF header requirements, the program header offset e_phoff
starts in byte 32
. This translates to address 0x20
in hex, which is how the xxd command outputs the addresses. There in address 0x20
, is our target value, 30
. The binary is currently reading the program header offset as 48
, which in hexadecimal is 30
, so this is exactly what we would expect to see there (it always feels good to be validated)! Let’s change that to 40
(hex for 64
) with a modification to our previous script:
with open("13bit_fixed", "rb") as f:
data = bytearray(f.read())
data[32] = 0x40 # 30 -> 40
with open("13bit_fixed2", "wb") as f:
f.write(data)
Let’s see how that affected my file:
file 13bit_fixed2
13bit_fixed2: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/lb-linux-x86-64.so.2, BuildID[sha1]=4c4aeaa3691528f4774aedac52dac2e20e45c924, for GNU/Linux 3.2.0, not stripped
./13bit_fixed2
-bash: ./13bit_fixed2: cannot execute: required file not found
readelf -l 13bit_fixed2
Elf file type is DYN (Position-Independent Executable file)
Entry point 0x10c0
There are 13 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040
0x00000000000002d8 0x00000000000002d8 R 0x8
INTERP 0x0000000000000318 0x0000000000000318 0x0000000000000318
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/lb-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x00000000000007c8 0x00000000000007c8 R 0x1000
LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000
0x0000000000000351 0x0000000000000351 R E 0x1000
LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000
0x0000000000000134 0x0000000000000134 R 0x1000
LOAD 0x0000000000002dc8 0x0000000000003dc8 0x0000000000003dc8
0x0000000000003458 0x0000000000003488 RW 0x1000
DYNAMIC 0x0000000000002de0 0x0000000000003de0 0x0000000000003de0
0x00000000000001e0 0x00000000000001e0 RW 0x8
NOTE 0x0000000000000338 0x0000000000000338 0x0000000000000338
0x0000000000000020 0x0000000000000020 R 0x8
NOTE 0x0000000000000358 0x0000000000000358 0x0000000000000358
0x0000000000000044 0x0000000000000044 R 0x4
GNU_PROPERTY 0x0000000000000338 0x0000000000000338 0x0000000000000338
0x0000000000000020 0x0000000000000020 R 0x8
GNU_EH_FRAME 0x0000000000002008 0x0000000000002008 0x0000000000002008
0x000000000000003c 0x000000000000003c R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x0000000000002dc8 0x0000000000003dc8 0x0000000000003dc8
0x0000000000000238 0x0000000000000238 R 0x1
...
Look! The starting at offset
shows 64
, and we have Program Headers!
Fixed Bits So Far: 4
Progress! We’re not seg fault
ing immediately, but now we get this new required file not found
when we try to execute. Fortunately, we can see the file that is being looked for in the readelf
output above in the line [Requesting program interpreter: /lib64/lb-linux-x86-64.so.2]
.
I did some research on the missing file, and found that it is a very important Dynamic Linker library. Definitely a problem to be missing, and this took longer for me to recognize the issue than I’d like to admit (due to a combination of dyslexia moments and due to what I choose to believe was some extra deviousness on the part of the challenge author), but eventually I realized that the Dynamic Linker file is named
/lib64/ld-linux-x86-64.so.2
^
but the 13bit
program is looking for
/lib64/lb-linux-x86-64.so.2
^
The lb
vs the intended ld
was right in front of me the whole time. This is pretty obviously an error from a bit flip to me. Using the same methodology as before, let’s correct that. Here’s the xxd output showing the hex representation of the library:
xxd -s 0x318 -l 0x1c 13bit_fixed2
00000318: 2f6c 6962 3634 2f6c 622d 6c69 6e75 782d /lib64/lb-linux-
00000328: 7838 362d 3634 2e73 6f2e 3200 x86-64.so.2.
The byte we want is in the fifth hex pair. 62
is the hex representation of the ASCII character b
. We want to change that to 64
for d
.
>>> bin(int('62',16))
'0b1100010'
>>> bin(int('64',16))
'0b1100100'
Nice! Another two bits to change:
with open("13bit_fixed2", "rb") as f:
data = bytearray(f.read())
data[0x320] = 0x64 # 'b' (0x62) to 'd' (0x64)
with open("13bit_fixed3", "wb") as f:
f.write(data)
Let’s look at what our file looks like now:
file 13bit_fixed3
13bit_fixed3: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=4c4aeaa3691528f4774aedac52dac2e20e45c924, for GNU/Linux 3.2.0, not stripped
./13bit_fixed3
Segmentation fault (core dumped)
readelf -l 13bit_fixed3
Elf file type is DYN (Position-Independent Executable file)
Entry point 0x10c0
There are 13 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040
0x00000000000002d8 0x00000000000002d8 R 0x8
INTERP 0x0000000000000318 0x0000000000000318 0x0000000000000318
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x00000000000007c8 0x00000000000007c8 R 0x1000
LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000
0x0000000000000351 0x0000000000000351 R E 0x1000
LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000
0x0000000000000134 0x0000000000000134 R 0x1000
LOAD 0x0000000000002dc8 0x0000000000003dc8 0x0000000000003dc8
0x0000000000003458 0x0000000000003488 RW 0x1000
DYNAMIC 0x0000000000002de0 0x0000000000003de0 0x0000000000003de0
0x00000000000001e0 0x00000000000001e0 RW 0x8
NOTE 0x0000000000000338 0x0000000000000338 0x0000000000000338
0x0000000000000020 0x0000000000000020 R 0x8
NOTE 0x0000000000000358 0x0000000000000358 0x0000000000000358
0x0000000000000044 0x0000000000000044 R 0x4
GNU_PROPERTY 0x0000000000000338 0x0000000000000338 0x0000000000000338
0x0000000000000020 0x0000000000000020 R 0x8
GNU_EH_FRAME 0x0000000000002008 0x0000000000002008 0x0000000000002008
0x000000000000003c 0x000000000000003c R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x0000000000002dc8 0x0000000000003dc8 0x0000000000003dc8
0x0000000000000238 0x0000000000000238 R 0x1
...
Nice! Note that the program interpreter is now correctly displaying as /lib64/ld-linux-x86-64.so.2
.
Fixed Bits So Far: 6
We’re back to seg fault
ing on running the program, so we know that there’s still some more work to do to get it to run. Let’s do an objdump
on the most up-to-date file and see what we’re working with.
objdump -d 13bit_fixed3
...
Disassembly of section .text:
0000000000001090 <_start>:
1090: 31 ed xor %ebp,%ebp
1092: 49 89 d1 mov %rdx,%r9
1095: 5e pop %rsi
1096: 48 89 e2 mov %rsp,%rdx
1099: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp
109d: 50 push %rax
109e: 54 push %rsp
109f: 45 31 c0 xor %r8d,%r8d
10a2: 31 c9 xor %ecx,%ecx
10a4: 48 8d 3d 89 01 00 00 lea 0x189(%rip),%rdi # 1234 <main>
10ab: ff 15 0f 2f 00 00 call *0x2f0f(%rip) # 3fc0 <__libc_start_main@GLIBC_2.34>
10b1: f4 hlt
10b2: 66 2e 0f 1f 84 00 00 cs nopw 0x0(%rax,%rax,1)
10b9: 00 00 00
10bc: 0f 1f 40 00 nopl 0x0(%rax)
00000000000010c0 <deregister_tm_clones>:
10c0: 48 8d 3d 59 61 00 00 lea 0x6159(%rip),%rdi # 7220 <stdout@GLIBC_2.2.5>
10c7: 48 8d 05 52 61 00 00 lea 0x6152(%rip),%rax # 7220 <stdout@GLIBC_2.2.5>
10ce: 48 39 f8 cmp %rdi,%rax
10d1: 74 15 je 10e8 <deregister_tm_clones+0x28>
10d3: 48 8b 05 ee 2e 00 00 mov 0x2eee(%rip),%rax # 3fc8 <_ITM_deregisterTMCloneTable@Base>
10da: 48 85 c0 test %rax,%rax
10dd: 74 09 je 10e8 <deregister_tm_clones+0x28>
10df: ff e0 jmp *%rax
10e1: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
10e8: c3 ret
10e9: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
...
This command decompiles the executable and lets us peruse the code. Since the challenge is all about ELF header information, I felt that it was likely that the seg fault
likely comes from basic information about the file rather than from a logic error within its code, so I started at the very beginning and walked myself down the assembly code. It didn’t take long to spot something off.
The readelf
command from above tells us that the entry point (e_entry
), where the program’s <start>
function is, lies at address 10c0
. Interestingly enough, navigating to that point in the disassembled code, this is not the <start>
function, but is instead the <deregister_tm_clones>
function. Fortunately, <start>
isn’t too difficult to locate — it’s the function immediately preceeding it, at address 1090
. I think this is the next bit flip to fix, let’s change the c0
to 90
and point the entry point to the actual <start>
function.
>>> bin(int('c0',16))
'0b11000000'
>>> bin(int('90',16))
'0b10010000'
Here is the modification to my script to accomplish this:
with open("13bit_fixed3", "rb") as f:
data = bytearray(f.read())
data[0x18] = 0x90 # Change C0 -> 90
data[0x19] = 0x10 # (Remains the same)
with open("13bit_fixed4", "wb") as f:
f.write(data)
That’s 2 more bits!
Fixed Bits So Far: 8
You know the drill by now, let’s see where the program is at. I’ll start by running the newest…

Wait. Did that just run? Hooray! It looks like it didn’t do anything though, so I opened it up in Ghidra to see what’s going on. Here is the decompiled main
code.
undefined8 main(void)
{
int __fd;
undefined8 uVar1;
ssize_t sVar2;
int local_c;
for (local_c = 0; local_c < 0x31ab; local_c = local_c + 1) {
data[local_c] = data[local_c] ^ (byte)*(undefined4 *)(data2 + (long)(local_c % 4) * 4);
}
function(data,0x31ab);
__fd = open("bad",0x241,0x1a4);
if (__fd == -1) {
perror("bad");
uVar1 = 1;
}
else {
sVar2 = write(__fd,data,0x31ab);
if (sVar2 == -1) {
perror("bad");
close(__fd);
uVar1 = 1;
}
else {
close(__fd);
uVar1 = 0;
}
}
return uVar1;
}
On a low level, I can see that the function manipulates a large array (0x31ab
values) with some XOR funtions, processes the data, and creates a file called bad
and writes the data to it.
Looking back at the directory with my binaries, I see this bad
file had successfully written, there, I had just missed it! Based on the code, I’m going to assume that the flag is built from the array and will be printed into this bad
file. Once again, let’s check the new file:
file bad
bad: data
xxd bad
00000000: 8260 42ae 444e 4549 0000 0000 d2e1 d825 .`B.DNEI.......%
00000010: 1a57 3a4e 7ffd 555e c800 0000 0001 e008 .W:N..U^........
00000020: 0140 0000 0000 0f00 400a 0000 0000 0078 .@......@......x
00000030: 0200 5000 0000 0003 c010 0280 0000 0000 ..P.............
Well this one looks more corrupted than the original 13bit
file. bad
lives up to its name! I still have six more bits to flip, but the magic number alone would need more than that to fix this one, so I doubt that the challenge scope is going to have us interacting with bad
like we have 13bit
, so I turned my attention back to the latter.
There’s a single function in main
, so I opened it up to see what it did:
************
* FUNCTION *
************
undefined function()
undefined <UNASSIGNED> <RETURN>
function XREF[4]: Entry Point(*), main:001012b1(c),
00102034, 001020f8(*)
001011bc c3 RET
001011bd 48 ?? 48h H
001011be 89 ?? 89h
001011bf e5 ?? E5h
001011c0 48 ?? 48h H
001011c1 89 ?? 89h
001011c2 7d ?? 7Dh }
001011c3 e8 ?? E8h
...
Right away, I see that the first operation is a RET
, but there are something like 100 more instructions after it. I’d wager that RET
has been corrupted, but the question is, what from?
I tried setting it to NOP
(no operation) with the value of 90
to see if I could skip over it, but that caused the program to seg fault
again. Turning to the trusty ol’ interwebs, I found this Stack Overflow thread that mentions a standard C function prologue that seems familiar:
// Standard function prologue (setting up the stack frame):
40052d: 55 push %rbp
40052e: 48 89 e5 mov %rsp,%rbp
the 48
89
e5
at the end of the first four hex digits in this function match what I have in mine. So I’ll change the RET
function (c3
) to be push %rbp
(55
). Here is the byte comparison:
>>> bin(int('c3',16))
'0b11000011'
>>> bin(int('55',16))
'0b01010101'
That’s four bits different, so if it’s successful, that’s 12 bits corrected! I made the change with the following modification to my script:
with open("13bit_fixed4", "rb") as f:
data = bytearray(f.read())
data[0x11bc] = 0x55 # Change C3 -> 55
with open("13bit_fixed5", "wb") as f:
f.write(data)
Running ./13bit_fixed5
now runs fully, and still outputs a new file called bad
. But now, when I analyze the new file, I get this output:
file bad
bad: PNG image data, 1008 x 195, 8-bit/color RGBA, non-interlaced
xxd bad
00000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452 .PNG........IHDR
00000010: 0000 03f0 0000 00c3 0806 0000 0025 59fa .............%Y.
00000020: 9000 0000 0173 5247 4200 aece 1ce9 0000 .....sRGB.......
00000030: 0004 6741 4d41 0000 b18f 0bfc 6105 0000 ..gAMA......a...
To be honest, I figured bad was going to be a simple text document that just gave us the flag, but apparently it’s a PNG. That explains the large size of the array of data being processed in the main function, that was nagging at the back of my mind because that was much bigger than the simple text file I was expecting. I renamed the file to give it the proper extension for Windows (bad.png
), then opened it up in the default photo app:

Artwork to rival Da Vinci! It brought tears to my eyes.
By my count, I only fixed 8 bits plus 4 more bits (so 12 total) from this most recent edit… Maybe I skipped a step, maybe the last bit set up an OCR so I don’t have to hand type the flag out, or maybe I’m bad at counting, who knows? Either way, we took a corrupt ELF file and reverse engineered it by using our knowledge of ELF headers and assembly OP codes in order to fix the program, run it, and read the PNG file it produced. This challenge taught me about the fragility and precision of ELF structures, and perhaps caused a little retrospection on myself and my impact on the world around me. If just 13 seemingly insignificant bits can wreak havoc, what could a couple ordinary people do when united in a cause? Hopefully something better than just corrupt a silly challenge executeable…
Flag
vere{de4db3ef_d4d}