/
asmhttpd.asm
496 lines (396 loc) · 12.6 KB
/
asmhttpd.asm
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
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
; vim: syntax=nasm
;
; asmhttpd: A minimalist HTTP webserver for Linux, written in x86_64 assembly.
;
; Copyright (C) 2012 Calvin Owens <jcalvinowens@gmail.com>
;
; This program is free software; you can redistribute it and/or modify
; it under the terms of the GNU General Public License as published by
; the Free Software Foundation; either version 2 of the License, or
; (at your option) any later version.
;
; This program is distributed in the hope that it will be useful,
; but WITHOUT ANY WARRANTY; without even the implied warranty of
; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
; GNU General Public License for more details.
[bits 64]
global _start
%include "constants.inc" ; Flags for system calls, etc.
%include "syscall.inc" ; syscall() definition
%assign thread_memory_offset 0 ; This is rewritten each time tmalloc() is called
%macro reserve_thread_memory 2
%define %1 rbp+%[thread_memory_offset]
%define len%1 %2
%assign thread_memory_offset thread_memory_offset+%[%2]
%endmacro
%define tmalloc(a,b) reserve_thread_memory a,b
%define GLOBAL_REQUEST_TIMEOUT 5
%define GLOBAL_MAX_HTTP_REQLEN 1024
%define THREAD_MEM_SIZE 4096
%define CLIENT_TIMEOUT_MS 60000
%define NAME_MAX 254
segment .text
SEGMENT_BEGIN:
Setsock_ARGUMENT:
dd 0x00000001 ; Turn option ON
Sighand_SIGPIPE:
dq 0x0000000000000001 ; SIG_IGN is 1
dq 0x0000000000000000
SocketAddress:
dw 0x0002 ; AF_INET is 2 (AF_INET6 is 0xa)
dw 0x5000 ; TCP Port we want to bind to (:80) (Net byte ordering)
dd 0x00000000 ; Address to bind to (0.0.0.0 or ::)
dd 0x00000000
dd 0x00000000
dd 0x00000000
dd 0x00000000
SocketAddressNotRoot:
dw 0x0002 ; AF_INET is 2 (AF_INET6 is 0xa)
dw 0x901F ; TCP Port we want to bind to (:8080) (Net byte ordering)
dd 0x00000000 ; Address to bind to (0.0.0.0 or ::)
dd 0x00000000
dd 0x00000000
dd 0x00000000
dd 0x00000000
DecAsciiConvTable:
db 0x30,0x31,0x21,0x32,0x33,0x21,0x34,0x21,0x35,0x36,0x21,0x37,0x38,0x21,0x39,0x21
Response200:
db "HTTP/1.1 200 OK",0x0d,0x0a
%define lenResponse200 17
Header12:
db "Content-Length: "
%define lenHeader12 16
Error400:
db "HTTP/1.1 400 Bad Request",0x0d,0x0a,0x0d,0x0a
%define lenError400 28
Error403:
db "HTTP/1.1 403 Forbidden",0x0d,0x0a,0x0d,0x0a
%define lenError403 26
Error404:
db "HTTP/1.1 404 Not Found",0x0d,0x0a,0x0d,0x0a
%define lenError404 26
Error408:
db "HTTP/1.1 408 Request Timeout",0x0d,0x0a,0x0d,0x0a
%define lenError408 32
Error411:
db "HTTP/1.1 411 Length Required",0x0d,0x0a,0x0d,0x0a
%define lenError411 32
Error413:
db "HTTP/1.1 413 Request Entity Too Large",0x0d,0x0a,0x0d,0x0a
%define lenError413 41
Error414:
db "HTTP/1.1 414 Request URI Too Long",0x0d,0x0a,0x0d,0x0a
%define lenError414 37
Error500:
db "HTTP/1.1 500 Internal Server Error",0x0d,0x0a,0x0d,0x0a
%define lenError500 38
Error501:
db "HTTP/1.1 501 Not Implemented",0x0d,0x0a,0x0d,0x0a
%define lenError501 32
Error599:
db "HTTP/1.1 599 System Call Not Implemented",0x0d,0x0a,0x0d,0x0a
db "Your kernel is probably missing openat2(): Upgrade to >= 5.6",0x0a
%define lenError599 105
HelpMessage:
db "Usage: ./asmhttpd <webroot>",0x0a
%define lenHelpMessage 28
ech(____die) ; Who shall catch the catchers?
DieClientDisconnected:
syscall(sys_close,+rbx)
syscall(sys_munmap,+rbp,THREAD_MEM_SIZE)
syscall(sys_exit,-1)
DieError403:
lea rcx,[Error403]
mov edx,lenError403
jmp __die
DieError404:
lea rcx,[Error404]
mov edx,lenError404
jmp __die
DieError414:
lea rcx,[Error414]
mov edx,lenError414
jmp __die
DieError500:
lea rcx,[Error500]
mov edx,lenError500
jmp __die
DieError408:
lea rcx,[Error408]
mov edx,lenError408
jmp __die
DieError400:
lea rcx,[Error400]
mov edx,lenError400
jmp __die
DieError599:
lea rcx,[Error599]
mov edx,lenError599
jmp __die
PrintHelpMessageAndDie:
syscall(sys_write,2,[HelpMessage],lenHelpMessage)
jmp ____die
__die:
syscall(sys_write,+rbx,+rcx,+rdx)
syscall(sys_close,+rbx)
syscall(sys_munmap,+rbp,THREAD_MEM_SIZE)
____die:
syscall(sys_exit,-1);
HandleInitSysError:
HandleServeSysError:
mov rbx,rax
syscall(sys_exit,+rbx)
HandleGeneralSysErrorAfterOpen:
syscall(sys_close,+r13)
HandleGeneralSysError:
cmp eax,-EPERM
je DieError403 ; Permission Denied
cmp eax,-ENOENT
je DieError404 ; File not found
cmp eax,-EACCES
je DieError403 ; Permission denied
cmp eax,-ENAMETOOLONG
je DieError414 ; URI too long (although, is it?)
cmp eax,-EPIPE
je DieClientDisconnected
cmp eax,-ENOSYS
je DieError599
jmp DieError500
_start:
ech(HandleInitSysError)
; Parse command line arguments. At the start of the program, [rsp] contains a
; qword that tells us how many arguments we got, and [rsp+8] is the beginning of
; a list of pointers to the NUL-terminated argument strings.
; Make sure we have 2 arguments:
; 0: [rsp+8*1] Executable name (we ignore this)
; 1: [rsp+8*2] Webroot
mov r11,[rsp]
cmp r11,2
jne PrintHelpMessageAndDie
syscall(sys_open,+[rsp+8*2],O_DIRECTORY,NULL)
mov r15,rax ; Save webroot file descriptor
; Ignore SIGPIPE
syscall(sys_rt_sigact,13,[Sighand_SIGPIPE],NULL,8)
; Create and set SO_REUSEADDR on the socket
syscall(sys_socket,2,1,NULL)
mov rbx,rax
syscall(sys_setsockopt,+rbx,SOL_SOCKET,SO_REUSEADDR,[Setsock_ARGUMENT],4)
syscall(sys_getuid)
test rax,rax
jnz .NotRoot
; Bind, and drop privs (nobody)
syscall(sys_bind,+rbx,[SocketAddress],24)
syscall(sys_setgid,65535)
syscall(sys_setuid)
jmp .OverNotRoot
.NotRoot:
syscall(sys_bind,+rbx,[SocketAddressNotRoot],24)
.OverNotRoot:
syscall(sys_listen,+rbx,1024)
; Now, we can unmap everything but our single page, including the stack!
; If you use L5 pagetables, change the shift by 47 to 56
lea rdi,[SEGMENT_BEGIN]
shr rdi,12
inc rdi
shl rdi,12
; Unmap everything after .text
mov rsi,1
shl rsi,47
sub rsi,rdi
sub rsi,4096 ; Highest page is off-limits
syscall(sys_munmap)
; Unmap everything before .text
mov rsi,rdi
shr rsi,12
dec rsi
shl rsi,12
dec rsi
xor rdi,rdi
syscall(sys_munmap)
ServeHTTP:
ech(HandleServeSysError)
mov r8,rbx ; file descriptor for listening socket
mov r9,CLONE_FS|CLONE_THREAD|CLONE_SIGHAND|CLONE_VM|CLONE_FILES
mov r10d,sys_accept
mov r12d,sys_clone
xor rsi,rsi
xor rdx,rdx
.AcceptNextRequest:
syscall(+r10,+r8)
mov rbx,rax
syscall(+r12,+r9)
jnz .AcceptNextRequest
;; (rbx contains client connection file descriptor)
HandleConnection:
ech(HandleGeneralSysError)
syscall(sys_mmap,NULL,THREAD_MEM_SIZE,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS|MAP_POPULATE,NULL,NULL)
mov rbp,rax ; Store offset for tmalloc() macro
tmalloc(PollStruct,8)
; Build a struct pollfd in memory
mov [PollStruct],rbx
mov dword [PollStruct+4],POLLIN
tmalloc(ClientRequest,GLOBAL_MAX_HTTP_REQLEN)
; Load offset and counter for receieve loop
lea r9,[ClientRequest]
mov r10d,GLOBAL_MAX_HTTP_REQLEN
ReceiveRequest:
syscall(sys_poll,[+PollStruct],1,CLIENT_TIMEOUT_MS)
jz DieError408
syscall(sys_read,+rbx,+r9,+r10)
jz DieClientDisconnected
; Did we get enough data to be valid?
mov ecx,GLOBAL_MAX_HTTP_REQLEN
sub rcx,r10
add rcx,rax
cmp rcx,18
jge GotEnough
; Need to wait for more, adjust offsets and block again
add r9,rax
sub r10,rax
jmp ReceiveRequest
GotEnough:
mov esi,0x0a0d0a0d
lea rdi,[r9+rax-4]
lea r8,[ClientRequest]
.FindTerminator:
cmp esi,dword [rdi]
je short .FoundTerminator
dec rdi ; If we haven't reached the beginning...
cmp rdi,r8 ; ...then keep looking
jne short .FindTerminator
add r9,rax ; Adjust offset and counter
sub r10,rax
jz DieError414 ; If zero, send 412 REQUEST URI TOO LARGE
jmp ReceiveRequest ; Go back up and wait for more data
.FoundTerminator:
lea r14,[r9+rax] ; Store last byte received from client
; Skip past the METHOD specification, find the start of the URI:
mov eax,0x20
lea rdi,[ClientRequest]
mov ecx,20
repne scasb
jne DieError400 ; Go thou and fuck with me no more
mov r13,rdi
; NULL-terminate the filename
mov ecx,GLOBAL_MAX_HTTP_REQLEN
repne scasb
jne DieError400 ; This should never happen
mov byte [rdi-1],0x00
lea r12,[rdi-1] ; Store addr of last byte of URI
lea r8,[rbp+thread_memory_offset] ; Load scratch address, to store offsets of escaped %xx literals
xor r10,r10 ; Zero index registers
xor r9,r9
mov eax,0x25 ; '%' in ASCII
mov rcx,r12 ; Counter for 'repne scasb'...
sub rcx,r13 ; ...subtract from end to get length
cmp rcx,3 ; If there are less than 3 characters...
jl ExpandDone ; ...there can't be anything to unescape
mov rdi,r13 ; Beginning of URI
ExpandChar:
repne scasb ; Search for a '%' character
jne ExpandOut ; ...if we didn't find one, we're done
cmp rcx,1 ; If there are less than 2 characters left...
jle ExpandOut ; ...don't try to unescape
movzx rdx,word [rdi] ; Pull the ASCII-encoded hex byte into dx
ror dl,4 ; You are not expected to understand this
ror dx,4
ror dl,4
and dh,0x44
shr dh,2
add dl,dh
shl dh,3
or dl,dh
mov [rdi-1],dl ; Overwrite the '%' character with the newly-unescaped byte
mov word [rdi],r10w ; Write NULL's over the just-unescaped ASCII-encoded hex
mov [r8+r9*8],rdi ; Add the address to our list of unescaped literals
inc r9 ; Increment the counter
jmp short ExpandChar ; ...and do it again
ExpandOut:
or r9,r9 ; If r9 is zero...
jz ExpandDone ; ...then there was nothing to un-escape - jump over copying
mov [r8+r9*8],rdi ; Store the offset of the last byte of the URI
xor rax,rax ; Zero out rax (index register for the loop)
mov rdi,[r8] ; Get the offset of the first un-escaped literal
mov rsi,rdi ; rsi holds the destination
DoCopying:
add rsi,2 ; Jump over unescaped xx
mov rcx,[r8+rax*8+8] ; Load the offset of the next escapee
sub rcx,rsi ; Subtract where we are now to get how far to copy
rep movsb ; Boom
inc rax ; Increment the idex register
cmp rax,r9 ; If we're not done...
jne DoCopying ; ...do it again
mov rcx,r9 ; Use the number of %xx's we unescaped as a counter
xor dx,dx ; Zero dx so we can write NULL words
mov [rdi],dx ; NUL terminate the (now shorter) string
ExpandDone:
tmalloc(OpenAtStruct,24)
mov qword [OpenAtStruct],0
mov qword [OpenAtStruct+8],0
mov qword [OpenAtStruct+16],RESOLVE_IN_ROOT
syscall(sys_openat2,+r15,+r13,[OpenAtStruct],lenOpenAtStruct)
mov r13,rax ; Save file descriptor
ech(HandleGeneralSysErrorAfterOpen) ; Now if we crash we have to close the file to
; avoid leaking file descriptors
tmalloc(StatStruct,144)
syscall(sys_fstat,+r13,[StatStruct])
; Make sure we have a regular file or a symlink
mov r12w,[StatStruct+24] ; Get the file mode
shr r12w,14
cmp r12w,2
jne DieError403
mov r12,[StatStruct+48] ; Get the file size out of the struct from stat()
lea rsp,[rbp+thread_memory_offset] ; Get scratch space, store it in rsp for awhile
; Convert the filesize to ASCII
mov rax,r12 ; The file's length to convert (as an unsigned quadword)
mov ecx,16 ; Counter for the conversion loop
lea rsi,[DecAsciiConvTable] ; Address of table for ASCII conversion
mov r8,0x199999999999999a ; Multiplicative inverse of 10d (0xa)
xor r9,r9 ; Zero out registers for the ASCII string
xor r10,r10
DivisionLoop:
shld r9,r10,8 ; Shift the most significant byte of r10 into r9
shl r10,8 ; (shld only shifts in, not out, so we have to discard the byte)
mul r8 ; Execute the division
shr rax,60 ; Shift the most significant byte of the remainder to the least
movzx rax,byte [rsi+rax]; Load the value from our conversion table
or r10,rax ; Store the new value in r10
mov rax,rdx ; Restore the working quotient for the next iteration
loop DivisionLoop ; ...and do it again
mov [rsp],r10 ; Store the ASCII decimal value in memory
mov [rsp+8],r9
mov eax,0x30 ; Skip over the leading zeros (they break stupid browsers...)
mov ecx,16
mov rdi,rsp
repe scasb
mov r11,[rdi-1] ; Move the significant values back to the beginning of [rsp]
mov [rsp],r11
mov r11,[rdi-1+8]
mov [rsp+8],r11
lea rsp,[rsp+rcx+1] ; Skip past the ASCII number
mov rdi,rsp ; Copy to the scratch area...
mov rsi,Response200 ; ...from the canned HTTP 200 response
mov ecx,lenResponse200 ; Size
rep movsb ; Boom
mov rsi,Header12 ; Copy the "Content-Length: " header
mov ecx,lenHeader12 ; Size
rep movsb ; Boom
lea rsi,[rbp+thread_memory_offset] ; Load rsi with the address of the ASCII-expressed file size
mov rcx,rsp ; Get the address of the start of the send buffer
sub rcx,rsi ; Subtract the offset of the size from the offset of the buffer (strlen(num))
rep movsb ; ...and copy the difference in bytes to the header field
mov dword [rdi], 0x0a0d0a0d ; Terminate the HTTP response
lea rdx,[rdi+4] ; Address from which to send response plus 4...
sub rdx,rsp ; ...and subtract the end to get the length
syscall(sys_sendto,+rbx,[+rsp],+rdx,MSG_MORE,NULL,NULL)
keep_sending:
syscall(sys_sendfile,+rbx,+r13,NULL,+r12)
sub r12,rax
jnz keep_sending
; Close file descriptor and socket
syscall(sys_close,+r13)
syscall(sys_close,+rbx)
; Deallocate thread-local memory and exit
syscall(sys_munmap,+rbp,THREAD_MEM_SIZE)
syscall(sys_exit,NULL)
ud2 ; SIGILL