fun-fiber

这是一道很有意思的题,是一个简单协程的实现方式,是我从看雪holling师傅发的文章里面看见的,第一次看见这种题,人生有很多第一次 :)

如果你对glibc里面协程实现方式不是很了解的话,需要去了解一下:

https://segmentfault.com/p/1210000009166339/read#2-1-_getcontext_u5B9E_u73B0

这篇文章讲的比较清楚,如果是第一次接触ucontext_t的话,这篇文章是不二之选。

要了解是ucontext_t基本结构和四个函数:

1
2
3
4
5
6
7
8
9
10
typedef struct ucontext
{
unsigned long int uc_flags;
struct ucontext *uc_link; //后继上下文
stack_t uc_stack; //用户自定义栈
mcontext_t uc_mcontext; //保存当前上下文,即各个寄存器的状态
__sigset_t uc_sigmask; //保存当前线程的信号屏蔽掩码

} ucontext_t;

1
2
3
4
5
6
7
8
int getcontext(ucontext_t *ucp); //将当前的寄存器状态放进ucp里面,相当于保存当前状态。

int setcontext(const ucontext_t *ucp); //从ucp取出状态恢复上下文。

void makecontext(ucontext_t *ucp, void (*func)(), int argc, ...); //设置已经初始过的ucp,改变其具指向的上下文

int swapcontext(ucontext_t *oucp, ucontext_t *ucp);//切换ucp保存的状态,并保存当前的状态到oucp

这里值得一提的是,ucontext_t->uc_link的用来保存后继的上下文。这点是怎么做到的可以去看看具体的swapcontext的汇编代码,这几个函数几乎都是用内联的汇编直接写的。它会劫持调用指定函数的ret来进行ucontext_t后继状态的切换。不过这道题没那么复杂。

这道题的难点都在于你对协程应用的了解,具体exp过程holling的文章已经写的很详细,在这里我只想记录一下我当时一些想法和心得。

协程很关键的地方在于状态的切换,比如现在执行的某个过程,到一定程度,你需要去暂停当前的状态,转而去执行另一项任务,就相当于cpu分配给执行程序的时间片一样。什么时候该切,在切的时候需要注意什么,这是我们需要关注的。

这道题就体现了单线程下的协程中临界区存在的问题。先把题目给的source贴一下

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
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <ucontext.h>
#include <assert.h>
#include <unistd.h>

// CRC

static uint32_t crc32_tab[] = {
0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f,
0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2,
0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9,
0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c,
0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423,
0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106,
0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d,
0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950,
0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7,
0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa,
0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81,
0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84,
0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb,
0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e,
0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55,
0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28,
0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f,
0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242,
0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69,
0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc,
0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693,
0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d
};

uint32_t crc32(uint32_t crc, const unsigned char *buf, size_t size) {
const uint8_t *p;

p = buf;
crc = crc ^ ~0U;

while (size--) {
crc = crc32_tab[(crc ^ *p++) & 0xFF] ^ (crc >> 8);
}

return crc ^ ~0U;
}

// Fiber

static ucontext_t *g_fib;
static ucontext_t *g_active_fibs;
static ucontext_t *g_unactive_fibs;

void fiber_init() {
ucontext_t *fib = malloc(sizeof(ucontext_t));
getcontext(fib);
fib->uc_link = 0;
g_fib = fib;
g_active_fibs = fib;
}

void fiber_yield() {
ucontext_t *next = g_fib->uc_link;
if (next == NULL)
next = g_active_fibs;

if (next == g_fib)
return;

ucontext_t *current_context = g_fib; // g_fib
g_fib = next;
swapcontext(current_context, g_fib); //first g_active_fib swapcontent(g_fib,g_fib)
}

void __fiber_new(void (*f)()) {
f();
while (1)
fiber_yield();
}

void fiber_new(void (*func)()) {
ucontext_t *fib = malloc(sizeof(ucontext_t));
getcontext(fib); //init current status

fib->uc_stack.ss_sp = malloc(0x8000); // init customize stack
fib->uc_stack.ss_size = 0x8000;

makecontext(fib, (void*)__fiber_new, 1, func); //
printf("Starting Worker #%08x\n", (unsigned)fib);
fib->uc_link = g_active_fibs;
g_active_fibs = fib;
}

int fiber_toggle(ucontext_t *moving, ucontext_t **from, ucontext_t **to) {
ucontext_t *fib = *from;
ucontext_t *last = NULL;
while(fib != NULL && fib != moving) {
last = fib;
fib = fib->uc_link;
}
if(fib == NULL)
return 1;

if(last == NULL)
*from = fib->uc_link;
else
last->uc_link = moving->uc_link;//unlink

moving->uc_link = *to;//insert to other queue
*to = moving;
return 0;
}

int fiber_pause(void *id) {
return fiber_toggle(id, &g_active_fibs, &g_unactive_fibs);
}

int fiber_resume(void *id) {
return fiber_toggle(id, &g_unactive_fibs, &g_active_fibs);
}

// Lock free stack
struct node {
void *entry;
struct node* next;
};

struct node* job_stack;
struct node* result_stack;

void push(struct node **stack, void* e){
struct node* n = malloc(sizeof(struct node));
n->entry = e;
n->next = *stack;
*stack = n;
}

void* pop(struct node **stack) {
struct node *old_head, *new_head;
while(1) {
old_head = *stack;
if(old_head == NULL){
return NULL;//empty stack case
}
new_head = old_head->next;
/*
这里用了一个中间变量来判断临界区有效性,但是这里存在堆上内存空间的复用

第一次yield的时候,worker都会停在这里。不会继续执行下去

*/
fiber_yield();//exploit here
if(*stack == old_head) {
*stack = new_head;//if exploit properly, *stack can be setted to a dangling pointer
break;
}
}

void *result = old_head->entry;
free(old_head);
return result;
}

// App

struct job {
unsigned int id;
unsigned int len;
unsigned char *input;
unsigned int *result;
};

unsigned int job_id = 0;
unsigned int n_jobs = 0;
unsigned int n_workers = 0;
unsigned int n_results = 0;

void worker() {
while(1) {
printf("[Worker #%08x] Getting Job\n", (unsigned)g_fib);
struct job* job = pop(&job_stack);
if(job == NULL) {
printf("[Worker #%08x] Empty queue, sleeping\n", (unsigned)g_fib);
fiber_yield();
continue;
}
printf("[Worker #%08x] Got a job\n", (unsigned)g_fib);

*(job->result) = crc32(0, job->input, job->len);
n_jobs -= 1;
n_results += 1;
push(&result_stack, (void*)job);
}
}

int menu() {
printf("Menu: (stats: %d workers, %d jobs, %d results)\n", n_workers, n_jobs, n_results);
printf(" 1. Request CRC32 computation\n");
printf(" 2. Add a worker\n");
printf(" 3. Yield to workers\n");
printf(" 4. Toggle worker\n");
printf(" 5. Gather some results\n");
printf(" 6. Exit\n");
printf("> ");
int choice;
scanf("%d", &choice);
fgetc(stdin); // consume new line
return choice;
}

int main() {
unsigned int wid, size;
setbuf(stdout, 0);
fiber_init();
printf("=================================\n");
printf("=== CRC32 As A Service ==\n");
printf("=================================\n\n");

while(1) {
switch(menu()) {
case 1:
if(n_jobs < 10) {
printf("Size: ");
scanf("%d", &size);
fgetc(stdin); // consume new line
if(size > 0x100) {
printf("Error: input needs to be smaller than 0x100");
} else {
unsigned char *input = malloc(size);
printf("Contents:\n");
assert(size == fread(input, 1, size, stdin));

struct job *job = malloc(sizeof(struct job));
job->id = job_id;
job->len = size;
job->input = input;
job->result = malloc(sizeof(unsigned int));

push(&job_stack, job);
n_jobs += 1;

printf("Requested job. ID: #%08x\n", job_id++);
}
} else {
printf("Error: job worker limit\n");
}
break;
case 2:
if(n_workers < 4) { // max 4 workers
n_workers++;
fiber_new(worker);
} else {
printf("Error: reached worker limit\n");
}
break;
case 3:
printf("Working...\n");
fiber_yield();
printf("Finished working for now.\n");
break;
case 4:
printf("Worker ID: #");
scanf("%x", &wid);
fgetc(stdin); // consume new line
if(fiber_pause((ucontext_t *)wid) == 0) {
printf("Pausing worker #%08x.\n", wid);
} else if(fiber_resume((ucontext_t *)wid) == 0) {
printf("Resuming worker #%08x.\n", wid);
} else {
printf("Error: Worker #%08x not found.\n", wid);
}
break;
case 5:
while(1) {
struct job *job = pop(&result_stack);
if(job == NULL) {
printf("No more results right now. Try again later.\n");
n_results = 0;
break;
} else {
printf("Job #%08x result: %08x\n", job->id, *job->result);
free(job->result);
free(job);
}
}
break;
default:
return 0;
}
}
return 0;
}

主要功能提供是crc的计算,在此之前我还去特意复习了一下crc,大学学的几乎全忘了 :(

整个过程相当于一个协程池,job_stacktaskswokers相当于协程,关注点在于什么切的状态。最先遇到切状态的点在于pop里面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void* pop(struct node **stack) {
struct node *old_head, *new_head;
while(1) {
old_head = *stack;
if(old_head == NULL){
return NULL;//empty stack case
}
new_head = old_head->next;
fiber_yield();//exploit here
if(*stack == old_head) {
*stack = new_head;//if exploit properly, *stack can be setted to a dangling pointer
break;
}
}
void *result = old_head->entry;
free(old_head);
return result;
}

对于临界区的正常读写处理应该是先给临界区加锁。比如这里pop应该是一个原子操作,包括出栈和读写。但是这里你可以看到的是,它却在读以后,并没有出栈,而是直接切了状态。这样做其实也没有问题,在多线程操作的如果不能加锁,在进行对临界区数据处理的时候,应该进行相应的数据效验,判断这个拿到的数据是不是有效的。它也做了这个操作,但是这个check操作显然不太合适,仅仅比较了拿到的数据 是否还是栈顶元素,并没有进一步验证数据的内容完整性。

起初这个题我是x64编译,x64编译和x32这里有一点点小差异。差异在哪呢?

就是当malloc(sizeof(struct job)) free 掉以后会进入fastbins[0]nodejob->result一样。其实这里也影响不大,照样可以让job->result malloc到我们想要的fastbin chunk

这里的exp可能有一点绕。因为一直在会经历很多次出栈压栈。目标chunk的位置的引用一直在变。

具体exp的过程就是

  1. 申请开始2个worker,这里开始就直接申请两个worker是有好处的便于计算heap address。
  2. 再添加两个job。
  3. 第一次fiber_yield。第一次fiber_yield ,两个worker的状态都会停在pop的地方,拿到的job都是栈顶元素。
  4. 暂停第一个worker,再次fiber_yield ,让第二个worker处理完栈顶的job。把结果压入结果栈。
  5. 输出结果,在这里输出的结果的时候。同样会经历pop,第二个worker也会处理完,把结构压入结果栈。接下来就是输出结果,输出结果的时候首先会释放result_node ,再释放job->result, 这两个都是在32位下都是fastbin[0]0x10下面的chunk,这个时候很巧妙是fastbin的结构。

首先是job入栈,然后是job_node出栈,然后result_node入栈,然后result_node出栈,同时释放所有job结构,但是这里job->input始终是不会释放的。正是因为这种对称的流程,在完成两个job以后,fastbin[0]会有四个chunk。如果再次分配正常job,确保input的大小还是大于0xc。释放的第一次栈顶node内存空间,同样还是栈顶node。

这时候再次恢复被pause的worker1,check的条件还是成立。这里会继续将栈顶元素出栈。同时栈顶变成了原先第二个job_node,但是这个job_node 现在还躺在fastbin里面。所以这里造成了一个UAF。

如何利用这个UAF来改变进程控制呢?简单

再次把worker1 pause掉。只要一个worker处理就行了,两个worker指向现在都是一样的,都是在fastbin里面的那个node。仔细看malloc的流程。如果这次让input的size小于0xc,input也会拿到0x10的chunk,这个uaf指向的node 就会被分配给接下来的job->result

这时在来一次yield。会开始计算我们新分配的job,同时结果写进了job->result, 也写进了next_job_node的entry。所以这里我们可以拿到一个任意地址的job结构。同时有了这个伪造job我们就可以射任何地方了

1
2
3
4
5
6
struct job {
unsigned int id;
unsigned int len;
unsigned char *input; //写的内容 当然这里还是要爆破滴滴滴
unsigned int *result; // 写哪里
};

这道题因为crc的存在。不能直接写,得爆破一下。 再就是考虑设哪里,射__malloc_hook 这些需要先leak先libc_base,所以这里还是得射stack上,来搞ROP。这也是这题我学到的东西。

worker的栈在heap上,所以这道题射worker的栈,第一步就是切栈,把栈切到事先准备好的job的input上,前面已经说过了这里job->input永远都不会被释放,所以这里还是安逸的。如何切呢?

在fiber_yield会用用leave/ret来切上下文。 leave == mov esp ebp; pop ebp.所以这里最好的就是在这里直接射ebp。holling师傅没有用算术gadget来加减 got表上的函数地址来计算system的地址。直接用的是putscanf的函数调用,这里跳内存方式也很精髓,也不能说跳内存,应该是循环调用函数。循环的用leave来切栈。在x32确实很方便。

这道题花费前前后后应该1天多。但是值得,工作的时候无法一直看,只能断断续续的看。第一次看这种类型题,但是人生有很多第一次。以后出现并发的先看临界区。:)