[PatchMe] Playing With ELF Structures

Anyone who calls him/herself a hacker, should know about binary formats, especially if you are a low-level wizard.

How well do you know the ELF?


###Difficulty

Meh, probably easy.


###Rules
Though it’s easy to identify what’s up, do justify why you patched the binary that way.


###Hints
Hell no.


###Binary
The binary is super tiny and can be downloaded from here.


Have fun!

8 Likes

Solved, here are the changes and md5sum:

$ ./fixed
$ radiff2 exit fixed
0x00000042 00 => 01 0x00000042
0x00000044 05 => 00 0x00000044
0x00000046 00 => 05 0x00000046
0x00000052 40 => 00 0x00000052
0x00000054 00 => 40 0x00000054
0x0000005a 40 => 00 0x0000005a
0x0000005c 00 => 40 0x0000005c
0x00000060 89 => 00 0x00000060
0x00000062 00 => 89 0x00000062
0x00000068 89 => 00 0x00000068
0x0000006a 00 => 89 0x0000006a
0x00000072 20 => 00 0x00000072
0x00000074 00 => 20 0x00000074
$ md5sum fixed
f4d39629c07a157f80e544b1bd04bcc8

Sorry mate, your solution is disqualified.

As I mentioned above, you need to explain why you did what you did. In ELF terms to be even more precise. Just comparing and changing values doesn’t say anything to me.

I set that challenge so people can read up the ELF structures because it’s a valuable piece of info.

2 Likes

Awesome challenge @_py! :smile:

Starting simply, running exit produces a Segmentation fault.

Dumping the symbol table by running readelf -s exit shows that there is no main function but there is a _start. However, it has a type of NOTYPE which is odd as it would usually have a type of FUNC:

Symbol table '.symtab' contains 7 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000400080     0 SECTION LOCAL  DEFAULT    1
     2: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS exit.nasm
     3: 0000000000400080     0 NOTYPE  GLOBAL DEFAULT    1 _start
     4: 0000000000600089     0 NOTYPE  GLOBAL DEFAULT    1 __bss_start
     5: 0000000000600089     0 NOTYPE  GLOBAL DEFAULT    1 _edata
     6: 0000000000600090     0 NOTYPE  GLOBAL DEFAULT    1 _end

Running gdb exit, setting a breakpoint at _start and running it, also seg faults before hitting the breakpoint. This suggests something is badly wrong with the ELF file…

Running readelf -SW exit shows that there aren’t many sections but they look OK:

There are 5 section headers, starting at offset 0x180:

Section Headers:
  [Nr] Name              Type            Address          Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            0000000000000000 000000 000000 00      0   0  0
  [ 1] .text             PROGBITS        0000000000400080 000080 000009 00  AX  0   0 16
  [ 2] .shstrtab         STRTAB          0000000000000000 00015b 000021 00      0   0  1
  [ 3] .symtab           SYMTAB          0000000000000000 000090 0000a8 18      4   3  8
  [ 4] .strtab           STRTAB          0000000000000000 000138 000023 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

However, the program (segment) headers (dumped using readelf -lW) look damaged:

Elf file type is EXEC (Executable file)
Entry point 0x400080
There are 1 program headers, starting at offset 66

Program Headers:
  Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
  <unknown>: 500 0x000000 0x0000000000000040 0x0089000000000040 0x89000000000000 0x000000     0x20

 Section to Segment mapping:
  Segment Sections...
   00     .shstrtab .symtab .strtab

Unless we want to start moving parts of the binary around and updating offsets let’s try fixing up the one program (segment) header we have:

First, set the segment type to 1 so that the segment is loadable:

  • Change byte 0x42 from 0x00 to 0x01
  • Change byte 0x44 from 0x05 to 0x00

Then set the flags to 5 so that the segment is both readable (4) and executable (1)

  • Change byte 0x46 from 0x00 to 0x05

Now we can load and execute the segment we need to make sure it contains the .text section which contains the code:

Update the virtual address so that it plus the size can contain the address of the .text section:

  • Change byte 0x52 from 0x40 to 0x00
  • Change byte 0x54 from 0x00 to 0x40

Update the ‘size in file’ so that it contains the address of the .text section:

  • Change byte 0x62 from 0x00 to 0x89
  • Change byte 0x68 from 0x89 to 0x00

Finally update the ‘size in memory’ so that it also contains the address of the .text section:

  • Change byte 0x6A from 0x00 to 0x89

Re-running readelf -lW exit shows that we’ve fixed up the program header:

Elf file type is EXEC (Executable file)
Entry point 0x400080
There are 1 program headers, starting at offset 66

Program Headers:
  Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
  LOAD           0x000000 0x0000000000400000 0x0089000000000040 0x000089 0x000089 R E 0x20

 Section to Segment mapping:
  Segment Sections...
   00     .text 

And running exit now no longer seg faults :slight_smile:

2 Likes

God damn! You killed it man! +1 for the unintended solution :ok_hand:

However, the fix could be much simpler, 1 byte patching to be precise :slight_smile:

You identified the problem correctly. The issue was indeed related to the program headers. If you think of it carefuly though, why would readelf assume that the program header was corrupted? Its job is to parse the ELF header after all.

Hint: The e_phoff field holds the offset of the program header table. The ELF header is 64 bytes (0x40) and this structure will immediately follow.

2 Likes

I solved it as it was intended. Feeling no glory now anymore with that hint being given by @_py tho :smiley:
Anyways… First things first impressive solution @ATGC .

As in the extended solution above I first used tools like

gdb


readelf


objdump


xxd/hexdump

on that file.
I came to the same conclusion that the Program Headers were kinda weird with that unknown type.

I didn’t really know how to continue with my created binary logs and hex dumps, until
I read more into that matter on how such an ELF binary is structured.
Let’s take a look at the hex dump and some explanations along the way…

But first here’s an awesome paper on the ELF specifications

An ELF header is 64 bytes in total (0x40) and structured like shown below.

typedef struct {
    unsigned char e_ident[EI_NIDENT];          //red
    uint16_t      e_type;                      // yellow
    uint16_t      e_machine;                   // green  
    uint32_t      e_version;                   // turquoise
    ElfN_Addr     e_entry;                     // blue
    ElfN_Off      e_phoff;                     // pink
    ElfN_Off      e_shoff;                   
    uint32_t      e_flags;
    uint16_t      e_ehsize;
    uint16_t      e_phentsize;
    uint16_t      e_phnum;
    uint16_t      e_shentsize;
    uint16_t      e_shnum;
    uint16_t      e_shstrndx;
} ElfN_Ehdr;

Funky colors inc!

Ok what the hell is this supposed to be.

red

Basically e_ident is 16 bytes and the first 4 bytes are fixed: 0x7F, E, L, F. (7f45 4c46)
The 5th byte determines if the program will be running on 32-bit(0x01) or 64-bit(0x02).
The 6th byte indicates the integer format.
The 7th byte is the ELF version ( always 0x01 btw. )
The 8th byte is the ABI (Application Binary Interface)
The 9th byte is the version.
Rest is padding.

=> This makes the red marked area. If you wanna read more about it or why these values are set click the awesome paper above.

yellow

This field determines if its an executable(0x02), an object file(0x01), a shared library(0x03) or a core file(0x04)

=> executable is set. Ok.

green

This one sets the CPU arch basically.
Look up the EM_values here
We have a 0x3e (int=62) which stands for EM_AMD64

turquoise

This one is always 0x01 so just let it be.

blue

Now it’s getting more interesting!
This 8 byte area ( because of 64-bit ELF), marks the virtual address of the programs entry point.

How it’s calculated is a different story, and takes a lot more text.
Just keep that in mind that we can find it here.

pink

This field holds the program header table’s file offset in bytes.
If the file has no program header table, this member holds zero.

The ELF header is 64 bytes (0x40) and this structure will immediately follow.

It should be set to 0x40 <=> ‘4000 0000 0000 0000’ as well then!

So this one is the match winning one! :trophy:

So let’s edit it to 0x40 and assembly it again:

$ xxd -r exit.dump > new_exit && chmod +x new_exit

The Segfault is gone!

Edit: We didn’t finish looking at the whole ELF header this time, since we found the error quite early!
So don’t take these 6 colors above as “the whole ELF header” :slight_smile: .

Final Words

Py labeled this challenge as easy so I jumped the train and thought I’d be done in a couple of minutes (haha…), but then I realized that I lacked heavy knowledge on this internal stuff!
So he made me read page after page to advance and try out a few things.
After finishing this challenge I got to understand that this whole ELF thing was not a small chapter which I can quickly finish, but that it is a damn big book.
I definitely need some more deep diving into this material to fully grasp the concepts of ELF binaries!

It was fun though. These kind of challenges which make you investigate stuff make you learn the most imho :thumbsup:

4 Likes

As an ELF-noob, this was incredibly helpful for me to understand how this works. Thanks!

2 Likes

Congrats @ricksanchez! :muscle:

Now you understand why it was easy :wink: If you know the internals, everything is easy.

I hope you learnt something new out of it.

2 Likes

@_py Ah, of course! I noticed that the bytes I was patching seemed suspiciously similar to those nearby, and it crossed my mind that if I could just shift the header everything might fall into place… It never crossed my mind to just change the offset though! :man_facepalming:

Great learning experience for me :smiley:

@ricksanchez Great analysis and a clean, simple solution! You rock! :smiley:

@_py Yes I understand now, and I definitely learned a ton since I never really looked at the ELF internals the last few weeks when trying to advance in RE. This was a nice change for me.

@ATGC Thanks man :slight_smile: !

BTW you can patch it from the command line like this. Nice challenge @_py

echo -e "\x40" | dd of=exit seek=32 bs=1 count=1 conv=notrunc
3 Likes

I’m not home atm but are you sure it shouldn’t be:

[spoiler]echo -ne “\x40” | dd of=exit seek=32 bs=1 count=1 cont=notrunc

IIRC, -e will add a newline at the end too. Might be wrong though.
[/spoiler]

4 Likes


Setting count and bs to 1 we are effectively discarding the \n (one single byte will be written to the output, despite of how many we provide as input), so it works fine, but your is, let’s say, more formal :slight_smile:

3 Likes

@_py The link to binary is not available any more :disappointed: Can we still play with it ?

base64 -d thebelow | gunzip > chall
H4sICAvqr1kAA2V4aXQAq3f1cWNiZGSAASYGOwYQr4HBAcx3goo3IJQAZSyAahwYWIGqQcKsDMjA
AYXuhPI64fIKKKoN/++wAVL8qGZgB8xA2xrg5kMAyH4Who//0dUKgDGmem6oeCdDAoq4OA5xOaj4
BDRxhtSKzBK9vMTiXIb4+KTi4vjiksSiEob41JTEkkQglZfCwKBXXJlbkpgEpEuKIHQGjFWSWlFC
hI8JA2kGSBiwQfkw/zZA+Zxo6gXQ+IIMoHDFBNHQ+FZEE2fEwmfCon8ClF4BpVmg9nBA+RJI7sNm
vwXUImUC9gMAxdDVAcACAAA=

My apologies for the inconvenience.

Thanks for the challenge !
here’s my solution :

[spoiler]readelf gives me the 66 for the start of header but 64 for the size of

$readelf -h chall [...] Début des en-têtes de programme : 66 (octets dans le fichier) [...] Taille de cet en-tête: 64 (octets)
because the length of the ELF header is 64 the start of the program header should be 64 for as well.
So I edited e_phoff from 0x42 ( 66 ) to 0x40 ( 64 ) :

neolex@archlinux-pc ~/D/h/c/0/patchme_playing_with_elf> radiff2 chall patched 0x00000020 42 => 40 0x00000020
And it executes without errors!
[/spoiler]

2 Likes